diff --git a/crates/common/src/expr/if_block.rs b/crates/common/src/expr/if_block.rs index c5218b46..2c926b0d 100644 --- a/crates/common/src/expr/if_block.rs +++ b/crates/common/src/expr/if_block.rs @@ -74,7 +74,7 @@ impl IfBlock { } impl Expression { - fn parse(token_map: &TokenMap, expr: &str) -> Self { + pub fn parse(token_map: &TokenMap, expr: &str) -> Self { ExpressionParser::new(Tokenizer::new(expr, token_map)) .parse() .unwrap() diff --git a/crates/store/src/build/lookup.rs b/crates/store/src/build/lookup.rs index 8a8da41f..216fed50 100644 --- a/crates/store/src/build/lookup.rs +++ b/crates/store/src/build/lookup.rs @@ -36,6 +36,11 @@ impl LookupStores { .await .map(InMemoryStore::Store) } + #[cfg(feature = "sqlite")] + LookupStore::Sqlite(sqlite_store) => { + crate::backend::sqlite::SqliteStore::open(sqlite_store) + .map(InMemoryStore::Store) + } // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC // SPDX-License-Identifier: LicenseRef-SEL diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 37e6159a..27ff36cf 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -6,7 +6,7 @@ edition = "2024" [features] #default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "nats", "azure", "foundationdb"] #default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "foundationdb"] -default = ["rocks"] +default = ["rocks", "sqlite"] sqlite = ["store/sqlite", "directory/sqlite"] foundationdb = ["store/foundation", "common/foundation"] postgres = ["store/postgres", "directory/postgres"] diff --git a/tests/resources/smtp/sieve/stage_mail.sieve b/tests/resources/smtp/sieve/stage_mail.sieve index 473a7631..7c67b383 100644 --- a/tests/resources/smtp/sieve/stage_mail.sieve +++ b/tests/resources/smtp/sieve/stage_mail.sieve @@ -11,7 +11,7 @@ if eval "query('sql', 'SELECT 1 FROM blocked_senders WHERE addr=? LIMIT 1', [env reject "Your address has been blocked."; } -if eval "!is_local_domain('', 'localdomain.org') || is_local_domain('', 'other.org')" { - let "reason" "'result: ' + is_local_domain('', 'localdomain.org') + ' ' + is_local_domain('', 'other.org')"; +if eval "!is_local_domain('foobar.org') || is_local_domain('other.org')" { + let "reason" "'result: ' + is_local_domain('foobar.org') + ' ' + is_local_domain('other.org')"; reject "is_local_domain function failed: ${reason}"; } diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index fcdfe339..fd5891cd 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -207,9 +207,7 @@ pub async fn imap_tests() { }) .await; admin.reload_settings().await; - admin - .registry_create_object(Action::ReloadLookupStores) - .await; + admin.reload_lookup_stores().await; test.insert_account(admin); diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index 4565522c..034db9e5 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -7,7 +7,7 @@ use crate::{ http_server::{HttpMessage, spawn_mock_http_server}, jmap::server::enterprise::EnterpriseCore, - smtp::{DnsCache, TempDir, TestSMTP, session::TestSession}, + smtp::{DnsCache, session::TestSession}, }; use ahash::{AHashMap, AHashSet}; use common::{ @@ -171,9 +171,6 @@ allow-invalid-certs = true #[tokio::test(flavor = "multi_thread")] async fn antispam() { - - - // Prepare config let tmp_dir = TempDir::new("smtp_antispam_test", true); let mut config = CONFIG.replace("{PATH}", tmp_dir.temp_dir.as_path().to_str().unwrap()); @@ -389,7 +386,7 @@ async fn antispam() { let mut in_params = true; // Build session - let mut session = Session::test(server.clone()); + let mut session = test.new_mta_session(); let mut arc_result = None; let mut dkim_result = None; let mut dkim_signatures = vec![]; diff --git a/tests/src/smtp/inbound/asn.rs b/tests/src/smtp/inbound/asn.rs index 28cc9e45..4a0a25fe 100644 --- a/tests/src/smtp/inbound/asn.rs +++ b/tests/src/smtp/inbound/asn.rs @@ -4,66 +4,64 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#[cfg(test)] -mod tests { - use std::time::{Duration, Instant}; +use crate::utils::server::TestServerBuilder; +use registry::{ + schema::structs::{Asn, AsnDns, AsnResource}, + types::map::Map, +}; +use std::time::{Duration, Instant}; - use common::{Core, Server, config::network::AsnGeoLookupConfig}; +#[ignore] +#[tokio::test] +async fn asn() { + let mut test = TestServerBuilder::new("smtp_asn_test") + .await + .with_http_listener(19011) + .await + .disable_services() + .capture_queue() + .build() + .await; - #[tokio::test] - #[ignore] - async fn lookup_asn_country_dns() { - let mut core = Core::default(); - core.network.asn_geo_lookup = AsnGeoLookupConfig::Dns { - zone_ipv4: "origin.asn.cymru.com".to_string(), - zone_ipv6: "origin6.asn.cymru.com".to_string(), - separator: '|'.to_string(), + let admin = test.account("admin"); + admin + .registry_create_object(Asn::Dns(AsnDns { index_asn: 0, index_asn_name: 3.into(), index_country: 2.into(), - }; - let server = Server { - core: core.into(), - inner: Default::default(), - }; + separator: '|'.to_string(), + zone_ip_v4: "origin.asn.cymru.com".to_string(), + zone_ip_v6: "origin6.asn.cymru.com".to_string(), + })) + .await; + admin.reload_settings().await; + test.reload_core(); + let admin = test.account("admin"); - for (ip, asn, asn_name, country) in [ - ("8.8.8.8", 15169, "arin", "US"), - ("1.1.1.1", 13335, "apnic", "AU"), - ("2a01:4f9:c011:b43c::1", 24940, "ripencc", "DE"), - ("1.33.1.1", 2514, "apnic", "JP"), - ] { - let result = server.lookup_asn_country(ip.parse().unwrap()).await; - println!("{ip}: {result:?}"); - assert_eq!(result.asn.as_ref().map(|r| r.id), Some(asn)); - assert_eq!( - result.asn.as_ref().and_then(|r| r.name.as_deref()), - Some(asn_name) - ); - assert_eq!(result.country.as_ref().map(|s| s.as_str()), Some(country)); - } + for (ip, asn, asn_name, country) in [ + ("8.8.8.8", 15169, "arin", "US"), + ("1.1.1.1", 13335, "apnic", "AU"), + ("2a01:4f9:c011:b43c::1", 24940, "ripencc", "DE"), + ("1.33.1.1", 2514, "apnic", "JP"), + ] { + let result = test.server.lookup_asn_country(ip.parse().unwrap()).await; + println!("{ip}: {result:?}"); + assert_eq!(result.asn.as_ref().map(|r| r.id), Some(asn)); + assert_eq!( + result.asn.as_ref().and_then(|r| r.name.as_deref()), + Some(asn_name) + ); + assert_eq!(result.country.as_ref().map(|s| s.as_str()), Some(country)); } - #[tokio::test] - #[ignore] - async fn lookup_asn_country_http() { - let mut core = Core::default(); - core.network.asn_geo_lookup = AsnGeoLookupConfig::Resource { - expires: Duration::from_secs(86400), - timeout: Duration::from_secs(100), - max_size: 100 * 1024 * 1024, - headers: Default::default(), - asn_resources: vec![ - //url: "file:///Users/me/code/playground/asn-ipv4.csv".to_string(), - //url: "file:///Users/me/code/playground/asn-ipv6.csv".to_string(), + admin + .registry_create_object(Asn::Resource(AsnResource { + asn_urls: Map::new(vec![ "https://cdn.jsdelivr.net/npm/@ip-location-db/asn/asn-ipv4.csv".to_string(), "https://cdn.jsdelivr.net/npm/@ip-location-db/asn/asn-ipv6.csv".to_string(), - ], - geo_resources: vec![ - //url: "file:///Users/me/code/playground/geolite2-geo-whois-asn-country-ipv4.csv" - // .to_string(), - //url: "file:///Users/me/code/playground/geolite2-geo-whois-asn-country-ipv6.csv" - // .to_string(), + ]), + expires: 86_400_100u64.into(), + geo_urls: Map::new(vec![ concat!( "https://cdn.jsdelivr.net/npm/@ip-location-db/geolite2-geo-whois-", "asn-country/geolite2-geo-whois-asn-country-ipv4.csv" @@ -74,37 +72,40 @@ mod tests { "asn-country/geolite2-geo-whois-asn-country-ipv6.csv" ) .to_string(), - ], - }; - let server = Server { - core: core.into(), - inner: Default::default(), - }; + ]), + max_size: 100 * 1024 * 1024, + timeout: 100_000u64.into(), + ..Default::default() + })) + .await; + admin.reload_settings().await; + test.reload_core(); - server.lookup_asn_country("8.8.8.8".parse().unwrap()).await; - let time = Instant::now(); - loop { - tokio::time::sleep(Duration::from_millis(500)).await; - if server.inner.data.asn_geo_data.lock.available_permits() > 0 { - break; - } - } - println!("Fetch took {:?}", time.elapsed()); - - for (ip, asn, asn_name, country) in [ - ("8.8.8.8", 15169, "Google LLC", "US"), - ("1.1.1.1", 13335, "Cloudflare, Inc.", "AU"), - ("2a01:4f9:c011:b43c::1", 24940, "Hetzner Online GmbH", "FI"), - ("1.33.1.1", 2514, "NTT PC Communications, Inc.", "JP"), - ] { - let result = server.lookup_asn_country(ip.parse().unwrap()).await; - println!("{ip}: {result:?}"); - assert_eq!(result.asn.as_ref().map(|r| r.id), Some(asn)); - assert_eq!( - result.asn.as_ref().and_then(|r| r.name.as_deref()), - Some(asn_name) - ); - assert_eq!(result.country.as_ref().map(|s| s.as_str()), Some(country)); + test.server + .lookup_asn_country("8.8.8.8".parse().unwrap()) + .await; + let time = Instant::now(); + loop { + tokio::time::sleep(Duration::from_millis(500)).await; + if test.server.inner.data.asn_geo_data.lock.available_permits() > 0 { + break; } } + println!("Fetch took {:?}", time.elapsed()); + + for (ip, asn, asn_name, country) in [ + ("8.8.8.8", 15169, "Google LLC", "US"), + ("1.1.1.1", 13335, "Cloudflare, Inc.", "AU"), + ("2a01:4f9:c011:b43c::1", 24940, "Hetzner Online GmbH", "FI"), + ("1.33.1.1", 2514, "NTT PC Communications, Inc.", "JP"), + ] { + let result = test.server.lookup_asn_country(ip.parse().unwrap()).await; + println!("{ip}: {result:?}"); + assert_eq!(result.asn.as_ref().map(|r| r.id), Some(asn)); + assert_eq!( + result.asn.as_ref().and_then(|r| r.name.as_deref()), + Some(asn_name) + ); + assert_eq!(result.country.as_ref().map(|s| s.as_str()), Some(country)); + } } diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index b08074b9..4f92d4ac 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -193,6 +193,7 @@ async fn data() { .await; admin.reload_settings().await; test.reload_core(); + test.expect_reload_settings().await; // Test queue message builder let mut session = test.new_mta_session(); diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index c1c99856..49942ae5 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -4,12 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{ - DnsCache, TempDir, TestSMTP, - inbound::{TestMessage, TestReportingEvent, sign::SIGNATURES}, - session::{TestSession, VerifyResponse}, +use crate::{ + smtp::{ + inbound::{TestMessage, TestReportingEvent}, + session::{TestSession, VerifyResponse}, + }, + utils::{dns::DnsCache, server::TestServerBuilder}, }; -use common::{Core, config::smtp::report::AggregateFrequency}; +use common::config::smtp::report::AggregateFrequency; use mail_auth::{ common::{parse::TxtRecordParser, verify::DomainKey}, dkim::DomainKeyReport, @@ -17,83 +19,167 @@ use mail_auth::{ report::DmarcResult, spf::Spf, }; -use smtp::core::Session; +use registry::{ + schema::structs::{ + DkimReportSettings, DmarcReportSettings, Domain, Expression, ExpressionMatch, MtaStageAuth, + MtaStageData, SenderAuth, SpfReportSettings, + }, + types::list::List, +}; use std::time::{Duration, Instant}; -const CONFIG: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/queue.db" - -[directory."local"] -type = "memory" - -[[directory."local".principals]] -name = "john" -description = "John Doe" -secret = "secret" -email = ["jdoe@example.com"] - -[session.rcpt] -directory = "'local'" - -[session.data.add-headers] -received = true -received-spf = true -auth-results = true -message-id = true -date = true -return-path = false - -[report.dkim] -send = "[1, 1s]" -sign = "['rsa']" - -[report.spf] -send = "[1, 1s]" -sign = "['rsa']" - -[report.dmarc] -send = "[1, 1s]" -sign = "['rsa']" - -[report.dmarc.aggregate] -send = "daily" - -[auth.spf.verify] -ehlo = [{if = "remote_ip = '10.0.0.2'", then = 'strict'}, - { else = 'relaxed' }] -mail-from = [{if = "remote_ip = '10.0.0.2'", then = 'strict'}, - { else = 'relaxed' }] - -[auth.dmarc] -verify = "strict" - -[auth.arc] -verify = "strict" - -[auth.dkim] -verify = [{if = "sender_domain = 'test.net'", then = 'relaxed'}, - { else = 'strict' }] - -"#; - #[tokio::test] async fn dmarc() { - - + let mut test = TestServerBuilder::new("smtp_dmarc_test") + .await + .with_http_listener(19012) + .await + .disable_services() + .capture_queue() + .capture_reporting() + .build() + .await; - let tmp_dir = TempDir::new("smtp_dmarc_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG.to_string() + SIGNATURES)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; - let test = TestSMTP::from_core(core); + // Add test settings + let admin = test.account("admin"); + let domain_id = admin + .registry_create_object(Domain { + name: "localdomain.org".into(), + allow_relaying: true, + ..Default::default() + }) + .await; + admin.create_dkim_signatures(domain_id).await; + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageData { + add_date_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_message_id_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_received_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_received_spf_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_auth_results_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_return_path_header: Expression { + else_: "false".into(), + ..Default::default() + }, + enable_spam_filter: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SenderAuth { + dmarc_verify: Expression { + else_: "strict".into(), + ..Default::default() + }, + reverse_ip_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + spf_ehlo_verify: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "strict".into(), + }]), + else_: "relaxed".into(), + }, + spf_from_verify: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "strict".into(), + }]), + else_: "relaxed".into(), + }, + arc_verify: Expression { + else_: "strict".into(), + ..Default::default() + }, + dkim_sign_domain: Expression { + else_: "'localdomain.org'".into(), + ..Default::default() + }, + dkim_verify: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "sender_domain = 'test.net'".into(), + then: "relaxed".into(), + }]), + else_: "strict".into(), + }, + dkim_strict: false, + }) + .await; + admin + .registry_create_object(DkimReportSettings { + dkim_sign_domain: Expression { + else_: "'localdomain.org'".into(), + ..Default::default() + }, + send_frequency: Expression { + else_: "[1, 1s]".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SpfReportSettings { + dkim_sign_domain: Expression { + else_: "'localdomain.org'".into(), + ..Default::default() + }, + send_frequency: Expression { + else_: "[1, 1s]".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(DmarcReportSettings { + failure_dkim_sign_domain: Expression { + else_: "'localdomain.org'".into(), + ..Default::default() + }, + failure_send_frequency: Expression { + else_: "[1, 1s]".into(), + ..Default::default() + }, + aggregate_send_frequency: Expression { + else_: "daily".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin.reload_settings().await; + test.reload_core(); + test.expect_reload_settings().await; // Add SPF, DKIM and DMARC records test.server.txt_add( @@ -158,9 +244,7 @@ async fn dmarc() { ); // SPF must pass - let mut rr = test.report_receiver; - let mut qr = test.queue_receiver; - let mut session = Session::test(test.server.clone()); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.2".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.eval_session_params().await; @@ -168,22 +252,22 @@ async fn dmarc() { session.mail_from("bill@example.com", "550 5.7.23").await; // Expect SPF auth failure report - let message = qr.expect_message().await; + let message = test.expect_message().await; assert_eq!( message.message.recipients.last().unwrap().address(), "spf-failures@example.com" ); message - .read_lines(&qr) + .read_lines(&test) .await - .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com;") + .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=localdomain.org;") .assert_contains("To: spf-failures@example.com") .assert_contains("Feedback-Type: auth-failure") .assert_contains("Auth-Failure: spf"); // Second DKIM failure report should be rate limited session.mail_from("bill@example.com", "550 5.7.23").await; - qr.assert_no_events(); + test.assert_no_events(); // Invalid DKIM signatures should be rejected session.data.remote_ip_str = "10.0.0.1".into(); @@ -192,22 +276,22 @@ async fn dmarc() { session .send_message( "bill@example.com", - &["jdoe@example.com"], + &["jdoe@localdomain.org"], "test:invalid_dkim", "550 5.7.20", ) .await; // Expect DKIM auth failure report - let message = qr.expect_message().await; + let message = test.expect_message().await; assert_eq!( message.message.recipients.last().unwrap().address(), "dkim-failures@example.com" ); message - .read_lines(&qr) + .read_lines(&test) .await - .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com;") + .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=localdomain.org;") .assert_contains("To: dkim-failures@example.com") .assert_contains("Feedback-Type: auth-failure") .assert_contains("Auth-Failure: bodyhash"); @@ -216,23 +300,23 @@ async fn dmarc() { session .send_message( "bill@example.com", - &["jdoe@example.com"], + &["jdoe@localdomain.org"], "test:invalid_dkim", "550 5.7.20", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Invalid ARC should be rejected session .send_message( "bill@example.com", - &["jdoe@example.com"], + &["jdoe@localdomain.org"], "test:invalid_arc", "550 5.7.29", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Unaligned DMARC should be rejected test.server.txt_add( @@ -243,29 +327,29 @@ async fn dmarc() { session .send_message( "joe@test.net", - &["jdoe@example.com"], + &["jdoe@localdomain.org"], "test:invalid_dkim", "550 5.7.1", ) .await; // Expect DMARC auth failure report - let message = qr.expect_message().await; + let message = test.expect_message().await; assert_eq!( message.message.recipients.last().unwrap().address(), "dmarc-failures@example.com" ); message - .read_lines(&qr) + .read_lines(&test) .await - .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com;") + .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=localdomain.org;") .assert_contains("To: dmarc-failures@example.com") .assert_contains("Feedback-Type: auth-failure") .assert_contains("Auth-Failure: dmarc") .assert_contains("dmarc=3Dnone"); // Expect DMARC aggregate report - let report = rr.read_report().await.unwrap_dmarc(); + let report = test.read_report().await.unwrap_dmarc(); assert_eq!(report.domain, "example.com"); assert_eq!(report.interval, AggregateFrequency::Daily); assert_eq!(report.dmarc_record.rua().len(), 1); @@ -275,25 +359,25 @@ async fn dmarc() { session .send_message( "joe@test.net", - &["jdoe@example.com"], + &["jdoe@localdomain.org"], "test:invalid_dkim", "550 5.7.1", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Messages passing DMARC should be accepted session .send_message( "bill@example.com", - &["jdoe@example.com"], + &["jdoe@localdomain.org"], "test:dkim", "250", ) .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("dkim=pass") .assert_contains("spf=pass") diff --git a/tests/src/smtp/inbound/ehlo.rs b/tests/src/smtp/inbound/ehlo.rs index 9d154c0a..f5ab20ce 100644 --- a/tests/src/smtp/inbound/ehlo.rs +++ b/tests/src/smtp/inbound/ehlo.rs @@ -4,55 +4,121 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{ - DnsCache, TestSMTP, - session::{TestSession, VerifyResponse}, +use crate::{ + smtp::session::{TestSession, VerifyResponse}, + utils::{dns::DnsCache, server::TestServerBuilder}, }; -use common::Core; use mail_auth::{SpfResult, common::parse::TxtRecordParser, spf::Spf}; -use smtp::core::Session; +use registry::{ + schema::structs::{ + Expression, ExpressionMatch, MtaExtensions, MtaStageAuth, MtaStageData, MtaStageEhlo, + SenderAuth, + }, + types::list::List, +}; use std::time::{Duration, Instant}; -const CONFIG: &str = r#" -[session.data.limits] -size = [{if = "remote_ip = '10.0.0.1'", then = 1024}, - {else = 2048}] - -[session.extensions] -future-release = [{if = "remote_ip = '10.0.0.1'", then = '1h'}, - {else = false}] -mt-priority = [{if = "remote_ip = '10.0.0.1'", then = 'nsep'}, - {else = false}] - -[session.ehlo] -reject-non-fqdn = "starts_with(remote_ip, '10.0.0.')" - -[auth.spf.verify] -ehlo = [{if = "remote_ip = '10.0.0.2'", then = 'strict'}, - {else = 'relaxed'}] -"#; - #[tokio::test] async fn ehlo() { - - + let mut test = TestServerBuilder::new("smtp_ehlo_test") + .await + .with_http_listener(19005) + .await + .disable_services() + .build() + .await; - let mut config = Config::new(CONFIG).unwrap(); - let core = Core::parse(&mut config, Default::default(), Default::default()).await; - let server = TestSMTP::from_core(core).server; - server.txt_add( + // Add test settings + let admin = test.account("admin"); + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaExtensions { + future_release: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "1h".into(), + }]), + else_: "false".into(), + }, + mt_priority: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "nsep".into(), + }]), + else_: "false".into(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageEhlo { + reject_non_fqdn: Expression { + else_: "starts_with(remote_ip, '10.0.0.')".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageData { + max_message_size: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "1024".into(), + }]), + else_: "2048".into(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SenderAuth { + dmarc_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + reverse_ip_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + spf_ehlo_verify: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "strict".into(), + }]), + else_: "relaxed".into(), + }, + spf_from_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin.reload_settings().await; + test.reload_core(); + + test.server.txt_add( "mx1.foobar.org", Spf::parse(b"v=spf1 ip4:10.0.0.1 -all").unwrap(), Instant::now() + Duration::from_secs(5), ); - server.txt_add( + test.server.txt_add( "mx2.foobar.org", Spf::parse(b"v=spf1 ip4:10.0.0.2 -all").unwrap(), Instant::now() + Duration::from_secs(5), ); // Reject non-FQDN domains - let mut session = Session::test(server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.stream.tls = false; diff --git a/tests/src/smtp/inbound/limits.rs b/tests/src/smtp/inbound/limits.rs index 48a2705e..e8eedd6c 100644 --- a/tests/src/smtp/inbound/limits.rs +++ b/tests/src/smtp/inbound/limits.rs @@ -4,37 +4,58 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{ - TestSMTP, - session::{TestSession, VerifyResponse}, +use crate::{ + smtp::session::{TestSession, VerifyResponse}, + utils::server::TestServerBuilder, +}; +use registry::{ + schema::structs::{Expression, ExpressionMatch, MtaInboundSession}, + types::list::List, }; -use common::Core; -use smtp::core::Session; use std::time::{Duration, Instant}; -use tokio::sync::watch; - -const CONFIG: &str = r#" -[session] -transfer-limit = [{if = "remote_ip = '10.0.0.1'", then = 10}, - {else = 1024}] -timeout = [{if = "remote_ip = '10.0.0.2'", then = '500ms'}, - {else = '30m'}] -duration = [{if = "remote_ip = '10.0.0.3'", then = '500ms'}, - {else = '60m'}] -"#; #[tokio::test] async fn limits() { - - + let mut test = TestServerBuilder::new("smtp_inbound_limits_test") + .await + .with_http_listener(19013) + .await + .disable_services() + .build() + .await; - let mut config = Config::new(CONFIG).unwrap(); - let core = Core::parse(&mut config, Default::default(), Default::default()).await; - - let (_tx, rx) = watch::channel(true); + // Add test settings + let admin = test.account("admin"); + admin + .registry_create_object(MtaInboundSession { + max_duration: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.3'".into(), + then: "500ms".into(), + }]), + else_: "60m".into(), + }, + timeout: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.2'".into(), + then: "500ms".into(), + }]), + else_: "30m".into(), + }, + transfer_limit: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "10".into(), + }]), + else_: "1024".into(), + }, + }) + .await; + admin.reload_settings().await; + test.reload_core(); // Exceed max line length - let mut session = Session::test_with_shutdown(TestSMTP::from_core(core).server, rx); + let (mut session, _tx) = test.new_mta_session_with_shutdown(); session.data.remote_ip_str = "10.0.0.1".into(); let mut buf = vec![b'A'; 4097]; session.ingest(&buf).await.unwrap(); diff --git a/tests/src/smtp/inbound/milter.rs b/tests/src/smtp/inbound/milter.rs index f0f149aa..5fd2a2ec 100644 --- a/tests/src/smtp/inbound/milter.rs +++ b/tests/src/smtp/inbound/milter.rs @@ -4,14 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{ - TempDir, TestSMTP, - inbound::TestMessage, - session::{TestSession, VerifyResponse, load_test_message}, +use crate::{ + smtp::{ + inbound::TestMessage, + session::{TestSession, VerifyResponse, load_test_message}, + }, + utils::server::TestServerBuilder, }; use ahash::AHashSet; use common::{ - Core, config::smtp::session::{Milter, MilterVersion, Stage}, expr::if_block::IfBlock, manager::application::Resource, @@ -21,9 +22,17 @@ use hyper::{body, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; use mail_auth::AuthenticatedMessage; use mail_parser::MessageParser; +use registry::{ + schema::{ + enums::{self, MtaStage}, + prelude::{ObjectType, Property}, + structs::{Expression, MtaHook, MtaMilter, MtaStageAuth, MtaStageRcpt}, + }, + types::map::Map, +}; use serde::Deserialize; use smtp::{ - core::{Session, SessionData}, + core::SessionData, inbound::{ hooks::{self, Request, SmtpResponse}, milter::{ @@ -45,69 +54,60 @@ struct HeaderTest { result: String, } -const CONFIG_MILTER: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/queue.db" - -[session.rcpt] -relay = true - -[[session.milter]] -hostname = "127.0.0.1" -port = 9332 -#port = 11332 -#port = 7357 -enable = true -options.version = 6 -tls = false -stages = ["data"] - -"#; - -const CONFIG_JMILTER: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/queue.db" - -[session.rcpt] -relay = true - -[[session.hook]] -url = "http://127.0.0.1:9333" -enable = true -stages = ["data"] -"#; - #[tokio::test] async fn milter_session() { - - + let mut test = TestServerBuilder::new("smtp_milter_test") + .await + .with_http_listener(19014) + .await + .capture_queue() + .disable_services() + .build() + .await; + + // Add test settings + let admin = test.account("admin"); + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageRcpt { + allow_relaying: Expression { + else_: "true".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaMilter { + enable: Expression { + else_: "true".into(), + ..Default::default() + }, + hostname: "127.0.0.1".into(), + port: 9332, + use_tls: false, + stages: Map::new(vec![MtaStage::Data]), + protocol_version: enums::MilterVersion::V6, + ..Default::default() + }) + .await; + admin.reload_settings().await; + test.reload_core(); + test.expect_reload_settings().await; - // Configure tests - let tmp_dir = TempDir::new("smtp_milter_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG_MILTER)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; let _rx = spawn_mock_milter_server(); tokio::time::sleep(Duration::from_millis(100)).await; // Build session - let test = TestSMTP::from_core(core); - let mut qr = test.queue_receiver; - let mut session = Session::test(test.server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.doe.org").await; @@ -121,7 +121,7 @@ async fn milter_session() { "503 5.5.3", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test discard session @@ -132,7 +132,7 @@ async fn milter_session() { "250 2.0.0", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test temp fail session @@ -143,7 +143,7 @@ async fn milter_session() { "451 4.3.5", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test shutdown session @@ -154,7 +154,7 @@ async fn milter_session() { "421 4.3.0", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test reply code session @@ -165,7 +165,7 @@ async fn milter_session() { "321", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test accept with header addition session @@ -176,9 +176,9 @@ async fn milter_session() { "250 2.0.0", ) .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("X-Hello: World") .assert_contains("Subject: Is dinner ready?") @@ -193,9 +193,9 @@ async fn milter_session() { "250 2.0.0", ) .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("Subject: [SPAM] Saying Hello") .assert_count("References: ", 1) @@ -210,9 +210,9 @@ async fn milter_session() { "250 2.0.0", ) .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("X-Spam: Yes") .assert_contains("123456"); @@ -220,27 +220,55 @@ async fn milter_session() { #[tokio::test] async fn mta_hook_session() { - - /*let disable = "true"; - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(tracing::Level::TRACE) - .finish(), - ) - .unwrap();*/ + let mut test = TestServerBuilder::new("smtp_mta_hook_test") + .await + .with_http_listener(19015) + .await + .disable_services() + .capture_queue() + .build() + .await; + + // Add test settings + let admin = test.account("admin"); + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageRcpt { + allow_relaying: Expression { + else_: "true".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaHook { + enable: Expression { + else_: "true".into(), + ..Default::default() + }, + url: "http://127.0.0.1:9333".into(), + stages: Map::new(vec![MtaStage::Data]), + ..Default::default() + }) + .await; + admin.reload_settings().await; + test.reload_core(); + test.expect_reload_settings().await; - // Configure tests - let tmp_dir = TempDir::new("smtp_mta_hook_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG_JMILTER)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; let _rx = spawn_mock_mta_hook_server(); tokio::time::sleep(Duration::from_millis(100)).await; // Build session - let test = TestSMTP::from_core(core); - let mut qr = test.queue_receiver; - let mut session = Session::test(test.server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.doe.org").await; @@ -254,7 +282,7 @@ async fn mta_hook_session() { "503 5.5.3", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test discard session @@ -265,7 +293,7 @@ async fn mta_hook_session() { "250 2.0.0", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test temp fail session @@ -276,7 +304,7 @@ async fn mta_hook_session() { "451 4.3.5", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test shutdown session @@ -287,7 +315,7 @@ async fn mta_hook_session() { "421 4.3.0", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test reply code session @@ -298,7 +326,7 @@ async fn mta_hook_session() { "321", ) .await; - qr.assert_no_events(); + test.assert_no_events(); // Test accept with header addition session @@ -309,9 +337,9 @@ async fn mta_hook_session() { "250 2.0.0", ) .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("X-Hello: World") .assert_contains("Subject: Is dinner ready?") @@ -326,9 +354,9 @@ async fn mta_hook_session() { "250 2.0.0", ) .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("Subject: [SPAM] Saying Hello") .assert_count("References: ", 1) @@ -343,9 +371,9 @@ async fn mta_hook_session() { "250 2.0.0", ) .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("X-Spam: Yes") .assert_contains("123456"); @@ -547,8 +575,8 @@ async fn milter_client_test() { const PORT: u16 = 7357; let mut client = MilterClient::connect( &Milter { - enable: IfBlock::empty(""), - id: Arc::new("test".into()), + enable: IfBlock::empty(ObjectType::MtaMilter.singleton(), Property::Enable), + id: ObjectType::MtaMilter.singleton(), addrs: vec![SocketAddr::from(([127, 0, 0, 1], PORT))], hostname: "localhost".into(), port: PORT, diff --git a/tests/src/smtp/inbound/mod.rs b/tests/src/smtp/inbound/mod.rs index 75d0601b..84e1dd73 100644 --- a/tests/src/smtp/inbound/mod.rs +++ b/tests/src/smtp/inbound/mod.rs @@ -19,28 +19,29 @@ use store::{ use tokio::sync::mpsc::error::TryRecvError; use types::id::Id; +pub mod asn; pub mod auth; pub mod basic; pub mod data; -pub mod mail; -pub mod rcpt; - -/* -pub mod antispam; -pub mod asn; pub mod dmarc; pub mod ehlo; pub mod limits; +pub mod mail; pub mod milter; +pub mod rcpt; pub mod rewrite; pub mod scripts; pub mod sign; pub mod throttle; pub mod vrfy; + +/* +pub mod antispam; */ impl TestServer { pub async fn read_event(&mut self) -> QueueEvent { + let todo = "fix antispam tests"; match tokio::time::timeout(Duration::from_millis(100), self.queue_rx.recv()).await { Ok(Some(event)) => event, Ok(None) => panic!("Channel closed."), @@ -73,6 +74,10 @@ impl TestServer { assert_eq!(self.read_report_events::().await, vec![]); } + pub async fn expect_reload_settings(&mut self) { + self.read_event().await.assert_reload_settings(); + } + pub async fn expect_message(&mut self) -> MessageWrapper { self.read_event().await.assert_refresh(); self.last_queued_message().await @@ -230,6 +235,7 @@ impl TestServer { } pub trait TestQueueEvent { + fn assert_reload_settings(self); fn assert_refresh(self); fn assert_done(self); fn assert_refresh_or_done(self); @@ -238,8 +244,7 @@ pub trait TestQueueEvent { impl TestQueueEvent for QueueEvent { fn assert_refresh(self) { match self { - QueueEvent::ReloadSettings - | QueueEvent::Refresh + QueueEvent::Refresh | QueueEvent::WorkerDone { status: QueueEventStatus::Deferred, .. @@ -248,6 +253,13 @@ impl TestQueueEvent for QueueEvent { } } + fn assert_reload_settings(self) { + match self { + QueueEvent::ReloadSettings => (), + e => panic!("Unexpected event: {e:?}"), + } + } + fn assert_done(self) { match self { QueueEvent::WorkerDone { diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs index 4abcf882..d8d19711 100644 --- a/tests/src/smtp/inbound/rewrite.rs +++ b/tests/src/smtp/inbound/rewrite.rs @@ -4,74 +4,137 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{TestSMTP, session::TestSession}; -use common::Core; -use smtp::core::Session; - -const CONFIG: &str = r#" -[session.mail] -rewrite = [ { if = "ends_with(sender_domain, '.foobar.net') & matches('^([^.]+)@([^.]+)\.(.+)$', sender)", then = "$1 + '+' + $2 + '@' + $3"}, - { else = false } ] -script = [ { if = "sender_domain = 'foobar.org'", then = "'mail'" }, - { else = false } ] - -[session.rcpt] -rewrite = [ { if = "rcpt_domain = 'foobar.net' & matches('^([^.]+)\\.([^.]+)@(.+)$', rcpt)", then = "$1 + '+' + $2 + '@' + $3"}, - { else = false } ] -script = [ { if = "rcpt_domain = 'foobar.org'", then = "'rcpt'" }, - { else = false } ] -relay = true - -[sieve.trusted] -from-name = "Sieve Daemon" -from-addr = "sieve@foobar.org" -return-path = "" -hostname = "mx.foobar.org" - -[sieve.trusted.limits] -redirects = 3 -out-messages = 5 -received-headers = 50 -cpu = 10000 -nested-includes = 5 -duplicate-expiry = "7d" - -[sieve.trusted.scripts."mail"] -contents = ''' -require ["variables", "envelope"]; +use crate::{smtp::session::TestSession, utils::server::TestServerBuilder}; +use registry::{ + schema::structs::{ + Expression, ExpressionMatch, MtaStageAuth, MtaStageMail, MtaStageRcpt, + SieveSystemInterpreter, SieveSystemScript, + }, + types::list::List, +}; +const MAIL_SCRIPT: &str = r#"require ["variables", "envelope"]; if allof( envelope :domain :is "from" "foobar.org", envelope :localpart :contains "from" "admin" ) { set "envelope.from" "MAILER-DAEMON@foobar.org"; } - -''' - -[sieve.trusted.scripts."rcpt"] -contents = ''' -require ["variables", "envelope", "regex"]; - +"#; +const MAIL_RCPT: &str = r#"require ["variables", "envelope", "regex"]; if allof( envelope :localpart :contains "to" ".", envelope :regex "to" "(.+)@(.+)$") { set :replace "." "" "to" "${1}"; set "envelope.to" "${to}@${2}"; } - -''' - "#; #[tokio::test] async fn address_rewrite() { - - + let mut test = TestServerBuilder::new("smtp_rewrite_test") + .await + .with_http_listener(19007) + .await + .disable_services() + .build() + .await; - // Prepare config - let mut config = Config::new(CONFIG).unwrap(); - let core = Core::parse(&mut config, Default::default(), Default::default()).await; + // Add test settings + let admin = test.account("admin"); + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageMail { + rewrite: Expression { + match_: List::from_iter([ + ExpressionMatch { + if_: "ends_with(sender_domain, '.foobar.net') & matches('^([^.]+)@([^.]+)\\.(.+)$', sender)".into(), + then: "$1 + '+' + $2 + '@' + $3".into(), + }, + ]), + else_: "false".into(), + }, + script: Expression { + match_: List::from_iter([ + ExpressionMatch { + if_: "sender_domain = 'foobar.org'".into(), + then: "'mail'".into(), + }, + ]), + else_: "false".into(), + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageRcpt { + rewrite: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "rcpt_domain = 'foobar.net' & matches('^([^.]+)\\.([^.]+)@(.+)$', rcpt)" + .into(), + then: "$1 + '+' + $2 + '@' + $3".into(), + }]), + else_: "false".into(), + }, + script: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "rcpt_domain = 'foobar.org'".into(), + then: "'rcpt'".into(), + }]), + else_: "false".into(), + }, + allow_relaying: Expression { + else_: "true".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SieveSystemInterpreter { + default_from_address: Expression { + else_: "'sieve@foobar.org'".into(), + ..Default::default() + }, + + default_from_name: Expression { + else_: "'Sieve Daemon'".into(), + ..Default::default() + }, + default_return_path: Expression { + else_: "''".into(), + ..Default::default() + }, + message_id_hostname: Some("'mx.foobar.org'".into()), + duplicate_expiry: (86_400u64 * 100 * 7).into(), + max_cpu_cycles: 10000, + max_nested_includes: 5, + max_out_messages: 5, + max_received_headers: 50, + max_redirects: 3, + ..Default::default() + }) + .await; + for (name, contents) in [("mail", MAIL_SCRIPT), ("rcpt", MAIL_RCPT)] { + admin + .registry_create_object(SieveSystemScript { + name: name.to_string(), + contents: contents.to_string(), + is_active: true, + ..Default::default() + }) + .await; + } + admin.reload_settings().await; + test.reload_core(); // Init session - let mut session = Session::test(TestSMTP::from_core(core).server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.doe.org").await; diff --git a/tests/src/smtp/inbound/scripts.rs b/tests/src/smtp/inbound/scripts.rs index f9ec9148..0c0f690e 100644 --- a/tests/src/smtp/inbound/scripts.rs +++ b/tests/src/smtp/inbound/scripts.rs @@ -5,101 +5,173 @@ */ use crate::{ - AssertConfig, enable_logging, smtp::{ - TempDir, TestSMTP, - inbound::{TestMessage, TestQueueEvent, sign::SIGNATURES}, + inbound::{TestMessage, TestQueueEvent}, session::{TestSession, VerifyResponse}, }, + utils::server::TestServerBuilder, }; -use common::Core; use core::panic; -use smtp::{ - core::Session, - scripts::{ScriptResult, event_loop::RunScript}, +use registry::schema::structs::{ + Domain, Expression, LookupStore, MtaStageAuth, MtaStageConnect, MtaStageData, MtaStageEhlo, + MtaStageMail, MtaStageRcpt, SieveSystemInterpreter, SieveSystemScript, SqliteStore, + StoreLookup, }; -use std::{fmt::Write, fs, path::PathBuf}; - -const CONFIG: &str = r#" -[storage] -data = "sql" -lookup = "sql" -blob = "sql" -fts = "sql" -directory = "local" - -[store."sql"] -type = "sqlite" -path = "{TMP}/smtp_sieve.db" - -[store."sql".pool] -max-connections = 10 -min-connections = 0 -idle-timeout = "5m" - -[spam-filter] -enable = false - -[sieve.trusted] -from-name = "'Sieve Daemon'" -from-addr = "'sieve@foobar.org'" -return-path = "''" -hostname = "mx.foobar.org" -sign = "['rsa']" - -[sieve.trusted.limits] -redirects = 3 -out-messages = 5 -received-headers = 50 -cpu = 10000 -nested-includes = 5 -duplicate-expiry = "7d" - -[session.connect] -script = "'stage_connect'" -greeting = "'mx.example.org at your service'" - -[session.ehlo] -script = "'stage_ehlo'" - -[session.mail] -script = "'stage_mail'" - -[session.rcpt] -script = "'stage_rcpt'" -relay = true - -[session.data] -script = "'stage_data'" - -[session.data.add-headers] -received = true -received-spf = true -auth-results = true -message-id = true -date = true -return-path = false - -[directory."local"] -type = "memory" - -[[directory."local".principals]] -name = "john" -description = "John Doe" -secret = "secret" -email = ["john@localdomain.org", "jdoe@localdomain.org", "john.doe@localdomain.org"] -email-list = ["info@localdomain.org"] -member-of = ["sales"] - -"#; +use smtp::scripts::{ScriptResult, event_loop::RunScript}; +use std::{fs, path::PathBuf}; #[tokio::test] async fn sieve_scripts() { - - enable_logging(); + let mut test = TestServerBuilder::new("smtp_sieve_test") + .await + .with_http_listener(19008) + .await + .disable_services() + .capture_queue() + .build() + .await; + + // Create test data + let admin = test.account("admin"); + let domain_id = admin + .registry_create_object(Domain { + name: "foobar.org".into(), + allow_relaying: true, + ..Default::default() + }) + .await; + admin.create_dkim_signatures(domain_id).await; + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SieveSystemInterpreter { + default_from_address: Expression { + else_: "'sieve@foobar.org'".into(), + ..Default::default() + }, + + default_from_name: Expression { + else_: "'Sieve Daemon'".into(), + ..Default::default() + }, + default_return_path: Expression { + else_: "''".into(), + ..Default::default() + }, + message_id_hostname: Some("'mx.foobar.org'".into()), + dkim_sign_domain: Expression { + else_: "'foobar.org'".into(), + ..Default::default() + }, + duplicate_expiry: (86_400u64 * 100 * 7).into(), + max_cpu_cycles: 10000, + max_nested_includes: 5, + max_out_messages: 5, + max_received_headers: 50, + max_redirects: 3, + ..Default::default() + }) + .await; + admin + .registry_create_object(StoreLookup { + namespace: "sql".into(), + store: LookupStore::Sqlite(SqliteStore { + path: format!("{}/smtp_sieve.db", test.tmp_dir()), + pool_max_connections: 10, + pool_workers: None, + }), + }) + .await; + admin + .registry_create_object(MtaStageConnect { + script: Expression { + else_: "'stage_connect'".into(), + ..Default::default() + }, + smtp_greeting: Expression { + else_: "'mx.example.org at your service'".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageEhlo { + script: Expression { + else_: "'stage_ehlo'".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageMail { + script: Expression { + else_: "'stage_mail'".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageRcpt { + script: Expression { + else_: "'stage_rcpt'".into(), + ..Default::default() + }, + allow_relaying: Expression { + else_: "true".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageData { + script: Expression { + else_: "'stage_data'".into(), + ..Default::default() + }, + add_date_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_message_id_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_received_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_received_spf_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_auth_results_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_return_path_header: Expression { + else_: "false".into(), + ..Default::default() + }, + enable_spam_filter: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; // Add test scripts - let mut config = CONFIG.to_string() + SIGNATURES; for entry in fs::read_dir( PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("resources") @@ -109,33 +181,29 @@ async fn sieve_scripts() { .unwrap() { let entry = entry.unwrap(); - writeln!( - &mut config, - "[sieve.trusted.scripts.{}]\ncontents = \"%{{file:{}}}%\"", - entry - .file_name() - .to_str() - .unwrap() - .split_once('.') - .unwrap() - .0, - entry.path().to_str().unwrap() - ) - .unwrap(); + admin + .registry_create_object(SieveSystemScript { + contents: fs::read_to_string(entry.path()).unwrap(), + description: None, + is_active: true, + name: entry + .file_name() + .to_str() + .unwrap() + .split_once('.') + .unwrap() + .0 + .to_string(), + }) + .await; } - - // Prepare config - let tmp_dir = TempDir::new("smtp_sieve_test", true); - let mut config = Config::new(tmp_dir.update_config(config)).unwrap(); - config.resolve_all_macros().await; - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; - config.assert_no_errors(); + admin.reload_settings().await; + admin.reload_lookup_stores().await; + test.reload_core(); + test.expect_reload_settings().await; // Build session - let test = TestSMTP::from_core(core); - let mut qr = test.queue_receiver; - let mut session = Session::test(test.server.clone()); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.88".parse().unwrap(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); assert!(!session.init_conn().await); @@ -204,13 +272,13 @@ async fn sieve_scripts() { // Expect a modified message session.data("test:multipart", "250").await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("X-Part-Number: 5") .assert_contains("THIS IS A PIECE OF HTML TEXT"); - qr.assert_no_events(); + test.assert_no_events(); // Expect rejection for bill@foobar.net session @@ -221,8 +289,8 @@ async fn sieve_scripts() { "503 5.5.3 Bill cannot receive messages", ) .await; - qr.assert_no_events(); - qr.clear_queue(&test.server).await; + test.assert_no_events(); + test.clear_queue().await; // Expect message delivery plus a notification session @@ -233,9 +301,9 @@ async fn sieve_scripts() { "250", ) .await; - qr.read_event().await.assert_refresh(); - qr.read_event().await.assert_refresh(); - let messages = qr.read_queued_messages().await; + test.read_event().await.assert_refresh(); + test.read_event().await.assert_refresh(); + let messages = test.read_queued_messages().await; assert_eq!(messages.len(), 2); let mut messages = messages.into_iter(); let notification = messages.next().unwrap(); @@ -250,9 +318,9 @@ async fn sieve_scripts() { "jane@example.org" ); notification - .read_lines(&qr) + .read_lines(&test) .await - .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com;") + .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=foobar.org;") .assert_contains("From: \"Sieve Daemon\" ") .assert_contains("To: ") .assert_contains("Cc: ") @@ -262,14 +330,14 @@ async fn sieve_scripts() { messages .next() .unwrap() - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("One Two Three Four") .assert_contains("multi-part message in MIME format") .assert_not_contains("X-Part-Number: 5") .assert_not_contains("THIS IS A PIECE OF HTML TEXT"); - qr.assert_no_events(); - qr.clear_queue(&test.server).await; + test.assert_no_events(); + test.clear_queue().await; // Expect a modified message delivery plus a notification session @@ -280,18 +348,18 @@ async fn sieve_scripts() { "250", ) .await; - qr.read_event().await.assert_refresh(); - qr.read_event().await.assert_refresh(); - let messages = qr.read_queued_messages().await; + test.read_event().await.assert_refresh(); + test.read_event().await.assert_refresh(); + let messages = test.read_queued_messages().await; assert_eq!(messages.len(), 2); let mut messages = messages.into_iter(); messages .next() .unwrap() - .read_lines(&qr) + .read_lines(&test) .await - .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com;") + .assert_contains("DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=foobar.org;") .assert_contains("From: \"Sieve Daemon\" ") .assert_contains("To: ") .assert_contains("Cc: ") @@ -301,12 +369,12 @@ async fn sieve_scripts() { messages .next() .unwrap() - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("X-Part-Number: 5") .assert_contains("THIS IS A PIECE OF HTML TEXT") .assert_not_contains("X-My-Header: true"); - qr.clear_queue(&test.server).await; + test.clear_queue().await; // Expect a modified redirected message session @@ -318,7 +386,7 @@ async fn sieve_scripts() { ) .await; - let redirect = qr.expect_message().await; + let redirect = test.expect_message().await; assert_eq!(redirect.message.return_path.as_ref(), ""); assert_eq!(redirect.message.recipients.len(), 1); assert_eq!( @@ -326,7 +394,7 @@ async fn sieve_scripts() { "redirect@here.email" ); redirect - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("From: no-reply@my.domain") .assert_contains("To: Suzie Q ") @@ -334,7 +402,7 @@ async fn sieve_scripts() { .assert_contains("Message-ID: <20030712040037.46341.5F8J@football.example.com>") .assert_contains("Received: ") .assert_not_contains("From: Joe SixPack "); - qr.assert_no_events(); + test.assert_no_events(); // Expect an intact redirected message session @@ -346,7 +414,7 @@ async fn sieve_scripts() { ) .await; - let redirect = qr.expect_message().await; + let redirect = test.expect_message().await; assert_eq!(redirect.message.return_path.as_ref(), ""); assert_eq!(redirect.message.recipients.len(), 1); assert_eq!( @@ -354,7 +422,7 @@ async fn sieve_scripts() { "redirect@somewhere.email" ); redirect - .read_lines(&qr) + .read_lines(&test) .await .assert_not_contains("From: no-reply@my.domain") .assert_contains("To: Suzie Q ") @@ -363,5 +431,5 @@ async fn sieve_scripts() { .assert_contains("From: Joe SixPack ") .assert_contains("Received: ") .assert_contains("Authentication-Results: "); - qr.assert_no_events(); + test.assert_no_events(); } diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs index f2ca56e4..405373d3 100644 --- a/tests/src/smtp/inbound/sign.rs +++ b/tests/src/smtp/inbound/sign.rs @@ -4,128 +4,126 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{ - DnsCache, TempDir, TestSMTP, - inbound::TestMessage, - session::{TestSession, VerifyResponse}, +use crate::{ + smtp::{ + inbound::TestMessage, + session::{TestSession, VerifyResponse}, + }, + utils::{account::Account, dns::DnsCache, server::TestServerBuilder}, }; -use common::Core; use mail_auth::{ common::{parse::TxtRecordParser, verify::DomainKey}, spf::Spf, }; -use smtp::core::Session; +use registry::schema::{ + enums::DkimCanonicalization, + structs::{ + Dkim1Signature, DkimPrivateKey, DkimSignature, Domain, Expression, MtaStageAuth, + MtaStageData, SecretTextValue, SenderAuth, + }, +}; use std::time::{Duration, Instant}; - -pub const SIGNATURES: &str = " -[signature.rsa] -private-key = ''' ------BEGIN RSA PRIVATE KEY----- -MIIEowIBAAKCAQEAv9XYXG3uK95115mB4nJ37nGeNe2CrARm1agrbcnSk5oIaEfM -ZLUR/X8gPzoiNHZcfMZEVR6bAytxUhc5EvZIZrjSuEEeny+fFd/cTvcm3cOUUbIa -UmSACj0dL2/KwW0LyUaza9z9zor7I5XdIl1M53qVd5GI62XBB76FH+Q0bWPZNkT4 -NclzTLspD/MTpNCCPhySM4Kdg5CuDczTH4aNzyS0TqgXdtw6A4Sdsp97VXT9fkPW -9rso3lrkpsl/9EQ1mR/DWK6PBmRfIuSFuqnLKY6v/z2hXHxF7IoojfZLa2kZr9Ae -d4l9WheQOTA19k5r2BmlRw/W9CrgCBo0Sdj+KQIDAQABAoIBAFPChEi/OvnulReB -ECQWhOUYuNKlFKQU++2YEvZJ4+bMn5UgnE7wfJ1pj2Pr9xlfALz+OMHNrjMxGbaV -KzdrT2uCkYcf78XjnhuH9gKIiXDUv4L4N+P3u6w8yOx4bFgOS9IjS53yDOPM7SC5 -g6dIg5aigHaHlffqIuFFv4yQMI/+Ai+zBKxS7wRhxK/7nnAuo28fe5MEdp57ho9/ -AGlDNsdg9zCgjwhokwFE3+AaD+bkUFm4gQ1XjkUFrlmnQn8vDQ0i9toEWhCj+UPY -iOKL63MJnr90MXTXWLHoFj99wBp//mYygbF9Lj8fa28/oa8LWp3Jhb7QeMgH46iv -3aLHbTECgYEA5M2dAw+nyMw9vYlkMejhwObKYP8Mr/6zcGMLCalYvRJM5iUAM0JI -H6sM6pV9/nv167cbKocj3xYPdtE7FPOn4132MLM8Ne1f8nPE64Qrcbj5WBXvLnU8 -hpWbwe2Z8h7UUMKx6q4F1/TXYkc3ScxYwfjM4mP/pLsAOgVzRSEEgrUCgYEA1qNQ -xaQHNWZ1O8WuTnqWd5JSsic6iURAmUcLeFDZY2PWhVoaQ8L/xMQhDYs1FIbLWArW -4Qq3Ibu8AbSejAKuaJz7Uf26PX+PYVUwAOO0qamCJ8d/qd6So7qWMDyAY2yXI39Y -1nMqRjr7bkEsggAZao7BKqA7ZtmogjOusBT38iUCgYEA06agJ8TDoKvOMRZ26PRU -YO0dKLzGL8eclcoI29cbj0rud7aiiMg3j5PbTuUat95TjsjDCIQaWrM9etvxm2AJ -Xfn9Uu96MyhyKQWOk46f4YMKpMElkARDCPw8KRhx39dE77AqhLyWCz8iPndCXbH6 -KPTOEl4OjYOuof2Is9nnIkECgYBh948RdsnXhNlzm8nwhiGRmBbou+EK8D0v+O5y -Tyy6IcKzgSnFzgZh8EdJ4EUtBk1f9SqY8wQdgIvSl3daXorusuA/TzkngsaV3YUY -ktZOLlF7CKLrjOyPkMWmZKcROmpNyH1q/IvKHHfQnizLdXIkYd4nL5WNX0F7lE1i -j1+QhQKBgB2lviBK7rJFwlFYdQUP1NAN2dKxMZk8uJS8JglHrM0+8nRI83HbTdEQ -vB0ManEKBkbS4T5n+gRtdEqKSDmWDTXDlrBfcdCHNQLwYtBpOotCqQn/AmfjcPBl -byAbwh4+HiZ5JISoRZpiZqy67aJNVoXmdtb/E9mi7ozzytpxMNql ------END RSA PRIVATE KEY-----''' -domain = 'example.com' -selector = 'rsa' -headers = ['From', 'To', 'Date', 'Subject', 'Message-ID'] -algorithm = 'rsa-sha256' -canonicalization = 'simple/relaxed' -expire = '10d' -set-body-length = true -report = true - -[signature.ed] -private-key = '-----BEGIN PRIVATE KEY----- -MC4CAQAwBQYDK2VwBCIEIAO3hAf144lTAVjTkht3ZwBTK0CMCCd1bI0alggneN3B ------END PRIVATE KEY-----' -domain = 'example.com' -selector = 'ed' -headers = ['From', 'To', 'Date', 'Subject', 'Message-ID'] -algorithm = 'ed25519-sha256' -canonicalization = 'relaxed/simple' -set-body-length = false -"; - -const CONFIG: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/queue.db" - -[directory."local"] -type = "memory" - -[[directory."local".principals]] -name = "john" -description = "John Doe" -secret = "secret" -email = ["jdoe@example.com"] - -[session.rcpt] -directory = "'local'" - -[session.data.add-headers] -received = true -received-spf = true -auth-results = true -message-id = true -date = true -return-path = false - -[auth.spf.verify] -ehlo = "relaxed" -mail-from = "relaxed" - -[auth.dkim] -verify = "relaxed" -sign = "['rsa']" - -[auth.arc] -verify = "relaxed" -seal = "'ed'" - -[auth.dmarc] -verify = "relaxed" - -"#; +use types::id::Id; #[tokio::test] async fn sign_and_seal() { - - + let mut test = TestServerBuilder::new("smtp_sign_test") + .await + .with_http_listener(19010) + .await + .disable_services() + .capture_queue() + .build() + .await; - let tmp_dir = TempDir::new("smtp_sign_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG.to_string() + SIGNATURES)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; - let test = TestSMTP::from_core(core); + // Add test settings + let admin = test.account("admin"); + let domain_id = admin + .registry_create_object(Domain { + name: "example.com".into(), + allow_relaying: true, + ..Default::default() + }) + .await; + admin.create_dkim_signatures(domain_id).await; + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaStageData { + add_date_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_message_id_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_received_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_received_spf_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_auth_results_header: Expression { + else_: "true".into(), + ..Default::default() + }, + add_return_path_header: Expression { + else_: "false".into(), + ..Default::default() + }, + enable_spam_filter: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(SenderAuth { + dmarc_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + reverse_ip_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + spf_ehlo_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + spf_from_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + arc_verify: Expression { + else_: "strict".into(), + ..Default::default() + }, + dkim_sign_domain: Expression { + else_: "'example.com'".into(), + ..Default::default() + }, + dkim_verify: Expression { + else_: "relaxed".into(), + ..Default::default() + }, + dkim_strict: false, + }) + .await; + admin.reload_settings().await; + test.reload_core(); + test.expect_reload_settings().await; // Add SPF, DKIM and DMARC records test.server.txt_add( @@ -167,8 +165,7 @@ async fn sign_and_seal() { ); // Test DKIM signing - let mut qr = test.queue_receiver; - let mut session = Session::test(test.server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session.ehlo("mx.example.com").await; @@ -180,21 +177,24 @@ async fn sign_and_seal() { "250", ) .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains( "DKIM-Signature: v=1; a=rsa-sha256; s=rsa; d=example.com; c=simple/relaxed;", ); - // Test ARC verify and seal + // Test ARC verify session .send_message("bill@foobar.org", &["jdoe@example.com"], "test:arc", "250") .await; - qr.expect_message() - .await - .read_lines(&qr) + test.expect_message().await; + + /* + // DMARC WG is ending the ARC experiment + + .read_lines(&test) .await .assert_contains("ARC-Seal: i=3; a=ed25519-sha256; s=ed; d=example.com; cv=pass;") .assert_contains( @@ -205,12 +205,78 @@ async fn sign_and_seal() { session .send_message("bill@foobar.org", &["jdoe@example.com"], "test:dkim", "250") .await; - qr.expect_message() + test.expect_message() .await - .read_lines(&qr) + .read_lines(&test) .await .assert_contains("ARC-Seal: i=1; a=ed25519-sha256; s=ed; d=example.com; cv=none;") .assert_contains( "ARC-Message-Signature: i=1; a=ed25519-sha256; s=ed; d=example.com; c=relaxed/simple;", - ); + );*/ } + +impl Account { + pub async fn create_dkim_signatures(&self, domain_id: Id) -> Vec { + let rsa_id = self + .registry_create_object(DkimSignature::Dkim1RsaSha256(Dkim1Signature { + enabled: true, + selector: "rsa".to_string(), + canonicalization: DkimCanonicalization::SimpleRelaxed, + domain_id, + private_key: DkimPrivateKey::Value(SecretTextValue { + secret: RSA_KEY.to_string(), + }), + ..Default::default() + })) + .await; + + let ed_id = self + .registry_create_object(DkimSignature::Dkim1Ed25519Sha256(Dkim1Signature { + enabled: true, + selector: "ed".to_string(), + canonicalization: DkimCanonicalization::RelaxedSimple, + domain_id, + private_key: DkimPrivateKey::Value(SecretTextValue { + secret: ED25519_KEY.to_string(), + }), + ..Default::default() + })) + .await; + + vec![rsa_id, ed_id] + } +} + +const RSA_KEY: &str = r#"-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAv9XYXG3uK95115mB4nJ37nGeNe2CrARm1agrbcnSk5oIaEfM +ZLUR/X8gPzoiNHZcfMZEVR6bAytxUhc5EvZIZrjSuEEeny+fFd/cTvcm3cOUUbIa +UmSACj0dL2/KwW0LyUaza9z9zor7I5XdIl1M53qVd5GI62XBB76FH+Q0bWPZNkT4 +NclzTLspD/MTpNCCPhySM4Kdg5CuDczTH4aNzyS0TqgXdtw6A4Sdsp97VXT9fkPW +9rso3lrkpsl/9EQ1mR/DWK6PBmRfIuSFuqnLKY6v/z2hXHxF7IoojfZLa2kZr9Ae +d4l9WheQOTA19k5r2BmlRw/W9CrgCBo0Sdj+KQIDAQABAoIBAFPChEi/OvnulReB +ECQWhOUYuNKlFKQU++2YEvZJ4+bMn5UgnE7wfJ1pj2Pr9xlfALz+OMHNrjMxGbaV +KzdrT2uCkYcf78XjnhuH9gKIiXDUv4L4N+P3u6w8yOx4bFgOS9IjS53yDOPM7SC5 +g6dIg5aigHaHlffqIuFFv4yQMI/+Ai+zBKxS7wRhxK/7nnAuo28fe5MEdp57ho9/ +AGlDNsdg9zCgjwhokwFE3+AaD+bkUFm4gQ1XjkUFrlmnQn8vDQ0i9toEWhCj+UPY +iOKL63MJnr90MXTXWLHoFj99wBp//mYygbF9Lj8fa28/oa8LWp3Jhb7QeMgH46iv +3aLHbTECgYEA5M2dAw+nyMw9vYlkMejhwObKYP8Mr/6zcGMLCalYvRJM5iUAM0JI +H6sM6pV9/nv167cbKocj3xYPdtE7FPOn4132MLM8Ne1f8nPE64Qrcbj5WBXvLnU8 +hpWbwe2Z8h7UUMKx6q4F1/TXYkc3ScxYwfjM4mP/pLsAOgVzRSEEgrUCgYEA1qNQ +xaQHNWZ1O8WuTnqWd5JSsic6iURAmUcLeFDZY2PWhVoaQ8L/xMQhDYs1FIbLWArW +4Qq3Ibu8AbSejAKuaJz7Uf26PX+PYVUwAOO0qamCJ8d/qd6So7qWMDyAY2yXI39Y +1nMqRjr7bkEsggAZao7BKqA7ZtmogjOusBT38iUCgYEA06agJ8TDoKvOMRZ26PRU +YO0dKLzGL8eclcoI29cbj0rud7aiiMg3j5PbTuUat95TjsjDCIQaWrM9etvxm2AJ +Xfn9Uu96MyhyKQWOk46f4YMKpMElkARDCPw8KRhx39dE77AqhLyWCz8iPndCXbH6 +KPTOEl4OjYOuof2Is9nnIkECgYBh948RdsnXhNlzm8nwhiGRmBbou+EK8D0v+O5y +Tyy6IcKzgSnFzgZh8EdJ4EUtBk1f9SqY8wQdgIvSl3daXorusuA/TzkngsaV3YUY +ktZOLlF7CKLrjOyPkMWmZKcROmpNyH1q/IvKHHfQnizLdXIkYd4nL5WNX0F7lE1i +j1+QhQKBgB2lviBK7rJFwlFYdQUP1NAN2dKxMZk8uJS8JglHrM0+8nRI83HbTdEQ +vB0ManEKBkbS4T5n+gRtdEqKSDmWDTXDlrBfcdCHNQLwYtBpOotCqQn/AmfjcPBl +byAbwh4+HiZ5JISoRZpiZqy67aJNVoXmdtb/E9mi7ozzytpxMNql +-----END RSA PRIVATE KEY----- +"#; + +const ED25519_KEY: &str = r#"-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIAO3hAf144lTAVjTkht3ZwBTK0CMCCd1bI0alggneN3B +-----END PRIVATE KEY----- +"#; diff --git a/tests/src/smtp/inbound/throttle.rs b/tests/src/smtp/inbound/throttle.rs index 60738f90..55b0a583 100644 --- a/tests/src/smtp/inbound/throttle.rs +++ b/tests/src/smtp/inbound/throttle.rs @@ -4,52 +4,87 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{TempDir, TestSMTP, session::TestSession}; -use common::Core; -use smtp::core::{Session, SessionAddress}; +use crate::utils::server::TestServerBuilder; +use registry::{ + schema::{ + enums::MtaInboundThrottleKey, + structs::{Expression, MtaInboundThrottle, MtaStageAuth, Rate}, + }, + types::map::Map, +}; +use smtp::core::SessionAddress; use std::time::Duration; -const CONFIG: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/data.db" - -[[queue.limiter.inbound]] -match = "remote_ip = '10.0.0.1'" -key = 'remote_ip' -rate = '2/1s' -enable = true - -[[queue.limiter.inbound]] -key = 'sender' -rate = '2/1s' -enable = true - -[[queue.limiter.inbound]] -key = ['remote_ip', 'rcpt'] -rate = '2/1s' -enable = true - -"#; - #[tokio::test] async fn throttle_inbound() { - - + let mut test = TestServerBuilder::new("smtp_inbound_throttle_test") + .await + .with_http_listener(19009) + .await + .disable_services() + .build() + .await; - let tmp_dir = TempDir::new("smtp_inbound_throttle", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; + // Add test settings + let admin = test.account("admin"); + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + + admin + .registry_create_object(MtaInboundThrottle { + description: None, + enable: true, + key: Map::new(vec![MtaInboundThrottleKey::RemoteIp]), + match_: Expression { + else_: "remote_ip = '10.0.0.1'".into(), + ..Default::default() + }, + rate: Rate { + count: 2, + period: 1000u64.into(), + }, + }) + .await; + + admin + .registry_create_object(MtaInboundThrottle { + enable: true, + key: Map::new(vec![MtaInboundThrottleKey::Sender]), + rate: Rate { + count: 2, + period: 1000u64.into(), + }, + ..Default::default() + }) + .await; + + admin + .registry_create_object(MtaInboundThrottle { + enable: true, + key: Map::new(vec![ + MtaInboundThrottleKey::RemoteIp, + MtaInboundThrottleKey::Rcpt, + ]), + rate: Rate { + count: 2, + period: 1000u64.into(), + }, + ..Default::default() + }) + .await; + + admin.reload_settings().await; + test.reload_core(); // Test connection rate limit - let mut session = Session::test(TestSMTP::from_core(core).server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.1".into(); assert!(session.is_allowed().await, "Rate limiter too strict."); assert!(session.is_allowed().await, "Rate limiter too strict."); diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs index bd4b6945..05b862d8 100644 --- a/tests/src/smtp/inbound/vrfy.rs +++ b/tests/src/smtp/inbound/vrfy.rs @@ -5,75 +5,88 @@ */ use crate::{ - AssertConfig, - smtp::{ - TempDir, TestSMTP, - session::{TestSession, VerifyResponse}, - }, + smtp::session::{TestSession, VerifyResponse}, + utils::server::TestServerBuilder, +}; +use registry::{ + schema::structs::{Expression, ExpressionMatch, MailingList, MtaExtensions, MtaStageAuth}, + types::{list::List, map::Map}, }; -use common::Core; -use smtp::core::Session; - -const CONFIG: &str = r#" -[storage] -data = "rocksdb" -lookup = "rocksdb" -blob = "rocksdb" -fts = "rocksdb" -directory = "local" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/data.db" - -[directory."local"] -type = "memory" - -[[directory."local".principals]] -name = "john" -description = "John Doe" -secret = "secret" -email = ["john@foobar.org"] -email-list = ["sales@foobar.org"] - -[[directory."local".principals]] -name = "jane" -description = "Jane Doe" -secret = "p4ssw0rd" -email = "jane@foobar.org" -email-list = ["sales@foobar.org"] - -[[directory."local".principals]] -name = "bill" -description = "Bill Foobar" -secret = "p4ssw0rd" -email = "bill@foobar.org" -email-list = ["sales@foobar.org"] - -[session.rcpt] -directory = "'local'" - -[session.extensions] -vrfy = [{if = "remote_ip = '10.0.0.1'", then = true}, - {else = false}] -expn = [{if = "remote_ip = '10.0.0.1'", then = true}, - {else = false}] - -"#; #[tokio::test] async fn vrfy_expn() { - - + let mut test = TestServerBuilder::new("smtp_vrfy_test") + .await + .with_http_listener(19006) + .await + .disable_services() + .build() + .await; - let tmp_dir = TempDir::new("smtp_vrfy_test", true); - let mut config = Config::new(tmp_dir.update_config(CONFIG)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - let core = Core::parse(&mut config, stores, Default::default()).await; - config.assert_no_errors(); + // Create test users + let admin = test.account("admin"); + for (name, secret, description, aliases) in [ + ("john@foobar.org", "12345 + extra safety", "John Doe", &[]), + ("jane@foobar.org", "abcde + extra safety", "Jane Smith", &[]), + ( + "bill@foobar.org", + "p4ssw0rd + extra safety", + "Bill Foobar", + &[], + ), + ] { + admin + .create_user_account(name, secret, description, aliases, vec![]) + .await; + } + let domain_id = admin.find_or_create_domain("foobar.org").await; + admin + .registry_create_object(MailingList { + domain_id, + name: "sales".into(), + recipients: Map::new(vec![ + "john@foobar.org".into(), + "jane@foobar.org".into(), + "bill@foobar.org".into(), + ]), + ..Default::default() + }) + .await; + + // Add test settings + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaExtensions { + vrfy: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + expn: Expression { + match_: List::from_iter([ExpressionMatch { + if_: "remote_ip = '10.0.0.1'".into(), + then: "true".into(), + }]), + else_: "false".into(), + }, + ..Default::default() + }) + .await; + admin.reload_settings().await; + test.reload_core(); // EHLO should not advertise VRFY/EXPN to 10.0.0.2 - let mut session = Session::test(TestSMTP::from_core(core).server); + let mut session = test.new_mta_session(); session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session @@ -81,7 +94,7 @@ async fn vrfy_expn() { .await .assert_not_contains("EXPN") .assert_not_contains("VRFY"); - session.cmd("VRFY john", "252 2.5.1").await; + session.cmd("VRFY john@foobar.org", "252 2.5.1").await; session.cmd("EXPN sales@foobar.org", "252 2.5.1").await; // EHLO should advertise VRFY/EXPN for 10.0.0.1 @@ -94,7 +107,9 @@ async fn vrfy_expn() { .assert_contains("VRFY"); // Successful VRFY - session.cmd("VRFY john", "250 john@foobar.org").await; + session + .cmd("VRFY john@foobar.org", "250 john@foobar.org") + .await; // Successful EXPN session diff --git a/tests/src/smtp/lookup/expressions.rs b/tests/src/smtp/lookup/expressions.rs new file mode 100644 index 00000000..691b238b --- /dev/null +++ b/tests/src/smtp/lookup/expressions.rs @@ -0,0 +1,130 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::utils::{dns::DnsCache, server::TestServerBuilder}; +use common::expr::{tokenizer::TokenMap, *}; +use mail_auth::MX; +use registry::schema::{ + enums::ExpressionVariable, + prelude::{ObjectType, Property}, + structs::{LookupStore, SqliteStore, StoreLookup}, +}; +use smtp::queue::RecipientDomain; +use std::time::{Duration, Instant}; + +const TESTS: &[(&str, &str)] = &[ + ("dns_query(rcpt_domain, 'mx')[0]", "mx.foobar.org"), + ( + "key_get('sql', 'hello') + '-' + key_exists('sql', 'hello') + '-' + key_set('sql', 'hello', 'world') + '-' + key_get('sql', 'hello') + '-' + key_exists('sql', 'hello')", + "-0-1-world-1", + ), + ( + "counter_get('sql', 'county') + '-' + counter_incr('sql', 'county', 1) + '-' + counter_incr('sql', 'county', 1) + '-' + counter_get('sql', 'county')", + "0-1-2-2", + ), + ( + "sql_query('sql', 'SELECT description FROM domains WHERE name = ?', 'foobar.org')", + "Main domain", + ), + ( + "is_local_domain('foobar.org') + '-' + is_local_domain('unknown.org') + '-' + is_local_address('john@foobar.org') + '-' + is_local_address('unknown@foobar.org')", + "1-0-1-0", + ), +]; + +#[tokio::test] +async fn expressions() { + let mut test = TestServerBuilder::new("smtp_lookup_test") + .await + .with_http_listener(19017) + .await + .disable_services() + .capture_queue() + .build() + .await; + + // Create test data + let admin = test.account("admin"); + for (name, secret, description, aliases) in [ + ("john@foobar.org", "12345 + extra safety", "John Doe", &[]), + ("jane@domain.net", "abcde + extra safety", "Jane Smith", &[]), + ] { + admin + .create_user_account(name, secret, description, aliases, vec![]) + .await; + } + admin + .registry_create_object(StoreLookup { + namespace: "sql".into(), + store: LookupStore::Sqlite(SqliteStore { + path: format!("{}/smtp_sql.db", test.tmp_dir()), + pool_max_connections: 10, + pool_workers: None, + }), + }) + .await; + admin.reload_lookup_stores().await; + test.reload_core(); + + test.server.mx_add( + "test.org", + vec![MX { + exchanges: vec!["mx.foobar.org".into()].into_boxed_slice(), + preference: 10, + }], + Instant::now() + Duration::from_secs(10), + ); + + let sql = test + .server + .get_lookup_store("sql") + .unwrap() + .into_store() + .unwrap(); + sql.create_tables().await.unwrap(); + for query in [ + "CREATE TABLE domains (name TEXT PRIMARY KEY, description TEXT);", + "INSERT INTO domains (name, description) VALUES ('foobar.org', 'Main domain');", + "INSERT INTO domains (name, description) VALUES ('foobar.net', 'Secondary domain');", + "CREATE TABLE allowed_ips (addr TEXT PRIMARY KEY);", + "INSERT INTO allowed_ips (addr) VALUES ('10.0.0.50');", + ] { + sql.sql_query::(query, Vec::new()).await.unwrap(); + } + + // Test expression functions + let token_map = TokenMap::default().with_variables(&[ + ExpressionVariable::Rcpt, + ExpressionVariable::RcptDomain, + ExpressionVariable::Sender, + ExpressionVariable::SenderDomain, + ExpressionVariable::Mx, + ExpressionVariable::HeloDomain, + ExpressionVariable::AuthenticatedAs, + ExpressionVariable::Listener, + ExpressionVariable::RemoteIp, + ExpressionVariable::LocalIp, + ExpressionVariable::Priority, + ]); + for (expr, expected) in TESTS { + let e = Expression::parse(&token_map, expr); + assert_eq!( + test.server + .eval_expr::( + &e, + &RecipientDomain::new("test.org"), + ObjectType::Account.singleton(), + Property::AccountName, + 0 + ) + .await + .unwrap(), + *expected, + "failed for '{}'", + expr + ); + } +} diff --git a/tests/src/smtp/lookup/mod.rs b/tests/src/smtp/lookup/mod.rs index 28beea0b..8811accd 100644 --- a/tests/src/smtp/lookup/mod.rs +++ b/tests/src/smtp/lookup/mod.rs @@ -4,5 +4,5 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -pub mod sql; +pub mod expressions; pub mod utils; diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs deleted file mode 100644 index 7c578dcf..00000000 --- a/tests/src/smtp/lookup/sql.rs +++ /dev/null @@ -1,303 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::smtp::session::{TestSession, VerifyResponse}; -use common::{ - Core, - expr::{tokenizer::TokenMap, *}, -}; -use mail_auth::MX; -use smtp::{core::Session, queue::RecipientDomain}; -use std::time::{Duration, Instant}; - -const CONFIG: &str = r#" -[storage] -data = "sql" -blob = "sql" -fts = "sql" -lookup = "sql" -directory = "sql" - -[store."sql"] -type = "sqlite" -path = "{TMP}/smtp_sql.db" - -[store."sql".query] -name = "SELECT name, type, secret, description, quota FROM accounts WHERE name = ? AND active = true" -members = "SELECT member_of FROM group_members WHERE name = ?" -recipients = "SELECT name FROM emails WHERE address = ?" -emails = "SELECT address FROM emails WHERE name = ? AND type != 'list' ORDER BY type DESC, address ASC" -verify = "SELECT address FROM emails WHERE address LIKE '%' || ? || '%' AND type = 'primary' ORDER BY address LIMIT 5" -expand = "SELECT p.address FROM emails AS p JOIN emails AS l ON p.name = l.name WHERE p.type = 'primary' AND l.address = ? AND l.type = 'list' ORDER BY p.address LIMIT 50" -domains = "SELECT 1 FROM emails WHERE address LIKE '%@' || ? LIMIT 1" - -[directory."sql"] -type = "sql" -store = "sql" - -[directory."sql".columns] -name = "name" -description = "description" -secret = "secret" -email = "address" -quota = "quota" -class = "type" - -[session.auth] -directory = "'sql'" -mechanisms = "[plain, login]" -errors.wait = "5ms" - -[session.rcpt] -directory = "'sql'" -relay = false -errors.wait = "5ms" - -[session.extensions] -requiretls = [{if = "sql_query('sql', 'SELECT addr FROM allowed_ips WHERE addr = ? LIMIT 1', remote_ip)", then = true}, - {else = false}] -expn = true -vrfy = true - -[test."sql"] -expr = "sql_query('sql', 'SELECT description FROM domains WHERE name = ?', 'foobar.org')" -expect = "Main domain" - -[test."dns"] -expr = "dns_query(rcpt_domain, 'mx')[0]" -expect = "mx.foobar.org" - -[test."key_get"] -expr = "key_get('sql', 'hello') + '-' + key_exists('sql', 'hello') + '-' + key_set('sql', 'hello', 'world') + '-' + key_get('sql', 'hello') + '-' + key_exists('sql', 'hello')" -expect = "-0-1-world-1" - -[test."counter_get"] -expr = "counter_get('sql', 'county') + '-' + counter_incr('sql', 'county', 1) + '-' + counter_incr('sql', 'county', 1) + '-' + counter_get('sql', 'county')" -expect = "0-1-2-2" - -"#; - -#[tokio::test] -async fn lookup_sql() { - - - - // Parse settings - let temp_dir = TempDir::new("smtp_lookup_tests", true); - let mut config = Config::new(temp_dir.update_config(CONFIG)).unwrap(); - let stores = Stores::parse_all(&mut config, false).await; - - let core = Core::parse(&mut config, stores, Default::default()).await; - - // Obtain directory handle - let handle = DirectoryStore { - store: core.storage.stores.get("sql").unwrap().clone(), - }; - let test = TestSMTP::from_core(core); - - test.server.mx_add( - "test.org", - vec![MX { - exchanges: vec!["mx.foobar.org".to_string()], - preference: 10, - }], - Instant::now() + Duration::from_secs(10), - ); - - // Create tables - handle.create_test_directory().await; - - // Create test records - handle - .create_test_user_with_email("jane@foobar.org", "s3cr3tp4ss", "Jane") - .await; - handle - .create_test_user_with_email("john@foobar.org", "mypassword", "John") - .await; - handle - .create_test_user_with_email("bill@foobar.org", "123456", "Bill") - .await; - handle - .create_test_user_with_email("mike@foobar.net", "098765", "Mike") - .await; - - for query in [ - "CREATE TABLE domains (name TEXT PRIMARY KEY, description TEXT);", - "INSERT INTO domains (name, description) VALUES ('foobar.org', 'Main domain');", - "INSERT INTO domains (name, description) VALUES ('foobar.net', 'Secondary domain');", - "CREATE TABLE allowed_ips (addr TEXT PRIMARY KEY);", - "INSERT INTO allowed_ips (addr) VALUES ('10.0.0.50');", - ] { - handle - .store - .sql_query::(query, Vec::new()) - .await - .unwrap(); - } - - // Create local domains - let internal_store = &test.server.core.storage.data; - for name in ["foobar.org", "foobar.net"] { - internal_store - .create_principal( - PrincipalSet::new(0, Type::Domain).with_field(PrincipalField::Name, name), - None, - None, - ) - .await - .unwrap(); - } - - // Create lists - internal_store - .create_principal( - PrincipalSet::new(0, Type::List) - .with_field(PrincipalField::Name, "support@foobar.org") - .with_field(PrincipalField::Emails, "support@foobar.org") - .with_field( - PrincipalField::ExternalMembers, - PrincipalValue::StringList(vec!["mike@foobar.net".to_string()]), - ), - None, - None, - ) - .await - .unwrap(); - internal_store - .create_principal( - PrincipalSet::new(0, Type::List) - .with_field(PrincipalField::Name, "sales@foobar.org") - .with_field(PrincipalField::Emails, "sales@foobar.org") - .with_field( - PrincipalField::ExternalMembers, - PrincipalValue::StringList(vec![ - "jane@foobar.org".to_string(), - "john@foobar.org".to_string(), - "bill@foobar.org".to_string(), - ]), - ), - None, - None, - ) - .await - .unwrap(); - - // Test expression functions - let token_map = TokenMap::default().with_variables(&[ - ExpressionVariable::Rcpt, - ExpressionVariable::RcptDomain, - ExpressionVariable::Sender, - ExpressionVariable::SenderDomain, - ExpressionVariable::Mx, - ExpressionVariable::HeloDomain, - ExpressionVariable::AuthenticatedAs, - ExpressionVariable::Listener, - ExpressionVariable::RemoteIp, - ExpressionVariable::LocalIp, - ExpressionVariable::Priority, - ]); - for test_name in ["sql", "dns", "key_get", "counter_get"] { - let e = - Expression::try_parse(&mut config, ("test", test_name, "expr"), &token_map).unwrap(); - assert_eq!( - test.server - .eval_expr::(&e, &RecipientDomain::new("test.org"), "text", 0) - .await - .unwrap(), - config.value(("test", test_name, "expect")).unwrap(), - "failed for '{}'", - test_name - ); - } - - let mut session = Session::test(test.server); - session.data.remote_ip_str = "10.0.0.50".parse().unwrap(); - session.eval_session_params().await; - session.stream.tls = true; - session - .ehlo("mx.foobar.org") - .await - .assert_contains("REQUIRETLS"); - session.data.remote_ip_str = "10.0.0.1".into(); - session.eval_session_params().await; - session - .ehlo("mx1.foobar.org") - .await - .assert_not_contains("REQUIRETLS"); - - // Test RCPT - session.mail_from("john@example.net", "250").await; - - // External domain - session.rcpt_to("user@otherdomain.org", "550 5.1.2").await; - - // Non-existent user - session.rcpt_to("jack@foobar.org", "550 5.1.2").await; - - // Valid users - session.rcpt_to("jane@foobar.org", "250").await; - session.rcpt_to("john@foobar.org", "250").await; - session.rcpt_to("bill@foobar.org", "250").await; - - // Lists - session.rcpt_to("sales@foobar.org", "250").await; - - // Test EXPN - session - .cmd("EXPN sales@foobar.org", "250") - .await - .assert_contains("jane@foobar.org") - .assert_contains("john@foobar.org") - .assert_contains("bill@foobar.org"); - session - .cmd("EXPN support@foobar.org", "250") - .await - .assert_contains("mike@foobar.net"); - session.cmd("EXPN marketing@foobar.org", "550 5.1.2").await; - - // Test VRFY - session - .server - .core - .storage - .directory - .query(QueryParams::name("john@foobar.org").with_return_member_of(true)) - .await - .unwrap() - .unwrap(); - session - .server - .core - .storage - .directory - .query(QueryParams::name("jane@foobar.org").with_return_member_of(true)) - .await - .unwrap() - .unwrap(); - session - .cmd("VRFY john", "250") - .await - .assert_contains("john@foobar.org"); - session - .cmd("VRFY jane", "250") - .await - .assert_contains("jane@foobar.org"); - session.cmd("VRFY tim", "550 5.1.2").await; - - // Test AUTH - session - .cmd( - "AUTH PLAIN AGphbmVAZm9vYmFyLm9yZwB3cm9uZ3Bhc3M=", - "535 5.7.8", - ) - .await; - session - .cmd( - "AUTH PLAIN AGphbmVAZm9vYmFyLm9yZwBzM2NyM3RwNHNz", - "235 2.7.0", - ) - .await; -} diff --git a/tests/src/smtp/lookup/utils.rs b/tests/src/smtp/lookup/utils.rs index 3b7ac588..e20f0bf2 100644 --- a/tests/src/smtp/lookup/utils.rs +++ b/tests/src/smtp/lookup/utils.rs @@ -4,18 +4,25 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::TestSMTP; +use crate::utils::server::TestServerBuilder; use ::smtp::outbound::NextHop; -use common::{ - Core, - config::smtp::{ - queue::{MxConfig, QueueExpiry, QueueName}, - report::AggregateFrequency, - resolver::{Mode, MxPattern, Policy}, - }, +use common::config::smtp::{ + queue::{MxConfig, QueueExpiry, QueueName}, + report::AggregateFrequency, + resolver::{Mode, MxPattern, Policy}, }; use mail_auth::{IpLookupStrategy, MX}; use mail_parser::DateTime; +use registry::{ + schema::{ + enums::MtaIpStrategy, + structs::{ + Expression, MtaConnectionIpHost, MtaConnectionStrategy, MtaOutboundStrategy, MtaRoute, + MtaRouteMx, MtaStageAuth, + }, + }, + types::{ipaddr::IpAddr, list::List}, +}; use smtp::{ outbound::{ lookup::{SourceIp, ToNextHop}, @@ -27,60 +34,11 @@ use smtp::{ }, reporting::AggregateTimestamp, }; -use std::net::IpAddr; +use std::{str::FromStr, sync::Arc}; use store::write::now; -const CONFIG: &str = r#" -[queue.connection.test.timeout] -connect = "10s" - -[queue.connection.test] -ehlo-hostname = "test.example.com" -source-ips = ["10.0.0.1", "10.0.0.2", "10.0.0.3", "10.0.0.4", - "a:b::1", "a:b::2", "a:b::3", "a:b::4"] - -[queue.source-ip."10.0.0.1"] -ehlo-hostname = "test1.example.com" - -[queue.source-ip."10.0.0.2"] -ehlo-hostname = "test2.example.com" - -[queue.source-ip."10.0.0.3"] -ehlo-hostname = "test3.example.com" - -[queue.source-ip."10.0.0.4"] -ehlo-hostname = "test4.example.com" - -[queue.source-ip."a:b::1"] -ehlo-hostname = "test5.example.com" - -[queue.source-ip."a:b::2"] -ehlo-hostname = "test6.example.com" - -[queue.source-ip."a:b::3"] -ehlo-hostname = "test7.example.com" - -[queue.source-ip."a:b::4"] -ehlo-hostname = "test8.example.com" - -[queue.test-v4.type] -type = "mx" -ip-lookup-strategy = "ipv4_then_ipv6" - -[queue.test-v6.type] -type = "mx" -ip-lookup-strategy = "ipv6_then_ipv4" - -[queue.strategy] -schedule = "source + ' ' + received_from_ip + ' ' + received_via_port + ' ' + queue_name + ' ' + last_error + ' ' + rcpt_domain + ' ' + size + ' ' + queue_age" - -"#; - #[tokio::test] async fn strategies() { - - - let ipv6: [IpAddr; 4] = [ "a:b::1".parse().unwrap(), "a:b::2".parse().unwrap(), @@ -106,9 +64,98 @@ async fn strategies() { "test8.example.com".to_string(), ]; - let mut config = Config::new(CONFIG).unwrap(); - let test = - TestSMTP::from_core(Core::parse(&mut config, Default::default(), Default::default()).await); + let mut test = TestServerBuilder::new("smtp_strategies_test") + .await + .with_http_listener(19016) + .await + .disable_services() + .capture_queue() + .build() + .await; + + // Add test settings + let admin = test.account("admin"); + admin + .registry_create_object(MtaStageAuth { + require: Expression { + else_: "false".into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaConnectionStrategy { + name: "test".into(), + ehlo_hostname: "test.example.com".to_string().into(), + connect_timeout: 10_000u64.into(), + source_ips: List::from_iter([ + MtaConnectionIpHost { + ehlo_hostname: "test1.example.com".to_string().into(), + source_ip: IpAddr::from_str("10.0.0.1").unwrap(), + }, + MtaConnectionIpHost { + ehlo_hostname: "test2.example.com".to_string().into(), + source_ip: IpAddr::from_str("10.0.0.2").unwrap(), + }, + MtaConnectionIpHost { + ehlo_hostname: "test3.example.com".to_string().into(), + source_ip: IpAddr::from_str("10.0.0.3").unwrap(), + }, + MtaConnectionIpHost { + ehlo_hostname: "test4.example.com".to_string().into(), + source_ip: IpAddr::from_str("10.0.0.4").unwrap(), + }, + MtaConnectionIpHost { + ehlo_hostname: "test5.example.com".to_string().into(), + source_ip: IpAddr::from_str("a:b::1").unwrap(), + }, + MtaConnectionIpHost { + ehlo_hostname: "test6.example.com".to_string().into(), + source_ip: IpAddr::from_str("a:b::2").unwrap(), + }, + MtaConnectionIpHost { + ehlo_hostname: "test7.example.com".to_string().into(), + source_ip: IpAddr::from_str("a:b::3").unwrap(), + }, + MtaConnectionIpHost { + ehlo_hostname: "test8.example.com".to_string().into(), + source_ip: IpAddr::from_str("a:b::4").unwrap(), + }, + ]), + ..Default::default() + }) + .await; + admin + .registry_create_object(MtaRoute::Mx(MtaRouteMx { + ip_lookup_strategy: MtaIpStrategy::V4ThenV6, + name: "test-v4".into(), + ..Default::default() + })) + .await; + admin + .registry_create_object(MtaRoute::Mx(MtaRouteMx { + ip_lookup_strategy: MtaIpStrategy::V6ThenV4, + name: "test-v6".into(), + ..Default::default() + })) + .await; + admin + .registry_create_object(MtaOutboundStrategy { + schedule: Expression { + else_: concat!( + "source + ' ' + received_from_ip + ' ' + ", + "received_via_port + ' ' + queue_name + ' ' + ", + "last_error + ' ' + rcpt_domain + ' ' + size + ' ' + queue_age" + ) + .into(), + ..Default::default() + }, + ..Default::default() + }) + .await; + admin.reload_settings().await; + test.reload_core(); let conn = test .server @@ -126,12 +173,18 @@ async fn strategies() { let ip_host = conn.source_ip(is_ipv4).unwrap(); if is_ipv4 { assert_eq!( - &ipv4_hosts[ipv4.iter().position(|&ip| ip == ip_host.ip).unwrap()], + &ipv4_hosts[ipv4 + .iter() + .position(|&ip| ip.into_inner() == ip_host.ip) + .unwrap()], ip_host.host.as_ref().unwrap() ); } else { assert_eq!( - &ipv6_hosts[ipv6.iter().position(|&ip| ip == ip_host.ip).unwrap()], + &ipv6_hosts[ipv6 + .iter() + .position(|&ip| ip.into_inner() == ip_host.ip) + .unwrap()], ip_host.host.as_ref().unwrap() ); } @@ -180,29 +233,25 @@ async fn strategies() { #[test] fn to_remote_hosts() { - let mx = vec![ + let mx: Arc<[MX]> = Arc::from(vec![ MX { - exchanges: vec!["mx1".to_string(), "mx2".to_string()], + exchanges: vec!["mx1".into(), "mx2".into()].into_boxed_slice(), preference: 10, }, MX { - exchanges: vec![ - "mx3".to_string(), - "mx4".to_string(), - "mx5".to_string(), - "mx6".to_string(), - ], + exchanges: vec!["mx3".into(), "mx4".into(), "mx5".into(), "mx6".into()] + .into_boxed_slice(), preference: 20, }, MX { - exchanges: vec!["mx7".to_string(), "mx8".to_string()], + exchanges: vec!["mx7".into(), "mx8".into()].into_boxed_slice(), preference: 10, }, MX { - exchanges: vec!["mx9".to_string(), "mxA".to_string()], + exchanges: vec!["mx9".into(), "mxA".into()].into_boxed_slice(), preference: 10, }, - ]; + ]); let mx_config = MxConfig { max_mx: 7, max_multi_homed: 2, @@ -215,10 +264,10 @@ fn to_remote_hosts() { assert!((*host.as_bytes().last().unwrap() - b'0') <= 8); } } - let mx = vec![MX { - exchanges: vec![".".to_string()], + let mx: Arc<[MX]> = Arc::from(vec![MX { + exchanges: vec![".".into()].into_boxed_slice(), preference: 0, - }]; + }]); assert!(mx.to_remote_hosts("domain", &mx_config).is_none()); } @@ -239,7 +288,8 @@ max_age: 604800", MxPattern::Equals("mail.example.com".to_string()), MxPattern::StartsWith("example.net".to_string()), MxPattern::Equals("backupmx.example.com".to_string()), - ], + ] + .into_boxed_slice(), max_age: 604800, }, ), @@ -256,7 +306,8 @@ max_age: 86400 mx: vec![ MxPattern::Equals("gmail-smtp-in.l.google.com".to_string()), MxPattern::StartsWith("gmail-smtp-in.l.google.com".to_string()), - ], + ] + .into_boxed_slice(), max_age: 86400, }, ), diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index f0fadfd9..59ee1f08 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -6,7 +6,7 @@ use crate::{ jmap::ManagementApi, - smtp::{DnsCache, TestSMTP, session::TestSession}, + smtp::{DnsCache, session::TestSession}, }; use ahash::{AHashMap, HashMap, HashSet}; use common::config::server::ServerProtocol; diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs index 1af8f115..490ebdfb 100644 --- a/tests/src/smtp/management/report.rs +++ b/tests/src/smtp/management/report.rs @@ -6,7 +6,7 @@ use crate::{ jmap::ManagementApi, - smtp::{TestSMTP, management::queue::List}, + smtp::{ management::queue::List}, }; use ahash::{AHashMap, HashSet}; use common::{ diff --git a/tests/src/smtp/mod.rs b/tests/src/smtp/mod.rs index bc02b7c7..ff2efcb7 100644 --- a/tests/src/smtp/mod.rs +++ b/tests/src/smtp/mod.rs @@ -5,72 +5,11 @@ */ pub mod inbound; +pub mod lookup; pub mod session; -/*pub mod lookup; +/* pub mod management; pub mod outbound; pub mod queue; pub mod reporting; */ - -const CONFIG: &str = r#" -[session.connect] -hostname = "'mx.example.org'" -greeting = "'Test SMTP instance'" - -[server.listener.smtp-debug] -bind = ['127.0.0.1:9925'] -protocol = 'smtp' - -[server.listener.lmtp-debug] -bind = ['127.0.0.1:9924'] -protocol = 'lmtp' -tls.implicit = true - -[server.listener.management-debug] -bind = ['127.0.0.1:9980'] -protocol = 'http' -tls.implicit = true - -[server.socket] -reuse-addr = true - -[server.tls] -enable = true -implicit = false -certificate = 'default' - -[certificate.default] -cert = '%{file:{CERT}}%' -private-key = '%{file:{PK}}%' - -[storage] -data = "{STORE}" -fts = "{STORE}" -blob = "{STORE}" -lookup = "{STORE}" - -[store."rocksdb"] -type = "rocksdb" -path = "{TMP}/queue.db" - -#[store."foundationdb"] -#type = "foundationdb" - -[store."postgresql"] -type = "postgresql" -host = "localhost" -port = 5432 -database = "stalwart" -user = "postgres" -password = "mysecretpassword" - -[store."mysql"] -type = "mysql" -host = "localhost" -port = 3307 -database = "stalwart" -user = "root" -password = "password" - -"#; diff --git a/tests/src/smtp/outbound/dane.rs b/tests/src/smtp/outbound/dane.rs index e06914b5..9b7c8deb 100644 --- a/tests/src/smtp/outbound/dane.rs +++ b/tests/src/smtp/outbound/dane.rs @@ -5,7 +5,7 @@ */ use crate::smtp::{ - DnsCache, TestSMTP, + DnsCache, inbound::{TestMessage, TestQueueEvent, TestReportingEvent}, session::{TestSession, VerifyResponse}, }; diff --git a/tests/src/smtp/outbound/extensions.rs b/tests/src/smtp/outbound/extensions.rs index ddec2fe4..92f85317 100644 --- a/tests/src/smtp/outbound/extensions.rs +++ b/tests/src/smtp/outbound/extensions.rs @@ -11,7 +11,7 @@ use mail_auth::MX; use smtp_proto::{MAIL_REQUIRETLS, MAIL_RET_HDRS, MAIL_SMTPUTF8, RCPT_NOTIFY_NEVER}; use crate::smtp::{ - DnsCache, TestSMTP, + DnsCache, inbound::{TestMessage, TestQueueEvent}, session::{TestSession, VerifyResponse}, }; diff --git a/tests/src/smtp/outbound/fallback_relay.rs b/tests/src/smtp/outbound/fallback_relay.rs index 4f8d1486..5da05e39 100644 --- a/tests/src/smtp/outbound/fallback_relay.rs +++ b/tests/src/smtp/outbound/fallback_relay.rs @@ -10,7 +10,7 @@ use common::config::server::ServerProtocol; use mail_auth::MX; use store::write::now; -use crate::smtp::{DnsCache, TestSMTP, session::TestSession}; +use crate::smtp::{DnsCache, session::TestSession}; const LOCAL: &str = r#" [queue.strategy] diff --git a/tests/src/smtp/outbound/ip_lookup.rs b/tests/src/smtp/outbound/ip_lookup.rs index ed0357e8..1f813dde 100644 --- a/tests/src/smtp/outbound/ip_lookup.rs +++ b/tests/src/smtp/outbound/ip_lookup.rs @@ -9,7 +9,7 @@ use std::time::{Duration, Instant}; use common::config::server::ServerProtocol; use mail_auth::{IpLookupStrategy, MX}; -use crate::smtp::{DnsCache, TestSMTP, session::TestSession}; +use crate::smtp::{DnsCache, session::TestSession}; const LOCAL: &str = r#" [session.rcpt] diff --git a/tests/src/smtp/outbound/lmtp.rs b/tests/src/smtp/outbound/lmtp.rs index 07e69285..5cd92996 100644 --- a/tests/src/smtp/outbound/lmtp.rs +++ b/tests/src/smtp/outbound/lmtp.rs @@ -7,7 +7,7 @@ use std::time::{Duration, Instant}; use crate::smtp::{ - DnsCache, TestSMTP, + DnsCache, inbound::TestMessage, queue::QueuedEvents, session::{TestSession, VerifyResponse}, diff --git a/tests/src/smtp/outbound/mta_sts.rs b/tests/src/smtp/outbound/mta_sts.rs index a893a923..b4a3fd87 100644 --- a/tests/src/smtp/outbound/mta_sts.rs +++ b/tests/src/smtp/outbound/mta_sts.rs @@ -21,7 +21,7 @@ use mail_auth::{ }; use crate::smtp::{ - DnsCache, TestSMTP, + DnsCache, inbound::{TestMessage, TestQueueEvent, TestReportingEvent}, session::{TestSession, VerifyResponse}, }; diff --git a/tests/src/smtp/outbound/smtp.rs b/tests/src/smtp/outbound/smtp.rs index ec2881c8..12eaa51d 100644 --- a/tests/src/smtp/outbound/smtp.rs +++ b/tests/src/smtp/outbound/smtp.rs @@ -14,7 +14,7 @@ use mail_auth::MX; use store::write::now; use crate::smtp::{ - DnsCache, TestSMTP, + DnsCache, inbound::{TestMessage, TestQueueEvent}, queue::QueuedEvents, session::{TestSession, VerifyResponse}, diff --git a/tests/src/smtp/outbound/throttle.rs b/tests/src/smtp/outbound/throttle.rs index a9f82a2a..d89eb57f 100644 --- a/tests/src/smtp/outbound/throttle.rs +++ b/tests/src/smtp/outbound/throttle.rs @@ -5,7 +5,7 @@ */ use crate::smtp::{ - DnsCache, TestSMTP, + DnsCache, inbound::TestQueueEvent, queue::{build_rcpt, manager::new_message}, session::TestSession, diff --git a/tests/src/smtp/outbound/tls.rs b/tests/src/smtp/outbound/tls.rs index 6815d270..e3aca014 100644 --- a/tests/src/smtp/outbound/tls.rs +++ b/tests/src/smtp/outbound/tls.rs @@ -11,7 +11,7 @@ use mail_auth::MX; use store::write::now; use crate::smtp::{ - DnsCache, TestSMTP, + DnsCache, inbound::TestMessage, session::{TestSession, VerifyResponse}, }; diff --git a/tests/src/smtp/queue/concurrent.rs b/tests/src/smtp/queue/concurrent.rs index c9f1aa29..d9093603 100644 --- a/tests/src/smtp/queue/concurrent.rs +++ b/tests/src/smtp/queue/concurrent.rs @@ -5,7 +5,7 @@ */ use crate::{ - smtp::{DnsCache, TestSMTP, session::TestSession}, + smtp::{DnsCache, session::TestSession}, store::cleanup::store_assert_is_empty, }; use common::{config::server::ServerProtocol, ipc::QueueEvent}; diff --git a/tests/src/smtp/queue/manager.rs b/tests/src/smtp/queue/manager.rs index 3035f4f0..ce356257 100644 --- a/tests/src/smtp/queue/manager.rs +++ b/tests/src/smtp/queue/manager.rs @@ -5,7 +5,7 @@ */ use crate::smtp::{ - TestSMTP, + queue::{QueuedEvents, build_rcpt}, }; use common::config::smtp::queue::QueueName; diff --git a/tests/src/smtp/queue/retry.rs b/tests/src/smtp/queue/retry.rs index dbc5b88d..a7e1f5c3 100644 --- a/tests/src/smtp/queue/retry.rs +++ b/tests/src/smtp/queue/retry.rs @@ -7,7 +7,7 @@ use std::time::Duration; use crate::smtp::{ - TestSMTP, + inbound::{TestMessage, TestQueueEvent}, queue::QueuedEvents, session::{TestSession, VerifyResponse}, diff --git a/tests/src/smtp/queue/virtualq.rs b/tests/src/smtp/queue/virtualq.rs index 053d1c99..6d73af9e 100644 --- a/tests/src/smtp/queue/virtualq.rs +++ b/tests/src/smtp/queue/virtualq.rs @@ -5,7 +5,7 @@ */ use crate::{ - smtp::{DnsCache, TestSMTP, session::TestSession}, + smtp::{DnsCache, session::TestSession}, store::cleanup::store_assert_is_empty, }; use common::{ diff --git a/tests/src/smtp/reporting/analyze.rs b/tests/src/smtp/reporting/analyze.rs index ed6d1457..d094b60f 100644 --- a/tests/src/smtp/reporting/analyze.rs +++ b/tests/src/smtp/reporting/analyze.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::smtp::{TestSMTP, inbound::TestQueueEvent, session::TestSession}; +use crate::smtp::{ inbound::TestQueueEvent, session::TestSession}; use std::time::Duration; use store::{IterateParams, ValueKey, write::ValueClass}; diff --git a/tests/src/smtp/reporting/tls.rs b/tests/src/smtp/reporting/tls.rs index 432fb2a8..47805bb9 100644 --- a/tests/src/smtp/reporting/tls.rs +++ b/tests/src/smtp/reporting/tls.rs @@ -18,7 +18,7 @@ use store::write::QueueClass; use smtp::reporting::tls::{TLS_HTTP_REPORT, TlsReporting}; use crate::smtp::{ - TestSMTP, + inbound::{TestMessage, sign::SIGNATURES}, session::VerifyResponse, }; diff --git a/tests/src/utils/registry.rs b/tests/src/utils/registry.rs index 14656a01..c4bbd535 100644 --- a/tests/src/utils/registry.rs +++ b/tests/src/utils/registry.rs @@ -218,6 +218,11 @@ impl Account { self.registry_create_object(Action::ReloadSettings).await; } + pub async fn reload_lookup_stores(&self) { + self.registry_create_object(Action::ReloadLookupStores) + .await; + } + pub async fn registry_update_object_expect_err( &self, object: ObjectType, diff --git a/tests/src/utils/server.rs b/tests/src/utils/server.rs index 7ca5e08f..4a8b08da 100644 --- a/tests/src/utils/server.rs +++ b/tests/src/utils/server.rs @@ -419,6 +419,10 @@ impl TestServer { self.reset } + pub fn tmp_dir(&self) -> &str { + self.temp_dir.path.as_os_str().to_str().unwrap() + } + pub fn shutdown(&self) { let _ = self.shutdown_tx.send(true); } @@ -427,6 +431,11 @@ impl TestServer { Session::test(self.server.clone()) } + pub fn new_mta_session_with_shutdown(&self) -> (Session, watch::Sender) { + let (tx, rx) = watch::channel(true); + (Session::test_with_shutdown(self.server.clone(), rx), tx) + } + pub async fn resources(&self, name: &'static str, collection: Collection) -> Arc { let account_id = self.account(name).id().document_id(); self.server