How to check DMARC record
Query TXT on the _dmarc label of the domain: nslookup -type=TXT _dmarc.github.com 8.8.8.8. The answer starts with v=DMARC1 and carries the policy in p=. Read sp= as well, because subdomains follow it and it is often stricter than p=.
Why check this
Run this before any release that sends mail from a new subdomain, and again after a DNS cutover. The policy decides what a receiver does with a message that fails authentication, so it is the difference between a broken sending path that degrades quietly and one that loses transactional mail outright. A staging system that starts sending as mail.staging.example.com inherits whatever the parent domain publishes, which can be reject.
Two facts in the record are usually skipped and both change the answer. sp= governs subdomains independently of p=. And a rua= address on another domain needs that domain's permission before any report is sent, so a policy can be live for months while you receive nothing and conclude nobody is spoofing you.
Prerequisites
- RFC 7489 for the tag list and the discovery rules.
- A resolver named in the command, so the answer does not depend on the network you are on.
- Node 22 for steps 2 to 4. Save this as
dmarc-record.js:
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
(async () => {
const d = process.argv[2];
let at = '_dmarc.' + d;
let found = (await txt(at)).filter(s => s.startsWith('v=DMARC1'));
if (!found.length && org(d) !== d) { // RFC 7489 organizational domain fallback
at = '_dmarc.' + org(d);
found = (await txt(at)).filter(s => s.startsWith('v=DMARC1'));
console.log('nothing at _dmarc.' + d + ', falling back to the organizational domain');
}
console.log('answered at : ' + at);
console.log('DMARC records : ' + found.length + (found.length === 1 ? '' : ' <-- must be exactly 1'));
if (found.length !== 1) return;
const rec = found[0];
const t = Object.fromEntries(rec.split(';').map(s => s.trim().split('='))
.filter(p => p.length === 2).map(([k, v]) => [k.toLowerCase(), v]));
console.log('p : ' + (t.p || 'MISSING, the tag is required'));
console.log('sp (subdomains) : ' + (t.sp || 'not set, subdomains inherit p=' + t.p));
console.log('pct : ' + (t.pct || '100 (default)'));
console.log('aspf / adkim : ' + (t.aspf || 'r (default)') + ' / ' + (t.adkim || 'r (default)'));
console.log('rua : ' + (t.rua || 'none, no aggregate reports will arrive'));
console.log('ruf : ' + (t.ruf || 'none'));
for (const dest of [t.rua, t.ruf].filter(Boolean).join(',').split(',')) {
const host = dest.trim().replace(/^mailto:/, '').split('@')[1];
if (!host || org(host) === org(d)) continue; // same org domain needs no authorization
const auth = await txt(d + '._report._dmarc.' + host);
console.log('external report : ' + d + '._report._dmarc.' + host +
(auth.length ? ' = ' + auth.join(' ') : ' = MISSING, reports are not sent'));
}
console.log('record : ' + rec);
})();
Steps
- Step 1.
Read the record straight from a named resolver.
nslookup -type=TXT _dmarc.github.com 8.8.8.8Non-authoritative answer: Server: dns.google Address: 8.8.8.8 _dmarc.github.com text = "v=DMARC1; p=quarantine; sp=reject; pct=100; rua=mailto:dmarc@github.com; ruf=mailto:dmarc@github.com; fo=1"The label is
_dmarcon the From domain. There is no such thing as a DMARC record on the bare domain. - Step 2.
Split the record into tags, with the defaults filled in.
node dmarc-record.js github.comanswered at : _dmarc.github.com DMARC records : 1 p : quarantine sp (subdomains) : reject pct : 100 aspf / adkim : r (default) / r (default) rua : mailto:dmarc@github.com ruf : mailto:dmarc@github.com record : v=DMARC1; p=quarantine; sp=reject; pct=100; rua=mailto:dmarc@github.com; ruf=mailto:dmarc@github.com; fo=1p=quarantineandsp=rejecttogether mean the organizational domain is at spam-folder enforcement while every subdomain is at full rejection. Readingpalone gets a subdomain test wrong. - Step 3.
Check a subdomain, which usually has no record of its own.
node dmarc-record.js blog.cloudflare.comnothing at _dmarc.blog.cloudflare.com, falling back to the organizational domain answered at : _dmarc.cloudflare.com DMARC records : 1 p : reject sp (subdomains) : reject pct : 100 aspf / adkim : r / r rua : mailto:a1c47f179bc04efd8ee4dcd4d85dfc65@dmarc-reports.cloudflare.net,mailto:rua@cloudflare.com ruf : none external report : blog.cloudflare.com._report._dmarc.dmarc-reports.cloudflare.net = v=DMARC1; record : v=DMARC1; p=reject; sp=reject; adkim=r; aspf=r; pct=100; rua=…,mailto:rua@cloudflare.comAn empty answer at the subdomain is not "no DMARC". The receiver walks up to the organizational domain and applies
sp=from there. - Step 4.
Verify that every external report address has authorised this domain.
node dmarc-record.js mozilla.organswered at : _dmarc.mozilla.org DMARC records : 1 p : reject sp (subdomains) : not set, subdomains inherit p=reject pct : 100 aspf / adkim : r / r rua : mailto:dmarc_agg@vali.email,mailto:dmarc@mozilla.com ruf : none external report : mozilla.org._report._dmarc.vali.email = v=DMARC1; external report : mozilla.org._report._dmarc.mozilla.com = v=DMARC1; record : v=DMARC1; p=reject; pct=100; adkim=r; aspf=r; rua=mailto:dmarc_agg@vali.email,mailto:dmarc@mozilla.comBoth destinations sit outside
mozilla.org, includingmozilla.com, so both need an authorisation record and both have one. - Step 5.
Find out whether that authorisation is specific to you or a wildcard.
node -e "const{Resolver}=require('dns');const r=new Resolver();r.setServers(['8.8.8.8']);r.resolveTxt('zzz-no-such-domain-42.example._report._dmarc.vali.email',(e,x)=>console.log(e?e.code:x.flat().join(' ')))"v=DMARC1;A name that cannot exist gets the same answer. The report host publishes a wildcard, so a pass on step 4 proves the host accepts reports for anyone, not that somebody set your domain up correctly.
How to read the result
| What you see | What it means | What to do |
| --- | --- | --- |
| p=none | Reporting only, nothing is enforced | Fine while you gather data. It stops no spoofing. |
| p=quarantine | Failing mail goes to the spam folder | Check sp= before assuming subdomains behave the same way. |
| p=reject | Failing mail is refused at the gateway | Every sending system needs an aligned pass first. See How to check DMARC alignment. |
| p : MISSING | The record has no valid policy | With a valid rua, RFC 7489 section 6.6.3 has receivers act as if it said p=none and keep reporting. Without one, they skip DMARC for the message. Neither enforces anything. |
| pct=20 | One message in five gets the policy | Under reject, the other four are quarantined. Under quarantine, they get the receiver's normal filtering. Test results will look random. |
| rua : none | No aggregate reports are produced | Add one before moving off p=none, or the move is blind. |
| DMARC records : 2 | The policy is ignored | Two v=DMARC1 records are a discovery failure. Remove one. |
Common mistakes
What to check next
- How to check DMARC alignment: the policy above only bites when SPF and DKIM fail to align, which is the part that surprises people.
- How to check SPF record: one of the two inputs the policy judges.
- How to check DKIM record: the other input, and the one that survives a forwarded message.
- How to check txt record of a domain: the underlying record type and its multi-string behaviour.
- Email testing checklist: where this read belongs in a release.
FAQ
How to check a DMARC record using nslookup?
nslookup -type=TXT _dmarc.example.com 8.8.8.8. Keep the _dmarc label and the trailing resolver argument. Without the label the query returns the domain's ordinary TXT set and no policy.
How to check DMARC policy for a subdomain?
Query the subdomain's own _dmarc label first. If nothing answers, query the organizational domain and read sp= there, because that is the tag a receiver applies to the subdomain.
How to check if DMARC is enabled for a domain?
A record alone is not enforcement. p=none publishes a policy that changes no delivery decision. DMARC is enforced when p or the relevant sp is quarantine or reject and pct is 100.
How to check a BIMI record?
Query TXT on default._bimi.example.com. Receivers only look at it when the domain is already at p=quarantine or p=reject, so check the DMARC policy first or the BIMI record cannot take effect.
What does the fo tag do?
It selects when failure reports are generated. fo=1 asks for a report whenever any underlying check fails, rather than only when both do. It has no effect on delivery.
Verified
Verified by Maks Vernynslookup Windows 10.0.22631node 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
basic5 minpublished updated Maks Verny