If you just want the “what do I do every week” version without the schema, start with what a DMARC report email actually means. This post is the field-by-field reference for when you need to know what a specific element actually contains.

Aggregate vs forensic

Aggregate reports (RUA) are daily XML summaries: which sources sent mail as your domain, whether they passed SPF/DKIM/DMARC, and how many messages. This is what every DMARC deployment should be reading.

Forensic reports (RUF) would carry per-message samples of failing mail, but almost nobody sends them in practice: they leak message content, so most receivers dropped support years ago. Aggregate reports are where the real signal is.

Report structure

Every aggregate report is one <feedback> document with three required parts, in this order, per RFC 9990 §3.1.1.2:

<feedback xmlns="urn:ietf:params:xml:ns:dmarc-2.0">
  <report_metadata>...</report_metadata>
  <policy_published>...</policy_published>
  <record>...</record>
  <!-- one or more <record> blocks -->
</feedback>

Note the namespace: reports built to the current schema declare urn:ietf:params:xml:ns:dmarc-2.0. A report still using the older http://dmarc.org/dmarc-xml/0.1 namespace (or none at all) is following the previous, now-obsolete schema, not this one.

report_metadata: who sent this and when

RFC 9990 §3.1.1.3: org_name names the reporting mailbox provider and email is the contact address for it, report_id is a unique ID for that report, and date_range gives the UTC window it covers (usually one day). An optional generator field names the software that produced the report, useful if you ever need to report a malformed one back to the sender.

policy_published: what the receiver saw in your DNS

This is your actual _dmarc TXT record as the receiver evaluated it. RFC 9990 §3.1.1.5 lists domain, p (your policy), sp and np (subdomain policy for existing and non-existent subdomains), adkim/aspf (alignment mode), and testing (the t=y flag, if set).

Two things changed from the older schema, and both trip people up if you’re used to the old one:

  • No pct field. RFC 9989 removed the pct tag from DMARC entirely (Appendix A.6), so current-schema reports don’t carry it. If you see pct in a report, it’s still on the obsolete schema.
  • discovery_method (optional) tells you whether the receiver found your policy via psl (the old Public Suffix List method) or treewalk (the tree walk defined in RFC 9989). Worth checking if a report’s policy looks wrong for a subdomain: it usually means the receiver hasn’t updated to tree-walk discovery yet.

record: the part that actually matters

One <record> per sending source. This is what you’re really reading:

  • row.source_ip: the IP that connected and sent mail.
  • row.count: how many messages from that IP in this window.
  • row.policy_evaluated.disposition: what the receiver actually did: none, pass, quarantine, or reject (pass means the message passed DMARC under an enforcing policy; passing mail under p=none is typically reported as none). This is the applied action, not necessarily what your published policy says (see override reasons below).
  • identifiers.header_from: the From: domain the recipient saw. This is what DMARC alignment is checked against.
  • auth_results.spf and auth_results.dkim: the raw SPF and DKIM results for that source, independent of DMARC. RFC 9990 §3.1.1.12 makes the DKIM selector field required now (it wasn’t always populated consistently under the old schema), so you can tell exactly which key signed a passing or failing message, which matters if you’re mid-rotation, see DKIM key rotation.

Result values shared by both spf and dkim (RFC 8601 §2.7): none, pass, fail, policy, neutral, temperror, permerror. SPF also has softfail; DKIM doesn’t. A permerror on SPF almost always means the record itself is broken, not that the sender is forging mail; see SPF permerror: why adding one SaaS tool breaks your email authentication.

Override reasons: why disposition doesn’t match your policy

When disposition is weaker than what you published, the record usually carries a reason element explaining why. RFC 9990 §3.1.6 defines five values:

typeMeaning
local_policyThe receiver’s own local rule exempted this message from your policy.
mailing_listThe receiver’s heuristics decided this came through a mailing list, where the original authentication wasn’t expected to survive.
trusted_forwarderThe receiver has independent evidence this came through a forwarder it already trusts, despite the authentication failure.
policy_test_modeYour DMARC record has t=y set, so the receiver applied one policy level weaker than published.
otherSomething else, with details in a free-text comment field.

If you’re rolling a policy from p=none toward p=reject and using t=y to test it quietly first: with t=y, a failing message is handled one level softer than your published policy (reject becomes quarantine, quarantine becomes none), and the row can carry policy_test_mode to say why. Watch disposition and reason together. t=y has no effect when p=none. Full mechanics in moving from p=none to p=reject safely.

A review loop that doesn’t take all day

  1. Confirm every sender you actually use is passing SPF or DKIM aligned to your From: domain.
  2. Watch disposition and reason together when you tighten policy, not disposition alone.
  3. Treat a sudden spike in failure volume from one source as a misconfiguration to check first, not an attack to panic about.
  4. Flag any source you don’t recognize, regardless of volume.

DMARC Guardian parses this XML for you. Its issues-only filter keeps the rows where SPF, DKIM or disposition shows a problem, so the row you need is the one you see first instead of the one buried in a zip file.

Check your DMARC, SPF and DKIM records, free, no signup →