# Email, Demystified (SPF, DKIM, DMARC)

> How email really travels and why yours lands in spam: SMTP, plus the three DNS records that prove a message is truly from your domain.


---

# Email, Demystified (SPF, DKIM, DMARC)

You sent a perfectly normal email and it landed in spam. Or worse: someone sent mail *as you* - your domain, your name - and your customers got it. Email feels like one of the oldest, simplest things on the internet, yet the moment deliverability breaks, the advice you find is a wall of acronyms with no model underneath. SPF, DKIM, DMARC, "soft fail," "alignment" - what *are* these things?

This guide gives you the model first. You'll follow one email on its real journey, see exactly why spoofing was so easy for so long, and then meet the three DNS records that fix it - what each one actually proves, the literal text you put in DNS, and why you need all three working together. By the end, "it's going to spam" will be a problem you can diagnose, not a curse.

## How to read this

- **Deliverability is on fire right now?** Skip to [Phase 3](03-fixing-deliverability.md) - it's the cure, the record-by-record checklist, and how to read a DMARC report.
- **Want it to actually make sense?** Read in order. The journey (Phase 1) explains *why* spoofing works; the three records (Phase 2) only make sense once you've seen the hole they plug.

## The phases

1. **[The Journey of One Email](01-the-journey-of-one-email.md)** - what SMTP really is, the hop-by-hop trip from your app to the recipient's inbox, and the open door that lets anyone claim to be you.
2. **[The Three Proofs: SPF, DKIM, DMARC](02-spf-dkim-dmarc.md)** - the three DNS records that authenticate your mail: who may send, a tamper-proof signature, and the policy that ties them together. The actual records, line by line.
3. **[Fixing Deliverability](03-fixing-deliverability.md)** - why mail still goes to spam after you "set up SPF," the alignment trap, reading a DMARC report, and a working checklist.

> This guide assumes you're comfortable with the basics of how machines find each other. If DNS records and ports feel fuzzy, read [IP Addresses, DNS & Ports](/guides/ip-dns-and-ports) first - email leans hard on DNS, and that foundation makes everything here click.


---

# The Journey of One Email

You hit Send. A second later it's in their inbox. That smoothness hides a surprising amount of machinery - and one design flaw from the 1980s that explains nearly every deliverability headache you'll ever have.

So let's slow the whole thing down and watch a single email make its trip. Once you've seen the route, the rest of this guide is mostly "here's how we bolted security onto a system that originally had none."

## The cast: four players, not one

When you picture email, you probably picture two things: you and the person you're emailing. There are actually four roles in the room, and confusing them is where most people get lost.

- **MUA - Mail User Agent.** Your email *app*. Gmail's web UI, Outlook, Apple Mail, the code in your app that sends a receipt. This is what *you* touch.
- **MSA / outgoing server - your sending server.** The machine your MUA hands the message to. For Gmail that's Google's servers; for an app it might be SendGrid, Postmark, Amazon SES, or your own.
- **MTA - Mail Transfer Agent.** The relay servers that pass the message across the internet, server to server, until it reaches the destination. Your sending server is the first MTA in the chain.
- **MDA - Mail Delivery Agent.** On the receiving side, the part that drops the message into the right mailbox, where the recipient's app finally reads it.

Hold onto one distinction above all: **the app you click in is not the server that sends.** The server is where authentication lives. That's the whole game.

## SMTP: the language servers speak

Every hop where mail moves *toward* the recipient uses one protocol: **SMTP** - Simple Mail Transfer Protocol. It runs on **port 25** between servers (and **587** when your app submits a new message to its sending server). It's a plain-text conversation, and reading one demystifies email faster than any diagram.

Here's a sending server talking to the recipient's server, lightly trimmed. Lines the client sends are plain; the server's numeric replies are the responses.

```text
S: 220 mx.recipient.com ESMTP ready
C: EHLO mail.yourcompany.com
S: 250-mx.recipient.com at your service
C: MAIL FROM:<alice@yourcompany.com>
S: 250 2.1.0 OK
C: RCPT TO:<bob@recipient.com>
S: 250 2.1.5 OK
C: DATA
S: 354 Go ahead, end with <CRLF>.<CRLF>
C: From: "Alice" <alice@yourcompany.com>
C: To: "Bob" <bob@recipient.com>
C: Subject: Lunch?
C:
C: Are you free Thursday?
C: .
S: 250 2.0.0 OK: queued as 9F2A1
C: QUIT
S: 221 Bye
```

