From 717b1d83972ed76029938373160e043281c29420 Mon Sep 17 00:00:00 2001 From: Harry Youd Date: Thu, 25 Jun 2026 23:24:10 +0100 Subject: [PATCH] feat(headers): add parsing for Stalwart spam headers Example of headers created by Stalwart below X-Spam-Result: TRUSTED_DOMAIN (-7.00), PROB_HAM_LOW (-2.00), RBL_SENDERSCORE_REPUT_9 (-1.00), DMARC_POLICY_ALLOW (-0.50), RCVD_DKIM_ARC_DNSWL_MED (-0.50) X-Spam-Score: ham, score=-5.60, avg_confidence=0.26 X-Spam-Status: No Only need to add small tweak by detecting spam|ham as well as Yes|No The most useful header is X-Spam-Score, so we make sure to parse that before X-Spam-Status --- lib/email-headers.ts | 3 ++- lib/jmap/client.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/email-headers.ts b/lib/email-headers.ts index 4d0cd390..53a79345 100644 --- a/lib/email-headers.ts +++ b/lib/email-headers.ts @@ -118,7 +118,8 @@ export function parseAuthenticationResults(header: string): AuthenticationResult */ export function parseSpamScore(header: string): { score: number; status: string } | null { // Try X-Spam-Status format: "No, score=-0.25" - const statusMatch = header.match(/^(Yes|No),?\s+score=([-\d.]+)/i); + // And try X-Spam-Score format (Stalwart): "ham, score=-0.25" + const statusMatch = header.match(/^(Yes|No|spam|ham),?\s+score=([-\d.]+)/i); if (statusMatch) { return { status: statusMatch[1].toLowerCase(), diff --git a/lib/jmap/client.ts b/lib/jmap/client.ts index dc5ea898..184c5883 100644 --- a/lib/jmap/client.ts +++ b/lib/jmap/client.ts @@ -1243,10 +1243,10 @@ export class JMAPClient implements IJMAPClient { email.authenticationResults = parseAuthenticationResults(value); } - for (const headerName of ['X-Spam-Status', 'X-Spam-Result', 'X-Rspamd-Score']) { + for (const headerName of ['X-Spam-Score', 'X-Spam-Status', 'X-Spam-Result', 'X-Rspamd-Score']) { if (!headersRecord[headerName]) continue; const value = Array.isArray(headersRecord[headerName]) ? headersRecord[headerName][0] : headersRecord[headerName]; - const spamResult = parseSpamScore(value as string); + const spamResult = parseSpamScore((value as string).trim()); if (spamResult) { email.spamScore = spamResult.score; email.spamStatus = spamResult.status;