Reading DMARC Reports: What the XML Files Actually Say

« Blog · By

A DMARC record with rua= has a side effect few people are prepared for: from the next day onward, messages arrive daily with ZIP or GZ files attached, from Google, Microsoft, Yahoo, and from mail servers whose names you have never heard. Inside is XML. Most people look twice and then set up a rule that sweeps the whole pile into a folder.

That is a shame, because these files answer a question nothing else can: who is actually sending mail under my name?

What a report contains — and what it does not

An aggregate report is a tally, not a collection of messages. It contains no subject lines, no recipients, no content. For one day and one receiver it says: these IP addresses sent this many messages carrying your domain in From:, and this is how they turned out.

The structure is always the same. Learn it once and you can read the file without any tool.

<feedback> ├ report_metadata ├ policy_published └ record ├ row / source_ip ├ row / count ├ row / policy_evaluated ├ identifiers / header_from ├ auth_results / spf └ auth_results / dkim who reports, which day what your DNS record said one block per sending source who sent how many messages domain in the visible From raw SPF result raw DKIM result the verdict after alignment The last three lines sit apart from the verdict for a reason.
The structure is identical in every report. What matters is the distance between policy_evaluated and auth_results: one is the result, the other the reason.

One record in the original

Here is a single record, unpacked and indented:

<record>
  <row>
    <source_ip>198.51.100.24</source_ip>
    <count>417</count>
    <policy_evaluated>
      <disposition>none</disposition>
      <dkim>fail</dkim>
      <spf>pass</spf>
    </policy_evaluated>
  </row>
  <identifiers>
    <header_from>company.com</header_from>
  </identifiers>
  <auth_results>
    <spf><domain>company.com</domain><result>pass</result></spf>
  </auth_results>
</record>

Read out: on that day, 417 messages came from 198.51.100.24 carrying company.com as the visible sender. SPF passed and was aligned, DKIM was not — there was no signature at all here. Since one aligned path is enough, DMARC passed. disposition: none says the receiver took no action.

The trap: two results with the same names

The words spf and dkim appear twice in that block, in two different places, with two different meanings:

  • In auth_results is the raw result: did the check itself pass?
  • In policy_evaluated is the result after alignment: does that pass count for DMARC as well?

A pass above and a fail below is the single most common finding, and it describes no contradiction but a source that sends legitimately, only under someone else's domain. Why that happens is covered in the article on alignment.

What a first report typically turns up

Anyone switching DMARC on finds, in the first two weeks, more sending sources than expected. The candidates repeat across organisations:

SourceTypical finding
Your own mail serverSPF and DKIM aligned — the uneventful part
Newsletter serviceSPF not aligned, DKIM on the provider domain
Ticket or CRM systemsends under the company domain with no DKIM at all
Invoicing or shop softwareits own sending path, often forgotten for years
Monitoring, backup, printerssmall counts, fixed IP, usually from your own network
Forwardersforeign IP, SPF broken, DKIM intact
Unrelated senderschanging IPs, both checks failed, small volumes

The last row is the reason DMARC gets introduced — and it is almost always the smallest. The work sits in the rows above it: every legitimate source has to be found and configured before the policy can be tightened.

The order that works

  1. Set p=none with rua=. This changes nothing about delivery and only turns the reports on.
  2. Collect for two to four weeks. Not less: the monthly invoice run and the newsletter that goes out every three weeks will otherwise never appear.
  3. List the sources and account for each. Every IP that sent more than a handful of messages needs a name.
  4. Establish alignment, usually via DKIM at the provider in question.
  5. Only then tighten — in steps, using pct=, which sets the share of messages the policy applies to.

Step three is where projects stall, and for an uncomfortable reason: some sources can only be identified by asking around the company who set up a service six years ago.

The forensic reports

Next to rua= there is ruf=: notifications about individual failed messages, with headers and sometimes content. In practice almost none arrive. The reports contained personal data belonging to third parties, and most large providers stopped sending them for that reason. You can set ruf=; you should expect nothing.

Do you need a tool for this?

For a couple of domains and a manageable volume it is enough to unpack the attachments and skim the records — the interesting lines are the ones with an unknown source_ip and two failures in policy_evaluated. Once you are into double-digit domain counts or high volume this stops being readable, and an analysis that merges reports and resolves IPs saves hours.

The point is a different one: the reports are not complicated. They are only packaged in a way that makes them look complicated.