DMARC aggregate reports are valid under neither RFC 9990 nor RFC 7489: two `<spf>` elements per record (Cloudflare 555 invalid_report_schema)

Issue Description

Outbound DMARC aggregate reports declare the RFC 9990 namespace (urn:ietf:params:xml:ns:dmarc-2.0) but still emit two results per record: the EHLO result (with no ) and the MAIL FROM result (mfrom). That combination is valid under neither schema:

  • RFC 7489: the namespaced root element is not recognised (as noted in #1250).
  • RFC 9990: only one SPF result is allowed, and only for the MAIL FROM identity:
    • §3.1.1.11 (“auth_results”): spf is “O” (optional, at most one); dkim is “*”.
    • §3.1.1.13: “Only the ‘MAIL FROM’ identity (see Section 2.4 of [RFC7208]) is used in DMARC.”
    • Appendix A: <xs:element name=“spf” … minOccurs=“0” maxOccurs=“1”/>

Related but not a duplicate: #1250 reported the same Cloudflare 555 on v0.16.15, and the reply attributed it to Cloudflare validating against RFC 7489. That may also be true, but the reports fail RFC 9990 validation independently of it.

Where it happens, on main:

  • crates/smtp/src/reporting/dmarc.rs (lines 319-323) calls with_spf_output for both spf_ehlo (SPFDomainScope::Helo) and spf_mail_from (SPFDomainScope::MailFrom).
  • mail-auth 0.13.2 Record::with_spf_output pushes onto auth_results.spf: Vec, and AuthResult::to_xml (src/report/dmarc/generate.rs) serialises every element.
  • Neither changed in v0.16.22.

Expected Behavior

Each contains at most one : the result for the MAIL FROM identity as defined in RFC 7208 §2.4 (for a null reverse-path, postmaster@ the HELO domain), per RFC 9990 §3.1.1.13. The EHLO check result is not emitted as a separate element. is 1.0 (§3.1.1.2: “MUST have the value 1.0”).

Actual Behavior

Every record where the EHLO SPF check ran contains two elements. The EHLO one has no , so it also reads as a second MAIL FROM result with a contradictory value (none vs pass). is emitted as 1.

Receivers that validate strictly reject the report. Between 2026-09-11 and 2026-09-16, 7 of 7 reports sent to dmarc-reports.cloudflare.net were rejected with “555 5.7.1 invalid_report_schema”. I recovered all 7 from the bounce DSNs. Each has the RFC 9990 namespace, one and two . Each fails RFC 9990 Appendix A validation only at the second , and all 7 validate once the EHLO is removed. No other schema violation exists in any of them.

Reproduction Steps

  1. Receive a message where both the EHLO and MAIL FROM SPF checks run, from a domain whose DMARC record has a rua.
  2. Let the aggregate report be generated.
  3. Validate it against RFC 9990 Appendix A: xmllint --noout --schema rfc9990-appendixA.xsd report.xml

Reproducer (anonymised; the original was rejected by Cloudflare on 2026-09-11):

<?xml version="1.0" encoding="UTF-8" ?>
<feedback xmlns="urn:ietf:params:xml:ns:dmarc-2.0">
      <version>1</version>
      <report_metadata>
              <org_name>example.org</org_name>
              <email>[email protected]</email>
              <report_id>1788990509_14293159088052110869</report_id>
              <date_range>
                      <begin>1788990509</begin>
                      <end>1789076909</end>
              </date_range>
      </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>
              <fo>1</fo>
              <testing>n</testing>
      </policy_published>
      <record>
              <row>
                      <source_ip>192.0.2.10</source_ip>
                      <count>1</count>
                      <policy_evaluated>
                              <disposition>pass</disposition>
                              <dkim>pass</dkim>
                              <spf>fail</spf>
                      </policy_evaluated>
              </row>
              <identifiers>
                      <envelope_from>example.net</envelope_from>
                      <header_from>example.com</header_from>
              </identifiers>
              <auth_results>
                      <dkim>
                              <domain>example.com</domain>
                              <selector>sel1</selector>
                              <result>pass</result>
                      </dkim>
                      <dkim>
                              <domain>esp.example.net</domain>
                              <selector>sel2</selector>
                              <result>pass</result>
                      </dkim>
                      <spf>
                              <domain>mail-out.example.net</domain>
                              <result>none</result>
                      </spf>
                      <spf>
                              <domain>example.net</domain>
                              <scope>mfrom</scope>
                              <result>pass</result>
                      </spf>
              </auth_results>
      </record>
</feedback>

Relevant Log Output

$ xmllint --noout --schema rfc9990-appendixA.xsd report.xml
report.xml:53: Schemas validity error : Element '{urn:ietf:params:xml:ns:dmarc-2.0}spf': This element is not expected.
report.xml fails to validate

$ # same file with only the EHLO <spf> removed
report.xml validates

Bounce DSN from the relay, original report attached:
Final-Recipient: rfc822; <redacted>@dmarc-reports.cloudflare.net
Diagnostic-Code: smtp; 555 5.7.1 invalid_report_schema. YQvpyL6J2GuD

Stalwart Version

v0.16.x

Installation Method

Docker

Database Backend

RocksDB

Blob Storage

RocksDB

Search Engine

Internal

Directory Backend

Internal

Additional Context

I have reviewed the documentation and FAQ and confirm that my issue is NOT addressed there.

on

I have searched this support forum (open and closed topics) and confirm this is not a duplicate.

on

This isn’t covered in the current docs. A community member or staff member will follow up.

This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong.

Fixed in v0.16.23 (yet to be released).