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:
@@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user