How to Read a DMARC Aggregate Report (rua XML) — A Practical Guide
Learn how to read DMARC reports (rua XML) in minutes. Decode report metadata, records, and auth results — and spot the 4 patterns that matter.
To read a DMARC aggregate report, unzip the attachment, find the <record> elements, and match each source_ip against the senders you know. A record tells you who sent mail claiming to be your domain, how many messages they sent, and whether SPF and DKIM passed and aligned. Everything else is metadata.
That's the whole skill. The rest of this guide is learning to do it fast — and knowing when to stop doing it manually.
Your first report looks terrifying. That's normal.
You added a p=none record with an rua tag last week. This morning an email arrived from [email protected] (or Yahoo, or Fastmail) with an attachment named something like:
google.com!example.com!1748736000!1748822400.xml.gzYou unzip it. Inside: 4,000 lines of XML. No documentation. No legend. Just nested tags.
Every engineer who sets up DMARC has this exact moment. The format is dense, but it follows a rigid structure defined in RFC 7489. Once you've decoded one record, you've decoded them all.
If you haven't set your record yet, or want to confirm your rua address is formatted correctly, run your domain through the free DMARC checker first — a malformed rua tag means you get nothing at all.
What aggregate reports actually are (and aren't)
Aggregate reports (rua) are daily-ish summaries sent by mailbox providers that received mail claiming to be from your domain. Each provider — Google, Microsoft, Yahoo, and dozens of smaller ones — sends one report per 24-hour period per domain.
What they contain:
- Who sent the mail (source IP)
- How many messages from that IP
- What DMARC, SPF, and DKIM evaluated to
- What policy you published and what was applied
What they do NOT contain:
- Message content or subject lines
- Recipient addresses (mostly — some providers include the envelope-to in extensions, but don't count on it)
- Sender identities beyond the IP and any PTR/HELO hints
- Why a message landed in spam or inbox
This trips people up. Aggregate reports answer "what systems sent mail as my domain, and did authentication pass?" They cannot answer "did this specific email to this specific person get delivered?" For that you need deliverability testing, not DMARC reports.
There's also a second report type — forensic/failure reports (ruf) — which are per-message and near-real-time. Almost nobody enables them anymore because they can contain message content (a privacy problem), and Gmail never sends them. This guide is about rua reports only.
Anatomy of the XML
Every aggregate report has three sections. Let's walk through a real-world-shaped example from Google.
1. Report metadata — who sent this report and when
<report_metadata>
<org_name>google.com</org_name>
<email>[email protected]</email>
<report_id>17261234567890123456</report_id>
<date_range>
<begin>1748736000</begin>
<end>1748822400</end>
</date_range>
</report_metadata>org_name is the reporting provider. begin/end are Unix timestamps covering the reporting window — usually exactly 24 hours. Note that Google reports on a UTC-ish cycle while some providers use their own; times across reports from different orgs won't align perfectly.
2. policy_published — the DMARC record they saw
<policy_published>
<domain>example.com</domain>
<adkim>r</adkim>
<aspf>r</aspf>
<p>none</p>
<sp>none</sp>
<pct>100</pct>
</policy_published>This is the receiver's copy of your DMARC record. Check it matches what you think you published. If you recently changed p=none to p=quarantine and reports still show none, you're either looking at a pre-change window or your DNS hasn't propagated (or you have a typo). pct=100 means the policy applies to 100% of failing mail — a common gotcha when people set pct=10 during rollout and forget to remove it.
adkim=r and aspf=r mean relaxed alignment (the default). More on why that matters below.
3. Records — one per sending source
The body of the report is a series of <record> elements. Each one is a distinct source. Here's a complete record, fully annotated:
<record>
<row>
<source_ip>209.85.220.69</source_ip>
<count>48</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>google</selector>
</dkim>
<spf>
<domain>example.com</domain>
<result>pass</result>
</spf>
</auth_results>
</record>Read it top to bottom:
| Field | Meaning |
|---|---|
source_ip | The IP the receiver saw the connection from |
count | Messages from this IP claiming your domain in the window |
disposition | What the receiver did: none (delivered), quarantine (spam folder), or reject (bounced) |
policy_evaluated dkim/spf | DMARC's verdict — this is alignment, not raw auth pass |
header_from | The domain in the visible From: header |
auth_results | The underlying SPF and DKIM check results, including which domains and selectors were checked |
The single most important distinction in the whole file: `auth_results` is what passed. `policy_evaluated` is what aligned. SPF can pass for mail.esp.com while DMARC fails because the authenticated domain doesn't match your header_from domain. That mismatch — passing auth, failing alignment — is the root cause of most DMARC confusion.
The 4 patterns you'll see over and over
Ninety-plus percent of records in any domain's reports fall into one of four buckets. Learn to recognize these and you can scan a report in under a minute.
Pattern 1: Legit source, fully passing
SPF pass + aligned, DKIM pass + aligned → disposition noneYour own mail server, your ESP, your transactional provider — all correctly configured. Source IP reverse-resolves to something you recognize. count looks sane for your volume. This is the healthy baseline. Do nothing.
Pattern 2: Legit source, failing alignment
SPF pass (domain: sendgrid.net), DKIM pass (domain: sendgrid.net)
→ policy_evaluated: failThis is your ESP, sending real mail, authenticating correctly as itself — but not as you. Classic misconfiguration: you added SendGrid/Mailgun/Postmark but never set up their custom return-path (for SPF alignment) or custom DKIM signing domain.
The tell: auth_results shows the provider's domain, not yours. The fix is in the provider's dashboard — enable their "authenticated domain" / "custom DKIM" feature so they sign with d=example.com. Until you do, this traffic will get quarantined or rejected the day you move off p=none.
This is exactly the failure our SPF, DKIM, and DMARC explainer walks through from the DNS side.
Pattern 3: Forwarders and mailing lists breaking things
SPF fail (forwarder re-sent from its own IP), DKIM pass + aligned
→ policy_evaluated: pass (via DKIM)Someone at a university or company forwarded your newsletter. The forward changed the connecting IP, so SPF failed. But DKIM survived because forwarding doesn't modify the signed body — so DMARC still passed on DKIM alignment. This is why DKIM alignment matters so much, and why DMARC only needs one of the two to align.
The dangerous variant: mailing lists that rewrite subject lines or footers ([List-Name] prefixes). That breaks DKIM's body hash too. Then both fail. These records show a recognizable list-server IP, low counts, and there's little you can do about them except note them — ARC adoption is slowly improving this, but you can't control receivers' lists.
Pattern 4: Unknown senders spoofing you
source_ip in a hosting provider range, SPF fail, DKIM fail,
no PTR to anything you know → disposition none (because p=none)Random IPs in cloud/VPS ranges, sometimes in countries you don't operate in, sending mail with your exact header_from domain. This is spoofing. It happens to basically every domain — phishers spray domains indiscriminately.
Under p=none, receivers delivered it anyway. That's the point of your current policy: observe, don't block. But these records are your evidence base for enforcement, and volume matters — a handful of spoofed messages is background noise; thousands of spoofed messages hitting consumer inboxes is damaging your reputation right now.
We wrote a whole companion piece on triaging these: DMARC aggregate reports: how to investigate unknown senders. If you want unknown sources labeled automatically instead of grepping IPs yourself, WillItInbox's DMARC monitoring ingests your aggregate reports and flags sources that don't match your known infrastructure.
What p= vs sp= means when you're reading reports
Two tags in policy_published confuse everyone:
- `p=` — the policy for the domain itself (e.g.,
example.com) - `sp=` — the policy for subdomains (e.g.,
mail.example.com)
Here's the trap: if sp is absent, subdomains inherit `p`. So p=reject with no sp= also rejects all subdomain mail. People routinely enforce on the root domain and accidentally break billing.example.com and support.example.com, which were never in the rua stream as separate records... except they were — DMARC reports include a record per subdomain that appeared in a header_from, each with its own policy_published block if the subdomain has its own DMARC record.
When scanning reports, always check the <domain> in policy_published. If you see subdomain records you forgot existed (old marketing subdomains, decommissioned apps), that's inventory work to do before enforcement.
Why everyone stops reading raw XML within a week
Be honest with yourself about the math:
- Google, Microsoft, Yahoo, plus a dozen smaller receivers each send one report daily.
- That's 15–30 gzipped XML files per day, every day, forever.
- Reports arrive with 24–48h lag, so manual review is always behind.
- Nothing dedupes sources across files. You're mentally joining 30 documents to answer "is there a new unknown sender this week?"
Engineers typically do one of three things:
- Give up. The rua mailbox becomes an unread archive. (Most common.)
- Write a parser. A weekend script using Python's
xml.etreeor a library likeparsedmarc, dumping records into SQLite/Postgres. Works, but now you own alerting, IP enrichment, and the dashboard. - Use a DMARC report analyzer / monitoring service. Point your
ruaaddress at the service; it ingests, normalizes across providers, groups records by known/unknown source, and alerts on new senders or auth-failure spikes.
If you're going the DIY parser route, parsedmarc is genuinely good — it handles gzip/zip, pushes to Elasticsearch or Splunk, and understands provider quirks. But the failure mode of both DIY options is the same: the data exists, nobody looks at it, and the first you hear of a broken ESP integration is a support ticket about missing invoices.
The pragmatic move: use a service that does ingestion plus alerting, and keep raw reports as the archive of record. WillItInbox's DMARC monitoring ingests aggregate reports automatically, labels sources against your known sending infrastructure, and flags drift — so a new sending IP or a suddenly-failing DKIM selector surfaces as an alert, not as row 3,847 of next Tuesday's XML.
Try it: Point your rua at WillItInbox and let the reports analyze themselves → DMARC monitoring
Graduating from p=none: reading reports as rollout evidence
The entire point of suffering through p=none is to earn the data for enforcement. Here's how to use reports as your go/no-go evidence:
Step 1 — Inventory (weeks 1–2). Collect reports. Build the list of every source_ip that appears with meaningful volume. Map each to a service: your mail host, ESPs, CRM, ticketing system, payroll provider, that monitoring tool that sends alerts.
Step 2 — Fix alignment failures. Every Pattern 2 record (legit source, alignment fail) gets fixed: custom DKIM signing domain, correct SPF include, verified return-path. Re-check with the DMARC checker after DNS changes.
Step 3 — Watch for a clean window. You want 2+ weeks where every non-trivial source in reports either passes DMARC alignment or is a known forwarder/noise case. "Non-trivial" is judgment — a source sending 3 messages a month from a list server is not a blocker.
Step 4 — Move to p=quarantine (optionally with pct). p=quarantine; pct=25 if you're cautious, straight to pct=100 if your traffic is simple. Watch reports — you now also see disposition values of quarantine, confirming enforcement behavior per source.
Step 5 — p=reject. Same evidence loop. Once quarantine shows no legit traffic being junked, flip to reject. Spoofed mail now bounces at the receiver.
The full decision tree, including the pct= rollout mechanics and the sp= subdomain trap, is in DMARC rollout: from none to reject without breaking email. Don't skip it — the enforcement step is where all the self-inflicted wounds happen.
One more discipline: after each policy change, run a real message through the deliverability tester. Reports confirm what receivers did yesterday; a live test confirms your authentication still passes right now, with 70+ checks across DNS, headers, and content.
Frequently asked questions
Sources reviewed
- RFC 7208: Sender Policy Framework(standard)
- RFC 7489: DMARC(standard)
Factual review: June 13, 2026 by WillItInbox Editorial.
Keep reading