How to Read DMARC Aggregate Reports (XML Explained)
DMARC aggregate reports are how mailbox providers tell you who is sending mail under your domain. They arrive as compressed XML attachments and they are dense, but they reveal exactly which sources are passing and failing your authentication policy.
What aggregate reports are
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. The report tells you, for each unique sending IP:
- How many messages arrived from that IP.
- What the SPF result was, and against which domain.
- What the DKIM result was, and against which domain.
- Whether the message aligned for DMARC.
- What action the receiver took (none, quarantine, reject).
This is the only way to discover the full set of senders using your domain. Authorised senders show up alongside forgers and forgotten SaaS tools.
The file you'll receive
Reports arrive as email attachments named like:
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, sometimes zipped. Decompress it and you have an XML document.
The XML structure
Every aggregate report follows the same schema. A simplified version:
<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>192.0.2.10</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>s1</selector>
</dkim>
<spf>
<domain>example.com</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
</feedback>
The three key sections
<report_metadata> — who sent the report and what time window it covers. Usually you ignore everything except the org_name (so you know which receiver) and the date range.
<policy_published> — what the receiver fetched from your DMARC record at the time. This is useful for debugging policy changes — if you tightened from p=quarantine to p=reject and the reports still show quarantine, your DNS hasn't propagated.
<record> — the meat of the report. One record per unique combination of sending IP and authentication result. The <count> tells you how many messages this combination represents.
What the result fields actually mean
Inside <policy_evaluated> you'll see three fields that summarise what the receiver decided:
<disposition>— what the receiver did.none= delivered as normal.quarantine= sent to spam.reject= bounced.<dkim>and<spf>— whether the message was DMARC-aligned for that mechanism. Note this is after alignment is applied, not the raw SPF/DKIM result.
Inside <auth_results> you see the raw SPF and DKIM results, and crucially the domain against which each was authenticated. 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 says example.com. Therefore DMARC alignment fails. The receiver's policy_evaluated will show dkim=fail spf=fail and the disposition will be whatever your DMARC p= says.
The patterns to look for
Pattern 1: Authorised senders failing alignment
An IP belongs to a third-party sender (Mailchimp, Salesforce, etc.) and is sending real mail for you, but neither SPF nor DKIM aligns to your From domain. Fix: configure DKIM with your domain at the ESP and update SPF.
Pattern 2: Mystery IPs
An IP from a hosting provider you don't recognise is sending mail under your domain. Two possibilities — a forgotten SaaS or actual abuse. Investigate using rDNS and WHOIS lookups on the IP.
Pattern 3: One ESP suddenly failing
A previously-aligned sender starts showing dkim=fail. Usually means a key rotation that you didn't update in DNS, or the ESP changed signing domains. Check their docs.
Pattern 4: High-volume, low-complaint forwarders
You'll see many records with spf=fail from IPs belonging to 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 isn't signing for you. Mostly harmless if DKIM still aligns; if not, you'll get a slow drip of legitimate mail being quarantined.
Tools that don't require parsing XML by hand
You can theoretically read aggregate reports manually, but at scale that is painful. The tools we recommend:
- dmarc.postmarkapp.com — free, weekly digest, very readable summary of what's 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 Email Routing & DMARC — if you already use Cloudflare, their bundled DMARC dashboard is free.
What "good" looks like
A healthy DMARC report set has these characteristics:
- One or two known sending sources, accounting for >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 don't, work source-by-source until they do. For more on the policies themselves, see our DMARC explainer.