Skip to content

Portal | Level: L1: Foundations | Topics: Email Infrastructure | Domain: Networking

Email Infrastructure — Primer

Why This Matters

Email is the oldest and most universal communication protocol on the internet, and it is one of the most abused. Phishing, spam, and spoofing all exploit the fact that SMTP has no built-in authentication — anyone can claim to be anyone. SPF, DKIM, and DMARC are the three layered defenses that modern email authentication is built on. Getting them right is the difference between your outbound email landing in the inbox and going to spam — or worse, being used for fraud against your customers while your domain reputation is destroyed.

Every engineer who works on infrastructure will eventually be asked why emails from a new service are going to spam, or why a domain is being spoofed, or why a SaaS integration's emails are failing. Understanding MX records, SMTP flow, authentication headers, and reputation mechanics is essential for diagnosing these problems and building email systems that work reliably.

This topic covers the full outbound email stack: how mail flows, how each authentication layer works, what the DNS records look like, and how to diagnose delivery failures.

Core Concepts

1. MX Records and SMTP Flow

The MX (Mail Exchanger) record tells senders where to deliver mail for a domain.

$ dig MX example.com
;; ANSWER SECTION:
example.com.  3600  IN  MX  10  mail1.example.com.
example.com.  3600  IN  MX  20  mail2.example.com.

# Lower preference value = higher priority
# If mail1 is unavailable, sender tries mail2

SMTP delivery flow:

1. Sending MTA looks up MX records for recipient domain
2. Connects to highest-priority MX on port 25 (SMTP)
3. Performs EHLO/STARTTLS
4. Authenticates (if relay, not direct delivery)
5. DATA: sends From, To, Subject, body with headers
6. Receiving MTA accepts or rejects based on:
   - SPF check (did this IP have permission to send?)
   - DKIM verification (is the signature valid?)
   - DMARC policy (what to do if SPF/DKIM fail?)
7. Message queued for delivery to mailbox

Key SMTP response codes: | Code | Meaning | |------|---------| | 220 | Service ready | | 250 | OK — accepted | | 421 | Service temporarily unavailable (try again) | | 450 | Mailbox temporarily unavailable | | 550 | Mailbox not found / rejected | | 553 | Mailbox name invalid | | 554 | Transaction failed / spam rejection |

2. SPF — Sender Policy Framework

Remember: The three email authentication layers mnemonic: SPF checks the Sender IP, DKIM checks the Digital signature, DMARC Decides the action. SPF = "who sent it?", DKIM = "was it tampered with?", DMARC = "what to do if checks fail?"

SPF (RFC 7208) declares which IP addresses are authorized to send mail for a domain. It's a TXT record published in DNS.

Basic SPF record:

example.com.  IN  TXT  "v=spf1 mx a:mail.example.com ip4:203.0.113.10 include:_spf.sendgrid.net -all"

Mechanism breakdown: - v=spf1 — required prefix - mx — allow all IPs in the domain's MX records - a:mail.example.com — allow the A record IP of mail.example.com - ip4:203.0.113.10 — allow specific IP - ip6:2001:db8::/32 — allow IPv6 range - include:_spf.sendgrid.net — include another domain's SPF policy - -all — hard fail anything not listed (reject)

Qualifiers: | Qualifier | Meaning | Action | |-----------|---------|--------| | + (default) | Pass | Authorized, deliver | | - | Fail | Not authorized, reject | | ~ | Softfail | Suspicious, accept but mark | | ? | Neutral | No assertion |

~all vs -all: - ~all (softfail): Receiving server accepts the message but marks it as suspicious. Often results in spam folder. Useful during initial deployment. - -all (hard fail): Receiving server should reject. This is the correct production setting once SPF is fully configured.

Default trap: Using ~all (softfail) instead of -all (hard fail) is one of the most common SPF mistakes in production. Many teams deploy ~all during testing and never switch to -all. Softfail means spoofed emails from your domain still get delivered (just flagged), which defeats the purpose of SPF.

