# SPF, DKIM and DMARC: what each record does and why your mail needs all three

> Three DNS records decide whether a mailbox provider treats your email as authentic. Here is what each one proves, how to set them up on a subdomain, and how to read a failure.

You added a sending domain, copied two DNS records, and your first email landed
in spam anyway. Or Resend returns a 403 and refuses to send at all. Or Gmail
delivers it and Outlook does not.

All three symptoms come from the same place: email has no built-in
authentication, so three separate DNS records were bolted on over twenty years
to answer the question "is this sender allowed to use this domain". Mailbox
providers now check all three, and Google and Yahoo have required them for bulk
senders since 2024.

## What each record proves

**SPF**: *which servers may send for this domain*. A TXT record listing
authorised senders:

```
v=spf1 include:amazonses.com ~all
```

The receiving server compares the connecting IP against that list. Two traps.
First, SPF checks the **envelope** sender, not the `From:` header a human sees,
which is why SPF alone cannot stop spoofing. Second, there is a hard limit of
**ten DNS lookups**; every `include:` costs at least one, and a domain with
Google Workspace plus a CRM plus a marketing tool plus a transactional provider
blows through it. Past ten, the check returns `permerror` and fails.

**DKIM**: *this message was not tampered with, and the domain owner signed it*.
Your provider signs each message with a private key; the public key sits in DNS
at `<selector>._domainkey.yourdomain.com`. The receiver fetches it and verifies.
DKIM survives forwarding, which SPF does not: a forwarded message keeps its
signature but arrives from the forwarder's IP.

**DMARC**: *what to do when the first two fail, and where to send reports*. A
TXT record at `_dmarc.yourdomain.com`:

```
v=DMARC1; p=none; rua=mailto:dmarc@yourdomain.com; pct=100
```

DMARC adds the piece SPF and DKIM lack: **alignment**. It requires that the
domain that passed SPF or DKIM matches the domain in the visible `From:` header.
That is what actually stops someone sending as you.

`p=` is the instruction to receivers: `none` (monitor only), `quarantine` (spam
folder), `reject` (refuse outright).

## Set it up on a subdomain

Use `mail.yourdomain.com`, not `yourdomain.com`.

Reputation is tracked per domain and per subdomain. Keeping transactional mail
on its own subdomain means a bad week (a bounce spike, a spam complaint from
one campaign) cannot damage the domain your team sends sales mail from, and a
compromised marketing tool cannot damage your password resets.

In Resend: **Domains → Add domain → `mail.yourdomain.com`**. It shows you the
records. Add them at your DNS provider exactly as given, with two gotchas that
account for most failed verifications:

- **Do not double the domain suffix.** Many DNS UIs append the zone
  automatically, so entering `resend._domainkey.mail.yourdomain.com` creates
  `resend._domainkey.mail.yourdomain.com.yourdomain.com`. Enter the host part
  only.
- **Turn off proxying.** On Cloudflare, DNS records for mail must be
  "DNS only" (grey cloud), not proxied.

Verification is usually minutes. Propagation can be an hour. Until the domain
shows **Verified**, Resend rejects sends with a 403: this is not a filtering
problem, and no amount of copy tuning fixes it.

## Start DMARC at p=none

Publish `p=none` first and read the aggregate reports for a couple of weeks.
They arrive as XML at the `rua=` address; a free parser is enough. What you are
looking for is any legitimate mail failing alignment: a helpdesk, an invoicing
tool, a CRM, something a colleague set up two years ago.

Then tighten to `p=quarantine`, then `p=reject`. Going straight to `reject`
before you know what sends as you is how a company discovers on a Monday that
its invoices have been bouncing since Friday.

## Reading a failure

Open the message in Gmail and choose **Show original**. The header block tells
you everything:

```
SPF:   PASS with IP 54.240.x.x
DKIM:  PASS with domain mail.yourdomain.com
DMARC: PASS
```

- **SPF fail, DKIM pass, DMARC pass**: fine. Common after forwarding, and the
  reason DKIM matters more than SPF.
- **DKIM fail**: the record is wrong, missing, or the message was modified in
  transit (some mailing lists rewrite bodies). Re-check the selector record.
- **DMARC fail with both passing**: an alignment problem. Something is sending
  with a `From:` on your root domain while authenticating as a different one.
- **SPF permerror**: you are past ten DNS lookups. Flatten the record or move
  senders onto subdomains.

## The verify check

`bun run verify` asserts that the domain in `EMAIL_FROM` exists on the Resend
account and reads `verified`. It is deliberately part of the standard check
because "we forgot to verify the production domain" is a launch-day failure that
looks like a code bug.

One caveat worth knowing before it surprises you: listing domains needs a
full-access key, and the onboarding tells you to create a **sending-access** key,
which is the right advice. With such a key Resend answers `restricted_api_key`
and the check passes with a note saying the domain state was not read: confirm
it in the dashboard instead. Any *other* rejection still fails the check, so a
wrong key and a deliberately restricted one do not read the same.

## Two more things worth doing

**Do not send from a no-reply address.** Replies to a black hole correlate with
complaints, and someone will reply. `REPLY_TO` exists for this.

**Never put a customer's address in `From:`.** "Sam invited you" as a display
name is fine; `sam@theircompany.com` as the actual `From` fails DMARC on their
domain, lands in spam, and is technically indistinguishable from spoofing. Set
`replyTo` to the inviter instead. `agent/skills/add-email-template.md` builds an
invite template as its worked example and does exactly this at the call site.

---

Agentic Boilerplate: A Next.js repo your agent already knows. Free during launch, then $99 once.

- Site map for agents: https://agenticboilerplate.com/llms.txt
- Public API: https://agenticboilerplate.com/openapi.json
- Contact: agenticstudio@gmail.com
