diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx index a65bc5986..8353eb3d3 100644 --- a/components/email/email-composer.tsx +++ b/components/email/email-composer.tsx @@ -538,17 +538,25 @@ export function EmailComposer({ identities.map(i => i.email), [composerAccount?.email, composerAccount?.username, replyAccount?.email, replyAccount?.username], ); - const ownIdentityEmailsKey = ownIdentityEmails.map((e) => e.toLowerCase()).join(','); + // "Self-sent" means sent by the account holding the original. A message + // another connected account sent to this one was received here, and its + // reply must go back to that sender, not to this account's own address. + const sourceAccount = replyAccount ?? composerAccount; + const sourceAccountEmails = ownReplyAddresses( + sourceAccountIdentities.map(i => i.email), + [sourceAccount?.email, sourceAccount?.username], + ); + const ownIdentityEmailsKey = [...ownIdentityEmails, '|', ...sourceAccountEmails].map((e) => e.toLowerCase()).join(','); // Initialize with reply/forward data if provided const getInitialTo = (): Recipient[] => { if (mode !== 'reply' && mode !== 'replyAll') return []; - return buildReplyRecipients(replyTo, mode, ownIdentityEmails).to.map(toRecipient); + return buildReplyRecipients(replyTo, mode, ownIdentityEmails, sourceAccountEmails).to.map(toRecipient); }; const getInitialCc = (): Recipient[] => { if (mode !== 'replyAll') return []; - return buildReplyRecipients(replyTo, mode, ownIdentityEmails).cc.map(toRecipient); + return buildReplyRecipients(replyTo, mode, ownIdentityEmails, sourceAccountEmails).cc.map(toRecipient); }; const getInitialSubject = () => { @@ -1194,10 +1202,10 @@ export function EmailComposer({ // identity that sent it. Resolving from the recipients here would pick the // *other* party's address - and on a catch-all domain it would even set a // From override to their address. An identity on the message's own account - // wins when two accounts share the address. - if (isSelfSent({ from: replyTo?.from }, identities.map(i => i.email).filter(Boolean))) { - const senderIdentityId = findDraftIdentityId(sourceAccountIdentities, replyTo?.from?.[0]) - ?? findDraftIdentityId(identities, replyTo?.from?.[0]); + // wins when two accounts share the address. Only the message's own account + // counts: mail another connected account sent here is a received message. + if (isSelfSent({ from: replyTo?.from }, sourceAccountIdentities.map(i => i.email).filter(Boolean))) { + const senderIdentityId = findDraftIdentityId(sourceAccountIdentities, replyTo?.from?.[0]); if (senderIdentityId) { setSelectedIdentityId(senderIdentityId); return; diff --git a/lib/__tests__/reply-recipients.test.ts b/lib/__tests__/reply-recipients.test.ts index c3966e514..14ef07ec8 100644 --- a/lib/__tests__/reply-recipients.test.ts +++ b/lib/__tests__/reply-recipients.test.ts @@ -124,6 +124,40 @@ describe('buildReplyRecipients', () => { }); }); + describe('message from another connected account', () => { + // Two accounts connected: book@ sent this to mail@, and we reply from mail@. + const ALL = ['mail@hotel.example', 'book@hotel.example']; + const THIS_ACCOUNT = ['mail@hotel.example']; + const fromOtherAccount = { + from: [{ email: 'book@hotel.example', name: 'Booking' }], + to: [{ email: 'mail@hotel.example' }, { email: 'guest@other.com' }], + cc: [{ email: 'book@hotel.example' }], + }; + + it('replies to the other account, not back to ourselves', () => { + const { to, cc } = buildReplyRecipients(fromOtherAccount, 'reply', ALL, THIS_ACCOUNT); + expect(emails(to)).toEqual(['book@hotel.example']); + expect(cc).toEqual([]); + }); + + it('reply-all still drops every connected account from the other recipients', () => { + const { to, cc } = buildReplyRecipients(fromOtherAccount, 'replyAll', ALL, THIS_ACCOUNT); + expect(emails(to)).toEqual(['book@hotel.example', 'guest@other.com']); + expect(cc).toEqual([]); + }); + + it('still continues the thread for a message this account sent', () => { + const ownSent = { ...fromOtherAccount, from: [{ email: 'mail@hotel.example' }], to: [{ email: 'guest@other.com' }] }; + const { to } = buildReplyRecipients(ownSent, 'reply', ALL, THIS_ACCOUNT); + expect(emails(to)).toEqual(['guest@other.com']); + }); + + it('without selfSentEmails every connected account counts as self (single-account behaviour)', () => { + const { to } = buildReplyRecipients(fromOtherAccount, 'reply', ALL); + expect(emails(to)).toEqual(['mail@hotel.example', 'guest@other.com']); + }); + }); + it('returns nothing without a source message', () => { expect(buildReplyRecipients(undefined, 'replyAll', OWN)).toEqual({ to: [], cc: [] }); }); diff --git a/lib/reply-recipients.ts b/lib/reply-recipients.ts index fdf260825..380980e00 100644 --- a/lib/reply-recipients.ts +++ b/lib/reply-recipients.ts @@ -95,11 +95,19 @@ export function isSelfSent(source: ReplySource | undefined, ownEmails: string[]) * * A self-sent message with no visible recipients (Bcc-only) has nothing to * continue to, so it falls back to the normal behaviour. + * + * `selfSentEmails` decides what counts as "self-sent" and defaults to + * `ownEmails`. With several connected accounts, `ownEmails` holds every + * account's addresses (so reply-all still drops all of them), but a message + * one of those accounts sent to another is a received message for the + * account it landed in: pass only that account's addresses here, or the + * reply goes back to the original To - the account itself. */ export function buildReplyRecipients( source: ReplySource | undefined, mode: 'reply' | 'replyAll', ownEmails: string[], + selfSentEmails: string[] = ownEmails, ): ReplyRecipientsResult { if (!source) return { to: [], cc: [] }; @@ -108,7 +116,7 @@ export function buildReplyRecipients( const replyToList = withEmail(source.replyToAddresses); const externalReplyTo = replyToList.filter((r) => !isOwnAddress(r.email, ownEmails)); - if (!externalReplyTo.length && isSelfSent(source, ownEmails)) { + if (!externalReplyTo.length && isSelfSent(source, selfSentEmails)) { const originalTo = withEmail(source.to); if (originalTo.length > 0) { return {