*What just happened:* the two servers had a structured chat - greet (`EHLO`), declare the sender (`MAIL FROM`), declare the recipient (`RCPT TO`), then hand over the message body after `DATA`. Each `250` is the server saying "got it." The message body itself, including the `From:` header Bob will *see*, is nothing but text the sender typed.

## The hop-by-hop trip

Stitch the players and the protocol together and you get the journey. Notice the recipient's server doesn't magically know who to talk to - it's found through DNS, specifically the destination domain's **MX record** (Mail eXchanger), which says "mail for recipient.com goes to *this* server."

```mermaid
flowchart LR
  A[Your app / MUA] -->|submit :587| B[Your sending server]
  B -->|DNS: MX lookup| D[(DNS)]
  B -->|SMTP :25| C[Recipient's MX server]
  C --> E[Mailbox / MDA]
  E --> F[Their app reads it]
```

*What just happened:* your app submits the message once, to your server. Your server looks up the recipient domain's MX record in DNS to learn *where* their mail lives, then speaks SMTP to that server, which files it into the mailbox. The recipient's app reads it later. The address book is DNS; the conversation is SMTP.

## The open door: anyone can say they're you

Now look back at that SMTP transcript and ask the uncomfortable question: **what stopped the sender from typing a different `From:` line?**

Nothing.

SMTP was designed in an era when every server on the network was run by someone trustworthy. There was no built-in check that the machine claiming to send for `yourcompany.com` had any right to. The `From:` your recipient sees is decorative text inside `DATA` - the sending server fills it in with whatever it likes.

```text
C: MAIL FROM:<attacker@randomhost.ru>
...
C: From: "Your Bank" <security@yourbank.com>
C: Subject: Urgent: verify your account
```

*What just happened:* a server with no connection to `yourbank.com` declared itself as the bank in the visible `From:` line, and classic SMTP accepted it without protest. This is **spoofing**, and it's not a hack or an exploit - it's email working exactly as originally designed. Phishing lives in this gap.

> The trust problem isn't "can someone break in." It's that email, by default, has **no way to prove a message really came from the domain it claims.** Everything in Phase 2 exists to close that one gap.

## For builders

If your app sends mail - password resets, receipts, notifications - you are operating a sending server (or paying one like SES or Postmark to be yours). That means *your domain's* reputation is on the line every time. The receiving side can't see your good intentions; it can only check whether your domain has told the world which servers are allowed to send for it. That "telling the world" is three DNS records, and it's the difference between the inbox and the spam folder.

```quiz
[
  {
    "q": "In the SMTP world, which component is the server that actually transmits your message toward the recipient - not the app you click in?",
    "choices": ["The MUA", "The sending server / MTA", "The DNS resolver", "The MDA"],
    "answer": 1,
    "explain": "The MUA is your app. The sending server (an MTA) is what speaks SMTP to other servers - and where authentication lives."
  },
  {
    "q": "Why is spoofing the visible From: address possible in classic SMTP?",
    "choices": ["A bug in modern mail servers", "The From: header is just text the sending server fills in, with no built-in proof of identity", "Because port 25 is unencrypted", "Only if DNS is misconfigured"],
    "answer": 1,
    "explain": "SMTP was built for a trusted network. The From: line is decorative text inside DATA; nothing in the base protocol verifies the sender owns that domain."
  },
  {
    "q": "How does your sending server know which machine to deliver mail to for recipient.com?",
    "choices": ["It guesses based on the domain name", "It looks up the domain's MX record in DNS", "The recipient's app tells it", "It always uses port 587"],
    "answer": 1,
    "explain": "The MX (Mail eXchanger) record in the recipient domain's DNS names the server that accepts its mail. DNS is email's address book."
  }
]
```


---

# The Three Proofs: SPF, DKIM, DMARC

Phase 1 left us with one gap: email has no built-in way to prove a message came from the domain it claims. The fix isn't a new protocol - it's three DNS records, each answering a different question. Once you see *which* question each one answers, the acronyms stop blurring together.

- **SPF** answers: *which servers are allowed to send mail for my domain?*
- **DKIM** answers: *was this specific message actually signed by my domain, and untampered?*
- **DMARC** answers: *if SPF or DKIM fails, what should the receiver do - and tell me about it?*

Think of it as a guest list (SPF), a wax seal (DKIM), and a house policy with a logbook (DMARC). You need all three, and the reason will be obvious by the end.

## SPF - the guest list

