Send "none" instead of "pass" as the DMARC report disposition
Cloudflare's DMARC report intake rejects every aggregate report we send with "555 5.7.1 invalid_report_schema". Bisected against the live endpoint: the only element it objects to is <disposition>pass</disposition>, the value RFC 9990 added for mail that passed DMARC under an enforcing policy. The RFC 9990 namespace, <np>, <discovery_method>, <testing> and a missing <pct> are all accepted, and a report that differs only in using "none" there goes through. "none" (no action taken) is valid under both RFC 9990 and RFC 7489 and says the same thing to the reader, so reports now go out with it. The stored report keeps "pass"; only the serialized copy changes.
This commit is contained in:
@@ -26,7 +26,10 @@ use mail_auth::{
|
||||
common::verify::VerifySignature,
|
||||
dkim2::Dkim2Output,
|
||||
dmarc::{self},
|
||||
report::{AuthFailureType, IdentityAlignment, PolicyPublished, Record, SPFDomainScope},
|
||||
report::{
|
||||
ActionDisposition, AuthFailureType, IdentityAlignment, PolicyPublished, Record, Report,
|
||||
SPFDomainScope,
|
||||
},
|
||||
};
|
||||
use registry::{
|
||||
schema::{
|
||||
@@ -459,7 +462,7 @@ impl DmarcReporting for Server {
|
||||
.await
|
||||
.unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string());
|
||||
let mut message = Vec::with_capacity(2048);
|
||||
let _ = mail_auth::report::Report::from(report.report).write_rfc5322(
|
||||
let _ = with_compatible_dispositions(Report::from(report.report)).write_rfc5322(
|
||||
&self
|
||||
.eval_if(
|
||||
&self.core.smtp.report.submitter,
|
||||
@@ -710,3 +713,55 @@ impl DmarcReporting for Server {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// inbuxa: RFC 9990 added "pass" to the evaluated disposition for mail that
|
||||
// passed DMARC under an enforcing policy. Cloudflare's report intake rejects
|
||||
// the whole report with "555 5.7.1 invalid_report_schema" when it sees that
|
||||
// value, and older parsers built on the RFC 7489 schema do the same. "none"
|
||||
// (no action taken) is valid under both and says the same thing, so reports
|
||||
// go out with that instead.
|
||||
fn with_compatible_dispositions(mut report: Report) -> Report {
|
||||
for record in &mut report.record {
|
||||
let disposition = &mut record.row.policy_evaluated.disposition;
|
||||
if *disposition == ActionDisposition::Pass {
|
||||
*disposition = ActionDisposition::None;
|
||||
}
|
||||
}
|
||||
report
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mail_auth::report::DmarcResult;
|
||||
|
||||
fn record(disposition: ActionDisposition) -> Record {
|
||||
Record::new()
|
||||
.with_source_ip("192.0.2.1".parse().unwrap())
|
||||
.with_count(1)
|
||||
.with_action_disposition(disposition)
|
||||
.with_dmarc_dkim_result(DmarcResult::Pass)
|
||||
.with_dmarc_spf_result(DmarcResult::Fail)
|
||||
.with_header_from("example.org")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pass_disposition_is_reported_as_none() {
|
||||
let xml = with_compatible_dispositions(
|
||||
Report::new()
|
||||
.with_domain("example.org")
|
||||
.with_record(record(ActionDisposition::Pass))
|
||||
.with_record(record(ActionDisposition::Quarantine))
|
||||
.with_record(record(ActionDisposition::Reject)),
|
||||
)
|
||||
.to_xml();
|
||||
|
||||
assert!(!xml.contains("<disposition>pass</disposition>"), "{xml}");
|
||||
assert!(xml.contains("<disposition>none</disposition>"), "{xml}");
|
||||
assert!(
|
||||
xml.contains("<disposition>quarantine</disposition>"),
|
||||
"{xml}"
|
||||
);
|
||||
assert!(xml.contains("<disposition>reject</disposition>"), "{xml}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
|
||||
*
|
||||
* Modified by Coffey Labs in 2026 for INBUXA.
|
||||
*/
|
||||
|
||||
use crate::{
|
||||
@@ -174,7 +176,8 @@ async fn report_dmarc() {
|
||||
let source_ip = record.source_ip().unwrap();
|
||||
if source_ip == "192.168.1.2".parse::<IpAddr>().unwrap() {
|
||||
assert_eq!(record.count(), 2);
|
||||
assert_eq!(record.action_disposition(), ActionDisposition::Pass);
|
||||
// inbuxa: "pass" goes out as "none" for RFC 7489 parsers
|
||||
assert_eq!(record.action_disposition(), ActionDisposition::None);
|
||||
assert_eq!(record.envelope_from(), "[email protected]");
|
||||
assert_eq!(record.header_from(), "[email protected]");
|
||||
assert_eq!(record.envelope_to().unwrap(), "[email protected]");
|
||||
|
||||
Reference in New Issue
Block a user