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