How to check DMARC alignment
Compare three domains from one message: the From: header, the Return-Path that SPF authenticated, and the d= tag of the DKIM signature. DMARC passes only when one of the latter two matches the From: domain under the mode in aspf= or adkim=. Neither has to equal the other.
Why check this
Run this the first time a service sends production mail through a new provider, and again whenever the provider changes the bounce domain. The failure it catches is the one that reads as impossible in a ticket: SPF passes, DKIM passes, DMARC fails, and the receipt never arrives. Every individual check is green because each authenticates the provider, and DMARC is the only one that asks whether either of them authenticated the domain in the From: header.
Alignment is a comparison, not a lookup, so a checker that reports "SPF: pass, DKIM: pass" without printing the three domains has not answered the question. The procedure below prints all three next to the record that judges them.
Prerequisites
- Node 22 and a copy of a real message. Export it from the mailbox as
.emlwith headers, not as a forward. - RFC 7489 section 3.1 for the definition of identifier alignment.
dmarc-record.jsfrom How to check DMARC record, used in steps 2 and 5.- Two saved fixtures for this walkthrough.
esp.emlis a message sent through a provider whose bounce domain was left at the default:
Return-Path: <bounces+4821-9f2a@sendgrid.net>
Received: from o1.sendgrid.net (o1.sendgrid.net [167.89.12.34])
by mx.local (Postfix) with ESMTPS id 4C2F1A9
for <qa@h2check.test>; Fri, 11 Sep 2026 09:14:02 +0000
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=sendgrid.net;
s=smtpapi; h=from:subject:to; bh=47DEQpj8HBSa+/TImW+5JCeuQeR=;
b=Zk9m2Qw1Jt7pR0sXbL4hGf8nCvY6aUeD3iK5oM1rT2wH
From: Example Billing <billing@example.com>
To: qa@h2check.test
Subject: Invoice 2026-0914
Message-ID: <20260911091402.4C2F1A9@sendgrid.net>
Your invoice is attached.
- And
subdomain.eml, the same message sent from a subdomain of theFrom:domain:
Return-Path: <bounce-77@mail.example.com>
Received: from mail.example.com (mail.example.com [93.184.216.34])
by mx.local (Postfix) with ESMTPS id 71B03C4
for <qa@h2check.test>; Fri, 11 Sep 2026 09:16:40 +0000
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=mail.example.com;
s=s1; h=from:subject:to; bh=47DEQpj8HBSa+/TImW+5JCeuQeR=;
b=Qb3nW8vL1yE6tA0dKpXcR5fJ2mHs9uZ4gN7iO1rM3wT
From: Example Billing <billing@example.com>
To: qa@h2check.test
Subject: Invoice 2026-0915
Message-ID: <20260911091640.71B03C4@mail.example.com>
Your invoice is attached.
- The evaluator, saved as
dmarc-align.js. It reads the three identifiers from the file and the alignment modes from live DNS:
const fs = require('fs');
const { Resolver } = require('dns');
const r = new Resolver();
r.setServers(['8.8.8.8']); // name the resolver: Node's default is not always usable
const txt = n => new Promise(ok => r.resolveTxt(n, (e, recs) => ok(e ? [] : recs.map(c => c.join('')))));
const org = d => d.split('.').slice(-2).join('.'); // two labels; the real rule is the Public Suffix List
const unfold = raw => raw.replace(/\r?\n[ \t]+/g, ' ').split(/\r?\n/);
const head = (lines, name) =>
(lines.find(l => l.toLowerCase().startsWith(name + ':')) || '').slice(name.length + 1).trim();
(async () => {
const lines = unfold(fs.readFileSync(process.argv[2], 'utf8').split(/\r?\n\r?\n/)[0]);
const from = head(lines, 'from').match(/@([^>\s]+)/)[1].toLowerCase(); // the identity DMARC protects
const env = head(lines, 'return-path').match(/@([^>\s]+)/)[1].toLowerCase(); // the identity SPF authenticates
const dkim = head(lines, 'dkim-signature').match(/[;\s]d=([^;\s]+)/)[1].toLowerCase();
const rec = (await txt('_dmarc.' + from)).find(s => s.startsWith('v=DMARC1')) ||
(await txt('_dmarc.' + org(from))).find(s => s.startsWith('v=DMARC1'));
const t = Object.fromEntries(rec.split(';').map(s => s.trim().split('='))
.filter(p => p.length === 2).map(([k, v]) => [k.toLowerCase(), v]));
const row = (label, id, mode) => {
const strict = id === from; // adkim=s / aspf=s: the domains must be identical
const relaxed = org(id) === org(from); // adkim=r / aspf=r: same organizational domain is enough
const ok = mode === 's' ? strict : relaxed;
console.log(label.padEnd(5) + id.padEnd(21) + 'org=' + org(id).padEnd(15) +
'relaxed=' + String(relaxed).padEnd(6) + 'strict=' + String(strict).padEnd(6) +
'mode=' + mode + ' -> ' + (ok ? 'ALIGNED' : 'NOT ALIGNED'));
return ok;
};
console.log('From header domain : ' + from + ' org=' + org(from));
console.log('DMARC record : ' + rec);
console.log('');
const spfOk = row('SPF', env, t.aspf || 'r');
const dkimOk = row('DKIM', dkim, t.adkim || 'r');
console.log('');
console.log('DMARC = ' + (spfOk || dkimOk ? 'pass' : 'fail') + '; policy in force: p=' + (t.p || 'none'));
})();
Steps
- Step 1.
Confirm that SPF really does pass for the envelope domain in
esp.eml.node -e "const{Resolver}=require('dns');const r=new Resolver();r.setServers(['8.8.8.8']);r.resolveTxt('sendgrid.net',(e,x)=>{const s=x.map(c=>c.join('')).find(t=>t.startsWith('v=spf1'));console.log(s.split(' ').filter(t=>t.startsWith('ip4:167')).join(' '))})"ip4:167.89.0.0/17The
Receivedheader names167.89.12.34as the connecting address, and that address is inside167.89.0.0/17. SPF passes, forsendgrid.net. - Step 2.
Read the alignment modes the
From:domain publishes.node dmarc-record.js example.comanswered at : _dmarc.example.com DMARC records : 1 p : reject sp (subdomains) : reject pct : 100 (default) aspf / adkim : s / s rua : none, no aggregate reports will arrive ruf : none record : v=DMARC1;p=reject;sp=reject;adkim=s;aspf=saspf=sandadkim=sare strict. The reader for this step is in How to check DMARC record. - Step 3.
Evaluate the provider message.
node dmarc-align.js esp.emlFrom header domain : example.com org=example.com DMARC record : v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s SPF sendgrid.net org=sendgrid.net relaxed=false strict=false mode=s -> NOT ALIGNED DKIM sendgrid.net org=sendgrid.net relaxed=false strict=false mode=s -> NOT ALIGNED DMARC = fail; policy in force: p=rejectThis is the result the ticket calls impossible. SPF passed in step 1 and the signature is valid, yet both identifiers say
sendgrid.netwhile the reader seesexample.com. Underp=rejectthe message is refused. - Step 4.
Evaluate the subdomain message, where relaxed and strict disagree.
node dmarc-align.js subdomain.emlFrom header domain : example.com org=example.com DMARC record : v=DMARC1;p=reject;sp=reject;adkim=s;aspf=s SPF mail.example.com org=example.com relaxed=true strict=false mode=s -> NOT ALIGNED DKIM mail.example.com org=example.com relaxed=true strict=false mode=s -> NOT ALIGNED DMARC = fail; policy in force: p=rejectrelaxed=true strict=falseon both rows. The same message would pass under a record that setsaspf=r, and fails here only because this domain choses. - Step 5.
Confirm that the mode is a per-domain choice, not a constant.
for d in example.com cloudflare.com github.com; do echo -n "$d "; node dmarc-record.js $d | grep 'aspf /'; doneexample.com aspf / adkim : s / s cloudflare.com aspf / adkim : r / r github.com aspf / adkim : r (default) / r (default)Two of the three are relaxed, one by default. A verdict copied from another domain's record is worth nothing.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| Both rows ALIGNED | DMARC passes | Nothing. One aligned pass is enough. |
| SPF not aligned, DKIM aligned | DMARC passes | Normal for provider mail with a default bounce domain. Leave it. |
| Both not aligned, different org domains | The provider was authenticated, your domain was not | Set a custom return path and a DKIM d= on your own domain at the provider. |
| relaxed=true strict=false with mode=s | A subdomain is failing a strict record | Sign as the exact From: domain, or move the record to aspf=r. |
| relaxed=false for DKIM only | The signature is the provider's, not yours | Ask the provider for a domain-signing key and publish its selector. |
| p=none under a failing verdict | Nothing is blocked yet | Fix alignment before moving to quarantine, not after. |
Common mistakes
What to check next
- How to check DMARC record: the record that supplies
aspf,adkimand the policy applied to a failure. - How to check SPF record: the record behind the envelope identity in step 1.
- How to verify dkim signature: confirms the signature is valid before its
d=domain is worth comparing. - How to check if an email has passed spf dkim and dmarc: the same verdict as the receiver wrote it, from the
Authentication-Resultsheader. - Email testing checklist: the wider set of checks for a release that sends mail.
FAQ
Why does DMARC fail when SPF and DKIM pass?
Because both passed for a domain that is not the one in the From: header. A provider's bounce domain and a provider's signing domain each authenticate the provider. DMARC asks a different question, and step 3 shows it answering no.
What is the difference between relaxed and strict alignment?
Relaxed accepts any subdomain of the same organizational domain. Strict requires the domains to be identical. The aspf and adkim tags set them separately, and both default to relaxed when the record omits them.
Does DMARC need both SPF and DKIM to align?
No. One aligned pass is enough. DKIM is the more useful of the two, because it survives forwarding while SPF does not.
Can I check alignment without sending a message?
Only partly. The DMARC record and its modes are a DNS read, but the three identifiers come from a delivered message. Keep one known-good .eml per sending system as a fixture and re-run this after every provider change.
Verified
Verified by Maks Vernynode 22.23.2
Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.
Related on this site
intermediate8 minpublished updated Maks Verny