Your welcome email passes every test
and scores 6.4 in a spam filter
Your E2E suite proves the email arrived. It says nothing about whether the email looks like spam. A well-authenticated message from a warm domain can still trip a content filter — an image-only HTML body, a missing plain-text part, a shouty subject line — and land in Promotions or Junk while every test stays green. Here's how to put a spam score in the same assertion.
Authentication is necessary, not sufficient
If you've already wired up SPF/DKIM/DMARC assertions, good — that catches the binary "is this mail forged?" question. But authentication passing is table stakes, not a clean bill of health. Real inbox providers also score the content: they weigh header hygiene, HTML-to-text ratio, link shape, trigger phrases, and dozens of other signals into a spamminess number, and that number is what decides the Promotions tab versus the primary inbox.
The classic ways transactional mail earns points, all while authenticating perfectly:
- An HTML-only email with no plain-text part. A designer ships a new template through the ESP's visual editor; the multipart/alternative loses its text leg. Filters read that as a red flag.
- An image-only body. The "verify" button is a single background image, the fallback text is three words. To a content filter that's the shape of a spam campaign.
- Missing or malformed headers. A
Date:header dropped by a misconfigured relay, aMessage-ID:that isn't a valid address — each is a small, cumulative penalty. - Copy that drifts. Marketing edits the password-reset subject to "⚡ ACT NOW: Secure Your Account!!!" and nobody connects the A/B test to the deliverability dip two weeks later.
The check that doesn't catch it
The usual answer is a one-time paste into a web spam-checker when the template was first built. That's a point-in-time manual check: it scored a clean 1.2 in March, and every regression since — the dropped text part, the new image-only hero, the punched-up subject — is invisible until someone thinks to re-run it, or until the open rate quietly falls off. It's the same "prod finds out first" loop your test suite exists to break, just for deliverability instead of correctness.
The fix: score the mail your tests already receive
Every message that lands in a MailFixture inbox is run through SpamAssassin — the same engine behind a large share of real-world spam filtering — at receive time. The result rides on the message as extracted.spam, typed in both SDKs, so a deliverability guardrail is one more assertion in a test you already have:
const message = await inbox.waitForMessage({ timeout: 30_000 }); // the email arrived — now assert it doesn't look like spam const { verdict, score, threshold } = message.spam!; expect(verdict).toBe("ham"); expect(score).toBeLessThan(threshold); // or gate on your own budget, well under the 5.0 spam line expect(score).toBeLessThan(3);
When a run fails, the rules array tells you exactly why — every rule that fired, its point contribution, and a description:
[
{ "name": "MIME_HTML_ONLY", "score": 1.1, "description": "Message only has text/html MIME parts" },
{ "name": "HTML_IMAGE_ONLY_16", "score": 2.3, "description": "HTML: images with 1600-2400 bytes of words" },
{ "name": "MISSING_DATE", "score": 1.4, "description": "Missing Date: header" }
]
That's a bug report, not a mystery: add a plain-text part, give the image some real copy, fix the relay dropping the Date: header. The same score and breakdown show in the dashboard message viewer, next to the extracted OTP, links, and auth verdicts.
Assert on the verdict, not the number
verdict === "ham" or score < threshold — not score === 2.3. SpamAssassin's rulesets update over time, so an exact-score assertion is brittle by construction, while "stays under the line" is the property you actually care about.Two things worth stating plainly. First, we score content rules only — no network or RBL lookups. Those score the delivering hop, which in a test run is your CI runner or your ESP's sandbox, so they'd add noise and run-to-run flap for no signal. Sender-reputation questions belong to the SPF/DKIM/DMARC verdicts; content is what a spam score is good at, and it's deterministic, so you can assert on it.
Second: a spam score is not an inbox-placement guarantee. Gmail and Outlook run their own proprietary filters on top of engagement and reputation signals no offline tool can see. What SpamAssassin gives you is the widely-used, well-understood content baseline — the part that's under your control and regresses silently. Pin it in CI and a dropped text part fails a pipeline instead of your open rate.
Test the path that actually sends
Like the auth checks, the assertion earns its keep when the message travels your real sending path — your staging app rendering the real template through your real ESP — not a fixture your test script pushes straight to us. That's the configuration where a template change quietly adds three points, and the one your users receive from. Wire the spam assertion into the E2E suite that already exercises signup, password reset, and receipts end-to-end, and "did marketing just make our reset email spammy?" becomes a failing check instead of a support trend.