From 2e8e9c4fcda0878805d58ca1427a696163628555 Mon Sep 17 00:00:00 2001 From: StefanSa Date: Wed, 4 Nov 2026 09:40:54 +0100 Subject: [PATCH 1/2] Preserve Milter SMTP reply code and text in delivery traces Populate the existing Code and Details trace fields for Milter ReplyCode actions without changing the SMTP response. Extend the existing session test to check the actual emitted event and complete reply. --- crates/smtp/src/inbound/milter/message.rs | 8 +++++ tests/src/smtp/inbound/milter.rs | 39 +++++++++++++++++++++-- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs index e910c2ba..c92dbd91 100644 --- a/crates/smtp/src/inbound/milter/message.rs +++ b/crates/smtp/src/inbound/milter/message.rs @@ -93,6 +93,14 @@ impl Session { QueueId = queue_id, Id = milter.id.to_string(), Elapsed = time.elapsed(), + Code = match &action { + Action::ReplyCode { code, .. } => Some(code.to_vec().into_string()), + _ => None, + }, + Details = match &action { + Action::ReplyCode { text, .. } => Some(text.clone()), + _ => None, + }, ); return Err(match action { diff --git a/tests/src/smtp/inbound/milter.rs b/tests/src/smtp/inbound/milter.rs index fa51c4d1..c915ec79 100644 --- a/tests/src/smtp/inbound/milter.rs +++ b/tests/src/smtp/inbound/milter.rs @@ -52,6 +52,7 @@ use tokio::{ net::{TcpListener, TcpStream}, sync::watch, }; +use trc::{Collector, EventType, Key, MilterEvent, ipc::subscriber::SubscriberBuilder}; #[derive(Debug, Deserialize)] struct HeaderTest { @@ -145,17 +146,51 @@ async fn milter_session() { .await; test.assert_no_events(); - // Test reply code + // Capture the actual event emitted by the SMTP session, not a reconstructed event. + let subscriber_id = "smtp-milter-reply-code".to_string(); + let subscriber = SubscriberBuilder::new(subscriber_id.clone()) + .set_interests([EventType::Milter(MilterEvent::ActionReplyCode)]) + .with_lossy(false); + Collector::union_interests(subscriber.interests.clone()); + let (_, mut events) = subscriber.register(); + tokio::time::timeout(Duration::from_secs(5), async { + while !Collector::get_subscribers().contains(&subscriber_id) { + tokio::time::sleep(Duration::from_millis(10)).await; + } + }) + .await + .expect("Milter trace subscriber was not registered"); + + // Test reply code: the SMTP response and its trace retain the Milter's code/text. session .send_message( "reply_code@doe.org", &["bill@foobar.org"], "test:no_dkim", - "321", + "321 test", ) .await; test.assert_no_events(); + let event = tokio::time::timeout(Duration::from_secs(5), async { + loop { + for event in events.recv().await.expect("Milter trace subscriber closed") { + if event.span_id() == Some(session.data.session_id) { + return event; + } + } + } + }) + .await + .expect("Milter reply-code event was not emitted"); + Collector::remove_subscriber(subscriber_id); + assert_eq!( + event.inner.typ, + EventType::Milter(MilterEvent::ActionReplyCode) + ); + assert_eq!(event.value_as_str(Key::Code), Some("321")); + assert_eq!(event.value_as_str(Key::Details), Some("test")); + // Test accept with header addition session .send_message( -- 2.54.0 From 50fab51d51b8a5cff729752f5f254316d2823fd7 Mon Sep 17 00:00:00 2001 From: John Coffey Date: Wed, 4 Nov 2026 21:13:05 -0700 Subject: [PATCH 2/2] Milter reply traces: numeric code, no empty fields Record Code as a number, as outbound SMTP traces do, and trim the line ending off Details. Only the reply-code event gets the two fields, so text logs don't show "(null)" for them on the other actions. Add the modification notice. --- crates/smtp/src/inbound/milter/message.rs | 57 +++++++++++++---------- tests/src/smtp/inbound/milter.rs | 2 +- 2 files changed, 33 insertions(+), 26 deletions(-) diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs index c92dbd91..e34327f3 100644 --- a/crates/smtp/src/inbound/milter/message.rs +++ b/crates/smtp/src/inbound/milter/message.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 super::{Action, Error, Macros, Modification}; @@ -77,31 +79,36 @@ impl Session { } } Err(Rejection::Action(action)) => { - trc::event!( - Milter(match &action { - Action::Discard => MilterEvent::ActionDiscard, - Action::Reject => MilterEvent::ActionReject, - Action::TempFail => MilterEvent::ActionTempFail, - Action::ReplyCode { .. } => { - MilterEvent::ActionReplyCode - } - Action::Shutdown => MilterEvent::ActionShutdown, - Action::ConnectionFailure => MilterEvent::ActionConnectionFailure, - Action::Accept | Action::Continue => unreachable!(), - }), - SpanId = self.data.session_id, - QueueId = queue_id, - Id = milter.id.to_string(), - Elapsed = time.elapsed(), - Code = match &action { - Action::ReplyCode { code, .. } => Some(code.to_vec().into_string()), - _ => None, - }, - Details = match &action { - Action::ReplyCode { text, .. } => Some(text.clone()), - _ => None, - }, - ); + if let Action::ReplyCode { code, text } = &action { + trc::event!( + Milter(MilterEvent::ActionReplyCode), + SpanId = self.data.session_id, + QueueId = queue_id, + Id = milter.id.to_string(), + Code = std::str::from_utf8(code) + .ok() + .and_then(|code| code.parse::().ok()), + Details = text.trim_end().to_string(), + Elapsed = time.elapsed(), + ); + } else { + trc::event!( + Milter(match &action { + Action::Discard => MilterEvent::ActionDiscard, + Action::Reject => MilterEvent::ActionReject, + Action::TempFail => MilterEvent::ActionTempFail, + Action::Shutdown => MilterEvent::ActionShutdown, + Action::ConnectionFailure => MilterEvent::ActionConnectionFailure, + Action::ReplyCode { .. } | Action::Accept | Action::Continue => { + unreachable!() + } + }), + SpanId = self.data.session_id, + QueueId = queue_id, + Id = milter.id.to_string(), + Elapsed = time.elapsed(), + ); + } return Err(match action { Action::Discard => FilterResponse::accept(), diff --git a/tests/src/smtp/inbound/milter.rs b/tests/src/smtp/inbound/milter.rs index c915ec79..53fb2de1 100644 --- a/tests/src/smtp/inbound/milter.rs +++ b/tests/src/smtp/inbound/milter.rs @@ -188,7 +188,7 @@ async fn milter_session() { event.inner.typ, EventType::Milter(MilterEvent::ActionReplyCode) ); - assert_eq!(event.value_as_str(Key::Code), Some("321")); + assert_eq!(event.value_as_uint(Key::Code), Some(321)); assert_eq!(event.value_as_str(Key::Details), Some("test")); // Test accept with header addition -- 2.54.0