fix: send from a shared mailbox through its own identity for the from address #1450

This commit is contained in:
Linus Rath
2026-10-31 01:09:49 +01:00
parent 9af0304616
commit d2f9f1a1a4
2 changed files with 54 additions and 3 deletions
+38 -2
View File
@@ -48,7 +48,7 @@ interface Recorded {
identityId?: string;
}
function mockServer(): Recorded {
function mockServer(identities = IDENTITIES): Recorded {
const rec: Recorded = {};
vi.spyOn(globalThis, 'fetch').mockImplementation(async (_url, init) => {
const { methodCalls } = JSON.parse((init as { body: string }).body) as { methodCalls: MethodCall[] };
@@ -60,7 +60,7 @@ function mockServer(): Recorded {
{ id: `${accountId}-sent`, name: 'Sent', role: 'sent' },
] }, callId];
}
if (name === 'Identity/get') return [name, { list: IDENTITIES[accountId] ?? [] }, callId];
if (name === 'Identity/get') return [name, { list: identities[accountId] ?? [] }, callId];
if (name === 'Email/set') {
if (args.create) {
rec.emailSetAccount = accountId;
@@ -154,6 +154,42 @@ describe('JMAPClient.sendEmail picks the account that owns the From identity (#1
expectAccount(rec, 'grp');
});
// #1450: the composer's identity ids are the member's own. In the group
// account the same id can be another of the group's addresses, and the
// server takes MAIL FROM from the identity the submission names.
describe('identity ids of the member’s account (#1450)', () => {
const COLLIDING: Record<string, Array<{ id: string; email: string }>> = {
me: [
{ id: 'by', email: 'info@radar.example' },
{ id: 'bw', email: '*@lists.example' },
{ id: 'c', email: 'carol@example.org' },
],
grp: [
{ id: 'by', email: 'info@alias.example' },
{ id: 'bw', email: 'info@alias.example' },
{ id: 'b7', email: 'info@radar.example' },
{ id: 'w', email: '*@lists.example' },
],
};
it.each([
['the From address', 'info@radar.example', 'by', 'b7'],
['a sub-addressed From', 'info+news@radar.example', 'by', 'b7'],
['a From covered by a wildcard identity', 'announce@lists.example', 'bw', 'w'],
])('submits through the group’s identity for %s', async (_case, fromEmail, memberId, groupId) => {
const rec = mockServer(COLLIDING);
await send(createClient({ me: 'carol@example.org', grp: 'team' }), fromEmail, memberId);
expectAccount(rec, 'grp');
expect(rec.identityId).toBe(groupId);
});
it('keeps the id when it names the From address in the group account too', async () => {
const rec = mockServer();
await send(createClient({ me: 'carol@example.org', grp: 'team' }), 'team@example.org', 'b');
expect(rec.identityId).toBe('b');
});
});
describe('drafts', () => {
const draft = (client: JMAPClient, fromEmail: string, draftId?: string, options?: { accountId?: string; previousDraftAccountId?: string }) =>
client.createDraft(['rcpt@example.net'], 'Subject', 'body', undefined, undefined, 'b', fromEmail, draftId,
+16 -1
View File
@@ -4865,13 +4865,28 @@ export class JMAPClient implements IJMAPClient {
? identities.find((id) => id.email.toLowerCase() === requestedMailFrom.toLowerCase())
: undefined;
if (!matchingIdentity && identityId) {
matchingIdentity = identities.find((id) => id.id === identityId);
// Identity ids are per account, and the composer's come from the
// user's own account. In a shared account the same id can be
// another of its addresses, and the server derives MAIL FROM from
// that identity (#1450): there the id only counts when it names
// the From address.
const byId = identities.find((id) => id.id === identityId);
if (byId && (targetAccountId === this.accountId || !fromEmail || emailMatchesLogin(byId.email, fromEmail))) {
matchingIdentity = byId;
}
}
if (!matchingIdentity) {
// Addresses compared as the server does: regardless of case.
const target = fromEmail || this.username;
matchingIdentity = identities.find((id) => emailMatchesLogin(id.email, target));
}
if (!matchingIdentity && fromEmail) {
// A sub-addressed From or one covered by a wildcard identity, the
// other ways resolveSendAccountId picks an account.
const from = parseRecipientString(fromEmail).email.toLowerCase();
matchingIdentity = identities.find((id) => identityEmailIsSubAddressOf(id.email, from))
?? identities.find((id) => identityEmailIsWildcardFor(id.email, from));
}
finalIdentityId = matchingIdentity?.id || identities[0].id;
identityReplyTo = matchingIdentity?.replyTo || identities[0].replyTo;
identityEmail = (matchingIdentity || identities[0]).email;