Dev.to WebDev 🛠 Dev 👁 0 📖 8 min read

SPF passed. DKIM passed. DMARC still failed. Here's why

You open the raw headers of an email that landed in spam and see this: Authentication-Results: mx.google.com; dkim=pass [email protected] header.s=s1; spf=pass [email protected].

You open the raw headers of an email that landed in spam and see this:

Authentication-Results: mx.google.com;
       dkim=pass [email protected] header.s=s1;
       spf=pass [email protected];
       dmarc=fail (p=QUARANTINE) header.from=acme.com

SPF passed. DKIM passed. DMARC failed. It looks like a bug in Gmail. It isn't.

DMARC doesn't ask "did SPF pass?" or "did DKIM pass?". It asks a stricter question: did either of them pass for the same domain your reader sees in the From line? That's called alignment, and it's the most common reason for exactly this result. This post explains alignment in plain terms, shows how to check it with dig and the raw headers, and walks through the five setups that cause it, each with its fix.

The 30-second version

There are three different domains in every email, and most people only think about one of them:

Domain Where it lives Who checks it
From domain The From: header your reader sees: [email protected] DMARC
Envelope (Return-Path) domain The SMTP MAIL FROM, where bounces go: [email protected] SPF
DKIM signing domain The d= tag in the DKIM-Signature header: d=sendgrid.net DKIM

SPF checks the envelope domain. DKIM checks the signing domain. Neither one, on its own, looks at the From line.

DMARC adds the missing link. It passes if at least one of these is true:

  • SPF passed and the envelope domain matches the From domain, or
  • DKIM passed and the signing domain matches the From domain.

In the headers above, SPF passed for em.sendgrid.net and DKIM passed for sendgrid.net. The From domain is acme.com. Neither matches, so DMARC fails, even though both checks individually passed.

What "matches" means: relaxed vs strict

DMARC has two alignment modes, set separately for SPF (aspf) and DKIM (adkim) in your DMARC record:

  • Relaxed (the default, r): the domains match if they share the same organizational domain. mail.acme.com and news.acme.com both align with acme.com.
  • Strict (s): the domains must be identical. mail.acme.com does not align with acme.com.

Almost everyone should use relaxed, which is what you get if you don't set the tags at all. If someone copied adkim=s; aspf=s from a "maximum security" guide, that alone can break DMARC for a setup that sends from a subdomain.

Step 1: Read what the receiver actually decided

Don't guess from your DNS. Look at the verdict the receiving server wrote into the message.

In Gmail: open the email, click ⋮ → Show original. The summary at the top shows SPF, DKIM and DMARC, and the raw message below includes the full Authentication-Results header.

In other clients: look for "View source", "Show raw message" or "View headers".

You're looking for three values:

  1. header.from= is the From domain DMARC evaluated.
  2. smtp.mailfrom= is the envelope domain SPF checked.
  3. header.i= or header.d= is the DKIM signing domain.

If neither the SPF domain nor the DKIM domain shares an organizational domain with header.from, you've found the problem.

A tiny script to check alignment from headers

If you debug this often, paste the Authentication-Results header into a script instead of eyeballing it. This one is deliberately simple. It uses the last two labels as the organizational domain, which is right for acme.com but wrong for domains like acme.co.uk (use a public-suffix library for those):

// check-alignment.js: node check-alignment.js < headers.txt
import { readFileSync } from "node:fs";

const raw = readFileSync(0, "utf8").replace(/\r?\n\s+/g, " "); // unfold header lines
const pick = (re) => (raw.match(re) || [])[1]?.toLowerCase();

const fromDomain = pick(/header\.from=([^\s;]+)/);
const spfDomain = pick(/smtp\.mailfrom=(?:[^@\s;]*@)?([^\s;]+)/);
const dkimDomain = pick(/header\.(?:d|i)=@?([^\s;]+)/);
const spfPass = /spf=pass/.test(raw);
const dkimPass = /dkim=pass/.test(raw);

// Simplified organizational domain: last two labels.
const org = (d) => d?.split(".").slice(-2).join(".");

const spfAligned = spfPass && org(spfDomain) === org(fromDomain);
const dkimAligned = dkimPass && org(dkimDomain) === org(fromDomain);

console.table({
  From: { domain: fromDomain },
  SPF: { domain: spfDomain, pass: spfPass, aligned: spfAligned },
  DKIM: { domain: dkimDomain, pass: dkimPass, aligned: dkimAligned },
});
console.log(spfAligned || dkimAligned ? "DMARC should pass (relaxed)" : "DMARC fails: nothing aligns with the From domain");

Run against the example at the top, it reports SPF and DKIM as passing but not aligned, which is the whole story.

Step 2: Check what you've published with dig

Once you know which domain is misaligned, check the DNS that's supposed to fix it.

# Your DMARC policy and alignment modes
dig +short TXT _dmarc.acme.com
# "v=DMARC1; p=quarantine; rua=mailto:[email protected]"   ← no aspf/adkim = relaxed

