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
+217
View File
@@ -0,0 +1,217 @@
/*
* 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 common::{BuildServer, ipc::QueueEvent};
use mail_auth::{DnssecStatus, MX};
use registry::{
schema::{
enums::NetworkListenerProtocol,
prelude::ObjectType,
structs::{
Expression, MtaDeliveryExpiration, MtaDeliveryExpirationTtl, MtaDeliverySchedule,
MtaDeliveryScheduleInterval, MtaDeliveryScheduleIntervals,
MtaDeliveryScheduleIntervalsOrDefault, MtaOutboundStrategy, MtaStageData,
MtaVirtualQueue,
},
},
types::list::List,
};
use smtp::queue::manager::Queue;
use std::time::{Duration, Instant};
const NUM_MESSAGES: usize = 100;
const NUM_QUEUES: usize = 10;
#[tokio::test(flavor = "multi_thread", worker_threads = 18)]
#[serial_test::serial]
async fn concurrent_queue() {
let mut local = TestServerBuilder::new("smtp_concurrent_queue_local")
.await
.with_http_listener(19037)
.await
.disable_services()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_concurrent_queue_remote")
.await
.with_http_listener(19038)
.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(MtaStageData {
max_messages: Expression {
else_: "2000".into(),
..Default::default()
},
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaOutboundStrategy {
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: 4,
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: 86_400_000u64.into(),
}]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 86_400_000u64.into(),
}),
queue_id,
description: None,
})
.await;
local_admin.mta_allow_relaying().await;
local_admin.mta_disable_spam_filter().await;
local_admin.mta_allow_non_fqdn().await;
local_admin.mta_no_auth().await;
local_admin
.registry_destroy_all(ObjectType::MtaInboundThrottle)
.await;
local_admin.reload_settings().await;
local.reload_core();
let remote_admin = remote.account("admin");
remote_admin.mta_allow_relaying().await;
remote_admin.mta_disable_spam_filter().await;
remote_admin.mta_allow_non_fqdn().await;
remote_admin.mta_no_auth().await;
remote_admin
.registry_destroy_all(ObjectType::MtaInboundThrottle)
.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(100),
);
local.server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(100),
);
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;
// Spawn concurrent queues
let mut inners = vec![];
for _ in 0..NUM_QUEUES {
let (inner, rxs) = local.inner_with_rxs().await;
let server = inner.build_server();
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(100),
);
server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(100),
);
inners.push(inner.clone());
tokio::spawn(async move {
Queue::new(inner, rxs.queue_rx.unwrap()).start().await;
});
}
tokio::time::sleep(Duration::from_millis(200)).await;
// Send 1000 test messages
for _ in 0..(NUM_MESSAGES / 2) {
session
.send_message("[email protected]", &["[email protected]"], "test:no_dkim", "250")
.await;
}
// Wake up all queues
for inner in &inners {
inner.ipc.queue_tx.send(QueueEvent::Refresh).await.unwrap();
}
for _ in 0..(NUM_MESSAGES / 2) {
session
.send_message(
"[email protected]",
&["[email protected]"],
"test:no_dkim",
"250",
)
.await;
}
loop {
tokio::time::sleep(Duration::from_millis(1500)).await;
let m = local.read_queued_messages().await.len();
let e = local.read_queued_events().await.len();
if m + e != 0 {
println!("Queue still has {} messages and {} events", m, e);
/*for inner in &inners {
inner.ipc.queue_tx.send(QueueEvent::Refresh).await.unwrap();
}*/
} else {
break;
}
}
local.assert_queue_is_empty().await;
let remote_messages = remote.read_queued_messages().await;
assert_eq!(remote_messages.len(), NUM_MESSAGES);
// Make sure local store is queue
local
.account("admin")
.registry_destroy_all(ObjectType::MtaConnectionStrategy)
.await;
local.assert_is_empty().await;
}
+272
View File
@@ -0,0 +1,272 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use crate::utils::server::{TestServer, TestServerBuilder};
use common::config::smtp::queue::{QueueExpiry, QueueName};
use registry::schema::{
enums::CompressionAlgo,
structs::{DsnReportSettings, Expression, ReportSettings},
};
use smtp::queue::{
Error, ErrorDetails, HostResponse, Message, MessageWrapper, Recipient, Schedule, Status,
UnexpectedResponse, dsn::SendDsn,
};
use smtp_proto::{RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_SUCCESS, Response};
use std::{
fs,
net::{IpAddr, Ipv4Addr},
path::PathBuf,
time::SystemTime,
};
use store::write::now;
use types::blob_hash::BlobHash;
#[tokio::test]
async fn generate_dsn() {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("resources");
path.push("smtp");
path.push("dsn");
path.push("original.txt");
let size = fs::metadata(&path).unwrap().len() as u64;
let dsn_original = fs::read_to_string(&path).unwrap();
let flags = RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_DELAY | RCPT_NOTIFY_SUCCESS;
let mut message = MessageWrapper {
queue_id: 0,
span_id: 0,
is_multi_queue: false,
queue_name: QueueName::default(),
message: Message {
size,
created: SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs()),
return_path: "[email protected]".into(),
recipients: vec![Recipient {
address: "[email protected]".into(),
status: Status::PermanentFailure(ErrorDetails {
entity: "mx.example.org".into(),
details: Error::UnexpectedResponse(UnexpectedResponse {
command: "RCPT TO:<[email protected]>".into(),
response: Response {
code: 550,
esc: [5, 1, 2],
message: "User does not exist".into(),
},
}),
}),
flags: 0,
orcpt: None,
retry: Schedule::now(),
notify: Schedule::now(),
expires: QueueExpiry::Ttl(10),
queue: QueueName::default(),
}],
flags: 0,
env_id: None,
priority: 0,
blob_hash: BlobHash::generate(dsn_original.as_bytes()),
metadata: Default::default(),
received_from_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
received_via_port: 0,
},
};
let mut local = TestServerBuilder::new("smtp_queue_dsn")
.await
.with_http_listener(19039)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin
.registry_create_object(ReportSettings {
outbound_report_submitter: Expression {
else_: "'mx.example.org'".into(),
..Default::default()
},
..Default::default()
})
.await;
local_admin
.registry_create_object(DsnReportSettings {
dkim_sign_domain: Expression {
else_: "'example.org'".into(),
..Default::default()
},
from_address: Expression {
else_: "'[email protected]'".into(),
..Default::default()
},
from_name: Expression {
else_: "'Mail Delivery Subsystem'".into(),
..Default::default()
},
})
.await;
let domain_id = local_admin.find_or_create_domain("example.org").await;
local_admin.create_dkim_signatures(domain_id).await;
local_admin.mta_allow_non_fqdn().await;
local_admin.mta_allow_relaying().await;
local_admin.reload_settings().await;
local_admin.mta_allow_relaying().await;
local.reload_core();
local.expect_reload_settings().await;
// Create temp dir for queue
local
.server
.blob_store()
.put_blob(
message.message.blob_hash.as_slice(),
dsn_original.as_bytes(),
CompressionAlgo::Lz4,
)
.await
.unwrap();
// Disabled DSN
local.server.send_dsn(&mut message).await;
local.assert_no_events();
local.assert_queue_is_empty().await;
// Failure DSN
message.message.recipients[0].flags = flags;
local.server.send_dsn(&mut message).await;
let dsn_message = local.expect_message().await;
local.compare_dsn(dsn_message.message, "failure.eml").await;
// Success DSN
message.message.recipients.push(Recipient {
address: "[email protected]".into(),
status: Status::Completed(HostResponse {
hostname: "mx2.example.org".into(),
response: Response {
code: 250,
esc: [2, 1, 5],
message: "Message accepted for delivery".into(),
},
}),
flags,
orcpt: None,
retry: Schedule::now(),
notify: Schedule::now(),
expires: QueueExpiry::Ttl(10),
queue: QueueName::default(),
});
local.server.send_dsn(&mut message).await;
let dsn_message = local.expect_message().await;
local.compare_dsn(dsn_message.message, "success.eml").await;
// Delay DSN
message.message.recipients.push(Recipient {
address: "[email protected]".into(),
status: Status::TemporaryFailure(ErrorDetails {
entity: "mx.domain.org".into(),
details: Error::ConnectionError("Connection timeout".into()),
}),
flags,
orcpt: Some("[email protected]".into()),
retry: Schedule::now(),
notify: Schedule::now(),
expires: QueueExpiry::Ttl(10),
queue: QueueName::default(),
});
local.server.send_dsn(&mut message).await;
let dsn_message = local.expect_message().await;
local.compare_dsn(dsn_message.message, "delay.eml").await;
// Mixed DSN
for rcpt in &mut message.message.recipients {
rcpt.flags = flags;
}
message.message.recipients.last_mut().unwrap().notify.due = now();
local.server.send_dsn(&mut message).await;
let dsn_message = local.expect_message().await;
local.compare_dsn(dsn_message.message, "mixed.eml").await;
// Load queue
let queue = local.read_queued_messages().await;
assert_eq!(queue.len(), 4);
}
impl TestServer {
async fn compare_dsn(&self, message: Message, test: &str) {
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("resources");
path.push("smtp");
path.push("dsn");
path.push(test);
let bytes = self
.server
.blob_store()
.get_blob(message.blob_hash.as_slice(), 0..usize::MAX)
.await
.unwrap()
.unwrap();
let dsn = remove_ids(bytes);
let dsn_expected = fs::read_to_string(&path).unwrap();
if dsn != dsn_expected {
let mut failed = PathBuf::from(&path);
failed.set_extension("failed");
fs::write(&failed, dsn.as_bytes()).unwrap();
panic!(
"Failed for {}, output saved to {}",
path.display(),
failed.display()
);
}
}
}
fn remove_ids(message: Vec<u8>) -> String {
let old_message = String::from_utf8(message).unwrap();
let mut message = String::with_capacity(old_message.len());
let mut found_dkim = 0;
let mut skip = false;
let mut boundary = "";
for line in old_message.split("\r\n") {
if skip {
if line.chars().next().unwrap().is_ascii_whitespace() {
continue;
} else {
skip = false;
}
}
if line.starts_with("Date:") || line.starts_with("Message-ID:") {
continue;
} else if found_dkim < 2 && line.starts_with("DKIM-Signature:") {
found_dkim += 1;
skip = true;
continue;
} else if line.starts_with("--") {
message.push_str(&line.replace(boundary, "mime_boundary"));
} else if let Some((_, boundary_)) = line.split_once("boundary=\"") {
boundary = boundary_.split_once('"').unwrap().0;
message.push_str(&line.replace(boundary, "mime_boundary"));
} else if line.starts_with("Arrival-Date:") {
message.push_str("Arrival-Date: <date goes here>");
} else if line.starts_with("Will-Retry-Until:") {
message.push_str("Will-Retry-Until: <date goes here>");
} else {
message.push_str(line);
}
message.push_str("\r\n");
}
if found_dkim == 0 {
panic!("No DKIM signature found in: {old_message}");
}
message
}
+209
View File
@@ -0,0 +1,209 @@
/*
* 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},
utils::server::TestServerBuilder,
};
use common::config::smtp::queue::QueueName;
use smtp::queue::{Error, ErrorDetails, Message, Recipient, Status, spool::SmtpSpool};
use std::time::Duration;
use store::write::now;
#[tokio::test]
async fn queue_due() {
let mut local = TestServerBuilder::new("smtp_queue_manager")
.await
.with_http_listener(19040)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin.mta_allow_relaying().await;
local_admin.mta_allow_non_fqdn().await;
local_admin.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
let mut message = new_message(0);
message.message.recipients.push(build_rcpt("c", 3, 8, 9));
message.save_changes(&local.server, 0.into()).await;
let mut message = new_message(1);
message.message.recipients.push(build_rcpt("b", 2, 6, 7));
message.save_changes(&local.server, 0.into()).await;
let mut message = new_message(2);
message.message.recipients.push(build_rcpt("a", 1, 4, 5));
message.save_changes(&local.server, 0.into()).await;
for domain in vec!["a", "b", "c"].into_iter() {
let now = now();
let queued = local.all_queued_messages().await;
if queued.messages.is_empty() {
let wake_up = queued.next_refresh - now;
assert_eq!(wake_up, 1);
std::thread::sleep(Duration::from_secs(wake_up));
}
for queue_event in local.all_queued_messages().await.messages {
if let Some(message) = local
.server
.read_message(queue_event.queue_id, QueueName::default())
.await
{
message.message.rcpt(domain);
message.remove(&local.server, queue_event.due.into()).await;
} else {
panic!("Message not found");
}
}
}
local.assert_queue_is_empty().await;
}
#[test]
fn delivery_events() {
let mut message = new_message(0).message;
message.created = now();
message.recipients.push(build_rcpt("a", 1, 2, 3));
message.recipients.push(build_rcpt("b", 4, 5, 6));
message.recipients.push(build_rcpt("c", 7, 8, 9));
for t in 0..2 {
assert_eq!(
message.next_event(None).unwrap(),
message.rcpt("a").retry.due
);
assert_eq!(
message.next_delivery_event(None).unwrap(),
message.rcpt("a").retry.due
);
assert_eq!(
next_event_after(
&message,
None,
message.rcpt("a").expiration_time(message.created).unwrap()
)
.unwrap(),
message.rcpt("b").retry.due
);
assert_eq!(
next_event_after(
&message,
None,
message.rcpt("b").expiration_time(message.created).unwrap()
)
.unwrap(),
message.rcpt("c").retry.due
);
assert_eq!(
next_event_after(&message, None, message.rcpt("c").notify.due).unwrap(),
message.rcpt("c").expiration_time(message.created).unwrap()
);
assert!(
next_event_after(
&message,
None,
message.rcpt("c").expiration_time(message.created).unwrap()
)
.is_none()
);
if t == 0 {
message.recipients.reverse();
} else {
message.recipients.swap(0, 1);
}
}
message.rcpt_mut("a").status = Status::PermanentFailure(ErrorDetails {
entity: "localhost".into(),
details: Error::ConcurrencyLimited,
});
assert_eq!(
message.next_event(None).unwrap(),
message.rcpt("b").retry.due
);
assert_eq!(
message.next_delivery_event(None).unwrap(),
message.rcpt("b").retry.due
);
message.rcpt_mut("b").status = Status::PermanentFailure(ErrorDetails {
entity: "localhost".into(),
details: Error::ConcurrencyLimited,
});
assert_eq!(
message.next_event(None).unwrap(),
message.rcpt("c").retry.due
);
assert_eq!(
message.next_delivery_event(None).unwrap(),
message.rcpt("c").retry.due
);
message.rcpt_mut("c").status = Status::PermanentFailure(ErrorDetails {
entity: "localhost".into(),
details: Error::ConcurrencyLimited,
});
assert!(message.next_event(None).is_none());
}
fn next_event_after(message: &Message, queue: Option<QueueName>, instant: u64) -> Option<u64> {
let mut next_event = None;
for rcpt in &message.recipients {
if matches!(rcpt.status, Status::Scheduled | Status::TemporaryFailure(_))
&& queue.is_none_or(|q| rcpt.queue == q)
{
if rcpt.retry.due > instant
&& next_event.as_ref().is_none_or(|ne| rcpt.retry.due.lt(ne))
{
next_event = rcpt.retry.due.into();
}
if rcpt.notify.due > instant
&& next_event.as_ref().is_none_or(|ne| rcpt.notify.due.lt(ne))
{
next_event = rcpt.notify.due.into();
}
if let Some(expires) = rcpt.expiration_time(message.created)
&& expires > instant
&& next_event.as_ref().is_none_or(|ne| expires.lt(ne))
{
next_event = expires.into();
}
}
}
next_event
}
pub trait TestMessage {
fn rcpt(&self, name: &str) -> &Recipient;
fn rcpt_mut(&mut self, name: &str) -> &mut Recipient;
}
impl TestMessage for Message {
fn rcpt(&self, name: &str) -> &Recipient {
self.recipients
.iter()
.find(|d| d.address() == name)
.unwrap_or_else(|| panic!("Expected rcpt {name} not found in {:?}", self.recipients))
}
fn rcpt_mut(&mut self, name: &str) -> &mut Recipient {
self.recipients
.iter_mut()
.find(|d| d.address() == name)
.unwrap()
}
}
+51
View File
@@ -0,0 +1,51 @@
/*
* SPDX-FileCopyrightText: 2020 Stalwart Labs LLC <[email protected]>
*
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/
use common::config::smtp::queue::{QueueExpiry, QueueName};
use smtp::queue::{Message, MessageWrapper, Recipient, Schedule, Status};
use std::net::{IpAddr, Ipv4Addr};
use store::write::now;
pub mod concurrent;
pub mod dsn;
pub mod manager;
pub mod retry;
pub mod virtualq;
pub fn build_rcpt(address: &str, retry: u64, notify: u64, expires: u64) -> Recipient {
Recipient {
address: address.into(),
retry: Schedule::later(retry),
notify: Schedule::later(notify),
expires: QueueExpiry::Ttl(expires),
status: Status::Scheduled,
flags: 0,
orcpt: None,
queue: QueueName::default(),
}
}
pub fn new_message(queue_id: u64) -> MessageWrapper {
MessageWrapper {
queue_id,
span_id: 0,
queue_name: QueueName::default(),
is_multi_queue: false,
message: Message {
size: 0,
created: now(),
return_path: "[email protected]".into(),
recipients: vec![],
flags: 0,
env_id: None,
priority: 0,
metadata: Default::default(),
blob_hash: Default::default(),
received_from_ip: IpAddr::V4(Ipv4Addr::LOCALHOST),
received_via_port: 0,
},
}
}
+321
View File
@@ -0,0 +1,321 @@
/*
* 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::server::TestServerBuilder,
};
use ahash::AHashSet;
use common::{
config::smtp::queue::QueueName,
ipc::{QueueEvent, QueueEventStatus},
};
use registry::{
schema::structs::{
Expression, ExpressionMatch, MtaDeliveryExpiration, MtaDeliveryExpirationTtl,
MtaDeliverySchedule, MtaDeliveryScheduleInterval, MtaDeliveryScheduleIntervals,
MtaDeliveryScheduleIntervalsOrDefault, MtaExtensions, MtaOutboundStrategy, MtaVirtualQueue,
},
types::list::List,
};
use smtp::queue::spool::{QUEUE_REFRESH, SmtpSpool};
use std::time::Duration;
use store::write::now;
#[tokio::test]
async fn queue_retry() {
let mut local = TestServerBuilder::new("smtp_queue_retry")
.await
.with_http_listener(19041)
.await
.disable_services()
.capture_queue()
.build()
.await;
let local_admin = local.account("admin");
local_admin.mta_allow_relaying().await;
local_admin.mta_allow_non_fqdn().await;
local_admin.mta_no_auth().await;
local_admin
.registry_create_object(MtaOutboundStrategy {
schedule: Expression {
match_: List::from_iter([ExpressionMatch {
if_: "sender_domain == 'test.org'".into(),
then: "'sender-test'".into(),
}]),
else_: "'sender-default'".into(),
},
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaExtensions {
deliver_by: Expression {
else_: "1h".into(),
..Default::default()
},
future_release: Expression {
else_: "1h".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: "sender-default".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([
MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
},
MtaDeliveryScheduleInterval {
duration: 2_000u64.into(),
},
MtaDeliveryScheduleInterval {
duration: 3_000u64.into(),
},
]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: (15 * 60 * 60 * 1000u64).into(),
}]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 86_400_000u64.into(),
}),
queue_id,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "sender-test".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([
MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
},
MtaDeliveryScheduleInterval {
duration: 2_000u64.into(),
},
MtaDeliveryScheduleInterval {
duration: 3_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.reload_settings().await;
local.reload_core();
local.expect_reload_settings().await;
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;
let attempt = local.expect_message_for_queue_then_deliver("default").await;
// Expect a failed DSN
attempt.try_deliver(local.server.clone());
let message = local.expect_message().await;
assert_eq!(message.message.return_path.as_ref(), "");
assert_eq!(
message.message.recipients.first().unwrap().address(),
"[email protected]"
);
message
.read_lines(&local)
.await
.assert_contains("Content-Type: multipart/report")
.assert_contains("Final-Recipient: rfc822;[email protected]")
.assert_contains("Action: failed");
local.read_event().await.assert_done();
local.clear_queue().await;
// Expect a failed DSN for foobar.org, followed by two delayed DSN and
// a final failed DSN for _dns_error.org.
session
.send_message(
"[email protected]",
&["[email protected]", "jane@_dns_error.org"],
"test:no_dkim",
"250",
)
.await;
let mut in_fight = AHashSet::new();
let attempt = local.expect_message_for_queue_then_deliver("default").await;
let mut dsn = Vec::new();
let mut retries = Vec::new();
in_fight.insert(attempt.queue_id);
attempt.try_deliver(local.server.clone());
loop {
match local.try_read_event().await {
Some(QueueEvent::WorkerDone {
queue_id, status, ..
}) => {
in_fight.remove(&queue_id);
match &status {
QueueEventStatus::Completed | QueueEventStatus::Deferred => (),
_ => panic!("unexpected status {queue_id}: {status:?}"),
}
}
Some(QueueEvent::Refresh) | Some(QueueEvent::ReloadSettings) => (),
None | Some(QueueEvent::Stop) | Some(QueueEvent::Paused(_)) => break,
}
let now = now();
let mut events = local.all_queued_messages().await;
if events.messages.is_empty() {
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 if in_fight.is_empty() {
break;
}
}
for event in events.messages {
if in_fight.contains(&event.queue_id) {
continue;
}
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 {
retries.push(event.due.saturating_sub(now));
in_fight.insert(event.queue_id);
event.try_deliver(local.server.clone());
tokio::time::sleep(Duration::from_millis(100)).await;
}
}
}
local.assert_queue_is_empty().await;
assert_eq!(retries, vec![1, 2, 3]);
assert_eq!(dsn.len(), 4);
let mut dsn = dsn.into_iter();
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<[email protected]> (failed to lookup 'foobar.org'")
.assert_contains("Final-Recipient: rfc822;[email protected]")
.assert_contains("Action: failed");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<jane@_dns_error.org> (failed to lookup '_dns_error.org'")
.assert_contains("Final-Recipient: rfc822;jane@_dns_error.org")
.assert_contains("Action: delayed");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<jane@_dns_error.org> (failed to lookup '_dns_error.org'")
.assert_contains("Final-Recipient: rfc822;jane@_dns_error.org")
.assert_contains("Action: delayed");
dsn.next()
.unwrap()
.read_lines(&local)
.await
.assert_contains("<jane@_dns_error.org> (failed to lookup '_dns_error.org'")
.assert_contains("Final-Recipient: rfc822;jane@_dns_error.org")
.assert_contains("Action: failed");
// Test FUTURERELEASE + DELIVERBY (RETURN)
session.data.remote_ip_str = "10.0.0.2".into();
session.eval_session_params().await;
session
.send_message(
"<[email protected]> HOLDFOR=60 BY=3600;R",
&["[email protected]"],
"test:no_dkim",
"250",
)
.await;
let now_ = now();
let message = local.expect_message().await;
assert!([59, 60].contains(&(local.message_due(message.queue_id).await - now_)));
assert!([59, 60].contains(&(message.message.next_delivery_event(None).unwrap() - now_)));
assert!(
[3599, 3600].contains(
&(message
.message
.recipients
.first()
.unwrap()
.expiration_time(message.message.created)
.unwrap()
- now_)
)
);
assert!(
[54059, 54060].contains(&(message.message.recipients.first().unwrap().notify.due - now_)),
"diff: {}",
message.message.recipients.first().unwrap().notify.due - now_
);
// Test DELIVERBY (NOTIFY)
session
.send_message(
"<[email protected]> BY=3600;N",
&["[email protected]"],
"test:no_dkim",
"250",
)
.await;
let schedule = local.expect_message().await;
assert!(
[3599, 3600].contains(&(schedule.message.recipients.first().unwrap().notify.due - now())),
"diff: {}",
schedule.message.recipients.first().unwrap().notify.due - now()
);
}
+286
View File
@@ -0,0 +1,286 @@
/*
* 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 common::{BuildServer, config::smtp::queue::QueueName, ipc::QueueEvent};
use mail_auth::{DnssecStatus, MX};
use registry::{
schema::{
enums::NetworkListenerProtocol,
prelude::ObjectType,
structs::{
Expression, ExpressionMatch, MtaDeliveryExpiration, MtaDeliveryExpirationTtl,
MtaDeliverySchedule, MtaDeliveryScheduleInterval, MtaDeliveryScheduleIntervals,
MtaDeliveryScheduleIntervalsOrDefault, MtaOutboundStrategy, MtaStageData,
MtaVirtualQueue,
},
},
types::list::List,
};
use smtp::queue::manager::Queue;
use std::time::{Duration, Instant};
const NUM_MESSAGES: usize = 100;
const NUM_QUEUES: usize = 10;
#[tokio::test(flavor = "multi_thread", worker_threads = 18)]
#[serial_test::serial]
async fn virtual_queue() {
let mut local = TestServerBuilder::new("smtp_virtual_queue_local")
.await
.with_http_listener(19042)
.await
.disable_services()
.build()
.await;
let mut remote = TestServerBuilder::new("smtp_virtual_queue_remote")
.await
.with_http_listener(19043)
.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(MtaOutboundStrategy {
schedule: Expression {
match_: List::from_iter([ExpressionMatch {
if_: "rcpt == '[email protected]'".into(),
then: "'q2'".into(),
}]),
else_: "'q1'".into(),
},
..Default::default()
})
.await;
local_admin
.registry_create_object(MtaStageData {
max_messages: Expression {
else_: "2000".into(),
..Default::default()
},
..Default::default()
})
.await;
let queue1_id = local_admin
.registry_create_object(MtaVirtualQueue {
name: "q1".into(),
threads_per_node: 5,
description: None,
})
.await;
let queue2_id = local_admin
.registry_create_object(MtaVirtualQueue {
name: "q2".into(),
threads_per_node: 4,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "q1".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 86_400_000u64.into(),
}]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 86_400_000u64.into(),
}),
queue_id: queue1_id,
description: None,
})
.await;
local_admin
.registry_create_object(MtaDeliverySchedule {
name: "q2".into(),
retry: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 1_000u64.into(),
}]),
}),
notify: MtaDeliveryScheduleIntervalsOrDefault::Custom(MtaDeliveryScheduleIntervals {
intervals: List::from_iter([MtaDeliveryScheduleInterval {
duration: 86_400_000u64.into(),
}]),
}),
expiry: MtaDeliveryExpiration::Ttl(MtaDeliveryExpirationTtl {
expire: 86_400_000u64.into(),
}),
queue_id: queue2_id,
description: None,
})
.await;
local_admin.mta_allow_relaying().await;
local_admin.mta_disable_spam_filter().await;
local_admin.mta_allow_non_fqdn().await;
local_admin.mta_no_auth().await;
local_admin
.registry_destroy_all(ObjectType::MtaInboundThrottle)
.await;
local_admin.reload_settings().await;
local.reload_core();
let remote_admin = remote.account("admin");
remote_admin.mta_allow_relaying().await;
remote_admin.mta_disable_spam_filter().await;
remote_admin.mta_allow_non_fqdn().await;
remote_admin.mta_no_auth().await;
remote_admin
.registry_destroy_all(ObjectType::MtaInboundThrottle)
.await;
remote_admin.reload_settings().await;
remote.reload_core();
remote.expect_reload_settings().await;
// Validate parsing
for value in ["a", "ab", "abcdefgh"] {
let queue_name = QueueName::new(value).unwrap();
assert_eq!(queue_name.to_string(), value);
}
assert_eq!(
local
.server
.core
.smtp
.queue
.virtual_queues
.get(&QueueName::new("q1").unwrap())
.unwrap()
.threads,
5
);
assert_eq!(
local
.server
.core
.smtp
.queue
.virtual_queues
.get(&QueueName::new("q2").unwrap())
.unwrap()
.threads,
4
);
// 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(100),
);
local.server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(100),
);
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;
// Spawn concurrent queues
let mut inners = vec![];
for _ in 0..NUM_QUEUES {
let (inner, rxs) = local.inner_with_rxs().await;
let server = inner.build_server();
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(100),
);
server.ipv4_add(
"mx.foobar.org",
vec!["127.0.0.1".parse().unwrap()],
Instant::now() + Duration::from_secs(100),
);
inners.push(inner.clone());
tokio::spawn(async move {
Queue::new(inner, rxs.queue_rx.unwrap()).start().await;
});
}
tokio::time::sleep(Duration::from_millis(200)).await;
// Send 1000 test messages
for _ in 0..(NUM_MESSAGES / 2) {
session
.send_message(
"[email protected]",
&["[email protected]", "[email protected]"],
"test:no_dkim",
"250",
)
.await;
}
// Wake up all queues
for inner in &inners {
inner.ipc.queue_tx.send(QueueEvent::Refresh).await.unwrap();
}
for _ in 0..(NUM_MESSAGES / 2) {
session
.send_message(
"[email protected]",
&["[email protected]", "[email protected]"],
"test:no_dkim",
"250",
)
.await;
}
loop {
tokio::time::sleep(Duration::from_millis(1500)).await;
let m = local.read_queued_messages().await;
let e = local.read_queued_events().await;
if m.len() + e.len() != 0 {
println!(
"Queue still has {} messages and {} events",
m.len(),
e.len()
);
/*for inner in &inners {
inner.ipc.queue_tx.send(QueueEvent::Refresh).await.unwrap();
}*/
} else {
break;
}
}
local.assert_queue_is_empty().await;
let remote_messages = remote.read_queued_messages().await;
assert_eq!(remote_messages.len(), NUM_MESSAGES * 2);
// Make sure local store is queue
local
.account("admin")
.registry_destroy_all(ObjectType::MtaConnectionStrategy)
.await;
local.assert_is_empty().await;
}