Import upstream v0.16.22, stripped

Upstream commit: 474dd0229cb20cf513036619781ed97bd8073c3f
Enterprise-only files removed or emptied: 63
Enterprise-only snippets removed: 117 in 50 files
Dangling module declarations removed: 5
Cargo edits turning enterprise off: 14
Verification: clean
Enterprise feature gates left for rebuilt features: 19 in 18 files

Produced by tools/fork/strip.py. The full report is in docs/fork/strip-reports/ on main.
This commit is contained in:
2026-09-18 10:21:56 -07:00
commit 7dae9b29fd
1650 changed files with 485521 additions and 0 deletions
+971
View File
@@ -0,0 +1,971 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
smtp::{
inbound::TestMessage,
session::{TestSession, VerifyResponse, load_test_message},
},
utils::server::TestServerBuilder,
};
use ahash::AHashSet;
use common::{
config::smtp::session::{Milter, MilterVersion, Stage},
expr::if_block::IfBlock,
manager::application::Resource,
};
use http_proto::{ToHttpResponse, request::fetch_body};
use hyper::{body, server::conn::http1, service::service_fn};
use hyper_util::rt::TokioIo;
use mail_auth::AuthenticatedMessage;
use mail_parser::MessageParser;
use registry::{
schema::{
enums::{self, MtaStage},
prelude::{ObjectType, Property},
structs::{Expression, MtaHook, MtaMilter, MtaStageRcpt},
},
types::map::Map,
};
use serde::Deserialize;
use smtp::{
core::SessionData,
inbound::{
hooks::{self, Request, SmtpResponse},
milter::{
Action, Command, Macros, MilterClient, Modification, Options, Response,
receiver::{FrameResult, Receiver},
},
},
};
use std::{fs, net::SocketAddr, path::PathBuf, sync::Arc, time::Duration};
use tokio::{
io::{AsyncReadExt, AsyncWriteExt},
net::{TcpListener, TcpStream},
sync::watch,
};
#[derive(Debug, Deserialize)]
struct HeaderTest {
modifications: Vec<Modification>,
result: String,
}
#[tokio::test]
async fn milter_session() {
let mut test = TestServerBuilder::new("smtp_milter_test")
.await
.with_http_listener(19014)
.await
.capture_queue()
.disable_services()
.build()
.await;
// Add test settings
let admin = test.account("admin");
admin.mta_no_auth().await;
admin.mta_allow_relaying().await;
admin
.registry_create_object(MtaMilter {
enable: Expression {
else_: "true".into(),
..Default::default()
},
hostname: "127.0.0.1".into(),
port: 9332,
use_tls: false,
stages: Map::new(vec![MtaStage::Data]),
protocol_version: enums::MilterVersion::V6,
..Default::default()
})
.await;
admin.reload_settings().await;
test.reload_core();
test.expect_reload_settings().await;
let _rx = spawn_mock_milter_server();
tokio::time::sleep(Duration::from_millis(100)).await;
// Build session
let mut session = test.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.doe.org").await;
// Test reject
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"503 5.5.3",
)
.await;
test.assert_no_events();
// Test discard
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250 2.0.0",
)
.await;
test.assert_no_events();
// Test temp fail
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"451 4.3.5",
)
.await;
test.assert_no_events();
// Test shutdown
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"421 4.3.0",
)
.await;
test.assert_no_events();
// Test reply code
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"321",
)
.await;
test.assert_no_events();
// Test accept with header addition
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250 2.0.0",
)
.await;
test.expect_message()
.await
.read_lines(&test)
.await
.assert_contains("X-Hello: World")
.assert_contains("Subject: Is dinner ready?")
.assert_contains("Are you hungry yet?");
// Test accept with header replacement
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250 2.0.0",
)
.await;
test.expect_message()
.await
.read_lines(&test)
.await
.assert_contains("Subject: [SPAM] Saying Hello")
.assert_count("References: ", 1)
.assert_contains("Are you hungry yet?");
// Test accept with body replacement
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250 2.0.0",
)
.await;
test.expect_message()
.await
.read_lines(&test)
.await
.assert_contains("X-Spam: Yes")
.assert_contains("123456");
}
#[tokio::test]
async fn mta_hook_session() {
let mut test = TestServerBuilder::new("smtp_mta_hook_test")
.await
.with_http_listener(19015)
.await
.disable_services()
.capture_queue()
.build()
.await;
// Add test settings
let admin = test.account("admin");
admin.mta_no_auth().await;
admin
.registry_create_object(MtaStageRcpt {
allow_relaying: Expression {
else_: "true".into(),
..Default::default()
},
..Default::default()
})
.await;
admin
.registry_create_object(MtaHook {
enable: Expression {
else_: "true".into(),
..Default::default()
},
url: "http://127.0.0.1:9333".into(),
stages: Map::new(vec![MtaStage::Data]),
..Default::default()
})
.await;
admin.reload_settings().await;
test.reload_core();
test.expect_reload_settings().await;
let _rx = spawn_mock_mta_hook_server();
tokio::time::sleep(Duration::from_millis(100)).await;
// Build session
let mut session = test.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.doe.org").await;
// Test reject
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"503 5.5.3",
)
.await;
test.assert_no_events();
// Test discard
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250 2.0.0",
)
.await;
test.assert_no_events();
// Test temp fail
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"451 4.3.5",
)
.await;
test.assert_no_events();
// Test shutdown
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"421 4.3.0",
)
.await;
test.assert_no_events();
// Test reply code
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"321",
)
.await;
test.assert_no_events();
// Test accept with header addition
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250 2.0.0",
)
.await;
test.expect_message()
.await
.read_lines(&test)
.await
.assert_contains("X-Hello: World")
.assert_contains("Subject: Is dinner ready?")
.assert_contains("Are you hungry yet?");
// Test accept with header replacement
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250 2.0.0",
)
.await;
test.expect_message()
.await
.read_lines(&test)
.await
.assert_contains("Subject: [SPAM] Saying Hello")
.assert_count("References: ", 1)
.assert_contains("Are you hungry yet?");
// Test accept with body replacement
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250 2.0.0",
)
.await;
test.expect_message()
.await
.read_lines(&test)
.await
.assert_contains("X-Spam: Yes")
.assert_contains("123456");
}
#[test]
fn milter_address_modifications() {
let test_message = fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("smtp")
.join("milter")
.join("message.eml"),
)
.unwrap();
let parsed_test_message = AuthenticatedMessage::parse(test_message.as_bytes()).unwrap();
let mut data = SessionData::new(
"127.0.0.1".parse().unwrap(),
0,
"127.0.0.1".parse().unwrap(),
0,
Default::default(),
0,
);
// ChangeFrom
assert!(
data.apply_milter_modifications(
vec![Modification::ChangeFrom {
sender: "<>".into(),
args: "".into(),
}],
&parsed_test_message
)
.is_none()
);
let addr = data.mail_from.as_ref().unwrap();
assert_eq!(addr.address_lcase, "");
assert_eq!(addr.dsn_info, None);
assert_eq!(addr.flags, 0);
// ChangeFrom with parameters
assert!(
data.apply_milter_modifications(
vec![Modification::ChangeFrom {
sender: "[email protected]".into(),
args: "REQUIRETLS ENVID=abc123".into(), //"NOTIFY=SUCCESS,FAILURE ENVID=abc123\n".into()
}],
&parsed_test_message
)
.is_none()
);
let addr = data.mail_from.as_ref().unwrap();
assert_eq!(addr.address_lcase, "[email protected]");
assert_ne!(addr.flags, 0);
assert_eq!(addr.dsn_info, Some("abc123".into()));
// Add recipients
assert!(
data.apply_milter_modifications(
vec![
Modification::AddRcpt {
recipient: "[email protected]".into(),
args: "".into(),
},
Modification::AddRcpt {
recipient: "[email protected]".into(),
args: "NOTIFY=SUCCESS,FAILURE ORCPT=rfc822;[email protected]".into(),
},
Modification::AddRcpt {
recipient: "<[email protected]>".into(),
args: "".into(),
},
Modification::AddRcpt {
recipient: "<>".into(),
args: "".into(),
},
],
&parsed_test_message
)
.is_none()
);
assert_eq!(data.rcpt_to.len(), 2);
let addr = data.rcpt_to.first().unwrap();
assert_eq!(addr.address_lcase, "[email protected]");
assert_eq!(addr.dsn_info, None);
assert_eq!(addr.flags, 0);
let addr = data.rcpt_to.last().unwrap();
assert_eq!(addr.address_lcase, "[email protected]");
assert_ne!(addr.flags, 0);
assert_eq!(addr.dsn_info, Some("[email protected]".into()));
// Remove recipients
assert!(
data.apply_milter_modifications(
vec![
Modification::DeleteRcpt {
recipient: "[email protected]".into(),
},
Modification::DeleteRcpt {
recipient: "<>".into(),
},
],
&parsed_test_message
)
.is_none()
);
assert_eq!(data.rcpt_to.len(), 1);
let addr = data.rcpt_to.last().unwrap();
assert_eq!(addr.address_lcase, "[email protected]");
assert_ne!(addr.flags, 0);
assert_eq!(addr.dsn_info, Some("[email protected]".into()));
}
#[test]
fn milter_message_modifications() {
// Read test message
let milter_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("smtp")
.join("milter");
let test_message = fs::read_to_string(milter_path.join("message.eml")).unwrap();
let tests = serde_json::from_str::<Vec<HeaderTest>>(
&fs::read_to_string(milter_path.join("message.json")).unwrap(),
)
.unwrap();
let parsed_test_message = AuthenticatedMessage::parse(test_message.as_bytes()).unwrap();
let mut session_data = SessionData::new(
"127.0.0.1".parse().unwrap(),
0,
"127.0.0.1".parse().unwrap(),
0,
Default::default(),
0,
);
for test in tests {
assert_eq!(
test.result,
String::from_utf8(
session_data
.apply_milter_modifications(test.modifications, &parsed_test_message)
.unwrap()
)
.unwrap()
)
}
}
#[test]
fn milter_frame_receiver() {
let mut stream = Vec::new();
for i in 0u32..100u32 {
stream.extend_from_slice((i + 1).to_be_bytes().as_ref());
stream.push(i as u8);
for v in 0..i {
stream.push(v as u8);
}
}
for chunk_size in [stream.len(), 1, 2, 3, 4, 10, 20, 30, 40, 100, 200, 300, 400] {
let mut receiver = Receiver::with_max_frame_len(100);
let mut frame_num = 0;
'outer: for chunk in stream.chunks(chunk_size) {
loop {
match receiver.read_frame(chunk) {
FrameResult::Frame(bytes) => {
/*println!(
"frame {frame_num}, chunk: {chunk_size}, {}",
if matches!(bytes, std::borrow::Cow::Borrowed(_)) {
"borrowed"
} else {
"owned"
}
);*/
assert_eq!(*bytes.first().unwrap(), frame_num);
assert_eq!(bytes.len(), frame_num as usize + 1);
frame_num += 1;
}
FrameResult::Incomplete => continue 'outer,
FrameResult::TooLarge(size) => {
panic!("Frame too large: {size}")
}
}
}
}
assert_eq!(frame_num, 100, "chunk_size: {}", chunk_size);
}
}
#[tokio::test]
#[ignore]
async fn milter_client_test() {
//const PORT : u16 = 11332;
const PORT: u16 = 7357;
let mut client = MilterClient::connect(
&Milter {
enable: IfBlock::empty(ObjectType::MtaMilter.singleton(), Property::Enable),
id: ObjectType::MtaMilter.singleton(),
addrs: vec![SocketAddr::from(([127, 0, 0, 1], PORT))],
hostname: "localhost".into(),
port: PORT,
timeout_connect: Duration::from_secs(10),
timeout_command: Duration::from_secs(30),
timeout_data: Duration::from_secs(30),
tls: false,
tls_allow_invalid_certs: false,
tempfail_on_error: false,
max_frame_len: 5000000,
protocol_version: MilterVersion::V6,
flags_actions: None,
flags_protocol: None,
run_on_stage: AHashSet::from([Stage::Data]),
},
0,
)
.await
.unwrap();
client.init().await.unwrap();
let raw_message = load_test_message("arc", "messages");
let message = MessageParser::new().parse(raw_message.as_bytes()).unwrap();
let r = client
.connection(
"gmail.com",
"127.0.0.1".parse().unwrap(),
1235,
Macros::new(),
)
.await
.unwrap();
println!("CONNECT: {:?}", r);
let r = client
.mail_from("[email protected]", None::<&[&str]>, Macros::new())
.await
.unwrap();
println!("MAIL FROM: {:?}", r);
let r = client
.rcpt_to("[email protected]", None::<&[&str]>, Macros::new())
.await
.unwrap();
println!("RCPT TO: {:?}", r);
let r = client.data().await.unwrap();
println!("DATA: {:?}", r);
let r = client.headers(message.headers_raw()).await.unwrap();
println!("HEADERS: {:?}", r);
let r = client
.body(&message.raw_message()[message.root_part().raw_body_offset() as usize..])
.await
.unwrap();
println!("BODY: {:?}", r);
client.quit().await.unwrap();
}
pub fn spawn_mock_milter_server() -> watch::Sender<bool> {
let (tx, rx) = watch::channel(true);
let tests = Arc::new(
serde_json::from_str::<Vec<HeaderTest>>(
&fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("smtp")
.join("milter")
.join("message.json"),
)
.unwrap(),
)
.unwrap(),
);
tokio::spawn(async move {
let listener = TcpListener::bind("127.0.0.1:9332")
.await
.unwrap_or_else(|e| {
panic!("Failed to bind mock Milter server to 127.0.0.1:9332: {e}");
});
let mut rx_ = rx.clone();
//println!("Mock Milter server listening on port 9332");
loop {
tokio::select! {
stream = listener.accept() => {
match stream {
Ok((stream, _)) => {
tokio::spawn(accept_milter(stream, rx.clone(), tests.clone()));
}
Err(err) => {
panic!("Something went wrong: {err}" );
}
}
},
_ = rx_.changed() => {
//println!("Mock Milter server stopping");
break;
}
};
}
});
tx
}
async fn accept_milter(
mut stream: TcpStream,
mut rx: watch::Receiver<bool>,
tests: Arc<Vec<HeaderTest>>,
) {
let mut buf = vec![0u8; 1024];
let mut receiver = Receiver::with_max_frame_len(5000000);
let mut action = None;
let mut modifications = None;
'outer: loop {
let br = tokio::select! {
br = stream.read(&mut buf) => {
match br {
Ok(br) => {
br
}
Err(_) => {
break;
}
}
},
_ = rx.changed() => {
break;
}
};
if br == 0 {
break;
}
loop {
match receiver.read_frame(&buf[..br]) {
FrameResult::Frame(bytes) => {
let cmd = Command::deserialize(bytes.as_ref());
println!("CMD: {cmd}");
let response = match cmd {
Command::Abort | Command::Macro { .. } => continue,
Command::Body { .. }
| Command::Data
| Command::Connect { .. }
| Command::Header { .. }
| Command::Helo { .. }
| Command::Rcpt { .. }
| Command::QuitNewConnection
| Command::EndOfHeader => Response::Action(Action::Accept),
Command::OptionNegotiation(_) => Response::OptionNegotiation(Options {
version: 6,
actions: 0,
protocol: 0,
}),
Command::MailFrom { sender, .. } => {
let sender = std::str::from_utf8(sender).unwrap();
action = match sender
.strip_prefix('<')
.unwrap()
.split_once('@')
.unwrap()
.0
{
"accept" => Action::Accept,
"reject" => Action::Reject,
"discard" => Action::Discard,
"temp_fail" => Action::TempFail,
"shutdown" => Action::Shutdown,
"conn_fail" => Action::ConnectionFailure,
"reply_code" => Action::ReplyCode {
code: *b"321",
text: "test".into(),
},
test_num => {
modifications = tests[test_num.parse::<usize>().unwrap()]
.modifications
.clone()
.into();
Action::Accept
}
}
.into();
Response::Action(Action::Accept)
}
Command::Quit => break 'outer,
Command::EndOfBody => {
if let Some(modifications) = modifications.take() {
for modification in modifications {
// Write modifications
stream
.write_all(
&Response::Modification(modification).serialize(),
)
.await
.unwrap();
}
}
Response::Action(action.take().unwrap())
}
};
// Write response
stream.write_all(&response.serialize()).await.unwrap();
}
FrameResult::Incomplete => continue 'outer,
FrameResult::TooLarge(size) => {
panic!("Frame too large: {size}")
}
}
}
}
}
pub fn spawn_mock_mta_hook_server() -> watch::Sender<bool> {
let (tx, rx) = watch::channel(true);
let tests = Arc::new(
serde_json::from_str::<Vec<HeaderTest>>(
&fs::read_to_string(
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("resources")
.join("smtp")
.join("milter")
.join("message.json"),
)
.unwrap(),
)
.unwrap(),
);
tokio::spawn(async move {
let listener = TcpListener::bind("127.0.0.1:9333")
.await
.unwrap_or_else(|e| {
panic!("Failed to bind mock Milter server to 127.0.0.1:9333: {e}");
});
let mut rx_ = rx.clone();
//println!("Mock jMilter server listening on port 9333");
loop {
tokio::select! {
stream = listener.accept() => {
match stream {
Ok((stream, _)) => {
let _ = http1::Builder::new()
.keep_alive(false)
.serve_connection(
TokioIo::new(stream),
service_fn(|mut req: hyper::Request<body::Incoming>| {
let tests = tests.clone();
async move {
let request = serde_json::from_slice::<Request>(&fetch_body(&mut req, 1024 * 1024,0).await.unwrap())
.unwrap();
let response = handle_mta_hook(request, tests);
Ok::<_, hyper::Error>(
Resource::new("application/json", serde_json::to_string(&response).unwrap().into_bytes())
.into_http_response().build(),
)
}
}),
)
.await;
}
Err(err) => {
panic!("Something went wrong: {err}" );
}
}
},
_ = rx_.changed() => {
//println!("Mock jMilter server stopping");
break;
}
};
}
});
tx
}
fn handle_mta_hook(request: Request, tests: Arc<Vec<HeaderTest>>) -> hooks::Response {
match request
.envelope
.unwrap()
.from
.address
.split_once('@')
.unwrap()
.0
{
"accept" => hooks::Response {
action: hooks::Action::Accept,
response: None,
modifications: vec![],
},
"reject" => hooks::Response {
action: hooks::Action::Reject,
response: None,
modifications: vec![],
},
"discard" => hooks::Response {
action: hooks::Action::Discard,
response: None,
modifications: vec![],
},
"temp_fail" => hooks::Response {
action: hooks::Action::Reject,
response: SmtpResponse {
status: 451.into(),
enhanced_status: Some("4.3.5".into()),
message: Some("Unable to accept message at this time.".into()),
disconnect: false,
}
.into(),
modifications: vec![],
},
"shutdown" => hooks::Response {
action: hooks::Action::Reject,
response: SmtpResponse {
status: 421.into(),
enhanced_status: Some("4.3.0".into()),
message: Some("Server shutting down".into()),
disconnect: false,
}
.into(),
modifications: vec![],
},
"conn_fail" => hooks::Response {
action: hooks::Action::Accept,
response: SmtpResponse {
disconnect: true,
..Default::default()
}
.into(),
modifications: vec![],
},
"reply_code" => hooks::Response {
action: hooks::Action::Reject,
response: SmtpResponse {
status: 321.into(),
enhanced_status: Some("3.1.1".into()),
message: Some("Test".into()),
disconnect: false,
}
.into(),
modifications: vec![],
},
test_num => hooks::Response {
action: hooks::Action::Accept,
response: None,
modifications: tests[test_num.parse::<usize>().unwrap()]
.modifications
.iter()
.map(|m| match m {
Modification::ChangeFrom { sender, args } => hooks::Modification::ChangeFrom {
value: sender.clone(),
parameters: args
.split_whitespace()
.map(|arg| {
let (key, value) = arg.split_once('=').unwrap();
(key.into(), Some(value.into()))
})
.collect(),
},
Modification::AddRcpt { recipient, args } => {
hooks::Modification::AddRecipient {
value: recipient.clone(),
parameters: args
.split_whitespace()
.map(|arg| {
let (key, value) = arg.split_once('=').unwrap();
(key.into(), Some(value.into()))
})
.collect(),
}
}
Modification::DeleteRcpt { recipient } => {
hooks::Modification::DeleteRecipient {
value: recipient.clone(),
}
}
Modification::ReplaceBody { value } => hooks::Modification::ReplaceContents {
value: String::from_utf8(value.clone()).unwrap(),
},
Modification::AddHeader { name, value } => hooks::Modification::AddHeader {
name: name.clone(),
value: value.clone(),
},
Modification::InsertHeader { index, name, value } => {
hooks::Modification::InsertHeader {
index: *index,
name: name.clone(),
value: value.clone(),
}
}
Modification::ChangeHeader { index, name, value } => {
hooks::Modification::ChangeHeader {
index: *index,
name: name.clone(),
value: value.clone(),
}
}
Modification::Quarantine { reason } => hooks::Modification::AddHeader {
name: "X-Quarantine".into(),
value: reason.clone(),
},
})
.collect(),
},
}
}