# DKIM for YOUR domain: the selector comes from your provider's setup instructions
dig +short CNAME s1._domainkey.acme.com
dig +short TXT   s1._domainkey.acme.com

# SPF on the domain your provider uses as the envelope sender (if you set a custom one)
dig +short TXT bounces.acme.com

If the DKIM query returns nothing, your provider is signing with its domain, not yours, and that's why DKIM doesn't align.

The five setups that cause "SPF pass, DKIM pass, DMARC fail"

1. Your email provider signs DKIM with its own domain

Symptom: dkim=pass header.d=yourprovider.com while header.from=acme.com.

Most providers sign every message with their own domain by default, so it passes DKIM even before you've configured anything. That signature can never align with your From domain.

Fix: set up domain authentication (sometimes called "custom DKIM" or "sender authentication") in your provider. They'll give you CNAME or TXT records like s1._domainkey.acme.com. Publish them, verify in the provider's dashboard, and new mail will be signed with d=acme.com. Once DKIM aligns, DMARC passes even if SPF never does.

2. The envelope sender is your provider's bounce domain

Symptom: spf=pass [email protected].

SPF passes because your provider's servers are authorized to send for their own bounce domain. It doesn't align because that domain isn't yours.

Fix: configure a custom envelope domain, often called a "custom MAIL FROM" or "custom Return-Path" (for example bounces.acme.com), with the SPF and MX records your provider asks for. Or rely on aligned DKIM from fix #1: DMARC only needs one of the two to align. Most teams fix DKIM first because it also survives forwarding (see #5).

3. Strict alignment with a subdomain

Symptom: everything is on your domain, signed d=mail.acme.com, From is [email protected], and DMARC still fails.

Fix: check your DMARC record for adkim=s or aspf=s. With strict alignment, mail.acme.com isn't acme.com. Remove the tags (relaxed is the default), or send from the exact domain you sign with.

4. The From domain isn't the one you set up

Symptom: you authenticated acme.com, but the failing email says header.from=acme.io or header.from=app-acme.com.

This happens when a framework default, an old environment variable or a second app sends with a different From address. DMARC is evaluated against whatever the From header says, not what you meant.

Fix: search your code and config for every From address (EMAIL_FROM, MAIL_FROM_ADDRESS, DEFAULT_FROM_EMAIL, framework mailer configs) and make them all use the authenticated domain.

5. The email was forwarded

Symptom: it passes when sent to you directly, and fails for recipients who forward mail from another address, such as a university or old work mailbox forwarding to Gmail.

When a server forwards a message, it re-sends it from its own IP, so SPF fails at the final destination. DKIM usually survives because the signature travels with the message, unless the forwarder modifies the content (mailing lists that add footers are the usual culprit).

Fix: you can't control the forwarder, which is exactly why aligned DKIM matters more than aligned SPF. With fix #1 in place, forwarded mail keeps passing DMARC through DKIM. Some receivers also honor ARC headers added by well-behaved forwarders.

How to roll out DMARC without breaking your own mail

If you're setting DMARC up for the first time, or tightening it, go in this order:

  1. Start with p=none and reporting: v=DMARC1; p=none; rua=mailto:[email protected]. Nothing gets blocked; you just receive daily aggregate reports.
  2. Read the reports for two to four weeks. They list every source sending as your domain, and whether it passed SPF and DKIM alignment. You'll find the forgotten senders: the CRM, the helpdesk, the invoicing tool.
  3. Fix alignment for every legitimate source, usually by setting up custom DKIM in each tool.
  4. Move to p=quarantine, optionally with pct= to apply it to a percentage of mail first.
  5. Move to p=reject once the reports are clean.

The raw XML reports are hard to read by eye. A free report viewer or a short script that tallies the source_ip, dkim and spf results by sender makes them manageable.

The checklist

When you see "SPF pass, DKIM pass, DMARC fail":

  1. Open the raw headers and read header.from, smtp.mailfrom and the DKIM header.d.
  2. Check whether either domain shares an organizational domain with the From domain.
  3. If DKIM is signed by your provider's domain, set up custom DKIM for yours.
  4. If SPF uses the provider's bounce domain, set a custom MAIL FROM, or rely on aligned DKIM.
  5. Look for adkim=s / aspf=s in your DMARC record if you send from subdomains.
  6. Find every From address your apps use, and make sure they're all on the authenticated domain.
  7. Remember forwarding breaks SPF but usually not DKIM, so aligned DKIM is the one to get right.

Once you see the three domains separately, this error stops being mysterious: DMARC isn't failing your checks, it's checking something neither of them looked at.

I'm building Cresca, an email platform for app email and marketing campaigns. When you add a sending domain, it gives you the DKIM, custom MAIL FROM and ownership records for your own domain, so mail is signed and sent as you rather than as us. If you've hit a DMARC failure that doesn't fit these five cases, I'd like to hear about it in the comments.

📰 Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.