From ffdc51ef70124d3915e8da78e217bf6b3039e473 Mon Sep 17 00:00:00 2001 From: Stefan Hildebrandt <695494+hildebrandttk@users.noreply.github.com> Date: Thu, 13 Aug 2026 23:15:42 +0200 Subject: [PATCH] fix(reply): honour an external Reply-To even on a self-sent message The self-sent handling from #703 (reply to your own thread message -> address the original recipients, ignore Reply-To) was entered whenever From matched one of the user's identities. Contact-form / helpdesk mail commonly arrives From the user's own address with the real correspondent in Reply-To, so that branch ignored the Reply-To and looped the reply back to the user's own To. Only take the self-sent branch when there is no EXTERNAL Reply-To: a Reply-To resolving to a non-own address is an explicit RFC 5322 directive and now wins even for self-sent messages (#776). A Reply-To that is only our own address stays ignored for self-sent (#703 preserved). Closes #776. --- lib/__tests__/reply-recipients.test.ts | 23 +++++++++++++++++++++++ lib/reply-recipients.ts | 9 ++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/lib/__tests__/reply-recipients.test.ts b/lib/__tests__/reply-recipients.test.ts index 2a50ed493..66d2e2e72 100644 --- a/lib/__tests__/reply-recipients.test.ts +++ b/lib/__tests__/reply-recipients.test.ts @@ -81,6 +81,29 @@ describe('buildReplyRecipients', () => { expect(emails(to)).toEqual(['bob@other.com']); }); + it('honours an EXTERNAL Reply-To even on a self-sent message (#776)', () => { + // Contact-form / helpdesk pattern: the mail arrives From our own address + // (so it looks self-sent) but Reply-To carries the real correspondent. + // The reply must reach them, not loop back to our own To. + const { to } = buildReplyRecipients( + { ...sent, to: [{ email: 'me@example.com' }], replyToAddresses: [{ email: 'customer@external.com' }] }, + 'reply', + OWN, + ); + expect(emails(to)).toEqual(['customer@external.com']); + }); + + it('reply-all with an external Reply-To routes To to the Reply-To (#776)', () => { + const { to } = buildReplyRecipients( + { ...sent, replyToAddresses: [{ email: 'customer@external.com' }] }, + 'replyAll', + OWN, + ); + // Reply-To leads; the other original recipients (bob) are still included, + // our own address dropped. + expect(emails(to)).toEqual(['customer@external.com', 'bob@other.com']); + }); + it('keeps a self-addressed recipient we chose ourselves', () => { const { to } = buildReplyRecipients( { ...sent, to: [{ email: 'info@example.com' }] }, diff --git a/lib/reply-recipients.ts b/lib/reply-recipients.ts index 46d3d5603..f98a50c2f 100644 --- a/lib/reply-recipients.ts +++ b/lib/reply-recipients.ts @@ -80,7 +80,10 @@ export function buildReplyRecipients( const withEmail = (list: ReplyAddress[] | undefined) => (list ?? []).filter((r) => Boolean(r.email)); - if (isSelfSent(source, ownEmails)) { + const replyToList = withEmail(source.replyToAddresses); + const externalReplyTo = replyToList.filter((r) => !isOwnAddress(r.email, ownEmails)); + + if (!externalReplyTo.length && isSelfSent(source, ownEmails)) { const originalTo = withEmail(source.to); if (originalTo.length > 0) { return { @@ -90,8 +93,8 @@ export function buildReplyRecipients( } } - const replyTarget = withEmail(source.replyToAddresses).length - ? withEmail(source.replyToAddresses) + const replyTarget = replyToList.length + ? replyToList : (source.from?.[0]?.email ? [source.from[0]] : []); if (mode === 'reply') {