From 609464471df379eb770488192640bc5aa71e1e8e Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Fri, 30 Oct 2026 11:14:22 +0100 Subject: [PATCH] docs: note that a forged received line defeats the receipt check --- lib/email-headers.ts | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/lib/email-headers.ts b/lib/email-headers.ts index 13b835baa..087d50acf 100644 --- a/lib/email-headers.ts +++ b/lib/email-headers.ts @@ -141,15 +141,19 @@ export function getSenderVerification( const SUBMISSION_PROTOCOL_RE = /\bwith\s+(?:UTF8)?(?:E?SMTP|LMTP)S?A\b/i; /** - * Whether the topmost Authentication-Results header was written by a server - * that received the message, rather than by whoever composed it. Stalwart - * keeps an Authentication-Results header a signed-in user puts into a mail - * they submit, and adds no Received header to mail submitted over JMAP, so a - * forged "dkim=pass" can sit on top. A receiving server writes its Received - * line first and its Authentication-Results below it (Stalwart and Gmail - * both do), so: a Received header must sit above the topmost - * Authentication-Results, and the nearest one must not record an - * authenticated submission. Takes the headers in message order. + * Whether the topmost Authentication-Results header looks like it was + * written by a server that received the message, rather than by whoever + * composed it. A receiving server writes its Received line first and its + * Authentication-Results below it (Stalwart and Gmail both do), so: a + * Received header must sit above the topmost Authentication-Results, and the + * nearest one must not record an authenticated submission. Takes the headers + * in message order. + * + * This only catches a forged header on its own. Stalwart (0.16 and 1.0) + * keeps both headers when a signed-in user writes them into a mail they + * submit over JMAP, and adds nothing above them, so a forged Received line + * followed by a forged "dkim=pass" arrives exactly like mail from outside. + * Only the server can tell them apart. */ export function authenticationResultsOnReceipt(headers: readonly { name: string; value: string }[]): boolean { const index = headers.findIndex((header) => header.name.toLowerCase() === 'authentication-results');