How to Read DMARC Aggregate Reports (XML, Demystified)
DMARC aggregate reports are the only mechanism that exposes who is sending mail under your domain — legitimate or not. They land in your mailbox as compressed XML, dense and a little intimidating, but every authentication outcome you care about is in there. This guide walks the format and the patterns we look for first.
What aggregate reports actually contain
When you publish a DMARC record with a rua=mailto: tag, every receiver that supports DMARC will, once a day, send you an XML report summarising every message they received that claimed to be from your domain. For each unique sending IP, the report tells you:
- How many messages arrived from that IP.
- The SPF result, and the domain it was authenticated against.
- The DKIM result, and the domain it was authenticated against.
- Whether the message satisfied DMARC alignment.
- What the receiver actually did (none, quarantine, reject).
This is the only way to enumerate the full set of senders using your domain. Authorised senders sit alongside forgers and the SaaS tools you forgot existed.
The file you receive
Reports arrive as email attachments named in a fixed pattern:
google.com!example.com!1714435200!1714521600.xml.gz
The filename encodes:
- The reporter (here,
google.com). - The domain being reported on.
- Two Unix timestamps: the start and end of the report window.
The file is usually gzipped, occasionally zipped. Decompress and you have an XML document.
The XML schema
Every aggregate report follows the same shape. A simplified record:
<feedback>
<report_metadata>
<org_name>google.com</org_name>
<email>noreply-dmarc-support@google.com</email>
<report_id>14739184650837423891</report_id>
<date_range>
<begin>1714435200</begin>
<end>1714521600</end>
</date_range>
</report_metadata>
<policy_published>
<domain>example.com</domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>reject</p>
<sp>reject</sp>
<pct>100</pct>
</policy_published>
<record>
<row>
<source_ip>198.51.100.20</source_ip>
<count>42</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.com</domain>
<result>pass</result>
<selector>k1</selector>
</dkim>
<spf>
<domain>example.com</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
</feedback>
The three sections that matter
<report_metadata> — who sent the report and the time window it covers. In practice you only need the org_name (so you know which receiver is talking to you) and the date range.
<policy_published> — what the receiver fetched from your DMARC record at the time. Useful for debugging policy drift — if you tightened from p=quarantine to p=reject last week and the reports still show quarantine, your DNS has not propagated.
<record> — the substance. One record per unique combination of sending IP and authentication outcome. The <count> field tells you how many messages that combination represents.
What the result fields actually mean
Inside <policy_evaluated> three fields summarise the receiver's decision:
<disposition>— what the receiver did.nonemeans delivered as normal.quarantinemeans sent to spam.rejectmeans bounced.<dkim>and<spf>— whether the message was DMARC-aligned for that mechanism. Note these are the post-alignment results, not raw SPF/DKIM verdicts.
Inside <auth_results> you see the raw SPF and DKIM verdicts and, crucially, the domain each was authenticated against. This is where alignment failures become visible. A typical pattern from a forgotten SaaS sender:
<auth_results>
<dkim>
<domain>sendgrid.net</domain>
<result>pass</result>
</dkim>
<spf>
<domain>sendgrid.net</domain>
<result>pass</result>
</spf>
</auth_results>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
SPF and DKIM both pass — but for sendgrid.net, not example.com. The visible From header reads example.com. So DMARC alignment fails. The receiver's policy_evaluated will show dkim=fail spf=fail and the disposition will be whatever your p= says.
The patterns we look for first
Pattern 1: authorised senders that are not aligned
The IP belongs to a third-party ESP (Mailchimp, Salesforce, etc.) and is sending real mail for you, but neither SPF nor DKIM aligns to your From domain. Fix: configure DKIM under your domain at the ESP and update SPF.
Pattern 2: unknown IPs
An IP from a hosting provider you do not recognise sends mail under your domain. Two possibilities: a forgotten SaaS or actual abuse. Investigate using rDNS and WHOIS on the IP.
Pattern 3: a previously aligned sender suddenly failing
A sender that used to align starts showing dkim=fail. Usually means a key rotation that you did not update in DNS, or the ESP changed its signing domain. Check their changelog.
Pattern 4: high-volume, low-complaint forwarders
You will see many records with spf=fail from IPs at large free-mail providers. These are almost always forwarding to a personal address: a user has set up important@example.com to forward to their gmail.com, and the forwarder is not signing for you. Mostly harmless if DKIM still aligns; if not, you will see a slow drip of legitimate mail being quarantined.
Tools that read XML for you
You can technically read aggregate reports by hand, but at any volume that becomes painful. The tools we recommend:
- dmarc.postmarkapp.com — free, weekly digest, very readable summary of what is failing and why.
- dmarcian.com — commercial, but the free tier is generous and the UI is unmatched. Best for organisations with multiple domains.
- parsedmarc — open-source Python tool that ingests reports from an IMAP mailbox, parses them and ships to Elasticsearch. We use this internally.
- EasyDMARC — another commercial option, particularly good at flagging unknown senders.
- Cloudflare DMARC dashboard — if you already use Cloudflare, the bundled DMARC view is free.
What "good" looks like
A healthy report set has these characteristics:
- One or two known sending sources accounting for over 99% of volume.
- Both SPF and DKIM aligned (passing for your domain) on virtually every message.
- A long tail of forwarder failures — expected, not a problem.
- Occasional unknown sources, mostly forgers, all being rejected by your
p=rejectpolicy.
If your reports look like that, you can tighten alignment to strict, narrow your SPF, and otherwise leave the policy alone. If they do not, work source-by-source until they do. For more on the policies themselves, see our DMARC explainer.