**SPF (Sender Policy Framework)** is a single TXT record in your domain's DNS that lists which servers may send mail on your behalf. When a receiving server gets a message, it looks up your SPF record and checks: *did this connection come from one of the listed servers?*

```text
yourcompany.com.  TXT  "v=spf1 include:_spf.google.com include:sendgrid.net ~all"
```

*What just happened:* you published a guest list. `v=spf1` marks it as SPF. `include:_spf.google.com` and `include:sendgrid.net` pull in the server ranges of Google and SendGrid - saying "those vendors send for me." The `~all` at the end is the catch-all verdict for *everyone else*: `~` means **softfail** (treat as suspicious), `-` means **hardfail** (reject outright), `+` would mean allow-all (never use it).

The crucial detail most people miss: **SPF checks the `MAIL FROM` envelope address from the SMTP conversation, not the `From:` your recipient sees.** A spoofer can pass SPF for *their own* domain while still showing your name in the visible header. SPF alone proves "this server is allowed to send for *some* domain," not "this matches what the human reads." Hold that thought - DMARC fixes it.

> One real-world trap: SPF allows a maximum of **10 DNS lookups** when evaluating includes. Stack too many `include:` vendors and SPF returns a `permerror` - silently failing for everyone. Keep the list lean.

## DKIM - the wax seal

**DKIM (DomainKeys Identified Mail)** adds a cryptographic signature to each message. Your sending server signs key parts of the email (headers and body) with a **private key**; you publish the matching **public key** in DNS. The receiver verifies the signature against that public key.

This proves two things SPF can't: the message genuinely came from your domain, *and* it wasn't altered in transit.

The signature rides along as a header inside the message:

```text
DKIM-Signature: v=1; a=rsa-sha256; d=yourcompany.com; s=mail2024;
  h=from:to:subject:date; bh=2jUSOH9NhtVGCQWNr9...;
  b=Cs4Hd9aP1n0kFqL7mZ...signature...
```

*What just happened:* the sending server stamped the message. `d=yourcompany.com` is the signing domain; `s=mail2024` is the **selector** (a label that lets you run multiple keys). `bh=` is a hash of the body; `b=` is the actual signature over the chosen headers (`h=`). If anyone changed the body or a signed header in transit, the hash won't match and DKIM fails.

The receiver fetches the public key from a predictable DNS name built from the selector and domain:

```text
mail2024._domainkey.yourcompany.com.  TXT  "v=DKIM1; k=rsa; p=MIGfMA0GCSq...public-key...AQAB"
```

*What just happened:* the receiver took `s=mail2024` and `d=yourcompany.com` from the signature, looked up `mail2024._domainkey.yourcompany.com`, and got your public key (`p=...`). It uses that key to verify the `b=` signature. Match means the message truly came from a holder of your private key and arrived intact.

Because the private key never leaves your sending infrastructure, a spoofer **cannot forge a valid DKIM signature for your domain.** This is the strongest of the three proofs.

## DMARC - the policy and the logbook

SPF and DKIM each check *a* domain - but, as noted, that domain doesn't have to be the one the human sees in `From:`. **DMARC (Domain-based Message Authentication, Reporting & Conformance)** closes that loophole with one idea: **alignment.**

DMARC says: SPF or DKIM must pass *and* the domain it passed for must **match the visible `From:` domain.** That's the piece that finally protects the address your recipient actually reads.

It also does two more things SPF and DKIM can't: it tells receivers **what to do** when checks fail, and it asks them to **send you reports.**

```text
_dmarc.yourcompany.com.  TXT  "v=DMARC1; p=quarantine; rua=mailto:dmarc@yourcompany.com; adkim=s; aspf=s; pct=100"
```

*What just happened:* you published a policy. `p=quarantine` is the instruction for failing mail - `none` (monitor only), `quarantine` (send to spam), or `reject` (refuse it). `rua=mailto:...` is where aggregate reports get sent - your logbook. `adkim=s` and `aspf=s` demand **strict alignment** (exact domain match; `r` for relaxed allows subdomains). `pct=100` applies the policy to all mail.

Here's the relationship in one picture:

```mermaid
flowchart TD
  M[Incoming message] --> S{SPF pass and aligned?}
  M --> K{DKIM pass and aligned?}
  S -->|yes| P[DMARC pass]
  K -->|yes| P
  S -->|no| C{either aligned pass?}
  K -->|no| C
  C -->|no| F[Apply DMARC policy: none / quarantine / reject]
  P --> D[Deliver, send report]
```

