Every mailbox provider that supports DMARC will eventually mail you an XML file. Open it and you get a wall of angle brackets, IP addresses, and terse status codes with no explanation attached. Most guides route around this problem by pointing you at a dashboard vendor that promises to translate the mess into charts. That's fine if you only want the summary. It's useless if you need to know which sending source is failing, why, and what to fix first. This walkthrough treats DMARC aggregate report analysis as a reading skill, not a shopping decision. You'll learn to open the raw file, find the row that matters, and decide what to do about it. No dashboard required.

What a DMARC Aggregate Report XML File Actually Contains

A DMARC aggregate report is an XML document. You don't generate it. The mailbox provider that received your mail does, and it sends the report to whatever address you listed in your DMARC record's rua tag. Gmail, Microsoft 365 and Yahoo all do this. The report covers a fixed window, typically a single UTC day, and it summarizes every message that provider saw claiming to be from you.

The file almost always arrives gzip-compressed, with a filename in a fixed format: receiver!policy-domain!begin!end.xml.gz. The timestamps are Unix epoch seconds. That naming convention is worth knowing, because it lets you sort and deduplicate a mailbox full of reports before you parse a single one.

The schema itself now has its own RFC. DMARC's original specification, RFC 7489, was obsoleted in May 2026, and aggregate reporting was split out into RFC 9990. The current namespace is urn:ietf:params:xml:ns:dmarc-2.0. If a parsing guide predates that, treat its field list as incomplete.

The file has three main sections. report_metadata identifies who sent the report and when: the reporting organization, a report ID, and the date range covered. policy_published restates your own DMARC record as the receiver understood it. That means your policy (p), your subdomain policies (sp and np), and your alignment modes. It also reports whether you are in testing mode, and which discovery method the receiver used to find your record. record is where the actual data lives, and a report can contain dozens of these blocks, one per unique combination of source IP and authentication outcome.

If you've never opened one of these files directly, start there before touching any third-party tool. Email Decoded's forensic tracing chapter treats each row as a piece of evidence, not a summary statistic. That's the same evidentiary approach used when dissecting raw headers, and that mindset matters more than any dashboard.

Why Your rua Address May Never Receive Anything

One thing breaks report delivery more often than any parsing problem. If your rua address sits on a different domain from the one publishing the policy, that other domain has to authorize it in DNS. The receiver looks for a TXT record at policy-domain._report._dmarc.destination-domain containing v=DMARC1. No record, no reports: the URI is ignored outright, silently, by every conforming receiver.

This is why so many people point rua at a vendor address, wait two weeks, and conclude DMARC reporting is broken. It isn't. The authorization record just isn't there. Check that before you debug anything else.

Anatomy of the DMARC Report Row

Each <record> block has three parts: row, identifiers and auth_results. The row element carries the source IP, a count of how many messages matched, and the policy_evaluated verdict. That verdict includes the SPF and DKIM alignment result, plus what disposition (none, quarantine, reject) the receiver applied. It can also carry a reason element when the receiver overrode your policy, for a mailing list or a trusted forwarder.

identifiers gives you the header From: domain and, if present, the envelope-from domain. auth_results then lists the raw SPF and DKIM check results as the receiver's MTA saw them, independent of alignment. Note that under the current schema the DKIM selector is mandatory, not optional. That single field saves you a lookup every time a vendor rotates keys. A record looks like this in miniature:

<record>
  <row>
    <source_ip>203.0.113.42</source_ip>
    <count>1840</count>
    <policy_evaluated>
      <disposition>none</disposition>
      <dkim>pass</dkim>
      <spf>fail</spf>
    </policy_evaluated>
  </row>
  <identifiers>
    <envelope_from>mail-vendor.net</envelope_from>
    <header_from>example.com</header_from>
  </identifiers>
  <auth_results>
    <dkim>
      <domain>example.com</domain>
      <selector>s2026</selector>
      <result>pass</result>
    </dkim>
    <spf>
      <domain>mail-vendor.net</domain>
      <result>pass</result>
    </spf>
  </auth_results>
</record>

That block alone tells you a lot: 1,840 messages from one IP, DKIM aligned and passing, SPF technically passing for a different domain but failing alignment against the From: header. That's a specific, fixable problem, not a mystery.

rua vs. ruf: Aggregate Data vs. Failure Samples

The rua tag in your DMARC record requests aggregate reports, the daily XML rollups described above. The ruf tag requests failure reports, which older guides call forensic reports. RFC 9991 is the current specification for them, and it uses the term failure report. These are meant to deliver a redacted copy of an individual failing message, header and all, close to real time.

