Gmail + Yahoo — February 2024
The original bar: any sender over 5,000 messages/day to personal accounts must authenticate with SPF and DKIM, publish DMARC, and offer one-click unsubscribe on marketing mail. This is the wave that started it all.
The three DNS-based protocols, SPF, DKIM, and DMARC, that get your app's mail trusted by inbox providers and stop attackers from spoofing your domain.
In the last lesson you added your sending domain to Resend, pasted its DNS records at your registrar, and watched the dashboard flip to Verified. A test email landed in your inbox. By every signal in front of you, the job is done.
It isn’t, and here is why: deliverability is your app’s responsibility, not your vendor’s. Verified means one narrow thing, that Resend can now sign mail on your behalf. It is not Gmail promising to trust that mail, not Outlook promising the inbox over the spam folder, and it does nothing to stop a stranger from sending mail that shows your domain in the From line to defraud your customers.
That leaves two gaps. The first is trust at the receiving end: since 2024 the major mailbox providers reject unauthenticated mail outright rather than spam-foldering it. The second is brand protection: stopping spoofers from forging your From: address. The same three protocols close both, and the fix lives in your DNS and your operational decisions, not in Resend’s infrastructure.
Those protocols are SPF , DKIM , and DMARC , and they stack. SPF and DKIM are two peers you publish; DMARC sits on top of both and makes them mean something. There is almost no application code here. The deliverables are DNS records you can read instead of paste blindly, and a staged plan for turning the protection on without breaking your own mail.
Email moves over SMTP , a protocol from an era when every machine on the network was run by someone you knew, so it has no way to verify who a message is from. The From: header is just text the sending server types in, and nothing stops a server from writing From: ceo@yourcompany.com on a message it composed itself. It works like the return address handwritten on a physical envelope: the post office delivers the letter regardless of what you scribble there, because checking it was never its job. That open-trust default is the hole all three protocols exist to patch.
The first thing to fix in your head is that an email does not have one identity field. It has several, and they can disagree.
bounces@send.yourapp.com Answers: Which server was allowed to send this? The hidden return address the receiving server bounces failures to — the recipient never sees it. d=send.yourapp.com Answers: Was this message altered, and who signed it? Stamped inside a signature header — invisible to the reader. YourApp <hello@send.yourapp.com> Answers: Does the visible sender match the checks that passed? The only field the human actually reads in their inbox. The field a human reads in their inbox, the visible From:, is not the field SPF checks, and not the field DKIM checks. SPF validates the envelope sender , the hidden return address used during the SMTP handshake. DKIM validates the domain named in its own signature. A spammer can make both pass perfectly on their own domain while writing yourbank.com in the visible From:, and unless something compares the passing checks back to that visible line, nobody catches it.
Here is what each protocol contributes:
From: the user sees, and if not, what should I do about it?SPF and DKIM are the two peers you publish. DMARC is the policy on top that turns them from disconnected checks into real brand protection. We will build them in that order.
SPF is the simplest of the three. An SPF record is a single TXT record published at your sending domain listing who may send mail for it. The list can be raw IP ranges or, far more commonly, include: references that point at another provider’s published list of IPs. When mail arrives, the receiving server checks the IP of the machine that connected against your record: a match passes, an unknown server fails.
Here is the SPF record Resend asked you to add in the last lesson, on your sending subdomain:
v=spf1 include:_spf.resend.com ~allRead it left to right. v=spf1 is the version marker that opens every SPF record. include:_spf.resend.com is the workhorse: it tells the receiver to look up Resend’s own SPF record and treat every IP it authorizes as authorized for you. Resend maintains that list, so when they add or rotate servers your record keeps working untouched. That is the include mechanism . The last token, ~all, is the catch-all for any server not matched above.
That last token is a real decision. ~all is a softfail : “this server probably isn’t authorized, but don’t hard-reject the mail on that basis alone.” -all is a hardfail : “this server is definitely not authorized; treat its mail as a failure.” Hardfail is stricter, but it is only safe once every path sending from this subdomain goes through Resend. Resend defaults to ~all, and that is the right default: start permissive, and tighten to -all only after you have confirmed Resend is the sole sender on the subdomain.
One detail sets up DMARC later: SPF authenticates the envelope MAIL FROM, not the visible From:. It only ever looks at that hidden return address from the SMTP handshake and has no opinion about the From: line your customer reads. This is half of why DMARC has to exist.
SPF carries one trap that bites hard in production. It allows a maximum of 10 DNS lookups per evaluation, and every include: costs one. Picture an apex domain that already includes Google Workspace, Microsoft 365, and three SaaS vendors that send on your behalf, an ordinary setup that sits close to the ceiling. Someone adds Resend’s include:, the evaluation tips over 10 lookups, and the receiver gives up with a permerror. The consequence is severe and easy to miss: SPF then fails not just for the new sender but for every sender on that domain at once. The dedicated sending subdomain you have been using defuses this. When Resend sends from send.yourapp.com, that subdomain carries its own SPF record holding nothing but Resend’s one include:, so the lookup limit never comes into play and your crowded apex record is left untouched. The next lesson makes the full case for splitting your domains.
One more record to recognize: alongside the SPF entry, Resend asks you to add an MX record on the sending subdomain. It sets up a custom return path so bounces and complaint feedback flow back to Resend instead of vanishing, which also keeps SPF aligned on that subdomain. You don’t need to do anything with that feedback yet; a later lesson puts the bounce and complaint data to work.
SPF answers whether mail came from an allowed server. It says nothing about whether the message was tampered with in transit, or whether the sender actually holds the keys to the domain they claim. DKIM closes both gaps with public-key cryptography.
A keypair is two matched keys. The private key signs data and stays secret. The public key verifies that signature and can be shared with anyone. Holding the public key lets you check a signature but never forge one, because only the private key can produce a valid one.
DKIM applies this to email. When Resend sends your message, its MTA signs the message with a private key and attaches a DKIM-Signature header. The matching public key lives in your DNS, as a TXT record at a special selector subdomain. Resend’s selector is resend, so the public key sits at resend._domainkey.send.yourapp.com. On arrival, the receiver reads the selector from the signature header, fetches the public key from that exact DNS location, and verifies the signature against the message it received.
The record looks like this, and you would never type it by hand:
v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ…v=DKIM1 is the version, k=rsa names the key algorithm, and p= carries the public key. That base64 blob is truncated above; in reality it runs for hundreds of characters. Resend generates the keypair, keeps the private half, and publishes the public half. You copy the value into your registrar exactly as given, never authoring it yourself, and Resend can rotate it over time on its own.
DKIM-Signature reads DKIM-Signature selector + d= domain resend._domainkey.send.yourapp.com
public key
published · anyone may read A passing DKIM check buys you two guarantees. Integrity: the signed parts of the message arrived exactly as they were signed, with no relay altering the body or the key headers in transit. Authorization: whoever produced the signature held the private key for the domain in the signature’s d= tag. But the same gap from SPF remains. A passing check proves things about the d= domain, and d= need not match the domain in the visible From:. On its own, DKIM does not prove that the From: your customer reads is legitimate, which is DMARC’s job.
Start with the hole SPF and DKIM leave open: both pass independently of the visible From:.
An attacker registers evil.com, a domain they fully control, and sets up flawless SPF and DKIM for it, because nothing stops you from authenticating your own domain.
Then they send mail with the visible From: set to security@yourbank.com.
At the receiver, SPF passes (the connecting server is authorized for evil.com) and DKIM passes (the message is validly signed for evil.com).
Both checks are green, yet neither has anything to do with the yourbank.com the human will read.
Authentication “worked,” and your brand was spoofed anyway.
DMARC closes that hole through one idea: alignment .
DMARC requires that at least one of SPF or DKIM both passes and is aligned with the visible
From:, meaning the domain that passed is the domain the human sees.
In the spoof above both checks pass, but on evil.com while the From: says yourbank.com.
Nothing is aligned, DMARC fails, and the receiver now has an explicit instruction to act on.
The figure runs the legitimate case and the spoof side by side, this time forging your own domain so the stakes are concrete.
bounces@ send.yourapp.com
aligned
send.yourapp.com
aligned
hello@ send.yourapp.com reference bounces@ evil.com
not aligned
SPF passes — but on evil.com evil.com
not aligned
DKIM passes — but on evil.com security@ yourapp.com reference forged — the human reads this A DMARC record is a TXT record published at _dmarc.yourapp.com that declares two things SPF and DKIM never could: the policy to apply when no aligned check passes, and where to send reports about your mail.
It is the only one of the three that carries a policy and a feedback channel; SPF and DKIM are pure checks with no notion of what to do on failure.
So the layering is: SPF and DKIM are the checks, and DMARC binds them to the From: your user reads and tells receivers what to do when that binding fails.
Match each protocol to the one thing it checks. Click an item on the left, then its match on the right. Press Check when done.
MAIL FROM.d= signing domain.From: and sets the policy on failure.Here is a complete, realistic DMARC record. We’ll read it field by field.
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com; ruf=mailto:dmarc-failures@yourapp.com; fo=1; pct=100; adkim=s; aspf=s;v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com; ruf=mailto:dmarc-failures@yourapp.com; fo=1; pct=100; adkim=s; aspf=s;The version marker. It’s required, and it must come first; a record that doesn’t start with this is ignored entirely.
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com; ruf=mailto:dmarc-failures@yourapp.com; fo=1; pct=100; adkim=s; aspf=s;The policy applied to the apex when no aligned check passes: none (monitor only, reporting but taking no action), quarantine (deliver to spam), or reject (refuse the mail outright). This is the dial the next section ramps, and the one that can break real mail, which is why it’s marked red.
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com; ruf=mailto:dmarc-failures@yourapp.com; fo=1; pct=100; adkim=s; aspf=s;Where to send aggregate reports: daily XML roll-ups of how much of your mail passed and failed alignment, and from which sources. This is the single most valuable field while you’re at p=none, because it’s how you discover senders you forgot you had.
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com; ruf=mailto:dmarc-failures@yourapp.com; fo=1; pct=100; adkim=s; aspf=s;Where to send forensic (per-message failure) reports. They’re redacted and privacy-limited, and many receivers don’t send them at all. Useful to have, but don’t expect much from it.
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com; ruf=mailto:dmarc-failures@yourapp.com; fo=1; pct=100; adkim=s; aspf=s;Forensic-report options: 1 means generate a failure report if either SPF or DKIM fails to align. A minor knob that only matters if ruf reports actually arrive.
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com; ruf=mailto:dmarc-failures@yourapp.com; fo=1; pct=100; adkim=s; aspf=s;The percentage of failing mail the policy applies to. At 100 it hits all failing mail. This is your rollout throttle: you’ll start a stricter policy at a low percentage and ramp it up.
v=DMARC1; p=none; rua=mailto:dmarc-reports@yourapp.com; ruf=mailto:dmarc-failures@yourapp.com; fo=1; pct=100; adkim=s; aspf=s;Alignment mode for DKIM and SPF: s is strict (the domains must match exactly), r is relaxed (a subdomain of the From: org domain counts as aligned). Use strict on a sensitive apex, and relaxed when subdomains legitimately send on your behalf.
One field is missing from the record above and deserves attention for that reason: sp=, the policy for subdomains. Leave it out and subdomains inherit whatever p= is. Inheritance runs from the apex down, so the DMARC record at your apex, _dmarc.yourapp.com, automatically covers every subdomain beneath it. The trap is to publish DMARC only at a subdomain like _dmarc.send.yourapp.com and leave the apex with no record at all: the one domain attackers most want to spoof, your bare apex, is now unprotected even though you “set up DMARC.” Publish at the apex first, since it covers everything, and reach for sp= only to run a subdomain at a different strictness than the apex.
You're publishing your first DMARC record. You want monitor-only (no mail affected), applied to all of your mail, with strict DKIM alignment. Fill in the blanks. Pick the right option from each dropdown, then press Check.
v=DMARC1; p=___; rua=mailto:dmarc-reports@yourapp.com; pct=___; adkim=___;The highest-payoff idea here, and the one most people get wrong: p=reject is not a setting you turn on. It is a destination you arrive at, over weeks, on evidence.
A wrong p=reject does not fail gracefully. It hard-bounces every message from any legitimate sender you forgot to authenticate: the billing tool that emails invoices from your domain, the CRM that sends from a marketing address, the founder running a mail-merge from their laptop. Flip straight to reject and all of it starts bouncing the moment your DNS propagates, silently, while you believe you just hardened your security. So you do the opposite of a one-time DNS edit: start permissive, watch what actually happens, and tighten only when the evidence says it is safe.
That discipline is a four-stage playbook.
Publish p=none with rua flowing to a report parser. At p=none, no mail is affected; you are doing nothing but collecting the daily aggregate reports. Point rua at a DMARC monitoring service rather than a raw mailbox, because the reports are XML and you should never hand-parse them. Services like dmarcian, PowerDMARC, and Postmark’s DMARC monitoring all have free tiers that turn the XML into a readable dashboard.
Watch for about a week and authenticate every legitimate source. This is what the reports are for: they reveal every system currently sending under your domain, including the ones you’d forgotten. Work down the list, adding SPF includes, wiring up DKIM, or routing stray senders through your sending subdomain, until everything legitimate shows up as aligned.
Move to p=quarantine with pct=10, then ramp pct toward 100 over a week. Now you begin enforcing, but on only a tenth of failing mail at first, watching the reports for collateral damage: any legitimate mail you missed showing up as a failure. If it’s clean, raise the percentage in steps.
Move to p=reject once quarantine has run clean for about two weeks. Only when enforcement has been quietly working with no legitimate casualties do you reach the destination: spoofed mail is now refused outright, and your brand is protected.
This is where your project lands. Its first deploy publishes p=none with monitoring and graduates up the ramp over the project’s lifetime, which is what this unit’s project chapter ships. You launch at none and know the path, not at reject.
So when is it actually safe to take the next step? The gate is always what your reports show, never how much time has passed. Walk it through.
You can’t tighten what you can’t see. Point rua at a monitoring service, let the daily aggregate reports start flowing, and read them for about a week before you change anything.
Tightening now would hard-bounce the legitimate senders that still fail alignment. Work down the report, adding SPF includes, wiring up DKIM, or routing stray senders through your sending subdomain, until everything legitimate shows as aligned.
Begin enforcing, but on only a tenth of failing mail. Ramp pct toward 100 over about a week, watching the reports for any legitimate mail that starts failing. A clean window here is what earns the move to reject.
Enforcement has run quietly with no legitimate casualties. Move to p=reject: spoofed mail is now refused outright and your brand is protected. This is a destination reached on evidence, not a day-one setting.
SPF, DKIM, and DMARC became non-optional in 2026 because the major mailbox providers now enforce them at the door.
Gmail + Yahoo — February 2024
The original bar: any sender over 5,000 messages/day to personal accounts must authenticate with SPF and DKIM, publish DMARC, and offer one-click unsubscribe on marketing mail. This is the wave that started it all.
Microsoft (Outlook / Hotmail / Live) — May 5, 2025
The same 5,000/day threshold and the same SPF + DKIM + DMARC requirement, with p=none as the minimum to be accepted at all.
La Poste — September 2025
France’s postal email service followed with equivalent authentication requirements, a sign the bar is now an industry baseline, not a Big-Tech quirk.
The written threshold is 5,000 messages a day to consumer mailboxes, but enforcement has hardened and now reaches far smaller senders. Build to the bar from day one, whatever your volume; retrofitting authentication after your signup emails start disappearing is a bad week.
And “disappearing” is literal, because the consequence changed in kind. Failing mail is no longer dropped into a spam folder a determined user could dig through; it is rejected outright. Here is Microsoft’s actual SMTP response when authentication falls short:
550 5.7.515 Access denied, sending domain … does not meet the required authentication level“Went to spam” means the user can still find your verification email. “Rejected” means they never receive it, and your signup funnel breaks silently: the rejection happens at the recipient’s server, so no error reaches you.
Here is the 2026 baseline to build against.
p=none is the minimum to send at all to the major providers; p=quarantine or p=reject is the target to work toward.List-Unsubscribe and List-Unsubscribe-Post headers below). Transactional mail, meaning verification codes, password resets, and receipts, is exempt from the unsubscribe requirement but not from authentication. The next lesson covers the transactional/marketing split.The unsubscribe headers from that checklist look like this. You won’t implement them in this chapter, but recognize the shape so the marketing/transactional line stays concrete:
List-Unsubscribe: <https://yourapp.com/unsubscribe?token=…>List-Unsubscribe-Post: List-Unsubscribe=One-ClickThat is one-click unsubscribe : the List-Unsubscribe-Post header lets Gmail or Outlook show a native “unsubscribe” button that works in a single tap, without the user loading your page. Transactional mail doesn’t need it but still needs every authentication item above.
Finally, prove the setup works. Once your records are live, verify them three ways:
Send a test to a header-parsing verifier. Tools like learndmarc.com give you an address to mail, and their auto-reply reports your SPF, DKIM, and DMARC results back to you, broken down field by field. This is the fastest way to see all three checks at once.
Send to a Gmail inbox and open “Show original.” In the message’s three-dot menu, “Show original” displays the raw headers with a summary at the top. Confirm you see SPF: PASS, DKIM: PASS, and DMARC: PASS. This is the receiving provider’s own verdict, which is the one that counts.
Check the Resend dashboard. The latest send shows its authentication result, and if something failed, it points you straight at the misconfigured record so you know which DNS entry to fix.
When all three agree, your domain is authenticated where it counts: not merely Verified by your vendor, but trusted by the providers your users read mail in.
These references go deeper on the specifics for your own domain.
Resend's canonical guide to the exact records and the staged p=none to p=reject path.
Google's authoritative statement of the Gmail authentication requirements and spam-rate thresholds.
Interactive visualizer that steps a real message through SPF, DKIM, and DMARC alignment live.
dmarcian's primer, plus the report-parsing service you point rua at in stage one of the rollout.