Milter: keep the reply code and text in delivery traces #293

Merged
jcoffey-dev merged 2 commits from fix/milter-reply-code-trace into main 2026-11-05 04:46:21 +00:00
2 changed files with 69 additions and 19 deletions

No files matched your search

+32 -17
View File
@@ -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 super::{Action, Error, Macros, Modification};
@@ -77,23 +79,36 @@ impl<T: SessionStream> Session<T> {
}
}
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::<u16>().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(),
+37 -2
View File
@@ -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(
"[email protected]",
&["[email protected]"],
"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(