In practice, ruf support has collapsed across most large mailbox providers, largely because forwarding message content, even partial, raises privacy concerns. So aggregate reports are what you'll actually work with day to day. That's why this walkthrough focuses on rua data: it's the schema you'll actually receive, even though it tells you about aggregated patterns rather than a single message's full journey. When you need the message-level detail that ruf reports used to promise, troubleshooting DMARC failures from the Authentication-Results header on a live message is the practical substitute.

Decoding policy_evaluated vs. auth_results: Where the Real Story Lives

The single most useful skill in DMARC report parsing is comparing two blocks inside the same record: policy_evaluated and auth_results. They answer different questions. Skip that distinction, and you'll escalate false alarms as spoofing incidents.

auth_results reports the raw pass/fail outcome of SPF and DKIM checks, tied to whichever domain each mechanism actually authenticated. policy_evaluated reports whether that result aligned with your From: header domain under your DMARC policy's alignment mode, strict or relaxed. A message can pass SPF and DKIM in the raw sense and still fail DMARC evaluation, because alignment is a separate test layered on top.

Strict alignment means the two domains are identical. Relaxed alignment, the default, means they share an organizational domain. How that organizational domain gets determined changed in 2026. Receivers used to consult the Public Suffix List. The current specification defines a DNS tree walk instead, which looks for a DMARC record up the namespace rather than consulting a static list. The report tells you which method was used: the discovery_method element in policy_published reads either psl or treewalk. When a relaxed-alignment result surprises you, that field is the first thing to check.

Why the Two Blocks Can Disagree

A common false alarm looks exactly like this: policy_evaluated shows dkim=fail, while auth_results shows dkim=pass for the same message. The signature verified fine. The DKIM d= domain just doesn't align with the From: domain. That's an alignment problem, not spoofing.

Two different situations produce it, and they need different fixes. In the first, the vendor signs with a subdomain of yours and you have adkim=s published. Relaxed alignment would accept that signature; strict doesn't. In the second, the vendor signs with its own domain entirely, so no alignment mode will ever match, and the vendor has to sign with your domain instead. Both look identical in the report. Only the d= value tells you which one you're looking at. If you haven't reviewed how these mechanisms interlock, how SPF, DKIM, and DMARC actually work together is worth reading before you second-guess every failing row.

Alignment Failures That Look Like Spoofing But Aren't

Legitimate email fails DMARC even when SPF or DKIM individually pass, for one recurring reason: alignment. SPF can pass because the sending IP is authorized in someone else's SPF record, a forwarding service, for instance, while the From: header still says your domain. DKIM can pass because the signature is cryptographically valid, but signed by a domain that isn't yours or a proper subdomain.

DMARC doesn't just ask "did SPF or DKIM pass." It asks "did the domain that passed match the domain your recipient actually saw." Most tutorials that reproduce the schema verbatim never explain that layer. It's exactly why a report full of spf=fail or dkim=fail in policy_evaluated doesn't automatically mean you're being spoofed. It might mean a legitimate vendor forwards, resends, or signs your mail in a way your policy doesn't recognize yet. For a deeper look at why individual DKIM checks fail even outside the alignment question, see why DKIM signature verification fails.

DMARC Sending Source Identification: Mapping IPs to Real Senders

A report with forty unique source IPs and no context isn't useful until you know who's behind each one. DMARC sending source identification is the unglamorous, essential work of turning an IP address into a name: a vendor, a department, or an attacker.

Treat every unrecognized source IP in a report as a question, not a verdict. Resolve it, check reverse DNS and ASN ownership, and only then decide whether it's shadow IT, a forgotten vendor, or a spoofing attempt. Jump straight to "we're being spoofed" before doing that lookup, and you'll block legitimate infrastructure while missing real incidents underneath the noise.

Resolving Source IPs to ASN and Reverse DNS

Start with a reverse DNS lookup on the source IP. A result like o1.email-vendor.net tells you far more than the bare IP does. Follow that with an ASN lookup through any public WHOIS or routing registry tool. This tells you which organization owns the netblock, which is often enough to identify a known ESP, CRM, or helpdesk platform even when rDNS is vague.

Cross-reference the result against your own SPF record. If the IP falls inside an include: mechanism you've already authorized, you've likely found a known vendor that just isn't aligning cleanly. If it's outside everything you've published and outside any vendor you recognize, that's the row that deserves real scrutiny, not the ones from your known ESP with a minor alignment quirk.

A single record from a rua report can show an IP resolving to a legitimate third-party ESP that's sending on your behalf but was never added to your SPF record. The fix there is a policy change, add the include, adjust alignment, not an incident response. If your SPF record needs an update as a result, SPF record syntax and mechanisms covers exactly how to add it correctly.

Building a Source Inventory From Report History

One report is a snapshot. A few weeks of reports is a pattern. Keep a running list of every source IP you've resolved, what service it maps to, whether it's currently in your SPF record, and its typical DKIM alignment behavior. Record the DKIM selector too, now that the schema always gives you one. This becomes your source inventory: the single reference that turns "another unfamiliar IP" into "oh, that's the survey tool the sales team signed up for in March."

