Email Deliverability Testing: SPF, DKIM, DMARC, and Spam Score Validation

Email Deliverability Testing: SPF, DKIM, DMARC, and Spam Score Validation

Writing the email and ensuring it sends is only half the job. The other half is ensuring it actually reaches the inbox. Email deliverability testing validates that your DNS configuration, email headers, and content won't cause your emails to be rejected or filtered as spam.

What Affects Deliverability

  • SPF — Sender Policy Framework: authorizes which servers can send from your domain
  • DKIM — DomainKeys Identified Mail: cryptographically signs emails to verify authenticity
  • DMARC — Domain-based Message Authentication: policy for SPF/DKIM failures
  • Spam score — content and header analysis by filters like SpamAssassin
  • IP reputation — whether your sending IP is on blocklists
  • Bounce rate — high bounce rates hurt sender reputation

SPF Validation

SPF is a DNS TXT record that lists authorized sending IPs for your domain.

yourdomain.com. TXT "v=spf1 include:mailgun.org include:sendgrid.net ~all"

Test SPF programmatically:

# Check SPF record
dig TXT yourdomain.com <span class="hljs-pipe">| grep spf

<span class="hljs-comment"># Expected output
yourdomain.com. 300 IN TXT <span class="hljs-string">"v=spf1 include:sendgrid.net ~all"

Node.js SPF check:

import dns from 'dns/promises'

async function validateSPF(domain) {
  const records = await dns.resolveTxt(domain)
  const spf = records.flat().find(r => r.startsWith('v=spf1'))
  
  if (!spf) throw new Error(`No SPF record found for ${domain}`)
  
  // Validate structure
  if (!spf.includes('~all') && !spf.includes('-all')) {
    throw new Error('SPF record missing fail policy (~all or -all)')
  }
  
  return spf
}

// In tests
test('SPF record exists and is valid', async () => {
  const spf = await validateSPF('yourdomain.com')
  expect(spf).toMatch(/v=spf1/)
  expect(spf).toMatch(/include:sendgrid\.net|include:mailgun\.org/)
})

DKIM Validation

DKIM uses a public key in DNS to verify the email signature.

# Check DKIM for selector "mail"
dig TXT mail._domainkey.yourdomain.com

<span class="hljs-comment"># Expected
mail._domainkey.yourdomain.com. TXT <span class="hljs-string">"v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOC..."
async function validateDKIM(domain, selector = 'mail') {
  const dkimDomain = `${selector}._domainkey.${domain}`
  const records = await dns.resolveTxt(dkimDomain)
  const dkim = records.flat().join('')
  
  if (!dkim.includes('v=DKIM1')) {
    throw new Error(`No DKIM record at ${dkimDomain}`)
  }
  if (!dkim.includes('p=')) {
    throw new Error('DKIM record missing public key (p=)')
  }
  
  return dkim
}

DMARC Validation

DMARC builds on SPF and DKIM, specifying what to do when they fail.

dig TXT _dmarc.yourdomain.com

# Expected
_dmarc.yourdomain.com. TXT <span class="hljs-string">"v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com"
async function validateDMARC(domain) {
  const records = await dns.resolveTxt(`_dmarc.${domain}`)
  const dmarc = records.flat().find(r => r.startsWith('v=DMARC1'))
  
  if (!dmarc) throw new Error(`No DMARC record for ${domain}`)
  
  // Policy should not be 'none' in production
  if (dmarc.includes('p=none')) {
    console.warn('DMARC policy is "none" — no enforcement')
  }
  
  return dmarc
}

Spam Score Testing with SpamAssassin

SpamAssassin scores emails from 0 (clean) to 10+ (spam). Most email providers use a threshold of 5.

Docker SpamAssassin

docker run -d -p 783:783 instantlinux/spamassassin

Test Email Spam Score

# Pipe an .eml file through SpamAssassin
<span class="hljs-built_in">cat test-email.eml <span class="hljs-pipe">| docker <span class="hljs-built_in">exec -i spamassassin spamassassin -t 2>&1 <span class="hljs-pipe">| grep <span class="hljs-string">"X-Spam-Score"

Node.js integration:

import { execSync } from 'child_process'
import fs from 'fs'
import nodemailer from 'nodemailer'

async function getSpamScore(emailContent) {
  // Write email to temp file
  const tmpFile = `/tmp/test-email-${Date.now()}.eml`
  fs.writeFileSync(tmpFile, emailContent)
  
  try {
    const result = execSync(
      `cat ${tmpFile} | spamassassin -t 2>&1`,
      { encoding: 'utf8' }
    )
    const scoreMatch = result.match(/X-Spam-Score: ([\d.]+)/)
    return scoreMatch ? parseFloat(scoreMatch[1]) : null
  } finally {
    fs.unlinkSync(tmpFile)
  }
}

test('welcome email spam score is acceptable', async () => {
  const emailContent = buildWelcomeEmail('user@example.com')
  const score = await getSpamScore(emailContent)
  
  expect(score).toBeLessThan(3)
}, 30000)

Common Spam Triggers

Avoid these in your email content:

Pattern Risk
ALL CAPS subject High
Excessive !!! High
"Click here" as link text Medium
Image-only emails (no text) High
Mismatched link URLs High
Missing unsubscribe link High
style="display:none" hidden text High

Automated Deliverability CI Check

# .github/workflows/email-deliverability.yml
name: Email Deliverability Check
on:
  schedule:
    - cron: '0 9 * * 1'  # Every Monday
  workflow_dispatch:

jobs:
  check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Check SPF record
        run: |
          SPF=$(dig TXT ${{ vars.EMAIL_DOMAIN }} | grep spf)
          if [ -z "$SPF" ]; then exit 1; fi
          echo "SPF: $SPF"
      
      - name: Check DKIM
        run: |
          DKIM=$(dig TXT mail._domainkey.${{ vars.EMAIL_DOMAIN }})
          if [ -z "$DKIM" ]; then exit 1; fi
          echo "DKIM found"
      
      - name: Check DMARC
        run: |
          DMARC=$(dig TXT _dmarc.${{ vars.EMAIL_DOMAIN }} | grep DMARC1)
          if [ -z "$DMARC" ]; then exit 1; fi
          echo "DMARC: $DMARC"
      
      - name: Check blocklists
        run: |
          IP=$(dig A mail.${{ vars.EMAIL_DOMAIN }} | grep -oP '\d+\.\d+\.\d+\.\d+' | head -1)
          # Check MXToolbox-style
          echo "Sending IP: $IP"

External Deliverability Tools

For deeper analysis beyond DNS records:

  • mail-tester.com — send a test email, get a deliverability score
  • MXToolbox — blocklist checks, DNS record validation
  • Google Postmaster Tools — spam rate, IP reputation for Gmail
  • Microsoft SNDS — reputation data for Outlook/Hotmail

Summary

Email deliverability testing has two layers: DNS validation (SPF, DKIM, DMARC) which you can automate in CI, and content/reputation testing which requires periodic manual checks or paid tools. Automate the DNS checks on a schedule — they're cheap to run and catch configuration drift before it affects delivery rates.

Read more