fix: never count a helo spf pass as verifying the sender

This commit is contained in:
Linus Rath
2026-10-29 04:41:02 +01:00
parent 67cdb4e792
commit 8adef1e13f
2 changed files with 11 additions and 6 deletions
+5 -1
View File
@@ -11,7 +11,7 @@ describe('parseAuthenticationResults', () => {
const r = parseAuthenticationResults(
'mx.example; spf=pass smtp.mailfrom=news.example; dkim=pass header.d=news.example header.s=s1; dmarc=pass header.from=news.example policy.dmarc=none; iprev=pass policy.iprev=1.2.3.4',
);
expect(r.spf).toEqual({ result: 'pass', domain: 'news.example' });
expect(r.spf).toEqual({ result: 'pass', identity: 'mailfrom', domain: 'news.example' });
expect(r.dkim).toEqual({ result: 'pass', domain: 'news.example', selector: 's1' });
expect(r.dmarc).toEqual({ result: 'pass', domain: 'news.example', policy: 'none' });
expect(r.iprev).toEqual({ result: 'pass', ip: '1.2.3.4' });
@@ -88,6 +88,10 @@ describe('getSenderVerification', () => {
it('does not count a HELO pass', () => {
const auth = parseAuthenticationResults('mx; spf=pass smtp.helo=bank.example; spf=none smtp.mailfrom=bank.example');
expect(getSenderVerification(auth, 'a@bank.example')?.status).toBe('unverified');
// Nor a HELO pass that is the only SPF result.
const only = parseAuthenticationResults('mx; spf=pass smtp.helo=bank.example; dkim=none; dmarc=none');
expect(hasAlignedAuthenticationPass(only, 'a@bank.example')).toBe(false);
expect(getSenderVerification(only, 'a@bank.example')?.status).toBe('unverified');
});
});
+6 -5
View File
@@ -45,7 +45,7 @@ export interface DkimEntry {
}
export interface AuthenticationResults {
spf?: { result: SpfResult; domain?: string; all?: SpfEntry[] };
spf?: { result: SpfResult; identity?: SpfEntry['identity']; domain?: string; all?: SpfEntry[] };
/** The first signature's result; `all` lists every one when there are several. */
dkim?: DkimEntry & { all?: DkimEntry[] };
dmarc?: { result: DmarcResult; domain?: string; policy?: DmarcPolicy };
@@ -140,10 +140,10 @@ function alignedWith(fromDomain: string, candidate: string | undefined): boolean
* the visible sender. A HELO pass never does.
*/
function hasAlignedSpfPass(auth: AuthenticationResults, fromDomain: string): boolean {
if (auth.spf?.all) {
return auth.spf.all.some((entry) => entry.identity !== 'helo' && entry.result === 'pass' && alignedWith(fromDomain, entry.domain));
}
return auth.spf?.result === 'pass' && alignedWith(fromDomain, auth.spf.domain);
// With one SPF result there is no `all` list, and its identity is checked
// too: `spf=pass smtp.helo=<From domain>` alone used to count.
const entries = auth.spf?.all ?? (auth.spf ? [auth.spf] : []);
return entries.some((entry) => entry.identity === 'mailfrom' && entry.result === 'pass' && alignedWith(fromDomain, entry.domain));
}
/** A DKIM pass for a signing domain that aligns with the From domain. */
@@ -381,6 +381,7 @@ export function parseAuthenticationResults(headers: string | readonly string[]):
}
results.spf = {
result: primary.result,
...(primary.identity ? { identity: primary.identity } : {}),
domain: primary.domain,
...(spfResults.length > 1 ? { all: spfResults } : {}),
};