SMTP codebase import
This commit is contained in:
462
tests/src/smtp/management/queue.rs
Normal file
462
tests/src/smtp/management/queue.rs
Normal file
@@ -0,0 +1,462 @@
|
||||
/*
|
||||
* Copyright (c) 2023 Stalwart Labs Ltd.
|
||||
*
|
||||
* This file is part of the Stalwart SMTP Server.
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of
|
||||
* the License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
* in the LICENSE file at the top-level directory of this distribution.
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
* You can be released from the requirements of the AGPLv3 license by
|
||||
* purchasing a commercial license. Please contact licensing@stalw.art
|
||||
* for more details.
|
||||
*/
|
||||
|
||||
use std::{
|
||||
sync::Arc,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use ahash::{AHashMap, AHashSet, HashMap, HashSet};
|
||||
use hyper::{header::AUTHORIZATION, StatusCode};
|
||||
use mail_auth::MX;
|
||||
use mail_parser::DateTime;
|
||||
use utils::config::ServerProtocol;
|
||||
|
||||
use crate::smtp::{
|
||||
inbound::TestQueueEvent, management::send_manage_request, outbound::start_test_server,
|
||||
session::TestSession, TestConfig, TestCore,
|
||||
};
|
||||
use smtp::{
|
||||
config::IfBlock,
|
||||
core::{management::Message, Core, Session},
|
||||
lookup::Lookup,
|
||||
queue::{
|
||||
manager::{Queue, SpawnQueue},
|
||||
QueueId, Status,
|
||||
},
|
||||
};
|
||||
|
||||
#[tokio::test]
|
||||
#[serial_test::serial]
|
||||
async fn manage_queue() {
|
||||
/*tracing::subscriber::set_global_default(
|
||||
tracing_subscriber::FmtSubscriber::builder()
|
||||
.with_max_level(tracing::Level::DEBUG)
|
||||
.finish(),
|
||||
)
|
||||
.unwrap();*/
|
||||
|
||||
// Start remote test server
|
||||
let mut core = Core::test();
|
||||
core.session.config.rcpt.relay = IfBlock::new(true);
|
||||
let mut remote_qr = core.init_test_queue("smtp_manage_queue_remote");
|
||||
let _rx_remote = start_test_server(core.into(), &[ServerProtocol::Smtp]);
|
||||
|
||||
// Add mock DNS entries
|
||||
let mut core = Core::test();
|
||||
core.resolvers.dns.mx_add(
|
||||
"foobar.org",
|
||||
vec![MX {
|
||||
exchanges: vec!["mx1.foobar.org".to_string()],
|
||||
preference: 10,
|
||||
}],
|
||||
Instant::now() + Duration::from_secs(10),
|
||||
);
|
||||
|
||||
core.resolvers.dns.ipv4_add(
|
||||
"mx1.foobar.org",
|
||||
vec!["127.0.0.1".parse().unwrap()],
|
||||
Instant::now() + Duration::from_secs(10),
|
||||
);
|
||||
|
||||
// Start local management interface
|
||||
core.session.config.rcpt.relay = IfBlock::new(true);
|
||||
core.session.config.rcpt.max_recipients = IfBlock::new(100);
|
||||
core.session.config.extensions.future_release = IfBlock::new(Some(Duration::from_secs(86400)));
|
||||
core.session.config.extensions.dsn = IfBlock::new(true);
|
||||
core.queue.config.retry = IfBlock::new(vec![Duration::from_secs(1000)]);
|
||||
core.queue.config.notify = IfBlock::new(vec![Duration::from_secs(2000)]);
|
||||
core.queue.config.expire = IfBlock::new(Duration::from_secs(3000));
|
||||
core.queue.config.management_lookup = Arc::new(Lookup::Local(AHashSet::from_iter([
|
||||
"admin:secret".to_string(),
|
||||
])));
|
||||
let local_qr = core.init_test_queue("smtp_manage_queue_local");
|
||||
let core = Arc::new(core);
|
||||
local_qr.queue_rx.spawn(core.clone(), Queue::default());
|
||||
let _rx_manage = start_test_server(core.clone(), &[ServerProtocol::Http]);
|
||||
|
||||
// Send test messages
|
||||
let envelopes = HashMap::from_iter([
|
||||
(
|
||||
"a",
|
||||
(
|
||||
"bill1@foobar.net",
|
||||
vec![
|
||||
"rcpt1@example1.org",
|
||||
"rcpt1@example2.org",
|
||||
"rcpt1@example2.org",
|
||||
],
|
||||
),
|
||||
),
|
||||
(
|
||||
"b",
|
||||
(
|
||||
"bill2@foobar.net",
|
||||
vec!["rcpt3@example1.net", "rcpt4@example1.net"],
|
||||
),
|
||||
),
|
||||
(
|
||||
"c",
|
||||
(
|
||||
"bill3@foobar.net",
|
||||
vec![
|
||||
"rcpt5@example1.com",
|
||||
"rcpt6@example2.com",
|
||||
"rcpt7@example2.com",
|
||||
"rcpt8@example3.com",
|
||||
"rcpt9@example4.com",
|
||||
],
|
||||
),
|
||||
),
|
||||
("d", ("bill4@foobar.net", vec!["delay@foobar.org"])),
|
||||
("e", ("bill5@foobar.net", vec!["john@foobar.org"])),
|
||||
("f", ("", vec!["success@foobar.org", "delay@foobar.org"])),
|
||||
]);
|
||||
let mut session = Session::test(core.clone());
|
||||
session.data.remote_ip = "10.0.0.1".parse().unwrap();
|
||||
session.eval_session_params().await;
|
||||
session.ehlo("foobar.net").await;
|
||||
for test_num in 0..6 {
|
||||
let env_id = char::from(b'a' + test_num).to_string();
|
||||
let hold_for = ((test_num + 1) as u32) * 100;
|
||||
let (sender, recipients) = envelopes.get(env_id.as_str()).unwrap();
|
||||
session
|
||||
.send_message(
|
||||
&if env_id != "f" {
|
||||
format!("<{sender}> ENVID={env_id} HOLDFOR={hold_for}")
|
||||
} else {
|
||||
format!("<{sender}> ENVID={env_id}")
|
||||
},
|
||||
recipients,
|
||||
"test:no_dkim",
|
||||
"250",
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
// Expect delivery to success@foobar.org
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert_eq!(
|
||||
remote_qr
|
||||
.read_event()
|
||||
.await
|
||||
.unwrap_message()
|
||||
.recipients
|
||||
.into_iter()
|
||||
.map(|r| r.address)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["success@foobar.org".to_string()]
|
||||
);
|
||||
|
||||
// Fetch and validate messages
|
||||
let ids = send_manage_request::<Vec<QueueId>>("/queue/list")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data();
|
||||
assert_eq!(ids.len(), 6);
|
||||
let mut id_map = AHashMap::new();
|
||||
let mut id_map_rev = AHashMap::new();
|
||||
let mut test_search = String::new();
|
||||
for (message, id) in get_messages(&ids).await.into_iter().zip(ids) {
|
||||
let message = message.unwrap();
|
||||
let env_id = message.env_id.as_ref().unwrap().clone();
|
||||
|
||||
// Validate return path and recipients
|
||||
let (sender, recipients) = envelopes.get(env_id.as_str()).unwrap();
|
||||
assert_eq!(&message.return_path, sender);
|
||||
'outer: for recipient in recipients {
|
||||
for domain in &message.domains {
|
||||
for rcpt in &domain.recipients {
|
||||
if &rcpt.address == recipient {
|
||||
continue 'outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
panic!("Recipient {recipient} not found in message.");
|
||||
}
|
||||
|
||||
// Validate status and datetimes
|
||||
let created = message.created.to_timestamp();
|
||||
let hold_for = (env_id.as_bytes().first().unwrap() - b'a' + 1) as i64 * 100;
|
||||
let next_retry = created + hold_for;
|
||||
let next_notify = created + 2000 + hold_for;
|
||||
let expires = created + 3000 + hold_for;
|
||||
for domain in &message.domains {
|
||||
if env_id == "c" {
|
||||
test_search = domain.next_retry.as_ref().unwrap().to_rfc3339();
|
||||
}
|
||||
if env_id != "f" {
|
||||
assert_eq!(domain.retry_num, 0);
|
||||
assert_timestamp(
|
||||
domain.next_retry.as_ref().unwrap(),
|
||||
next_retry,
|
||||
"retry",
|
||||
&message,
|
||||
);
|
||||
assert_timestamp(
|
||||
domain.next_notify.as_ref().unwrap(),
|
||||
next_notify,
|
||||
"notify",
|
||||
&message,
|
||||
);
|
||||
assert_timestamp(&domain.expires, expires, "expires", &message);
|
||||
for rcpt in &domain.recipients {
|
||||
assert_eq!(&rcpt.status, &Status::Scheduled, "{message:#?}");
|
||||
}
|
||||
} else {
|
||||
assert_eq!(domain.retry_num, 1);
|
||||
for rcpt in &domain.recipients {
|
||||
if rcpt.address == "success@foobar.org" {
|
||||
assert!(
|
||||
matches!(&rcpt.status, Status::Completed(_)),
|
||||
"{:?}",
|
||||
rcpt.status
|
||||
);
|
||||
} else {
|
||||
assert!(
|
||||
matches!(&rcpt.status, Status::TemporaryFailure(_)),
|
||||
"{:?}",
|
||||
rcpt.status
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
id_map.insert(env_id.clone(), id);
|
||||
id_map_rev.insert(id, env_id);
|
||||
}
|
||||
assert_eq!(id_map.len(), 6);
|
||||
|
||||
// Test list search
|
||||
for (query, expected_ids) in [
|
||||
("/queue/list?from=bill1@foobar.net".to_string(), vec!["a"]),
|
||||
("/queue/list?to=foobar.org".to_string(), vec!["d", "e", "f"]),
|
||||
(
|
||||
"/queue/list?from=bill3@foobar.net&to=rcpt5@example1.com".to_string(),
|
||||
vec!["c"],
|
||||
),
|
||||
(format!("/queue/list?before={test_search}"), vec!["a", "b"]),
|
||||
(
|
||||
format!("/queue/list?after={test_search}"),
|
||||
vec!["d", "e", "f", "c"],
|
||||
),
|
||||
] {
|
||||
let expected_ids = HashSet::from_iter(expected_ids.into_iter().map(|s| s.to_string()));
|
||||
let ids = send_manage_request::<Vec<QueueId>>(&query)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
.into_iter()
|
||||
.map(|id| id_map_rev.get(&id).unwrap().clone())
|
||||
.collect::<HashSet<_>>();
|
||||
assert_eq!(ids, expected_ids, "failed for {query}");
|
||||
}
|
||||
|
||||
// Retry delivery
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<bool>>(&format!(
|
||||
"/queue/retry?id={},{}",
|
||||
id_map.get("e").unwrap(),
|
||||
id_map.get("f").unwrap()
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data(),
|
||||
vec![true, true]
|
||||
);
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<bool>>(&format!(
|
||||
"/queue/retry?id={}&filter=example1.org&at=2200-01-01T00:00:00Z",
|
||||
id_map.get("a").unwrap(),
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data(),
|
||||
vec![true]
|
||||
);
|
||||
|
||||
// Expect delivery to john@foobar.org
|
||||
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||
assert_eq!(
|
||||
remote_qr
|
||||
.read_event()
|
||||
.await
|
||||
.unwrap_message()
|
||||
.recipients
|
||||
.into_iter()
|
||||
.map(|r| r.address)
|
||||
.collect::<Vec<_>>(),
|
||||
vec!["john@foobar.org".to_string()]
|
||||
);
|
||||
|
||||
// Message 'e' should be gone, 'f' should have retry_num == 2
|
||||
// while 'a' should have a retry time of 2200-01-01T00:00:00Z for example1.org
|
||||
let mut messages = get_messages(&[
|
||||
*id_map.get("e").unwrap(),
|
||||
*id_map.get("f").unwrap(),
|
||||
*id_map.get("a").unwrap(),
|
||||
])
|
||||
.await
|
||||
.into_iter();
|
||||
assert_eq!(messages.next().unwrap(), None);
|
||||
assert_eq!(
|
||||
messages
|
||||
.next()
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.domains
|
||||
.first()
|
||||
.unwrap()
|
||||
.retry_num,
|
||||
2
|
||||
);
|
||||
for domain in messages.next().unwrap().unwrap().domains {
|
||||
let next_retry = domain.next_retry.as_ref().unwrap().to_rfc3339();
|
||||
let matched =
|
||||
["2200-01-01T00:00:00Z", "2199-12-31T23:59:59Z"].contains(&next_retry.as_str());
|
||||
if domain.name == "example1.org" {
|
||||
assert!(matched, "{next_retry}");
|
||||
} else {
|
||||
assert!(!matched, "{next_retry}");
|
||||
}
|
||||
}
|
||||
|
||||
// Cancel deliveries
|
||||
for (id, filter) in [
|
||||
("a", "example2.org"),
|
||||
("b", "example1.net"),
|
||||
("c", "rcpt6@example2.com"),
|
||||
("d", ""),
|
||||
] {
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<bool>>(&format!(
|
||||
"/queue/cancel?id={}{}{}",
|
||||
id_map.get(id).unwrap(),
|
||||
if !filter.is_empty() { "&filter=" } else { "" },
|
||||
filter
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data(),
|
||||
vec![true],
|
||||
"failed for {id}: {filter}"
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
send_manage_request::<Vec<QueueId>>("/queue/list")
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
for (message, id) in get_messages(&[
|
||||
*id_map.get("a").unwrap(),
|
||||
*id_map.get("b").unwrap(),
|
||||
*id_map.get("c").unwrap(),
|
||||
*id_map.get("d").unwrap(),
|
||||
])
|
||||
.await
|
||||
.into_iter()
|
||||
.zip(["a", "b", "c", "d"])
|
||||
{
|
||||
if ["b", "d"].contains(&id) {
|
||||
assert_eq!(message, None);
|
||||
} else {
|
||||
let message = message.unwrap();
|
||||
assert!(!message.domains.is_empty());
|
||||
for domain in message.domains {
|
||||
match id {
|
||||
"a" => {
|
||||
if domain.name == "example2.org" {
|
||||
assert_eq!(&domain.status, &Status::Completed("".to_string()));
|
||||
for rcpt in &domain.recipients {
|
||||
assert!(matches!(&rcpt.status, Status::Completed(_)));
|
||||
}
|
||||
} else {
|
||||
assert_eq!(&domain.status, &Status::Scheduled);
|
||||
for rcpt in &domain.recipients {
|
||||
assert!(matches!(&rcpt.status, Status::Scheduled));
|
||||
}
|
||||
}
|
||||
}
|
||||
"c" => {
|
||||
assert_eq!(&domain.status, &Status::Scheduled);
|
||||
if domain.name == "example2.com" {
|
||||
for rcpt in &domain.recipients {
|
||||
if rcpt.address == "rcpt6@example2.com" {
|
||||
assert!(matches!(&rcpt.status, Status::Completed(_)));
|
||||
} else {
|
||||
assert!(matches!(&rcpt.status, Status::Scheduled));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for rcpt in &domain.recipients {
|
||||
assert!(matches!(&rcpt.status, Status::Scheduled));
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test authentication error
|
||||
assert_eq!(
|
||||
reqwest::Client::builder()
|
||||
.timeout(Duration::from_millis(500))
|
||||
.danger_accept_invalid_certs(true)
|
||||
.build()
|
||||
.unwrap()
|
||||
.get("https://127.0.0.1:9980/list")
|
||||
.header(AUTHORIZATION, "Basic YWRtaW46aGVsbG93b3JsZA==")
|
||||
.send()
|
||||
.await
|
||||
.unwrap()
|
||||
.status(),
|
||||
StatusCode::UNAUTHORIZED
|
||||
);
|
||||
}
|
||||
|
||||
fn assert_timestamp(timestamp: &DateTime, expected: i64, ctx: &str, message: &Message) {
|
||||
let timestamp = timestamp.to_timestamp();
|
||||
let diff = timestamp - expected;
|
||||
if ![-2, -1, 0, 1, 2].contains(&diff) {
|
||||
panic!("Got timestamp {timestamp}, expected {expected} (diff {diff} for {ctx}) for {message:?}");
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_messages(ids: &[QueueId]) -> Vec<Option<Message>> {
|
||||
send_manage_request(&format!(
|
||||
"/queue/status?id={}",
|
||||
ids.iter()
|
||||
.map(|id| id.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join(",")
|
||||
))
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap_data()
|
||||
}
|
||||
Reference in New Issue
Block a user