How to read a DMARC report
You published a DMARC record with a rua= address on it, and now something turns up every morning: a zipped attachment from google.com, one from Microsoft, one from a hosting company in a country you don't do business in. Inside each is a single XML file that is obviously full of information and completely unreadable.
Most people open one, scroll, and never open another. That's a waste, because the aggregate report is the only view you will ever get of who is sending mail using your domain — including the people who have no business doing it.
Here's how to read one by hand. Do it twice and you'll understand what the automated parsers are telling you, and more usefully, when they're overreacting.
What's in the file
Strip the tags away and every aggregate report is three things: who wrote it, what policy they found for you, and one block per sending source.
The top is <report_metadata> — <org_name> for the reporting provider, a <report_id>, and a <date_range> whose <begin> and <end> are Unix timestamps. Most reports cover 24 hours.
Then <policy_published>, which echoes back the DMARC record the receiver actually read: <domain>, <p>, the subdomain policy <sp>, and the alignment modes <adkim> and <aspf>. Read this section first, every time. If it says <p>none</p> and you were certain you'd moved to quarantine last month, you have a DNS problem, not a mail problem, and nothing else in the file matters until you fix it.
Everything after that is a series of <record> blocks. That's the part worth your attention.
One record, translated
<record>
<row>
<source_ip>192.0.2.47</source_ip>
<count>100</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>fail</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.com</domain>
<selector>s1</selector>
<result>pass</result>
</dkim>
<spf>
<domain>example.com</domain>
<result>fail</result>
</spf>
</auth_results>
</record>
Read it as a sentence. The host 192.0.2.47 sent 100 messages showing example.com in the From line the recipient sees. DKIM verified and aligned; SPF did not. DMARC needs only one of the two to pass, so the receiver applied no penalty — <disposition>none</disposition>.
Two things to hold onto. First, <count> is the number of messages that shared that exact combination of source and result during the window, which is why one row says 100 and the next says 1. Second, SPF and DKIM appear twice for a reason. <auth_results> is the raw check — did the signature verify, did the connecting IP match the SPF record. <policy_evaluated> is the DMARC verdict after alignment against <header_from>. Seeing pass in one and fail in the other for the same message is normal, not a contradiction, and not a bug in the report.
There may also be a <reason> element inside <policy_evaluated> with a <type> such as trusted_forwarder, mailing_list, or local_policy. That is the receiver telling you it chose not to apply your policy, and why.
Here's the part the parsers get wrong
A row appears: 100 messages, an IP you don't recognize, SPF failed, disposition none. Every dashboard will paint it red. Most of the time it is not an attack.
The tell is which check failed. When a message is forwarded — a customer's info@ alias pointing at their personal Gmail, a mailing list, a university relay — the forwarding server re-sends it from its own IP while keeping your original envelope sender. SPF fails, because that IP was never in your record and never could be. The DKIM signature, though, travels with the message and keeps verifying, as long as nothing rewrote the body or the signed headers. SPF fail, DKIM pass, on mail you actually sent, is forwarding. It is the single most common pattern in these reports and it is harmless.
Now look at what a forgery looks like. Someone spoofing your domain does not have your DKIM private key, so they cannot sign anything. Their <auth_results> will show either no <dkim> element at all, or one whose <domain> is somebody else's. SPF fails too, because they're sending from their own infrastructure. Both failing, with no DKIM selector of yours anywhere in the block, is the row to care about — especially at volume, from a source that never appears again.
And there's a third pattern people misread as an attack when it's really a bill they forgot to pay: SPF passes, DKIM fails or is absent, source is a marketing or invoicing platform. That's a service you legitimately signed up for, added to SPF, and never turned on signing for. Nobody is attacking you. You just did half the setup.
What to do
- Read
<policy_published>first and confirm it matches the record you think you published. - Sort the records by
<count>. Ninety percent of your volume comes from a handful of sources you'll recognize immediately. - For anything failing, check
<auth_results>before you panic. Your own DKIM selector present and passing means forwarding. Your selector absent, from a source you don't know, at volume, means forgery. - Fix the legitimate senders — signing enabled, alignment correct — until your known sources are clean.
- Only then tighten
p=nonetoquarantine, and laterreject. Enforce while you still have unexplained passing traffic and you will block your own invoices.
Once your reports are boring, they stay useful: a new red row in a quiet report means something actually changed.
Not sure your DMARC record is even publishing a report address? The free email check reads it back to you in seconds.
Need this kind of thinking applied to your own setup? Get in touch →