From b5409a0b00bdd78873624d4cbea1f6d8b695db5e Mon Sep 17 00:00:00 2001 From: mdecimus Date: Fri, 20 Oct 2023 15:31:05 +0200 Subject: [PATCH] Macros, include support, added scores --- crates/directory/src/scheduled.rs | 1 + crates/jmap/src/api/config.rs | 4 +- crates/jmap/src/api/session.rs | 8 +- crates/jmap/src/lib.rs | 48 +-- crates/smtp/src/config/scripts.rs | 45 ++- crates/smtp/src/inbound/data.rs | 33 +- crates/smtp/src/inbound/mail.rs | 8 +- crates/smtp/src/inbound/rcpt.rs | 10 +- crates/smtp/src/lib.rs | 6 +- crates/smtp/src/scripts/envelope.rs | 182 +++++---- crates/smtp/src/scripts/event_loop.rs | 8 +- crates/smtp/src/scripts/mod.rs | 16 +- crates/smtp/src/scripts/plugins/headers.rs | 47 +++ crates/smtp/src/scripts/plugins/mod.rs | 10 +- crates/utils/src/config/mod.rs | 90 ++++- crates/utils/src/config/parser.rs | 369 ++++++++++-------- crates/utils/src/config/utils.rs | 11 +- resources/config/common/server.toml | 2 +- resources/config/common/tracing.toml | 2 +- resources/config/config.toml | 36 ++ resources/config/directory/imap.toml | 4 +- resources/config/directory/ldap.toml | 2 +- resources/config/directory/lmtp.toml | 4 +- resources/config/directory/memory.toml | 14 +- resources/config/directory/sql.toml | 4 +- resources/config/jmap/listener.toml | 2 +- resources/config/jmap/sieve.toml | 8 +- resources/config/jmap/store.toml | 6 +- resources/config/macros.toml | 8 - resources/config/smtp/queue.toml | 6 +- resources/config/smtp/remote.toml | 9 +- resources/config/smtp/report.toml | 22 +- resources/config/smtp/resolver.toml | 3 +- resources/config/smtp/session.toml | 6 +- resources/config/smtp/sieve.toml | 14 +- resources/config/smtp/signature.toml | 6 +- resources/config/smtp/spamfilter.toml | 91 ++--- resources/config/spamfilter/maps/scores.map | 360 +++++++++++++++++ .../config/spamfilter/scripts/epilogue.sieve | 4 +- .../config/spamfilter/scripts/replyto.sieve | 4 + tests/resources/create_test_env.sh | 17 + tests/src/directory/mod.rs | 6 +- tests/src/imap/mod.rs | 2 +- tests/src/jmap/mod.rs | 2 +- tests/src/jmap/push_subscription.rs | 2 +- tests/src/smtp/config.rs | 12 +- tests/src/smtp/inbound/antispam.rs | 8 +- tests/src/smtp/inbound/auth.rs | 2 +- tests/src/smtp/inbound/data.rs | 2 +- tests/src/smtp/inbound/dmarc.rs | 2 +- tests/src/smtp/inbound/rcpt.rs | 2 +- tests/src/smtp/inbound/rewrite.rs | 8 +- tests/src/smtp/inbound/scripts.rs | 8 +- tests/src/smtp/inbound/sign.rs | 4 +- tests/src/smtp/inbound/vrfy.rs | 2 +- tests/src/smtp/lookup/sql.rs | 2 +- tests/src/smtp/management/queue.rs | 2 +- tests/src/smtp/management/report.rs | 2 +- tests/src/smtp/mod.rs | 10 +- tests/src/smtp/outbound/lmtp.rs | 2 +- tests/src/smtp/outbound/mod.rs | 2 +- tests/src/store/blob.rs | 8 +- tests/src/store/mod.rs | 2 +- 63 files changed, 1128 insertions(+), 494 deletions(-) create mode 100644 crates/smtp/src/scripts/plugins/headers.rs create mode 100644 resources/config/config.toml delete mode 100644 resources/config/macros.toml create mode 100644 tests/resources/create_test_env.sh diff --git a/crates/directory/src/scheduled.rs b/crates/directory/src/scheduled.rs index 5029ca76..c70ebfad 100644 --- a/crates/directory/src/scheduled.rs +++ b/crates/directory/src/scheduled.rs @@ -27,6 +27,7 @@ use crate::DirectorySchedule; impl DirectorySchedule { pub fn spawn(self, mut shutdown_rx: watch::Receiver) { + tracing::debug!("Directory query scheduler task starting."); tokio::spawn(async move { loop { if tokio::time::timeout(self.cron.time_to_next(), shutdown_rx.changed()) diff --git a/crates/jmap/src/api/config.rs b/crates/jmap/src/api/config.rs index d784a205..d42a2da2 100644 --- a/crates/jmap/src/api/config.rs +++ b/crates/jmap/src/api/config.rs @@ -85,10 +85,10 @@ impl crate::Config { .property("jmap.email.parse.max-items")? .unwrap_or(10), sieve_max_script_name: settings - .property("jmap.sieve.limits.name-length")? + .property("sieve.jmap.limits.name-length")? .unwrap_or(512), sieve_max_scripts: settings - .property("jmap.sieve.limits.max-scripts")? + .property("sieve.jmap.limits.max-scripts")? .unwrap_or(256), capabilities: BaseCapabilities::default(), session_cache_ttl: settings diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index a54b9f9d..5c750b78 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -379,7 +379,7 @@ impl SieveCapabilities { pub fn new(config: &crate::Config, settings: &utils::config::Config) -> Self { let mut notification_methods = Vec::new(); - for (_, uri) in settings.values("jmap.sieve.notification-uris") { + for (_, uri) in settings.values("sieve.jmap.notification-uris") { notification_methods.push(uri.to_string()); } if notification_methods.is_empty() { @@ -389,7 +389,7 @@ impl SieveCapabilities { let mut capabilities: AHashSet = AHashSet::from_iter(sieve::compiler::grammar::Capability::all().iter().cloned()); - for (_, capability) in settings.values("jmap.sieve.disabled-capabilities") { + for (_, capability) in settings.values("sieve.jmap.disabled-capabilities") { capabilities.remove(&sieve::compiler::grammar::Capability::parse(capability)); } @@ -402,12 +402,12 @@ impl SieveCapabilities { SieveCapabilities { max_script_name: config.sieve_max_script_name, max_script_size: settings - .property("jmap.sieve.max-script-size") + .property("sieve.jmap.max-script-size") .failed("Invalid configuration file") .unwrap_or(1024 * 1024), max_scripts: config.sieve_max_scripts, max_redirects: settings - .property("jmap.sieve.max-redirects") + .property("sieve.jmap.max-redirects") .failed("Invalid configuration file") .unwrap_or(1), extensions, diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index 6ca93c86..d372f252 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -228,98 +228,98 @@ impl JMAP { sieve_compiler: Compiler::new() .with_max_script_size( config - .property("jmap.sieve.limits.script-size")? + .property("sieve.jmap.limits.script-size")? .unwrap_or(1024 * 1024), ) .with_max_string_size( config - .property("jmap.sieve.limits.string-length")? + .property("sieve.jmap.limits.string-length")? .unwrap_or(4096), ) .with_max_variable_name_size( config - .property("jmap.sieve.limits.variable-name-length")? + .property("sieve.jmap.limits.variable-name-length")? .unwrap_or(32), ) .with_max_nested_blocks( config - .property("jmap.sieve.limits.nested-blocks")? + .property("sieve.jmap.limits.nested-blocks")? .unwrap_or(15), ) .with_max_nested_tests( config - .property("jmap.sieve.limits.nested-tests")? + .property("sieve.jmap.limits.nested-tests")? .unwrap_or(15), ) .with_max_nested_foreverypart( config - .property("jmap.sieve.limits.nested-foreverypart")? + .property("sieve.jmap.limits.nested-foreverypart")? .unwrap_or(3), ) .with_max_match_variables( config - .property("jmap.sieve.limits.match-variables")? + .property("sieve.jmap.limits.match-variables")? .unwrap_or(30), ) .with_max_local_variables( config - .property("jmap.sieve.limits.local-variables")? + .property("sieve.jmap.limits.local-variables")? .unwrap_or(128), ) .with_max_header_size( config - .property("jmap.sieve.limits.header-size")? + .property("sieve.jmap.limits.header-size")? .unwrap_or(1024), ) - .with_max_includes(config.property("jmap.sieve.limits.includes")?.unwrap_or(3)), + .with_max_includes(config.property("sieve.jmap.limits.includes")?.unwrap_or(3)), sieve_runtime: Runtime::new() .with_max_nested_includes( config - .property("jmap.sieve.limits.nested-includes")? + .property("sieve.jmap.limits.nested-includes")? .unwrap_or(3), ) - .with_cpu_limit(config.property("jmap.sieve.limits.cpu")?.unwrap_or(5000)) + .with_cpu_limit(config.property("sieve.jmap.limits.cpu")?.unwrap_or(5000)) .with_max_variable_size( config - .property("jmap.sieve.limits.variable-size")? + .property("sieve.jmap.limits.variable-size")? .unwrap_or(4096), ) - .with_max_redirects(config.property("jmap.sieve.limits.redirects")?.unwrap_or(1)) + .with_max_redirects(config.property("sieve.jmap.limits.redirects")?.unwrap_or(1)) .with_max_received_headers( config - .property("jmap.sieve.limits.received-headers")? + .property("sieve.jmap.limits.received-headers")? .unwrap_or(10), ) .with_max_header_size( config - .property("jmap.sieve.limits.header-size")? + .property("sieve.jmap.limits.header-size")? .unwrap_or(1024), ) .with_max_out_messages( config - .property("jmap.sieve.limits.outgoing-messages")? + .property("sieve.jmap.limits.outgoing-messages")? .unwrap_or(3), ) .with_default_vacation_expiry( config - .property::("jmap.sieve.default-expiry.vacation")? + .property::("sieve.jmap.default-expiry.vacation")? .unwrap_or(Duration::from_secs(30 * 86400)) .as_secs(), ) .with_default_duplicate_expiry( config - .property::("jmap.sieve.default-expiry.duplicate")? + .property::("sieve.jmap.default-expiry.duplicate")? .unwrap_or(Duration::from_secs(7 * 86400)) .as_secs(), ) .without_capabilities( config - .values("jmap.sieve.disable-capabilities") + .values("sieve.jmap.disable-capabilities") .map(|(_, v)| v), ) .with_valid_notification_uris({ let values = config - .values("jmap.sieve.notification-uris") + .values("sieve.jmap.notification-uris") .map(|(_, v)| v.to_string()) .collect::>(); if !values.is_empty() { @@ -330,7 +330,7 @@ impl JMAP { }) .with_protected_headers({ let values = config - .values("jmap.sieve.protected-headers") + .values("sieve.jmap.protected-headers") .map(|(_, v)| v.to_string()) .collect::>(); if !values.is_empty() { @@ -346,13 +346,13 @@ impl JMAP { }) .with_vacation_default_subject( config - .value("jmap.sieve.vacation.default-subject") + .value("sieve.jmap.vacation.default-subject") .unwrap_or("Automated reply") .to_string(), ) .with_vacation_subject_prefix( config - .value("jmap.sieve.vacation.subject-prefix") + .value("sieve.jmap.vacation.subject-prefix") .unwrap_or("Auto: ") .to_string(), ) diff --git a/crates/smtp/src/config/scripts.rs b/crates/smtp/src/config/scripts.rs index f4bd610b..96c4f0b4 100644 --- a/crates/smtp/src/config/scripts.rs +++ b/crates/smtp/src/config/scripts.rs @@ -95,7 +95,7 @@ impl ConfigSieve for Config { .with_max_header_size(10240) .with_max_includes(10) .with_no_capability_check( - self.property_or_static("sieve.no-capability-check", "false")?, + self.property_or_static("sieve.smtp.no-capability-check", "false")?, ) .register_functions(&mut fnc_map); @@ -114,31 +114,33 @@ impl ConfigSieve for Config { ]) .with_capability(Capability::Expressions) .with_capability(Capability::While) - .with_max_variable_size(102400) + .with_max_variable_size( + self.property_or_static("sieve.smtp.limits.variable-size", "52428800")?, + ) .with_max_header_size(10240) .with_valid_notification_uri("mailto") .with_valid_ext_lists(ctx.directory.lookups.keys().map(|k| k.to_string())) .with_functions(&mut fnc_map); - if let Some(value) = self.property("sieve.limits.redirects")? { + if let Some(value) = self.property("sieve.smtp.limits.redirects")? { runtime.set_max_redirects(value); } - if let Some(value) = self.property("sieve.limits.out-messages")? { + if let Some(value) = self.property("sieve.smtp.limits.out-messages")? { runtime.set_max_out_messages(value); } - if let Some(value) = self.property("sieve.limits.cpu")? { + if let Some(value) = self.property("sieve.smtp.limits.cpu")? { runtime.set_cpu_limit(value); } - if let Some(value) = self.property("sieve.limits.nested-includes")? { + if let Some(value) = self.property("sieve.smtp.limits.nested-includes")? { runtime.set_max_nested_includes(value); } - if let Some(value) = self.property("sieve.limits.received-headers")? { + if let Some(value) = self.property("sieve.smtp.limits.received-headers")? { runtime.set_max_received_headers(value); } - if let Some(value) = self.property::("sieve.limits.duplicate-expiry")? { + if let Some(value) = self.property::("sieve.smtp.limits.duplicate-expiry")? { runtime.set_default_duplicate_expiry(value.as_secs()); } - let hostname = if let Some(hostname) = self.value("sieve.hostname") { + let hostname = if let Some(hostname) = self.value("sieve.smtp.hostname") { hostname } else { self.value_require("server.hostname")? @@ -146,8 +148,19 @@ impl ConfigSieve for Config { runtime.set_local_hostname(hostname.to_string()); // Parse scripts - for id in self.sub_keys("sieve.scripts") { - let script = self.file_contents(("sieve.scripts", id))?; + for id in self.sub_keys("sieve.smtp.scripts") { + let key = ("sieve.smtp.scripts", id); + + let script = if !self.contains_key(key) { + let mut script = Vec::new(); + for sub_key in self.sub_keys(key) { + script.extend(self.file_contents(sub_key)?); + } + script + } else { + self.file_contents(key)? + }; + ctx.scripts.insert( id.to_string(), compiler @@ -159,14 +172,14 @@ impl ConfigSieve for Config { // Parse DKIM signatures let mut sign = Vec::new(); - for (pos, id) in self.values("sieve.sign") { + for (pos, id) in self.values("sieve.smtp.sign") { if let Some(dkim) = ctx.signers.get(id) { sign.push(dkim.clone()); } else { return Err(format!( "No DKIM signer found with id {:?} for key {:?}.", id, - ("sieve.sign", pos).as_key() + ("sieve.smtp.sign", pos).as_key() )); } } @@ -177,15 +190,15 @@ impl ConfigSieve for Config { lookup: ctx.directory.lookups.clone(), config: SieveConfig { from_addr: self - .value("sieve.from-addr") + .value("sieve.smtp.from-addr") .map(|a| a.to_string()) .unwrap_or(format!("MAILER-DAEMON@{hostname}")), from_name: self - .value("sieve.from-name") + .value("sieve.smtp.from-name") .unwrap_or("Mailer Daemon") .to_string(), return_path: self - .value("sieve.return-path") + .value("sieve.smtp.return-path") .unwrap_or_default() .to_string(), sign, diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index 64674182..68c0928f 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -47,7 +47,7 @@ use crate::{ core::{Session, SessionAddress, State}, queue::{self, Message, SimpleEnvelope}, reporting::analysis::AnalyzeReport, - scripts::ScriptResult, + scripts::{ScriptModification, ScriptResult}, }; use super::{AuthResult, IsTls}; @@ -400,6 +400,7 @@ impl Session { } // Sieve filtering + let mut headers = Vec::with_capacity(64); if let Some(script) = dc.script.eval(self).await { let params = self .build_script_parameters("data") @@ -450,20 +451,14 @@ impl Session { .unwrap_or_default(), ); - match self.run_script(script.clone(), params).await { - ScriptResult::Accept { modifications } => { - if !modifications.is_empty() { - self.data.apply_sieve_modifications(modifications) - } - } + let modifications = match self.run_script(script.clone(), params).await { + ScriptResult::Accept { modifications } => modifications, ScriptResult::Replace { message, modifications, } => { - if !modifications.is_empty() { - self.data.apply_sieve_modifications(modifications) - } edited_message = Arc::new(message).into(); + modifications } ScriptResult::Reject(message) => { tracing::info!(parent: &self.span, @@ -476,6 +471,23 @@ impl Session { ScriptResult::Discard => { return (b"250 2.0.0 Message queued for delivery.\r\n"[..]).into(); } + }; + + // Apply modifications + for modification in modifications { + match modification { + ScriptModification::AddHeader { name, value } => { + headers.extend_from_slice(name.as_bytes()); + headers.extend_from_slice(b": "); + headers.extend_from_slice(value.as_bytes()); + if !value.ends_with('\n') { + headers.extend_from_slice(b"\r\n"); + } + } + ScriptModification::SetEnvelope { name, value } => { + self.data.apply_envelope_modification(name, value); + } + } } } @@ -485,7 +497,6 @@ impl Session { let mut message = self.build_message(mail_from, rcpt_to).await; // Add Received header - let mut headers = Vec::with_capacity(64); if *dc.add_received.eval(self).await { self.write_received(&mut headers, message.id) } diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs index 5ae17dce..dbd352d7 100644 --- a/crates/smtp/src/inbound/mail.rs +++ b/crates/smtp/src/inbound/mail.rs @@ -30,7 +30,7 @@ use tokio::io::{AsyncRead, AsyncWrite}; use crate::{ core::{Session, SessionAddress}, queue::DomainPart, - scripts::ScriptResult, + scripts::{ScriptModification, ScriptResult}, }; use super::IsTls; @@ -127,7 +127,11 @@ impl Session { event = "modify", address = &self.data.mail_from.as_ref().unwrap().address, modifications = ?modifications); - self.data.apply_sieve_modifications(modifications) + for modification in modifications { + if let ScriptModification::SetEnvelope { name, value } = modification { + self.data.apply_envelope_modification(name, value); + } + } } } ScriptResult::Reject(message) => { diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index 7b627686..f4d0446d 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -29,7 +29,7 @@ use tokio::io::{AsyncRead, AsyncWrite}; use crate::{ core::{Session, SessionAddress}, queue::DomainPart, - scripts::ScriptResult, + scripts::{ScriptModification, ScriptResult}, }; use super::IsTls; @@ -102,7 +102,13 @@ impl Session { event = "modify", address = self.data.rcpt_to.last().unwrap().address, modifications = ?modifications); - self.data.apply_sieve_modifications(modifications); + for modification in modifications { + if let ScriptModification::SetEnvelope { name, value } = + modification + { + self.data.apply_envelope_modification(name, value); + } + } } } ScriptResult::Reject(message) => { diff --git a/crates/smtp/src/lib.rs b/crates/smtp/src/lib.rs index 29189b3b..0c3c6a00 100644 --- a/crates/smtp/src/lib.rs +++ b/crates/smtp/src/lib.rs @@ -63,6 +63,10 @@ impl SMTP { let mut config_ctx = ConfigContext::new(&servers.inner); config_ctx.directory = directory.clone(); + // Parse remote hosts + config.parse_remote_hosts(&mut config_ctx)?; + + // Add local delivery host #[cfg(feature = "local_delivery")] { config_ctx.hosts.insert( @@ -81,7 +85,7 @@ impl SMTP { ); } - config.parse_remote_hosts(&mut config_ctx)?; + // Parse configuration config.parse_signatures(&mut config_ctx)?; let sieve_config = config.parse_sieve(&mut config_ctx)?; let session_config = config.parse_session_config(&config_ctx)?; diff --git a/crates/smtp/src/scripts/envelope.rs b/crates/smtp/src/scripts/envelope.rs index 084f343c..f119aed4 100644 --- a/crates/smtp/src/scripts/envelope.rs +++ b/crates/smtp/src/scripts/envelope.rs @@ -33,114 +33,112 @@ use crate::{ }; impl SessionData { - pub fn apply_sieve_modifications(&mut self, modifications: Vec<(Envelope, String)>) { - for (envelope, value) in modifications { - match envelope { - Envelope::From => { - let (address, address_lcase, domain) = if value.contains('@') { - let address_lcase = value.to_lowercase(); - let domain = address_lcase.domain_part().to_string(); - (value, address_lcase, domain) - } else if value.is_empty() { - (String::new(), String::new(), String::new()) + pub fn apply_envelope_modification(&mut self, envelope: Envelope, value: String) { + match envelope { + Envelope::From => { + let (address, address_lcase, domain) = if value.contains('@') { + let address_lcase = value.to_lowercase(); + let domain = address_lcase.domain_part().to_string(); + (value, address_lcase, domain) + } else if value.is_empty() { + (String::new(), String::new(), String::new()) + } else { + return; + }; + if let Some(mail_from) = &mut self.mail_from { + mail_from.address = address; + mail_from.address_lcase = address_lcase; + mail_from.domain = domain; + } else { + self.mail_from = SessionAddress { + address, + address_lcase, + domain, + flags: 0, + dsn_info: None, + } + .into(); + } + } + Envelope::To => { + if value.contains('@') { + let address_lcase = value.to_lowercase(); + let domain = address_lcase.domain_part().to_string(); + if let Some(rcpt_to) = self.rcpt_to.last_mut() { + rcpt_to.address = value; + rcpt_to.address_lcase = address_lcase; + rcpt_to.domain = domain; } else { - continue; - }; - if let Some(mail_from) = &mut self.mail_from { - mail_from.address = address; - mail_from.address_lcase = address_lcase; - mail_from.domain = domain; - } else { - self.mail_from = SessionAddress { - address, + self.rcpt_to.push(SessionAddress { + address: value, address_lcase, domain, flags: 0, dsn_info: None, - } - .into(); + }); } } - Envelope::To => { - if value.contains('@') { - let address_lcase = value.to_lowercase(); - let domain = address_lcase.domain_part().to_string(); - if let Some(rcpt_to) = self.rcpt_to.last_mut() { - rcpt_to.address = value; - rcpt_to.address_lcase = address_lcase; - rcpt_to.domain = domain; - } else { - self.rcpt_to.push(SessionAddress { - address: value, - address_lcase, - domain, - flags: 0, - dsn_info: None, - }); - } + } + Envelope::ByMode => { + if let Some(mail_from) = &mut self.mail_from { + mail_from.flags &= !(MAIL_BY_NOTIFY | MAIL_BY_RETURN); + if value == "N" { + mail_from.flags |= MAIL_BY_NOTIFY; + } else if value == "R" { + mail_from.flags |= MAIL_BY_RETURN; } } - Envelope::ByMode => { - if let Some(mail_from) = &mut self.mail_from { - mail_from.flags &= !(MAIL_BY_NOTIFY | MAIL_BY_RETURN); - if value == "N" { - mail_from.flags |= MAIL_BY_NOTIFY; - } else if value == "R" { - mail_from.flags |= MAIL_BY_RETURN; - } + } + Envelope::ByTrace => { + if let Some(mail_from) = &mut self.mail_from { + if value == "T" { + mail_from.flags |= MAIL_BY_TRACE; + } else { + mail_from.flags &= !MAIL_BY_TRACE; } } - Envelope::ByTrace => { - if let Some(mail_from) = &mut self.mail_from { - if value == "T" { - mail_from.flags |= MAIL_BY_TRACE; - } else { - mail_from.flags &= !MAIL_BY_TRACE; - } - } - } - Envelope::Notify => { - if let Some(rcpt_to) = self.rcpt_to.last_mut() { - rcpt_to.flags &= !(RCPT_NOTIFY_DELAY - | RCPT_NOTIFY_FAILURE - | RCPT_NOTIFY_SUCCESS - | RCPT_NOTIFY_NEVER); - if value == "NEVER" { - rcpt_to.flags |= RCPT_NOTIFY_NEVER; - } else { - for value in value.split(',') { - match value.trim() { - "SUCCESS" => rcpt_to.flags |= RCPT_NOTIFY_SUCCESS, - "FAILURE" => rcpt_to.flags |= RCPT_NOTIFY_FAILURE, - "DELAY" => rcpt_to.flags |= RCPT_NOTIFY_DELAY, - _ => (), - } + } + Envelope::Notify => { + if let Some(rcpt_to) = self.rcpt_to.last_mut() { + rcpt_to.flags &= !(RCPT_NOTIFY_DELAY + | RCPT_NOTIFY_FAILURE + | RCPT_NOTIFY_SUCCESS + | RCPT_NOTIFY_NEVER); + if value == "NEVER" { + rcpt_to.flags |= RCPT_NOTIFY_NEVER; + } else { + for value in value.split(',') { + match value.trim() { + "SUCCESS" => rcpt_to.flags |= RCPT_NOTIFY_SUCCESS, + "FAILURE" => rcpt_to.flags |= RCPT_NOTIFY_FAILURE, + "DELAY" => rcpt_to.flags |= RCPT_NOTIFY_DELAY, + _ => (), } } } } - Envelope::Ret => { - if let Some(mail_from) = &mut self.mail_from { - mail_from.flags &= !(MAIL_RET_FULL | MAIL_RET_HDRS); - if value == "FULL" { - mail_from.flags |= MAIL_RET_FULL; - } else if value == "HDRS" { - mail_from.flags |= MAIL_RET_HDRS; - } - } - } - Envelope::Orcpt => { - if let Some(rcpt_to) = self.rcpt_to.last_mut() { - rcpt_to.dsn_info = value.into(); - } - } - Envelope::Envid => { - if let Some(mail_from) = &mut self.mail_from { - mail_from.dsn_info = value.into(); - } - } - Envelope::ByTimeAbsolute | Envelope::ByTimeRelative => (), } + Envelope::Ret => { + if let Some(mail_from) = &mut self.mail_from { + mail_from.flags &= !(MAIL_RET_FULL | MAIL_RET_HDRS); + if value == "FULL" { + mail_from.flags |= MAIL_RET_FULL; + } else if value == "HDRS" { + mail_from.flags |= MAIL_RET_HDRS; + } + } + } + Envelope::Orcpt => { + if let Some(rcpt_to) = self.rcpt_to.last_mut() { + rcpt_to.dsn_info = value.into(); + } + } + Envelope::Envid => { + if let Some(mail_from) = &mut self.mail_from { + mail_from.dsn_info = value.into(); + } + } + Envelope::ByTimeAbsolute | Envelope::ByTimeRelative => (), } } } diff --git a/crates/smtp/src/scripts/event_loop.rs b/crates/smtp/src/scripts/event_loop.rs index 0de4978b..39e4309a 100644 --- a/crates/smtp/src/scripts/event_loop.rs +++ b/crates/smtp/src/scripts/event_loop.rs @@ -40,7 +40,7 @@ use crate::{ queue::{DomainPart, InstantFromTimestamp, Message}, }; -use super::{plugins::PluginContext, ScriptParameters, ScriptResult}; +use super::{plugins::PluginContext, ScriptModification, ScriptParameters, ScriptResult}; impl SMTP { pub fn run_script_blocking( @@ -122,6 +122,7 @@ impl SMTP { handle: &handle, core: self, message: instance.message(), + modifications: &mut modifications, arguments, }, ); @@ -315,7 +316,10 @@ impl SMTP { input = true.into(); } Event::SetEnvelope { envelope, value } => { - modifications.push((envelope, value)); + modifications.push(ScriptModification::SetEnvelope { + name: envelope, + value, + }); input = true.into(); } unsupported => { diff --git a/crates/smtp/src/scripts/mod.rs b/crates/smtp/src/scripts/mod.rs index c53b686d..e461f1f8 100644 --- a/crates/smtp/src/scripts/mod.rs +++ b/crates/smtp/src/scripts/mod.rs @@ -35,16 +35,28 @@ pub mod plugins; #[derive(Debug)] pub enum ScriptResult { Accept { - modifications: Vec<(Envelope, String)>, + modifications: Vec, }, Replace { message: Vec, - modifications: Vec<(Envelope, String)>, + modifications: Vec, }, Reject(String), Discard, } +#[derive(Debug)] +pub enum ScriptModification { + SetEnvelope { + name: Envelope, + value: String, + }, + AddHeader { + name: Arc, + value: Arc, + }, +} + pub struct ScriptParameters { message: Option>>, variables: AHashMap, Variable>, diff --git a/crates/smtp/src/scripts/plugins/headers.rs b/crates/smtp/src/scripts/plugins/headers.rs new file mode 100644 index 00000000..0160556c --- /dev/null +++ b/crates/smtp/src/scripts/plugins/headers.rs @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2023 Stalwart Labs Ltd. + * + * This file is part of Stalwart Mail Server. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of + * the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * in the LICENSE file at the top-level directory of this distribution. + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + * You can be released from the requirements of the AGPLv3 license by + * purchasing a commercial license. Please contact licensing@stalw.art + * for more details. +*/ + +use sieve::{runtime::Variable, FunctionMap}; + +use crate::{config::scripts::SieveContext, scripts::ScriptModification}; + +use super::PluginContext; + +pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) { + fnc_map.set_external_function("add_header", plugin_id, 2); +} + +pub fn exec(ctx: PluginContext<'_>) -> Variable { + if let (Variable::String(name), Variable::String(value)) = + (&ctx.arguments[0], &ctx.arguments[1]) + { + ctx.modifications.push(ScriptModification::AddHeader { + name: name.clone(), + value: value.clone(), + }); + true + } else { + false + } + .into() +} diff --git a/crates/smtp/src/scripts/plugins/mod.rs b/crates/smtp/src/scripts/plugins/mod.rs index 778907e0..5099913e 100644 --- a/crates/smtp/src/scripts/plugins/mod.rs +++ b/crates/smtp/src/scripts/plugins/mod.rs @@ -24,6 +24,7 @@ pub mod bayes; pub mod dns; pub mod exec; +pub mod headers; pub mod http; pub mod lookup; pub mod pyzor; @@ -35,6 +36,8 @@ use tokio::runtime::Handle; use crate::{config::scripts::SieveContext, core::SMTP}; +use super::ScriptModification; + type RegisterPluginFnc = fn(u32, &mut FunctionMap) -> (); type ExecPluginFnc = fn(PluginContext<'_>) -> Variable; @@ -43,10 +46,11 @@ pub struct PluginContext<'x> { pub handle: &'x Handle, pub core: &'x SMTP, pub message: &'x Message<'x>, + pub modifications: &'x mut Vec, pub arguments: Vec, } -const PLUGINS_EXEC: [ExecPluginFnc; 14] = [ +const PLUGINS_EXEC: [ExecPluginFnc; 15] = [ query::exec, exec::exec, lookup::exec, @@ -61,8 +65,9 @@ const PLUGINS_EXEC: [ExecPluginFnc; 14] = [ bayes::exec_classify, bayes::exec_is_balanced, pyzor::exec, + headers::exec, ]; -const PLUGINS_REGISTER: [RegisterPluginFnc; 14] = [ +const PLUGINS_REGISTER: [RegisterPluginFnc; 15] = [ query::register, exec::register, lookup::register, @@ -77,6 +82,7 @@ const PLUGINS_REGISTER: [RegisterPluginFnc; 14] = [ bayes::register_classify, bayes::register_is_balanced, pyzor::register, + headers::register, ]; pub trait RegisterSievePlugins { diff --git a/crates/utils/src/config/mod.rs b/crates/utils/src/config/mod.rs index 7e2076a0..5660f7d3 100644 --- a/crates/utils/src/config/mod.rs +++ b/crates/utils/src/config/mod.rs @@ -36,6 +36,7 @@ use std::{ time::Duration, }; +use ahash::{AHashMap, AHashSet}; use rustls::ServerConfig; use tokio::net::TcpSocket; @@ -43,7 +44,7 @@ use crate::{failed, UnwrapFailure}; use self::utils::ParseValue; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Config { pub keys: BTreeMap, } @@ -164,12 +165,87 @@ impl Config { } } - Config::parse( - &std::fs::read_to_string( - config_path.failed("Missing parameter --config=."), + // Read main configuration file + let mut config = Config::default(); + config + .parse( + &std::fs::read_to_string( + config_path.failed("Missing parameter --config=."), + ) + .failed("Could not read configuration file"), ) - .failed("Could not read configuration file"), - ) - .failed("Invalid configuration file") + .failed("Invalid configuration file"); + + // Extract macros and includes + let mut keys = BTreeMap::new(); + let mut includes = AHashSet::new(); + let mut macros = AHashMap::new(); + + for (key, mut value) in config.keys { + value.replace_macros(&key, ¯os); + if let Some(macro_name) = key.strip_prefix("macro.") { + macros.insert(macro_name.to_ascii_lowercase(), value); + } else if key.starts_with("include.files.") { + includes.insert(value); + } else { + keys.insert(key, value); + } + } + + // Include files + config.keys = keys; + for include in includes { + config + .parse( + &std::fs::read_to_string(include) + .failed("Could not read included configuration file"), + ) + .failed("Invalid included configuration file"); + } + + // Replace macros + for (key, value) in &mut config.keys { + value.replace_macros(key, ¯os); + } + + config + } +} + +trait ReplaceMacros: Sized { + fn replace_macros(&mut self, key: &str, macros: &AHashMap); +} + +impl ReplaceMacros for String { + fn replace_macros(&mut self, key: &str, macros: &AHashMap) { + if self.contains("%{") { + let mut result = String::with_capacity(self.len()); + let mut value = self.as_str(); + + loop { + if let Some((suffix, macro_name)) = value.split_once("%{") { + if !suffix.is_empty() { + result.push_str(suffix); + } + if let Some((macro_name, rest)) = macro_name.split_once('}') { + if let Some(macro_value) = macros.get(¯o_name.to_ascii_lowercase()) { + result.push_str(macro_value); + value = rest; + } else { + failed(&format!("Unknown macro {macro_name:?} for key {key:?}")); + } + } else { + failed(&format!( + "Unterminated macro name {value:?} for key {key:?}" + )); + } + } else { + result.push_str(value); + break; + } + } + + *self = result; + } } } diff --git a/crates/utils/src/config/parser.rs b/crates/utils/src/config/parser.rs index f328cc5e..b29c42b1 100644 --- a/crates/utils/src/config/parser.rs +++ b/crates/utils/src/config/parser.rs @@ -35,8 +35,14 @@ const MAX_NEST_LEVEL: usize = 10; // Simple TOML parser for Stalwart Mail Server configuration files. impl Config { - pub fn parse(toml: &str) -> Result { - let mut parser = TomlParser::new(toml); + pub fn new(toml: &str) -> Result { + let mut config = Config::default(); + config.parse(toml)?; + Ok(config) + } + + pub fn parse(&mut self, toml: &str) -> Result<()> { + let mut parser = TomlParser::new(&mut self.keys, toml); let mut table_name = String::new(); let mut last_array_name = String::new(); let mut last_array_pos = 0; @@ -77,7 +83,7 @@ impl Config { last_array_name = table_name.to_string(); } is_array = false; - write!(table_name, ".{last_array_pos}").ok(); + write!(table_name, ".{last_array_pos:04}").ok(); } else { break; } @@ -122,20 +128,20 @@ impl Config { } } - Ok(Self { keys: parser.keys }) + Ok(()) } } -struct TomlParser<'x> { - keys: BTreeMap, +struct TomlParser<'x, 'y> { + keys: &'y mut BTreeMap, iter: Peekable>, line: usize, } -impl<'x> TomlParser<'x> { - fn new(toml: &'x str) -> Self { +impl<'x, 'y> TomlParser<'x, 'y> { + fn new(keys: &'y mut BTreeMap, toml: &'x str) -> Self { Self { - keys: BTreeMap::new(), + keys, iter: toml.chars().peekable(), line: 1, } @@ -256,7 +262,11 @@ impl<'x> TomlParser<'x> { let mut array_pos = 0; self.seek_next_char(); loop { - match self.value(format!("{key}.{array_pos}"), &[',', ']'], nest_level + 1)? { + match self.value( + format!("{key}.{array_pos:04}"), + &[',', ']'], + nest_level + 1, + )? { ',' => { self.seek_next_char(); array_pos += 1; @@ -340,19 +350,8 @@ impl<'x> TomlParser<'x> { last_ch = ch; } } - match self.keys.entry(key) { - Entry::Vacant(e) => { - value.shrink_to_fit(); - e.insert(value); - } - Entry::Occupied(e) => { - return Err(format!( - "Duplicate key {:?} at line {}.", - e.key(), - self.line - )); - } - } + + self.insert_key(key, value)?; } ch if ch.is_alphanumeric() || ['.', '+', '-'].contains(&ch) => { let mut value = String::with_capacity(4); @@ -364,19 +363,7 @@ impl<'x> TomlParser<'x> { break; } } - match self.keys.entry(key) { - Entry::Vacant(e) => { - value.shrink_to_fit(); - e.insert(value); - } - Entry::Occupied(e) => { - return Err(format!( - "Duplicate key {:?} at line {}.", - e.key(), - self.line - )); - } - } + self.insert_key(key, value)?; } '!' => { let mut value = String::with_capacity(4); @@ -394,18 +381,7 @@ impl<'x> TomlParser<'x> { String::new() } }; - match self.keys.entry(key) { - Entry::Vacant(e) => { - e.insert(value); - } - Entry::Occupied(e) => { - return Err(format!( - "Duplicate key {:?} at line {}.", - e.key(), - self.line - )); - } - } + self.insert_key(key, value)?; } ch => { return if stop_chars.contains(&ch) { @@ -440,6 +416,21 @@ impl<'x> TomlParser<'x> { } } } + + fn insert_key(&mut self, key: String, mut value: String) -> Result<()> { + match self.keys.entry(key) { + Entry::Vacant(e) => { + value.shrink_to_fit(); + e.insert(value); + Ok(()) + } + Entry::Occupied(e) => Err(format!( + "Duplicate key {:?} at line {}.", + e.key(), + self.line + )), + } + } } #[cfg(test)] @@ -462,121 +453,171 @@ mod tests { .join("config") .join("toml-parser.toml"); - let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap(); - assert_eq!( - config.keys, - BTreeMap::from_iter([ - ("arrays.colors.0".to_string(), "red".to_string()), - ("arrays.colors.1".to_string(), "yellow".to_string()), - ("arrays.colors.2".to_string(), "green".to_string()), - ( - "arrays.contributors.0".to_string(), - "Foo Bar ".to_string() - ), - ( - "arrays.contributors.1.email".to_string(), - "bazqux@example.com".to_string() - ), - ( - "arrays.contributors.1.name".to_string(), - "Baz Qux".to_string() - ), - ( - "arrays.contributors.1.url".to_string(), - "https://example.com/bazqux".to_string() - ), - ("arrays.integers.0".to_string(), "1".to_string()), - ("arrays.integers.1".to_string(), "2".to_string()), - ("arrays.integers.2".to_string(), "3".to_string()), - ("arrays.integers2.0".to_string(), "1".to_string()), - ("arrays.integers2.1".to_string(), "2".to_string()), - ("arrays.integers2.2".to_string(), "3".to_string()), - ("arrays.integers3.0".to_string(), "4".to_string()), - ("arrays.integers3.1".to_string(), "5".to_string()), - ( - "arrays.nested_arrays_of_ints.0.0".to_string(), - "1".to_string() - ), - ( - "arrays.nested_arrays_of_ints.0.1".to_string(), - "2".to_string() - ), - ( - "arrays.nested_arrays_of_ints.1.0".to_string(), - "3".to_string() - ), - ( - "arrays.nested_arrays_of_ints.1.1".to_string(), - "4".to_string() - ), - ( - "arrays.nested_arrays_of_ints.1.2".to_string(), - "5".to_string() - ), - ("arrays.nested_mixed_array.0.0".to_string(), "1".to_string()), - ("arrays.nested_mixed_array.0.1".to_string(), "2".to_string()), - ("arrays.nested_mixed_array.1.0".to_string(), "a".to_string()), - ("arrays.nested_mixed_array.1.1".to_string(), "b".to_string()), - ("arrays.nested_mixed_array.1.2".to_string(), "c".to_string()), - ("arrays.numbers.0".to_string(), "0.1".to_string()), - ("arrays.numbers.1".to_string(), "0.2".to_string()), - ("arrays.numbers.2".to_string(), "0.5".to_string()), - ("arrays.numbers.3".to_string(), "1".to_string()), - ("arrays.numbers.4".to_string(), "2".to_string()), - ("arrays.numbers.5".to_string(), "5".to_string()), - ("arrays.string_array.0".to_string(), "all".to_string()), - ("arrays.string_array.1".to_string(), "strings".to_string()), - ( - "arrays.string_array.2".to_string(), - "are the same".to_string() - ), - ("arrays.string_array.3".to_string(), "type".to_string()), - ("database.data.0.0".to_string(), "delta".to_string()), - ("database.data.0.1".to_string(), "phi".to_string()), - ("database.data.1.0".to_string(), "3.14".to_string()), - ("database.enabled".to_string(), "true".to_string()), - ("database.ports.0".to_string(), "8000".to_string()), - ("database.ports.1".to_string(), "8001".to_string()), - ("database.ports.2".to_string(), "8002".to_string()), - ("database.temp_targets.case".to_string(), "72.0".to_string()), - ("database.temp_targets.cpu".to_string(), "79.5".to_string()), - ("products.0.name".to_string(), "Hammer".to_string()), - ("products.0.sku".to_string(), "738594937".to_string()), - ("products.2.color".to_string(), "gray".to_string()), - ("products.2.name".to_string(), "Nail".to_string()), - ("products.2.sku".to_string(), "284758393".to_string()), - ("servers.127.0.0.1".to_string(), "value".to_string()), - ("servers.alpha.ip".to_string(), "10.0.0.1".to_string()), - ("servers.alpha.role".to_string(), "frontend".to_string()), - ("servers.beta.ip".to_string(), "10.0.0.2".to_string()), - ("servers.beta.role".to_string(), "backend".to_string()), - ( - "servers.character encoding".to_string(), - "value".to_string() - ), - ( - "strings.my \"string\" test.lines".to_string(), - concat!( - "The first newline is\ntrimmed in raw strings.\n", - "All other whitespace\nis preserved.\n" - ) - .to_string() - ), - ( - "strings.my \"string\" test.str1".to_string(), - "I'm a string.".to_string() - ), - ( - "strings.my \"string\" test.str2".to_string(), - "You can \"quote\" me.".to_string() - ), - ( - "strings.my \"string\" test.str3".to_string(), - "Name\tTabs\nNew Line.".to_string() - ), - ("env.var1".to_string(), "utils".to_string()), - ("env.var2".to_string(), "utils".to_string()), - ]) - ); + let mut config = Config::default(); + config.parse(&fs::read_to_string(file).unwrap()).unwrap(); + let expected = BTreeMap::from_iter([ + ("arrays.colors.0000".to_string(), "red".to_string()), + ("arrays.colors.0001".to_string(), "yellow".to_string()), + ("arrays.colors.0002".to_string(), "green".to_string()), + ( + "arrays.contributors.0000".to_string(), + "Foo Bar ".to_string(), + ), + ( + "arrays.contributors.0001.email".to_string(), + "bazqux@example.com".to_string(), + ), + ( + "arrays.contributors.0001.name".to_string(), + "Baz Qux".to_string(), + ), + ( + "arrays.contributors.0001.url".to_string(), + "https://example.com/bazqux".to_string(), + ), + ("arrays.integers.0000".to_string(), "1".to_string()), + ("arrays.integers.0001".to_string(), "2".to_string()), + ("arrays.integers.0002".to_string(), "3".to_string()), + ("arrays.integers2.0000".to_string(), "1".to_string()), + ("arrays.integers2.0001".to_string(), "2".to_string()), + ("arrays.integers2.0002".to_string(), "3".to_string()), + ("arrays.integers3.0000".to_string(), "4".to_string()), + ("arrays.integers3.0001".to_string(), "5".to_string()), + ( + "arrays.nested_arrays_of_ints.0000.0000".to_string(), + "1".to_string(), + ), + ( + "arrays.nested_arrays_of_ints.0000.0001".to_string(), + "2".to_string(), + ), + ( + "arrays.nested_arrays_of_ints.0001.0000".to_string(), + "3".to_string(), + ), + ( + "arrays.nested_arrays_of_ints.0001.0001".to_string(), + "4".to_string(), + ), + ( + "arrays.nested_arrays_of_ints.0001.0002".to_string(), + "5".to_string(), + ), + ( + "arrays.nested_mixed_array.0000.0000".to_string(), + "1".to_string(), + ), + ( + "arrays.nested_mixed_array.0000.0001".to_string(), + "2".to_string(), + ), + ( + "arrays.nested_mixed_array.0001.0000".to_string(), + "a".to_string(), + ), + ( + "arrays.nested_mixed_array.0001.0001".to_string(), + "b".to_string(), + ), + ( + "arrays.nested_mixed_array.0001.0002".to_string(), + "c".to_string(), + ), + ("arrays.numbers.0000".to_string(), "0.1".to_string()), + ("arrays.numbers.0001".to_string(), "0.2".to_string()), + ("arrays.numbers.0002".to_string(), "0.5".to_string()), + ("arrays.numbers.0003".to_string(), "1".to_string()), + ("arrays.numbers.0004".to_string(), "2".to_string()), + ("arrays.numbers.0005".to_string(), "5".to_string()), + ("arrays.string_array.0000".to_string(), "all".to_string()), + ( + "arrays.string_array.0001".to_string(), + "strings".to_string(), + ), + ( + "arrays.string_array.0002".to_string(), + "are the same".to_string(), + ), + ("arrays.string_array.0003".to_string(), "type".to_string()), + ("database.data.0000.0000".to_string(), "delta".to_string()), + ("database.data.0000.0001".to_string(), "phi".to_string()), + ("database.data.0001.0000".to_string(), "3.14".to_string()), + ("database.enabled".to_string(), "true".to_string()), + ("database.ports.0000".to_string(), "8000".to_string()), + ("database.ports.0001".to_string(), "8001".to_string()), + ("database.ports.0002".to_string(), "8002".to_string()), + ("database.temp_targets.case".to_string(), "72.0".to_string()), + ("database.temp_targets.cpu".to_string(), "79.5".to_string()), + ("products.0000.name".to_string(), "Hammer".to_string()), + ("products.0000.sku".to_string(), "738594937".to_string()), + ("products.0002.color".to_string(), "gray".to_string()), + ("products.0002.name".to_string(), "Nail".to_string()), + ("products.0002.sku".to_string(), "284758393".to_string()), + ("servers.127.0.0.1".to_string(), "value".to_string()), + ("servers.alpha.ip".to_string(), "10.0.0.1".to_string()), + ("servers.alpha.role".to_string(), "frontend".to_string()), + ("servers.beta.ip".to_string(), "10.0.0.2".to_string()), + ("servers.beta.role".to_string(), "backend".to_string()), + ( + "servers.character encoding".to_string(), + "value".to_string(), + ), + ( + "strings.my \"string\" test.lines".to_string(), + concat!( + "The first newline is\ntrimmed in raw strings.\n", + "All other whitespace\nis preserved.\n" + ) + .to_string(), + ), + ( + "strings.my \"string\" test.str1".to_string(), + "I'm a string.".to_string(), + ), + ( + "strings.my \"string\" test.str2".to_string(), + "You can \"quote\" me.".to_string(), + ), + ( + "strings.my \"string\" test.str3".to_string(), + "Name\tTabs\nNew Line.".to_string(), + ), + ("env.var1".to_string(), "utils".to_string()), + ("env.var2".to_string(), "utils".to_string()), + ]); + + if config.keys != expected { + for (key, value) in &config.keys { + if let Some(expected_value) = expected.get(key) { + if value != expected_value { + panic!( + "Expected value {:?} for key {:?} but found {:?}.", + expected_value, key, value + ); + } + } else { + panic!( + "Unexpected key {:?} found in config with value {:?}.", + key, value + ); + } + } + + for (key, value) in &expected { + if let Some(config_value) = config.keys.get(key) { + if value != config_value { + panic!( + "Expected value {:?} for key {:?} but found {:?}.", + value, key, config_value + ); + } + } else { + panic!( + "Expected key {:?} not found in config with value {:?}.", + key, value + ); + } + } + } } } diff --git a/crates/utils/src/config/utils.rs b/crates/utils/src/config/utils.rs index 38fe31d3..70bd2b80 100644 --- a/crates/utils/src/config/utils.rs +++ b/crates/utils/src/config/utils.rs @@ -117,6 +117,10 @@ impl Config { self.keys.get(&key.as_key()).map(|s| s.as_str()) } + pub fn contains_key(&self, key: impl AsKey) -> bool { + self.keys.contains_key(&key.as_key()) + } + pub fn value_require(&self, key: impl AsKey) -> super::Result<&str> { self.keys .get(&key.as_key()) @@ -652,7 +656,8 @@ idle = 20 hostname = "submit.example.org" ip = "a:b::1:1" "#; - let config = Config::parse(toml).unwrap(); + let mut config = Config::default(); + config.parse(toml).unwrap(); assert_eq!( config.sub_keys("queues").collect::>(), @@ -664,11 +669,11 @@ ip = "a:b::1:1" ); assert_eq!( config.sub_keys("queues.z.retry").collect::>(), - ["0", "1", "2", "3", "4"] + ["0000", "0001", "0002", "0003", "0004"] ); assert_eq!( config - .property::("servers.my relay.transaction.auth.limits.1.idle") + .property::("servers.my relay.transaction.auth.limits.0001.idle") .unwrap() .unwrap(), 20 diff --git a/resources/config/common/server.toml b/resources/config/common/server.toml index 0654b83a..36f0b260 100644 --- a/resources/config/common/server.toml +++ b/resources/config/common/server.toml @@ -3,7 +3,7 @@ ############################################# [server] -hostname = "__HOST__" +hostname = "%{HOST}%" max-connections = 8192 [server.run-as] diff --git a/resources/config/common/tracing.toml b/resources/config/common/tracing.toml index d2eb07dd..73b1c0d8 100644 --- a/resources/config/common/tracing.toml +++ b/resources/config/common/tracing.toml @@ -15,7 +15,7 @@ [global.tracing] method = "log" -path = "__PATH__/logs" +path = "%{BASE_PATH}%/logs" prefix = "stalwart.log" rotate = "daily" level = "info" diff --git a/resources/config/config.toml b/resources/config/config.toml new file mode 100644 index 00000000..5f6183b1 --- /dev/null +++ b/resources/config/config.toml @@ -0,0 +1,36 @@ +############################################# +# Stalwart Mail Server Configuration File +############################################# + +[macros] +host = "__HOST__" +default_domain = "__DOMAIN__" +base_path = "__BASE_PATH__" + +[include] +files = [ "%{BASE_PATH}%/etc/common/server.toml", + "%{BASE_PATH}%/etc/common/tls.toml", + "%{BASE_PATH}%/etc/common/tracing.toml", + "%{BASE_PATH}%/etc/directory/sql.toml", + "%{BASE_PATH}%/etc/imap/listener.toml", + "%{BASE_PATH}%/etc/imap/settings.toml", + "%{BASE_PATH}%/etc/jmap/auth.toml", + "%{BASE_PATH}%/etc/jmap/listener.toml", + "%{BASE_PATH}%/etc/jmap/oauth.toml", + "%{BASE_PATH}%/etc/jmap/protocol.toml", + "%{BASE_PATH}%/etc/jmap/push.toml", + "%{BASE_PATH}%/etc/jmap/ratelimit.toml", + "%{BASE_PATH}%/etc/jmap/sieve.toml", + "%{BASE_PATH}%/etc/jmap/store.toml", + "%{BASE_PATH}%/etc/jmap/websockets.toml", + "%{BASE_PATH}%/etc/smtp/auth.toml", + "%{BASE_PATH}%/etc/smtp/listener.toml", + "%{BASE_PATH}%/etc/smtp/milter.toml", + "%{BASE_PATH}%/etc/smtp/queue.toml", + "%{BASE_PATH}%/etc/smtp/remote.toml", + "%{BASE_PATH}%/etc/smtp/report.toml", + "%{BASE_PATH}%/etc/smtp/resolver.toml", + "%{BASE_PATH}%/etc/smtp/session.toml", + "%{BASE_PATH}%/etc/smtp/sieve.toml", + "%{BASE_PATH}%/etc/smtp/signature.toml", + "%{BASE_PATH}%/etc/smtp/spamfilter.toml" ] diff --git a/resources/config/directory/imap.toml b/resources/config/directory/imap.toml index a7441c4b..41a303ee 100644 --- a/resources/config/directory/imap.toml +++ b/resources/config/directory/imap.toml @@ -1,5 +1,5 @@ ############################################# -# Directory configuration +# IMAP Directory configuration ############################################# [directory."imap"] @@ -23,5 +23,5 @@ entries = 500 ttl = {positive = '1h', negative = '10m'} [directory."imap".lookup] -domains = ["__DOMAIN__"] +domains = ["%{DEFAULT_DOMAIN}%"] diff --git a/resources/config/directory/ldap.toml b/resources/config/directory/ldap.toml index 63bf5fe3..a08cd4d8 100644 --- a/resources/config/directory/ldap.toml +++ b/resources/config/directory/ldap.toml @@ -1,5 +1,5 @@ ############################################# -# Directory configuration +# LDAP Directory configuration ############################################# [directory."default"] diff --git a/resources/config/directory/lmtp.toml b/resources/config/directory/lmtp.toml index 824c9169..c88aa253 100644 --- a/resources/config/directory/lmtp.toml +++ b/resources/config/directory/lmtp.toml @@ -1,5 +1,5 @@ ############################################# -# Directory configuration +# LMTP Directory configuration ############################################# [directory."lmtp"] @@ -27,5 +27,5 @@ entries = 500 ttl = {positive = '1h', negative = '10m'} [directory."lmtp".lookup] -domains = ["__DOMAIN__"] +domains = ["%{DEFAULT_DOMAIN}%"] diff --git a/resources/config/directory/memory.toml b/resources/config/directory/memory.toml index 18b9e37e..a5fe97ea 100644 --- a/resources/config/directory/memory.toml +++ b/resources/config/directory/memory.toml @@ -1,5 +1,5 @@ ############################################# -# Directory configuration +# In-Memory Directory configuration ############################################# [directory."default"] @@ -16,15 +16,15 @@ superuser-group = "superusers" name = "admin" description = "Superuser" secret = "changeme" -email = ["postmaster@__DOMAIN__"] +email = ["postmaster@%{DEFAULT_DOMAIN}%"] member-of = ["superusers"] [[directory."default".users]] name = "jane" description = "Jane Doe" secret = "abcde" -email = ["jane@__DOMAIN__", "jane.doe@__DOMAIN__"] -email-list = ["info@__DOMAIN__"] +email = ["jane@%{DEFAULT_DOMAIN}%", "jane.doe@%{DEFAULT_DOMAIN}%"] +email-list = ["info@%{DEFAULT_DOMAIN}%"] member-of = ["sales", "support"] [[directory."default".users]] @@ -32,8 +32,8 @@ name = "bill" description = "Bill Foobar" secret = "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe" quota = 50000000 -email = ["bill@__DOMAIN__", "bill.foobar@__DOMAIN__"] -email-list = ["info@__DOMAIN__"] +email = ["bill@%{DEFAULT_DOMAIN}%", "bill.foobar@%{DEFAULT_DOMAIN}%"] +email-list = ["info@%{DEFAULT_DOMAIN}%"] [[directory."default".groups]] name = "sales" @@ -44,4 +44,4 @@ name = "support" description = "Support Team" [directory."default".lookup] -domains = ["__DOMAIN__"] +domains = ["%{DEFAULT_DOMAIN}%"] diff --git a/resources/config/directory/sql.toml b/resources/config/directory/sql.toml index 54217283..da8dd85a 100644 --- a/resources/config/directory/sql.toml +++ b/resources/config/directory/sql.toml @@ -1,10 +1,10 @@ ############################################# -# Directory configuration +# SQL Directory configuration ############################################# [directory."default"] type = "sql" -address = "sqlite://__PATH__/data/accounts.sqlite3?mode=rwc" +address = "sqlite://%{BASE_PATH}%/data/accounts.sqlite3?mode=rwc" [directory."default".options] catch-all = true diff --git a/resources/config/jmap/listener.toml b/resources/config/jmap/listener.toml index 86b91e07..619fc4db 100644 --- a/resources/config/jmap/listener.toml +++ b/resources/config/jmap/listener.toml @@ -4,5 +4,5 @@ [server.listener."jmap"] bind = ["[::]:8080"] -url = "https://__HOST__:8080" +url = "https://%{HOST}%:8080" protocol = "jmap" diff --git a/resources/config/jmap/sieve.toml b/resources/config/jmap/sieve.toml index fda509ec..67d1d21e 100644 --- a/resources/config/jmap/sieve.toml +++ b/resources/config/jmap/sieve.toml @@ -2,12 +2,12 @@ # JMAP Sieve interpreter configuration ############################################# -[jmap.sieve] +[sieve.jmap] disable-capabilities = [] notification-uris = ["mailto"] protected-headers = ["Original-Subject", "Original-From", "Received", "Auto-Submitted"] -[jmap.sieve.limits] +[sieve.jmap.limits] name-length = 512 max-scripts = 256 script-size = 102400 @@ -27,10 +27,10 @@ redirects = 1 received-headers = 10 outgoing-messages = 3 -[jmap.sieve.vacation] +[sieve.jmap.vacation] default-subject = "Automated reply" subject-prefix = "Auto: " -[jmap.sieve.default-expiry] +[sieve.jmap.default-expiry] vacation = "30d" duplicate = "7d" diff --git a/resources/config/jmap/store.toml b/resources/config/jmap/store.toml index 0dd5ab8d..0e436e65 100644 --- a/resources/config/jmap/store.toml +++ b/resources/config/jmap/store.toml @@ -3,7 +3,7 @@ ############################################# [store.db] -path = "__PATH__/data/index.sqlite3" +path = "%{BASE_PATH}%/data/index.sqlite3" [store.db.pool] max-connections = 10 @@ -13,10 +13,10 @@ max-connections = 10 size = 1000 [store.blob] -type = "__BLOB_STORE__" +type = "local" [store.blob.local] -path = "__PATH__/data/blobs" +path = "%{BASE_PATH}%/data/blobs" [store.blob.s3] bucket = "stalwart" diff --git a/resources/config/macros.toml b/resources/config/macros.toml deleted file mode 100644 index 61d99244..00000000 --- a/resources/config/macros.toml +++ /dev/null @@ -1,8 +0,0 @@ -############################################# -# Configuration file macros -############################################# - -[macros] -default_domain = "__HOST__" -base_path = "__BASE_PATH__" - diff --git a/resources/config/smtp/queue.toml b/resources/config/smtp/queue.toml index f1f4141c..230461e8 100644 --- a/resources/config/smtp/queue.toml +++ b/resources/config/smtp/queue.toml @@ -3,7 +3,7 @@ ############################################# [queue] -path = "__PATH__/queue" +path = "%{BASE_PATH}%/queue" hash = 64 [queue.schedule] @@ -12,8 +12,8 @@ notify = ["1d", "3d"] expire = "5d" [queue.outbound] -#hostname = "__HOST__" -next-hop = [ { if = "rcpt-domain", in-list = "__SMTP_DIRECTORY__/domains", then = "__NEXT_HOP__" }, +#hostname = "%{HOST}%" +next-hop = [ { if = "rcpt-domain", in-list = "default/domains", then = "local" }, { else = false } ] ip-strategy = "ipv4-then-ipv6" diff --git a/resources/config/smtp/remote.toml b/resources/config/smtp/remote.toml index 29d8d09b..a159725f 100644 --- a/resources/config/smtp/remote.toml +++ b/resources/config/smtp/remote.toml @@ -2,18 +2,17 @@ # SMTP remote servers configuration ############################################# -[remote."lmtp"] +[remote."local"] address = "127.0.0.1" port = 11200 -protocol = "lmtp" +protocol = "local" concurrency = 10 timeout = "1m" -[remote."lmtp".tls] +[remote."local".tls] implicit = false allow-invalid-certs = true -#[remote."lmtp".auth] +#[remote."local".auth] #username = "" #secret = "" - diff --git a/resources/config/smtp/report.toml b/resources/config/smtp/report.toml index 3fecc555..39fea895 100644 --- a/resources/config/smtp/report.toml +++ b/resources/config/smtp/report.toml @@ -3,45 +3,45 @@ ############################################# [report] -path = "__PATH__/reports" +path = "%{BASE_PATH}%/reports" hash = 64 -#submitter = "__HOST__" +#submitter = "%{HOST}%" [report.analysis] addresses = ["dmarc@*", "abuse@*", "postmaster@*"] forward = true -#store = "__PATH__/incoming" +#store = "%{BASE_PATH}%/incoming" [report.dsn] from-name = "Mail Delivery Subsystem" -from-address = "MAILER-DAEMON@__DOMAIN__" +from-address = "MAILER-DAEMON@%{DEFAULT_DOMAIN}%" sign = ["rsa"] [report.dkim] from-name = "Report Subsystem" -from-address = "noreply-dkim@__DOMAIN__" +from-address = "noreply-dkim@%{DEFAULT_DOMAIN}%" subject = "DKIM Authentication Failure Report" sign = ["rsa"] send = "1/1d" [report.spf] from-name = "Report Subsystem" -from-address = "noreply-spf@__DOMAIN__" +from-address = "noreply-spf@%{DEFAULT_DOMAIN}%" subject = "SPF Authentication Failure Report" send = "1/1d" sign = ["rsa"] [report.dmarc] from-name = "Report Subsystem" -from-address = "noreply-dmarc@__DOMAIN__" +from-address = "noreply-dmarc@%{DEFAULT_DOMAIN}%" subject = "DMARC Authentication Failure Report" send = "1/1d" sign = ["rsa"] [report.dmarc.aggregate] from-name = "DMARC Report" -from-address = "noreply-dmarc@__DOMAIN__" -org-name = "__DOMAIN__" +from-address = "noreply-dmarc@%{DEFAULT_DOMAIN}%" +org-name = "%{DEFAULT_DOMAIN}%" #contact-info = "" send = "daily" max-size = 26214400 # 25mb @@ -49,8 +49,8 @@ sign = ["rsa"] [report.tls.aggregate] from-name = "TLS Report" -from-address = "noreply-tls@__DOMAIN__" -org-name = "__DOMAIN__" +from-address = "noreply-tls@%{DEFAULT_DOMAIN}%" +org-name = "%{DEFAULT_DOMAIN}%" #contact-info = "" send = "daily" max-size = 26214400 # 25 mb diff --git a/resources/config/smtp/resolver.toml b/resources/config/smtp/resolver.toml index 3f48a104..af2bdb37 100644 --- a/resources/config/smtp/resolver.toml +++ b/resources/config/smtp/resolver.toml @@ -10,7 +10,7 @@ timeout = "5s" attempts = 2 try-tcp-on-error = true public-suffix = ["https://publicsuffix.org/list/public_suffix_list.dat", - "file://%%{BASE_PATH}%%/etc/spamfilter/maps/suffix_list.dat.gz"] + "file://%{BASE_PATH}%/etc/spamfilter/maps/suffix_list.dat.gz"] [resolver.cache] txt = 2048 @@ -20,4 +20,3 @@ ipv6 = 1024 ptr = 1024 tlsa = 1024 mta-sts = 1024 - diff --git a/resources/config/smtp/session.toml b/resources/config/smtp/session.toml index a11e7c95..d67d30c7 100644 --- a/resources/config/smtp/session.toml +++ b/resources/config/smtp/session.toml @@ -37,7 +37,7 @@ mt-priority = [ { if = "authenticated-as", ne = "", then = "mixer"}, [session.auth] mechanisms = [ { if = "listener", ne = "smtp", then = ["plain", "login"]}, { else = [] } ] -directory = [ { if = "listener", ne = "smtp", then = "__SMTP_DIRECTORY__" }, +directory = [ { if = "listener", ne = "smtp", then = "default" }, { else = false } ] require = [ { if = "listener", ne = "smtp", then = true}, { else = false } ] @@ -58,12 +58,12 @@ wait = "5s" #script = "greylist" relay = [ { if = "authenticated-as", ne = "", then = true }, { else = false } ] -#rewrite = [ { all-of = [ { if = "rcpt-domain", in-list = "__SMTP_DIRECTORY__/domains" }, +#rewrite = [ { all-of = [ { if = "rcpt-domain", in-list = "default/domains" }, # { if = "rcpt", matches = "^([^.]+)\.([^.]+)@(.+)$"}, # ], then = "${1}+${2}@${3}" }, # { else = false } ] max-recipients = 25 -directory = "__SMTP_DIRECTORY__" +directory = "default" [session.rcpt.errors] total = 5 diff --git a/resources/config/smtp/sieve.toml b/resources/config/smtp/sieve.toml index 7c725d3c..68664bf3 100644 --- a/resources/config/smtp/sieve.toml +++ b/resources/config/smtp/sieve.toml @@ -2,14 +2,14 @@ # SMTP Sieve interpreter configuration ############################################# -[sieve] +[sieve.smtp] from-name = "Automated Message" -from-addr = "no-reply@__DOMAIN__" +from-addr = "no-reply@%{DEFAULT_DOMAIN}%" return-path = "" -#hostname = "__HOST__" +#hostname = "%{HOST}%" sign = ["rsa"] -[sieve.limits] +[sieve.smtp.limits] redirects = 3 out-messages = 5 received-headers = 50 @@ -17,14 +17,14 @@ cpu = 50000 nested-includes = 5 duplicate-expiry = "7d" -[sieve.scripts] +[sieve.smtp.scripts] #connect = '''require ["variables", "extlists", "reject"]; -# if string :list "${env.remote_ip}" "__SMTP_DIRECTORY__/blocked-ips" { +# if string :list "${env.remote_ip}" "default/blocked-ips" { # reject "Your IP '${env.remote_ip}' is not welcomed here."; # }''' #ehlo = '''require ["variables", "extlists", "reject"]; -# if string :list "${env.helo_domain}" "__SMTP_DIRECTORY__/blocked-domains" { +# if string :list "${env.helo_domain}" "default/blocked-domains" { # reject "551 5.1.1 Your domain '${env.helo_domain}' has been blacklisted."; # }''' diff --git a/resources/config/smtp/signature.toml b/resources/config/smtp/signature.toml index ebd6e89a..ef9497b5 100644 --- a/resources/config/smtp/signature.toml +++ b/resources/config/smtp/signature.toml @@ -3,9 +3,9 @@ ############################################# [signature."rsa"] -#public-key = "file://__PATH__/etc/dkim/__DOMAIN__.cert" -private-key = "file://__PATH__/etc/dkim/__DOMAIN__.key" -domain = "__DOMAIN__" +#public-key = "file://%{BASE_PATH}%/etc/dkim/%{DEFAULT_DOMAIN}%.cert" +private-key = "file://%{BASE_PATH}%/etc/dkim/%{DEFAULT_DOMAIN}%.key" +domain = "%{DEFAULT_DOMAIN}%" selector = "stalwart" headers = ["From", "To", "Date", "Subject", "Message-ID"] algorithm = "rsa-sha256" diff --git a/resources/config/smtp/spamfilter.toml b/resources/config/smtp/spamfilter.toml index dd78abfb..60c7e081 100644 --- a/resources/config/smtp/spamfilter.toml +++ b/resources/config/smtp/spamfilter.toml @@ -4,7 +4,7 @@ [directory."spamdb"] type = "sql" -address = "sqlite://__PATH__/data/spamfilter.sqlite3" +address = "sqlite://%{BASE_PATH}%/data/spamfilter.sqlite3" [directory."spamdb".pool] max-connections = 10 @@ -31,81 +31,82 @@ frequency = "0 3 *" [directory."spam".lookup."free-domains"] type = "glob" comment = '#' -values = ["https://get.stalw.art/resources/etc/spamfilter/maps/domains_free.list", - "file+fallback://%%{BASE_PATH}%%/etc/spamfilter/maps/domains_free.list"] +values = ["https://get.stalw.art/resources/config/spamfilter/maps/domains_free.list", + "file+fallback://%{BASE_PATH}%/etc/spamfilter/maps/domains_free.list"] [directory."spam".lookup."disposable-domains"] type = "glob" comment = '#' -values = ["https://get.stalw.art/resources/etc/spamfilter/maps/domains_disposable.list", - "file+fallback://%%{BASE_PATH}%%/etc/spamfilter/maps/domains_disposable.list"] +values = ["https://get.stalw.art/resources/config/spamfilter/maps/domains_disposable.list", + "file+fallback://%{BASE_PATH}%/etc/spamfilter/maps/domains_disposable.list"] [directory."spam".lookup."redirectors"] type = "glob" comment = '#' -values = ["https://get.stalw.art/resources/etc/spamfilter/maps/url_redirectors.list", - "file+fallback://%%{BASE_PATH}%%/etc/spamfilter/maps/url_redirectors.list"] +values = ["https://get.stalw.art/resources/config/spamfilter/maps/url_redirectors.list", + "file+fallback://%{BASE_PATH}%/etc/spamfilter/maps/url_redirectors.list"] [directory."spam".lookup."domains-allow"] type = "glob" comment = '#' -values = ["https://get.stalw.art/resources/etc/spamfilter/maps/allow_domains.list", - "file+fallback://%%{BASE_PATH}%%/etc/spamfilter/maps/allow_domains.list"] +values = ["https://get.stalw.art/resources/config/spamfilter/maps/allow_domains.list", + "file+fallback://%{BASE_PATH}%/etc/spamfilter/maps/allow_domains.list"] [directory."spam".lookup."dmarc-allow"] type = "glob" comment = '#' -values = ["https://get.stalw.art/resources/etc/spamfilter/maps/allow_dmarc.list", - "file+fallback://%%{BASE_PATH}%%/etc/spamfilter/maps/allow_dmarc.list"] +values = ["https://get.stalw.art/resources/config/spamfilter/maps/allow_dmarc.list", + "file+fallback://%{BASE_PATH}%/etc/spamfilter/maps/allow_dmarc.list"] [directory."spam".lookup."spf-dkim-allow"] type = "glob" comment = '#' -values = ["https://get.stalw.art/resources/etc/spamfilter/maps/allow_spf_dkim.list", - "file+fallback://%%{BASE_PATH}%%/etc/spamfilter/maps/allow_spf_dkim.list"] +values = ["https://get.stalw.art/resources/config/spamfilter/maps/allow_spf_dkim.list", + "file+fallback://%{BASE_PATH}%/etc/spamfilter/maps/allow_spf_dkim.list"] [directory."spam".lookup."mime-types"] type = "map" comment = '#' -values = ["https://get.stalw.art/resources/etc/spamfilter/maps/mime_types.map", - "file+fallback://%%{BASE_PATH}%%/etc/spamfilter/maps/mime_types.map"] +values = ["https://get.stalw.art/resources/config/spamfilter/maps/mime_types.map", + "file+fallback://%{BASE_PATH}%/etc/spamfilter/maps/mime_types.map"] [directory."spam".lookup."trap-address"] type = "glob" comment = '#' -values = "file://%%{BASE_PATH}%%/etc/spamfilter/maps/spam_trap.list" +values = "file://%{BASE_PATH}%/etc/spamfilter/maps/spam_trap.list" [directory."spam".lookup."scores"] type = "map" -values = "file://%%{BASE_PATH}%%/etc/spamfilter/maps/scores.map" +values = "file://%{BASE_PATH}%/etc/spamfilter/maps/scores.map" -[sieve.scripts] -spam-filter = ["file://%%{BASE_PATH}%%/etc/spamfilter/scripts/config", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/prelude", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/from", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/recipient", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/subject", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/replyto", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/date", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/messageid", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/received", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/headers", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/bounce", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/html", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/mime", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/dmarc", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/ip", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/helo", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/replies_in", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/spamtrap", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/bayes_classify", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/url", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/rbl", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/pyzor", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/scores", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/reputation", - "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/epilogue"] +[sieve.smtp.scripts] +spam-filter = ["file://%{BASE_PATH}%/etc/spamfilter/scripts/config", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/prelude", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/from", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/recipient", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/subject", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/replyto", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/date", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/messageid", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/received", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/headers", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/bounce", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/html", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/mime", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/dmarc", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/ip", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/helo", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/replies_in", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/spamtrap", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/bayes_classify", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/url", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/rbl", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/pyzor", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/composites", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/scores", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/reputation", + "file://%{BASE_PATH}%/etc/spamfilter/scripts/epilogue"] -track-replies = "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/replies_out.sieve" +track-replies = "file://%{BASE_PATH}%/etc/spamfilter/scripts/replies_out.sieve" -greylist = "file://%%{BASE_PATH}%%/etc/spamfilter/scripts/greylist.sieve" +greylist = "file://%{BASE_PATH}%/etc/spamfilter/scripts/greylist.sieve" diff --git a/resources/config/spamfilter/maps/scores.map b/resources/config/spamfilter/maps/scores.map index e69de29b..a4a2020c 100644 --- a/resources/config/spamfilter/maps/scores.map +++ b/resources/config/spamfilter/maps/scores.map @@ -0,0 +1,360 @@ +ABUSE_SURBL 5.0 +ALLOWLIST_DKIM -1.0 +ALLOWLIST_DMARC -7.0 +ALLOWLIST_SPF -1.0 +ALLOWLIST_SPF_DKIM -3.0 +ARC_ALLOW -1.0 +ARC_DNSFAIL 0.0 +ARC_INVALID 0.5 +ARC_NA 0.0 +ARC_REJECT 1.0 +ARC_SIGNED 0.0 +AUTH_NA 1.0 +AUTH_NA_OR_FAIL 1.0 +AUTOGEN_PHP_SPAMMY 1.0 +BAYES_HAM -3.0 +BAYES_SPAM 5.1 +BLOCKLIST_DKIM 2.0 +BLOCKLIST_DMARC 6.0 +BLOCKLIST_SPF 1.0 +BLOCKLIST_SPF_DKIM 3.0 +BODY_URI_ONLY 2.0 +BOGUS_ENCRYPTED_AND_TEXT 10.0 +BOUNCE -0.1 +BOUNCE_NO_AUTH 1.0 +BROKEN_CONTENT_TYPE 1.5 +COMPROMISED_ACCT_BULK 3.0 +CRACKED_SURBL 5.0 +CTE_CASE 0.5 +CTYPE_MISSING_DISPOSITION 4.0 +CTYPE_MIXED_BOGUS 1.0 +CT_EXTRA_SEMI 1.0 +DATA_URI_OBFU 2.0 +DATE_IN_FUTURE 4.0 +DATE_IN_PAST 1.0 +DBL_ABUSE 5.0 +DBL_ABUSE_BOTNET 6.5 +DBL_ABUSE_MALWARE 6.5 +DBL_ABUSE_PHISH 6.5 +DBL_ABUSE_REDIR 5.0 +DBL_BLOCKED 0.0 +DBL_BLOCKED_OPENRESOLVER 0.0 +DBL_BOTNET 7.5 +DBL_MALWARE 7.5 +DBL_PHISH 7.5 +DBL_SPAM 6.5 +DCC_BULK 3.0 +DIRECT_TO_MX 0.0 +DISPOSABLE_CC 0.0 +DISPOSABLE_ENVFROM 0.0 +DISPOSABLE_FROM 0.0 +DISPOSABLE_REPLYTO 0.0 +DISPOSABLE_TO 0.0 +DKIM_SIGNED 0.0 +DMARC_BAD_POLICY 0.5 +DMARC_DNSFAIL 0.0 +DMARC_NA 0.0 +DMARC_POLICY_ALLOW -0.5 +DMARC_POLICY_ALLOW_WITH_FAILURES -0.5 +DMARC_POLICY_QUARANTINE 1.5 +DMARC_POLICY_REJECT 2.0 +DMARC_POLICY_SOFTFAIL 0.1 +DNSWL_BLOCKED 0.0 +DWL_DNSWL_BLOCKED 0.0 +DWL_DNSWL_HI -3.5 +DWL_DNSWL_LOW -1.0 +DWL_DNSWL_MED -2.0 +DWL_DNSWL_NONE 0.0 +EMPTY_SUBJECT 1.0 +ENCRYPTED_PGP -0.5 +ENCRYPTED_SMIME -0.5 +ENVFROM_INVALID 2.0 +ENVFROM_SERVICE_ACCT 1.0 +EXT_CSS 1.0 +FAKE_REPLY 1.0 +FORGED_RCVD_TRAIL 1.0 +FORGED_RECIPIENTS 2.0 +FORGED_RECIPIENTS_MAILLIST 0.0 +FORGED_SENDER 0.3 +FORGED_SENDER_MAILLIST 0.0 +FREEMAIL_AFF 4.0 +FREEMAIL_CC 0.0 +FREEMAIL_ENVFROM 0.0 +FREEMAIL_FROM 0.0 +FREEMAIL_REPLYTO 0.0 +FREEMAIL_REPLYTO_NEQ_FROM_DOM 3.0 +FREEMAIL_TO 0.0 +FROM_DN_EQ_ADDR 1.0 +FROM_EQ_ENVFROM 0.0 +FROM_EXCESS_BASE64 1.5 +FROM_EXCESS_QP 1.2 +FROM_HAS_DN 0.0 +FROM_INVALID 2.0 +FROM_NAME_EXCESS_SPACE 1.0 +FROM_NAME_HAS_TITLE 1.0 +FROM_NEEDS_ENCODING 1.0 +FROM_NEQ_DISPLAY_NAME 4.0 +FROM_NEQ_ENVFROM 0.0 +FROM_NO_DN 0.0 +FROM_SERVICE_ACCT 1.0 +HACKED_WP_PHISHING 4.5 +HAS_ANON_DOMAIN 0.1 +HAS_ATTACHMENT 0.0 +HAS_DATA_URI 0.0 +HAS_GOOGLE_FIREBASE_URL 2.0 +HAS_GOOGLE_REDIR 1.0 +HAS_GUC_PROXY_URI 1.0 +HAS_IPFS_GATEWAY_URL 6.0 +HAS_LIST_UNSUB -0.01 +HAS_ONION_URI 0.0 +HAS_ORG_HEADER 0.0 +HAS_PHPMAILER_SIG 0.0 +HAS_REPLYTO 0.0 +HAS_WP_URI 0.0 +HAS_XAW 0.0 +HAS_XOIP 0.0 +HAS_X_ANTIABUSE 0.0 +HAS_X_AS 0.0 +HAS_X_GMSV 0.0 +HAS_X_PHP_SCRIPT 0.0 +HAS_X_POS 0.0 +HAS_X_PRIO_FIVE 0.0 +HAS_X_PRIO_ONE 0.0 +HAS_X_PRIO_THREE 0.0 +HAS_X_PRIO_TWO 0.0 +HAS_X_PRIO_ZERO 0.0 +HAS_X_SOURCE 0.0 +HEADER_EMPTY_DELIMITER 1.0 +HEADER_FORGED_MDN 2.0 +HEADER_RCONFIRM_MISMATCH 2.0 +HFILTER_FROMHOST_NORES_A_OR_MX 1.5 +HFILTER_FROM_BOUNCE 0.0 +HFILTER_HELO_BAREIP 3.0 +HFILTER_HELO_IP_A 1.0 +HFILTER_HELO_NORES_A_OR_MX 0.3 +HFILTER_HELO_NOT_FQDN 2.0 +HFILTER_HOSTNAME_UNKNOWN 2.5 +HFILTER_RCPT_BOUNCEMOREONE 1.5 +HFILTER_URL_ONLY 2.2 +HIDDEN_SOURCE_OBJ 2.0 +HTML_META_REFRESH_URL 5.0 +HTML_SHORT_LINK_IMG_1 2.0 +HTML_SHORT_LINK_IMG_2 1.0 +HTML_SHORT_LINK_IMG_3 0.5 +HTML_TEXT_IMG_RATIO 1.0 +HTML_UNBALANCED_TAG 0.5 +HTTP_TO_HTTPS 0.5 +HTTP_TO_IP 1.0 +INFO_TO_INFO_LU 2.0 +INVALID_DATE 1.5 +INVALID_FROM_8BIT 6.0 +INVALID_MSGID 1.7 +KLMS_SPAM 5.0 +LONG_SUBJ 3.0 +MAILLIST -0.2 +MANY_INVISIBLE_PARTS 1.0 +MID_BARE_IP 2.0 +MID_CONTAINS_FROM 1.0 +MID_CONTAINS_TO 1.0 +MID_MISSING_BRACKETS 1.0 +MID_RHS_IP_LITERAL 1.0 +MID_RHS_MATCH_FROM 1.0 +MID_RHS_MATCH_FROMTLD 1.0 +MID_RHS_MATCH_TO 1.0 +MID_RHS_NOT_FQDN 0.5 +MID_RHS_WWW 0.5 +MIME_ARCHIVE_IN_ARCHIVE 5.0 +MIME_BAD 1.0 +MIME_BAD_ATTACHMENT 4.0 +MIME_BAD_EXTENSION 2.0 +MIME_BAD_UNICODE 8.0 +MIME_BASE64_TEXT 0.1 +MIME_BASE64_TEXT_BOGUS 1.0 +MIME_DOUBLE_BAD_EXTENSION 2.0 +MIME_GOOD -0.1 +MIME_HEADER_CTYPE_ONLY 2.0 +MIME_HTML_ONLY 0.2 +MIME_MA_MISSING_HTML 1.0 +MIME_MA_MISSING_TEXT 2.0 +MISSING_DATE 1.0 +MISSING_FROM 2.0 +MISSING_MID 2.5 +MISSING_MIME_VERSION 2.0 +MISSING_SUBJECT 2.0 +MISSING_TO 2.0 +MSBL_EBL 7.5 +MSBL_EBL_GREY 0.5 +MULTIPLE_FROM 8.0 +MULTIPLE_UNIQUE_HEADERS 7.0 +MV_CASE 0.5 +MW_SURBL_MULTI 7.5 +OMOGRAPH_URL 5.0 +ONCE_RECEIVED 0.1 +PHISHED_OPENPHISH 7.0 +PHISHED_PHISHTANK 7.0 +PHISHING 4.0 +PHISH_EMOTION 1.0 +PHP_XPS_PATTERN 0.0 +PH_SURBL_MULTI 7.5 +PRECEDENCE_BULK 0.0 +PREVIOUSLY_DELIVERED 0.0 +PYZOR 3.5 +RBL_BARRACUDA 4.0 +RBL_BLOCKLISTDE 4.0 +RBL_MAILSPIKE_BAD 1.0 +RBL_MAILSPIKE_VERYBAD 1.5 +RBL_MAILSPIKE_WORST 2.0 +RBL_NIXSPAM 4.0 +RBL_SEM 1.0 +RBL_SEM_IPV6 1.0 +RBL_SENDERSCORE 2.0 +RBL_SPAMCOP 4.0 +RBL_SPAMHAUS 0.0 +RBL_SPAMHAUS_BLOCKED 0.0 +RBL_SPAMHAUS_BLOCKED_OPENRESOLVER 0.0 +RBL_SPAMHAUS_CSS 2.0 +RBL_SPAMHAUS_DROP 7.0 +RBL_SPAMHAUS_PBL 2.0 +RBL_SPAMHAUS_SBL 4.0 +RBL_SPAMHAUS_XBL 4.0 +RBL_VIRUSFREE_BOTNET 2.0 +RCPT_ADDR_IN_SUBJECT 3.0 +RCPT_COUNT_FIVE 0.0 +RCPT_COUNT_GT_50 0.0 +RCPT_COUNT_ONE 0.0 +RCPT_COUNT_SEVEN 0.0 +RCPT_COUNT_THREE 0.0 +RCPT_COUNT_TWELVE 0.0 +RCPT_COUNT_TWO 0.0 +RCPT_COUNT_ZERO 0.0 +RCPT_LOCAL_IN_SUBJECT 2.0 +RCVD_COUNT_FIVE 0.0 +RCVD_COUNT_ONE 0.0 +RCVD_COUNT_SEVEN 0.0 +RCVD_COUNT_THREE 0.0 +RCVD_COUNT_TWELVE 0.0 +RCVD_COUNT_TWO 0.0 +RCVD_COUNT_ZERO 0.0 +RCVD_DKIM_ARC_DNSWL_HI -1.0 +RCVD_DKIM_ARC_DNSWL_MED -0.5 +RCVD_DOUBLE_IP_SPAM 2.0 +RCVD_FROM_SMTP_AUTH 0.0 +RCVD_HELO_USER 3.0 +RCVD_ILLEGAL_CHARS 4.0 +RCVD_IN_DNSWL_HI -0.5 +RCVD_IN_DNSWL_LOW -0.1 +RCVD_IN_DNSWL_MED -0.2 +RCVD_IN_DNSWL_NONE 0.0 +RCVD_NO_TLS_LAST 0.1 +RCVD_TLS_ALL 0.0 +RCVD_TLS_LAST 0.0 +RCVD_UNAUTH_PBL 2.0 +RCVD_VIA_SMTP_AUTH 0.0 +RDNS_DNSFAIL 0.0 +RDNS_NONE 1.0 +RECEIVED_BLOCKLISTDE 3.0 +RECEIVED_SPAMHAUS_BLOCKED 0.0 +RECEIVED_SPAMHAUS_BLOCKED_OPENRESOLVER 0.0 +RECEIVED_SPAMHAUS_CSS 1.0 +RECEIVED_SPAMHAUS_PBL 0.0 +RECEIVED_SPAMHAUS_SBL 3.0 +RECEIVED_SPAMHAUS_XBL 1.0 +REDIRECTOR_URL 0.0 +REDIRECTOR_URL_ONLY 1.0 +REPLYTO_ADDR_EQ_FROM 0.0 +REPLYTO_DN_EQ_FROM_DN 0.0 +REPLYTO_DOM_EQ_FROM_DOM 0.0 +REPLYTO_DOM_NEQ_FROM_DOM 0.0 +REPLYTO_EMAIL_HAS_TITLE 2.0 +REPLYTO_EQ_FROM 0.0 +REPLYTO_EQ_TO_ADDR 5.0 +REPLYTO_EXCESS_BASE64 1.5 +REPLYTO_EXCESS_QP 1.2 +REPLYTO_UNPARSEABLE 1.0 +RWL_MAILSPIKE_EXCELLENT -0.4 +RWL_MAILSPIKE_GOOD -0.1 +RWL_MAILSPIKE_NEUTRAL 0.0 +RWL_MAILSPIKE_POSSIBLE 0.0 +RWL_MAILSPIKE_VERYGOOD -0.2 +R_BAD_CTE_7BIT 3.5 +R_DKIM_ALLOW -0.2 +R_DKIM_NA 0.0 +R_DKIM_PERMFAIL 0.0 +R_DKIM_REJECT 1.0 +R_DKIM_TEMPFAIL 0.0 +R_MISSING_CHARSET 0.5 +R_MIXED_CHARSET 5.0 +R_MIXED_CHARSET_URL 7.0 +R_NO_SPACE_IN_FROM 1.0 +R_PARTS_DIFFER 1.0 +R_SPF_ALLOW -0.2 +R_SPF_DNSFAIL 0.0 +R_SPF_FAIL 1.0 +R_SPF_NA 0.0 +R_SPF_NEUTRAL 0.0 +R_SPF_PERMFAIL 0.0 +R_SPF_SOFTFAIL 0.0 +R_SUSPICIOUS_URL 5.0 +R_UNDISC_RCPT 3.0 +SEM_URIBL 3.5 +SEM_URIBL_FRESH15 3.0 +SIGNED_PGP -2.0 +SIGNED_SMIME -2.0 +SORTED_RECIPS 3.5 +SPAM_FLAG 5.0 +SPAM_TRAP discard +SPOOF_DISPLAY_NAME 8.0 +SPOOF_REPLYTO 6.0 +SUBJECT_ENDS_EXCLAIM 0.0 +SUBJECT_ENDS_QUESTION 1.0 +SUBJECT_ENDS_SPACES 0.5 +SUBJECT_HAS_CURRENCY 1.0 +SUBJECT_HAS_EXCLAIM 0.0 +SUBJECT_HAS_QUESTION 0.0 +SUBJECT_NEEDS_ENCODING 1.0 +SUBJ_ALL_CAPS 3.0 +SUBJ_BOUNCE_WORDS 0.0 +SUBJ_EXCESS_BASE64 1.5 +SUBJ_EXCESS_QP 1.2 +SURBL_BLOCKED 0.0 +SURBL_HASHBL_ABUSE 5.0 +SURBL_HASHBL_CRACKED 5.0 +SURBL_HASHBL_EMAIL 5.0 +SURBL_HASHBL_MALWARE 6.5 +SURBL_HASHBL_PHISH 6.5 +SUSPICIOUS_RECIPS 1.5 +TAGGED_FROM 0.0 +TAGGED_RCPT 0.0 +THREAD_HIJACKING_FROM_INJECTOR 2.0 +TO_DN_ALL 0.0 +TO_DN_EQ_ADDR_ALL 0.0 +TO_DN_EQ_ADDR_SOME 0.0 +TO_DN_NONE 0.0 +TO_DN_RECIPIENTS 2.0 +TO_DN_SOME 0.0 +TO_DOM_EQ_FROM_DOM 0.0 +TO_EQ_FROM 0.0 +TO_EXCESS_BASE64 1.5 +TO_EXCESS_QP 1.2 +TO_MATCH_ENVRCPT_ALL 0.0 +TO_MATCH_ENVRCPT_SOME 0.0 +TO_NEEDS_ENCODING 1.0 +TO_WRAPPED_IN_SPACES 2.0 +TRUSTED_REPLY -7.0 +UNDISC_RCPTS_BULK 3.0 +UNITEDINTERNET_SPAM 5.0 +URIBL_BLACK 7.5 +URIBL_BLOCKED 0.0 +URIBL_GREY 1.5 +URIBL_RED 3.5 +URI_COUNT_ODD 1.0 +URI_HIDDEN_PATH 1.0 +URL_IN_SUBJECT 4.0 +URL_REDIRECTOR_NESTED 1.0 +VIOLATED_DIRECT_SPF 3.5 +WP_COMPROMISED 0.0 +WWW_DOT_DOMAIN 0.5 +XM_CASE 0.5 +XM_UA_NO_VERSION 0.01 +X_PHP_EVAL 4.0 +ZERO_WIDTH_SPACE_URL 7.0 diff --git a/resources/config/spamfilter/scripts/epilogue.sieve b/resources/config/spamfilter/scripts/epilogue.sieve index d3226765..b47a7f2a 100644 --- a/resources/config/spamfilter/scripts/epilogue.sieve +++ b/resources/config/spamfilter/scripts/epilogue.sieve @@ -20,9 +20,9 @@ if "SCORE_REJECT_THRESHOLD && score >= SCORE_REJECT_THRESHOLD" { } else { let "spam_status" "'No, score=' + score"; } - eval "add-header('X-Spam-Status', spam_status)"; + eval "add_header('X-Spam-Status', spam_status)"; if eval "!is_empty(spam_result)" { - eval "add-header('X-Spam-Result', spam_result)"; + eval "add_header('X-Spam-Result', spam_result)"; } } diff --git a/resources/config/spamfilter/scripts/replyto.sieve b/resources/config/spamfilter/scripts/replyto.sieve index 526b6aa8..d3293d8d 100644 --- a/resources/config/spamfilter/scripts/replyto.sieve +++ b/resources/config/spamfilter/scripts/replyto.sieve @@ -45,6 +45,10 @@ if eval "!is_empty(rto_raw)" { } } + if eval "rto_addr == envelope.from" { + let "t.REPLYTO_ADDR_EQ_FROM" "1'; + } + if eval "lookup('spam/free-domains', rto_domain_sld)" { let "t.FREEMAIL_REPLYTO" "1"; if eval "rto_domain_sld != from_domain_sld && lookup('spam/free-domains', from_domain_sld)" { diff --git a/tests/resources/create_test_env.sh b/tests/resources/create_test_env.sh new file mode 100644 index 00000000..1f394ee3 --- /dev/null +++ b/tests/resources/create_test_env.sh @@ -0,0 +1,17 @@ +#!/bin/sh + +BASE_DIR = "/tmp/stalwart-test" + +# Delete previous tests +rm -rf $BASE_DIR + +# Create directories +mkdir -p $BASE_DIR $BASE_DIR/data $BASE_DIR/data/blobs $BASE_DIR/logs $BASE_DIR/reports $BASE_DIR/queue + +# Copy config files +cp -r resources/config $BASE_DIR/etc + +# Copy self-signed certs +cp -r tests/resources/tls_cert.pem $BASE_DIR/etc +cp -r tests/resources/tls_privatekey.pem $BASE_DIR/etc + diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index 79f51489..b44734c9 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -176,7 +176,7 @@ domains = ["example.org"] "#; pub fn parse_config() -> DirectoryConfig { - utils::config::Config::parse(CONFIG) + utils::config::Config::new(CONFIG) .unwrap() .parse_directory() .unwrap() @@ -423,7 +423,7 @@ async fn lookup_local() { ) .unwrap();*/ - let lookups = utils::config::Config::parse( + let lookups = utils::config::Config::new( &LOOKUP_CONFIG.replace( "%PATH%", PathBuf::from(env!("CARGO_MANIFEST_DIR")) @@ -493,7 +493,7 @@ fn address_mappings() { expected-catch = "info@example.org" "#; - let config = utils::config::Config::parse(MAPPINGS).unwrap(); + let config = utils::config::Config::new(MAPPINGS).unwrap(); const ADDR: &str = "john.doe+alias@example.org"; for test in ["enable", "disable", "custom"] { diff --git a/tests/src/imap/mod.rs b/tests/src/imap/mod.rs index fefa32a7..712e2f8d 100644 --- a/tests/src/imap/mod.rs +++ b/tests/src/imap/mod.rs @@ -235,7 +235,7 @@ struct IMAPTest { async fn init_imap_tests(delete_if_exists: bool) -> IMAPTest { // Load and parse config let temp_dir = TempDir::new("imap_tests", delete_if_exists); - let config = utils::config::Config::parse( + let config = utils::config::Config::new( &add_test_certs(SERVER).replace("{TMP}", &temp_dir.path.display().to_string()), ) .unwrap(); diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 8f8e5bd8..4f7c76d1 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -283,7 +283,7 @@ struct JMAPTest { async fn init_jmap_tests(delete_if_exists: bool) -> JMAPTest { // Load and parse config let temp_dir = TempDir::new("jmap_tests", delete_if_exists); - let config = utils::config::Config::parse( + let config = utils::config::Config::new( &add_test_certs(SERVER).replace("{TMP}", &temp_dir.path.display().to_string()), ) .unwrap(); diff --git a/tests/src/jmap/push_subscription.rs b/tests/src/jmap/push_subscription.rs index 9fd43be0..aae18e76 100644 --- a/tests/src/jmap/push_subscription.rs +++ b/tests/src/jmap/push_subscription.rs @@ -104,7 +104,7 @@ pub async fn test(server: Arc, admin_client: &mut Client) { }); // Start mock push server - let settings = utils::config::Config::parse(&add_test_certs(SERVER)).unwrap(); + let settings = utils::config::Config::new(&add_test_certs(SERVER)).unwrap(); let servers = settings.parse_servers().unwrap(); // Start JMAP server diff --git a/tests/src/smtp/config.rs b/tests/src/smtp/config.rs index de553c9b..ffc1e857 100644 --- a/tests/src/smtp/config.rs +++ b/tests/src/smtp/config.rs @@ -67,7 +67,7 @@ fn parse_conditions() { file.push("config"); file.push("rules.toml"); - let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap(); + let config = Config::new(&fs::read_to_string(file).unwrap()).unwrap(); let servers = vec![Server { id: "smtp".to_string(), internal_id: 123, @@ -190,7 +190,7 @@ fn parse_if_blocks() { file.push("config"); file.push("if-blocks.toml"); - let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap(); + let config = Config::new(&fs::read_to_string(file).unwrap()).unwrap(); // Create context and add some conditions let context = ConfigContext::new(&[]); @@ -375,7 +375,7 @@ fn parse_throttle() { EnvelopeKey::Priority, ]; - let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap(); + let config = Config::new(&fs::read_to_string(file).unwrap()).unwrap(); let context = ConfigContext::new(&[]); let throttle = config .parse_throttle("throttle", &context, &available_keys, u16::MAX) @@ -424,7 +424,7 @@ fn parse_servers() { let toml = add_test_certs(&fs::read_to_string(file).unwrap()); // Parse servers - let config = Config::parse(&toml).unwrap(); + let config = Config::new(&toml).unwrap(); let servers = config.parse_servers().unwrap().inner; let expected_servers = vec![ Server { @@ -554,7 +554,7 @@ async fn eval_if() { file.push("config"); file.push("rules-eval.toml"); - let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap(); + let config = Config::new(&fs::read_to_string(file).unwrap()).unwrap(); let servers = vec![ Server { id: "smtp".to_string(), @@ -600,7 +600,7 @@ async fn eval_dynvalue() { file.push("config"); file.push("rules-dynvalue.toml"); - let config = Config::parse(&fs::read_to_string(file).unwrap()).unwrap(); + let config = Config::new(&fs::read_to_string(file).unwrap()).unwrap(); let mut context = ConfigContext::new(&[]); context.directory = config.parse_directory().unwrap(); diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index c8a5aba1..fd0c273d 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -27,14 +27,14 @@ use utils::config::Config; use crate::smtp::{TestConfig, TestSMTP}; const CONFIG: &str = r#" -[sieve] +[sieve.smtp] from-name = "Sieve Daemon" from-addr = "sieve@foobar.org" return-path = "" hostname = "mx.foobar.org" no-capability-check = true -[sieve.limits] +[sieve.smtp.limits] redirects = 3 out-messages = 5 received-headers = 50 @@ -127,7 +127,7 @@ values = ["SPAM_TRAP discard"] [resolver] public-suffix = "file://%LIST_PATH%/public-suffix.dat" -[sieve.scripts] +[sieve.smtp.scripts] "#; const CREATE_TABLES: &[&str; 3] = &[ @@ -239,7 +239,7 @@ async fn antispam() { config.push_str(&format!("combined = '''{all_scripts}\n'''\n")); // Parse config - let config = Config::parse(&config).unwrap(); + let config = Config::new(&config).unwrap(); let mut ctx = ConfigContext::new(&[]); ctx.directory = config.parse_directory().unwrap(); core.sieve = config.parse_sieve(&mut ctx).unwrap(); diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index d98c4cfc..34f8cd56 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -59,7 +59,7 @@ member-of = ["sales", "support"] async fn auth() { let mut core = SMTP::test(); let mut ctx = ConfigContext::new(&[]); - ctx.directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); + ctx.directory = Config::new(DIRECTORY).unwrap().parse_directory().unwrap(); let config = &mut core.session.config.auth; diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index 2c3e78a8..59700d43 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -79,7 +79,7 @@ async fn data() { // Create temp dir for queue let mut qr = core.init_test_queue("smtp_data_test"); - let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); + let directory = Config::new(DIRECTORY).unwrap().parse_directory().unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( directory.directories.get("local").unwrap().clone(), diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index 6a4e4e3f..debd000d 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -134,7 +134,7 @@ async fn dmarc() { // Create report channels let mut rr = core.init_test_report(); - let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); + let directory = Config::new(DIRECTORY).unwrap().parse_directory().unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( directory.directories.get("local").unwrap().clone(), diff --git a/tests/src/smtp/inbound/rcpt.rs b/tests/src/smtp/inbound/rcpt.rs index 9ec7a3e9..e63017f1 100644 --- a/tests/src/smtp/inbound/rcpt.rs +++ b/tests/src/smtp/inbound/rcpt.rs @@ -73,7 +73,7 @@ async fn rcpt() { let mut core = SMTP::test(); let config_ext = &mut core.session.config.extensions; - let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); + let directory = Config::new(DIRECTORY).unwrap().parse_directory().unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( directory.directories.get("local").unwrap().clone(), diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs index 57133a3b..51b33d2b 100644 --- a/tests/src/smtp/inbound/rewrite.rs +++ b/tests/src/smtp/inbound/rewrite.rs @@ -46,13 +46,13 @@ rewrite = [ { all-of = [ { if = "rcpt-domain", eq = "foobar.net" }, script = [ { if = "rcpt-domain", eq = "foobar.org", then = "rcpt" }, { else = false } ] -[sieve] +[sieve.smtp] from-name = "Sieve Daemon" from-addr = "sieve@foobar.org" return-path = "" hostname = "mx.foobar.org" -[sieve.limits] +[sieve.smtp.limits] redirects = 3 out-messages = 5 received-headers = 50 @@ -60,7 +60,7 @@ cpu = 10000 nested-includes = 5 duplicate-expiry = "7d" -[sieve.scripts] +[sieve.smtp.scripts] mail = ''' require ["variables", "envelope"]; @@ -102,7 +102,7 @@ async fn address_rewrite() { ]; let mut core = SMTP::test(); let mut ctx = ConfigContext::new(&[]).parse_signatures(); - let settings = Config::parse(CONFIG).unwrap(); + let settings = Config::new(CONFIG).unwrap(); ctx.directory = settings.parse_directory().unwrap(); core.sieve = settings.parse_sieve(&mut ctx).unwrap(); let config = &mut core.session.config; diff --git a/tests/src/smtp/inbound/scripts.rs b/tests/src/smtp/inbound/scripts.rs index fbd15fbf..c6043c18 100644 --- a/tests/src/smtp/inbound/scripts.rs +++ b/tests/src/smtp/inbound/scripts.rs @@ -60,14 +60,14 @@ command = [ { if = "remote-ip", eq = "10.0.0.123", then = "/bin/bash" }, arguments = ["%CFG_PATH%/pipe_me.sh", "hello", "world"] timeout = "10s" -[sieve] +[sieve.smtp] from-name = "Sieve Daemon" from-addr = "sieve@foobar.org" return-path = "" hostname = "mx.foobar.org" sign = ["rsa"] -[sieve.limits] +[sieve.smtp.limits] redirects = 3 out-messages = 5 received-headers = 50 @@ -75,7 +75,7 @@ cpu = 10000 nested-includes = 5 duplicate-expiry = "7d" -[sieve.scripts] +[sieve.smtp.scripts] "#; #[tokio::test] @@ -117,7 +117,7 @@ async fn sieve_scripts() { let mut core = SMTP::test(); let mut qr = core.init_test_queue("smtp_sieve_test"); let mut ctx = ConfigContext::new(&[]).parse_signatures(); - let config = Config::parse( + let config = Config::new( &config .replace("%PATH%", qr._temp_dir.temp_dir.as_path().to_str().unwrap()) .replace( diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs index 51d64c39..0752cd4f 100644 --- a/tests/src/smtp/inbound/sign.rs +++ b/tests/src/smtp/inbound/sign.rs @@ -154,7 +154,7 @@ async fn sign_and_seal() { Instant::now() + Duration::from_secs(5), ); - let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); + let directory = Config::new(DIRECTORY).unwrap().parse_directory().unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( directory.directories.get("local").unwrap().clone(), @@ -225,7 +225,7 @@ pub trait TextConfigContext<'x> { impl<'x> TextConfigContext<'x> for ConfigContext<'x> { fn parse_signatures(mut self) -> Self { - Config::parse(SIGNATURES) + Config::new(SIGNATURES) .unwrap() .parse_signatures(&mut self) .unwrap(); diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs index f3b248e9..55d184e7 100644 --- a/tests/src/smtp/inbound/vrfy.rs +++ b/tests/src/smtp/inbound/vrfy.rs @@ -65,7 +65,7 @@ async fn vrfy_expn() { let mut core = SMTP::test(); let ctx = ConfigContext::new(&[]); - let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); + let directory = Config::new(DIRECTORY).unwrap().parse_directory().unwrap(); let config = &mut core.session.config.rcpt; config.directory = IfBlock::new(Some(MaybeDynValue::Static( directory.directories.get("local").unwrap().clone(), diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index e381e59c..d645a1ae 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -83,7 +83,7 @@ async fn lookup_sql() { // Parse settings let mut core = SMTP::test(); let mut ctx = ConfigContext::new(&[]); - let config = Config::parse(CONFIG).unwrap(); + let config = Config::new(CONFIG).unwrap(); ctx.directory = config.parse_directory().unwrap(); // Obtain directory handle diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index 7db2bd80..a0abba5c 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -95,7 +95,7 @@ async fn manage_queue() { ); // Start local management interface - let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); + let directory = Config::new(DIRECTORY).unwrap().parse_directory().unwrap(); core.queue.config.management_lookup = directory.directories.get("local").unwrap().clone(); core.session.config.rcpt.relay = IfBlock::new(true); core.session.config.rcpt.max_recipients = IfBlock::new(100); diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs index be0b6ee6..8e68593b 100644 --- a/tests/src/smtp/management/report.rs +++ b/tests/src/smtp/management/report.rs @@ -82,7 +82,7 @@ async fn manage_reports() { config.hash = IfBlock::new(16); config.dmarc_aggregate.max_size = IfBlock::new(1024); config.tls.max_size = IfBlock::new(1024); - let directory = Config::parse(DIRECTORY).unwrap().parse_directory().unwrap(); + let directory = Config::new(DIRECTORY).unwrap().parse_directory().unwrap(); core.queue.config.management_lookup = directory.directories.get("local").unwrap().clone(); let (report_tx, report_rx) = mpsc::channel(1024); core.report.tx = report_tx; diff --git a/tests/src/smtp/mod.rs b/tests/src/smtp/mod.rs index 863c2bf5..f2f37a63 100644 --- a/tests/src/smtp/mod.rs +++ b/tests/src/smtp/mod.rs @@ -73,7 +73,7 @@ pub trait ParseTestConfig { impl ParseTestConfig for &str { fn parse_if(&self, ctx: &ConfigContext) -> IfBlock { - Config::parse(&format!("test = {self}\n")) + Config::new(&format!("test = {self}\n")) .unwrap() .parse_if_block( "test", @@ -97,7 +97,7 @@ impl ParseTestConfig for &str { } fn parse_throttle(&self, ctx: &ConfigContext) -> Vec { - Config::parse(self) + Config::new(self) .unwrap() .parse_throttle( "throttle", @@ -121,18 +121,18 @@ impl ParseTestConfig for &str { } fn parse_quota(&self, ctx: &ConfigContext) -> QueueQuotas { - Config::parse(self).unwrap().parse_queue_quota(ctx).unwrap() + Config::new(self).unwrap().parse_queue_quota(ctx).unwrap() } fn parse_queue_throttle(&self, ctx: &ConfigContext) -> QueueThrottle { - Config::parse(self) + Config::new(self) .unwrap() .parse_queue_throttle(ctx) .unwrap() } fn parse_milters(&self, ctx: &ConfigContext) -> Vec { - Config::parse(self) + Config::new(self) .unwrap() .parse_milters( ctx, diff --git a/tests/src/smtp/outbound/lmtp.rs b/tests/src/smtp/outbound/lmtp.rs index be62e947..9a9898ee 100644 --- a/tests/src/smtp/outbound/lmtp.rs +++ b/tests/src/smtp/outbound/lmtp.rs @@ -80,7 +80,7 @@ async fn lmtp_delivery() { let mut local_qr = core.init_test_queue("lmtp_delivery_local"); let mut ctx = ConfigContext::new(&[]); - let config = Config::parse(REMOTE).unwrap(); + let config = Config::new(REMOTE).unwrap(); config.parse_remote_hosts(&mut ctx).unwrap(); core.queue.config.next_hop = "[{if = 'rcpt-domain', eq = 'foobar.org', then = 'lmtp'}, {else = false}]" diff --git a/tests/src/smtp/outbound/mod.rs b/tests/src/smtp/outbound/mod.rs index 09792794..60e43755 100644 --- a/tests/src/smtp/outbound/mod.rs +++ b/tests/src/smtp/outbound/mod.rs @@ -72,7 +72,7 @@ private-key = 'file://{PK}' pub fn start_test_server(core: Arc, protocols: &[ServerProtocol]) -> watch::Sender { // Spawn listeners - let config = Config::parse(&add_test_certs(SERVER)).unwrap(); + let config = Config::new(&add_test_certs(SERVER)).unwrap(); let mut servers = config.parse_servers().unwrap(); // Filter out protocols diff --git a/tests/src/store/blob.rs b/tests/src/store/blob.rs index a2b0e292..081e6bbe 100644 --- a/tests/src/store/blob.rs +++ b/tests/src/store/blob.rs @@ -61,10 +61,8 @@ pub async fn blob_tests() { let temp_dir = TempDir::new("blob_tests", true); test_blob( Store::open( - &Config::parse( - &CONFIG_LOCAL.replace("{TMP}", temp_dir.path.as_path().to_str().unwrap()), - ) - .unwrap(), + &Config::new(&CONFIG_LOCAL.replace("{TMP}", temp_dir.path.as_path().to_str().unwrap())) + .unwrap(), ) .await .unwrap(), @@ -72,7 +70,7 @@ pub async fn blob_tests() { .await; test_blob( Store::open( - &Config::parse(&CONFIG_S3.replace("{TMP}", temp_dir.path.as_path().to_str().unwrap())) + &Config::new(&CONFIG_S3.replace("{TMP}", temp_dir.path.as_path().to_str().unwrap())) .unwrap(), ) .await diff --git a/tests/src/store/mod.rs b/tests/src/store/mod.rs index 7a7e745d..bfaea154 100644 --- a/tests/src/store/mod.rs +++ b/tests/src/store/mod.rs @@ -49,7 +49,7 @@ pub async fn store_tests() { temp_dir.path.display() ); let db = Arc::new( - Store::open(&Config::parse(&config_file).unwrap()) + Store::open(&Config::new(&config_file).unwrap()) .await .unwrap(), );