*What just happened:* DMARC passes if **either** SPF **or** DKIM passes *and* is aligned with the visible `From:` domain. Only one needs to succeed. If neither does, the receiver applies your `p=` policy and logs it in the report it sends to your `rua` address.

## Why you need all three

Each plugs the other's hole:

- **SPF alone** proves a server is authorized, but checks the hidden envelope address - a spoofer slips past it on the visible `From:`.
- **DKIM alone** proves authenticity and integrity, but it doesn't say what to do when a message *isn't* signed, and on its own it doesn't require alignment with the visible `From:`.
- **DMARC alone** is meaningless - it has nothing to evaluate. It's the referee that needs SPF and DKIM to be the players.

Together: SPF and DKIM provide the evidence, and DMARC enforces that the evidence matches what the human sees, tells receivers what to do, and reports back so you can see who's sending as you.

## For builders

When you onboard a new sending vendor (a new transactional email provider, a marketing tool), you touch all three: add the vendor's `include:` to SPF, publish the DKIM public key the vendor gives you under a selector, and confirm your DMARC alignment still holds. Forget the DKIM step and your mail passes SPF but fails alignment under a strict DMARC policy - straight to spam, with no error you'd notice unless you read the reports. That report-reading is Phase 3.

```quiz
[
  {
    "q": "What does an SPF record actually list?",
    "choices": ["The cryptographic signature of each message", "Which servers are authorized to send mail for your domain", "What to do when authentication fails", "The recipient's mail server"],
    "answer": 1,
    "explain": "SPF is a guest list of authorized sending servers, published as a TXT record and checked against the connecting server."
  },
  {
    "q": "What can DKIM prove that SPF cannot?",
    "choices": ["Which port the mail used", "That the message is cryptographically signed by the domain and was not altered in transit", "Which servers are allowed to send", "The recipient's identity"],
    "answer": 1,
    "explain": "DKIM signs the message with a private key; the public key in DNS verifies both origin and integrity. A spoofer can't forge it."
  },
  {
    "q": "What is DMARC's core requirement on top of SPF and DKIM?",
    "choices": ["That both SPF and DKIM must pass", "That SPF or DKIM passes AND aligns with the visible From: domain, plus a policy and reports", "That the message is encrypted", "That port 25 is used"],
    "answer": 1,
    "explain": "DMARC needs only one of SPF/DKIM to pass, but it must be aligned with the From: the human sees. It also defines the failure policy and requests reports."
  }
]
```


---

# Fixing Deliverability

You added an SPF record. You're still in spam. This is the moment most people give up and blame "the algorithm." But deliverability failures are almost never mysterious - they're a short list of specific, checkable mistakes. This phase is the cure: why mail still fails after a partial setup, how to read the report that tells you the truth, and a checklist you can run today.

## The number one cause: alignment, not authentication

Here's the trap that catches nearly everyone. You set up SPF. Your mail *passes SPF*. And it still fails DMARC, because passing isn't enough - it has to **align**.

Remember from Phase 2: SPF checks the hidden `MAIL FROM` envelope address, not the visible `From:`. When you send through a vendor, the envelope often belongs to *the vendor's* domain, while your `From:` says `you@yourcompany.com`. SPF passes - for the vendor - but the domains don't match, so DMARC sees a misalignment and fails.

```text
Envelope MAIL FROM:  bounce@mail.somevendor.com   <- SPF passes for THIS
Visible   From:      you@yourcompany.com           <- DMARC checks alignment to THIS
Result:   SPF=pass, but SPF-alignment=fail
```

*What just happened:* SPF gave a green light to the vendor's domain, but DMARC asks "does the authenticated domain match the From: domain?" and the answer is no. The fix is **DKIM** - sign with `d=yourcompany.com` and DKIM aligns to your visible `From:` regardless of whose servers relayed it. This is exactly why DKIM is non-negotiable, not optional.

## Read the report - it tells you who's failing

DMARC's `rua=` address gets you a daily XML report from every major receiver. It's ugly raw, but it answers the only question that matters: *which of my sending sources are passing, and which are failing?* Trimmed to the human-readable core, one record looks like this:

```text
source_ip:        198.51.100.24
header_from:      yourcompany.com
count:            42
spf:   result=pass   domain=mail.somevendor.com   aligned=no
dkim:  result=pass   domain=yourcompany.com        aligned=yes
disposition:      none   (DMARC overall: PASS via DKIM)
```

