diff --git a/lib/__tests__/email-headers.test.ts b/lib/__tests__/email-headers.test.ts index c126a3a61..3e22765c6 100644 --- a/lib/__tests__/email-headers.test.ts +++ b/lib/__tests__/email-headers.test.ts @@ -434,6 +434,14 @@ describe('getSenderVerification', () => { expect(getSenderVerification(parseAuthenticationResults('mx; dmarc=pass header.from=shop.example'), from)).toBeNull(); }); + it('takes the signing domain from header.i when the receiver writes no header.d (#1420)', () => { + const from = 'noreply@example.org'; + expect(getSenderVerification(parseAuthenticationResults('mx.example.net; dkim=pass header.i=@example.org'), from)).toBeNull(); + expect(getSenderVerification(parseAuthenticationResults('mx; dkim=pass header.i=bounce@mail.example.org'), from)).toBeNull(); + expect(getSenderVerification(parseAuthenticationResults('mx; dkim=pass header.i=@other.example'), from)) + .toEqual({ status: 'unverified', domain: 'example.org' }); + }); + it('does not take a pass for an unrelated domain as verifying the From domain', () => { // The sender passes SPF for their own envelope domain (or signs with // their own key) and writes someone else's From; the From domain diff --git a/lib/email-headers.ts b/lib/email-headers.ts index 18287ef3e..79a021a68 100644 --- a/lib/email-headers.ts +++ b/lib/email-headers.ts @@ -228,6 +228,11 @@ const DMARC_SEVERITY: Record = { * DKIM, DMARC and iprev come from the topmost header only, and the others * can only escalate SPF to a failure, never supply a pass. */ +function identityDomain(identity: string | undefined): string | undefined { + const at = identity?.lastIndexOf('@') ?? -1; + return identity && at >= 0 && at < identity.length - 1 ? identity.slice(at + 1) : undefined; +} + export function parseAuthenticationResults(headers: string | readonly string[]): AuthenticationResults { const results: AuthenticationResults = {}; const list = typeof headers === 'string' ? [headers] : headers; @@ -285,7 +290,9 @@ export function parseAuthenticationResults(headers: string | readonly string[]): .filter((info) => info.method === 'dkim') .map((info) => ({ result: info.result as DkimResult, - domain: info.props['header.d'], + // Some receivers name the signature only by its identity (`header.i`, + // `@domain` or `user@domain`, RFC 8601 2.7.1); its domain is the signer's. + domain: info.props['header.d'] ?? identityDomain(info.props['header.i']), selector: info.props['header.s'], })); if (dkimResults.length > 0) {