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
File diff suppressed because it is too large Load Diff
+183
View File
@@ -0,0 +1,183 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
smtp::{
inbound::{TestMessage, TestQueueEvent},
session::{TestSession, VerifyResponse},
},
utils::{dns::DnsCache, server::TestServerBuilder},
};
use mail_auth::{DnssecStatus, MX};
use registry::schema::structs::{Expression, MtaStageData};
use smtp_proto::{MAIL_REQUIRETLS, MAIL_RET_HDRS, MAIL_SMTPUTF8, RCPT_NOTIFY_NEVER};
use std::time::{Duration, Instant};
#[tokio::test]
#[serial_test::serial]
async fn extensions() {
let mut local = TestServerBuilder::new("smtp_ext_local")
.await
.with_http_listener(19020)
.await
.disable_services()
.capture_queue()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_ext_remote")
.await
.with_http_listener(19021)
.await
.with_smtp_listener(9925)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin.mta_allow_relaying().await;
local_admin.mta_no_auth().await;
local_admin.mta_all_extensions().await;
local_admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let remote_admin = remote.account("admin");
remote_admin.mta_all_extensions().await;
remote_admin.mta_allow_relaying().await;
remote_admin.mta_no_auth().await;
remote_admin
.registry_create_object(MtaStageData {
max_message_size: Expression {
else_: "1500".into(),
..Default::default()
},
add_date_header: Expression {
else_: "true".into(),
..Default::default()
},
add_message_id_header: Expression {
else_: "true".into(),
..Default::default()
},
add_received_header: Expression {
else_: "true".into(),
..Default::default()
},
add_received_spf_header: Expression {
else_: "true".into(),
..Default::default()
},
add_auth_results_header: Expression {
else_: "true".into(),
..Default::default()
},
add_return_path_header: Expression {
else_: "false".into(),
..Default::default()
},
enable_spam_filter: Expression {
else_: "false".into(),
..Default::default()
},
..Default::default()
})
.await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
// Add mock DNS entries
local.server.mx_add(
"foobar.org",
vec![MX {
exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
local.server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message(
"[email protected]",
&["<[email protected]> NOTIFY=SUCCESS,FAILURE"],
"test:no_dkim",
"250",
)
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
local
.expect_message()
.await
.read_lines(&local)
.await
.assert_contains("<[email protected]> (delivered to")
.assert_contains("Final-Recipient: rfc822;[email protected]")
.assert_contains("Action: delivered");
local.read_event().await.assert_done();
remote
.expect_message()
.await
.read_lines(&remote)
.await
.assert_contains("using TLSv1.3 with cipher");
// Test SIZE extension
session
.send_message("[email protected]", &["[email protected]"], "test:arc", "250")
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
local
.expect_message()
.await
.read_lines(&local)
.await
.assert_contains("<[email protected]> (host 'mx.foobar.org' rejected command 'MAIL FROM:")
.assert_contains("Action: failed")
.assert_contains("Diagnostic-Code: smtp;552")
.assert_contains("Status: 5.3.4");
local.read_event().await.assert_done();
remote.assert_no_events();
// Test DSN, SMTPUTF8 and REQUIRETLS extensions
session
.send_message(
"<[email protected]> ENVID=abc123 RET=HDRS REQUIRETLS SMTPUTF8",
&["<[email protected]> NOTIFY=NEVER"],
"test:no_dkim",
"250",
)
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
local.read_event().await.assert_done();
let message = remote.expect_message().await;
assert_eq!(message.message.env_id, Some("abc123".into()));
assert!((message.message.flags & MAIL_RET_HDRS) != 0);
assert!((message.message.flags & MAIL_REQUIRETLS) != 0);
assert!((message.message.flags & MAIL_SMTPUTF8) != 0);
assert!((message.message.recipients.last().unwrap().flags & RCPT_NOTIFY_NEVER) != 0);
}
+137
View File
@@ -0,0 +1,137 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
smtp::session::TestSession,
utils::{dns::DnsCache, server::TestServerBuilder},
};
use mail_auth::{DnssecStatus, MX};
use registry::{
schema::{
enums::MtaProtocol,
structs::{
Expression, ExpressionMatch, MtaOutboundStrategy, MtaRoute, MtaRouteRelay, MtaStageRcpt,
},
},
types::list::List,
};
use std::time::{Duration, Instant};
use store::write::now;
#[tokio::test]
#[serial_test::serial]
async fn fallback_relay() {
let mut local = TestServerBuilder::new("smtp_fallback_local")
.await
.with_http_listener(19022)
.await
.disable_services()
.capture_queue()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_fallback_remote")
.await
.with_http_listener(19023)
.await
.with_smtp_listener(9925)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin
.registry_create_object(MtaStageRcpt {
max_recipients: Expression {
else_: "100".into(),
..Default::default()
},
allow_relaying: Expression {
else_: "true".into(),
..Default::default()
},
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaOutboundStrategy {
route: Expression {
match_: List::from_iter([ExpressionMatch {
if_: "retry_num > 0".into(),
then: "'fallback'".into(),
}]),
else_: "'mx'".into(),
},
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaRoute::Relay(MtaRouteRelay {
address: "fallback.foobar.org".into(),
implicit_tls: false,
allow_invalid_certs: true,
name: "fallback".into(),
port: 9925,
protocol: MtaProtocol::Smtp,
..Default::default()
}))
.await;
local_admin.mta_no_auth().await;
local_admin.mta_all_extensions().await;
local_admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let remote_admin = remote.account("admin");
remote_admin.mta_allow_relaying().await;
remote_admin.mta_no_auth().await;
remote_admin.mta_all_extensions().await;
remote_admin.mta_allow_non_fqdn().await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
// Add mock DNS entries
local.server.mx_add(
"foobar.org",
vec![MX {
exchanges: vec!["_dns_error.foobar.org".into()].into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
local.server.ipv4_add(
"fallback.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
let mut retry = local.expect_message().await;
let prev_due = retry.message.recipients[0].retry.due;
let next_due = now();
let queue_id = retry.queue_id;
retry.message.recipients[0].retry.due = next_due;
retry.save_changes(&local.server, prev_due.into()).await;
local
.delivery_attempt(queue_id)
.await
.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
remote.expect_message().await;
}
+129
View File
@@ -0,0 +1,129 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
smtp::session::TestSession,
utils::{dns::DnsCache, server::TestServerBuilder},
};
use mail_auth::{DnssecStatus, MX};
use registry::{
schema::{
enums::MtaIpStrategy,
prelude::{ObjectType, Property},
structs::MtaRoute,
},
types::EnumImpl,
};
use serde_json::json;
use std::time::{Duration, Instant};
#[tokio::test]
#[serial_test::serial]
async fn ip_lookup_strategy() {
let mut local = TestServerBuilder::new("smtp_iplookup_local")
.await
.with_http_listener(19024)
.await
.disable_services()
.capture_queue()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_iplookup_remote")
.await
.with_http_listener(19025)
.await
.with_smtp_listener(9925)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin.mta_allow_relaying().await;
local_admin.mta_no_auth().await;
local_admin.reload_settings().await;
let (mx_route_id, _) = local_admin
.registry_get_all::<MtaRoute>()
.await
.into_iter()
.find(|(_, s)| matches!(s, MtaRoute::Mx(_)))
.unwrap();
local.reload_core();
local.expect_reload_settings().await;
let remote_admin = remote.account("admin");
remote_admin.mta_allow_relaying().await;
remote_admin.mta_no_auth().await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
for strategy in [MtaIpStrategy::V6Only, MtaIpStrategy::V6ThenV4] {
local
.account("admin")
.registry_update_object(
ObjectType::MtaRoute,
mx_route_id,
json!({
Property::IpLookupStrategy: strategy.as_str(),
}),
)
.await;
local.account("admin").reload_settings().await;
local.expect_reload_settings().await;
println!("-> Strategy: {:?}", strategy);
// Add mock DNS entries
local.server.mx_add(
"foobar.org",
vec![MX {
exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
if matches!(strategy, MtaIpStrategy::V6ThenV4) {
local.server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
}
local.server.ipv6_add(
"mx.foobar.org",
vec!["::1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
// Retry on failed STARTTLS
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
if matches!(strategy, MtaIpStrategy::V6ThenV4) {
remote.expect_message().await;
} else {
let message = local.last_queued_message().await;
let status = message.message.recipients[0].status.to_string();
assert!(
status.contains("Connection refused"),
"Message: {:?}",
message
);
local.expect_refresh().await;
}
}
}
+296
View File
@@ -0,0 +1,296 @@
/*
* 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},
},
utils::{dns::DnsCache, server::TestServerBuilder},
};
use common::{config::smtp::queue::QueueName, ipc::QueueEvent};
use registry::{
schema::{
enums::{MtaProtocol, NetworkListenerProtocol},
structs::{
Expression, ExpressionMatch, MtaConnectionStrategy, MtaDeliveryExpiration,
MtaDeliveryExpirationTtl, MtaDeliverySchedule, MtaDeliveryScheduleInterval,
MtaDeliveryScheduleIntervals, MtaDeliveryScheduleIntervalsOrDefault,
MtaOutboundStrategy, MtaRoute, MtaRouteRelay, MtaStageRcpt, MtaVirtualQueue,
},
},
types::list::List,
};
use smtp::queue::spool::{QUEUE_REFRESH, SmtpSpool};
use std::time::{Duration, Instant};
use store::write::now;
#[tokio::test]
#[serial_test::serial]
async fn lmtp_delivery() {
let mut local = TestServerBuilder::new("lmtp_delivery_local")
.await
.with_http_listener(19026)
.await
.disable_services()
.capture_queue()
.build()
.await;
let mut remote = TestServerBuilder::new("lmtp_delivery_remote")
.await
.with_http_listener(19027)
.await
.with_listener(NetworkListenerProtocol::Lmtp, "lmtp-debug", 9924, true)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin
.registry_create_object(MtaStageRcpt {
max_recipients: Expression {
else_: "100".into(),
..Default::default()
},
allow_relaying: Expression {
else_: "true".into(),
..Default::default()
},
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaOutboundStrategy {
route: Expression {
match_: List::from_iter([ExpressionMatch {
if_: "rcpt_domain = 'foobar.org'".into(),
then: "'lmtp'".into(),
}]),
else_: "'mx'".into(),
},
schedule: Expression {
match_: List::from_iter([ExpressionMatch {
if_: "rcpt_domain = 'foobar.org'".into(),
then: "'foobar'".into(),
}]),
else_: "'default'".into(),
},
connection: Expression {
else_: "'impatient'".into(),
..Default::default()
},
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaRoute::Relay(MtaRouteRelay {
address: "lmtp.foobar.org".into(),
allow_invalid_certs: true,
implicit_tls: true,
name: "lmtp".into(),
port: 9924,
protocol: MtaProtocol::Lmtp,
..Default::default()
}))
.await;
let queue_id = local_admin
.registry_create_object(MtaVirtualQueue {
name: "default".into(),
threads_per_node: 25,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "foobar".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([
MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
},
MtaDeliveryScheduleInterval {
duration: 2_000u64.into(),
},
]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 4_000u64.into(),
}),
queue_id,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "default".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 5_000u64.into(),
}),
queue_id,
description: None,
})
.await;
local_admin
.registry_create_object(MtaConnectionStrategy {
name: "impatient".into(),
connect_timeout: 1_000u64.into(),
data_timeout: 50u64.into(),
..Default::default()
})
.await;
local_admin.mta_no_auth().await;
local_admin.mta_all_extensions().await;
local_admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let remote_admin = remote.account("admin");
remote_admin.mta_allow_relaying().await;
remote_admin.mta_no_auth().await;
remote_admin.mta_all_extensions().await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
// Add mock DNS entries
local.server.ipv4_add(
"lmtp.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message(
"[email protected]",
&[
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
],
"test:no_dkim",
"250",
)
.await;
local
.expect_message_for_queue_then_deliver("default")
.await
.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(500)).await;
let mut dsn = Vec::new();
loop {
match local.try_read_event().await {
Some(QueueEvent::Refresh | QueueEvent::WorkerDone { .. }) => {}
Some(QueueEvent::Paused(_)) | Some(QueueEvent::ReloadSettings) => unreachable!(),
None | Some(QueueEvent::Stop) => break,
}
let mut events = local.all_queued_messages().await;
if events.messages.is_empty() {
let now = now();
if events.next_refresh < now + QUEUE_REFRESH {
tokio::time::sleep(Duration::from_secs(events.next_refresh - now)).await;
events = local.all_queued_messages().await;
} else {
break;
}
}
for event in events.messages {
let message = local
.server
.read_message(event.queue_id, QueueName::default())
.await
.unwrap();
if message.message.return_path.is_empty() {
message
.clone()
.remove(&local.server, event.due.into())
.await;
dsn.push(message);
} else {
event.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
local.assert_queue_is_empty().await;
assert_eq!(dsn.len(), 4);
let mut dsn = dsn.into_iter();
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (delivered to")
.assert_contains("<[email protected]> (delivered to")
.assert_contains("<[email protected]> (delivered to")
.assert_contains("<[email protected]> (failed to lookup")
.assert_contains("<[email protected]> (host 'lmtp.foobar.org' rejected command");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (host 'lmtp.foobar.org' rejected")
.assert_contains("Action: delayed");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (host 'lmtp.foobar.org' rejected")
.assert_contains("Action: delayed");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (host 'lmtp.foobar.org' rejected")
.assert_contains("Action: failed");
assert_eq!(
remote
.expect_message()
.await
.message
.recipients
.into_iter()
.map(|r| r.address().to_string())
.collect::<Vec<_>>(),
vec![
"[email protected]".to_string(),
"[email protected]".to_string(),
"[email protected]".to_string()
]
);
remote.assert_no_events();
}
+15
View File
@@ -0,0 +1,15 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
pub mod dane;
pub mod extensions;
pub mod fallback_relay;
pub mod ip_lookup;
pub mod lmtp;
pub mod mta_sts;
pub mod smtp;
pub mod throttle;
pub mod tls;
+366
View File
@@ -0,0 +1,366 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
smtp::{
inbound::{TestMessage, TestQueueEvent, TestReportingEvent},
session::{TestSession, VerifyResponse},
},
utils::{dns::DnsCache, server::TestServerBuilder},
};
use common::{config::smtp::resolver::Policy, ipc::PolicyType};
use mail_auth::{
DnssecStatus, MX,
common::parse::TxtRecordParser,
mta_sts::{MtaSts, ReportUri, TlsRpt},
report::tlsrpt::ResultType,
};
use registry::{
schema::{
enums::{MtaRequiredOrOptional, NetworkListenerProtocol},
prelude::ObjectType,
structs::{Expression, MtaTlsStrategy, NetworkListener, TlsReportSettings},
},
types::{map::Map, socketaddr::SocketAddr},
};
use smtp::outbound::mta_sts::{lookup::STS_TEST_POLICY, parse::ParsePolicy};
use std::{
str::FromStr,
sync::Arc,
time::{Duration, Instant},
};
#[tokio::test]
#[serial_test::serial]
async fn mta_sts_verify() {
let mut local = TestServerBuilder::new("smtp_mta_sts_local")
.await
.with_http_listener(19028)
.await
.disable_services()
.capture_queue()
.capture_reporting()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_mta_sts_remote")
.await
.with_http_listener(19029)
.await
.with_smtp_listener(9925)
.await
.with_dummy_tls_cert(["*.foobar.org"])
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin.mta_allow_relaying().await;
local_admin.mta_no_auth().await;
local_admin
.registry_create_object(TlsReportSettings {
send_frequency: Expression {
else_: "weekly".into(),
..Default::default()
},
..Default::default()
})
.await;
let (tls_strategy_id, mut tls_strategy) = local_admin
.registry_get_all::<MtaTlsStrategy>()
.await
.into_iter()
.find(|(_, s)| s.name == "default")
.unwrap();
tls_strategy.mta_sts = MtaRequiredOrOptional::Require;
tls_strategy.allow_invalid_certs = false;
let mut tls_strategy = serde_json::to_value(tls_strategy).unwrap();
tls_strategy
.as_object_mut()
.unwrap()
.retain(|k, _| k != "name");
local_admin
.registry_update_object(ObjectType::MtaTlsStrategy, tls_strategy_id, tls_strategy)
.await;
local_admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let remote_admin = remote.account("admin");
remote_admin.mta_no_auth().await;
remote_admin.mta_allow_relaying().await;
remote_admin.mta_allow_non_fqdn().await;
remote_admin.mta_add_all_headers().await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
// Add mock DNS entries
local.server.mx_add(
"foobar.org",
vec![MX {
exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
local.server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
local.server.txt_add(
"_smtp._tls.foobar.org",
TlsRpt::parse(b"v=TLSRPTv1; rua=mailto:[email protected]").unwrap(),
Instant::now() + Duration::from_secs(10),
);
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
local
.expect_message()
.await
.read_lines(&local)
.await
.assert_contains("<[email protected]> (MTA-STS failed to authenticate")
.assert_contains("Record not =")
.assert_contains("found");
local.read_event().await.assert_done();
// Expect TLS failure report
let report = local.read_report().await.unwrap_tls();
assert_eq!(report.domain, "foobar.org");
assert_eq!(report.policy, PolicyType::Sts(None));
assert_eq!(
report.failure.as_ref().unwrap().result_type,
ResultType::Other
);
assert_eq!(
report.tls_record.rua,
vec![ReportUri::Mail("[email protected]".to_string())]
);
// MTA-STS policy fetch failure
local.server.txt_add(
"_mta-sts.foobar.org",
MtaSts::parse(b"v=STSv1; id=policy_will_fail;").unwrap(),
Instant::now() + Duration::from_secs(10),
);
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
local
.expect_message()
.await
.read_lines(&local)
.await
.assert_contains("<[email protected]> (MTA-STS failed to authenticate")
.assert_contains("No 'mx' entries found");
local.read_event().await.assert_done();
// Expect TLS failure report
let report = local.read_report().await.unwrap_tls();
assert_eq!(report.policy, PolicyType::Sts(None));
assert_eq!(
report.failure.as_ref().unwrap().result_type,
ResultType::StsPolicyInvalid
);
// MTA-STS policy does not authorize mx.foobar.org
let policy = concat!(
"version: STSv1\n",
"mode: enforce\n",
"mx: mail.foobar.net\n",
"max_age: 604800\n"
);
STS_TEST_POLICY.lock().extend_from_slice(policy.as_bytes());
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
local
.expect_message()
.await
.read_lines(&local)
.await
.assert_contains("<[email protected]> (MTA-STS failed to authenticate")
.assert_contains("not authorized by policy");
local.read_event().await.assert_done();
// Expect TLS failure report
let report = local.read_report().await.unwrap_tls();
assert_eq!(
report.policy,
PolicyType::Sts(
Arc::new(Policy::parse(policy, "policy_will_fail".to_string()).unwrap()).into()
)
);
assert_eq!(
report.failure.as_ref().unwrap().receiving_mx_hostname,
Some("mx.foobar.org".to_string())
);
assert_eq!(
report.failure.as_ref().unwrap().result_type,
ResultType::ValidationFailure
);
remote.assert_no_events();
// MTA-STS successful validation
local.server.txt_add(
"_mta-sts.foobar.org",
MtaSts::parse(b"v=STSv1; id=policy_will_work;").unwrap(),
Instant::now() + Duration::from_secs(10),
);
let policy = concat!(
"version: STSv1\n",
"mode: enforce\n",
"mx: *.foobar.org\n",
"max_age: 604800\n"
);
STS_TEST_POLICY.lock().clear();
STS_TEST_POLICY.lock().extend_from_slice(policy.as_bytes());
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
local.read_event().await.assert_done();
remote
.expect_message()
.await
.read_lines(&remote)
.await
.assert_contains("using TLSv1.3 with cipher");
// Expect TLS success report
let report = local.read_report().await.unwrap_tls();
assert_eq!(
report.policy,
PolicyType::Sts(
Arc::new(Policy::parse(policy, "policy_will_work".to_string()).unwrap()).into()
)
);
assert!(report.failure.is_none());
}
#[tokio::test]
#[serial_test::serial]
async fn mta_sts_testing_mode_does_not_enforce_tls() {
let mut local = TestServerBuilder::new("smtp_mta_sts_testing_local")
.await
.with_http_listener(19051)
.await
.disable_services()
.capture_queue()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_mta_sts_testing_remote")
.await
.with_http_listener(19052)
.await
.with_object(NetworkListener {
bind: Map::new(vec![SocketAddr::from_str("0.0.0.0:9925").unwrap()]),
name: "smtp".to_string(),
protocol: NetworkListenerProtocol::Smtp,
use_tls: false,
tls_implicit: false,
..Default::default()
})
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin.mta_allow_relaying().await;
local_admin.mta_no_auth().await;
local_admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let remote_admin = remote.account("admin");
remote_admin.mta_no_auth().await;
remote_admin.mta_allow_relaying().await;
remote_admin.mta_allow_non_fqdn().await;
remote_admin.mta_add_all_headers().await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
local.server.mx_add(
"foobar.org",
vec![MX {
exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
local.server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
local.server.txt_add(
"_mta-sts.foobar.org",
MtaSts::parse(b"v=STSv1; id=policy_in_testing;").unwrap(),
Instant::now() + Duration::from_secs(10),
);
STS_TEST_POLICY.lock().clear();
STS_TEST_POLICY.lock().extend_from_slice(
concat!(
"version: STSv1\n",
"mode: testing\n",
"mx: *.foobar.org\n",
"max_age: 604800\n"
)
.as_bytes(),
);
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_then_deliver()
.await
.try_deliver(local.server.clone());
local.read_event().await.assert_done();
remote
.expect_message()
.await
.read_lines(&remote)
.await
.assert_not_contains("using TLSv1.3 with cipher");
STS_TEST_POLICY.lock().clear();
}
+429
View File
@@ -0,0 +1,429 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
smtp::{
inbound::{TestMessage, TestQueueEvent},
session::{TestSession, VerifyResponse},
},
utils::{dns::DnsCache, server::TestServerBuilder},
};
use common::{config::smtp::queue::QueueName, ipc::QueueEvent};
use mail_auth::{DnssecStatus, MX};
use registry::{
schema::{
enums::NetworkListenerProtocol,
structs::{
Expression, ExpressionMatch, MtaDeliveryExpiration, MtaDeliveryExpirationTtl,
MtaDeliverySchedule, MtaDeliveryScheduleInterval, MtaDeliveryScheduleIntervals,
MtaDeliveryScheduleIntervalsOrDefault, MtaExtensions, MtaOutboundStrategy,
MtaStageRcpt, MtaVirtualQueue,
},
},
types::list::List,
};
use smtp::queue::spool::{QUEUE_REFRESH, SmtpSpool};
use std::time::{Duration, Instant};
use store::write::now;
const SMUGGLER: &str = r#"From: Joe SixPack <[email protected]>
To: Suzie Q <[email protected]>
Subject: Is dinner ready?
Hi.
We lost the game. Are you hungry yet?
.hey
Joe.
<SEP>.
MAIL FROM:<[email protected]>
RCPT TO:<[email protected]>
DATA
From: Joe SixPack <[email protected]>
To: Suzie Q <[email protected]>
Subject: smuggled message
This is a smuggled message
"#;
#[tokio::test]
#[serial_test::serial]
async fn smtp_delivery() {
let mut local = TestServerBuilder::new("smtp_delivery_local")
.await
.with_http_listener(19030)
.await
.disable_services()
.capture_queue()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_delivery_remote")
.await
.with_http_listener(19031)
.await
.with_listener(NetworkListenerProtocol::Smtp, "smtp-debug", 9925, false)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin
.registry_create_object(MtaStageRcpt {
max_recipients: Expression {
else_: "100".into(),
..Default::default()
},
allow_relaying: Expression {
else_: "true".into(),
..Default::default()
},
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaOutboundStrategy {
schedule: Expression {
match_: List::from_iter([
ExpressionMatch {
if_: "rcpt_domain == 'foobar.org'".into(),
then: "'foobar-org'".into(),
},
ExpressionMatch {
if_: "rcpt_domain == 'foobar.com'".into(),
then: "'foobar-com'".into(),
},
]),
else_: "'default'".into(),
},
..Default::default()
})
.await;
let queue_id = local_admin
.registry_create_object(MtaVirtualQueue {
name: "default".into(),
threads_per_node: 25,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "default".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 7_000u64.into(),
}),
queue_id,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "foobar-org".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([
MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
},
MtaDeliveryScheduleInterval {
duration: 2_000u64.into(),
},
]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 6_000u64.into(),
}),
queue_id,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "foobar-com".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([
MtaDeliveryScheduleInterval {
duration: 5_000u64.into(),
},
MtaDeliveryScheduleInterval {
duration: 6_000u64.into(),
},
]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 7_000u64.into(),
}),
queue_id,
description: None,
})
.await;
local_admin.mta_no_auth().await;
local_admin.mta_all_extensions().await;
local_admin.mta_disable_spam_filter().await;
local_admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let remote_admin = remote.account("admin");
remote_admin.mta_allow_relaying().await;
remote_admin.mta_no_auth().await;
remote_admin.mta_disable_spam_filter().await;
remote_admin.mta_allow_non_fqdn().await;
remote_admin
.registry_create_object(MtaExtensions {
chunking: Expression {
else_: "false".into(),
..Default::default()
},
dsn: Expression {
else_: "true".into(),
..Default::default()
},
..Default::default()
})
.await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
// Add mock DNS entries
for domain in ["foobar.org", "foobar.net", "foobar.com"] {
local.server.mx_add(
domain,
vec![MX {
exchanges: vec![
format!("mx1.{domain}").into(),
format!("mx2.{domain}").into(),
]
.into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
local.server.ipv4_add(
format!("mx1.{domain}"),
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(30),
);
local.server.ipv4_add(
format!("mx2.{domain}"),
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(30),
);
}
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message(
"[email protected]",
&[
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
"<[email protected]> NOTIFY=SUCCESS,DELAY,FAILURE",
],
"test:no_dkim",
"250",
)
.await;
let message = local.expect_message().await;
let num_recipients = message.message.recipients.len();
assert_eq!(num_recipients, 7);
local
.delivery_attempt_for_queue(message.queue_id, "default")
.await
.try_deliver(local.server.clone());
let mut dsn = Vec::new();
let mut rcpt_retries = vec![0; num_recipients];
loop {
match local.try_read_event().await {
Some(QueueEvent::Refresh | QueueEvent::WorkerDone { .. }) => {}
Some(QueueEvent::Paused(_)) | Some(QueueEvent::ReloadSettings) => unreachable!(),
None | Some(QueueEvent::Stop) => {
break;
}
}
let mut events = local.all_queued_messages().await;
if events.messages.is_empty() {
let now = now();
if events.next_refresh < now + QUEUE_REFRESH {
tokio::time::sleep(Duration::from_secs(events.next_refresh - now)).await;
events = local.all_queued_messages().await;
} else {
break;
}
}
for event in events.messages {
let message = local
.server
.read_message(event.queue_id, QueueName::default())
.await
.unwrap();
if message.message.return_path.is_empty() {
message
.clone()
.remove(&local.server, event.due.into())
.await;
dsn.push(message);
} else {
for (idx, rcpt) in message.message.recipients.iter().enumerate() {
rcpt_retries[idx] = rcpt.retry.inner;
}
event.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
assert_eq!(rcpt_retries[0], 0, "retries {rcpt_retries:?}");
assert!(rcpt_retries[1] >= 5, "retries {rcpt_retries:?}");
assert_eq!(rcpt_retries[2], 0, "retries {rcpt_retries:?}");
assert_eq!(rcpt_retries[3], 0, "retries {rcpt_retries:?}");
assert!(rcpt_retries[4] >= 5, "retries {rcpt_retries:?}");
assert_eq!(rcpt_retries[5], 0, "retries {rcpt_retries:?}");
assert_eq!(rcpt_retries[6], 0, "retries {rcpt_retries:?}");
assert!(
rcpt_retries[1] >= rcpt_retries[4],
"retries {rcpt_retries:?}"
);
local.assert_queue_is_empty().await;
assert_eq!(dsn.len(), 5);
let mut dsn = dsn.into_iter();
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (delivered to")
.assert_contains("<[email protected]> (delivered to")
.assert_contains("<[email protected]> (failed to lookup")
.assert_contains("<[email protected]> (host ")
.assert_contains("<[email protected]> (host ");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (host ")
.assert_contains("<[email protected]> (host ")
.assert_contains("Action: delayed");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (host ")
.assert_contains("Action: delayed");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (host ");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (host ")
.assert_contains("Action: failed");
let mut recipients = remote
.consume_message()
.await
.message
.recipients
.into_iter()
.map(|r| r.address().to_string())
.collect::<Vec<_>>();
recipients.extend(
remote
.consume_message()
.await
.message
.recipients
.into_iter()
.map(|r| r.address().to_string()),
);
recipients.sort();
assert_eq!(
recipients,
vec!["[email protected]".to_string(), "[email protected]".to_string()]
);
remote.assert_no_events();
// SMTP smuggling
for separator in ["\n", "\r"].iter() {
session.data.remote_ip_str = "10.0.0.2".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
let out_message = SMUGGLER
.replace('\r', "")
.replace('\n', "\r\n")
.replace("<SEP>", separator);
session
.send_message("[email protected]", &["[email protected]"], &out_message, "250")
.await;
local
.expect_message_for_queue_then_deliver("default")
.await
.try_deliver(local.server.clone());
local.read_event().await.assert_refresh_or_done();
let message = remote.consume_message().await.read_message(&remote).await;
assert!(
message.contains("This is a smuggled message"),
"message: {:?}",
message
);
assert!(
message.contains("We lost the game."),
"message: {:?}",
message
);
assert!(
message.contains(&format!("{separator}..\r\nMAIL FROM:<",)),
"Message {message:?} does not contain separator {:?}",
format!("{separator}..\r\nMAIL FROM:<",)
);
}
}
+412
View File
@@ -0,0 +1,412 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::{
smtp::{
queue::{build_rcpt, new_message},
session::TestSession,
},
utils::{dns::DnsCache, server::TestServerBuilder},
};
use common::config::smtp::queue::QueueName;
use mail_auth::{DnssecStatus, MX};
use registry::{
schema::{
enums::MtaOutboundThrottleKey,
structs::{
Expression, MtaDeliveryExpiration, MtaDeliveryExpirationTtl, MtaDeliverySchedule,
MtaDeliveryScheduleInterval, MtaDeliveryScheduleIntervals,
MtaDeliveryScheduleIntervalsOrDefault, MtaOutboundStrategy, MtaOutboundThrottle,
MtaVirtualQueue, Rate,
},
},
types::{list::List, map::Map},
};
use smtp::queue::{Message, QueueEnvelope, Recipient, throttle::IsAllowed};
use std::{
net::{IpAddr, Ipv4Addr},
time::{Duration, Instant},
};
use store::write::now;
#[tokio::test]
async fn throttle_outbound() {
let mut local = TestServerBuilder::new("smtp_throttle_outbound")
.await
.with_http_listener(19032)
.await
.disable_services()
.capture_queue()
.build()
.await;
let admin = local.account("admin");
let queue_id = admin
.registry_create_object(MtaVirtualQueue {
name: "default".into(),
threads_per_node: 25,
description: None,
})
.await;
admin
.registry_create_object(MtaDeliverySchedule {
name: "default".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 3_600_000u64.into(),
}]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 3_600_000u64.into(),
}]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 3_600_000u64.into(),
}),
queue_id,
description: None,
})
.await;
admin
.registry_create_object(MtaOutboundStrategy {
schedule: Expression {
else_: "'default'".into(),
..Default::default()
},
..Default::default()
})
.await;
for (expr, key, rate_count, rate_duration) in [
(
"sender_domain = 'foobar.net'",
MtaOutboundThrottleKey::SenderDomain,
1,
30 * 60 * 1000,
),
(
"rcpt_domain = 'example.net'",
MtaOutboundThrottleKey::RcptDomain,
1,
40 * 60 * 1000,
),
("mx = 'mx.test.org'", MtaOutboundThrottleKey::Mx, 1, 99999),
(
"mx = 'mx.test.net'",
MtaOutboundThrottleKey::Mx,
1,
50 * 60 * 1000,
),
] {
admin
.registry_create_object(MtaOutboundThrottle {
enable: true,
key: Map::new(vec![key]),
match_: Expression {
else_: expr.into(),
..Default::default()
},
rate: Rate {
count: rate_count,
period: rate_duration.into(),
},
description: "Test throttle".into(),
})
.await;
}
admin.mta_no_auth().await;
admin.mta_allow_relaying().await;
admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
// Build test message
let mut test_message = new_message(0).message;
test_message.return_path = "[email protected]".into();
test_message
.recipients
.push(build_rcpt("[email protected]", 0, 0, 0));
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
assert_eq!(local.last_queued_due().await as i64 - now() as i64, 0);
// Throttle sender
let core = local.server.core.clone();
let throttle = &core.smtp.queue.outbound_limiters;
for t in &throttle.sender {
local
.server
.is_allowed(
t,
&QueueEnvelope::test(&test_message, &test_message.recipients[0], ""),
0,
)
.await
.unwrap();
}
// Expect rate limit throttle for sender domain 'foobar.net'
test_message.return_path = "[email protected]".into();
for t in &throttle.sender {
local
.server
.is_allowed(
t,
&QueueEnvelope::test(&test_message, &test_message.recipients[0], ""),
0,
)
.await
.unwrap();
}
test_message.recipients.clear();
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_for_queue_then_deliver("default")
.await
.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
local.expect_refresh().await;
let due = local.last_queued_due().await - now();
assert!(due > 0, "Due: {}", due);
// Expect concurrency throttle for recipient domain 'example.org'
test_message.return_path = "[email protected]".into();
test_message
.recipients
.push(build_rcpt("[email protected]", 0, 0, 0));
for t in &throttle.rcpt {
local
.server
.is_allowed(
t,
&QueueEnvelope::test(&test_message, &test_message.recipients[0], ""),
0,
)
.await
.unwrap();
}
// Expect rate limit throttle for recipient domain 'example.net'
test_message
.recipients
.push(build_rcpt("[email protected]", 0, 0, 0));
for t in &throttle.rcpt {
local
.server
.is_allowed(
t,
&QueueEnvelope::test(&test_message, &test_message.recipients[1], ""),
0,
)
.await
.unwrap();
}
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250",
)
.await;
local
.expect_message_for_queue_then_deliver("default")
.await
.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
local.expect_refresh().await;
let due = local.last_queued_due().await - now();
assert!(due > 0, "Due: {}", due);
// Expect concurrency throttle for mx 'mx.test.org'
local.server.mx_add(
"test.org",
vec![MX {
exchanges: vec!["mx.test.org".into()].into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
local.server.ipv4_add(
"mx.test.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
test_message
.recipients
.push(build_rcpt("[email protected]", 0, 0, 0));
for t in &throttle.remote {
local
.server
.is_allowed(
t,
&QueueEnvelope::test(&test_message, &test_message.recipients[2], "mx.test.org"),
0,
)
.await
.unwrap();
}
// Expect rate limit throttle for mx 'mx.test.net'
local.server.mx_add(
"test.net",
vec![MX {
exchanges: vec!["mx.test.net".into()].into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
local.server.ipv4_add(
"mx.test.net",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
for t in &throttle.remote {
local
.server
.is_allowed(
t,
&QueueEnvelope::test(&test_message, &test_message.recipients[1], "mx.test.net"),
0,
)
.await
.unwrap();
}
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_for_queue_then_deliver("default")
.await
.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
local.expect_refresh().await;
let due = local.last_queued_due().await - now();
assert!(due > 0, "Due: {}", due);
}
#[tokio::test]
async fn throttle_outbound_queue_name() {
let mut local = TestServerBuilder::new("smtp_throttle_outbound_queue_name")
.await
.with_http_listener(19033)
.await
.disable_services()
.capture_queue()
.build()
.await;
let admin = local.account("admin");
let queue_id = admin
.registry_create_object(MtaVirtualQueue {
name: "default".into(),
threads_per_node: 25,
description: None,
})
.await;
admin
.registry_create_object(MtaDeliverySchedule {
name: "default".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Default,
notify: MtaDeliveryScheduleIntervalsOrDefault::Default,
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 3_600_000u64.into(),
}),
queue_id,
description: None,
})
.await;
admin
.registry_create_object(MtaOutboundStrategy {
schedule: Expression {
else_: "'default'".into(),
..Default::default()
},
..Default::default()
})
.await;
admin
.registry_create_object(MtaOutboundThrottle {
enable: true,
key: Map::new(vec![MtaOutboundThrottleKey::SenderDomain]),
match_: Expression {
else_: "queue_name == 'default'".into(),
..Default::default()
},
rate: Rate {
count: 1,
period: (30u64 * 60 * 1000).into(),
},
description: "queue_name throttle".into(),
})
.await;
admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let core = local.server.core.clone();
let throttle = &core.smtp.queue.outbound_limiters;
assert_eq!(throttle.sender.len(), 1);
assert!(throttle.rcpt.is_empty());
assert!(throttle.remote.is_empty());
let matching = new_message(0);
assert_eq!(matching.queue_name, QueueName::default());
local
.server
.is_allowed(&throttle.sender[0], &matching, 0)
.await
.unwrap();
assert!(
local
.server
.is_allowed(&throttle.sender[0], &matching, 0)
.await
.is_err(),
"sender-bucket throttle failed to resolve queue_name and never engaged"
);
let mut other_queue = new_message(0);
other_queue.queue_name = QueueName::new("remote").unwrap();
local
.server
.is_allowed(&throttle.sender[0], &other_queue, 0)
.await
.unwrap();
}
pub trait TestQueueEnvelope<'x> {
fn test(message: &'x Message, rcpt: &'x Recipient, mx: &'x str) -> Self;
}
impl<'x> TestQueueEnvelope<'x> for QueueEnvelope<'x> {
fn test(message: &'x Message, rcpt: &'x Recipient, mx: &'x str) -> Self {
QueueEnvelope {
message,
mx,
remote_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
local_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
domain: rcpt.domain_part(),
rcpt,
}
}
}
+159
View File
@@ -0,0 +1,159 @@
/*
* 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},
},
utils::{dns::DnsCache, server::TestServerBuilder},
};
use mail_auth::{DnssecStatus, MX};
use registry::{
schema::{
enums::MtaRequiredOrOptional,
structs::{
Expression, ExpressionMatch, MtaConnectionStrategy, MtaDeliverySchedule,
MtaOutboundStrategy, MtaTlsStrategy, MtaVirtualQueue,
},
},
types::list::List,
};
use std::time::{Duration, Instant};
use store::write::now;
#[tokio::test]
#[serial_test::serial]
async fn starttls_optional() {
let mut local = TestServerBuilder::new("smtp_starttls_local")
.await
.with_http_listener(19034)
.await
.disable_services()
.capture_queue()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_starttls_remote")
.await
.with_http_listener(19035)
.await
.with_smtp_listener(9925)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin.mta_no_auth().await;
local_admin.mta_allow_relaying().await;
local_admin
.registry_create_object(MtaOutboundStrategy {
tls: Expression {
match_: List::from_iter([ExpressionMatch {
if_: "retry_num > 0 && last_error == 'tls'".into(),
then: "'no-tls'".into(),
}]),
else_: "'default'".into(),
},
connection: Expression {
else_: "'badtls'".into(),
..Default::default()
},
schedule: Expression {
else_: "'default'".into(),
..Default::default()
},
..Default::default()
})
.await;
let queue_id = local_admin
.registry_create_object(MtaVirtualQueue {
name: "default".into(),
threads_per_node: 25,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "default".into(),
queue_id,
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaConnectionStrategy {
name: "badtls".into(),
ehlo_hostname: "badtls.foobar.org".to_string().into(),
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaTlsStrategy {
name: "no-tls".into(),
allow_invalid_certs: true,
start_tls: MtaRequiredOrOptional::Disable,
..Default::default()
})
.await;
local_admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let remote_admin = remote.account("admin");
remote_admin.mta_no_auth().await;
remote_admin.mta_allow_relaying().await;
remote_admin.mta_allow_non_fqdn().await;
remote_admin.mta_all_extensions().await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
// Add mock DNS entries
local.server.mx_add(
"foobar.org",
vec![MX {
exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(),
preference: 10,
}],
DnssecStatus::Secure,
Instant::now() + Duration::from_secs(10),
);
local.server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(10),
);
let mut session = local.new_mta_session();
session.data.remote_ip_str = "10.0.0.1".into();
session.eval_session_params().await;
session.ehlo("mx.test.org").await;
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
local
.expect_message_for_queue_then_deliver("default")
.await
.try_deliver(local.server.clone());
let mut retry = local.expect_message().await;
let prev_due = retry.message.recipients[0].retry.due;
let next_due = now();
let queue_id = retry.queue_id;
retry.message.recipients[0].retry.due = next_due;
retry.save_changes(&local.server, prev_due.into()).await;
local
.delivery_attempt_for_queue(queue_id, "default")
.await
.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
remote
.expect_message()
.await
.read_lines(&remote)
.await
.assert_not_contains("using TLSv1.3 with cipher");
}