The 10-lookup limit: SPF processing must complete in 10 DNS lookups (includes include:, mx, a, ptr, exists mechanisms — ip4/ip6 don't count). Exceeding this causes a PermError which many servers treat as neutral or fail.

# Check lookup count with spfwalk tool
spfwalk example.com
# Or manually trace:
dig TXT example.com | grep spf
dig TXT _spf.sendgrid.net | grep spf
# Count each include, mx, a, ptr, exists

# Test SPF evaluation
dig TXT example.com
# Check if your sending IP is covered:
# python3 -c "import spf; print(spf.check2(i='203.0.113.10', s='user@example.com', h='mail.example.com'))"

3. DKIM — DomainKeys Identified Mail

DKIM (RFC 6376) adds a cryptographic signature to each email. The private key signs a hash of selected headers and body; the public key is published in DNS under a selector record.

DKIM DNS record (selector = mail):

mail._domainkey.example.com.  IN  TXT
  "v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC..."

Generate DKIM keypair:

# OpenSSL method
openssl genrsa -out dkim-private.pem 2048
openssl rsa -in dkim-private.pem -pubout -out dkim-public.pem

# Extract for DNS record (remove headers, join lines)
grep -v "^-----" dkim-public.pem | tr -d '\n'

# Or use opendkim-genkey (Postfix/OpenDKIM)
opendkim-genkey -s mail -d example.com -b 2048
# Creates mail.private and mail.txt (DNS record ready to publish)

DKIM signature in email headers:

DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=example.com; s=mail;
        h=from:to:subject:date:message-id;
        bh=47DEQpj8HBSa+/TImW+5JCeuQeRkm5NMpJWZG3hSuFU=;
        b=ABC123...long-base64-signature...

  • d= — signing domain
  • s= — selector (used to look up s._domainkey.d in DNS)
  • h= — signed headers (from and subject should always be signed)
  • bh= — hash of message body
  • b= — signature of the hash

OpenDKIM config (Postfix integration):

# /etc/opendkim.conf
Mode                  sv          # sign and verify
Domain                example.com
Selector              mail
KeyFile               /etc/opendkim/keys/mail.private
Socket                inet:8891@localhost
Canonicalization      relaxed/relaxed
SignHeaders           From,To,Subject,Date,Message-ID

4. DMARC — Domain-based Message Authentication

DMARC (RFC 7489) ties SPF and DKIM together by requiring that at least one passes in alignment with the From: header domain. It also provides a policy for what to do on failure, and a reporting mechanism.

DMARC DNS record:

_dmarc.example.com.  IN  TXT
  "v=DMARC1; p=reject; pct=100; rua=mailto:dmarc-agg@example.com; ruf=mailto:dmarc-fail@example.com; sp=reject; adkim=s; aspf=s"

Policy values (p=): | Value | Meaning | |-------|---------| | none | Monitor only — take no action on failure, just report | | quarantine | Send failing messages to spam folder | | reject | Reject failing messages at SMTP level |

Alignment modes: - aspf=r (relaxed): SPF domain can be a parent domain of From: (e.g., SPF passes for mail.example.com, From: is example.com) - aspf=s (strict): SPF domain must exactly match From: domain - adkim=r (relaxed): DKIM d= domain can be a parent domain of From: - adkim=s (strict): DKIM d= must exactly match From: domain

Other DMARC fields: - pct= — percentage of messages to apply policy to (100 = all). Use pct=10 when rolling out reject policy cautiously. - rua= — aggregate report destination (XML report, delivered daily) - ruf= — forensic/failure report destination (per-failure, not all providers send these) - sp= — policy for subdomains (defaults to p= if not set)

Deployment path (never jump straight to reject):

Week 1-2:  p=none; rua=mailto:dmarc@example.com
           # Monitor reports, identify all legitimate senders

Week 3-4:  p=quarantine; pct=10
           # Start quarantining 10% of failures

Week 5-6:  p=quarantine; pct=100
           # Full quarantine

Week 7+:   p=reject; pct=100
           # Full enforcement

5. Reading DMARC Aggregate Reports

DMARC aggregate reports (rua) arrive as gzip XML files. Use a tool to parse them:

# Unzip and read raw XML
gzip -d report.xml.gz
cat report.xml

# Key fields in the XML:
# <policy_published> — your DMARC policy at the time
# <record><row><source_ip> — IP that sent the mail
# <record><row><count> — how many messages
# <record><auth_results><spf><result> — pass/fail/none
# <record><auth_results><dkim><result> — pass/fail
# <record><row><policy_evaluated><disposition> — none/quarantine/reject

# Use dmarc-aggregate-parser or parsedmarc for human-readable output
pip install parsedmarc
parsedmarc report.xml.gz

# Sample parsed output:
# Source IP: 203.0.113.50  Count: 1250  SPF: pass  DKIM: pass  Disposition: none
# Source IP: 198.51.100.20  Count: 3    SPF: fail  DKIM: fail  Disposition: quarantine

The reports tell you which IP addresses are sending as your domain, whether they pass SPF/DKIM, and what action the receiving server took. Use p=none monitoring to find all legitimate email flows before moving to enforcement.

6. ARC — Authenticated Received Chain

When a message is forwarded (e.g., through a mailing list), the forwarding server modifies the message. This breaks DKIM (the body hash no longer matches) and SPF (the message now comes from the forwarding server's IP, not the original sender's). DMARC then fails for legitimate forwarded mail.

Name origin: DKIM stands for DomainKeys Identified Mail, a merger of Yahoo's DomainKeys and Cisco's Identified Internet Mail. The RFC was published in 2007. The "selector" mechanism (s=mail in the DKIM header) was a clever design choice — it allows key rotation without downtime by publishing the new key under a new selector while the old selector remains valid for in-flight messages.

ARC (RFC 8617) solves this by having each forwarding server stamp and sign the original authentication results before modifying the message:

ARC-Authentication-Results: i=1; mx.example.com;
       dkim=pass header.d=sender.com;
       spf=pass smtp.mailfrom=sender.com;
       dmarc=pass action=none header.from=sender.com
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; ...
ARC-Seal: i=1; a=rsa-sha256; cv=none; ...

The receiving server that sees a forwarded message can evaluate the ARC chain to determine if the message was legitimate before forwarding. Gmail and Microsoft 365 both honor ARC chains.

7. PTR Records and rDNS

A PTR (reverse DNS) record maps an IP back to a hostname. Receiving mail servers check this as a spam signal.

Requirements: - The PTR record for your sending IP must exist - The A record for the hostname in the PTR must point back to the same IP (forward-confirmed reverse DNS) - The PTR hostname should match or be related to the domain you're sending from

# Check PTR record for sending IP
dig -x 203.0.113.10
# 10.113.0.203.in-addr.arpa.  IN  PTR  mail.example.com.

# Verify forward confirmation
dig A mail.example.com
# mail.example.com.  IN  A  203.0.113.10  ← must match

# PTR records are set by your IP space owner (hosting provider/ISP)
# For cloud: set via EC2 console, Azure portal, GCP console
# For colo: request via IP provider

8. IP Warming and Blocklists

A new IP address has no sending reputation. ISPs rate-limit or reject mail from unknown IPs.

IP warming schedule (rough guideline):

Day 1:   500 messages
Day 2:   1,000
Day 3:   2,000
Day 5:   5,000
Day 7:   10,000
Week 2:  25,000/day
Week 3:  50,000/day
Week 4+: Full volume

Gotcha: Each major email provider (Gmail, Microsoft 365, Yahoo) has its own reputation system and rate limits. Gmail is particularly aggressive — a new IP sending more than a few hundred messages to Gmail addresses on day one will likely be throttled or blocked, even with perfect SPF/DKIM/DMARC.

Start with your most engaged users (high open rates). Avoid sending to old/stale addresses — bounces kill reputation.

Check blocklists:

# Check MXToolbox
curl "https://mxtoolbox.com/SuperTool.aspx?action=blacklist%3a203.0.113.10&run=toolpage"

# CLI check using local DNS
# Check Spamhaus ZEN (combines SBL, XBL, PBL)
dig 10.113.0.203.zen.spamhaus.org +short
# Returns 127.0.0.x if listed:
# 127.0.0.2 = SBL (Spamhaus Block List — spammer)
# 127.0.0.4 = XBL (Exploits Block List — infected/botnet)
# 127.0.0.10 = PBL (Policy Block List — dynamic/residential)

# Check multiple RBLs
for rbl in zen.spamhaus.org bl.spamcop.net b.barracudacentral.org; do
    result=$(dig +short 10.113.0.203.${rbl})
    if [ -n "$result" ]; then
        echo "LISTED on $rbl: $result"
    fi
done

Google Postmaster Tools: Sign up at postmastertools.google.com. After verifying domain ownership, you get: - Domain reputation (high/medium/low/bad) - IP reputation - Spam rate over time - Authentication (DKIM, SPF, DMARC) pass rates - Delivery errors by category

9. Transactional Email Services vs Self-Hosted

Self-hosted (Postfix + OpenDKIM): - Full control over configuration - You manage IP reputation and deliverability - Responsible for monitoring, bounce processing, unsubscribes - Good for: internal notifications, small volume, infrastructure alerting

# Postfix main.cf key settings for outbound
myhostname = mail.example.com
mydomain = example.com
myorigin = $mydomain
inet_interfaces = all
smtpd_banner = $myhostname ESMTP $mail_name
smtp_tls_security_level = may
smtpd_tls_security_level = may
milter_protocol = 6
smtpd_milters = inet:localhost:8891      # OpenDKIM
non_smtpd_milters = $smtpd_milters

Transactional services (Amazon SES, SendGrid, Postmark, Mailgun): - Managed IP warm-up and shared/dedicated IP pools - Bounce and complaint handling - Analytics, click tracking, template management - Better deliverability out of the box for most users

Amazon SES configuration:

# Verify sending domain
aws ses verify-domain-identity --domain example.com
# Returns a verification token — add as TXT record to domain

# Enable DKIM (SES generates the keys)
aws ses put-email-identity-dkim-attributes \
    --email-identity example.com \
    --dkim-signing-attributes SigningEnabled=true

# Get DKIM DNS records to publish
aws sesv2 get-email-identity --email-identity example.com \
    | jq '.DkimAttributes.Tokens[]' \
    | sed 's/"//g' \
    | while read token; do
        echo "${token}._domainkey.example.com CNAME ${token}.dkim.amazonses.com"
    done

# Set DMARC for SES-sent mail (add to your DNS)
# _dmarc.example.com TXT "v=DMARC1; p=reject; rua=mailto:dmarc@example.com"

10. Diagnosing Why Email Goes to Spam

# Step 1: Check authentication headers in the received email
# Gmail: Click "..." → "Show original"
# Look for:
#   Authentication-Results: mx.google.com;
#          dkim=pass header.i=@example.com
#          spf=pass smtp.mailfrom=example.com
#          dmarc=pass action=none

# Step 2: Test with mail-tester.com
# Send a test message to the provided address, get a score and breakdown

# Step 3: Check sending IP reputation
dig -x <your-ip>  # check PTR
dig +short <reversed-ip>.zen.spamhaus.org  # check Spamhaus

# Step 4: Validate DNS records
dig TXT example.com | grep spf
dig TXT mail._domainkey.example.com
dig TXT _dmarc.example.com

# Step 5: Use swaks for SMTP testing
swaks --to test@gmail.com \
      --from noreply@example.com \
      --server mail.example.com \
      --tls \
      --auth LOGIN \
      --auth-user noreply@example.com

# Step 6: Check Google Postmaster Tools domain reputation
# Step 7: Review bounce messages for specific rejection codes
# 550 5.7.26 = DMARC failure
# 421 4.7.0 = IP rate limited (reputation issue)
# 550 5.7.1 = Rejected by receiving server policy

Quick Reference

DNS records for a properly configured domain:

# SPF (hard fail after listing all legitimate senders)
example.com.             TXT  "v=spf1 include:_spf.google.com ip4:203.0.113.10 -all"

# DKIM (one record per selector/service)
mail._domainkey.example.com.  TXT  "v=DKIM1; k=rsa; p=MIGfMA0..."
google._domainkey.example.com. CNAME googleXXX.domainkey.google.com.

# DMARC
_dmarc.example.com.      TXT  "v=DMARC1; p=reject; pct=100; rua=mailto:dmarc@example.com"

# MX
example.com.             MX   10  mail.example.com.

# PTR (set at IP provider)
10.113.0.203.in-addr.arpa.  PTR  mail.example.com.

# Full header check commands
dig MX example.com
dig TXT example.com
dig TXT mail._domainkey.example.com
dig TXT _dmarc.example.com
dig -x <sending-ip>

# Spamhaus check
dig +short <reversed-ip>.zen.spamhaus.org

# Send test
swaks --to recipient@gmail.com --from user@example.com --server mail.example.com --tls

Wiki Navigation

Prerequisites