*What just happened:* 42 messages came from one source. SPF passed but *didn't align* (vendor's domain). DKIM passed *and aligned* to `yourcompany.com`. Because DMARC needs only one aligned pass, the overall verdict is PASS - saved by DKIM. If both `aligned` columns said `no`, you'd see your policy applied and your real mail getting quarantined. The report is how you find the *one* sending source you forgot to set up DKIM for.

> Reading raw DMARC XML by hand is miserable. Point your `rua` at a free or paid DMARC report aggregator - it turns the XML into a dashboard of "source X is failing." That's the single highest-leverage move for diagnosing deliverability.

## Roll out DMARC safely - don't start at reject

A common self-inflicted outage: publishing `p=reject` on day one. If any legitimate sending source isn't fully set up - a CRM, an old script, a billing system you forgot - its mail gets *rejected* the moment you flip the switch. Roll out in stages:

```text
Week 1+:  p=none        <- monitor only; collect reports, change nothing
Then:     p=quarantine  <- once reports show all real sources passing
Finally:  p=reject      <- when you're confident every source is aligned
```

*What just happened:* `p=none` lets you watch reality through the reports without affecting delivery. You only tighten to `quarantine` and then `reject` after the reports confirm every legitimate source authenticates and aligns. The reports are your runway; don't take off blind.

## Beyond the three records

Authentication gets you *eligible* for the inbox; it doesn't guarantee it. A few things still move the needle:

- **Reputation.** Receivers track how your domain and sending IP behave over time. New domains and IPs start cold and must warm up with consistent, wanted mail.
- **Content and lists.** Spammy subject lines, image-only emails, and sending to people who never opted in all push you toward spam regardless of perfect records.
- **PTR / reverse DNS.** If you run your own sending server, its IP should have a reverse DNS record matching its hostname. Mismatches look suspicious.
- **TLS.** Modern receivers expect mail over encrypted connections; bare port 25 with no TLS reads as old or sketchy.

Authentication is the floor, not the ceiling - but without it, nothing else matters, because you can't even prove you're you.

## The working checklist

Run this top to bottom for any domain that sends mail:

```text
[ ] MX record points to your real mail host
[ ] SPF: one TXT record, lists every sending source, ends in ~all or -all
[ ] SPF: under 10 DNS lookups (no permerror)
[ ] DKIM: public key published per vendor selector (s=...)
[ ] DKIM: vendor is actually signing with d=yourdomain.com
[ ] DMARC: _dmarc record exists with rua= reporting address
[ ] DMARC: started at p=none, watching reports before tightening
[ ] Reports: every legitimate source shows an aligned pass
[ ] Then and only then: move p= to quarantine, then reject
```

*What just happened:* you turned a vague "fix my email" into nine verifiable steps. Each line is checkable in DNS or in a report - no guessing, no blaming the algorithm.

## For builders

Bake this into onboarding, not firefighting. When your app adds a new way to send mail, the pull request that wires up the vendor should also note the SPF `include:`, the DKIM selector, and a check of the next DMARC report. Treat the three records as part of "shipping email," the same way you'd treat a database migration as part of shipping a feature. The teams that never think about deliverability are the ones who built it in from the start.

```quiz
[
  {
    "q": "Your mail passes SPF but still fails DMARC. What's the most likely cause?",
    "choices": ["Port 25 is blocked", "SPF passed for the vendor's domain, which doesn't align with your visible From: - and DKIM isn't set up to fix it", "The recipient's server is down", "Your MX record is missing"],
    "answer": 1,
    "explain": "SPF checks the envelope domain, often the vendor's. Without aligned DKIM signing as your domain, DMARC sees a mismatch and fails."
  },
  {
    "q": "Why start a DMARC rollout at p=none instead of p=reject?",
    "choices": ["p=none is more secure", "p=none lets you collect reports and confirm every legitimate source passes before any mail gets blocked", "reject doesn't work on most servers", "none enables encryption"],
    "answer": 1,
    "explain": "p=none is monitor-only. It gives you a safe window to find unconfigured sending sources via reports before tightening to quarantine and reject."
  },
  {
    "q": "What does the DMARC aggregate report (rua) most usefully tell you?",
    "choices": ["The content of each email", "Which of your sending sources are passing or failing authentication and alignment", "The recipients' passwords", "Your server's CPU usage"],
    "answer": 1,
    "explain": "The report breaks delivery down by source IP, showing SPF/DKIM results and alignment - so you can pinpoint the source you forgot to configure."
  }
]
```
