diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs
index c24a03c..b3ed5d2 100644
--- a/crates/smtp/src/reporting/dmarc.rs
+++ b/crates/smtp/src/reporting/dmarc.rs
@@ -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("pass"), "{xml}");
+ assert!(xml.contains("none"), "{xml}");
+ assert!(
+ xml.contains("quarantine"),
+ "{xml}"
+ );
+ assert!(xml.contains("reject"), "{xml}");
+ }
+}
diff --git a/tests/src/smtp/reporting/dmarc.rs b/tests/src/smtp/reporting/dmarc.rs
index 52b20f0..970e0c2 100644
--- a/tests/src/smtp/reporting/dmarc.rs
+++ b/tests/src/smtp/reporting/dmarc.rs
@@ -2,6 +2,8 @@
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC
*
* 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::().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(), "hello@example.org");
assert_eq!(record.header_from(), "bye@example.org");
assert_eq!(record.envelope_to().unwrap(), "other@example.org");