diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs index e910c2ba..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,23 +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(), - ); + 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 fa51c4d1..53fb2de1 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_uint(Key::Code), Some(321)); + assert_eq!(event.value_as_str(Key::Details), Some("test")); + // Test accept with header addition session .send_message(