GUIDE

Your OTP email works in staging
and lands in spam in prod

Authentication breakage — SPF, DKIM, DMARC — is invisible to a normal E2E suite. The email still arrives in your test inbox, your assertion still passes, and meanwhile Gmail has started quietly junking the same message for real users. Here's why the usual checks miss it, and how to assert authentication in CI on every message your suite already receives.

The failure mode: green tests, junked mail

When SPF or DKIM breaks, receiving providers almost never bounce your mail — they deliver it to spam, or accept it and throttle your domain's reputation. Nothing in your application errors. Nothing in your test suite errors either: the message reaches the test inbox, the OTP extracts, the run is green.

And these things break silently, from changes that don't look email-related: a DNS cleanup that drops one include:, a new egress IP after an infra migration, an ESP switch that changes the envelope sender so DMARC alignment fails, a DKIM key rotated in the ESP dashboard but never re-published in DNS. Since Gmail's and Yahoo's sender requirements took effect in 2024, failing authentication isn't a deliverability nuance anymore — SPF or DKIM is mandatory for everyone, and DMARC on top for bulk senders. This is now a class of bug that ships to production with a green pipeline.

The checks that don't catch it

The fix: verify like a receiver, assert like a test

The place to catch this is the mail your suite already receives. Every message that lands in a MailFixture inbox is verified on arrival the same way a real inbox provider does it: SPF against the actual connecting IP and envelope sender, every DKIM signature against the key actually published in DNS, DMARC alignment against the actual From domain. The verdicts ride on the message as extracted.auth, typed in both SDKs — so the deliverability check is one more assertion in a test you already have:

signup.spec.ts
const message = await inbox.waitForMessage({ timeout: 30_000 });

// the email arrived — now assert it authenticates
expect(message.auth?.spf.result).toBe("pass");
expect(message.auth?.dkim.some((s) => s.result === "pass")).toBe(true);
expect(message.auth?.dmarc.result).toBe("pass");

Reading the verdicts: results use the standard RFC 8601 strings (pass, fail, softfail, neutral, none, temperror, permerror). dkim carries one entry per signature — an empty array means the mail was simply unsigned, and a signature that doesn't verify reports neutral with the reason in error (a body-hash mismatch reads very differently from a missing DNS key). dmarc.result is pass when either aligned mechanism passed, and dmarc.policy tells you the blast radius of a failure: none is monitoring, reject means failing mail gets refused outright.

Verification never affects delivery — a hard-failing message still lands in your inbox wearing its failing verdict, because seeing the failure is the point. The same verdicts show as chips in the dashboard message viewer, next to the extracted OTP and links.

Test the path that actually sends

HONESTY Mail your CI runner delivers to us directly — a local SMTP push from a fixture script — will read none across the board. There's no DNS trail to verify: no published SPF for the runner's IP, no DKIM signature, no DMARC record. That's the honest verdict, not a bug.

The assertion earns its keep when the message travels your real sending path: your staging app sending through your actual ESP (SES, SendGrid, Postmark, …) from your actual domain. That's the configuration that breaks silently, and the one your users receive from. Wire the auth assertions into the E2E suite that exercises staging end-to-end, and a dropped include: or a rotated selector fails a pipeline instead of a customer.

Also worth stating plainly: this checks that your mail authenticates — it is not an inbox-placement or spam-score predictor. Content reputation, IP warm-up, and engagement signals are a different problem. But authentication is the part that's binary, entirely under your control, and required by the major providers — exactly the kind of invariant a test suite should pin.

every received message is verified — assert on the verdict
Start free Read the extraction docs