All posts
    Deliverability

    How to Test SPF, DKIM, and DMARC in Your Email Pipeline

    The Devmailr Team6 min read

    Your transactional email can pass every assertion in your test suite and still land in spam. Tests usually check that a message was sent and what it says — not whether it’s authenticated. Here’s how to check SPF, DKIM, and DMARC by actually sending a message and reading the result, right in CI.

    A 60-second refresher

    Three DNS-based mechanisms decide whether a receiving server trusts your mail:

    • SPF — is this sending IP allowed to send for the envelope domain?
    • DKIM — is the message cryptographically signed by the domain, with the signature intact?
    • DMARC — given the SPF/DKIM results and their alignment with the From: domain, does its policy say to accept the message?

    Get one wrong and mailbox providers quietly route you to spam — or reject outright. The catch: these only fail in the real send path, which is exactly what unit tests don’t exercise.

    Test them by actually sending

    A static DNS checker tells you your records parse. It doesn’t tell you that the message your app actually sent — from the IP your provider actually used, with the signature your library actually attached — passes. To know that, send a real message and inspect how a receiving server graded it.

    Send to a disposable Devmailr inbox and read the verdicts the receiving mail server computed:

    javascript
    const inbox = await createInbox(`deliv-${Date.now()}`)
    
    // send a real message from your app / ESP to inbox.address
    await sendTransactionalEmail({ to: inbox.address, template: 'welcome' })
    
    const email = await waitForEmail(inbox._id)
    console.log(email.verdicts)
    // { spf: 'PASS', dkim: 'PASS', dmarc: 'PASS', spam: 'PASS', virus: 'PASS' }

    Reading the verdicts

    Every inbound email carries a verdicts object. Each value is one of:

    • PASS — the check passed.
    • FAIL — it failed (this is what sends you to spam).
    • GRAY — inconclusive or not applicable.
    • PROCESSING_FAILED — the check couldn’t be evaluated.

    The keys are spf, dkim, dmarc (authentication) plus spam and virus (content scan). A key is present only if the receiving server reported it.

    Assert it in CI

    Now make a broken DKIM signature fail the build, not your users’ inboxes:

    javascript
    const { verdicts } = await waitForEmail(inbox._id)
    
    expect(verdicts.spf).toBe('PASS')
    expect(verdicts.dkim).toBe('PASS')
    expect(verdicts.dmarc).toBe('PASS')

    Run that after you rotate a DKIM key, switch email provider, or move sending infrastructure — the three moments email authentication usually breaks silently.

    Spam and virus, too

    The same object surfaces the content scan. A spam verdict of FAIL on a message you control is an early warning that your template or sending reputation is tripping filters:

    javascript
    if (verdicts.spam !== 'PASS') {
      console.warn('Receiving server flagged this message as spam')
    }

    Why this beats a DNS checker

    • It tests the real path — your actual sending IP and signature, end to end.
    • It catches regressions a record-parser can’t: an unsigned message, a misaligned From:, an IP that fell off your SPF include.
    • It runs on every deploy, so a deliverability regression fails fast instead of quietly tanking your open rates.

    Good to know: Verdicts are computed by the receiving server at receive time and are available on every plan — on GET /emails/:id, the list endpoint, /wait, and the email.received webhook.

    Related reading

    Try it in two minutes

    Create a disposable prefix@devmailr.app inbox over the API, send mail to it from your app, and read it in your tests. Free plan, no card required.