What's New in DMARCbis Aggregate Reports (RFC 9990)
RFC 9990 is the new aggregate report format that ships alongside RFC 9989 (DMARCbis). Both were published in May 2026 and replace RFC 7489. The report you get is still a feedback XML document, but the schema moved: policy_published gains np, testing, and discovery_method, report_metadata gains generator, pct is gone, a DKIM selector is required, disposition can now be pass, and there is a new override reason, policy_test_mode.
None of that means your parser can drop the old format. There is no flag day, and reports from receivers still on RFC 7489 will keep arriving next to RFC 9990 ones.
An annotated RFC 9990 report
Here is a sample report for example.com, which publishes v=DMARC1; p=reject; np=reject; t=y; pct=0; rua=mailto:.... That is a domain testing a reject policy with the transition pair t=y; pct=0. The structure follows the RFC 9990 schema (Appendix A). The values are made up.
<?xml version="1.0" encoding="UTF-8"?>
<feedback xmlns="urn:ietf:params:xml:ns:dmarc-2.0">
<version>1.0</version>
<report_metadata>
<org_name>receiver.example</org_name>
<email>dmarc-reports@receiver.example</email>
<report_id>8f3c2a91e7d04b6c</report_id>
<date_range>
<begin>1790380800</begin>
<end>1790467199</end>
</date_range>
<generator>Example Aggregate Reporter 2.1</generator>
</report_metadata>
<policy_published>
<domain>example.com</domain>
<p>reject</p>
<sp>reject</sp>
<np>reject</np>
<adkim>r</adkim>
<aspf>r</aspf>
<discovery_method>treewalk</discovery_method>
<testing>y</testing>
</policy_published>
<record>
<row>
<source_ip>192.0.2.10</source_ip>
<count>412</count>
<policy_evaluated>
<disposition>none</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
</row>
<identifiers>
<envelope_from>send.example.com</envelope_from>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.com</domain>
<selector>resend</selector>
<result>pass</result>
</dkim>
<spf>
<domain>send.example.com</domain>
<result>pass</result>
</spf>
</auth_results>
</record>
<record>
<row>
<source_ip>198.51.100.7</source_ip>
<count>9</count>
<policy_evaluated>
<disposition>quarantine</disposition>
<dkim>fail</dkim>
<spf>fail</spf>
<reason>
<type>policy_test_mode</type>
</reason>
</policy_evaluated>
</row>
<identifiers>
<header_from>example.com</header_from>
</identifiers>
<auth_results>
<dkim>
<domain>example.com</domain>
<selector>s2024</selector>
<result>fail</result>
</dkim>
<spf>
<domain>example.com</domain>
<result>fail</result>
<human_result lang="en">198.51.100.7 not permitted by example.com SPF</human_result>
</spf>
</auth_results>
</record>
</feedback>
What changed, keyed to the sample:
xmlns="urn:ietf:params:xml:ns:dmarc-2.0". RFC 9990 §3.1.1.1 says the rootfeedbackelement has "its XML namespace set to the DMARC namespace", which §6.1 registers asurn:ietf:params:xml:ns:dmarc-2.0. This is the reliable format signal.<version>1.0</version>. §3.1.1.2 marks it optional and says it "MUST have the value 1.0." More on why that doesn't help below.<generator>. New inreport_metadata. §3.1.1.3: "The name and version of the report generator; this can help the Report Consumer find out where to report bugs." Optional.<np>. The non-existent subdomain policy, now part of core DMARC, gets its own slot inpolicy_published(§3.1.1.5).<discovery_method>. How the receiver found your record:pslortreewalk(theDiscoveryTypeenumeration in Appendix A). RFC 9989 replaced the Public Suffix List with a DNS tree walk, and this field tells you which method the reporter used. Optional.<testing>. §3.1.1.5: "The value of the "t" tag." Values areyorn. Optional, and §3.1.1.5 says "Unspecified tags have their default values", so a reporter can sendneven if your record has nottag.- No
<pct>. The RFC 9990PolicyPublishedTypehas nopctelement. Your record can still carrypct=0for older receivers, but an RFC 9990 report won't echo it back. <disposition>quarantine</disposition>plus<reason><type>policy_test_mode</type>. The domain asked forreject, butt=ytells receivers to apply the policy one level lower (RFC 9989 §4.7), so failing mail gets quarantine. §3.1.1.9 requires thereasonelement when "alignment fails and the policy applied does not match the DMARC Policy Domain's configured policy." The matching reason is new. §3.1.6 definespolicy_test_modeas "The message was exempted from application of policy by the testing mode ("t" tag) in the DMARC Policy Record."<selector>on every DKIM result. In the RFC 7489 schema,selectorhadminOccurs="0". In RFC 9990 it isminOccurs="1", and Appendix C lists "Selector is now required when reporting a DKIM signature." That makes per-selector breakdowns reliable, which matters when you rotate keys or run several ESPs on one domain.- No
<envelope_from>in the second record. It's optional in RFC 9990. <human_result lang="en">on the SPF result. RFC 7489 only hadhuman_resulton DKIM results, as a plain string. RFC 9990 allows it on bothdkimandspf(§3.1.1.12, §3.1.1.13) and gives it an optionallangattribute that defaults toen. It's free text for debugging, so display it but don't parse it.
The pass disposition
The first record shows none for mail that passed DMARC. RFC 9990 adds a fourth value to ActionDispositionType. The schema comment defines it as "pass": No action, passing DMARC w/enforcing policy. Once this domain drops t=y and runs a real p=reject, a reporter may render the same passing row like this:
<policy_evaluated>
<disposition>pass</disposition>
<dkim>pass</dkim>
<spf>pass</spf>
</policy_evaluated>
Treat none and pass as the same outcome for passing mail: nothing happened to the message. Whether a reporter uses pass under testing mode isn't pinned down by the schema comment, so don't build alerting on the choice between the two.
What was dropped
Two override reasons. The RFC 7489 PolicyOverrideType enumeration had forwarded, sampled_out, trusted_forwarder, mailing_list, local_policy, and other. RFC 9990 keeps local_policy, mailing_list, other, and trusted_forwarder, drops forwarded and sampled_out, and adds policy_test_mode.
RFC 9990 doesn't give a reason for each removal, but sampled_out only ever made sense with pct: with no sampling percentage in the record, there's nothing to sample out. The closest replacement is policy_test_mode, which explains a softer disposition caused by t=y rather than by pct.
pct in policy_published. Covered above. RFC 9989 removed pct from the record (its IANA status is "historic"), and the report schema followed.
Why <version> can't tell you which format you have
It's tempting to branch on <version>. It doesn't work, for three reasons:
- RFC 9990 makes
versionoptional, so a conforming report can omit it. - When it is present, RFC 9990 fixes it at
1.0. - RFC 7489's schema declared a
versionelement but never pinned a value for it, so a1.0tells you nothing about which spec the reporter followed.
Detect the format by the namespace on feedback instead. If it's urn:ietf:params:xml:ns:dmarc-2.0, you have an RFC 9990 report. As a fallback, fields that only exist in RFC 9990 (np, testing, discovery_method, generator) are positive evidence.
Absence is not evidence, though. Every one of those fields is optional, so a minimal RFC 9990 report can look a lot like an RFC 7489 one. And discovery_method can legally be psl in an RFC 9990 report, so psl doesn't mean the reporter is on the old spec.
Why your tooling has to accept both
Receivers move to new specs on their own schedules. We haven't seen public confirmation that Google, Microsoft, or Yahoo send RFC 9990 reports yet, so plan on a long stretch where your rua mailbox gets both formats, sometimes for the same domain on the same day.
In practice, a parser that handles both needs to:
- Accept either namespace, or no namespace, on
feedback, and not requireversion. - Tolerate a missing DKIM
selectorin legacy reports instead of rejecting the record. - Keep accepting
forwardedandsampled_outas reason types from old reporters, and acceptpolicy_test_modefrom new ones. - Treat
passas a valid disposition. - Store
testinganddiscovery_methodas nullable. "Not reported" andnare different facts. - Stop assuming
pctwill be present inpolicy_published.
The last point about testing matters for alerting. A row with policy_test_mode and a disposition one level below your published policy is the receiver doing what you asked. If your drift logic compares disposition to p and flags every mismatch, testing mode will look like an incident.
Check a report yourself
Paste any aggregate report, old or new format, into the DMARC XML Analyzer. It parses in your browser and shows testing and discovery_method when the reporter includes them. For a walkthrough of the fields that didn't change, see what's in a DMARC aggregate report. For the record-side changes, see what RFC 9989 changed.
