Back to Articles

How to Read a DMARC Report (Aggregate XML, Explained Simply)

August 12, 2026 8 min read DNSLens Team
Email Security DMARC Reporting

How to Read a DMARC Report (Aggregate XML, Explained Simply)

You published a DMARC record, added rua=mailto:you@example.com, and now your inbox is filling with .xml (or .xml.gz) attachments from Google, Microsoft, Yahoo, and others. They look like this:

<record>
  <row>
    <source_ip>203.0.113.10</source_ip>
    <count>42</count>
    <policy_evaluated>
      <disposition>none</disposition>
      <dkim>pass</dkim>
      <spf>pass</spf>
    </policy_evaluated>
  </row>
  ...
</record>

Dense, but not complicated once you know what you're looking at. This guide breaks a DMARC aggregate report down field by field so you can tell who is sending mail as your domain, whether they pass authentication, and what DMARC would do about it.

If you'd rather not read raw XML, upload the file to our DMARC Report Analyzer and it will parse everything below for you. But understanding the fields makes the parsed output far more useful.

💡 First, what kind of report is this?

DMARC produces two report types. Aggregate reports (RUA) are the daily XML summaries this guide covers — statistics, no message content. Forensic reports (RUF) are per-message samples and are rarely sent today for privacy reasons. When people say "DMARC reports," they almost always mean aggregate/RUA.

The Shape of an Aggregate Report

Every aggregate report has two parts: a header describing who sent it and the policy they saw, and a set of records, one per sending source.

The report header (report_metadata + policy_published)

<report_metadata>
  <org_name>google.com</org_name>
  <email>noreply-dmarc-support@google.com</email>
  <report_id>1234567890</report_id>
  <date_range>
    <begin>1754870400</begin>
    <end>1754956800</end>
  </date_range>
</report_metadata>
<policy_published>
  <domain>example.com</domain>
  <p>none</p>
  <sp>none</sp>
  <pct>100</pct>
</policy_published>
Field Meaning
org_name Who generated the report (the receiving provider, e.g. Google)
date_range The 24-hour window, as Unix timestamps
p The policy you published for the domain (none / quarantine / reject)
sp The policy for subdomains
pct The percentage of mail the policy was applied to

The header confirms the receiver saw the DMARC policy you think you published. If p here doesn't match your intended policy, your DNS record is wrong — verify it with the DMARC checker.

The records (one block per sending IP)

This is the important part. Each <record> describes one source IP that sent mail claiming to be from your domain, how many messages it sent, and how they authenticated.

<record>
  <row>
    <source_ip>203.0.113.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>
    <spf>
      <domain>example.com</domain>
      <result>pass</result>
    </spf>
    <dkim>
      <domain>example.com</domain>
      <selector>s1</selector>
      <result>pass</result>
    </dkim>
  </auth_results>
</record>

Field-by-Field: What Actually Matters

source_ip — the IP that sent the mail. Your job is to recognize it. Is it your email provider? A marketing platform you use? Or something you don't recognize (a potential spoofer)?

count — how many messages this IP sent in the window. A big count from an unknown IP is a red flag; a big count from your known provider is normal.

policy_evaluated — what DMARC decided:

  • disposition: the action taken — none (delivered), quarantine (spam folder), or reject (blocked). This reflects your published p.
  • dkim / spf here are the aligned results (see below), not raw pass/fail.

auth_results — the raw SPF and DKIM checks: which domain was authenticated and whether it passed. The gap between raw results here and the aligned results in policy_evaluated is where most confusion lives.

The Concept That Trips Everyone Up: Alignment

A message passes DMARC if it passes SPF or DKIM and that check is aligned. Alignment means the domain that authenticated matches the domain in the visible From: header.

💡 Pass ≠ aligned

A message can pass SPF (the sending server is authorized) yet fail DMARC because the SPF domain doesn't match the From: domain. This is why you'll see spf: pass in auth_results but spf: fail in policy_evaluated. We cover this in depth in DMARC alignment explained.

So when you read a record, check three things in order:

  1. Did it pass DMARC? Look at policy_evaluated → at least one of dkim/spf is pass.
  2. If it failed, why? Compare auth_results (raw) against policy_evaluated (aligned). Raw pass + aligned fail = an alignment problem, usually a legitimate sender using their own domain in Return-Path or DKIM signature.
  3. Do you recognize the IP? Failing + unknown IP = possible spoofing. Failing + known IP = a legitimate service you need to authenticate properly.

A Practical Reading Workflow

For each report, sort the records into three buckets:

Bucket What you see What to do
✅ Legitimate & aligned Known IP, DMARC pass Nothing — this is healthy
⚠️ Legitimate but failing Known service, DMARC fail Fix its SPF include or DKIM signing so it aligns
🚩 Unknown & failing Unrecognized IP, DMARC fail Likely spoofing — this is what DMARC protects against

The goal over time: get every legitimate source into the "pass" bucket, then tighten your policy from p=none to p=quarantine to p=reject. Don't move to reject while legitimate senders are still failing — you'll block your own mail.

⚠️ Don't enforce blind

Set p=none first and read reports for a few weeks. Only raise the policy once your reports show all real senders passing DMARC. Enforcing too early quarantines or rejects legitimate email.

Reading Reports at Scale

One domain with a couple of senders is readable by hand. But a busy domain generates dozens of reports a day across many IPs — reading raw XML stops scaling fast. That's what parsers are for: they aggregate every report, group by sending source, and show you pass/fail trends over time instead of one 24-hour snapshot.

Upload your .xml or .xml.gz files to the DMARC Report Analyzer to see the sources, counts, and alignment results laid out — no XML required.

🚀 Next Steps

Frequently Asked Questions

How do I read a DMARC report? Open the aggregate XML, check policy_published to confirm your policy, then read each <record>: identify the source_ip, its message count, and the policy_evaluated result. Compare raw auth_results with the aligned result to understand failures.

Where do DMARC reports come from? Receiving mail providers (Google, Microsoft, Yahoo, etc.) generate them and send them to the address in your DMARC record's rua= tag, once per day per domain.

Why does SPF pass but DMARC fail? Because of alignment — SPF authenticated a domain that doesn't match your visible From: domain. DMARC requires an aligned pass. See DMARC alignment explained.

What's the difference between RUA and RUF reports? RUA (aggregate) are daily statistical summaries with no message content — the ones covered here. RUF (forensic) are per-message samples and are rarely sent today.

Key Takeaways

  • ✅ Aggregate (RUA) reports are daily XML statistics — no message content.
  • ✅ The header confirms which policy the receiver saw; the records show each sending source.
  • ✅ For each source, read source_ip, count, and policy_evaluated.
  • Pass ≠ aligned — compare raw auth_results with aligned policy_evaluated to explain failures.
  • ✅ Sort sources into legitimate-aligned, legitimate-failing, and unknown-failing, then fix the middle bucket before enforcing.
  • ✅ Use a parser once you're past a handful of reports a day.

Still setting up email authentication? Start with SPF vs DKIM vs DMARC, build your record with the SPF record generator, validate it with the SPF checker, and read DNS TXT records: why one typo breaks everything.