Issue Description
Version: 1
Feedback-Type: auth-failure
Arrival-Date: Tue, 28 Jul 2026 07:14:46 +0000
Auth-Failure: dmarc
Identity-Alignment: none
Authentication-Results: xxxxxxxxxxxxxx;
dkim=pass header.d=yyyyyyyyyyyyyyy header.s=brevo2 header.b=dsxfJIpQ;
spf=pass (xxxxxxxxxxxxxx: domain of [email protected] designates z.z.z.z. as permitted sender) smtp.helo=ak.d.mailin.fr;
spf=pass (xxxxxxxxxxxxxx: domain of [email protected] designates z.z.z.z. as permitted sender) smtp.mailfrom="[email protected]";
iprev=pass policy.iprev=z.z.z.z.;
dmarc=pass header.from=yyyyyyyyyyyyyyy policy.dmarc=reject
Reporting-MTA: dns;xxxxxxxxxxxxxx
Source-IP: z.z.z.z.
User-Agent: Stalwart/1.0.0
(i.e. spf passes but is not aligned with “mail from”).
ChatGPT says that as per RFC 9991 it should say what failed (i.e. Identity-Alignment: spf), while as per the obsolete RFC 7489 it should say what succeeded (i.e. Identity-Alignment: dkim), but in any case the value of none is wrong.
Taking the LLM suggestion with a grain of salt, this is its suggested fix:
diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs
index e36add76..f09bb94a 100644
--- a/crates/smtp/src/reporting/dmarc.rs
+++ b/crates/smtp/src/reporting/dmarc.rs
@@ -19,7 +19,7 @@ use common::{
use compact_str::ToCompactString;
use mail_auth::{
ArcOutput, AuthenticatedMessage, AuthenticationResults, DkimOutput, DkimResult, DmarcOutput,
- SpfResult,
+ DmarcResult, SpfResult,
common::verify::VerifySignature,
dkim2::Dkim2Output,
dmarc::{self},
@@ -136,7 +136,7 @@ impl<T: SessionStream> Session<T> {
.with_headers(std::str::from_utf8(message.raw_headers()).unwrap_or_default());
// Report the first failed signature
- let dkim_failed = if let (
+ if let (
dmarc::Report::Dkim
| dmarc::Report::DkimSpf
| dmarc::Report::All
@@ -157,13 +157,10 @@ impl<T: SessionStream> Session<T> {
.with_dkim_domain(signature.domain())
.with_dkim_selector(signature.selector())
.with_dkim_identity(signature.identity());
- true
- } else {
- false
- };
+ }
// Report SPF failure
- let spf_failed = if let (
+ if let (
dmarc::Report::Spf
| dmarc::Report::DkimSpf
| dmarc::Report::All
@@ -194,17 +191,17 @@ impl<T: SessionStream> Session<T> {
auth_failure =
auth_failure.with_spf_dns(format!("txt : {} : v=SPF1", output.domain()));
// TODO use DNS record
- true
- } else {
- false
- };
+ }
auth_failure
- .with_identity_alignment(match (dkim_failed, spf_failed) {
- (true, true) => IdentityAlignment::DkimSpf,
- (true, false) => IdentityAlignment::Dkim,
- (false, true) => IdentityAlignment::Spf,
- (false, false) => IdentityAlignment::None,
+ .with_identity_alignment(match (
+ matches!(dmarc_output.dkim_result(), DmarcResult::Pass),
+ matches!(dmarc_output.spf_result(), DmarcResult::Pass),
+ ) {
+ (false, false) => IdentityAlignment::DkimSpf,
+ (false, true) => IdentityAlignment::Dkim,
+ (true, false) => IdentityAlignment::Spf,
+ (true, true) => IdentityAlignment::None,
})
.write_rfc5322(
(
diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs
index 05357cdd..55cd1ac4 100644
--- a/tests/src/smtp/inbound/dmarc.rs
+++ b/tests/src/smtp/inbound/dmarc.rs
@@ -6,7 +6,7 @@
use crate::{
smtp::{
- inbound::{TestMessage, TestReportingEvent},
+ inbound::{TestMessage, TestQueueEvent, TestReportingEvent},
session::{TestSession, VerifyResponse},
},
utils::{dns::DnsCache, server::TestServerBuilder},
@@ -329,6 +329,48 @@ async fn dmarc() {
.await;
test.assert_no_events();
+ // DMARC pass with unaligned SPF should generate an fo=1 failure report
+ test.server.txt_add(
+ "_dmarc.example.com",
+ Dmarc::parse(
+ concat!(
+ "v=DMARC1; p=reject; sp=quarantine; np=None; aspf=s; adkim=s; fo=1;",
+ "rua=mailto:[email protected];",
+ "ruf=mailto:[email protected]"
+ )
+ .as_bytes(),
+ )
+ .unwrap(),
+ Instant::now() + Duration::from_secs(5),
+ );
+ session
+ .send_message(
+ "[email protected]",
+ &["[email protected]"],
+ "test:dkim",
+ "250",
+ )
+ .await;
+ test.read_event().await.assert_refresh();
+ test.read_event().await.assert_refresh();
+ test.read_queued_messages()
+ .await
+ .into_iter()
+ .find(|message| {
+ message
+ .message
+ .recipients
+ .iter()
+ .any(|recipient| recipient.address() == "[email protected]")
+ })
+ .unwrap()
+ .read_lines(&test)
+ .await
+ .assert_contains("Feedback-Type: auth-failure")
+ .assert_contains("Auth-Failure: dmarc")
+ .assert_contains("Identity-Alignment: spf")
+ .assert_contains("dmarc=3Dpass");
+
// Messages passing DMARC should be accepted
session
.send_message(
Expected Behavior
Identity-Alignment should have the expected value, whatever it is.
Actual Behavior
Identity-Alignment has a value of none.
Stalwart Version
v0.16.x
Installation Method
Docker
Database Backend
PostgreSQL
Blob Storage
S3-compatible
Search Engine
PostgreSQL
Directory Backend
Internal
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
I understand that topics in this category are triaged by a bot first but a human reply will follow up. If I’d prefer a human-only reply, I’ll add the no-ai tag to my topic.
on