/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ use std::time::{Duration, Instant}; use ahash::{AHashMap, HashMap, HashSet}; use common::config::server::ServerProtocol; use http::management::queue::Message; use mail_auth::MX; use mail_parser::DateTime; use reqwest::{Method, StatusCode, header::AUTHORIZATION}; use crate::{ jmap::ManagementApi, smtp::{DnsCache, TestSMTP, session::TestSession}, }; use smtp::queue::{QueueId, Status, manager::SpawnQueue}; const LOCAL: &str = r#" [storage] directory = "local" [directory."local"] type = "memory" [[directory."local".principals]] name = "admin" type = "admin" description = "Superuser" secret = "secret" class = "admin" [queue.schedule.default] retry = "1000s" notify = "2000s" expire = "3000s" queue-name = "default" [session.rcpt] relay = true max-recipients = 100 [session.extensions] dsn = true future-release = "1h" "#; const REMOTE: &str = r#" [session.ehlo] reject-non-fqdn = false [session.rcpt] relay = true "#; #[derive(serde::Deserialize, Debug)] #[allow(dead_code)] pub(super) struct List { pub items: Vec, pub total: usize, } #[tokio::test] #[serial_test::serial] async fn manage_queue() { // Enable logging crate::enable_logging(); // Start remote test server let mut remote = TestSMTP::new("smtp_manage_queue_remote", REMOTE).await; let _rx = remote.start(&[ServerProtocol::Smtp]).await; let remote_core = remote.build_smtp(); // Start local management interface let local = TestSMTP::new("smtp_manage_queue_local", LOCAL).await; // Add mock DNS entries let core = local.build_smtp(); core.mx_add( "foobar.org", vec![MX { exchanges: vec!["mx1.foobar.org".to_string()], preference: 10, }], Instant::now() + Duration::from_secs(10), ); core.ipv4_add( "mx1.foobar.org", vec!["127.0.0.1".parse().unwrap()], Instant::now() + Duration::from_secs(10), ); let _rx_manage = local.start(&[ServerProtocol::Http]).await; // 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 = local.new_session(); local .queue_receiver .queue_rx .spawn(local.server.inner.clone()); session.data.remote_ip_str = "10.0.0.1".into(); 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 .queue_receiver .consume_message(&remote_core) .await .message .recipients .into_iter() .map(|r| r.address().to_string()) .collect::>(), vec!["success@foobar.org"] ); // Fetch and validate messages let api = ManagementApi::default(); let ids = api .request::>(Method::GET, "/api/queue/messages") .await .unwrap() .unwrap_data() .items; 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 api.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 rcpt in &message.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 rcpt in &message.recipients { if env_id == "c" { let mut dt = *rcpt.next_retry.as_ref().unwrap(); dt.second -= 1; test_search = dt.to_rfc3339(); } if env_id != "f" { // HOLDFOR messages assert_eq!(rcpt.retry_num, 0); assert_timestamp( rcpt.next_retry.as_ref().unwrap(), next_retry, "retry", &message, ); assert_timestamp( rcpt.next_notify.as_ref().unwrap(), next_notify, "notify", &message, ); assert_timestamp(&rcpt.expires.unwrap(), expires, "expires", &message); assert_eq!(&rcpt.status, &Status::Scheduled, "{message:#?}"); } else if rcpt.address == "success@foobar.org" { assert_eq!(rcpt.retry_num, 0); assert!( matches!(&rcpt.status, Status::Completed(_)), "{:?}", rcpt.status ); } else { assert_eq!(rcpt.retry_num, 1); 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 [ ( "/api/queue/messages?from=bill1@foobar.net".to_string(), vec!["a"], ), ( "/api/queue/messages?to=foobar.org".to_string(), vec!["d", "e", "f"], ), ( "/api/queue/messages?from=bill3@foobar.net&to=rcpt5@example1.com".to_string(), vec!["c"], ), ( format!("/api/queue/messages?before={test_search}"), vec!["a", "b"], ), ( format!("/api/queue/messages?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 = api .request::>(Method::GET, &query) .await .unwrap() .unwrap_data() .items .into_iter() .map(|id| id_map_rev.get(&id).unwrap().clone()) .collect::>(); assert_eq!(ids, expected_ids, "failed for {query}"); } // Retry delivery for id in [id_map.get("e").unwrap(), id_map.get("f").unwrap()] { assert!( api.request::(Method::PATCH, &format!("/api/queue/messages/{id}",)) .await .unwrap() .unwrap_data(), ); } assert!( api.request::( Method::PATCH, &format!( "/api/queue/messages/{}?filter=example1.org&at=2200-01-01T00:00:00Z", id_map.get("a").unwrap(), ) ) .await .unwrap() .unwrap_data() ); // Expect delivery to john@foobar.org tokio::time::sleep(Duration::from_millis(100)).await; assert_eq!( remote .queue_receiver .consume_message(&remote_core) .await .message .recipients .into_iter() .map(|r| r.address().to_string()) .collect::>(), 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 = api .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() .recipients .first() .unwrap() .retry_num, 2 ); for rcpt in messages.next().unwrap().unwrap().recipients { let next_retry = rcpt.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 rcpt.address.ends_with("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!( api.request::( Method::DELETE, &format!( "/api/queue/messages/{}{}{}", id_map.get(id).unwrap(), if !filter.is_empty() { "?filter=" } else { "" }, filter ) ) .await .unwrap() .unwrap_data(), "failed for {id}: {filter}" ); } assert_eq!( api.request::>(Method::GET, "/api/queue/messages") .await .unwrap() .unwrap_data() .items .len(), 3 ); for (message, id) in api .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.recipients.is_empty()); for rcpt in message.recipients { match id { "a" => { if rcpt.address.ends_with("example2.org") { assert!(matches!(&rcpt.status, Status::PermanentFailure(_))); } else { assert!(matches!(&rcpt.status, Status::Scheduled)); } } "c" => { if rcpt.address.ends_with("example2.com") { if rcpt.address == "rcpt6@example2.com" { assert!(matches!(&rcpt.status, Status::PermanentFailure(_))); } else { assert!(matches!(&rcpt.status, Status::Scheduled)); } } else { assert!(matches!(&rcpt.status, Status::Scheduled)); } } _ => unreachable!(), } } } } // Bulk cancel assert_eq!( api.request::>(Method::GET, "/api/queue/messages?values=1") .await .unwrap() .unwrap_data() .items .len(), 3 ); assert!( api.request::(Method::DELETE, "/api/queue/messages?text=example2.com") .await .unwrap() .unwrap_data() ); tokio::time::sleep(Duration::from_millis(100)).await; assert_eq!( api.request::>(Method::GET, "/api/queue/messages") .await .unwrap() .unwrap_data() .items .len(), 2 ); assert!( api.request::(Method::DELETE, "/api/queue/messages") .await .unwrap() .unwrap_data() ); tokio::time::sleep(Duration::from_millis(100)).await; assert_eq!( api.request::>(Method::GET, "/api/queue/messages") .await .unwrap() .unwrap_data() .items .len(), 0 ); // 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/api/queue/messages") .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:?}" ); } } impl ManagementApi { async fn get_messages(&self, ids: &[QueueId]) -> Vec> { let mut results = Vec::with_capacity(ids.len()); for id in ids { let message = self .request::(Method::GET, &format!("/api/queue/messages/{id}",)) .await .unwrap() .try_unwrap_data(); results.push(message); } results } }