← Blog
Dark developer workspace at night with a monitor showing a DMARC aggregate report XML file open beside a parsed table of sending sources

By DMARCdrift Team

What's New in DMARCbis Aggregate Reports (RFC 9990)

7 min readdmarcrfcmonitoring

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:

  1. xmlns="urn:ietf:params:xml:ns:dmarc-2.0". RFC 9990 §3.1.1.1 says the root feedback element has "its XML namespace set to the DMARC namespace", which §6.1 registers as urn:ietf:params:xml:ns:dmarc-2.0. This is the reliable format signal.
  2. <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.
  3. <generator>. New in report_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.
  4. <np>. The non-existent subdomain policy, now part of core DMARC, gets its own slot in policy_published (§3.1.1.5).
  5. <discovery_method>. How the receiver found your record: psl or treewalk (the DiscoveryType enumeration 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.
  6. <testing>. §3.1.1.5: "The value of the "t" tag." Values are y or n. Optional, and §3.1.1.5 says "Unspecified tags have their default values", so a reporter can send n even if your record has no t tag.
  7. No <pct>. The RFC 9990 PolicyPublishedType has no pct element. Your record can still carry pct=0 for older receivers, but an RFC 9990 report won't echo it back.
  8. <disposition>quarantine</disposition> plus <reason><type>policy_test_mode</type>. The domain asked for reject, but t=y tells receivers to apply the policy one level lower (RFC 9989 §4.7), so failing mail gets quarantine. §3.1.1.9 requires the reason element 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 defines policy_test_mode as "The message was exempted from application of policy by the testing mode ("t" tag) in the DMARC Policy Record."
  9. <selector> on every DKIM result. In the RFC 7489 schema, selector had minOccurs="0". In RFC 9990 it is minOccurs="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.
  10. No <envelope_from> in the second record. It's optional in RFC 9990.
  11. <human_result lang="en"> on the SPF result. RFC 7489 only had human_result on DKIM results, as a plain string. RFC 9990 allows it on both dkim and spf (§3.1.1.12, §3.1.1.13) and gives it an optional lang attribute that defaults to en. 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 version optional, so a conforming report can omit it.
  • When it is present, RFC 9990 fixes it at 1.0.
  • RFC 7489's schema declared a version element but never pinned a value for it, so a 1.0 tells 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 require version.
  • Tolerate a missing DKIM selector in legacy reports instead of rejecting the record.
  • Keep accepting forwarded and sampled_out as reason types from old reporters, and accept policy_test_mode from new ones.
  • Treat pass as a valid disposition.
  • Store testing and discovery_method as nullable. "Not reported" and n are different facts.
  • Stop assuming pct will be present in policy_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.