Update it every time you review a report. New IPs from a known vendor's range don't need investigation. Genuinely new senders do. Over a few months, this inventory becomes more valuable than any single report, because it tells you what "normal" looks like for your domain.

Prioritizing DMARC Report Remediation: What to Fix First

A report with dozens of failing rows can look like an emergency. It usually isn't. DMARC report remediation works best as triage, not panic. You fix the rows that matter most first, and you decide what matters by volume and pattern, not by how alarming the failure code looks.

Triage by Volume, Not by Alarm

Sort every failing row by message count, highest first. A source sending 12,000 messages with an spf=fail alignment issue outranks a source sending six messages with dkim=fail, every time, even though both look identical in a raw failure list. Volume tells you what's actually affecting deliverability and what's just noise.

Also check whether the failure is new or persistent. Most mailbox providers that generate rua reports only send them once every 24 hours in batches. So a single day's spike in failures often reflects one misconfigured source going through a change, not an ongoing attack. A failure pattern that repeats across a full week of reports is a real problem. A one-day blip that resolves itself is usually a deploy, a config push, or a DNS propagation delay on someone else's end.

Common Root Causes and Their Fixes

Most recurring failures trace back to a short list of causes:

  • Missing SPF include, a vendor's sending IPs aren't listed in your SPF record. Fix: add the vendor's documented include: mechanism, and check you're still under the ten-lookup limit.
  • DKIM key rotation, a vendor rotated signing keys and your DNS still has the old selector's public key, or none at all. Fix: read the selector straight out of the failing auth_results block, confirm it against the vendor's current selector, and republish the DNS record.
  • Forwarding, a recipient's mail client or a mailing list resends your message, changing the envelope path and breaking SPF alignment while DKIM (if untouched) still passes. Fix: often nothing to fix on your end; DKIM's resilience to forwarding is exactly why DMARC weighs it independently.
  • Alignment mode mismatch, strict alignment configured when a vendor signs from a related but non-identical subdomain. Fix: relax the alignment mode (aspf/adkim) or get the vendor to sign with an exact-match domain.
  • Unprotected non-existent subdomains, attackers sending as hosts you never created. Fix: publish np=reject, the tag added in 2026 specifically for subdomains that don't exist.

Work through the list in volume order, confirm the fix in the next report cycle, and move to the next row. That's the entire framework. No dashboard-driven alerting required.

When you're ready to tighten your policy, don't reach for pct=. That tag was removed from the specification in 2026 because partial enforcement produced unpredictable results. The replacement is t=y, a testing mode that steps the published policy down one level for everyone while still reporting what the full policy would have done. It answers the same question the percentage tag was meant to answer, and it answers it on every message instead of a random sample.

Parsing DMARC Reports at Scale Without a Vendor Dashboard

Dashboard tools exist for a reason: once you're managing dozens of domains and hundreds of daily reports, manual XML reading stops scaling. But most teams reach for a dashboard long before they've actually learned to read the schema. That means they end up trusting a summarized chart they couldn't independently verify if it were wrong.

Simple Parsing Approaches for Ongoing Review

For a single domain or a small handful, a lightweight script that unzips incoming report attachments, parses the XML, and dumps rows into a spreadsheet or a simple database table is enough. Group by source IP, sum the counts, and flag anything with a failing policy_evaluated result above a volume threshold you set. Point the parser at the current namespace, urn:ietf:params:xml:ns:dmarc-2.0, so the newer fields don't get dropped. That's a working DMARC aggregate report analysis pipeline built from parts you already understand, because you built the query yourself.

This approach also keeps you honest. When a script surfaces a raw row, you're looking at the same data a dashboard would summarize. You just haven't outsourced the interpretation.

When a Dashboard Tool Actually Earns Its Keep

Once you're tracking many domains, high report volume, or need historical trend charts for a compliance audit, a dashboard tool earns its cost. It aggregates faster than a script you maintain in your spare time, and it's built to handle report volume that would make manual review impractical.

The order matters, though. Learn to read a raw record, policy_evaluated, auth_results, source IP, count, before you adopt a tool that hides those fields behind a green checkmark. A healthy report, reviewed weekly at minimum and daily if you're mid-remediation, shows a shrinking list of unresolved source IPs and a rising percentage of messages passing DMARC evaluation cleanly over time. That trend line, not any single day's report, is the real measure of progress.

When a report row still doesn't add up after you've checked alignment, resolved the IP, and ruled out the usual root causes, the next step is message-level forensics. Pull the actual header and read the Authentication-Results header field by field, then cross-check it against detecting spoofing through header analysis before you conclude anything's actually malicious. Aggregate reports tell you where to look. They don't always tell you the whole story. That's what per-message forensic tracing is for.