From 4551576e04184aa09f43bba3ed99d48673bba016 Mon Sep 17 00:00:00 2001 From: mdecimus Date: Thu, 18 Jan 2024 18:18:32 +0100 Subject: [PATCH] Expressions in configuration files (untested) --- Cargo.lock | 1 + crates/directory/src/core/config.rs | 30 +- crates/directory/src/core/dispatch.rs | 12 +- crates/directory/src/lib.rs | 57 +- crates/main/Cargo.toml | 2 +- crates/smtp/src/config/auth.rs | 110 +-- crates/smtp/src/config/condition.rs | 315 -------- crates/smtp/src/config/if_block.rs | 341 --------- crates/smtp/src/config/mod.rs | 378 ++++------ crates/smtp/src/config/queue.rs | 462 +++++------- crates/smtp/src/config/remote.rs | 63 -- crates/smtp/src/config/report.rs | 206 +++--- crates/smtp/src/config/scripts.rs | 11 +- crates/smtp/src/config/session.rs | 670 ++++++++++-------- crates/smtp/src/config/shared.rs | 100 +++ crates/smtp/src/config/throttle.rs | 51 +- crates/smtp/src/core/eval.rs | 318 +++++++++ crates/smtp/src/core/if_block.rs | 290 -------- crates/smtp/src/core/management.rs | 5 +- crates/smtp/src/core/mod.rs | 98 +-- crates/smtp/src/core/params.rs | 118 ++- crates/smtp/src/core/throttle.rs | 75 +- crates/smtp/src/inbound/data.rs | 163 ++++- crates/smtp/src/inbound/ehlo.rs | 68 +- crates/smtp/src/inbound/mail.rs | 65 +- crates/smtp/src/inbound/milter/message.rs | 7 +- crates/smtp/src/inbound/rcpt.rs | 40 +- crates/smtp/src/inbound/session.rs | 68 +- crates/smtp/src/inbound/spawn.rs | 7 +- crates/smtp/src/inbound/vrfy.rs | 16 +- crates/smtp/src/lib.rs | 39 +- crates/smtp/src/outbound/delivery.rs | 170 +++-- crates/smtp/src/outbound/lookup.rs | 27 +- crates/smtp/src/queue/dsn.rs | 53 +- crates/smtp/src/queue/manager.rs | 120 ++-- crates/smtp/src/queue/mod.rs | 119 +--- crates/smtp/src/queue/quota.rs | 26 +- crates/smtp/src/queue/spool.rs | 25 +- crates/smtp/src/queue/throttle.rs | 22 +- crates/smtp/src/reporting/dkim.rs | 23 +- crates/smtp/src/reporting/dmarc.rs | 143 ++-- crates/smtp/src/reporting/mod.rs | 41 +- crates/smtp/src/reporting/scheduler.rs | 136 ++-- crates/smtp/src/reporting/spf.rs | 23 +- crates/smtp/src/reporting/tls.rs | 48 +- crates/smtp/src/scripts/event_loop.rs | 15 +- crates/smtp/src/scripts/plugins/bayes.rs | 12 +- crates/smtp/src/scripts/plugins/lookup.rs | 16 +- crates/smtp/src/scripts/plugins/query.rs | 2 +- crates/store/src/dispatch/lookup.rs | 18 + crates/utils/Cargo.toml | 1 + crates/utils/src/config/dynvalue.rs | 191 ----- crates/utils/src/config/if_block.rs | 181 +++++ crates/utils/src/config/mod.rs | 45 +- crates/utils/src/config/utils.rs | 227 ++++-- crates/utils/src/expr/eval.rs | 569 +++++++++++++++ crates/utils/src/expr/functions/array.rs | 82 +++ crates/utils/src/expr/functions/email.rs | 121 ++++ crates/utils/src/expr/functions/misc.rs | 67 ++ crates/utils/src/expr/functions/mod.rs | 92 +++ crates/utils/src/expr/functions/text.rs | 301 ++++++++ crates/utils/src/expr/mod.rs | 308 ++++++++ crates/utils/src/expr/parser.rs | 293 ++++++++ crates/utils/src/expr/tokenizer.rs | 338 +++++++++ crates/utils/src/lib.rs | 1 + tests/Cargo.toml | 2 +- tests/resources/smtp/config/if-blocks.toml | 30 +- .../resources/smtp/config/rules-dynvalue.toml | 61 +- tests/resources/smtp/config/rules-eval.toml | 178 +---- tests/src/directory/mod.rs | 24 +- tests/src/directory/sql.rs | 17 +- tests/src/smtp/config.rs | 612 +++++++--------- tests/src/smtp/inbound/antispam.rs | 4 +- tests/src/smtp/inbound/auth.rs | 18 +- tests/src/smtp/inbound/data.rs | 17 +- tests/src/smtp/inbound/dmarc.rs | 34 +- tests/src/smtp/inbound/ehlo.rs | 14 +- tests/src/smtp/inbound/limits.rs | 11 +- tests/src/smtp/inbound/mail.rs | 19 +- tests/src/smtp/inbound/milter.rs | 5 +- tests/src/smtp/inbound/rcpt.rs | 23 +- tests/src/smtp/inbound/rewrite.rs | 45 +- tests/src/smtp/inbound/scripts.rs | 18 +- tests/src/smtp/inbound/sign.rs | 20 +- tests/src/smtp/inbound/throttle.rs | 11 +- tests/src/smtp/inbound/vrfy.rs | 12 +- tests/src/smtp/lookup/sql.rs | 18 +- tests/src/smtp/lookup/utils.rs | 25 +- tests/src/smtp/management/queue.rs | 11 +- tests/src/smtp/management/report.rs | 8 +- tests/src/smtp/mod.rs | 207 +++--- tests/src/smtp/outbound/dane.rs | 4 +- tests/src/smtp/outbound/extensions.rs | 3 +- tests/src/smtp/outbound/ip_lookup.rs | 3 +- tests/src/smtp/outbound/lmtp.rs | 15 +- tests/src/smtp/outbound/mta_sts.rs | 4 +- tests/src/smtp/outbound/smtp.rs | 10 +- tests/src/smtp/outbound/throttle.rs | 110 ++- tests/src/smtp/outbound/tls.rs | 4 +- tests/src/smtp/queue/dsn.rs | 18 +- tests/src/smtp/queue/retry.rs | 16 +- tests/src/smtp/reporting/analyze.rs | 3 +- tests/src/smtp/reporting/dmarc.rs | 16 +- tests/src/smtp/reporting/scheduler.rs | 5 +- tests/src/smtp/reporting/tls.rs | 15 +- 105 files changed, 5555 insertions(+), 4157 deletions(-) delete mode 100644 crates/smtp/src/config/condition.rs delete mode 100644 crates/smtp/src/config/if_block.rs delete mode 100644 crates/smtp/src/config/remote.rs create mode 100644 crates/smtp/src/config/shared.rs create mode 100644 crates/smtp/src/core/eval.rs delete mode 100644 crates/smtp/src/core/if_block.rs delete mode 100644 crates/utils/src/config/dynvalue.rs create mode 100644 crates/utils/src/config/if_block.rs create mode 100644 crates/utils/src/expr/eval.rs create mode 100644 crates/utils/src/expr/functions/array.rs create mode 100644 crates/utils/src/expr/functions/email.rs create mode 100644 crates/utils/src/expr/functions/misc.rs create mode 100644 crates/utils/src/expr/functions/mod.rs create mode 100644 crates/utils/src/expr/functions/text.rs create mode 100644 crates/utils/src/expr/mod.rs create mode 100644 crates/utils/src/expr/parser.rs create mode 100644 crates/utils/src/expr/tokenizer.rs diff --git a/Cargo.lock b/Cargo.lock index c8ee2794..79bea81c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6434,6 +6434,7 @@ dependencies = [ "proxy-header", "rand", "rcgen", + "regex", "reqwest", "ring 0.17.7", "rustls 0.22.1", diff --git a/crates/directory/src/core/config.rs b/crates/directory/src/core/config.rs index 94007a6b..27ce1c65 100644 --- a/crates/directory/src/core/config.rs +++ b/crates/directory/src/core/config.rs @@ -25,7 +25,6 @@ use deadpool::{ managed::{Manager, Pool}, Runtime, }; -use regex::Regex; use std::{sync::Arc, time::Duration}; use store::{Store, Stores}; use utils::config::{ @@ -40,7 +39,7 @@ use crate::{ imap::ImapDirectory, internal::manage::ManageDirectory, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory, sql::SqlDirectory, }, - AddressMapping, Directories, Directory, DirectoryInner, Lookup, + AddressMapping, Directories, Directory, DirectoryInner, }; use super::cache::CachedDirectory; @@ -64,7 +63,6 @@ impl ConfigDirectory for Config { ) -> utils::config::Result { let mut config = Directories { directories: AHashMap::new(), - lookups: AHashMap::new(), }; for id in self.sub_keys("directory", ".type") { @@ -154,16 +152,6 @@ impl ConfigDirectory for Config { blocked_ips: servers.blocked_ips.clone(), }); - // Add lookups - config.lookups.insert( - format!("{id}/domains"), - Lookup::DomainExists(directory.clone()), - ); - config.lookups.insert( - format!("{id}/recipients"), - Lookup::EmailExists(directory.clone()), - ); - // Add directory config.directories.insert(id.to_string(), directory); } @@ -183,18 +171,10 @@ impl AddressMapping { "Invalid value for address mapping {key:?}: {value:?}", )), } - } else if let Some(regex) = config.value((key.as_str(), "map")) { - Ok(AddressMapping::Custom { - regex: Regex::new(regex).map_err(|err| { - format!( - "Failed to compile regular expression {:?} for key {:?}: {}.", - regex, - (&key, "map").as_key(), - err - ) - })?, - mapping: config.property_require((key.as_str(), "to"))?, - }) + } else if let Some(if_block) = + config.parse_if_block(key, |name| Err(format!("Invalid variable name {name:?}.",)))? + { + Ok(AddressMapping::Custom(if_block)) } else { Ok(AddressMapping::Disable) } diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs index 73a11614..69626aa5 100644 --- a/crates/directory/src/core/dispatch.rs +++ b/crates/directory/src/core/dispatch.rs @@ -89,7 +89,7 @@ impl Directory { } pub async fn email_to_ids(&self, email: &str) -> crate::Result> { - let mut address = self.subaddressing.to_subaddress(email); + let mut address = self.subaddressing.to_subaddress(email).await; for _ in 0..2 { let result = match &self.store { DirectoryInner::Internal(store) => store.email_to_ids(address.as_ref()).await, @@ -102,7 +102,7 @@ impl Directory { if !result.is_empty() { return Ok(result); - } else if let Some(catch_all) = self.catch_all.to_catch_all(email) { + } else if let Some(catch_all) = self.catch_all.to_catch_all(email).await { address = catch_all; } else { break; @@ -139,7 +139,7 @@ impl Directory { pub async fn rcpt(&self, email: &str) -> crate::Result { // Expand subaddress - let mut address = self.subaddressing.to_subaddress(email); + let mut address = self.subaddressing.to_subaddress(email).await; // Check cache if let Some(cache) = &self.cache { @@ -164,7 +164,7 @@ impl Directory { cache.set_rcpt(address.as_ref(), true); } return Ok(true); - } else if let Some(catch_all) = self.catch_all.to_catch_all(email) { + } else if let Some(catch_all) = self.catch_all.to_catch_all(email).await { // Check cache if let Some(cache) = &self.cache { if let Some(result) = cache.get_rcpt(catch_all.as_ref()) { @@ -186,7 +186,7 @@ impl Directory { } pub async fn vrfy(&self, address: &str) -> crate::Result> { - let address = self.subaddressing.to_subaddress(address); + let address = self.subaddressing.to_subaddress(address).await; match &self.store { DirectoryInner::Internal(store) => store.vrfy(address.as_ref()).await, DirectoryInner::Ldap(store) => store.vrfy(address.as_ref()).await, @@ -198,7 +198,7 @@ impl Directory { } pub async fn expn(&self, address: &str) -> crate::Result> { - let address = self.subaddressing.to_subaddress(address); + let address = self.subaddressing.to_subaddress(address).await; match &self.store { DirectoryInner::Internal(store) => store.expn(address.as_ref()).await, DirectoryInner::Ldap(store) => store.expn(address.as_ref()).await, diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index bb5cbe6c..45532a22 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -37,7 +37,7 @@ use deadpool::managed::PoolError; use ldap3::LdapError; use mail_send::Credentials; use store::Store; -use utils::{config::DynValue, listener::blocked::BlockedIps}; +use utils::{config::if_block::IfBlock, expr::Variable, listener::blocked::BlockedIps}; pub mod backend; pub mod core; @@ -168,10 +168,7 @@ impl Type { #[derive(Debug, Default)] pub enum AddressMapping { Enable, - Custom { - regex: regex::Regex, - mapping: DynValue, - }, + Custom(IfBlock), #[default] Disable, } @@ -179,13 +176,6 @@ pub enum AddressMapping { #[derive(Default, Clone, Debug)] pub struct Directories { pub directories: AHashMap>, - pub lookups: AHashMap, -} - -#[derive(Clone, Debug)] -pub enum Lookup { - DomainExists(Arc), - EmailExists(Arc), } pub type Result = std::result::Result; @@ -300,7 +290,7 @@ impl DirectoryError { } impl AddressMapping { - pub fn to_subaddress<'x, 'y: 'x>(&'x self, address: &'y str) -> Cow<'x, str> { + pub async fn to_subaddress<'x, 'y: 'x>(&'x self, address: &'y str) -> Cow<'x, str> { match self { AddressMapping::Enable => { if let Some((local_part, domain_part)) = address.rsplit_once('@') { @@ -309,16 +299,16 @@ impl AddressMapping { } } } - AddressMapping::Custom { regex, mapping } => { - let mut regex_capture = Vec::new(); - for captures in regex.captures_iter(address) { - for capture in captures.iter() { - regex_capture.push(capture.map_or("", |m| m.as_str()).to_string()); - } - } - - if !regex_capture.is_empty() { - return mapping.apply(regex_capture, &()); + AddressMapping::Custom(if_block) => { + let result = if_block + .eval( + |_| Variable::default(), + |_, _| async { Variable::default() }, + ) + .await + .into_string(); + if !result.is_empty() { + return result.into_owned().into(); } } AddressMapping::Disable => (), @@ -327,21 +317,22 @@ impl AddressMapping { address.into() } - pub fn to_catch_all<'x, 'y: 'x>(&'x self, address: &'y str) -> Option> { + pub async fn to_catch_all<'x, 'y: 'x>(&'x self, address: &'y str) -> Option> { match self { AddressMapping::Enable => address .rsplit_once('@') .map(|(_, domain_part)| format!("@{}", domain_part)) .map(Cow::Owned), - AddressMapping::Custom { regex, mapping } => { - let mut regex_capture = Vec::new(); - for captures in regex.captures_iter(address) { - for capture in captures.iter() { - regex_capture.push(capture.map_or("", |m| m.as_str()).to_string()); - } - } - if !regex_capture.is_empty() { - Some(mapping.apply(regex_capture, &())) + AddressMapping::Custom(if_block) => { + let result = if_block + .eval( + |_| Variable::default(), + |_, _| async { Variable::default() }, + ) + .await + .into_string(); + if !result.is_empty() { + Some(result.into_owned().into()) } else { None } diff --git a/crates/main/Cargo.toml b/crates/main/Cargo.toml index 9d67d5ed..5f52b29d 100644 --- a/crates/main/Cargo.toml +++ b/crates/main/Cargo.toml @@ -32,7 +32,7 @@ jemallocator = "0.5.0" [features] #default = ["sqlite", "foundationdb", "postgres", "mysql", "rocks", "elastic", "s3", "redis"] -default = ["sqlite", "postgres", "mysql", "rocks", "elastic", "s3", "redis"] +default = ["sqlite", "postgres", "mysql"] sqlite = ["store/sqlite"] foundationdb = ["store/foundation"] postgres = ["store/postgres"] diff --git a/crates/smtp/src/config/auth.rs b/crates/smtp/src/config/auth.rs index 4f57c10c..9af8e692 100644 --- a/crates/smtp/src/config/auth.rs +++ b/crates/smtp/src/config/auth.rs @@ -28,82 +28,80 @@ use mail_auth::{ dkim::{Canonicalization, Done}, }; use mail_parser::decoders::base64::base64_decode; -use utils::config::{ - utils::{AsKey, ParseValue}, - Config, DynValue, +use utils::{ + config::{ + if_block::IfBlock, + utils::{AsKey, ConstantValue, ParseValue}, + Config, + }, + expr::{self, Constant, Token}, }; +use crate::core::eval::*; + use super::{ - if_block::ConfigIf, ArcAuthConfig, ArcSealer, ConfigContext, DkimAuthConfig, - DkimCanonicalization, DkimSigner, DmarcAuthConfig, EnvelopeKey, IfBlock, IpRevAuthConfig, - MailAuthConfig, SpfAuthConfig, VerifyStrategy, + map_expr_token, ArcAuthConfig, ArcSealer, ConfigContext, DkimAuthConfig, DkimCanonicalization, + DkimSigner, DmarcAuthConfig, IpRevAuthConfig, MailAuthConfig, SpfAuthConfig, VerifyStrategy, }; pub trait ConfigAuth { - fn parse_mail_auth(&self, ctx: &ConfigContext) -> super::Result; + fn parse_mail_auth(&self) -> super::Result; fn parse_signatures(&self, ctx: &mut ConfigContext) -> super::Result<()>; } impl ConfigAuth for Config { - fn parse_mail_auth(&self, ctx: &ConfigContext) -> super::Result { - let envelope_sender_keys = [ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - ]; - let envelope_conn_keys = [ - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - ]; + fn parse_mail_auth(&self) -> super::Result { + let fn_sender_keys = |name: &str| -> super::Result { + map_expr_token::( + name, + &[ + V_SENDER, + V_SENDER_DOMAIN, + V_PRIORITY, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + ], + ) + }; + let fn_conn_keys = |name: &str| -> super::Result { + map_expr_token::(name, &[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]) + }; Ok(MailAuthConfig { dkim: DkimAuthConfig { verify: self - .parse_if_block("auth.dkim.verify", ctx, &envelope_sender_keys)? + .parse_if_block("auth.dkim.verify", fn_sender_keys)? .unwrap_or_else(|| IfBlock::new(VerifyStrategy::Relaxed)), sign: self - .parse_if_block::>>( - "auth.dkim.sign", - ctx, - &envelope_sender_keys, - )? - .unwrap_or_default() - .map_if_block(&ctx.signers, "auth.dkim.sign", "signature")?, + .parse_if_block("auth.dkim.sign", fn_sender_keys)? + .unwrap_or_default(), }, arc: ArcAuthConfig { verify: self - .parse_if_block("auth.arc.verify", ctx, &envelope_sender_keys)? + .parse_if_block("auth.arc.verify", fn_sender_keys)? .unwrap_or_else(|| IfBlock::new(VerifyStrategy::Relaxed)), seal: self - .parse_if_block::>>( - "auth.arc.seal", - ctx, - &envelope_sender_keys, - )? - .unwrap_or_default() - .map_if_block(&ctx.sealers, "auth.arc.seal", "signature")?, + .parse_if_block("auth.arc.seal", fn_sender_keys)? + .unwrap_or_default(), }, spf: SpfAuthConfig { verify_ehlo: self - .parse_if_block("auth.spf.verify.ehlo", ctx, &envelope_conn_keys)? + .parse_if_block("auth.spf.verify.ehlo", fn_conn_keys)? .unwrap_or_else(|| IfBlock::new(VerifyStrategy::Relaxed)), verify_mail_from: self - .parse_if_block("auth.spf.verify.mail-from", ctx, &envelope_conn_keys)? + .parse_if_block("auth.spf.verify.mail-from", fn_conn_keys)? .unwrap_or_else(|| IfBlock::new(VerifyStrategy::Relaxed)), }, dmarc: DmarcAuthConfig { verify: self - .parse_if_block("auth.dmarc.verify", ctx, &envelope_sender_keys)? + .parse_if_block("auth.dmarc.verify", fn_sender_keys)? .unwrap_or_else(|| IfBlock::new(VerifyStrategy::Relaxed)), }, iprev: IpRevAuthConfig { verify: self - .parse_if_block("auth.iprev.verify", ctx, &envelope_conn_keys)? + .parse_if_block("auth.iprev.verify", fn_conn_keys)? .unwrap_or_else(|| IfBlock::new(VerifyStrategy::Relaxed)), }, }) @@ -301,6 +299,32 @@ fn parse_signature>( Ok((signer, sealer)) } +impl<'x> TryFrom> for VerifyStrategy { + type Error = (); + + fn try_from(value: expr::Variable<'x>) -> Result { + match value { + expr::Variable::Integer(c) => match c { + 0 => Ok(VerifyStrategy::Relaxed), + 1 => Ok(VerifyStrategy::Strict), + 2 => Ok(VerifyStrategy::Disable), + _ => Err(()), + }, + _ => Err(()), + } + } +} + +impl From for Constant { + fn from(value: VerifyStrategy) -> Self { + Constant::Integer(match value { + VerifyStrategy::Relaxed => 0, + VerifyStrategy::Strict => 1, + VerifyStrategy::Disable => 2, + }) + } +} + impl ParseValue for VerifyStrategy { fn parse_value(key: impl AsKey, value: &str) -> super::Result { match value { @@ -316,6 +340,8 @@ impl ParseValue for VerifyStrategy { } } +impl ConstantValue for VerifyStrategy {} + impl ParseValue for DkimCanonicalization { fn parse_value(key: impl AsKey, value: &str) -> super::Result { if let Some((headers, body)) = value.split_once('/') { diff --git a/crates/smtp/src/config/condition.rs b/crates/smtp/src/config/condition.rs deleted file mode 100644 index 163b9311..00000000 --- a/crates/smtp/src/config/condition.rs +++ /dev/null @@ -1,315 +0,0 @@ -/* - * 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 regex::Regex; - -use crate::config::StringMatch; - -use super::{Condition, ConditionMatch, Conditions, ConfigContext, EnvelopeKey}; -use utils::config::{ - utils::{AsKey, ParseKey}, - Config, -}; - -pub trait ConfigCondition { - fn parse_condition( - &self, - key: impl AsKey, - ctx: &ConfigContext, - available_keys: &[EnvelopeKey], - ) -> super::Result; - #[cfg(feature = "test_mode")] - fn parse_conditions( - &self, - ctx: &ConfigContext, - ) -> super::Result>; -} - -impl ConfigCondition for Config { - fn parse_condition( - &self, - key_: impl AsKey, - ctx: &ConfigContext, - available_keys: &[EnvelopeKey], - ) -> super::Result { - let mut conditions = Vec::new(); - let mut stack = Vec::new(); - let mut iter = None; - let mut jmp_pos = Vec::new(); - let mut prefix = key_.as_key(); - let mut is_all = false; - let mut is_not = false; - - 'outer: loop { - let mut op_str = ""; - - for key in self.sub_keys(prefix.as_str(), "") { - if !["if", "then"].contains(&key) { - if op_str.is_empty() { - op_str = key; - } else { - return Err(format!( - "Multiple operations found for condition {prefix:?}.", - )); - } - } - } - - if op_str.is_empty() { - return Err(format!("Missing operation for condition {prefix:?}.")); - } else if ["any-of", "all-of", "none-of"].contains(&op_str) { - stack.push(( - std::mem::replace( - &mut iter, - self.sub_keys((&prefix, op_str).as_key(), "") - .peekable() - .into(), - ), - (&prefix, op_str).as_key(), - std::mem::take(&mut jmp_pos), - is_all, - is_not, - )); - - match op_str { - "any-of" => { - if !is_not { - is_all = false; - is_not = false; - } else { - is_all = true; - is_not = true; - } - } - "all-of" => { - if !is_not { - is_all = true; - is_not = false; - } else { - is_all = false; - is_not = true; - } - } - _ => { - is_not = !is_not; - if !is_not { - is_all = true; - is_not = false; - } else { - is_all = false; - is_not = true; - } - } - } - } else { - let key = self.property_require::((&prefix, "if"))?; - if !available_keys.contains(&key) { - return Err(format!( - "Envelope key {key:?} is not available in this context for property {prefix:?}", - )); - } - - enum MatchType { - Equal, - Regex, - Lookup, - StartsWith, - EndsWith, - } - - let (op, op_is_not) = match op_str { - "eq" | "equal-to" | "ne" | "not-equal-to" => { - (MatchType::Equal, op_str == "ne" || op_str == "not-equal-to") - } - "in-list" | "not-in-list" => (MatchType::Lookup, op_str == "not-in-list"), - "matches" | "not-matches" => (MatchType::Regex, op_str.starts_with("not-")), - "starts-with" | "not-starts-with" => { - (MatchType::StartsWith, op_str == "not-starts-with") - } - "ends-with" | "not-ends-with" => { - (MatchType::EndsWith, op_str == "not-ends-with") - } - _ => { - return Err(format!("Invalid operation {op_str:?} for key {prefix:?}.")); - } - }; - - let value_str = self.value_require((&prefix, op_str))?; - let value = match (key, &op) { - (EnvelopeKey::Listener, MatchType::Equal) => { - ConditionMatch::UInt(if value_str != "sieve" { - ctx.servers - .iter() - .find_map(|s| { - if s.id == value_str { - s.internal_id.into() - } else { - None - } - }) - .ok_or_else(|| { - format!( - "Listener {:?} does not exist for property {:?}.", - value_str, - (&prefix, op_str).as_key() - ) - })? - } else { - u16::MAX - }) - } - (EnvelopeKey::LocalIp | EnvelopeKey::RemoteIp, MatchType::Equal) => { - ConditionMatch::IpAddrMask(value_str.parse_key((&prefix, op_str))?) - } - (EnvelopeKey::Priority, MatchType::Equal) => { - ConditionMatch::Int(value_str.parse_key((&prefix, op_str))?) - } - ( - EnvelopeKey::Recipient - | EnvelopeKey::RecipientDomain - | EnvelopeKey::Sender - | EnvelopeKey::SenderDomain - | EnvelopeKey::AuthenticatedAs - | EnvelopeKey::Mx - | EnvelopeKey::LocalIp - | EnvelopeKey::RemoteIp, - _, - ) => match op { - MatchType::Equal => { - ConditionMatch::String(StringMatch::Equal(value_str.to_string())) - } - MatchType::StartsWith => { - ConditionMatch::String(StringMatch::StartsWith(value_str.to_string())) - } - MatchType::EndsWith => { - ConditionMatch::String(StringMatch::EndsWith(value_str.to_string())) - } - MatchType::Regex => { - ConditionMatch::Regex(Regex::new(value_str).map_err(|err| { - format!( - "Failed to compile regular expression {:?} for key {:?}: {}.", - value_str, - (&prefix, value_str).as_key(), - err - ) - })?) - } - MatchType::Lookup => { - if let Some(lookup) = ctx.directory.lookups.get(value_str) { - ConditionMatch::Lookup(lookup.clone().into()) - } else if let Some(lookup) = ctx.stores.lookup_stores.get(value_str) { - ConditionMatch::Lookup(lookup.clone().into()) - } else { - return Err(format!( - "Lookup {:?} not found for property {:?}.", - value_str, - (&prefix, value_str).as_key() - )); - } - } - }, - _ => { - return Err(format!( - "Invalid 'op'/'value' combination for key {:?}.", - key_.as_key() - )); - } - }; - conditions.push(Condition::Match { - key, - value, - not: is_not ^ op_is_not, - }); - if iter.as_mut().map_or(false, |it| it.peek().is_some()) { - jmp_pos.push(conditions.len()); - conditions.push(if is_all { - Condition::JumpIfFalse { - positions: usize::MAX, - } - } else { - Condition::JumpIfTrue { - positions: usize::MAX, - } - }); - } - } - - loop { - if let Some(array_pos) = iter.as_mut().and_then(|it| it.next()) { - prefix = (stack.last().unwrap().1.as_str(), array_pos).as_key(); - break; - } else if let Some((prev_iter, _, prev_jmp_pos, prev_is_all, prev_is_not)) = - stack.pop() - { - let cur_pos = conditions.len() - 1; - for pos in jmp_pos { - if let Condition::JumpIfFalse { positions } - | Condition::JumpIfTrue { positions } = &mut conditions[pos] - { - *positions = cur_pos - pos; - } - } - - iter = prev_iter; - jmp_pos = prev_jmp_pos; - is_all = prev_is_all; - is_not = prev_is_not; - } else { - break 'outer; - } - } - } - - Ok(Conditions { conditions }) - } - - #[cfg(feature = "test_mode")] - fn parse_conditions( - &self, - ctx: &ConfigContext, - ) -> super::Result> { - use ahash::AHashMap; - let mut conditions = AHashMap::new(); - let available_keys = vec![ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, - EnvelopeKey::Mx, - ]; - - for rule_name in self.sub_keys("rule", "") { - conditions.insert( - rule_name.to_string(), - self.parse_condition(("rule", rule_name), ctx, &available_keys)?, - ); - } - - Ok(conditions) - } -} diff --git a/crates/smtp/src/config/if_block.rs b/crates/smtp/src/config/if_block.rs deleted file mode 100644 index 8414b9f9..00000000 --- a/crates/smtp/src/config/if_block.rs +++ /dev/null @@ -1,341 +0,0 @@ -/* - * 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 std::sync::Arc; - -use ahash::AHashMap; - -use super::{ - condition::ConfigCondition, ConfigContext, EnvelopeKey, IfBlock, IfThen, MaybeDynValue, -}; -use utils::config::{ - utils::{AsKey, ParseValues}, - Config, DynValue, -}; - -pub trait ConfigIf { - fn parse_if_block( - &self, - prefix: impl AsKey, - ctx: &ConfigContext, - available_keys: &[EnvelopeKey], - ) -> super::Result>>; -} - -impl ConfigIf for Config { - fn parse_if_block( - &self, - prefix: impl AsKey, - ctx: &ConfigContext, - available_keys: &[EnvelopeKey], - ) -> super::Result>> { - let key = prefix.as_key(); - let prefix = prefix.as_prefix(); - - let mut found_if = false; - let mut found_else = ""; - let mut found_then = false; - - // Parse conditions - let mut if_block = IfBlock::new(T::default()); - let mut last_array_pos = ""; - - for item in self.keys.keys() { - if let Some(suffix_) = item.strip_prefix(&prefix) { - if let Some((array_pos, suffix)) = suffix_.split_once('.') { - let if_key = suffix.split_once('.').map(|(v, _)| v).unwrap_or(suffix); - if ["if", "any-of", "all-of", "none-of"].contains(&if_key) { - if array_pos != last_array_pos { - if !last_array_pos.is_empty() && !found_then && !T::is_multivalue() { - return Err(format!( - "Missing 'then' in 'if' condition {} for property {:?}.", - last_array_pos.parse().unwrap_or(0) + 1, - key - )); - } - - if_block.if_then.push(IfThen { - conditions: self.parse_condition( - (key.as_str(), array_pos), - ctx, - available_keys, - )?, - then: T::default(), - }); - - found_then = false; - last_array_pos = array_pos; - } - - found_if = true; - } else if if_key == "else" { - if found_else.is_empty() { - if found_if { - if_block.default = T::parse_values( - (key.as_str(), suffix_.split_once(".else").unwrap().0, "else"), - self, - )?; - found_else = array_pos; - } else { - return Err(format!( - "Found 'else' before 'if' for property {key:?}.", - )); - } - } else if array_pos != found_else { - return Err(format!("Multiple 'else' found for property {key:?}.")); - } - } else if if_key == "then" { - if found_else.is_empty() { - if array_pos == last_array_pos { - if !found_then { - if_block.if_then.last_mut().unwrap().then = T::parse_values( - ( - key.as_str(), - suffix_.split_once(".then").unwrap().0, - "then", - ), - self, - )?; - found_then = true; - } - } else { - return Err(format!( - "Found 'then' without 'if' for property {key:?}.", - )); - } - } else { - return Err(format!( - "Found 'then' in 'else' block for property {key:?}.", - )); - } - } - } else if !found_if { - // Found probably a multi-value, parse and return - if_block.default = T::parse_values(key.as_str(), self)?; - return Ok(Some(if_block)); - } else { - return Err(format!("Invalid property {item:?} found in 'if' block.")); - } - } else if item == &key { - // There is a single value, parse and return - if_block.default = T::parse_values(key.as_str(), self)?; - return Ok(Some(if_block)); - } - } - - if !found_if { - Ok(None) - } else if !found_then && !T::is_multivalue() { - Err(format!( - "Missing 'then' in 'if' condition {} for property {:?}.", - last_array_pos.parse().unwrap_or(0) + 1, - key - )) - } else if found_else.is_empty() && !T::is_multivalue() { - Err(format!("Missing 'else' for property {key:?}.")) - } else { - Ok(Some(if_block)) - } - } -} - -impl IfBlock { - pub fn new(value: T) -> Self { - Self { - if_then: Vec::with_capacity(0), - default: value, - } - } -} - -impl IfBlock> { - pub fn try_unwrap(self, key: &str) -> super::Result> { - let mut if_then = Vec::with_capacity(self.if_then.len()); - for if_clause in self.if_then { - if_then.push(IfThen { - conditions: if_clause.conditions, - then: if_clause - .then - .ok_or_else(|| format!("Property {key:?} cannot contain null values."))?, - }); - } - - Ok(IfBlock { - if_then, - default: self - .default - .ok_or_else(|| format!("Property {key:?} cannot contain null values."))?, - }) - } -} - -impl IfBlock> { - pub fn is_empty(&self) -> bool { - self.default.is_none() && self.if_then.is_empty() - } -} - -impl IfBlock> { - pub fn map_if_block( - self, - map: &AHashMap>, - key_name: impl AsKey, - object_name: &str, - ) -> super::Result>>> { - let key_name = key_name.as_key(); - let mut if_then = Vec::with_capacity(self.if_then.len()); - for if_clause in self.if_then.into_iter() { - if_then.push(IfThen { - conditions: if_clause.conditions, - then: Self::map_value(map, if_clause.then, object_name, &key_name)?, - }); - } - - Ok(IfBlock { - if_then, - default: Self::map_value(map, self.default, object_name, &key_name)?, - }) - } - - fn map_value( - map: &AHashMap>, - value: Option, - object_name: &str, - key_name: &str, - ) -> super::Result>> { - if let Some(value) = value { - if let Some(value) = map.get(&value) { - Ok(Some(value.clone())) - } else { - Err(format!( - "Unable to find {object_name} {value:?} declared for {key_name:?}", - )) - } - } else { - Ok(None) - } - } -} - -impl IfBlock>> { - pub fn map_if_block( - self, - map: &AHashMap>, - key_name: &str, - object_name: &str, - ) -> super::Result>>> { - let mut if_then = Vec::with_capacity(self.if_then.len()); - for if_clause in self.if_then.into_iter() { - if_then.push(IfThen { - conditions: if_clause.conditions, - then: Self::map_value(map, if_clause.then, object_name, key_name)?, - }); - } - - Ok(IfBlock { - if_then, - default: Self::map_value(map, self.default, object_name, key_name)?, - }) - } - - fn map_value( - map: &AHashMap>, - values: Vec>, - object_name: &str, - key_name: &str, - ) -> super::Result>> { - let mut result = Vec::with_capacity(values.len()); - for value in values { - if let DynValue::String(value) = &value { - if let Some(value) = map.get(value) { - result.push(MaybeDynValue::Static(value.clone())); - } else { - return Err(format!( - "Unable to find {object_name} {value:?} declared for {key_name:?}", - )); - } - } else { - result.push(MaybeDynValue::Dynamic { - eval: value, - items: map.clone(), - }); - } - } - Ok(result) - } -} - -impl IfBlock>> { - pub fn map_if_block( - self, - map: &AHashMap>, - key_name: impl AsKey, - object_name: &str, - ) -> super::Result>>> { - let key_name = key_name.as_key(); - let mut if_then = Vec::with_capacity(self.if_then.len()); - for if_clause in self.if_then.into_iter() { - if_then.push(IfThen { - conditions: if_clause.conditions, - then: Self::map_value(map, if_clause.then, object_name, &key_name)?, - }); - } - - Ok(IfBlock { - if_then, - default: Self::map_value(map, self.default, object_name, &key_name)?, - }) - } - - fn map_value( - map: &AHashMap>, - value: Option>, - object_name: &str, - key_name: &str, - ) -> super::Result>> { - if let Some(value) = value { - if let DynValue::String(value) = &value { - if let Some(value) = map.get(value) { - Ok(Some(MaybeDynValue::Static(value.clone()))) - } else { - Err(format!( - "Unable to find {object_name} {value:?} declared for {key_name:?}", - )) - } - } else { - Ok(Some(MaybeDynValue::Dynamic { - eval: value, - items: map.clone(), - })) - } - } else { - Ok(None) - } - } -} - -impl IfBlock> { - pub fn has_empty_list(&self) -> bool { - self.default.is_empty() || self.if_then.iter().any(|v| v.then.is_empty()) - } -} diff --git a/crates/smtp/src/config/mod.rs b/crates/smtp/src/config/mod.rs index 1eb24bb7..f2c19033 100644 --- a/crates/smtp/src/config/mod.rs +++ b/crates/smtp/src/config/mod.rs @@ -22,67 +22,39 @@ */ pub mod auth; -pub mod condition; -pub mod if_block; pub mod queue; -pub mod remote; pub mod report; pub mod resolver; pub mod scripts; pub mod session; +pub mod shared; pub mod throttle; use std::{ - net::{Ipv4Addr, Ipv6Addr, SocketAddr}, + net::SocketAddr, path::PathBuf, sync::{atomic::AtomicU64, Arc}, time::Duration, }; use ahash::AHashMap; -use directory::{Directories, Directory}; +use directory::Directories; use mail_auth::{ common::crypto::{Ed25519Key, RsaKey, Sha256}, dkim::{Canonicalization, Done}, - IpLookupStrategy, }; use mail_send::Credentials; -use regex::Regex; use sieve::Sieve; -use smtp_proto::MtPriority; -use store::{LookupStore, Store, Stores}; -use utils::config::{ipmask::IpAddrMask, DynValue, Rate, Server, ServerProtocol}; +use store::Stores; +use utils::{ + config::{if_block::IfBlock, utils::ConstantValue, Rate, Server, ServerProtocol}, + expr::{Expression, Token}, +}; -use crate::{core::Lookup, inbound::milter}; - -#[derive(Debug)] -pub struct Host { - pub address: String, - pub port: u16, - pub protocol: ServerProtocol, - pub concurrency: usize, - pub timeout: Duration, - pub tls_implicit: bool, - pub tls_allow_invalid_certs: bool, - pub username: Option, - pub secret: Option, -} - -#[derive(Debug, Clone)] -#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] -pub enum Condition { - Match { - key: EnvelopeKey, - value: ConditionMatch, - not: bool, - }, - JumpIfTrue { - positions: usize, - }, - JumpIfFalse { - positions: usize, - }, -} +use crate::{ + core::eval::{FUNCTIONS_MAP, VARIABLES_MAP}, + inbound::milter, +}; #[derive(Debug, PartialEq, Eq, Clone)] pub enum StringMatch { @@ -91,92 +63,10 @@ pub enum StringMatch { EndsWith(String), } -#[derive(Clone)] -pub enum ConditionMatch { - String(StringMatch), - UInt(u16), - Int(i16), - IpAddrMask(IpAddrMask), - Lookup(Lookup), - Regex(Regex), -} - -#[cfg(feature = "test_mode")] -impl PartialEq for ConditionMatch { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Self::String(l0), Self::String(r0)) => l0 == r0, - (Self::UInt(l0), Self::UInt(r0)) => l0 == r0, - (Self::Int(l0), Self::Int(r0)) => l0 == r0, - (Self::IpAddrMask(l0), Self::IpAddrMask(r0)) => l0 == r0, - (Self::Lookup(l0), Self::Lookup(r0)) => l0 == r0, - (Self::Regex(_), Self::Regex(_)) => false, - _ => false, - } - } -} - -impl core::fmt::Debug for ConditionMatch { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::String(arg0) => f.debug_tuple("String").field(arg0).finish(), - Self::UInt(arg0) => f.debug_tuple("UInt").field(arg0).finish(), - Self::Int(arg0) => f.debug_tuple("Int").field(arg0).finish(), - Self::IpAddrMask(arg0) => f.debug_tuple("IpAddrMask").field(arg0).finish(), - Self::Lookup(_) => f.debug_tuple("Lookup").finish(), - Self::Regex(arg0) => f.debug_tuple("Regex").field(arg0).finish(), - } - } -} - -#[cfg(feature = "test_mode")] -impl Eq for ConditionMatch {} - -impl Default for Condition { - fn default() -> Self { - Condition::JumpIfFalse { positions: 0 } - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -pub enum EnvelopeKey { - Recipient, - RecipientDomain, - Sender, - SenderDomain, - Mx, - HeloDomain, - AuthenticatedAs, - Listener, - RemoteIp, - LocalIp, - Priority, -} - -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] -pub struct IfThen { - pub conditions: Conditions, - pub then: T, -} - -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] -pub struct Conditions { - pub conditions: Vec, -} - -#[derive(Debug, Clone, Default)] -#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] -pub struct IfBlock { - pub if_then: Vec>, - pub default: T, -} - #[derive(Debug, Default)] #[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] pub struct Throttle { - pub conditions: Conditions, + pub expr: Expression, pub keys: u16, pub concurrency: Option, pub rate: Option, @@ -194,84 +84,84 @@ pub const THROTTLE_LOCAL_IP: u16 = 1 << 8; pub const THROTTLE_HELO_DOMAIN: u16 = 1 << 9; pub struct Connect { - pub script: IfBlock>>, + pub script: IfBlock, } pub struct Ehlo { - pub script: IfBlock>>, - pub require: IfBlock, - pub reject_non_fqdn: IfBlock, + pub script: IfBlock, + pub require: IfBlock, + pub reject_non_fqdn: IfBlock, } pub struct Extensions { - pub pipelining: IfBlock, - pub chunking: IfBlock, - pub requiretls: IfBlock, - pub dsn: IfBlock, - pub vrfy: IfBlock, - pub expn: IfBlock, - pub no_soliciting: IfBlock>, - pub future_release: IfBlock>, - pub deliver_by: IfBlock>, - pub mt_priority: IfBlock>, + pub pipelining: IfBlock, + pub chunking: IfBlock, + pub requiretls: IfBlock, + pub dsn: IfBlock, + pub vrfy: IfBlock, + pub expn: IfBlock, + pub no_soliciting: IfBlock, + pub future_release: IfBlock, + pub deliver_by: IfBlock, + pub mt_priority: IfBlock, } pub struct Auth { - pub directory: IfBlock>>, - pub mechanisms: IfBlock, - pub require: IfBlock, - pub allow_plain_text: IfBlock, - pub must_match_sender: IfBlock, - pub errors_max: IfBlock, - pub errors_wait: IfBlock, + pub directory: IfBlock, + pub mechanisms: IfBlock, + pub require: IfBlock, + pub allow_plain_text: IfBlock, + pub must_match_sender: IfBlock, + pub errors_max: IfBlock, + pub errors_wait: IfBlock, } pub struct Mail { - pub script: IfBlock>>, - pub rewrite: IfBlock>>, + pub script: IfBlock, + pub rewrite: IfBlock, } pub struct Rcpt { - pub script: IfBlock>>, - pub relay: IfBlock, - pub directory: IfBlock>>, - pub rewrite: IfBlock>>, + pub script: IfBlock, + pub relay: IfBlock, + pub directory: IfBlock, + pub rewrite: IfBlock, // Errors - pub errors_max: IfBlock, - pub errors_wait: IfBlock, + pub errors_max: IfBlock, + pub errors_wait: IfBlock, // Limits - pub max_recipients: IfBlock, + pub max_recipients: IfBlock, } pub struct Data { - pub script: IfBlock>>, + pub script: IfBlock, pub pipe_commands: Vec, pub milters: Vec, // Limits - pub max_messages: IfBlock, - pub max_message_size: IfBlock, - pub max_received_headers: IfBlock, + pub max_messages: IfBlock, + pub max_message_size: IfBlock, + pub max_received_headers: IfBlock, // Headers - pub add_received: IfBlock, - pub add_received_spf: IfBlock, - pub add_return_path: IfBlock, - pub add_auth_results: IfBlock, - pub add_message_id: IfBlock, - pub add_date: IfBlock, + pub add_received: IfBlock, + pub add_received_spf: IfBlock, + pub add_return_path: IfBlock, + pub add_auth_results: IfBlock, + pub add_message_id: IfBlock, + pub add_date: IfBlock, } pub struct Pipe { - pub command: IfBlock>, - pub arguments: IfBlock>, - pub timeout: IfBlock, + pub command: IfBlock, + pub arguments: IfBlock, + pub timeout: IfBlock, } pub struct Milter { - pub enable: IfBlock, + pub enable: IfBlock, pub addrs: Vec, pub hostname: String, pub port: u16, @@ -288,9 +178,9 @@ pub struct Milter { } pub struct SessionConfig { - pub timeout: IfBlock, - pub duration: IfBlock, - pub transfer_limit: IfBlock, + pub timeout: IfBlock, + pub duration: IfBlock, + pub transfer_limit: IfBlock, pub throttle: SessionThrottle, pub connect: Connect, @@ -318,20 +208,20 @@ pub struct RelayHost { } pub struct QueueConfig { - pub path: IfBlock, - pub hash: IfBlock, + pub path: PathBuf, + pub hash: IfBlock, // Schedule - pub retry: IfBlock>, - pub notify: IfBlock>, - pub expire: IfBlock, + pub retry: IfBlock, + pub notify: IfBlock, + pub expire: IfBlock, // Outbound - pub hostname: IfBlock, - pub next_hop: IfBlock>, - pub max_mx: IfBlock, - pub max_multihomed: IfBlock, - pub ip_strategy: IfBlock, + pub hostname: IfBlock, + pub next_hop: IfBlock, + pub max_mx: IfBlock, + pub max_multihomed: IfBlock, + pub ip_strategy: IfBlock, pub source_ip: QueueOutboundSourceIp, pub tls: QueueOutboundTls, pub dsn: Dsn, @@ -342,22 +232,17 @@ pub struct QueueConfig { // Throttle and Quotas pub throttle: QueueThrottle, pub quota: QueueQuotas, - - // Default store and directory - pub directory: Arc, - pub data_store: Store, - pub lookup_store: LookupStore, } pub struct QueueOutboundSourceIp { - pub ipv4: IfBlock>, - pub ipv6: IfBlock>, + pub ipv4: IfBlock, + pub ipv6: IfBlock, } pub struct ReportConfig { - pub path: IfBlock, - pub hash: IfBlock, - pub submitter: IfBlock, + pub path: PathBuf, + pub hash: IfBlock, + pub submitter: IfBlock, pub analysis: ReportAnalysis, pub dkim: Report, @@ -380,55 +265,46 @@ pub enum AddressMatch { Equals(String), } -#[derive(Clone)] -pub enum MaybeDynValue { - Dynamic { - eval: DynValue, - items: AHashMap>, - }, - Static(Arc), -} - pub struct Dsn { - pub name: IfBlock, - pub address: IfBlock, - pub sign: IfBlock>>, + pub name: IfBlock, + pub address: IfBlock, + pub sign: IfBlock, } pub struct AggregateReport { - pub name: IfBlock, - pub address: IfBlock, - pub org_name: IfBlock>, - pub contact_info: IfBlock>, - pub send: IfBlock, - pub sign: IfBlock>>, - pub max_size: IfBlock, + pub name: IfBlock, + pub address: IfBlock, + pub org_name: IfBlock, + pub contact_info: IfBlock, + pub send: IfBlock, + pub sign: IfBlock, + pub max_size: IfBlock, } pub struct Report { - pub name: IfBlock, - pub address: IfBlock, - pub subject: IfBlock, - pub sign: IfBlock>>, - pub send: IfBlock>, + pub name: IfBlock, + pub address: IfBlock, + pub subject: IfBlock, + pub sign: IfBlock, + pub send: IfBlock, } pub struct QueueOutboundTls { - pub dane: IfBlock, - pub mta_sts: IfBlock, - pub start: IfBlock, - pub invalid_certs: IfBlock, + pub dane: IfBlock, + pub mta_sts: IfBlock, + pub start: IfBlock, + pub invalid_certs: IfBlock, } pub struct QueueOutboundTimeout { - pub connect: IfBlock, - pub greeting: IfBlock, - pub tls: IfBlock, - pub ehlo: IfBlock, - pub mail: IfBlock, - pub rcpt: IfBlock, - pub data: IfBlock, - pub mta_sts: IfBlock, + pub connect: IfBlock, + pub greeting: IfBlock, + pub tls: IfBlock, + pub ehlo: IfBlock, + pub mail: IfBlock, + pub rcpt: IfBlock, + pub data: IfBlock, + pub mta_sts: IfBlock, } #[derive(Debug)] @@ -445,7 +321,7 @@ pub struct QueueQuotas { } pub struct QueueQuota { - pub conditions: Conditions, + pub expr: Expression, pub keys: u16, pub size: Option, pub messages: Option, @@ -494,25 +370,25 @@ pub enum ArcSealer { } pub struct DkimAuthConfig { - pub verify: IfBlock, - pub sign: IfBlock>>, + pub verify: IfBlock, + pub sign: IfBlock, } pub struct ArcAuthConfig { - pub verify: IfBlock, - pub seal: IfBlock>>, + pub verify: IfBlock, + pub seal: IfBlock, } pub struct SpfAuthConfig { - pub verify_ehlo: IfBlock, - pub verify_mail_from: IfBlock, + pub verify_ehlo: IfBlock, + pub verify_mail_from: IfBlock, } pub struct DmarcAuthConfig { - pub verify: IfBlock, + pub verify: IfBlock, } pub struct IpRevAuthConfig { - pub verify: IfBlock, + pub verify: IfBlock, } #[derive(Debug, Clone)] @@ -532,10 +408,9 @@ pub enum VerifyStrategy { #[derive(Default)] pub struct ConfigContext<'x> { pub servers: &'x [Server], - pub hosts: AHashMap, - pub scripts: AHashMap>, pub directory: Directories, pub stores: Stores, + pub scripts: AHashMap>, pub signers: AHashMap>, pub sealers: AHashMap>, } @@ -549,6 +424,35 @@ impl<'x> ConfigContext<'x> { } } +pub fn map_expr_token(name: &str, allowed_vars: &[u32]) -> Result { + VARIABLES_MAP + .iter() + .find(|(n, _)| n == &name) + .and_then(|(_, id)| { + if allowed_vars.contains(id) { + Some(Token::Variable(*id)) + } else { + None + } + }) + .or_else(|| { + FUNCTIONS_MAP + .iter() + .find(|(n, _, _)| n == &name) + .map(|(name, id, num_args)| Token::Function { + name: (*name).into(), + id: *id, + num_args: *num_args, + }) + }) + .or_else(|| { + F::parse_value("", name) + .map(|v| Token::Constant(v.into())) + .ok() + }) + .ok_or_else(|| format!("Invalid variable: {name:?}")) +} + impl std::fmt::Debug for RelayHost { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RelayHost") diff --git a/crates/smtp/src/config/queue.rs b/crates/smtp/src/config/queue.rs index 7ede3adb..e689cd34 100644 --- a/crates/smtp/src/config/queue.rs +++ b/crates/smtp/src/config/queue.rs @@ -23,221 +23,205 @@ use std::time::Duration; -use mail_send::Credentials; +use mail_auth::IpLookupStrategy; + +use crate::core::eval::*; use super::{ - condition::ConfigCondition, - if_block::ConfigIf, + map_expr_token, throttle::{ConfigThrottle, ParseTrottleKey}, - *, + Dsn, QueueConfig, QueueOutboundSourceIp, QueueOutboundTimeout, QueueOutboundTls, QueueQuota, + QueueQuotas, QueueThrottle, RequireOptional, THROTTLE_LOCAL_IP, THROTTLE_MX, THROTTLE_RCPT, + THROTTLE_RCPT_DOMAIN, THROTTLE_REMOTE_IP, THROTTLE_SENDER, THROTTLE_SENDER_DOMAIN, }; -use utils::config::{ - utils::{AsKey, ParseValue}, - Config, DynValue, +use utils::{ + config::{ + if_block::IfBlock, + utils::{AsKey, ConstantValue, NoConstants, ParseValue}, + Config, + }, + expr::{Constant, Expression, ExpressionItem, Variable}, }; pub trait ConfigQueue { - fn parse_queue(&self, ctx: &ConfigContext) -> super::Result; - fn parse_queue_throttle(&self, ctx: &ConfigContext) -> super::Result; - fn parse_queue_quota(&self, ctx: &ConfigContext) -> super::Result; - fn parse_queue_quota_item( - &self, - prefix: impl AsKey, - ctx: &ConfigContext, - ) -> super::Result; + fn parse_queue(&self) -> super::Result; + fn parse_queue_throttle(&self) -> super::Result; + fn parse_queue_quota(&self) -> super::Result; + fn parse_queue_quota_item(&self, prefix: impl AsKey) -> super::Result; } impl ConfigQueue for Config { - fn parse_queue(&self, ctx: &ConfigContext) -> super::Result { - let rcpt_envelope_keys = [ - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, + fn parse_queue(&self) -> super::Result { + let rcpt_envelope_keys = &[V_RECIPIENT_DOMAIN, V_SENDER, V_SENDER_DOMAIN, V_PRIORITY]; + let sender_envelope_keys = &[V_SENDER, V_SENDER_DOMAIN, V_PRIORITY]; + let mx_envelope_keys = &[ + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_PRIORITY, + V_MX, ]; - let sender_envelope_keys = [ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, + let host_envelope_keys = &[ + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_PRIORITY, + V_LOCAL_IP, + V_REMOTE_IP, + V_MX, ]; - let mx_envelope_keys = [ - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, - EnvelopeKey::Mx, - ]; - let host_envelope_keys = [ - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, - EnvelopeKey::LocalIp, - EnvelopeKey::RemoteIp, - EnvelopeKey::Mx, - ]; - - let next_hop = self - .parse_if_block::>("queue.outbound.next-hop", ctx, &rcpt_envelope_keys)? - .unwrap_or_else(|| IfBlock::new(None)); let default_hostname = self.value_require("server.hostname")?; let config = QueueConfig { - path: self - .parse_if_block("queue.path", ctx, &sender_envelope_keys)? - .ok_or("Missing \"queue.path\" property.")?, + path: self.property_require("queue.path")?, hash: self - .parse_if_block("queue.hash", ctx, &sender_envelope_keys)? + .parse_if_block("queue.hash", |name| { + map_expr_token::(name, sender_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(32)), retry: self - .parse_if_block("queue.schedule.retry", ctx, &host_envelope_keys)? - .unwrap_or_else(|| { - IfBlock::new(vec![ - Duration::from_secs(60), - Duration::from_secs(2 * 60), - Duration::from_secs(5 * 60), - Duration::from_secs(10 * 60), - Duration::from_secs(15 * 60), - Duration::from_secs(30 * 60), - Duration::from_secs(3600), - Duration::from_secs(2 * 3600), - ]) - }), + .parse_if_block("queue.schedule.retry", |name| { + map_expr_token::(name, host_envelope_keys) + })? + .unwrap_or_else(|| IfBlock::new(Duration::from_secs(5 * 60))), notify: self - .parse_if_block("queue.schedule.notify", ctx, &rcpt_envelope_keys)? - .unwrap_or_else(|| { - IfBlock::new(vec![ - Duration::from_secs(86400), - Duration::from_secs(3 * 86400), - ]) - }), + .parse_if_block("queue.schedule.notify", |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? + .unwrap_or_else(|| IfBlock::new(Duration::from_secs(86400))), expire: self - .parse_if_block("queue.schedule.expire", ctx, &rcpt_envelope_keys)? + .parse_if_block("queue.schedule.expire", |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(5 * 86400))), hostname: self - .parse_if_block("queue.outbound.hostname", ctx, &sender_envelope_keys)? + .parse_if_block("queue.outbound.hostname", |name| { + map_expr_token::(name, sender_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(default_hostname.to_string())), max_mx: self - .parse_if_block("queue.outbound.limits.mx", ctx, &rcpt_envelope_keys)? + .parse_if_block("queue.outbound.limits.mx", |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(5)), max_multihomed: self - .parse_if_block("queue.outbound.limits.multihomed", ctx, &rcpt_envelope_keys)? + .parse_if_block("queue.outbound.limits.multihomed", |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(2)), ip_strategy: self - .parse_if_block("queue.outbound.ip-strategy", ctx, &sender_envelope_keys)? + .parse_if_block("queue.outbound.ip-strategy", |name| { + map_expr_token::(name, sender_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(IpLookupStrategy::Ipv4thenIpv6)), source_ip: QueueOutboundSourceIp { ipv4: self - .parse_if_block("queue.outbound.source-ip.v4", ctx, &mx_envelope_keys)? - .unwrap_or_else(|| IfBlock::new(Vec::new())), + .parse_if_block("queue.outbound.source-ip.v4", |name| { + map_expr_token::(name, mx_envelope_keys) + })? + .unwrap_or_default(), ipv6: self - .parse_if_block("queue.outbound.source-ip.v6", ctx, &mx_envelope_keys)? - .unwrap_or_else(|| IfBlock::new(Vec::new())), + .parse_if_block("queue.outbound.source-ip.v6", |name| { + map_expr_token::(name, mx_envelope_keys) + })? + .unwrap_or_default(), }, - next_hop: next_hop.into_relay_host(ctx)?, + next_hop: self + .parse_if_block("queue.outbound.next-hop", |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? + .unwrap_or_default(), tls: QueueOutboundTls { dane: self - .parse_if_block("queue.outbound.tls.dane", ctx, &mx_envelope_keys)? + .parse_if_block("queue.outbound.tls.dane", |name| { + map_expr_token::(name, mx_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(RequireOptional::Optional)), mta_sts: self - .parse_if_block("queue.outbound.tls.mta-sts", ctx, &rcpt_envelope_keys)? + .parse_if_block("queue.outbound.tls.mta-sts", |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(RequireOptional::Optional)), start: self - .parse_if_block("queue.outbound.tls.starttls", ctx, &mx_envelope_keys)? + .parse_if_block("queue.outbound.tls.starttls", |name| { + map_expr_token::(name, mx_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(RequireOptional::Optional)), invalid_certs: self - .parse_if_block( - "queue.outbound.tls.allow-invalid-certs", - ctx, - &mx_envelope_keys, - )? + .parse_if_block("queue.outbound.tls.allow-invalid-certs", |name| { + map_expr_token::(name, mx_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(false)), }, - throttle: self.parse_queue_throttle(ctx)?, - quota: self.parse_queue_quota(ctx)?, + throttle: self.parse_queue_throttle()?, + quota: self.parse_queue_quota()?, timeout: QueueOutboundTimeout { connect: self - .parse_if_block("queue.outbound.timeouts.connect", ctx, &host_envelope_keys)? + .parse_if_block("queue.outbound.timeouts.connect", |name| { + map_expr_token::(name, host_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(5 * 60))), greeting: self - .parse_if_block("queue.outbound.timeouts.greeting", ctx, &host_envelope_keys)? + .parse_if_block("queue.outbound.timeouts.greeting", |name| { + map_expr_token::(name, host_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(5 * 60))), tls: self - .parse_if_block("queue.outbound.timeouts.tls", ctx, &host_envelope_keys)? + .parse_if_block("queue.outbound.timeouts.tls", |name| { + map_expr_token::(name, host_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(3 * 60))), ehlo: self - .parse_if_block("queue.outbound.timeouts.ehlo", ctx, &host_envelope_keys)? + .parse_if_block("queue.outbound.timeouts.ehlo", |name| { + map_expr_token::(name, host_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(5 * 60))), mail: self - .parse_if_block( - "queue.outbound.timeouts.mail-from", - ctx, - &host_envelope_keys, - )? + .parse_if_block("queue.outbound.timeouts.mail-from", |name| { + map_expr_token::(name, host_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(5 * 60))), rcpt: self - .parse_if_block("queue.outbound.timeouts.rcpt-to", ctx, &host_envelope_keys)? + .parse_if_block("queue.outbound.timeouts.rcpt-to", |name| { + map_expr_token::(name, host_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(5 * 60))), data: self - .parse_if_block("queue.outbound.timeouts.data", ctx, &host_envelope_keys)? + .parse_if_block("queue.outbound.timeouts.data", |name| { + map_expr_token::(name, host_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(10 * 60))), mta_sts: self - .parse_if_block("queue.outbound.timeouts.mta-sts", ctx, &rcpt_envelope_keys)? + .parse_if_block("queue.outbound.timeouts.mta-sts", |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(10 * 60))), }, dsn: Dsn { name: self - .parse_if_block("report.dsn.from-name", ctx, &sender_envelope_keys)? + .parse_if_block("report.dsn.from-name", |name| { + map_expr_token::(name, sender_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new("Mail Delivery Subsystem".to_string())), address: self - .parse_if_block("report.dsn.from-address", ctx, &sender_envelope_keys)? + .parse_if_block("report.dsn.from-address", |name| { + map_expr_token::(name, sender_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(format!("MAILER-DAEMON@{default_hostname}"))), sign: self - .parse_if_block::>>( - "report.dsn.sign", - ctx, - &sender_envelope_keys, - )? - .unwrap_or_default() - .map_if_block(&ctx.signers, "report.dsn.sign", "signature")?, + .parse_if_block("report.dsn.sign", |name| { + map_expr_token::(name, sender_envelope_keys) + })? + .unwrap_or_default(), }, - directory: ctx - .directory - .directories - .get(self.value_require("storage.directory")?) - .ok_or_else(|| { - format!( - "Directory {:?} not found for key \"storage.directory\".", - self.value_require("storage.directory").unwrap() - ) - })? - .clone(), - data_store: ctx.stores.get_store(self, "storage.data")?, - lookup_store: self - .value_or_default("storage.lookup", "storage.data") - .and_then(|id| ctx.stores.lookup_stores.get(id)) - .ok_or_else(|| { - format!( - "Lookup store {:?} not found for key \"storage.lookup\".", - self.value_or_default("storage.lookup", "storage.data") - .unwrap() - ) - })? - .clone(), }; - if config.retry.has_empty_list() { - Err("Property \"queue.schedule.retry\" cannot contain empty lists.".to_string()) - } else if config.notify.has_empty_list() { - Err("Property \"queue.schedule.notify\" cannot contain empty lists.".to_string()) - } else { - Ok(config) - } + Ok(config) } - fn parse_queue_throttle(&self, ctx: &ConfigContext) -> super::Result { + fn parse_queue_throttle(&self) -> super::Result { // Parse throttle let mut throttle = QueueThrottle { sender: Vec::new(), @@ -245,17 +229,16 @@ impl ConfigQueue for Config { host: Vec::new(), }; let envelope_keys = [ - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, - EnvelopeKey::Mx, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_PRIORITY, + V_MX, + V_REMOTE_IP, + V_LOCAL_IP, ]; let all_throttles = self.parse_throttle( "queue.throttle", - ctx, &envelope_keys, THROTTLE_RCPT_DOMAIN | THROTTLE_SENDER @@ -266,27 +249,17 @@ impl ConfigQueue for Config { )?; for t in all_throttles { if (t.keys & (THROTTLE_MX | THROTTLE_REMOTE_IP | THROTTLE_LOCAL_IP)) != 0 - || t.conditions.conditions.iter().any(|c| { - matches!( - c, - Condition::Match { - key: EnvelopeKey::Mx | EnvelopeKey::RemoteIp | EnvelopeKey::LocalIp, - .. - } - ) - }) + || t.expr + .items() + .iter() + .any(|c| matches!(c, ExpressionItem::Variable(V_MX | V_REMOTE_IP | V_LOCAL_IP))) { throttle.host.push(t); } else if (t.keys & (THROTTLE_RCPT_DOMAIN)) != 0 - || t.conditions.conditions.iter().any(|c| { - matches!( - c, - Condition::Match { - key: EnvelopeKey::RecipientDomain, - .. - } - ) - }) + || t.expr + .items() + .iter() + .any(|c| matches!(c, ExpressionItem::Variable(V_RECIPIENT_DOMAIN))) { throttle.rcpt.push(t); } else { @@ -297,7 +270,7 @@ impl ConfigQueue for Config { Ok(throttle) } - fn parse_queue_quota(&self, ctx: &ConfigContext) -> super::Result { + fn parse_queue_quota(&self) -> super::Result { let mut capacities = QueueQuotas { sender: Vec::new(), rcpt: Vec::new(), @@ -305,30 +278,22 @@ impl ConfigQueue for Config { }; for array_pos in self.sub_keys("queue.quota", "") { - let quota = self.parse_queue_quota_item(("queue.quota", array_pos), ctx)?; + let quota = self.parse_queue_quota_item(("queue.quota", array_pos))?; if (quota.keys & THROTTLE_RCPT) != 0 - || quota.conditions.conditions.iter().any(|c| { - matches!( - c, - Condition::Match { - key: EnvelopeKey::Recipient, - .. - } - ) - }) + || quota + .expr + .items() + .iter() + .any(|c| matches!(c, ExpressionItem::Variable(V_RECIPIENT))) { capacities.rcpt.push(quota); } else if (quota.keys & THROTTLE_RCPT_DOMAIN) != 0 - || quota.conditions.conditions.iter().any(|c| { - matches!( - c, - Condition::Match { - key: EnvelopeKey::RecipientDomain, - .. - } - ) - }) + || quota + .expr + .items() + .iter() + .any(|c| matches!(c, ExpressionItem::Variable(V_RECIPIENT_DOMAIN))) { capacities.rcpt_domain.push(quota); } else { @@ -339,11 +304,7 @@ impl ConfigQueue for Config { Ok(capacities) } - fn parse_queue_quota_item( - &self, - prefix: impl AsKey, - ctx: &ConfigContext, - ) -> super::Result { + fn parse_queue_quota_item(&self, prefix: impl AsKey) -> super::Result { let prefix = prefix.as_key(); let mut keys = 0; for (key_, value) in self.values((&prefix, "key")) { @@ -361,22 +322,21 @@ impl ConfigQueue for Config { } let quota = QueueQuota { - conditions: if self.values((&prefix, "match")).next().is_some() { - self.parse_condition( - (&prefix, "match"), - ctx, - &[ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, - ], - )? + expr: if let Some(expr) = self.value((&prefix, "match")) { + Expression::parse((&prefix, "match"), expr, |name| { + map_expr_token::( + name, + &[ + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_PRIORITY, + ], + ) + })? } else { - Conditions { - conditions: Vec::with_capacity(0), - } + Expression::default() }, keys, size: self @@ -402,69 +362,6 @@ impl ConfigQueue for Config { } } -impl IfBlock> { - pub fn into_relay_host(self, ctx: &ConfigContext) -> super::Result>> { - Ok(IfBlock { - if_then: { - let mut if_then = Vec::with_capacity(self.if_then.len()); - - for i in self.if_then { - if_then.push(IfThen { - conditions: i.conditions, - then: if let Some(then) = i.then { - Some( - ctx.hosts - .get(&then) - .ok_or_else(|| { - format!( - "Host {then:?} not found for property \"queue.next-hop\".", - ) - })? - .into(), - ) - } else { - None - }, - }); - } - - if_then - }, - default: if let Some(default) = self.default { - Some( - ctx.hosts - .get(&default) - .ok_or_else(|| { - format!( - "Relay host {default:?} not found for property \"queue.next-hop\".", - ) - })? - .into(), - ) - } else { - None - }, - }) - } -} - -impl From<&Host> for RelayHost { - fn from(host: &Host) -> Self { - RelayHost { - address: host.address.to_string(), - port: host.port, - protocol: host.protocol, - auth: if let (Some(username), Some(secret)) = (&host.username, &host.secret) { - Credentials::new(username.to_string(), secret.to_string()).into() - } else { - None - }, - tls_implicit: host.tls_implicit, - tls_allow_invalid_certs: host.tls_allow_invalid_certs, - } - } -} - impl ParseValue for RequireOptional { fn parse_value(key: impl AsKey, value: &str) -> super::Result { match value { @@ -479,3 +376,28 @@ impl ParseValue for RequireOptional { } } } + +impl<'x> TryFrom> for RequireOptional { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + utils::expr::Variable::Integer(0) => Ok(RequireOptional::Optional), + utils::expr::Variable::Integer(1) => Ok(RequireOptional::Require), + utils::expr::Variable::Integer(2) => Ok(RequireOptional::Disable), + _ => Err(()), + } + } +} + +impl From for Constant { + fn from(value: RequireOptional) -> Self { + Constant::Integer(match value { + RequireOptional::Optional => 0, + RequireOptional::Require => 1, + RequireOptional::Disable => 2, + }) + } +} + +impl ConstantValue for RequireOptional {} diff --git a/crates/smtp/src/config/remote.rs b/crates/smtp/src/config/remote.rs deleted file mode 100644 index 68739ad5..00000000 --- a/crates/smtp/src/config/remote.rs +++ /dev/null @@ -1,63 +0,0 @@ -/* - * 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 std::time::Duration; - -use utils::config::Config; - -use super::{ConfigContext, Host}; - -pub trait ConfigHost { - fn parse_remote_hosts(&self, ctx: &mut ConfigContext) -> super::Result<()>; - fn parse_host(&self, id: &str) -> super::Result; -} - -impl ConfigHost for Config { - fn parse_remote_hosts(&self, ctx: &mut ConfigContext) -> super::Result<()> { - for id in self.sub_keys("remote", ".address") { - ctx.hosts.insert(id.to_string(), self.parse_host(id)?); - } - - Ok(()) - } - - fn parse_host(&self, id: &str) -> super::Result { - Ok(Host { - address: self.property_require(("remote", id, "address"))?, - port: self.property_require(("remote", id, "port"))?, - protocol: self.property_require(("remote", id, "protocol"))?, - concurrency: self.property(("remote", id, "concurrency"))?.unwrap_or(10), - tls_implicit: self - .property(("remote", id, "tls.implicit"))? - .unwrap_or(true), - tls_allow_invalid_certs: self - .property(("remote", id, "tls.allow-invalid-certs"))? - .unwrap_or(false), - username: self.property(("remote", id, "auth.username"))?, - secret: self.property(("remote", id, "auth.secret"))?, - timeout: self - .property(("remote", id, "timeout"))? - .unwrap_or(Duration::from_secs(60)), - }) - } -} diff --git a/crates/smtp/src/config/report.rs b/crates/smtp/src/config/report.rs index dca3d6db..230dc679 100644 --- a/crates/smtp/src/config/report.rs +++ b/crates/smtp/src/config/report.rs @@ -21,51 +21,57 @@ * for more details. */ -use super::{ - if_block::ConfigIf, AddressMatch, AggregateFrequency, AggregateReport, ConfigContext, - EnvelopeKey, IfBlock, Report, ReportAnalysis, ReportConfig, +use std::time::Duration; + +use crate::core::eval::*; +use utils::{ + config::{ + if_block::IfBlock, + utils::{AsKey, ConstantValue, NoConstants, ParseValue}, + Config, + }, + expr::{Constant, Variable}, }; -use utils::config::{ - utils::{AsKey, ParseValue}, - Config, DynValue, + +use super::{ + map_expr_token, AddressMatch, AggregateFrequency, AggregateReport, Report, ReportAnalysis, + ReportConfig, }; pub trait ConfigReport { - fn parse_reports(&self, ctx: &ConfigContext) -> super::Result; + fn parse_reports(&self) -> super::Result; fn parse_report( &self, - ctx: &ConfigContext, id: &str, default_hostname: &str, - available_keys: &[EnvelopeKey], + available_keys: &[u32], ) -> super::Result; fn parse_aggregate_report( &self, - ctx: &ConfigContext, id: &str, default_hostname: &str, - available_keys: &[EnvelopeKey], + available_keys: &[u32], ) -> super::Result; } impl ConfigReport for Config { - fn parse_reports(&self, ctx: &ConfigContext) -> super::Result { - let sender_envelope_keys = [ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, + fn parse_reports(&self) -> super::Result { + let sender_envelope_keys = &[ + V_SENDER, + V_SENDER_DOMAIN, + V_PRIORITY, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, ]; - let rcpt_envelope_keys = [ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Priority, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::RecipientDomain, + let rcpt_envelope_keys = &[ + V_SENDER, + V_SENDER_DOMAIN, + V_PRIORITY, + V_REMOTE_IP, + V_LOCAL_IP, + V_RECIPIENT_DOMAIN, ]; let mut addresses = Vec::new(); for address in self.properties::("report.analysis.addresses") { @@ -74,24 +80,25 @@ impl ConfigReport for Config { let default_hostname = self.value_require("server.hostname")?; Ok(ReportConfig { - dkim: self.parse_report(ctx, "dkim", default_hostname, &sender_envelope_keys)?, - spf: self.parse_report(ctx, "spf", default_hostname, &sender_envelope_keys)?, - dmarc: self.parse_report(ctx, "dmarc", default_hostname, &sender_envelope_keys)?, + dkim: self.parse_report("dkim", default_hostname, sender_envelope_keys)?, + spf: self.parse_report("spf", default_hostname, sender_envelope_keys)?, + dmarc: self.parse_report("dmarc", default_hostname, sender_envelope_keys)?, dmarc_aggregate: self.parse_aggregate_report( - ctx, "dmarc", default_hostname, - &sender_envelope_keys, + sender_envelope_keys, )?, - tls: self.parse_aggregate_report(ctx, "tls", default_hostname, &rcpt_envelope_keys)?, - path: self - .parse_if_block("report.path", ctx, &sender_envelope_keys)? - .ok_or("Missing \"report.path\" property.")?, + tls: self.parse_aggregate_report("tls", default_hostname, rcpt_envelope_keys)?, + path: self.property_require("report.path")?, submitter: self - .parse_if_block("report.submitter", ctx, &[EnvelopeKey::RecipientDomain])? + .parse_if_block("report.submitter", |name| { + map_expr_token::(name, &[V_RECIPIENT_DOMAIN]) + })? .unwrap_or_else(|| IfBlock::new(default_hostname.to_string())), hash: self - .parse_if_block("report.hash", ctx, &sender_envelope_keys)? + .parse_if_block("report.hash", |name| { + map_expr_token::(name, sender_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(32)), analysis: ReportAnalysis { addresses, @@ -104,96 +111,84 @@ impl ConfigReport for Config { fn parse_report( &self, - ctx: &ConfigContext, id: &str, default_hostname: &str, - available_keys: &[EnvelopeKey], + available_keys: &[u32], ) -> super::Result { Ok(Report { name: self - .parse_if_block(("report", id, "from-name"), ctx, available_keys)? + .parse_if_block(("report", id, "from-name"), |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new("Mail Delivery Subsystem".to_string())), address: self - .parse_if_block(("report", id, "from-address"), ctx, available_keys)? + .parse_if_block(("report", id, "from-address"), |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(format!("MAILER-DAEMON@{default_hostname}"))), subject: self - .parse_if_block(("report", id, "subject"), ctx, available_keys)? + .parse_if_block(("report", id, "subject"), |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(format!("{} Report", id.to_ascii_uppercase()))), sign: self - .parse_if_block::>>( - ("report", id, "sign"), - ctx, - available_keys, - )? - .unwrap_or_default() - .map_if_block(&ctx.signers, &("report", id, "sign").as_key(), "signature")?, + .parse_if_block(("report", id, "sign"), |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_default(), send: self - .parse_if_block(("report", id, "send"), ctx, available_keys)? + .parse_if_block(("report", id, "send"), |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_default(), }) } fn parse_aggregate_report( &self, - ctx: &ConfigContext, id: &str, default_hostname: &str, - available_keys: &[EnvelopeKey], + available_keys: &[u32], ) -> super::Result { - let rcpt_envelope_keys = [EnvelopeKey::RecipientDomain]; + let rcpt_envelope_keys = &[V_RECIPIENT_DOMAIN]; Ok(AggregateReport { name: self - .parse_if_block( - ("report", id, "aggregate.from-name"), - ctx, - &rcpt_envelope_keys, - )? + .parse_if_block(("report", id, "aggregate.from-name"), |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_else(|| { IfBlock::new(format!("{} Aggregate Report", id.to_ascii_uppercase())) }), address: self - .parse_if_block( - ("report", id, "aggregate.from-address"), - ctx, - &rcpt_envelope_keys, - )? + .parse_if_block(("report", id, "aggregate.from-address"), |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(format!("noreply-{id}@{default_hostname}"))), org_name: self - .parse_if_block( - ("report", id, "aggregate.org-name"), - ctx, - &rcpt_envelope_keys, - )? + .parse_if_block(("report", id, "aggregate.org-name"), |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_default(), contact_info: self - .parse_if_block( - ("report", id, "aggregate.contact-info"), - ctx, - &rcpt_envelope_keys, - )? + .parse_if_block(("report", id, "aggregate.contact-info"), |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_default(), send: self - .parse_if_block(("report", id, "aggregate.send"), ctx, available_keys)? - .unwrap_or_default(), + .parse_if_block(("report", id, "aggregate.send"), |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_else(|| IfBlock::new(AggregateFrequency::Never)), sign: self - .parse_if_block::>>( - ("report", id, "aggregate.sign"), - ctx, - &rcpt_envelope_keys, - )? - .unwrap_or_default() - .map_if_block( - &ctx.signers, - &("report", id, "aggregate.sign").as_key(), - "signature", - )?, + .parse_if_block(("report", id, "aggregate.sign"), |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? + .unwrap_or_default(), max_size: self - .parse_if_block( - ("report", id, "aggregate.max-size"), - ctx, - &rcpt_envelope_keys, - )? + .parse_if_block(("report", id, "aggregate.max-size"), |name| { + map_expr_token::(name, rcpt_envelope_keys) + })? .unwrap_or_else(|| IfBlock::new(25 * 1024 * 1024)), }) } @@ -215,6 +210,33 @@ impl ParseValue for AggregateFrequency { } } +impl From for Constant { + fn from(value: AggregateFrequency) -> Self { + match value { + AggregateFrequency::Never => 0.into(), + AggregateFrequency::Hourly => 1.into(), + AggregateFrequency::Daily => 2.into(), + AggregateFrequency::Weekly => 3.into(), + } + } +} + +impl<'x> TryFrom> for AggregateFrequency { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::Integer(0) => Ok(AggregateFrequency::Never), + Variable::Integer(1) => Ok(AggregateFrequency::Hourly), + Variable::Integer(2) => Ok(AggregateFrequency::Daily), + Variable::Integer(3) => Ok(AggregateFrequency::Weekly), + _ => Err(()), + } + } +} + +impl ConstantValue for AggregateFrequency {} + impl ParseValue for AddressMatch { fn parse_value(key: impl AsKey, value: &str) -> super::Result { if let Some(value) = value.strip_prefix('*').map(|v| v.trim()) { diff --git a/crates/smtp/src/config/scripts.rs b/crates/smtp/src/config/scripts.rs index 35ab1d51..06a3bac9 100644 --- a/crates/smtp/src/config/scripts.rs +++ b/crates/smtp/src/config/scripts.rs @@ -119,13 +119,7 @@ impl ConfigSieve for Config { ) .with_max_header_size(10240) .with_valid_notification_uri("mailto") - .with_valid_ext_lists( - ctx.stores - .lookup_stores - .keys() - .chain(ctx.directory.lookups.keys()) - .map(|k| k.to_string()), - ) + .with_valid_ext_lists(ctx.stores.lookup_stores.keys().map(|k| k.to_string())) .with_functions(&mut fnc_map); if let Some(value) = self.property("sieve.trusted.limits.redirects")? { @@ -192,9 +186,6 @@ impl ConfigSieve for Config { Ok(SieveCore { runtime, - scripts: ctx.scripts.clone(), - lookup_stores: ctx.stores.lookup_stores.clone(), - directories: ctx.directory.directories.clone(), from_addr: self .value("sieve.trusted.from-addr") .map(|a| a.to_string()) diff --git a/crates/smtp/src/config/session.rs b/crates/smtp/src/config/session.rs index 99cd9db2..0ffd59ce 100644 --- a/crates/smtp/src/config/session.rs +++ b/crates/smtp/src/config/session.rs @@ -25,66 +25,71 @@ use std::{net::ToSocketAddrs, time::Duration}; use smtp_proto::*; -use super::{if_block::ConfigIf, throttle::ConfigThrottle, *}; -use utils::config::{ - utils::{AsKey, ParseValue}, - Config, DynValue, +use crate::inbound::milter; + +use crate::core::eval::*; + +use super::{ + map_expr_token, throttle::ConfigThrottle, Auth, Connect, Data, Ehlo, Extensions, Mail, Milter, + Pipe, Rcpt, SessionConfig, SessionThrottle, THROTTLE_AUTH_AS, THROTTLE_HELO_DOMAIN, + THROTTLE_LISTENER, THROTTLE_LOCAL_IP, THROTTLE_RCPT, THROTTLE_RCPT_DOMAIN, THROTTLE_REMOTE_IP, + THROTTLE_SENDER, THROTTLE_SENDER_DOMAIN, +}; +use utils::{ + config::{ + if_block::IfBlock, + utils::{AsKey, ConstantValue, NoConstants, ParseValue}, + Config, + }, + expr::{Constant, ExpressionItem, Variable}, }; pub trait ConfigSession { - fn parse_session_config(&self, ctx: &ConfigContext) -> super::Result; - fn parse_session_throttle(&self, ctx: &ConfigContext) -> super::Result; - fn parse_session_connect(&self, ctx: &ConfigContext) -> super::Result; - fn parse_extensions(&self, ctx: &ConfigContext) -> super::Result; - fn parse_session_ehlo(&self, ctx: &ConfigContext) -> super::Result; - fn parse_session_auth(&self, ctx: &ConfigContext) -> super::Result; - fn parse_session_mail(&self, ctx: &ConfigContext) -> super::Result; - fn parse_session_rcpt(&self, ctx: &ConfigContext) -> super::Result; - fn parse_session_data(&self, ctx: &ConfigContext) -> super::Result; - fn parse_pipes( - &self, - ctx: &ConfigContext, - available_keys: &[EnvelopeKey], - ) -> super::Result>; - fn parse_milters( - &self, - ctx: &ConfigContext, - available_keys: &[EnvelopeKey], - ) -> super::Result>; + fn parse_session_config(&self) -> super::Result; + fn parse_session_throttle(&self) -> super::Result; + fn parse_session_connect(&self) -> super::Result; + fn parse_extensions(&self) -> super::Result; + fn parse_session_ehlo(&self) -> super::Result; + fn parse_session_auth(&self) -> super::Result; + fn parse_session_mail(&self) -> super::Result; + fn parse_session_rcpt(&self) -> super::Result; + fn parse_session_data(&self) -> super::Result; + fn parse_pipes(&self, available_keys: &[u32]) -> super::Result>; + fn parse_milters(&self, available_keys: &[u32]) -> super::Result>; } impl ConfigSession for Config { - fn parse_session_config(&self, ctx: &ConfigContext) -> super::Result { - let available_keys = [ - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - ]; + fn parse_session_config(&self) -> super::Result { + let available_keys = &[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]; Ok(SessionConfig { duration: self - .parse_if_block("session.duration", ctx, &available_keys)? + .parse_if_block("session.duration", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(15 * 60))), transfer_limit: self - .parse_if_block("session.transfer-limit", ctx, &available_keys)? + .parse_if_block("session.transfer-limit", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(250 * 1024 * 1024)), timeout: self - .parse_if_block::>("session.timeout", ctx, &available_keys)? - .unwrap_or_else(|| IfBlock::new(Some(Duration::from_secs(5 * 60)))) - .try_unwrap("session.timeout") - .unwrap_or_else(|_| IfBlock::new(Duration::from_secs(5 * 60))), - throttle: self.parse_session_throttle(ctx)?, - connect: self.parse_session_connect(ctx)?, - ehlo: self.parse_session_ehlo(ctx)?, - auth: self.parse_session_auth(ctx)?, - mail: self.parse_session_mail(ctx)?, - rcpt: self.parse_session_rcpt(ctx)?, - data: self.parse_session_data(ctx)?, - extensions: self.parse_extensions(ctx)?, + .parse_if_block("session.timeout", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_else(|| IfBlock::new(Duration::from_secs(5 * 60))), + throttle: self.parse_session_throttle()?, + connect: self.parse_session_connect()?, + ehlo: self.parse_session_ehlo()?, + auth: self.parse_session_auth()?, + mail: self.parse_session_mail()?, + rcpt: self.parse_session_rcpt()?, + data: self.parse_session_data()?, + extensions: self.parse_extensions()?, }) } - fn parse_session_throttle(&self, ctx: &ConfigContext) -> super::Result { + fn parse_session_throttle(&self) -> super::Result { // Parse throttle let mut throttle = SessionThrottle { connect: Vec::new(), @@ -93,18 +98,17 @@ impl ConfigSession for Config { }; let all_throttles = self.parse_throttle( "session.throttle", - ctx, &[ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, - EnvelopeKey::HeloDomain, + V_SENDER, + V_SENDER_DOMAIN, + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, + V_HELO_DOMAIN, ], THROTTLE_LISTENER | THROTTLE_REMOTE_IP @@ -118,13 +122,10 @@ impl ConfigSession for Config { )?; for t in all_throttles { if (t.keys & (THROTTLE_RCPT | THROTTLE_RCPT_DOMAIN)) != 0 - || t.conditions.conditions.iter().any(|c| { + || t.expr.items().iter().any(|c| { matches!( c, - Condition::Match { - key: EnvelopeKey::Recipient | EnvelopeKey::RecipientDomain, - .. - } + ExpressionItem::Variable(V_RECIPIENT | V_RECIPIENT_DOMAIN) ) }) { @@ -135,16 +136,12 @@ impl ConfigSession for Config { | THROTTLE_HELO_DOMAIN | THROTTLE_AUTH_AS)) != 0 - || t.conditions.conditions.iter().any(|c| { + || t.expr.items().iter().any(|c| { matches!( c, - Condition::Match { - key: EnvelopeKey::Sender - | EnvelopeKey::SenderDomain - | EnvelopeKey::HeloDomain - | EnvelopeKey::AuthenticatedAs, - .. - } + ExpressionItem::Variable( + V_SENDER | V_SENDER_DOMAIN | V_HELO_DOMAIN | V_AUTHENTICATED_AS + ) ) }) { @@ -157,310 +154,321 @@ impl ConfigSession for Config { Ok(throttle) } - fn parse_session_connect(&self, ctx: &ConfigContext) -> super::Result { - let available_keys = [ - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - ]; + fn parse_session_connect(&self) -> super::Result { + let available_keys = &[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]; Ok(Connect { script: self - .parse_if_block::>("session.connect.script", ctx, &available_keys)? - .unwrap_or_default() - .map_if_block(&ctx.scripts, "session.connect.script", "script")?, + .parse_if_block("session.connect.script", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_default(), }) } - fn parse_extensions(&self, ctx: &ConfigContext) -> super::Result { - let available_keys = [ - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::AuthenticatedAs, + fn parse_extensions(&self) -> super::Result { + let available_keys = &[ + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_SENDER, + V_SENDER_DOMAIN, + V_AUTHENTICATED_AS, ]; Ok(Extensions { pipelining: self - .parse_if_block("session.extensions.pipelining", ctx, &available_keys)? + .parse_if_block("session.extensions.pipelining", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), dsn: self - .parse_if_block("session.extensions.dsn", ctx, &available_keys)? + .parse_if_block("session.extensions.dsn", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), vrfy: self - .parse_if_block("session.extensions.vrfy", ctx, &available_keys)? + .parse_if_block("session.extensions.vrfy", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), expn: self - .parse_if_block("session.extensions.expn", ctx, &available_keys)? + .parse_if_block("session.extensions.expn", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), chunking: self - .parse_if_block("session.extensions.chunking", ctx, &available_keys)? + .parse_if_block("session.extensions.chunking", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), requiretls: self - .parse_if_block("session.extensions.requiretls", ctx, &available_keys)? - .unwrap_or_default(), + .parse_if_block("session.extensions.requiretls", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_else(|| IfBlock::new(true)), no_soliciting: self - .parse_if_block("session.extensions.no-soliciting", ctx, &available_keys)? - .unwrap_or_default(), + .parse_if_block("session.extensions.no-soliciting", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_else(|| IfBlock::new(false)), future_release: self - .parse_if_block("session.extensions.future-release", ctx, &available_keys)? + .parse_if_block("session.extensions.future-release", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_default(), deliver_by: self - .parse_if_block("session.extensions.deliver-by", ctx, &available_keys)? + .parse_if_block("session.extensions.deliver-by", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_default(), mt_priority: self - .parse_if_block("session.extensions.mt-priority", ctx, &available_keys)? + .parse_if_block("session.extensions.mt-priority", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_default(), }) } - fn parse_session_ehlo(&self, ctx: &ConfigContext) -> super::Result { - let available_keys = [ - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - ]; + fn parse_session_ehlo(&self) -> super::Result { + let available_keys = &[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP]; Ok(Ehlo { script: self - .parse_if_block::>("session.ehlo.script", ctx, &available_keys)? - .unwrap_or_default() - .map_if_block(&ctx.scripts, "session.ehlo.script", "script")?, + .parse_if_block("session.ehlo.script", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_default(), require: self - .parse_if_block("session.ehlo.require", ctx, &available_keys)? + .parse_if_block("session.ehlo.require", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), reject_non_fqdn: self - .parse_if_block("session.ehlo.reject-non-fqdn", ctx, &available_keys)? + .parse_if_block("session.ehlo.reject-non-fqdn", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), }) } - fn parse_session_auth(&self, ctx: &ConfigContext) -> super::Result { - let available_keys = [ - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::HeloDomain, - ]; - - let mechanisms = self - .parse_if_block::>("session.auth.mechanisms", ctx, &available_keys)? - .unwrap_or_default(); + fn parse_session_auth(&self) -> super::Result { + let available_keys = &[V_LISTENER, V_REMOTE_IP, V_LOCAL_IP, V_HELO_DOMAIN]; Ok(Auth { directory: self - .parse_if_block::>>( - "session.auth.directory", - ctx, - &available_keys, - )? - .unwrap_or_default() - .map_if_block( - &ctx.directory.directories, - "session.auth.directory", - "lookup list", - )?, - mechanisms: IfBlock { - if_then: mechanisms - .if_then - .into_iter() - .map(|i| IfThen { - conditions: i.conditions, - then: i.then.into_iter().fold(0, |acc, m| acc | m.mechanism), - }) - .collect(), - default: mechanisms - .default - .into_iter() - .fold(0, |acc, m| acc | m.mechanism), - }, + .parse_if_block("session.auth.directory", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_default(), + mechanisms: self + .parse_if_block("session.auth.mechanisms", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_default(), require: self - .parse_if_block("session.auth.require", ctx, &available_keys)? + .parse_if_block("session.auth.require", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(false)), errors_max: self - .parse_if_block("session.auth.errors.max", ctx, &available_keys)? + .parse_if_block("session.auth.errors.max", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(3)), errors_wait: self - .parse_if_block("session.auth.errors.wait", ctx, &available_keys)? + .parse_if_block("session.auth.errors.wait", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(30))), allow_plain_text: self - .parse_if_block("session.auth.allow-plain-text", ctx, &available_keys)? + .parse_if_block("session.auth.allow-plain-text", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(false)), must_match_sender: self - .parse_if_block("session.auth.must-match-sender", ctx, &available_keys)? + .parse_if_block("session.auth.must-match-sender", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), }) } - fn parse_session_mail(&self, ctx: &ConfigContext) -> super::Result { - let available_keys = [ - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::HeloDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, + fn parse_session_mail(&self) -> super::Result { + let available_keys = &[ + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_HELO_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, ]; Ok(Mail { script: self - .parse_if_block::>("session.mail.script", ctx, &available_keys)? - .unwrap_or_default() - .map_if_block(&ctx.scripts, "session.mail.script", "script")?, + .parse_if_block("session.mail.script", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_default(), rewrite: self - .parse_if_block::>>( - "session.mail.rewrite", - ctx, - &available_keys, - )? + .parse_if_block("session.mail.rewrite", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_default(), }) } - fn parse_session_rcpt(&self, ctx: &ConfigContext) -> super::Result { - let available_keys = [ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::HeloDomain, + fn parse_session_rcpt(&self) -> super::Result { + let available_keys = &[ + V_SENDER, + V_SENDER_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_HELO_DOMAIN, ]; - let available_keys_full = [ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::HeloDomain, + let available_keys_full = &[ + V_SENDER, + V_SENDER_DOMAIN, + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_HELO_DOMAIN, ]; Ok(Rcpt { script: self - .parse_if_block::>("session.rcpt.script", ctx, &available_keys_full)? - .unwrap_or_default() - .map_if_block(&ctx.scripts, "session.rcpt.script", "script")?, + .parse_if_block("session.rcpt.script", |name| { + map_expr_token::(name, available_keys_full) + })? + .unwrap_or_default(), relay: self - .parse_if_block("session.rcpt.relay", ctx, &available_keys_full)? + .parse_if_block("session.rcpt.relay", |name| { + map_expr_token::(name, available_keys_full) + })? .unwrap_or_else(|| IfBlock::new(false)), directory: self - .parse_if_block::>>( - "session.rcpt.directory", - ctx, - &available_keys_full, - )? - .unwrap_or_default() - .map_if_block( - &ctx.directory.directories, - "session.rcpt.directory", - "lookup list", - )?, + .parse_if_block("session.rcpt.directory", |name| { + map_expr_token::(name, available_keys_full) + })? + .unwrap_or_default(), errors_max: self - .parse_if_block("session.rcpt.errors.max", ctx, &available_keys)? + .parse_if_block("session.rcpt.errors.max", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(10)), errors_wait: self - .parse_if_block("session.rcpt.errors.wait", ctx, &available_keys)? + .parse_if_block("session.rcpt.errors.wait", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(30))), max_recipients: self - .parse_if_block("session.rcpt.max-recipients", ctx, &available_keys)? + .parse_if_block("session.rcpt.max-recipients", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(100)), rewrite: self - .parse_if_block::>>( - "session.rcpt.rewrite", - ctx, - &available_keys_full, - )? + .parse_if_block("session.rcpt.rewrite", |name| { + map_expr_token::(name, available_keys_full) + })? .unwrap_or_default(), }) } - fn parse_session_data(&self, ctx: &ConfigContext) -> super::Result { - let available_keys = [ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, - EnvelopeKey::HeloDomain, + fn parse_session_data(&self) -> super::Result { + let available_keys = &[ + V_SENDER, + V_SENDER_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, + V_HELO_DOMAIN, ]; Ok(Data { script: self - .parse_if_block::>("session.data.script", ctx, &available_keys)? - .unwrap_or_default() - .map_if_block(&ctx.scripts, "session.data.script", "script")?, + .parse_if_block("session.data.script", |name| { + map_expr_token::(name, available_keys) + })? + .unwrap_or_default(), max_messages: self - .parse_if_block("session.data.limits.messages", ctx, &available_keys)? + .parse_if_block("session.data.limits.messages", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(10)), max_message_size: self - .parse_if_block("session.data.limits.size", ctx, &available_keys)? + .parse_if_block("session.data.limits.size", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(25 * 1024 * 1024)), max_received_headers: self - .parse_if_block("session.data.limits.received-headers", ctx, &available_keys)? + .parse_if_block("session.data.limits.received-headers", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(50)), add_received: self - .parse_if_block("session.data.add-headers.received", ctx, &available_keys)? + .parse_if_block("session.data.add-headers.received", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), add_received_spf: self - .parse_if_block( - "session.data.add-headers.received-spf", - ctx, - &available_keys, - )? + .parse_if_block("session.data.add-headers.received-spf", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), add_return_path: self - .parse_if_block("session.data.add-headers.return-path", ctx, &available_keys)? + .parse_if_block("session.data.add-headers.return-path", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), add_auth_results: self - .parse_if_block( - "session.data.add-headers.auth-results", - ctx, - &available_keys, - )? + .parse_if_block("session.data.add-headers.auth-results", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), add_message_id: self - .parse_if_block("session.data.add-headers.message-id", ctx, &available_keys)? + .parse_if_block("session.data.add-headers.message-id", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), add_date: self - .parse_if_block("session.data.add-headers.date", ctx, &available_keys)? + .parse_if_block("session.data.add-headers.date", |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(true)), - pipe_commands: self.parse_pipes(ctx, &available_keys)?, - milters: self.parse_milters(ctx, &available_keys)?, + pipe_commands: self.parse_pipes(available_keys)?, + milters: self.parse_milters(available_keys)?, }) } - fn parse_pipes( - &self, - ctx: &ConfigContext, - available_keys: &[EnvelopeKey], - ) -> super::Result> { + fn parse_pipes(&self, available_keys: &[u32]) -> super::Result> { let mut pipes = Vec::new(); for id in self.sub_keys("session.data.pipe", "") { pipes.push(Pipe { command: self - .parse_if_block(("session.data.pipe", id, "command"), ctx, available_keys)? + .parse_if_block(("session.data.pipe", id, "command"), |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_default(), arguments: self - .parse_if_block(("session.data.pipe", id, "arguments"), ctx, available_keys)? + .parse_if_block(("session.data.pipe", id, "arguments"), |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_default(), timeout: self - .parse_if_block(("session.data.pipe", id, "timeout"), ctx, available_keys)? + .parse_if_block(("session.data.pipe", id, "timeout"), |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_else(|| IfBlock::new(Duration::from_secs(30))), }) } Ok(pipes) } - fn parse_milters( - &self, - ctx: &ConfigContext, - available_keys: &[EnvelopeKey], - ) -> super::Result> { + fn parse_milters(&self, available_keys: &[u32]) -> super::Result> { let mut milters = Vec::new(); for id in self.sub_keys("session.data.milter", "") { let hostname = self @@ -469,7 +477,9 @@ impl ConfigSession for Config { let port = self.property_require(("session.data.milter", id, "port"))?; milters.push(Milter { enable: self - .parse_if_block(("session.data.milter", id, "enable"), ctx, available_keys)? + .parse_if_block(("session.data.milter", id, "enable"), |name| { + map_expr_token::(name, available_keys) + })? .unwrap_or_default(), addrs: format!("{}:{}", hostname, port) .to_socket_addrs() @@ -520,64 +530,104 @@ impl ConfigSession for Config { } } -struct Mechanism { - mechanism: u64, -} +#[derive(Default)] +pub struct Mechanism(u64); impl ParseValue for Mechanism { fn parse_value(key: impl AsKey, value: &str) -> super::Result { - Ok(Mechanism { - mechanism: match value.to_ascii_uppercase().as_str() { - "LOGIN" => AUTH_LOGIN, - "PLAIN" => AUTH_PLAIN, - "XOAUTH2" => AUTH_XOAUTH2, - "OAUTHBEARER" => AUTH_OAUTHBEARER, - /*"SCRAM-SHA-256-PLUS" => AUTH_SCRAM_SHA_256_PLUS, - "SCRAM-SHA-256" => AUTH_SCRAM_SHA_256, - "SCRAM-SHA-1-PLUS" => AUTH_SCRAM_SHA_1_PLUS, - "SCRAM-SHA-1" => AUTH_SCRAM_SHA_1, - "XOAUTH" => AUTH_XOAUTH, - "9798-M-DSA-SHA1" => AUTH_9798_M_DSA_SHA1, - "9798-M-ECDSA-SHA1" => AUTH_9798_M_ECDSA_SHA1, - "9798-M-RSA-SHA1-ENC" => AUTH_9798_M_RSA_SHA1_ENC, - "9798-U-DSA-SHA1" => AUTH_9798_U_DSA_SHA1, - "9798-U-ECDSA-SHA1" => AUTH_9798_U_ECDSA_SHA1, - "9798-U-RSA-SHA1-ENC" => AUTH_9798_U_RSA_SHA1_ENC, - "EAP-AES128" => AUTH_EAP_AES128, - "EAP-AES128-PLUS" => AUTH_EAP_AES128_PLUS, - "ECDH-X25519-CHALLENGE" => AUTH_ECDH_X25519_CHALLENGE, - "ECDSA-NIST256P-CHALLENGE" => AUTH_ECDSA_NIST256P_CHALLENGE, - "EXTERNAL" => AUTH_EXTERNAL, - "GS2-KRB5" => AUTH_GS2_KRB5, - "GS2-KRB5-PLUS" => AUTH_GS2_KRB5_PLUS, - "GSS-SPNEGO" => AUTH_GSS_SPNEGO, - "GSSAPI" => AUTH_GSSAPI, - "KERBEROS_V4" => AUTH_KERBEROS_V4, - "KERBEROS_V5" => AUTH_KERBEROS_V5, - "NMAS-SAMBA-AUTH" => AUTH_NMAS_SAMBA_AUTH, - "NMAS_AUTHEN" => AUTH_NMAS_AUTHEN, - "NMAS_LOGIN" => AUTH_NMAS_LOGIN, - "NTLM" => AUTH_NTLM, - "OAUTH10A" => AUTH_OAUTH10A, - "OPENID20" => AUTH_OPENID20, - "OTP" => AUTH_OTP, - "SAML20" => AUTH_SAML20, - "SECURID" => AUTH_SECURID, - "SKEY" => AUTH_SKEY, - "SPNEGO" => AUTH_SPNEGO, - "SPNEGO-PLUS" => AUTH_SPNEGO_PLUS, - "SXOVER-PLUS" => AUTH_SXOVER_PLUS, - "CRAM-MD5" => AUTH_CRAM_MD5, - "DIGEST-MD5" => AUTH_DIGEST_MD5, - "ANONYMOUS" => AUTH_ANONYMOUS,*/ - _ => { - return Err(format!( - "Unsupported mechanism {:?} for property {:?}.", - value, - key.as_key() - )) - } - }, - }) + Ok(Mechanism(match value.to_ascii_uppercase().as_str() { + "LOGIN" => AUTH_LOGIN, + "PLAIN" => AUTH_PLAIN, + "XOAUTH2" => AUTH_XOAUTH2, + "OAUTHBEARER" => AUTH_OAUTHBEARER, + /*"SCRAM-SHA-256-PLUS" => AUTH_SCRAM_SHA_256_PLUS, + "SCRAM-SHA-256" => AUTH_SCRAM_SHA_256, + "SCRAM-SHA-1-PLUS" => AUTH_SCRAM_SHA_1_PLUS, + "SCRAM-SHA-1" => AUTH_SCRAM_SHA_1, + "XOAUTH" => AUTH_XOAUTH, + "9798-M-DSA-SHA1" => AUTH_9798_M_DSA_SHA1, + "9798-M-ECDSA-SHA1" => AUTH_9798_M_ECDSA_SHA1, + "9798-M-RSA-SHA1-ENC" => AUTH_9798_M_RSA_SHA1_ENC, + "9798-U-DSA-SHA1" => AUTH_9798_U_DSA_SHA1, + "9798-U-ECDSA-SHA1" => AUTH_9798_U_ECDSA_SHA1, + "9798-U-RSA-SHA1-ENC" => AUTH_9798_U_RSA_SHA1_ENC, + "EAP-AES128" => AUTH_EAP_AES128, + "EAP-AES128-PLUS" => AUTH_EAP_AES128_PLUS, + "ECDH-X25519-CHALLENGE" => AUTH_ECDH_X25519_CHALLENGE, + "ECDSA-NIST256P-CHALLENGE" => AUTH_ECDSA_NIST256P_CHALLENGE, + "EXTERNAL" => AUTH_EXTERNAL, + "GS2-KRB5" => AUTH_GS2_KRB5, + "GS2-KRB5-PLUS" => AUTH_GS2_KRB5_PLUS, + "GSS-SPNEGO" => AUTH_GSS_SPNEGO, + "GSSAPI" => AUTH_GSSAPI, + "KERBEROS_V4" => AUTH_KERBEROS_V4, + "KERBEROS_V5" => AUTH_KERBEROS_V5, + "NMAS-SAMBA-AUTH" => AUTH_NMAS_SAMBA_AUTH, + "NMAS_AUTHEN" => AUTH_NMAS_AUTHEN, + "NMAS_LOGIN" => AUTH_NMAS_LOGIN, + "NTLM" => AUTH_NTLM, + "OAUTH10A" => AUTH_OAUTH10A, + "OPENID20" => AUTH_OPENID20, + "OTP" => AUTH_OTP, + "SAML20" => AUTH_SAML20, + "SECURID" => AUTH_SECURID, + "SKEY" => AUTH_SKEY, + "SPNEGO" => AUTH_SPNEGO, + "SPNEGO-PLUS" => AUTH_SPNEGO_PLUS, + "SXOVER-PLUS" => AUTH_SXOVER_PLUS, + "CRAM-MD5" => AUTH_CRAM_MD5, + "DIGEST-MD5" => AUTH_DIGEST_MD5, + "ANONYMOUS" => AUTH_ANONYMOUS,*/ + _ => { + return Err(format!( + "Unsupported mechanism {:?} for property {:?}.", + value, + key.as_key() + )) + } + })) + } +} + +impl<'x> TryFrom> for Mechanism { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::Integer(value) => Ok(Mechanism(value as u64)), + Variable::Array(items) => { + let mut mechanism = 0; + + for item in items { + match item { + Variable::Integer(value) => mechanism |= value as u64, + _ => return Err(()), + } + } + + Ok(Mechanism(mechanism)) + } + _ => Err(()), + } + } +} + +impl From for Constant { + fn from(value: Mechanism) -> Self { + Constant::Integer(value.0 as i64) + } +} + +impl ConstantValue for Mechanism {} + +impl From for u64 { + fn from(value: Mechanism) -> Self { + value.0 + } +} + +impl From for Mechanism { + fn from(value: u64) -> Self { + Mechanism(value) } } diff --git a/crates/smtp/src/config/shared.rs b/crates/smtp/src/config/shared.rs new file mode 100644 index 00000000..f31210c4 --- /dev/null +++ b/crates/smtp/src/config/shared.rs @@ -0,0 +1,100 @@ +/* + * 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 ahash::AHashMap; +use mail_send::Credentials; +use utils::config::Config; + +use crate::core::Shared; + +use super::{ConfigContext, RelayHost}; + +pub trait ConfigShared { + fn parse_shared(&self, ctx: &ConfigContext) -> super::Result; + + fn parse_host(&self, id: &str) -> super::Result; +} + +impl ConfigShared for Config { + fn parse_shared(&self, ctx: &ConfigContext) -> super::Result { + let mut relay_hosts = AHashMap::new(); + + for id in self.sub_keys("remote", ".address") { + relay_hosts.insert(id.to_string(), self.parse_host(id)?); + } + + Ok(Shared { + scripts: ctx.scripts.clone(), + signers: ctx.signers.clone(), + sealers: ctx.sealers.clone(), + directories: ctx.directory.directories.clone(), + lookup_stores: ctx.stores.lookup_stores.clone(), + relay_hosts, + default_directory: ctx + .directory + .directories + .get(self.value_require("storage.directory")?) + .ok_or_else(|| { + format!( + "Directory {:?} not found for key \"storage.directory\".", + self.value_require("storage.directory").unwrap() + ) + })? + .clone(), + default_data_store: ctx.stores.get_store(self, "storage.data")?, + default_lookup_store: self + .value_or_default("storage.lookup", "storage.data") + .and_then(|id| ctx.stores.lookup_stores.get(id)) + .ok_or_else(|| { + format!( + "Lookup store {:?} not found for key \"storage.lookup\".", + self.value_or_default("storage.lookup", "storage.data") + .unwrap() + ) + })? + .clone(), + }) + } + + fn parse_host(&self, id: &str) -> super::Result { + let username = self.value(("remote", id, "auth.username")); + let secret = self.value(("remote", id, "auth.secret")); + + Ok(RelayHost { + address: self.property_require(("remote", id, "address"))?, + port: self.property_require(("remote", id, "port"))?, + protocol: self.property_require(("remote", id, "protocol"))?, + auth: if let (Some(username), Some(secret)) = (username, secret) { + Credentials::new(username.to_string(), secret.to_string()).into() + } else { + None + }, + tls_implicit: self + .property(("remote", id, "tls.implicit"))? + .unwrap_or(true), + tls_allow_invalid_certs: self + .property(("remote", id, "tls.allow-invalid-certs"))? + .unwrap_or(false), + }) + } +} diff --git a/crates/smtp/src/config/throttle.rs b/crates/smtp/src/config/throttle.rs index d04d1e99..0f41646e 100644 --- a/crates/smtp/src/config/throttle.rs +++ b/crates/smtp/src/config/throttle.rs @@ -21,9 +21,9 @@ * for more details. */ -use super::{condition::ConfigCondition, *}; +use super::*; use utils::config::{ - utils::{AsKey, ParseValue}, + utils::{AsKey, NoConstants}, Config, }; @@ -31,16 +31,14 @@ pub trait ConfigThrottle { fn parse_throttle( &self, prefix: impl AsKey, - ctx: &ConfigContext, - available_envelope_keys: &[EnvelopeKey], + available_envelope_keys: &[u32], available_throttle_keys: u16, ) -> super::Result>; fn parse_throttle_item( &self, prefix: impl AsKey, - ctx: &ConfigContext, - available_envelope_keys: &[EnvelopeKey], + available_envelope_keys: &[u32], available_throttle_keys: u16, ) -> super::Result; } @@ -49,8 +47,7 @@ impl ConfigThrottle for Config { fn parse_throttle( &self, prefix: impl AsKey, - ctx: &ConfigContext, - available_envelope_keys: &[EnvelopeKey], + available_envelope_keys: &[u32], available_throttle_keys: u16, ) -> super::Result> { let prefix_ = prefix.as_key(); @@ -58,7 +55,6 @@ impl ConfigThrottle for Config { for array_pos in self.sub_keys(prefix, "") { throttles.push(self.parse_throttle_item( (&prefix_, array_pos), - ctx, available_envelope_keys, available_throttle_keys, )?); @@ -70,8 +66,7 @@ impl ConfigThrottle for Config { fn parse_throttle_item( &self, prefix: impl AsKey, - ctx: &ConfigContext, - available_envelope_keys: &[EnvelopeKey], + available_envelope_keys: &[u32], available_throttle_keys: u16, ) -> super::Result { let prefix = prefix.as_key(); @@ -88,12 +83,12 @@ impl ConfigThrottle for Config { } let throttle = Throttle { - conditions: if self.values((&prefix, "match")).next().is_some() { - self.parse_condition((&prefix, "match"), ctx, available_envelope_keys)? + expr: if let Some(expr) = self.value((&prefix, "match")) { + Expression::parse((&prefix, "match"), expr, |name| { + map_expr_token::(name, available_envelope_keys) + })? } else { - Conditions { - conditions: Vec::with_capacity(0), - } + Expression::default() }, keys, concurrency: self @@ -119,30 +114,6 @@ impl ConfigThrottle for Config { } } -impl ParseValue for EnvelopeKey { - fn parse_value(key: impl AsKey, value: &str) -> super::Result { - Ok(match value { - "rcpt" => EnvelopeKey::Recipient, - "rcpt-domain" => EnvelopeKey::RecipientDomain, - "sender" => EnvelopeKey::Sender, - "sender-domain" => EnvelopeKey::SenderDomain, - "listener" => EnvelopeKey::Listener, - "remote-ip" => EnvelopeKey::RemoteIp, - "local-ip" => EnvelopeKey::LocalIp, - "priority" => EnvelopeKey::Priority, - "authenticated-as" => EnvelopeKey::AuthenticatedAs, - "mx" => EnvelopeKey::Mx, - _ => { - return Err(format!( - "Invalid context key {:?} for property {:?}.", - value, - key.as_key() - )) - } - }) - } -} - pub trait ParseTrottleKey { fn parse_throttle_key(&self, key: &str) -> super::Result; } diff --git a/crates/smtp/src/core/eval.rs b/crates/smtp/src/core/eval.rs new file mode 100644 index 00000000..948e13c4 --- /dev/null +++ b/crates/smtp/src/core/eval.rs @@ -0,0 +1,318 @@ +use std::{borrow::Cow, sync::Arc, vec::IntoIter}; + +use directory::Directory; +use sieve::Sieve; +use store::{LookupKey, LookupStore, LookupValue}; +use utils::{ + config::if_block::IfBlock, + expr::{Expression, Variable}, +}; + +use crate::{ + config::{ArcSealer, DkimSigner, RelayHost}, + scripts::plugins::lookup::VariableExists, +}; + +use super::{ResolveVariable, SMTP}; + +pub const V_RECIPIENT: u32 = 0; +pub const V_RECIPIENT_DOMAIN: u32 = 1; +pub const V_SENDER: u32 = 2; +pub const V_SENDER_DOMAIN: u32 = 3; +pub const V_MX: u32 = 4; +pub const V_HELO_DOMAIN: u32 = 5; +pub const V_AUTHENTICATED_AS: u32 = 6; +pub const V_LISTENER: u32 = 7; +pub const V_REMOTE_IP: u32 = 8; +pub const V_LOCAL_IP: u32 = 9; +pub const V_PRIORITY: u32 = 10; + +pub const F_IS_LOCAL_DOMAIN: u32 = 0; +pub const F_KEY_GET: u32 = 1; +pub const F_KEY_EXISTS: u32 = 2; + +pub const VARIABLES_MAP: &[(&str, u32)] = &[ + ("rcpt", V_RECIPIENT), + ("rcpt-domain", V_RECIPIENT_DOMAIN), + ("rcpt_domain", V_RECIPIENT_DOMAIN), + ("sender", V_SENDER), + ("sender-domain", V_SENDER_DOMAIN), + ("sender_domain", V_SENDER_DOMAIN), + ("mx", V_MX), + ("helo-domain", V_HELO_DOMAIN), + ("helo_domain", V_HELO_DOMAIN), + ("authenticated-as", V_AUTHENTICATED_AS), + ("authenticated_as", V_AUTHENTICATED_AS), + ("listener", V_LISTENER), + ("remote-ip", V_REMOTE_IP), + ("remote_ip", V_REMOTE_IP), + ("local-ip", V_LOCAL_IP), + ("local_ip", V_LOCAL_IP), + ("priority", V_PRIORITY), +]; + +pub const FUNCTIONS_MAP: &[(&str, u32, u32)] = &[ + ("is_local_domain", F_IS_LOCAL_DOMAIN, 2), + ("key_get", F_KEY_GET, 2), + ("key_exists", F_KEY_EXISTS, 2), +]; + +impl SMTP { + pub async fn eval_if TryFrom>, V: ResolveVariable>( + &self, + if_block: &IfBlock, + resolver: &V, + ) -> Option { + if if_block.is_empty() { + return None; + } + + let result = if_block + .eval( + |var_id| resolver.resolve_variable(var_id), + |fnc_id, params| async move { self.eval_fnc(fnc_id, params, &if_block.key).await }, + ) + .await; + + tracing::trace!(context = "eval_if", + property = if_block.key, + result = ?result, + ); + + match result.try_into() { + Ok(value) => Some(value), + Err(_) => { + tracing::warn!( + context = "eval_if", + event = "error", + property = if_block.key, + "Failed to convert value." + ); + None + } + } + } + + pub async fn eval_expr TryFrom>, V: ResolveVariable>( + &self, + expr: &Expression, + resolver: &V, + expr_id: &str, + ) -> Option { + if expr.is_empty() { + return None; + } + + let result = expr + .eval( + |var_id| resolver.resolve_variable(var_id), + |fnc_id, params| async move { self.eval_fnc(fnc_id, params, expr_id).await }, + &mut Vec::new(), + ) + .await; + + tracing::trace!(context = "eval_expr", + property = expr_id, + result = ?result, + ); + + match result.try_into() { + Ok(value) => Some(value), + Err(_) => { + tracing::warn!( + context = "eval_expr", + event = "error", + property = expr_id, + "Failed to convert value." + ); + None + } + } + } + + async fn eval_fnc<'x>( + &self, + fnc_id: u32, + params: Vec>, + property: &str, + ) -> Variable<'x> { + let mut params = FncParams::new(params); + + match fnc_id { + F_IS_LOCAL_DOMAIN => { + let directory = params.next_as_string(); + let domain = params.next_as_string(); + + self.get_directory_or_default(directory.as_ref()) + .is_local_domain(domain.as_ref()) + .await + .unwrap_or_else(|err| { + tracing::warn!( + context = "eval_if", + event = "error", + property = property, + error = ?err, + "Failed to check if domain is local." + ); + + false + }) + .into() + } + F_KEY_GET => { + let store = params.next_as_string(); + let key = params.next_as_string(); + + self.get_lookup_store(store.as_ref()) + .key_get::(LookupKey::Key(key.into_owned().into_bytes())) + .await + .map(|value| { + if let LookupValue::Value { value, .. } = value { + Variable::from(value) + } else { + Variable::default() + } + }) + .unwrap_or_else(|err| { + tracing::warn!( + context = "eval_if", + event = "error", + property = property, + error = ?err, + "Failed to get key." + ); + + Variable::default() + }) + } + F_KEY_EXISTS => { + let store = params.next_as_string(); + let key = params.next_as_string(); + + self.get_lookup_store(store.as_ref()) + .key_get::(LookupKey::Key(key.into_owned().into_bytes())) + .await + .map(|value| matches!(value, LookupValue::Value { .. })) + .unwrap_or_else(|err| { + tracing::warn!( + context = "eval_if", + event = "error", + property = property, + error = ?err, + "Failed to get key." + ); + + false + }) + .into() + } + _ => Variable::default(), + } + } + + pub fn get_directory(&self, name: &str) -> Option<&Arc> { + self.shared.directories.get(name) + } + + pub fn get_directory_or_default(&self, name: &str) -> &Arc { + self.shared.directories.get(name).unwrap_or_else(|| { + tracing::debug!( + context = "get_directory", + event = "error", + directory = name, + "Directory not found, using default." + ); + + &self.shared.default_directory + }) + } + + pub fn get_lookup_store(&self, name: &str) -> &LookupStore { + self.shared.lookup_stores.get(name).unwrap_or_else(|| { + tracing::debug!( + context = "get_lookup_store", + event = "error", + directory = name, + "Store not found, using default." + ); + + &self.shared.default_lookup_store + }) + } + + pub fn get_arc_sealer(&self, name: &str) -> Option<&ArcSealer> { + self.shared + .sealers + .get(name) + .map(|s| s.as_ref()) + .or_else(|| { + tracing::warn!( + context = "get_arc_sealer", + event = "error", + name = name, + "Arc sealer not found." + ); + + None + }) + } + + pub fn get_dkim_signer(&self, name: &str) -> Option<&DkimSigner> { + self.shared + .signers + .get(name) + .map(|s| s.as_ref()) + .or_else(|| { + tracing::warn!( + context = "get_dkim_signer", + event = "error", + name = name, + "DKIM signer not found." + ); + + None + }) + } + + pub fn get_sieve_script(&self, name: &str) -> Option<&Arc> { + self.shared.scripts.get(name).or_else(|| { + tracing::warn!( + context = "get_sieve_script", + event = "error", + name = name, + "Sieve script not found." + ); + + None + }) + } + + pub fn get_relay_host(&self, name: &str) -> Option<&RelayHost> { + self.shared.relay_hosts.get(name).or_else(|| { + tracing::warn!( + context = "get_relay_host", + event = "error", + name = name, + "Remote host not found." + ); + + None + }) + } +} + +struct FncParams<'x> { + params: IntoIter>, +} + +impl<'x> FncParams<'x> { + pub fn new(params: Vec>) -> Self { + Self { + params: params.into_iter(), + } + } + + pub fn next_as_string(&mut self) -> Cow<'x, str> { + self.params.next().unwrap().into_string() + } +} diff --git a/crates/smtp/src/core/if_block.rs b/crates/smtp/src/core/if_block.rs deleted file mode 100644 index 2141b20b..00000000 --- a/crates/smtp/src/core/if_block.rs +++ /dev/null @@ -1,290 +0,0 @@ -/* - * 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 std::{borrow::Cow, sync::Arc}; - -use utils::config::{DynValue, KeyLookup}; - -use crate::config::{ - Condition, ConditionMatch, Conditions, EnvelopeKey, IfBlock, MaybeDynValue, StringMatch, -}; - -pub struct Captures<'x, T> { - value: &'x T, - captures: Vec, -} - -impl IfBlock { - pub async fn eval(&self, envelope: &impl KeyLookup) -> &T { - for if_then in &self.if_then { - if if_then.conditions.eval(envelope).await { - return &if_then.then; - } - } - - &self.default - } - - pub async fn eval_and_capture( - &self, - envelope: &impl KeyLookup, - ) -> Captures<'_, T> { - for if_then in &self.if_then { - if let Some(captures) = if_then.conditions.eval_and_capture(envelope).await { - return Captures { - value: &if_then.then, - captures, - }; - } - } - - Captures { - value: &self.default, - captures: vec![], - } - } -} - -impl Conditions { - pub async fn eval(&self, envelope: &impl KeyLookup) -> bool { - let mut conditions = self.conditions.iter(); - let mut matched = false; - - while let Some(rule) = conditions.next() { - match rule { - Condition::Match { key, value, not } => { - matched = match value { - ConditionMatch::String(value) => { - let ctx_value = envelope.key(key); - match value { - StringMatch::Equal(value) => value.eq(ctx_value.as_ref()), - StringMatch::StartsWith(value) => ctx_value.starts_with(value), - StringMatch::EndsWith(value) => ctx_value.ends_with(value), - } - } - ConditionMatch::IpAddrMask(value) => { - value.matches(&envelope.key_as_ip(key)) - } - ConditionMatch::UInt(value) => *value == envelope.key_as_int(key) as u16, - ConditionMatch::Int(value) => *value == envelope.key_as_int(key) as i16, - ConditionMatch::Lookup(lookup) => { - if let Some(result) = lookup.contains(envelope.key(key).as_ref()).await - { - result - } else { - return false; - } - } - ConditionMatch::Regex(value) => value.is_match(envelope.key(key).as_ref()), - } ^ not; - } - Condition::JumpIfTrue { positions } => { - if matched { - //TODO use advance_by when stabilized - for _ in 0..*positions { - conditions.next(); - } - } - } - Condition::JumpIfFalse { positions } => { - if !matched { - //TODO use advance_by when stabilized - for _ in 0..*positions { - conditions.next(); - } - } - } - } - } - - matched - } - - pub async fn eval_and_capture( - &self, - envelope: &impl KeyLookup, - ) -> Option> { - let mut conditions = self.conditions.iter(); - let mut matched = false; - let mut last_capture = vec![]; - let mut regex_capture = vec![]; - - while let Some(rule) = conditions.next() { - match rule { - Condition::Match { key, value, not } => { - let ctx_value = envelope.key(key); - matched = match value { - ConditionMatch::String(value) => match value { - StringMatch::Equal(value) => value.eq(ctx_value.as_ref()), - StringMatch::StartsWith(value) => ctx_value.starts_with(value), - StringMatch::EndsWith(value) => ctx_value.ends_with(value), - }, - ConditionMatch::IpAddrMask(value) => { - value.matches(&envelope.key_as_ip(key)) - } - ConditionMatch::UInt(value) => *value == envelope.key_as_int(key) as u16, - ConditionMatch::Int(value) => *value == envelope.key_as_int(key) as i16, - ConditionMatch::Lookup(lookup) => { - lookup.contains(ctx_value.as_ref()).await? - } - ConditionMatch::Regex(value) => { - regex_capture.clear(); - - for captures in value.captures_iter(ctx_value.as_ref()) { - for capture in captures.iter() { - regex_capture - .push(capture.map_or("", |m| m.as_str()).to_string()); - } - } - - !regex_capture.is_empty() - } - } ^ not; - - // Save last capture - if matched { - last_capture = if regex_capture.is_empty() { - vec![ctx_value.into_owned()] - } else { - std::mem::take(&mut regex_capture) - }; - } - } - Condition::JumpIfTrue { positions } => { - if matched { - //TODO use advance_by when stabilized - for _ in 0..*positions { - conditions.next(); - } - } - } - Condition::JumpIfFalse { positions } => { - if !matched { - //TODO use advance_by when stabilized - for _ in 0..*positions { - conditions.next(); - } - } - } - } - } - - if matched { - Some(last_capture) - } else { - None - } - } -} - -impl<'x> Captures<'x, DynValue> { - pub fn into_value(self, keys: &'x impl KeyLookup) -> Cow<'x, str> { - self.value.apply(self.captures, keys) - } -} - -impl<'x> Captures<'x, Option>> { - pub fn into_value(self, keys: &'x impl KeyLookup) -> Option> { - self.value.as_ref().map(|v| v.apply(self.captures, keys)) - } -} - -impl<'x, T: ?Sized> Captures<'x, MaybeDynValue> { - pub fn into_value(self, keys: &impl KeyLookup) -> Option> { - match &self.value { - MaybeDynValue::Dynamic { eval, items } => { - let r = eval.apply(self.captures, keys); - - match items.get(r.as_ref()) { - Some(value) => value.clone().into(), - None => { - tracing::warn!( - context = "eval", - event = "error", - expression = ?eval, - result = ?r, - "Failed to resolve rule: value {r:?} not found in item list", - ); - None - } - } - } - MaybeDynValue::Static(value) => value.clone().into(), - } - } -} - -impl<'x, T: ?Sized> Captures<'x, Vec>> { - pub fn into_value(self, keys: &impl KeyLookup) -> Vec> { - let mut results = Vec::with_capacity(self.value.len()); - for value in self.value.iter() { - match value { - MaybeDynValue::Dynamic { eval, items } => { - let r = eval.apply_borrowed(&self.captures, keys); - match items.get(r.as_ref()) { - Some(value) => { - results.push(value.clone()); - } - None => { - tracing::warn!( - context = "eval", - event = "error", - expression = ?eval, - result = ?r, - "Failed to resolve rule: value {r:?} not found in item list", - ); - } - } - } - MaybeDynValue::Static(value) => { - results.push(value.clone()); - } - } - } - results - } -} - -impl<'x, T: ?Sized> Captures<'x, Option>> { - pub fn into_value(self, keys: &impl KeyLookup) -> Option> { - match self.value.as_ref()? { - MaybeDynValue::Dynamic { eval, items } => { - let r = eval.apply(self.captures, keys); - match items.get(r.as_ref()) { - Some(value) => value.clone().into(), - None => { - tracing::warn!( - context = "eval", - event = "error", - expression = ?eval, - result = ?r, - "Failed to resolve rule: value {r:?} not found in item list", - ); - None - } - } - } - MaybeDynValue::Static(value) => value.clone().into(), - } - } -} diff --git a/crates/smtp/src/core/management.rs b/crates/smtp/src/core/management.rs index 8d4b7057..246156e7 100644 --- a/crates/smtp/src/core/management.rs +++ b/crates/smtp/src/core/management.rs @@ -239,9 +239,8 @@ impl SMTP { }) { match self - .queue - .config - .directory + .shared + .default_directory .authenticate(&Credentials::Plain { username, secret }, remote_addr, false) .await { diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index 718309c5..28be3737 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -40,7 +40,7 @@ use smtp_proto::{ }, IntoString, }; -use store::{LookupKey, LookupStore, LookupValue, Value}; +use store::{LookupStore, Store, Value}; use tokio::{ io::{AsyncRead, AsyncWrite}, sync::mpsc, @@ -48,14 +48,15 @@ use tokio::{ use tokio_rustls::TlsConnector; use tracing::Span; use utils::{ + expr, ipc::DeliveryEvent, listener::{limiter::InFlight, stream::NullIo, ServerInstance, TcpAcceptor}, }; use crate::{ config::{ - scripts::SieveContext, DkimSigner, MailAuthConfig, QueueConfig, ReportConfig, - SessionConfig, VerifyStrategy, + scripts::SieveContext, ArcSealer, DkimSigner, MailAuthConfig, QueueConfig, RelayHost, + ReportConfig, SessionConfig, VerifyStrategy, }, inbound::auth::SaslToken, outbound::{ @@ -64,12 +65,11 @@ use crate::{ }, queue::{self, DomainPart, QueueId, QuotaLimiter}, reporting, - scripts::plugins::lookup::VariableExists, }; use self::throttle::{Limiter, ThrottleKey, ThrottleKeyHasherBuilder}; -pub mod if_block; +pub mod eval; pub mod management; pub mod params; pub mod throttle; @@ -105,20 +105,31 @@ pub struct SMTP { pub mail_auth: MailAuthConfig, pub report: ReportCore, pub sieve: SieveCore, + pub shared: Shared, #[cfg(feature = "local_delivery")] pub delivery_tx: mpsc::Sender, } +pub struct Shared { + pub scripts: AHashMap>, + pub signers: AHashMap>, + pub sealers: AHashMap>, + pub directories: AHashMap>, + pub lookup_stores: AHashMap, + pub relay_hosts: AHashMap, + + // Default store and directory + pub default_directory: Arc, + pub default_data_store: Store, + pub default_lookup_store: LookupStore, +} + pub struct SieveCore { pub runtime: Runtime, - pub scripts: AHashMap>, - pub from_addr: String, pub from_name: String, pub return_path: String, pub sign: Vec>, - pub directories: AHashMap>, - pub lookup_stores: AHashMap, } pub struct Resolvers { @@ -156,12 +167,6 @@ pub struct TlsConnectors { pub dummy_verify: TlsConnector, } -#[derive(Clone)] -pub enum Lookup { - Store(LookupStore), - Directory(directory::Lookup), -} - pub enum State { Request(RequestReceiver), Bdat(BdatReceiver), @@ -186,7 +191,9 @@ pub struct Session { pub struct SessionData { pub local_ip: IpAddr, + pub local_ip_str: String, pub remote_ip: IpAddr, + pub remote_ip_str: String, pub remote_port: u16, pub helo_domain: String, @@ -259,6 +266,8 @@ impl SessionData { SessionData { local_ip, remote_ip, + local_ip_str: local_ip.to_string(), + remote_ip_str: remote_ip.to_string(), remote_port, helo_domain: String::new(), mail_from: None, @@ -282,49 +291,8 @@ impl SessionData { } } -impl Lookup { - pub async fn contains(&self, item: &str) -> Option { - match self { - Lookup::Store(LookupStore::Query(lookup)) => lookup - .store - .query::(&lookup.query, vec![item.into()]) - .await - .ok(), - Lookup::Store(store) => store - .key_get::(LookupKey::Key(item.to_string().into_bytes())) - .await - .ok() - .map(|v| !matches!(v, LookupValue::None)), - Lookup::Directory(lookup) => match lookup { - directory::Lookup::DomainExists(directory) => { - directory.is_local_domain(item).await.ok() - } - directory::Lookup::EmailExists(directory) => directory.rcpt(item).await.ok(), - }, - } - } -} - -impl PartialEq for Lookup { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (Lookup::Store(LookupStore::Query(a)), Lookup::Store(LookupStore::Query(b))) => { - a.query == b.query - } - (Lookup::Store(LookupStore::Store(_)), Lookup::Store(LookupStore::Store(_))) => true, - (Lookup::Directory(a), Lookup::Directory(b)) => matches!( - (a, b), - ( - directory::Lookup::DomainExists(_), - directory::Lookup::DomainExists(_) - ) | ( - directory::Lookup::EmailExists(_), - directory::Lookup::EmailExists(_) - ) - ), - _ => false, - } - } +pub trait ResolveVariable { + fn resolve_variable(&self, variable: u32) -> expr::Variable<'_>; } pub fn into_sieve_value(value: Value) -> Variable { @@ -356,18 +324,6 @@ pub fn to_store_value(value: &Variable) -> Value<'static> { } } -impl From for Lookup { - fn from(lookup: LookupStore) -> Self { - Lookup::Store(lookup) - } -} - -impl From for Lookup { - fn from(lookup: directory::Lookup) -> Self { - Lookup::Directory(lookup) - } -} - impl Default for State { fn default() -> Self { State::Request(RequestReceiver::default()) @@ -522,6 +478,8 @@ impl SessionData { SessionData { local_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), remote_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), + local_ip_str: "127.0.0.1".to_string(), + remote_ip_str: "127.0.0.1".to_string(), remote_port: 0, helo_domain: "localhost".into(), mail_from, diff --git a/crates/smtp/src/core/params.rs b/crates/smtp/src/core/params.rs index 583fa52c..c0da42ef 100644 --- a/crates/smtp/src/core/params.rs +++ b/crates/smtp/src/core/params.rs @@ -21,62 +21,120 @@ * for more details. */ +use std::time::Duration; + use tokio::io::{AsyncRead, AsyncWrite}; +use crate::config::VerifyStrategy; + use super::Session; impl Session { pub async fn eval_session_params(&mut self) { let c = &self.core.session.config; - self.data.bytes_left = *c.transfer_limit.eval(self).await; - self.data.valid_until += *c.duration.eval(self).await; + self.data.bytes_left = self + .core + .eval_if(&c.transfer_limit, self) + .await + .unwrap_or(250 * 1024 * 1024); + self.data.valid_until += self + .core + .eval_if(&c.duration, self) + .await + .unwrap_or_else(|| Duration::from_secs(15 * 60)); - self.params.timeout = *c.timeout.eval(self).await; - self.params.spf_ehlo = *self.core.mail_auth.spf.verify_ehlo.eval(self).await; - self.params.spf_mail_from = *self.core.mail_auth.spf.verify_mail_from.eval(self).await; - self.params.iprev = *self.core.mail_auth.iprev.verify.eval(self).await; + self.params.timeout = self + .core + .eval_if(&c.timeout, self) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 60)); + self.params.spf_ehlo = self + .core + .eval_if(&self.core.mail_auth.spf.verify_ehlo, self) + .await + .unwrap_or(VerifyStrategy::Relaxed); + self.params.spf_mail_from = self + .core + .eval_if(&self.core.mail_auth.spf.verify_mail_from, self) + .await + .unwrap_or(VerifyStrategy::Relaxed); + self.params.iprev = self + .core + .eval_if(&self.core.mail_auth.iprev.verify, self) + .await + .unwrap_or(VerifyStrategy::Relaxed); // Ehlo parameters let ec = &self.core.session.config.ehlo; - self.params.ehlo_require = *ec.require.eval(self).await; - self.params.ehlo_reject_non_fqdn = *ec.reject_non_fqdn.eval(self).await; + self.params.ehlo_require = self.core.eval_if(&ec.require, self).await.unwrap_or(true); + self.params.ehlo_reject_non_fqdn = self + .core + .eval_if(&ec.reject_non_fqdn, self) + .await + .unwrap_or(true); // Auth parameters let ac = &self.core.session.config.auth; - self.params.auth_directory = ac.directory.eval_and_capture(self).await.into_value(self); - self.params.auth_require = *ac.require.eval(self).await; - self.params.auth_errors_max = *ac.errors_max.eval(self).await; - self.params.auth_errors_wait = *ac.errors_wait.eval(self).await; - self.params.auth_plain_text = *ac.allow_plain_text.eval(self).await; - self.params.auth_match_sender = *ac.must_match_sender.eval(self).await; + self.params.auth_directory = self + .core + .eval_if::(&ac.directory, self) + .await + .and_then(|name| self.core.get_directory(&name)) + .cloned(); + self.params.auth_require = self.core.eval_if(&ac.require, self).await.unwrap_or(false); + self.params.auth_errors_max = self.core.eval_if(&ac.errors_max, self).await.unwrap_or(3); + self.params.auth_errors_wait = self + .core + .eval_if(&ac.errors_wait, self) + .await + .unwrap_or_else(|| Duration::from_secs(30)); + self.params.auth_plain_text = self + .core + .eval_if(&ac.allow_plain_text, self) + .await + .unwrap_or(false); + self.params.auth_match_sender = self + .core + .eval_if(&ac.must_match_sender, self) + .await + .unwrap_or(true); // VRFY/EXPN parameters let ec = &self.core.session.config.extensions; - self.params.can_expn = *ec.expn.eval(self).await; - self.params.can_vrfy = *ec.vrfy.eval(self).await; + self.params.can_expn = self.core.eval_if(&ec.expn, self).await.unwrap_or(false); + self.params.can_vrfy = self.core.eval_if(&ec.vrfy, self).await.unwrap_or(false); } pub async fn eval_post_auth_params(&mut self) { // Refresh VRFY/EXPN parameters let ec = &self.core.session.config.extensions; - self.params.can_expn = *ec.expn.eval(self).await; - self.params.can_vrfy = *ec.vrfy.eval(self).await; + self.params.can_expn = self.core.eval_if(&ec.expn, self).await.unwrap_or(false); + self.params.can_vrfy = self.core.eval_if(&ec.vrfy, self).await.unwrap_or(false); } pub async fn eval_rcpt_params(&mut self) { let rc = &self.core.session.config.rcpt; - self.params.rcpt_errors_max = *rc.errors_max.eval(self).await; - self.params.rcpt_errors_wait = *rc.errors_wait.eval(self).await; - self.params.rcpt_max = *rc.max_recipients.eval(self).await; - self.params.rcpt_dsn = *self.core.session.config.extensions.dsn.eval(self).await; - - self.params.max_message_size = *self + self.params.rcpt_errors_max = self.core.eval_if(&rc.errors_max, self).await.unwrap_or(10); + self.params.rcpt_errors_wait = self .core - .session - .config - .data - .max_message_size - .eval(self) - .await; + .eval_if(&rc.errors_wait, self) + .await + .unwrap_or_else(|| Duration::from_secs(30)); + self.params.rcpt_max = self + .core + .eval_if(&rc.max_recipients, self) + .await + .unwrap_or(100); + self.params.rcpt_dsn = self + .core + .eval_if(&self.core.session.config.extensions.dsn, self) + .await + .unwrap_or(true); + + self.params.max_message_size = self + .core + .eval_if(&self.core.session.config.data.max_message_size, self) + .await + .unwrap_or(25 * 1024 * 1024); } } diff --git a/crates/smtp/src/core/throttle.rs b/crates/smtp/src/core/throttle.rs index 34bf3f4c..6cb3aa03 100644 --- a/crates/smtp/src/core/throttle.rs +++ b/crates/smtp/src/core/throttle.rs @@ -24,16 +24,13 @@ use ::utils::listener::limiter::{ConcurrencyLimiter, RateLimiter}; use dashmap::mapref::entry::Entry; use tokio::io::{AsyncRead, AsyncWrite}; -use utils::config::{KeyLookup, Rate}; +use utils::config::Rate; -use std::{ - hash::{BuildHasher, Hash, Hasher}, - net::IpAddr, -}; +use std::hash::{BuildHasher, Hash, Hasher}; use crate::config::*; -use super::Session; +use super::{eval::*, ResolveVariable, Session}; #[derive(Debug)] pub struct Limiter { @@ -85,17 +82,21 @@ impl BuildHasher for ThrottleKeyHasherBuilder { } impl QueueQuota { - pub fn new_key(&self, e: &impl KeyLookup) -> ThrottleKey { + pub fn new_key(&self, e: &impl ResolveVariable) -> ThrottleKey { let mut hasher = blake3::Hasher::new(); if (self.keys & THROTTLE_RCPT) != 0 { - hasher.update(e.key(&EnvelopeKey::Recipient).as_bytes()); + hasher.update(e.resolve_variable(V_RECIPIENT).to_string().as_bytes()); } if (self.keys & THROTTLE_RCPT_DOMAIN) != 0 { - hasher.update(e.key(&EnvelopeKey::RecipientDomain).as_bytes()); + hasher.update( + e.resolve_variable(V_RECIPIENT_DOMAIN) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_SENDER) != 0 { - let sender = e.key(&EnvelopeKey::Sender); + let sender = e.resolve_variable(V_SENDER).into_string(); hasher.update( if !sender.is_empty() { sender.as_ref() @@ -106,7 +107,7 @@ impl QueueQuota { ); } if (self.keys & THROTTLE_SENDER_DOMAIN) != 0 { - let sender_domain = e.key(&EnvelopeKey::SenderDomain); + let sender_domain = e.resolve_variable(V_SENDER_DOMAIN).into_string(); hasher.update( if !sender_domain.is_empty() { sender_domain.as_ref() @@ -132,17 +133,21 @@ impl QueueQuota { } impl Throttle { - pub fn new_key(&self, e: &impl KeyLookup) -> ThrottleKey { + pub fn new_key(&self, e: &impl ResolveVariable) -> ThrottleKey { let mut hasher = blake3::Hasher::new(); if (self.keys & THROTTLE_RCPT) != 0 { - hasher.update(e.key(&EnvelopeKey::Recipient).as_bytes()); + hasher.update(e.resolve_variable(V_RECIPIENT).to_string().as_bytes()); } if (self.keys & THROTTLE_RCPT_DOMAIN) != 0 { - hasher.update(e.key(&EnvelopeKey::RecipientDomain).as_bytes()); + hasher.update( + e.resolve_variable(V_RECIPIENT_DOMAIN) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_SENDER) != 0 { - let sender = e.key(&EnvelopeKey::Sender); + let sender = e.resolve_variable(V_SENDER).into_string(); hasher.update( if !sender.is_empty() { sender.as_ref() @@ -153,7 +158,7 @@ impl Throttle { ); } if (self.keys & THROTTLE_SENDER_DOMAIN) != 0 { - let sender_domain = e.key(&EnvelopeKey::SenderDomain); + let sender_domain = e.resolve_variable(V_SENDER_DOMAIN).into_string(); hasher.update( if !sender_domain.is_empty() { sender_domain.as_ref() @@ -164,36 +169,26 @@ impl Throttle { ); } if (self.keys & THROTTLE_HELO_DOMAIN) != 0 { - hasher.update(e.key(&EnvelopeKey::HeloDomain).as_bytes()); + hasher.update(e.resolve_variable(V_HELO_DOMAIN).to_string().as_bytes()); } if (self.keys & THROTTLE_AUTH_AS) != 0 { - hasher.update(e.key(&EnvelopeKey::AuthenticatedAs).as_bytes()); + hasher.update( + e.resolve_variable(V_AUTHENTICATED_AS) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_LISTENER) != 0 { - hasher.update(&e.key_as_int(&EnvelopeKey::Listener).to_ne_bytes()[..]); + hasher.update(e.resolve_variable(V_LISTENER).to_string().as_bytes()); } if (self.keys & THROTTLE_MX) != 0 { - hasher.update(e.key(&EnvelopeKey::Mx).as_bytes()); + hasher.update(e.resolve_variable(V_MX).to_string().as_bytes()); } if (self.keys & THROTTLE_REMOTE_IP) != 0 { - match &e.key_as_ip(&EnvelopeKey::RemoteIp) { - IpAddr::V4(ip) => { - hasher.update(&ip.octets()[..]); - } - IpAddr::V6(ip) => { - hasher.update(&ip.octets()[..]); - } - } + hasher.update(e.resolve_variable(V_REMOTE_IP).to_string().as_bytes()); } if (self.keys & THROTTLE_LOCAL_IP) != 0 { - match &e.key_as_ip(&EnvelopeKey::LocalIp) { - IpAddr::V4(ip) => { - hasher.update(&ip.octets()[..]); - } - IpAddr::V6(ip) => { - hasher.update(&ip.octets()[..]); - } - } + hasher.update(e.resolve_variable(V_LOCAL_IP).to_string().as_bytes()); } if let Some(rate_limit) = &self.rate { hasher.update(&rate_limit.period.as_secs().to_ne_bytes()[..]); @@ -220,7 +215,13 @@ impl Session { }; for t in throttles { - if t.conditions.conditions.is_empty() || t.conditions.eval(self).await { + if t.expr.is_empty() + || self + .core + .eval_expr(&t.expr, self, "throttle") + .await + .unwrap_or(false) + { if (t.keys & THROTTLE_RCPT_DOMAIN) != 0 { let d = self .data diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index 49b7aba2..72e8f8ca 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -39,9 +39,10 @@ use smtp_proto::{ MAIL_BY_RETURN, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, }; use tokio::{io::AsyncWriteExt, process::Command}; -use utils::listener::SessionStream; +use utils::{config::Rate, listener::SessionStream}; use crate::{ + config::VerifyStrategy, core::{Session, SessionAddress, State}, queue::{self, Message, SimpleEnvelope}, reporting::analysis::AnalyzeReport, @@ -69,7 +70,13 @@ impl Session { let dc = &self.core.session.config.data; let ac = &self.core.mail_auth; let rc = &self.core.report.config; - if auth_message.received_headers_count() > *dc.max_received_headers.eval(self).await { + if auth_message.received_headers_count() + > self + .core + .eval_if(&dc.max_received_headers, self) + .await + .unwrap_or(50) + { tracing::info!(parent: &self.span, context = "data", event = "loop-detected", @@ -81,8 +88,16 @@ impl Session { } // Verify DKIM - let dkim = *ac.dkim.verify.eval(self).await; - let dmarc = *ac.dmarc.verify.eval(self).await; + let dkim = self + .core + .eval_if(&ac.dkim.verify, self) + .await + .unwrap_or(VerifyStrategy::Relaxed); + let dmarc = self + .core + .eval_if(&ac.dmarc.verify, self) + .await + .unwrap_or(VerifyStrategy::Relaxed); let dkim_output = if dkim.verify() || dmarc.verify() { let dkim_output = self.core.resolvers.dns.verify_dkim(&auth_message).await; let rejected = dkim.is_strict() @@ -91,10 +106,10 @@ impl Session { .any(|d| matches!(d.result(), DkimResult::Pass)); // Send reports for failed signatures - if let Some(rate) = rc.dkim.send.eval(self).await { + if let Some(rate) = self.core.eval_if::(&rc.dkim.send, self).await { for output in &dkim_output { if let Some(rcpt) = output.failure_report_addr() { - self.send_dkim_report(rcpt, &auth_message, rate, rejected, output) + self.send_dkim_report(rcpt, &auth_message, &rate, rejected, output) .await; } } @@ -132,8 +147,16 @@ impl Session { }; // Verify ARC - let arc = *ac.arc.verify.eval(self).await; - let arc_sealer = ac.arc.seal.eval_and_capture(self).await.into_value(self); + let arc = self + .core + .eval_if(&ac.arc.verify, self) + .await + .unwrap_or(VerifyStrategy::Relaxed); + let arc_sealer = self + .core + .eval_if::(&ac.arc.seal, self) + .await + .and_then(|name| self.core.get_arc_sealer(&name)); let arc_output = if arc.verify() || arc_sealer.is_some() { let arc_output = self.core.resolvers.dns.verify_arc(&auth_message).await; @@ -316,12 +339,21 @@ impl Session { // Pipe message for pipe in &dc.pipe_commands { - if let Some(command_) = pipe.command.eval(self).await { + if let Some(command_) = self.core.eval_if::(&pipe.command, self).await { let piped_message = edited_message.as_ref().unwrap_or(&raw_message).clone(); - let timeout = *pipe.timeout.eval(self).await; + let timeout = self + .core + .eval_if(&pipe.timeout, self) + .await + .unwrap_or_else(|| Duration::from_secs(30)); - let mut command = Command::new(command_); - for argument in pipe.arguments.eval(self).await { + let mut command = Command::new(&command_); + for argument in self + .core + .eval_if::, _>(&pipe.arguments, self) + .await + .unwrap_or_default() + { command.arg(argument); } match command @@ -403,7 +435,12 @@ impl Session { // Sieve filtering let mut headers = Vec::with_capacity(64); - if let Some(script) = dc.script.eval(self).await { + if let Some(script) = self + .core + .eval_if::(&dc.script, self) + .await + .and_then(|name| self.core.get_sieve_script(&name)) + { let params = self .build_script_parameters("data") .with_message(edited_message.as_ref().unwrap_or(&raw_message).clone()) @@ -498,18 +535,33 @@ impl Session { let mut message = self.build_message(mail_from, rcpt_to).await; // Add Received header - if *dc.add_received.eval(self).await { + if self + .core + .eval_if(&dc.add_received, self) + .await + .unwrap_or(true) + { self.write_received(&mut headers, message.id) } // Add authentication results header - if *dc.add_auth_results.eval(self).await { + if self + .core + .eval_if(&dc.add_auth_results, self) + .await + .unwrap_or(true) + { auth_results.write_header(&mut headers); } // Add Received-SPF header if let Some(spf_output) = &self.data.spf_mail_from { - if *dc.add_received_spf.eval(self).await { + if self + .core + .eval_if(&dc.add_received_spf, self) + .await + .unwrap_or(true) + { ReceivedSpf::new( spf_output, self.data.remote_ip, @@ -541,19 +593,32 @@ impl Session { } // Add any missing headers - if !auth_message.has_date_header() && *dc.add_date.eval(self).await { + if !auth_message.has_date_header() + && self.core.eval_if(&dc.add_date, self).await.unwrap_or(true) + { headers.extend_from_slice(b"Date: "); headers.extend_from_slice(Date::now().to_rfc822().as_bytes()); headers.extend_from_slice(b"\r\n"); } - if !auth_message.has_message_id_header() && *dc.add_message_id.eval(self).await { + if !auth_message.has_message_id_header() + && self + .core + .eval_if(&dc.add_message_id, self) + .await + .unwrap_or(true) + { headers.extend_from_slice(b"Message-ID: "); let _ = generate_message_id_header(&mut headers, &self.instance.hostname); headers.extend_from_slice(b"\r\n"); } // Add Return-Path - if *dc.add_return_path.eval(self).await { + if self + .core + .eval_if(&dc.add_return_path, self) + .await + .unwrap_or(true) + { headers.extend_from_slice(b"Return-Path: <"); headers.extend_from_slice(message.return_path.as_bytes()); headers.extend_from_slice(b">\r\n"); @@ -561,17 +626,24 @@ impl Session { // DKIM sign let raw_message = edited_message.unwrap_or(raw_message); - for signer in ac.dkim.sign.eval_and_capture(self).await.into_value(self) { - match signer.sign_chained(&[headers.as_ref(), &raw_message]) { - Ok(signature) => { - signature.write_header(&mut headers); - } - Err(err) => { - tracing::info!(parent: &self.span, + for signer in self + .core + .eval_if::, _>(&ac.dkim.sign, self) + .await + .unwrap_or_default() + { + if let Some(signer) = self.core.get_dkim_signer(&signer) { + match signer.sign_chained(&[headers.as_ref(), &raw_message]) { + Ok(signature) => { + signature.write_header(&mut headers); + } + Err(err) => { + tracing::info!(parent: &self.span, context = "dkim", event = "sign-failed", return_path = message.return_path, "Failed to sign message: {}", err); + } } } } @@ -580,7 +652,7 @@ impl Session { message.size = raw_message.len() + headers.len(); // Verify queue quota - if self.core.queue.has_quota(&mut message).await { + if self.core.has_quota(&mut message).await { let queue_id = message.id; if self .core @@ -650,37 +722,52 @@ impl Session { // Set expiration and notification times let config = &self.core.queue.config; - let notify_intervals = config.notify.eval(&envelope).await; + let (num_intervals, next_notify) = self + .core + .eval_if::, _>(&config.notify, &envelope) + .await + .and_then(|v| (v.len(), v.into_iter().next()?).into()) + .unwrap_or_else(|| (1, Duration::from_secs(86400))); let (notify, expires) = if self.data.delivery_by == 0 { ( - queue::Schedule::later(future_release + *notify_intervals.first().unwrap()), - Instant::now() + future_release + *config.expire.eval(&envelope).await, + queue::Schedule::later(future_release + next_notify), + Instant::now() + + future_release + + self + .core + .eval_if(&config.expire, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 86400)), ) } else if (message.flags & MAIL_BY_RETURN) != 0 { ( - queue::Schedule::later(future_release + *notify_intervals.first().unwrap()), + queue::Schedule::later(future_release + next_notify), Instant::now() + Duration::from_secs(self.data.delivery_by as u64), ) } else { - let expire = *config.expire.eval(&envelope).await; + let expire = self + .core + .eval_if(&config.expire, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 86400)); let expire_secs = expire.as_secs(); let notify = if self.data.delivery_by.is_positive() { let notify_at = self.data.delivery_by as u64; if expire_secs > notify_at { Duration::from_secs(notify_at) } else { - *notify_intervals.first().unwrap() + next_notify } } else { let notify_at = -self.data.delivery_by as u64; if expire_secs > notify_at { Duration::from_secs(expire_secs - notify_at) } else { - *notify_intervals.first().unwrap() + next_notify } }; let mut notify = queue::Schedule::later(future_release + notify); - notify.inner = (notify_intervals.len() - 1) as u32; // Disable further notification attempts + notify.inner = (num_intervals - 1) as u32; // Disable further notification attempts (notify, Instant::now() + expire) }; @@ -721,7 +808,11 @@ impl Session { pub async fn can_send_data(&mut self) -> Result { if !self.data.rcpt_to.is_empty() { if self.data.messages_sent - < *self.core.session.config.data.max_messages.eval(self).await + < self + .core + .eval_if(&self.core.session.config.data.max_messages, self) + .await + .unwrap_or(10) { Ok(true) } else { diff --git a/crates/smtp/src/inbound/ehlo.rs b/crates/smtp/src/inbound/ehlo.rs index 11a81622..fa5ab299 100644 --- a/crates/smtp/src/inbound/ehlo.rs +++ b/crates/smtp/src/inbound/ehlo.rs @@ -21,9 +21,9 @@ * for more details. */ -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; -use crate::{core::Session, scripts::ScriptResult}; +use crate::{config::session::Mechanism, core::Session, scripts::ScriptResult}; use mail_auth::spf::verify::HasLabels; use smtp_proto::*; use utils::listener::SessionStream; @@ -80,7 +80,12 @@ impl Session { } // Sieve filtering - if let Some(script) = self.core.session.config.ehlo.script.eval(self).await { + if let Some(script) = self + .core + .eval_if::(&self.core.session.config.ehlo.script, self) + .await + .and_then(|name| self.core.get_sieve_script(&name)) + { if let ScriptResult::Reject(message) = self .run_script(script.clone(), self.build_script_parameters("ehlo")) .await @@ -121,38 +126,53 @@ impl Session { let dc = &self.core.session.config.data; // Pipelining - if *ec.pipelining.eval(self).await { + if self + .core + .eval_if(&ec.pipelining, self) + .await + .unwrap_or(true) + { response.capabilities |= EXT_PIPELINING; } // Chunking - if *ec.chunking.eval(self).await { + if self.core.eval_if(&ec.chunking, self).await.unwrap_or(true) { response.capabilities |= EXT_CHUNKING; } // Address Expansion - if *ec.expn.eval(self).await { + if self.core.eval_if(&ec.expn, self).await.unwrap_or(false) { response.capabilities |= EXT_EXPN; } // Recipient Verification - if *ec.vrfy.eval(self).await { + if self.core.eval_if(&ec.vrfy, self).await.unwrap_or(false) { response.capabilities |= EXT_VRFY; } // Require TLS - if *ec.requiretls.eval(self).await { + if self + .core + .eval_if(&ec.requiretls, self) + .await + .unwrap_or(true) + { response.capabilities |= EXT_REQUIRE_TLS; } // DSN - if *ec.dsn.eval(self).await { + if self.core.eval_if(&ec.dsn, self).await.unwrap_or(false) { response.capabilities |= EXT_DSN; } // Authentication if self.data.authenticated_as.is_empty() { - response.auth_mechanisms = *ac.mechanisms.eval(self).await; + response.auth_mechanisms = self + .core + .eval_if::(&ac.mechanisms, self) + .await + .unwrap_or_default() + .into(); if response.auth_mechanisms != 0 { if !self.stream.is_tls() && !self.params.auth_plain_text { response.auth_mechanisms &= !(AUTH_PLAIN | AUTH_LOGIN); @@ -164,7 +184,11 @@ impl Session { } // Future release - if let Some(value) = ec.future_release.eval(self).await { + if let Some(value) = self + .core + .eval_if::(&ec.future_release, self) + .await + { response.capabilities |= EXT_FUTURE_RELEASE; response.future_release_interval = value.as_secs(); response.future_release_datetime = SystemTime::now() @@ -175,25 +199,37 @@ impl Session { } // Deliver By - if let Some(value) = ec.deliver_by.eval(self).await { + if let Some(value) = self.core.eval_if::(&ec.deliver_by, self).await { response.capabilities |= EXT_DELIVER_BY; response.deliver_by = value.as_secs(); } // Priority - if let Some(value) = ec.mt_priority.eval(self).await { + if let Some(value) = self + .core + .eval_if::(&ec.mt_priority, self) + .await + { response.capabilities |= EXT_MT_PRIORITY; - response.mt_priority = *value; + response.mt_priority = value; } // Size - response.size = *dc.max_message_size.eval(self).await; + response.size = self + .core + .eval_if(&dc.max_message_size, self) + .await + .unwrap_or(25 * 1024 * 1024); if response.size > 0 { response.capabilities |= EXT_SIZE; } // No soliciting - if let Some(value) = ec.no_soliciting.eval(self).await { + if let Some(value) = self + .core + .eval_if::(&ec.no_soliciting, self) + .await + { response.capabilities |= EXT_NO_SOLICITING; response.no_soliciting = if !value.is_empty() { value.to_string().into() diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs index a3ee1ac5..5a144cd7 100644 --- a/crates/smtp/src/inbound/mail.rs +++ b/crates/smtp/src/inbound/mail.rs @@ -21,11 +21,11 @@ * for more details. */ -use std::time::SystemTime; +use std::time::{Duration, SystemTime}; use mail_auth::{IprevOutput, IprevResult, SpfOutput, SpfResult}; -use smtp_proto::{MailFrom, MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_REQUIRETLS}; -use utils::listener::SessionStream; +use smtp_proto::{MailFrom, MtPriority, MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_REQUIRETLS}; +use utils::{config::Rate, listener::SessionStream}; use crate::{ core::{Session, SessionAddress}, @@ -124,7 +124,12 @@ impl Session { .into(); // Sieve filtering - if let Some(script) = self.core.session.config.mail.script.eval(self).await { + if let Some(script) = self + .core + .eval_if::(&self.core.session.config.mail.script, self) + .await + .and_then(|name| self.core.get_sieve_script(&name)) + { match self .run_script(script.clone(), self.build_script_parameters("mail")) .await @@ -159,14 +164,8 @@ impl Session { // Address rewriting if let Some(new_address) = self .core - .session - .config - .mail - .rewrite - .eval_and_capture(self) + .eval_if::(&self.core.session.config.mail.rewrite, self) .await - .into_value(self) - .map(|s| s.into_owned()) { let mail_from = self.data.mail_from.as_mut().unwrap(); if new_address.contains('@') { @@ -183,14 +182,24 @@ impl Session { // Validate parameters let config = &self.core.session.config.extensions; let config_data = &self.core.session.config.data; - if (from.flags & MAIL_REQUIRETLS) != 0 && !*config.requiretls.eval(self).await { + if (from.flags & MAIL_REQUIRETLS) != 0 + && !self + .core + .eval_if(&config.requiretls, self) + .await + .unwrap_or(false) + { self.data.mail_from = None; return self .write(b"501 5.5.4 REQUIRETLS has been disabled.\r\n") .await; } if (from.flags & (MAIL_BY_NOTIFY | MAIL_BY_RETURN)) != 0 { - if let Some(duration) = config.deliver_by.eval(self).await { + if let Some(duration) = self + .core + .eval_if::(&config.deliver_by, self) + .await + { if from.by.checked_abs().unwrap_or(0) as u64 <= duration.as_secs() && (from.by.is_positive() || (from.flags & MAIL_BY_NOTIFY) != 0) { @@ -215,7 +224,12 @@ impl Session { } } if from.mt_priority != 0 { - if config.mt_priority.eval(self).await.is_some() { + if self + .core + .eval_if::(&config.mt_priority, self) + .await + .is_some() + { if (-6..6).contains(&from.mt_priority) { self.data.priority = from.mt_priority as i16; } else { @@ -229,14 +243,25 @@ impl Session { .await; } } - if from.size > 0 && from.size > *config_data.max_message_size.eval(self).await { + if from.size > 0 + && from.size + > self + .core + .eval_if(&config_data.max_message_size, self) + .await + .unwrap_or(25 * 1024 * 1024) + { self.data.mail_from = None; return self .write(b"552 5.3.4 Message too big for system.\r\n") .await; } if from.hold_for != 0 || from.hold_until != 0 { - if let Some(max_hold) = config.future_release.eval(self).await { + if let Some(max_hold) = self + .core + .eval_if::(&config.future_release, self) + .await + { let max_hold = max_hold.as_secs(); let hold_for = if from.hold_for != 0 { from.hold_for @@ -270,7 +295,7 @@ impl Session { .await; } } - if has_dsn && !*config.dsn.eval(self).await { + if has_dsn && !self.core.eval_if(&config.dsn, self).await.unwrap_or(false) { self.data.mail_from = None; return self .write(b"501 5.5.4 DSN extension has been disabled.\r\n") @@ -366,9 +391,11 @@ impl Session { // Send report if let (Some(recipient), Some(rate)) = ( spf_output.report_address(), - self.core.report.config.spf.send.eval(self).await, + self.core + .eval_if::(&self.core.report.config.spf.send, self) + .await, ) { - self.send_spf_report(recipient, rate, !result, spf_output) + self.send_spf_report(recipient, &rate, !result, spf_output) .await; } diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs index 3f9bc982..dda15017 100644 --- a/crates/smtp/src/inbound/milter/message.rs +++ b/crates/smtp/src/inbound/milter/message.rs @@ -55,7 +55,12 @@ impl Session { let mut modifications = Vec::new(); for milter in milters { - if !*milter.enable.eval(self).await { + if !self + .core + .eval_if(&milter.enable, self) + .await + .unwrap_or(false) + { continue; } diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index 87d809bf..08e95122 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -79,13 +79,11 @@ impl Session { // Address rewriting and Sieve filtering let rcpt_script = self .core - .session - .config - .rcpt - .script - .eval(self) + .eval_if::(&self.core.session.config.rcpt.script, self) .await - .clone(); + .and_then(|name| self.core.get_sieve_script(&name)) + .cloned(); + if rcpt_script.is_some() || !self.core.session.config.rcpt.rewrite.is_empty() { // Sieve filtering if let Some(script) = rcpt_script { @@ -125,14 +123,8 @@ impl Session { // Address rewriting if let Some(new_address) = self .core - .session - .config - .rcpt - .rewrite - .eval_and_capture(self) + .eval_if::(&self.core.session.config.rcpt.rewrite, self) .await - .into_value(self) - .map(|s| s.into_owned()) { let rcpt = self.data.rcpt_to.last_mut().unwrap(); if new_address.contains('@') { @@ -154,13 +146,9 @@ impl Session { let rcpt = self.data.rcpt_to.last().unwrap(); if let Some(directory) = self .core - .session - .config - .rcpt - .directory - .eval_and_capture(self) + .eval_if::(&self.core.session.config.rcpt.directory, self) .await - .into_value(self) + .and_then(|name| self.core.get_directory(&name)) { if let Ok(is_local_domain) = directory.is_local_domain(&rcpt.domain).await { if is_local_domain { @@ -189,7 +177,12 @@ impl Session { .write(b"451 4.4.3 Unable to verify address at this time.\r\n") .await; } - } else if !*self.core.session.config.rcpt.relay.eval(self).await { + } else if !self + .core + .eval_if(&self.core.session.config.rcpt.relay, self) + .await + .unwrap_or(false) + { tracing::debug!(parent: &self.span, context = "rcpt", event = "error", @@ -211,7 +204,12 @@ impl Session { .write(b"451 4.4.3 Unable to verify address at this time.\r\n") .await; } - } else if !*self.core.session.config.rcpt.relay.eval(self).await { + } else if !self + .core + .eval_if(&self.core.session.config.rcpt.relay, self) + .await + .unwrap_or(false) + { tracing::debug!(parent: &self.span, context = "rcpt", event = "error", diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index f4e2af93..b76d6252 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -21,8 +21,6 @@ * for more details. */ -use std::net::{IpAddr, Ipv4Addr}; - use smtp_proto::{ request::receiver::{ BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, @@ -31,14 +29,11 @@ use smtp_proto::{ *, }; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; -use utils::{ - config::{KeyLookup, ServerProtocol}, - listener::SessionStream, -}; +use utils::{config::ServerProtocol, listener::SessionStream}; use crate::{ - config::EnvelopeKey, - core::{Session, State}, + config::session::Mechanism, + core::{eval::*, ResolveVariable, Session, State}, }; use super::auth::SaslToken; @@ -98,8 +93,15 @@ impl Session { mechanism, initial_response, } => { - let auth = - *self.core.session.config.auth.mechanisms.eval(self).await; + let auth: u64 = self + .core + .eval_if::( + &self.core.session.config.auth.mechanisms, + self, + ) + .await + .unwrap_or_default() + .into(); if auth == 0 || self.params.auth_directory.is_none() { self.write(b"503 5.5.1 AUTH not allowed.\r\n").await?; } else if !self.data.authenticated_as.is_empty() { @@ -407,62 +409,44 @@ impl Session { } } -impl KeyLookup for Session { - type Key = EnvelopeKey; - - fn key(&self, key: &Self::Key) -> std::borrow::Cow<'_, str> { - match key { - EnvelopeKey::Recipient => self +impl ResolveVariable for Session { + fn resolve_variable(&self, variable: u32) -> utils::expr::Variable<'_> { + match variable { + V_RECIPIENT => self .data .rcpt_to .last() .map(|r| r.address_lcase.as_str()) .unwrap_or_default() .into(), - EnvelopeKey::RecipientDomain => self + V_RECIPIENT_DOMAIN => self .data .rcpt_to .last() .map(|r| r.domain.as_str()) .unwrap_or_default() .into(), - EnvelopeKey::Sender => self + V_SENDER => self .data .mail_from .as_ref() .map(|m| m.address_lcase.as_str()) .unwrap_or_default() .into(), - EnvelopeKey::SenderDomain => self + V_SENDER_DOMAIN => self .data .mail_from .as_ref() .map(|m| m.domain.as_str()) .unwrap_or_default() .into(), - EnvelopeKey::HeloDomain => self.data.helo_domain.as_str().into(), - EnvelopeKey::AuthenticatedAs => self.data.authenticated_as.as_str().into(), - EnvelopeKey::Listener => self.instance.id.as_str().into(), - EnvelopeKey::RemoteIp => self.data.remote_ip.to_string().into(), - EnvelopeKey::LocalIp => self.data.local_ip.to_string().into(), - EnvelopeKey::Priority => self.data.priority.to_string().into(), - EnvelopeKey::Mx => "".into(), - } - } - - fn key_as_int(&self, key: &Self::Key) -> i32 { - match key { - EnvelopeKey::Listener => self.instance.listener_id as i32, - EnvelopeKey::Priority => self.data.priority as i32, - _ => 0, - } - } - - fn key_as_ip(&self, key: &Self::Key) -> IpAddr { - match key { - EnvelopeKey::RemoteIp => self.data.remote_ip, - EnvelopeKey::LocalIp => self.data.local_ip, - _ => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), + V_HELO_DOMAIN => self.data.helo_domain.as_str().into(), + V_AUTHENTICATED_AS => self.data.authenticated_as.as_str().into(), + V_LISTENER => self.instance.id.as_str().into(), + V_REMOTE_IP => self.data.remote_ip_str.as_str().into(), + V_LOCAL_IP => self.data.local_ip_str.as_str().into(), + V_PRIORITY => self.data.priority.to_string().into(), + _ => utils::expr::Variable::default(), } } } diff --git a/crates/smtp/src/inbound/spawn.rs b/crates/smtp/src/inbound/spawn.rs index 7e564304..e968dd6d 100644 --- a/crates/smtp/src/inbound/spawn.rs +++ b/crates/smtp/src/inbound/spawn.rs @@ -83,7 +83,12 @@ impl Session { self.eval_session_params().await; // Sieve filtering - if let Some(script) = self.core.session.config.connect.script.eval(self).await { + if let Some(script) = self + .core + .eval_if::(&self.core.session.config.connect.script, self) + .await + .and_then(|name| self.core.get_sieve_script(&name)) + { if let ScriptResult::Reject(message) = self .run_script(script.clone(), self.build_script_parameters("connect")) .await diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs index e79294bf..8c8663bf 100644 --- a/crates/smtp/src/inbound/vrfy.rs +++ b/crates/smtp/src/inbound/vrfy.rs @@ -31,13 +31,9 @@ impl Session { pub async fn handle_vrfy(&mut self, address: String) -> Result<(), ()> { match self .core - .session - .config - .rcpt - .directory - .eval_and_capture(self) + .eval_if::(&self.core.session.config.rcpt.directory, self) .await - .into_value(self) + .and_then(|name| self.core.get_directory(&name)) { Some(address_lookup) if self.params.can_vrfy => { match address_lookup.vrfy(&address.to_lowercase()).await { @@ -92,13 +88,9 @@ impl Session { pub async fn handle_expn(&mut self, address: String) -> Result<(), ()> { match self .core - .session - .config - .rcpt - .directory - .eval_and_capture(self) + .eval_if::(&self.core.session.config.rcpt.directory, self) .await - .into_value(self) + .and_then(|name| self.core.get_directory(&name)) { Some(address_lookup) if self.params.can_expn => { match address_lookup.expn(&address.to_lowercase()).await { diff --git a/crates/smtp/src/lib.rs b/crates/smtp/src/lib.rs index e41d0ea4..922d8347 100644 --- a/crates/smtp/src/lib.rs +++ b/crates/smtp/src/lib.rs @@ -21,14 +21,17 @@ * for more details. */ -use crate::core::{ - throttle::ThrottleKeyHasherBuilder, QueueCore, ReportCore, SessionCore, TlsConnectors, SMTP, +use crate::{ + config::RelayHost, + core::{ + throttle::ThrottleKeyHasherBuilder, QueueCore, ReportCore, SessionCore, TlsConnectors, SMTP, + }, }; use std::sync::Arc; use config::{ - auth::ConfigAuth, queue::ConfigQueue, remote::ConfigHost, report::ConfigReport, - resolver::ConfigResolver, scripts::ConfigSieve, session::ConfigSession, ConfigContext, Host, + auth::ConfigAuth, queue::ConfigQueue, report::ConfigReport, resolver::ConfigResolver, + scripts::ConfigSieve, session::ConfigSession, shared::ConfigShared, ConfigContext, }; use dashmap::DashMap; use directory::Directories; @@ -66,36 +69,31 @@ impl SMTP { config_ctx.directory = directory.clone(); config_ctx.stores = stores.clone(); - // Parse remote hosts - 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()?; + let queue_config = config.parse_queue()?; + let mail_auth_config = config.parse_mail_auth()?; + let report_config = config.parse_reports()?; + let mut shared = config.parse_shared(&config_ctx)?; // Add local delivery host #[cfg(feature = "local_delivery")] { - config_ctx.hosts.insert( + shared.relay_hosts.insert( "local".to_string(), - Host { + RelayHost { address: String::new(), port: 0, protocol: ServerProtocol::Jmap, - concurrency: Default::default(), - timeout: Default::default(), tls_implicit: Default::default(), tls_allow_invalid_certs: Default::default(), - username: Default::default(), - secret: Default::default(), + auth: None, }, ); } - // 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)?; - let queue_config = config.parse_queue(&config_ctx)?; - let mail_auth_config = config.parse_mail_auth(&config_ctx)?; - let report_config = config.parse_reports(&config_ctx)?; - // Build core let (queue_tx, queue_rx) = mpsc::channel(1024); let (report_tx, report_rx) = mpsc::channel(1024); @@ -152,6 +150,7 @@ impl SMTP { }, mail_auth: mail_auth_config, sieve: sieve_config, + shared, #[cfg(feature = "local_delivery")] delivery_tx, }); diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index b19f77af..14da36b2 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -36,7 +36,7 @@ use smtp_proto::MAIL_REQUIRETLS; use utils::config::ServerProtocol; use crate::{ - config::{AggregateFrequency, TlsStrategy}, + config::{AggregateFrequency, RequireOptional, TlsStrategy}, core::SMTP, queue::ErrorDetails, reporting::{tls::TlsRptOptions, PolicyType, TlsEvent}, @@ -59,7 +59,7 @@ impl DeliveryAttempt { let has_pending_delivery = self.has_pending_delivery(); // Send any due Delivery Status Notifications - core.queue.send_dsn(&mut self).await; + core.send_dsn(&mut self).await; if has_pending_delivery { // Re-queue the message if its not yet due for delivery @@ -83,7 +83,6 @@ impl DeliveryAttempt { // Throttle sender for throttle in &core.queue.config.throttle.sender { if let Err(err) = core - .queue .is_allowed( throttle, self.message.as_ref(), @@ -150,7 +149,6 @@ impl DeliveryAttempt { let mut in_flight = Vec::new(); for throttle in &queue_config.throttle.rcpt { if let Err(err) = core - .queue .is_allowed(throttle, &envelope, &mut in_flight, &span) .await { @@ -160,7 +158,10 @@ impl DeliveryAttempt { } // Obtain next hop - let (mut remote_hosts, is_smtp) = match queue_config.next_hop.eval(&envelope).await + let (mut remote_hosts, is_smtp) = match core + .eval_if::(&queue_config.next_hop, &envelope) + .await + .and_then(|name| core.get_relay_host(&name)) { #[cfg(feature = "local_delivery")] Some(next_hop) if next_hop.protocol == ServerProtocol::Jmap => { @@ -175,8 +176,13 @@ impl DeliveryAttempt { .await; // Update status for the current domain and continue with the next one - domain - .set_status(delivery_result, queue_config.retry.eval(&envelope).await); + domain.set_status( + delivery_result, + &core + .eval_if::, _>(&queue_config.retry, &envelope) + .await + .unwrap_or_else(|| vec![Duration::from_secs(60)]), + ); continue 'next_domain; } Some(next_hop) => ( @@ -189,13 +195,23 @@ impl DeliveryAttempt { // Prepare TLS strategy let mut disable_tls = false; let mut tls_strategy = TlsStrategy { - mta_sts: *queue_config.tls.mta_sts.eval(&envelope).await, + mta_sts: core + .eval_if(&queue_config.tls.mta_sts, &envelope) + .await + .unwrap_or(RequireOptional::Optional), ..Default::default() }; - let allow_invalid_certs = *queue_config.tls.invalid_certs.eval(&envelope).await; + let allow_invalid_certs = core + .eval_if(&queue_config.tls.invalid_certs, &envelope) + .await + .unwrap_or(false); // Obtain TLS reporting - let tls_report = match core.report.config.tls.send.eval(&envelope).await { + let tls_report = match core + .eval_if(&core.report.config.tls.send, &envelope) + .await + .unwrap_or(AggregateFrequency::Never) + { interval @ (AggregateFrequency::Hourly | AggregateFrequency::Daily | AggregateFrequency::Weekly) @@ -213,11 +229,7 @@ impl DeliveryAttempt { event = "record-fetched", record = ?record); - TlsRptOptions { - record, - interval: *interval, - } - .into() + TlsRptOptions { record, interval }.into() } Err(err) => { tracing::debug!( @@ -238,7 +250,9 @@ impl DeliveryAttempt { match core .lookup_mta_sts_policy( envelope.domain, - *queue_config.timeout.mta_sts.eval(&envelope).await, + core.eval_if(&queue_config.timeout.mta_sts, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(10 * 60)), ) .await { @@ -294,7 +308,13 @@ impl DeliveryAttempt { "Failed to retrieve MTA-STS policy: {}", err ); - domain.set_status(err, queue_config.retry.eval(&envelope).await); + domain.set_status( + err, + &core + .eval_if::, _>(&queue_config.retry, &envelope) + .await + .unwrap_or_else(|| vec![Duration::from_secs(60)]), + ); continue 'next_domain; } else { tracing::debug!( @@ -326,14 +346,23 @@ impl DeliveryAttempt { event = "mx-lookup-failed", reason = %err, ); - domain.set_status(err, queue_config.retry.eval(&envelope).await); + domain.set_status( + err, + &core + .eval_if::, _>(&queue_config.retry, &envelope) + .await + .unwrap_or_else(|| vec![Duration::from_secs(60)]), + ); continue 'next_domain; } }; - if let Some(remote_hosts_) = mx_list - .to_remote_hosts(&domain.domain, *queue_config.max_mx.eval(&envelope).await) - { + if let Some(remote_hosts_) = mx_list.to_remote_hosts( + &domain.domain, + core.eval_if(&queue_config.max_mx, &envelope) + .await + .unwrap_or(5), + ) { remote_hosts = remote_hosts_; } else { tracing::info!( @@ -346,14 +375,20 @@ impl DeliveryAttempt { Status::PermanentFailure(Error::DnsError( "Domain does not accept messages (null MX)".to_string(), )), - queue_config.retry.eval(&envelope).await, + &core + .eval_if::, _>(&queue_config.retry, &envelope) + .await + .unwrap_or_else(|| vec![Duration::from_secs(60)]), ); continue 'next_domain; } } // Try delivering message - let max_multihomed = *queue_config.max_multihomed.eval(&envelope).await; + let max_multihomed = core + .eval_if(&queue_config.max_multihomed, &envelope) + .await + .unwrap_or(2); let mut last_status = Status::Scheduled; 'next_host: for remote_host in &remote_hosts { // Validate MTA-STS @@ -413,8 +448,14 @@ impl DeliveryAttempt { }; // Update TLS strategy - tls_strategy.dane = *queue_config.tls.dane.eval(&envelope).await; - tls_strategy.tls = *queue_config.tls.start.eval(&envelope).await; + tls_strategy.dane = core + .eval_if(&queue_config.tls.dane, &envelope) + .await + .unwrap_or(RequireOptional::Optional); + tls_strategy.tls = core + .eval_if(&queue_config.tls.start, &envelope) + .await + .unwrap_or(RequireOptional::Optional); // Lookup DANE policy let dane_policy = if tls_strategy.try_dane() && is_smtp { @@ -571,7 +612,6 @@ impl DeliveryAttempt { envelope.remote_ip = remote_ip; for throttle in &queue_config.throttle.host { if let Err(err) = core - .queue .is_allowed(throttle, &envelope, &mut in_flight_host, &span) .await { @@ -581,17 +621,21 @@ impl DeliveryAttempt { } // Connect + let conn_timeout = core + .eval_if(&queue_config.timeout.connect, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 60)); let mut smtp_client = match if let Some(ip_addr) = source_ip { SmtpClient::connect_using( ip_addr, SocketAddr::new(remote_ip, remote_host.port()), - *queue_config.timeout.connect.eval(&envelope).await, + conn_timeout, ) .await } else { SmtpClient::connect( SocketAddr::new(remote_ip, remote_host.port()), - *queue_config.timeout.connect.eval(&envelope).await, + conn_timeout, ) .await } { @@ -621,17 +665,33 @@ impl DeliveryAttempt { } }; - // Obtail session parameters + // Obtain session parameters + let local_hostname = core + .eval_if::(&queue_config.hostname, &envelope) + .await + .unwrap_or_else(|| "localhost".to_string()); let params = SessionParams { span: &span, credentials: remote_host.credentials(), is_smtp: remote_host.is_smtp(), hostname: envelope.mx, - local_hostname: queue_config.hostname.eval(&envelope).await, - timeout_ehlo: *queue_config.timeout.ehlo.eval(&envelope).await, - timeout_mail: *queue_config.timeout.mail.eval(&envelope).await, - timeout_rcpt: *queue_config.timeout.rcpt.eval(&envelope).await, - timeout_data: *queue_config.timeout.data.eval(&envelope).await, + local_hostname: &local_hostname, + timeout_ehlo: core + .eval_if(&queue_config.timeout.ehlo, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 60)), + timeout_mail: core + .eval_if(&queue_config.timeout.mail, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 60)), + timeout_rcpt: core + .eval_if(&queue_config.timeout.rcpt, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 60)), + timeout_data: core + .eval_if(&queue_config.timeout.data, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 60)), }; // Prepare TLS connector @@ -648,8 +708,10 @@ impl DeliveryAttempt { let delivery_result = if !remote_host.implicit_tls() { // Read greeting - smtp_client.timeout = - *queue_config.timeout.greeting.eval(&envelope).await; + smtp_client.timeout = core + .eval_if(&queue_config.timeout.greeting, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 60)); if let Err(status) = read_greeting(&mut smtp_client, envelope.mx).await { tracing::info!( @@ -683,8 +745,10 @@ impl DeliveryAttempt { // Try starting TLS if tls_strategy.try_start_tls() && !domain.disable_tls { - smtp_client.timeout = - *queue_config.timeout.tls.eval(&envelope).await; + smtp_client.timeout = core + .eval_if(&queue_config.timeout.tls, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(3 * 60)); match try_start_tls( smtp_client, tls_connector, @@ -873,7 +937,10 @@ impl DeliveryAttempt { } } else { // Start TLS - smtp_client.timeout = *queue_config.timeout.tls.eval(&envelope).await; + smtp_client.timeout = core + .eval_if(&queue_config.timeout.tls, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(3 * 60)); let mut smtp_client = match smtp_client.into_tls(tls_connector, envelope.mx).await { Ok(smtp_client) => smtp_client, @@ -892,8 +959,10 @@ impl DeliveryAttempt { }; // Read greeting - smtp_client.timeout = - *queue_config.timeout.greeting.eval(&envelope).await; + smtp_client.timeout = core + .eval_if(&queue_config.timeout.greeting, &envelope) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 60)); if let Err(status) = read_greeting(&mut smtp_client, envelope.mx).await { tracing::info!( @@ -919,21 +988,32 @@ impl DeliveryAttempt { }; // Update status for the current domain and continue with the next one - domain - .set_status(delivery_result, queue_config.retry.eval(&envelope).await); + domain.set_status( + delivery_result, + &core + .eval_if::, _>(&queue_config.retry, &envelope) + .await + .unwrap_or_else(|| vec![Duration::from_secs(60)]), + ); continue 'next_domain; } } // Update status domain.disable_tls = disable_tls; - domain.set_status(last_status, queue_config.retry.eval(&envelope).await); + domain.set_status( + last_status, + &core + .eval_if::, _>(&queue_config.retry, &envelope) + .await + .unwrap_or_else(|| vec![Duration::from_secs(60)]), + ); } self.message.domains = domains; self.message.recipients = recipients; // Send Delivery Status Notifications - core.queue.send_dsn(&mut self).await; + core.send_dsn(&mut self).await; // Notify queue manager let span = self.span; diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index 7cefec5a..06842581 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -21,15 +21,16 @@ * for more details. */ -use std::{net::IpAddr, sync::Arc}; +use std::{ + net::{IpAddr, Ipv4Addr, Ipv6Addr}, + sync::Arc, +}; use mail_auth::{IpLookupStrategy, MX}; use rand::{seq::SliceRandom, Rng}; -use utils::config::KeyLookup; use crate::{ - config::EnvelopeKey, - core::SMTP, + core::{eval::V_MX, ResolveVariable, SMTP}, queue::{Error, ErrorDetails, Status}, }; @@ -100,13 +101,15 @@ impl SMTP { pub async fn resolve_host( &self, remote_host: &NextHop<'_>, - envelope: &impl KeyLookup, + envelope: &impl ResolveVariable, max_multihomed: usize, ) -> Result> { let remote_ips = self .ip_lookup( remote_host.fqdn_hostname().as_ref(), - *self.queue.config.ip_strategy.eval(envelope).await, + self.eval_if(&self.queue.config.ip_strategy, envelope) + .await + .unwrap_or(IpLookupStrategy::Ipv4thenIpv6), max_multihomed, ) .await @@ -132,7 +135,10 @@ impl SMTP { }; // Obtain source IPv4 address - let source_ips = self.queue.config.source_ip.ipv4.eval(envelope).await; + let source_ips = self + .eval_if::, _>(&self.queue.config.source_ip.ipv4, envelope) + .await + .unwrap_or_default(); match source_ips.len().cmp(&1) { std::cmp::Ordering::Equal => { result.source_ipv4 = IpAddr::from(*source_ips.first().unwrap()).into(); @@ -146,7 +152,10 @@ impl SMTP { } // Obtain source IPv6 address - let source_ips = self.queue.config.source_ip.ipv6.eval(envelope).await; + let source_ips = self + .eval_if::, _>(&self.queue.config.source_ip.ipv6, envelope) + .await + .unwrap_or_default(); match source_ips.len().cmp(&1) { std::cmp::Ordering::Equal => { result.source_ipv6 = IpAddr::from(*source_ips.first().unwrap()).into(); @@ -163,7 +172,7 @@ impl SMTP { } else { Err(Status::TemporaryFailure(Error::DnsError(format!( "No IP addresses found for {:?}.", - envelope.key(&EnvelopeKey::Mx) + envelope.resolve_variable(V_MX).to_string() )))) } } diff --git a/crates/smtp/src/queue/dsn.rs b/crates/smtp/src/queue/dsn.rs index 3a58e2de..4027edb3 100644 --- a/crates/smtp/src/queue/dsn.rs +++ b/crates/smtp/src/queue/dsn.rs @@ -34,34 +34,38 @@ use std::time::{Duration, Instant}; use tokio::fs::File; use tokio::io::AsyncReadExt; -use crate::config::QueueConfig; -use crate::core::QueueCore; +use crate::core::SMTP; use super::{ instant_to_timestamp, DeliveryAttempt, Domain, Error, ErrorDetails, HostResponse, Message, Recipient, SimpleEnvelope, Status, RCPT_DSN_SENT, RCPT_STATUS_CHANGED, }; -impl QueueCore { +impl SMTP { pub async fn send_dsn(&self, attempt: &mut DeliveryAttempt) { if !attempt.message.return_path.is_empty() { - if let Some(dsn) = attempt.build_dsn(&self.config).await { + if let Some(dsn) = attempt.build_dsn(self).await { let mut dsn_message = Message::new_boxed("", "", ""); dsn_message .add_recipient_parts( &attempt.message.return_path, &attempt.message.return_path_lcase, &attempt.message.return_path_domain, - &self.config, + self, ) .await; // Sign message - let signature = attempt - .message - .sign(&self.config.dsn.sign, &dsn, &attempt.span) + let signature = self + .sign_message( + &mut attempt.message, + &self.queue.config.dsn.sign, + &dsn, + &attempt.span, + ) .await; - self.queue_message(dsn_message, signature.as_deref(), &dsn, &attempt.span) + self.queue + .queue_message(dsn_message, signature.as_deref(), &dsn, &attempt.span) .await; } } else { @@ -71,7 +75,8 @@ impl QueueCore { } impl DeliveryAttempt { - pub async fn build_dsn(&mut self, config: &QueueConfig) -> Option> { + pub async fn build_dsn(&mut self, core: &SMTP) -> Option> { + let config = &core.queue.config; let now = Instant::now(); let mut txt_success = String::new(); @@ -232,14 +237,15 @@ impl DeliveryAttempt { { let envelope = SimpleEnvelope::new(&self.message, &domain.domain); - if let Some(next_notify) = config - .notify - .eval(&envelope) + if let Some(next_notify) = core + .eval_if::, _>(&config.notify, &envelope) .await - .get((domain.notify.inner + 1) as usize) + .and_then(|notify| { + notify.into_iter().nth((domain.notify.inner + 1) as usize) + }) { domain.notify.inner += 1; - domain.notify.due = Instant::now() + *next_notify; + domain.notify.due = Instant::now() + next_notify; } else { domain.notify.due = domain.expires + Duration::from_secs(10); } @@ -250,14 +256,23 @@ impl DeliveryAttempt { } // Obtain hostname and sender addresses - let from_name = config.dsn.name.eval(self.message.as_ref()).await; - let from_addr = config.dsn.address.eval(self.message.as_ref()).await; - let reporting_mta = config.hostname.eval(self.message.as_ref()).await; + let from_name = core + .eval_if(&config.dsn.name, self.message.as_ref()) + .await + .unwrap_or_else(|| String::from("Mail Delivery Subsystem")); + let from_addr = core + .eval_if(&config.dsn.address, self.message.as_ref()) + .await + .unwrap_or_else(|| String::from("MAILER-DAEMON@localhost")); + let reporting_mta = core + .eval_if(&config.hostname, self.message.as_ref()) + .await + .unwrap_or_else(|| String::from("localhost")); // Prepare DSN let mut dsn_header = String::with_capacity(dsn.len() + 128); self.message - .write_dsn_headers(&mut dsn_header, reporting_mta); + .write_dsn_headers(&mut dsn_header, &reporting_mta); let dsn = dsn_header + &dsn; // Fetch up to 1024 bytes of message headers diff --git a/crates/smtp/src/queue/manager.rs b/crates/smtp/src/queue/manager.rs index f639c9dd..d44eaa35 100644 --- a/crates/smtp/src/queue/manager.rs +++ b/crates/smtp/src/queue/manager.rs @@ -436,72 +436,69 @@ impl QueueCore { let mut queue = Queue::default(); let mut messages = Vec::new(); - for path in self - .config - .path - .if_then - .iter() - .map(|t| &t.then) - .chain([&self.config.path.default]) - { - let mut dir = match tokio::fs::read_dir(path).await { - Ok(dir) => dir, - Err(_) => continue, - }; - loop { - match dir.next_entry().await { - Ok(Some(file)) => { - let file = file.path(); - if file.is_dir() { - match tokio::fs::read_dir(&file).await { - Ok(mut dir) => { - let file_ = file; - loop { - match dir.next_entry().await { - Ok(Some(file)) => { - let file = file.path(); - if file.extension().map_or(false, |e| e == "msg") { - messages.push(tokio::spawn( - Message::from_path(file), - )); - } - } - Ok(None) => break, - Err(err) => { - tracing::warn!( - "Failed to read queue directory {}: {}", - file_.display(), - err - ); - break; + let mut dir = match tokio::fs::read_dir(&self.config.path).await { + Ok(dir) => dir, + Err(err) => { + tracing::warn!( + "Failed to read queue directory {}: {}", + self.config.path.display(), + err + ); + return queue; + } + }; + loop { + match dir.next_entry().await { + Ok(Some(file)) => { + let file = file.path(); + if file.is_dir() { + match tokio::fs::read_dir(&file).await { + Ok(mut dir) => { + let file_ = file; + loop { + match dir.next_entry().await { + Ok(Some(file)) => { + let file = file.path(); + if file.extension().map_or(false, |e| e == "msg") { + messages + .push(tokio::spawn(Message::from_path(file))); } } + Ok(None) => break, + Err(err) => { + tracing::warn!( + "Failed to read queue directory {}: {}", + file_.display(), + err + ); + break; + } } } - Err(err) => { - tracing::warn!( - "Failed to read queue directory {}: {}", - file.display(), - err - ) - } - }; - } else if file.extension().map_or(false, |e| e == "msg") { - messages.push(tokio::spawn(Message::from_path(file))); - } - } - Ok(None) => { - break; - } - Err(err) => { - tracing::warn!( - "Failed to read queue directory {}: {}", - path.display(), - err - ); - break; + } + Err(err) => { + tracing::warn!( + "Failed to read queue directory {}: {}", + file.display(), + err + ) + } + }; + } else if file.extension().map_or(false, |e| e == "msg") { + messages.push(tokio::spawn(Message::from_path(file))); } } + Ok(None) => { + break; + } + Err(err) => { + tracing::warn!( + "Failed to read queue directory {}: {}", + self.config.path.display(), + err + ); + break; + } } } @@ -510,7 +507,8 @@ impl QueueCore { match message.await { Ok(Ok(mut message)) => { // Reserve quota - self.has_quota(&mut message).await; + let todo = true; + //self.has_quota(&mut message).await; // Schedule message queue.schedule(Schedule { diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs index 32e4135b..230e1892 100644 --- a/crates/smtp/src/queue/mod.rs +++ b/crates/smtp/src/queue/mod.rs @@ -23,7 +23,7 @@ use std::{ fmt::Display, - net::{IpAddr, Ipv4Addr}, + net::IpAddr, path::PathBuf, sync::{atomic::AtomicUsize, Arc}, time::{Duration, Instant, SystemTime}, @@ -31,12 +31,9 @@ use std::{ use serde::{Deserialize, Serialize}; use smtp_proto::Response; -use utils::{ - config::KeyLookup, - listener::limiter::{ConcurrencyLimiter, InFlight}, -}; +use utils::listener::limiter::{ConcurrencyLimiter, InFlight}; -use crate::{config::EnvelopeKey, core::management}; +use crate::core::{eval::*, management, ResolveVariable}; pub mod dsn; pub mod manager; @@ -245,31 +242,17 @@ impl<'x> SimpleEnvelope<'x> { } } -impl<'x> KeyLookup for SimpleEnvelope<'x> { - type Key = EnvelopeKey; - - fn key(&self, key: &Self::Key) -> std::borrow::Cow<'_, str> { - match key { - EnvelopeKey::Sender => self.message.return_path_lcase.as_str().into(), - EnvelopeKey::SenderDomain => self.message.return_path_domain.as_str().into(), - EnvelopeKey::Priority => self.message.priority.to_string().into(), - EnvelopeKey::Recipient => self.recipient.into(), - EnvelopeKey::RecipientDomain => self.domain.into(), +impl<'x> ResolveVariable for SimpleEnvelope<'x> { + fn resolve_variable(&self, variable: u32) -> utils::expr::Variable<'_> { + match variable { + V_SENDER => self.message.return_path_lcase.as_str().into(), + V_SENDER_DOMAIN => self.message.return_path_domain.as_str().into(), + V_PRIORITY => self.message.priority.to_string().into(), + V_RECIPIENT => self.recipient.into(), + V_RECIPIENT_DOMAIN => self.domain.into(), _ => "".into(), } } - - fn key_as_int(&self, key: &Self::Key) -> i32 { - if matches!(key, EnvelopeKey::Priority) { - self.message.priority as i32 - } else { - 0 - } - } - - fn key_as_ip(&self, _: &Self::Key) -> IpAddr { - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) - } } pub struct QueueEnvelope<'x> { @@ -280,60 +263,30 @@ pub struct QueueEnvelope<'x> { pub local_ip: IpAddr, } -impl<'x> KeyLookup for QueueEnvelope<'x> { - type Key = EnvelopeKey; - - fn key(&self, key: &Self::Key) -> std::borrow::Cow<'_, str> { - match key { - EnvelopeKey::Sender => self.message.return_path_lcase.as_str().into(), - EnvelopeKey::SenderDomain => self.message.return_path_domain.as_str().into(), - EnvelopeKey::RecipientDomain => self.domain.into(), - EnvelopeKey::Mx => self.mx.into(), - EnvelopeKey::Priority => self.message.priority.to_string().into(), +impl<'x> ResolveVariable for QueueEnvelope<'x> { + fn resolve_variable(&self, variable: u32) -> utils::expr::Variable<'x> { + match variable { + V_SENDER => self.message.return_path_lcase.as_str().into(), + V_SENDER_DOMAIN => self.message.return_path_domain.as_str().into(), + V_RECIPIENT_DOMAIN => self.domain.into(), + V_MX => self.mx.into(), + V_PRIORITY => self.message.priority.into(), + V_REMOTE_IP => self.remote_ip.to_string().into(), + V_LOCAL_IP => self.local_ip.to_string().into(), _ => "".into(), } } - - fn key_as_int(&self, key: &Self::Key) -> i32 { - if matches!(key, EnvelopeKey::Priority) { - self.message.priority as i32 - } else { - 0 - } - } - - fn key_as_ip(&self, key: &Self::Key) -> IpAddr { - match key { - EnvelopeKey::RemoteIp => self.remote_ip, - EnvelopeKey::LocalIp => self.local_ip, - _ => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), - } - } } -impl KeyLookup for Message { - type Key = EnvelopeKey; - - fn key(&self, key: &Self::Key) -> std::borrow::Cow<'_, str> { - match key { - EnvelopeKey::Sender => self.return_path_lcase.as_str().into(), - EnvelopeKey::SenderDomain => self.return_path_domain.as_str().into(), - EnvelopeKey::Priority => self.priority.to_string().into(), +impl ResolveVariable for Message { + fn resolve_variable(&self, variable: u32) -> utils::expr::Variable<'_> { + match variable { + V_SENDER => self.return_path_lcase.as_str().into(), + V_SENDER_DOMAIN => self.return_path_domain.as_str().into(), + V_PRIORITY => self.priority.into(), _ => "".into(), } } - - fn key_as_int(&self, key: &Self::Key) -> i32 { - if matches!(key, EnvelopeKey::Priority) { - self.priority as i32 - } else { - 0 - } - } - - fn key_as_ip(&self, _: &Self::Key) -> IpAddr { - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) - } } pub struct RecipientDomain<'x>(&'x str); @@ -344,23 +297,13 @@ impl<'x> RecipientDomain<'x> { } } -impl<'x> KeyLookup for RecipientDomain<'x> { - type Key = EnvelopeKey; - - fn key(&self, key: &Self::Key) -> std::borrow::Cow<'_, str> { - match key { - EnvelopeKey::RecipientDomain => self.0.into(), +impl<'x> ResolveVariable for RecipientDomain<'x> { + fn resolve_variable(&self, variable: u32) -> utils::expr::Variable<'_> { + match variable { + V_RECIPIENT_DOMAIN => self.0.into(), _ => "".into(), } } - - fn key_as_int(&self, _: &Self::Key) -> i32 { - 0 - } - - fn key_as_ip(&self, _: &Self::Key) -> IpAddr { - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) - } } #[inline(always)] diff --git a/crates/smtp/src/queue/quota.rs b/crates/smtp/src/queue/quota.rs index f39ec097..8b6b6412 100644 --- a/crates/smtp/src/queue/quota.rs +++ b/crates/smtp/src/queue/quota.rs @@ -24,21 +24,20 @@ use std::sync::{atomic::Ordering, Arc}; use dashmap::mapref::entry::Entry; -use utils::config::KeyLookup; use crate::{ - config::{EnvelopeKey, QueueQuota}, - core::QueueCore, + config::QueueQuota, + core::{ResolveVariable, SMTP}, }; use super::{Message, QuotaLimiter, SimpleEnvelope, Status, UsedQuota}; -impl QueueCore { +impl SMTP { pub async fn has_quota(&self, message: &mut Message) -> bool { let mut queue_refs = Vec::new(); - if !self.config.quota.sender.is_empty() { - for quota in &self.config.quota.sender { + if !self.queue.config.quota.sender.is_empty() { + for quota in &self.queue.config.quota.sender { if !self .reserve_quota(quota, message, message.size, 0, &mut queue_refs) .await @@ -48,7 +47,7 @@ impl QueueCore { } } - for quota in &self.config.quota.rcpt_domain { + for quota in &self.queue.config.quota.rcpt_domain { for (pos, domain) in message.domains.iter().enumerate() { if !self .reserve_quota( @@ -65,7 +64,7 @@ impl QueueCore { } } - for quota in &self.config.quota.rcpt { + for quota in &self.queue.config.quota.rcpt { for (pos, rcpt) in message.recipients.iter().enumerate() { if !self .reserve_quota( @@ -94,13 +93,18 @@ impl QueueCore { async fn reserve_quota( &self, quota: &QueueQuota, - envelope: &impl KeyLookup, + envelope: &impl ResolveVariable, size: usize, id: u64, refs: &mut Vec, ) -> bool { - if !quota.conditions.conditions.is_empty() && quota.conditions.eval(envelope).await { - match self.quota.entry(quota.new_key(envelope)) { + if !quota.expr.is_empty() + && self + .eval_expr("a.expr, envelope, "reserve_quota") + .await + .unwrap_or(false) + { + match self.queue.quota.entry(quota.new_key(envelope)) { Entry::Occupied(e) => { if let Some(qref) = e.get().is_allowed(id, size) { refs.push(qref); diff --git a/crates/smtp/src/queue/spool.rs b/crates/smtp/src/queue/spool.rs index 69a5a173..fde2171c 100644 --- a/crates/smtp/src/queue/spool.rs +++ b/crates/smtp/src/queue/spool.rs @@ -31,8 +31,7 @@ use std::time::{Duration, SystemTime}; use tokio::fs::OpenOptions; use tokio::{fs, io::AsyncWriteExt}; -use crate::config::QueueConfig; -use crate::core::QueueCore; +use crate::core::{QueueCore, SMTP}; use super::{Domain, Event, Message, Recipient, Schedule, SimpleEnvelope, Status}; @@ -53,8 +52,9 @@ impl QueueCore { } // Build path - message.path = self.config.path.eval(message.as_ref()).await.clone(); - let hash = *self.config.hash.eval(message.as_ref()).await; + let todo = 1; + message.path = self.config.path.clone(); + let hash = 1; if hash > 0 { message.path.push((message.id % hash).to_string()); } @@ -197,7 +197,7 @@ impl Message { rcpt: impl Into, rcpt_lcase: impl Into, rcpt_domain: impl Into, - config: &QueueConfig, + core: &SMTP, ) { let rcpt_domain = rcpt_domain.into(); let domain_idx = @@ -205,10 +205,13 @@ impl Message { idx } else { let idx = self.domains.len(); - let expires = *config - .expire - .eval(&SimpleEnvelope::new(self, &rcpt_domain)) - .await; + let expires = core + .eval_if( + &core.queue.config.expire, + &SimpleEnvelope::new(self, &rcpt_domain), + ) + .await + .unwrap_or_else(|| Duration::from_secs(5 * 86400)); self.domains.push(Domain { domain: rcpt_domain, retry: Schedule::now(), @@ -230,11 +233,11 @@ impl Message { }); } - pub async fn add_recipient(&mut self, rcpt: impl Into, config: &QueueConfig) { + pub async fn add_recipient(&mut self, rcpt: impl Into, core: &SMTP) { let rcpt = rcpt.into(); let rcpt_lcase = rcpt.to_lowercase(); let rcpt_domain = rcpt_lcase.domain_part().to_string(); - self.add_recipient_parts(rcpt, rcpt_lcase, rcpt_domain, config) + self.add_recipient_parts(rcpt, rcpt_lcase, rcpt_domain, core) .await; } diff --git a/crates/smtp/src/queue/throttle.rs b/crates/smtp/src/queue/throttle.rs index a0915260..5f624719 100644 --- a/crates/smtp/src/queue/throttle.rs +++ b/crates/smtp/src/queue/throttle.rs @@ -24,14 +24,11 @@ use std::time::{Duration, Instant}; use dashmap::mapref::entry::Entry; -use utils::{ - config::KeyLookup, - listener::limiter::{ConcurrencyLimiter, InFlight, RateLimiter}, -}; +use utils::listener::limiter::{ConcurrencyLimiter, InFlight, RateLimiter}; use crate::{ - config::{EnvelopeKey, Throttle}, - core::{throttle::Limiter, QueueCore}, + config::Throttle, + core::{throttle::Limiter, ResolveVariable, SMTP}, }; use super::{Domain, Status}; @@ -42,16 +39,21 @@ pub enum Error { Rate { retry_at: Instant }, } -impl QueueCore { +impl SMTP { pub async fn is_allowed( &self, throttle: &Throttle, - envelope: &impl KeyLookup, + envelope: &impl ResolveVariable, in_flight: &mut Vec, span: &tracing::Span, ) -> Result<(), Error> { - if throttle.conditions.conditions.is_empty() || throttle.conditions.eval(envelope).await { - match self.throttle.entry(throttle.new_key(envelope)) { + if throttle.expr.is_empty() + || self + .eval_expr(&throttle.expr, envelope, "throttle") + .await + .unwrap_or(false) + { + match self.queue.throttle.entry(throttle.new_key(envelope)) { Entry::Occupied(mut e) => { let limiter = e.get_mut(); if let Some(limiter) = &limiter.concurrency { diff --git a/crates/smtp/src/reporting/dkim.rs b/crates/smtp/src/reporting/dkim.rs index 8329c5da..8415eff8 100644 --- a/crates/smtp/src/reporting/dkim.rs +++ b/crates/smtp/src/reporting/dkim.rs @@ -58,7 +58,11 @@ impl Session { } let config = &self.core.report.config.dkim; - let from_addr = config.address.eval(self).await; + let from_addr = self + .core + .eval_if(&config.address, self) + .await + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); let mut report = Vec::with_capacity(128); self.new_auth_failure(output.result().into(), rejected) .with_authentication_results( @@ -71,9 +75,20 @@ impl Session { .with_dkim_identity(signature.identity()) .with_headers(message.raw_headers()) .write_rfc5322( - (config.name.eval(self).await.as_str(), from_addr.as_str()), + ( + self.core + .eval_if(&config.name, self) + .await + .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) + .as_str(), + from_addr.as_str(), + ), rcpt, - config.subject.eval(self).await, + &self + .core + .eval_if(&config.subject, self) + .await + .unwrap_or_else(|| "DKIM Report".to_string()), &mut report, ) .ok(); @@ -90,7 +105,7 @@ impl Session { // Send report self.core .send_report( - from_addr, + &from_addr, [rcpt].into_iter(), report, &config.sign, diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs index 4024c565..2df9b32c 100644 --- a/crates/smtp/src/reporting/dmarc.rs +++ b/crates/smtp/src/reporting/dmarc.rs @@ -36,6 +36,7 @@ use tokio::{ io::{AsyncRead, AsyncWrite}, runtime::Handle, }; +use utils::config::Rate; use crate::{ config::AggregateFrequency, @@ -73,9 +74,10 @@ impl Session { let config = &self.core.report.config.dmarc; // Send failure report - if let (Some(failure_rate), Some(report_options)) = - (config.send.eval(self).await, dmarc_output.failure_report()) - { + if let (Some(failure_rate), Some(report_options)) = ( + self.core.eval_if::(&config.send, self).await, + dmarc_output.failure_report(), + ) { // Verify that any external reporting addresses are authorized let rcpts = match self .core @@ -89,7 +91,7 @@ impl Session { rcpts .into_iter() .filter_map(|rcpt| { - if self.throttle_rcpt(rcpt.uri(), failure_rate, "dmarc") { + if self.throttle_rcpt(rcpt.uri(), &failure_rate, "dmarc") { rcpt.uri().into() } else { None @@ -126,7 +128,11 @@ impl Session { // Throttle recipient if !rcpts.is_empty() { let mut report = Vec::with_capacity(128); - let from_addr = config.address.eval(self).await; + let from_addr = self + .core + .eval_if(&config.address, self) + .await + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); let mut auth_failure = self .new_auth_failure(AuthFailureType::Dmarc, rejected) .with_authentication_results(auth_results.to_string()) @@ -205,9 +211,20 @@ impl Session { IdentityAlignment::Spf }) .write_rfc5322( - (config.name.eval(self).await.as_str(), from_addr.as_str()), + ( + self.core + .eval_if(&config.name, self) + .await + .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) + .as_str(), + from_addr.as_str(), + ), &rcpts.join(", "), - config.subject.eval(self).await, + &self + .core + .eval_if(&config.subject, self) + .await + .unwrap_or_else(|| "DMARC Report".to_string()), &mut report, ) .ok(); @@ -224,7 +241,7 @@ impl Session { // Send report self.core .send_report( - from_addr, + &from_addr, rcpts.into_iter(), report, &config.sign, @@ -246,12 +263,9 @@ impl Session { // Send agregate reports let interval = self .core - .report - .config - .dmarc_aggregate - .send - .eval(self) - .await; + .eval_if(&self.core.report.config.dmarc_aggregate.send, self) + .await + .unwrap_or(AggregateFrequency::Never); if matches!(interval, AggregateFrequency::Never) || dmarc_record.rua().is_empty() { return; @@ -286,7 +300,7 @@ impl Session { domain: dmarc_output.into_domain(), report_record, dmarc_record, - interval: *interval, + interval, }) .await; } @@ -377,59 +391,60 @@ impl GenerateDmarcReport for Arc { .with_date_range_end(deliver_at) .with_report_id(format!("{}_{}", domain.policy, path.created)) .with_email( - handle.block_on( - config - .address - .eval(&RecipientDomain::new(domain.inner.as_str())), - ), + handle + .block_on(core.eval_if( + &config.address, + &RecipientDomain::new(domain.inner.as_str()), + )) + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()), ); - if let Some(org_name) = handle.block_on( - config - .org_name - .eval(&RecipientDomain::new(domain.inner.as_str())), - ) { + if let Some(org_name) = handle.block_on(core.eval_if::( + &config.org_name, + &RecipientDomain::new(domain.inner.as_str()), + )) { report = report.with_org_name(org_name); } - if let Some(contact_info) = handle.block_on( - config - .contact_info - .eval(&RecipientDomain::new(domain.inner.as_str())), - ) { + if let Some(contact_info) = handle.block_on(core.eval_if::( + &config.contact_info, + &RecipientDomain::new(domain.inner.as_str()), + )) { report = report.with_extra_contact_info(contact_info); } for (record, count) in record_map { report.add_record(record.with_count(count)); } - let from_addr = handle.block_on( - config - .address - .eval(&RecipientDomain::new(domain.inner.as_str())), - ); + let from_addr = handle + .block_on(core.eval_if( + &config.address, + &RecipientDomain::new(domain.inner.as_str()), + )) + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); let mut message = Vec::with_capacity(path.size); - let _ = report.write_rfc5322( - handle.block_on( - core.report - .config - .submitter - .eval(&RecipientDomain::new(domain.inner.as_str())), - ), - ( - handle - .block_on( - config - .name - .eval(&RecipientDomain::new(domain.inner.as_str())), - ) - .as_str(), - from_addr.as_str(), - ), - rua.iter().map(|a| a.as_str()), - &mut message, - ); + let _ = + report.write_rfc5322( + &handle + .block_on(core.eval_if( + &core.report.config.submitter, + &RecipientDomain::new(domain.inner.as_str()), + )) + .unwrap_or_else(|| "localhost".to_string()), + ( + handle + .block_on(core.eval_if( + &config.name, + &RecipientDomain::new(domain.inner.as_str()), + )) + .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) + .as_str(), + from_addr.as_str(), + ), + rua.iter().map(|a| a.as_str()), + &mut message, + ); // Send report handle.block_on(core.send_report( - from_addr, + &from_addr, rua.iter(), message, &config.sign, @@ -453,12 +468,12 @@ impl GenerateDmarcReport for Arc { impl Scheduler { pub async fn schedule_dmarc(&mut self, event: Box, core: &SMTP) { let max_size = core - .report - .config - .dmarc_aggregate - .max_size - .eval(&RecipientDomain::new(event.domain.as_str())) - .await; + .eval_if( + &core.report.config.dmarc_aggregate.max_size, + &RecipientDomain::new(event.domain.as_str()), + ) + .await + .unwrap_or(25 * 1024 * 1024); let policy = event.dmarc_record.to_hash(); let (create, path) = match self.reports.entry(ReportType::Dmarc(ReportPolicy { @@ -506,9 +521,9 @@ impl Scheduler { policy, })); } - } else if path.size < *max_size { + } else if path.size < max_size { // Append to existing report - path.size += json_append(&path.path, &event.report_record, *max_size - path.size).await; + path.size += json_append(&path.path, &event.report_record, max_size - path.size).await; } } } diff --git a/crates/smtp/src/reporting/mod.rs b/crates/smtp/src/reporting/mod.rs index 0c13ef9c..d2dd6b73 100644 --- a/crates/smtp/src/reporting/mod.rs +++ b/crates/smtp/src/reporting/mod.rs @@ -34,9 +34,10 @@ use mail_auth::{ use mail_parser::DateTime; use tokio::io::{AsyncRead, AsyncWrite}; +use utils::config::if_block::IfBlock; use crate::{ - config::{AddressMatch, AggregateFrequency, DkimSigner, IfBlock, MaybeDynValue}, + config::{AddressMatch, AggregateFrequency, DkimSigner}, core::{management, Session, SMTP}, outbound::{dane::Tlsa, mta_sts::Policy}, queue::{DomainPart, Message}, @@ -129,7 +130,7 @@ impl SMTP { from_addr: &str, rcpts: impl Iterator>, report: Vec, - sign_config: &IfBlock>>, + sign_config: &IfBlock, span: &tracing::Span, deliver_now: bool, ) { @@ -138,13 +139,13 @@ impl SMTP { let from_addr_domain = from_addr_lcase.domain_part().to_string(); let mut message = Message::new_boxed(from_addr, from_addr_lcase, from_addr_domain); for rcpt_ in rcpts { - message - .add_recipient(rcpt_.as_ref(), &self.queue.config) - .await; + message.add_recipient(rcpt_.as_ref(), &self).await; } // Sign message - let signature = message.sign(sign_config, &report, span).await; + let signature = self + .sign_message(&mut message, sign_config, &report, span) + .await; // Schedule delivery at a random time between now and the next 3 hours if !deliver_now { @@ -173,28 +174,32 @@ impl SMTP { tracing::warn!(contex = "report", "Channel send failed."); } } -} -impl Message { - pub async fn sign( - &mut self, - config: &IfBlock>>, + pub async fn sign_message( + &self, + message: &mut Message, + config: &IfBlock, bytes: &[u8], span: &tracing::Span, ) -> Option> { - let signers = config.eval_and_capture(self).await.into_value(self); + let signers = self + .eval_if::, _>(config, message) + .await + .unwrap_or_default(); if !signers.is_empty() { let mut headers = Vec::with_capacity(64); for signer in signers.iter() { - match signer.sign(bytes) { - Ok(signature) => { - signature.write_header(&mut headers); - } - Err(err) => { - tracing::warn!(parent: span, + if let Some(signer) = self.get_dkim_signer(signer) { + match signer.sign(bytes) { + Ok(signature) => { + signature.write_header(&mut headers); + } + Err(err) => { + tracing::warn!(parent: span, context = "dkim", event = "sign-failed", reason = %err); + } } } } diff --git a/crates/smtp/src/reporting/scheduler.rs b/crates/smtp/src/reporting/scheduler.rs index a6650e15..30772c67 100644 --- a/crates/smtp/src/reporting/scheduler.rs +++ b/crates/smtp/src/reporting/scheduler.rs @@ -47,7 +47,7 @@ use tokio::{ use crate::{ config::AggregateFrequency, core::{management::ReportRequest, worker::SpawnCleanup, ReportCore, SMTP}, - queue::{InstantFromTimestamp, RecipientDomain, Schedule}, + queue::{InstantFromTimestamp, Schedule}, }; use super::{dmarc::GenerateDmarcReport, tls::GenerateTlsReport, Event}; @@ -198,19 +198,9 @@ impl SMTP { }; // Build base path - let mut path = self - .report - .config - .path - .eval(&RecipientDomain::new(domain)) - .await - .clone(); - let hash = *self - .report - .config - .hash - .eval(&RecipientDomain::new(domain)) - .await; + let mut path = self.report.config.path.clone(); + let todo = "fix"; + let hash = 1; if hash > 0 { path.push((policy % hash).to_string()); } @@ -242,77 +232,69 @@ impl ReportCore { pub async fn read_reports(&self) -> Scheduler { let mut scheduler = Scheduler::default(); - for path in self - .config - .path - .if_then - .iter() - .map(|t| &t.then) - .chain([&self.config.path.default]) - { - let mut dir = match tokio::fs::read_dir(path).await { - Ok(dir) => dir, - Err(_) => continue, - }; - loop { - match dir.next_entry().await { - Ok(Some(file)) => { - let file = file.path(); - if file.is_dir() { - match tokio::fs::read_dir(&file).await { - Ok(mut dir) => { - let file_ = file; - loop { - match dir.next_entry().await { - Ok(Some(file)) => { - let file = file.path(); - if file - .extension() - .map_or(false, |e| e == "t" || e == "d") - { - if let Err(err) = scheduler.add_path(file).await - { - tracing::warn!("{}", err); - } + let mut dir = match tokio::fs::read_dir(&self.config.path).await { + Ok(dir) => dir, + Err(_) => { + return scheduler; + } + }; + loop { + match dir.next_entry().await { + Ok(Some(file)) => { + let file = file.path(); + if file.is_dir() { + match tokio::fs::read_dir(&file).await { + Ok(mut dir) => { + let file_ = file; + loop { + match dir.next_entry().await { + Ok(Some(file)) => { + let file = file.path(); + if file + .extension() + .map_or(false, |e| e == "t" || e == "d") + { + if let Err(err) = scheduler.add_path(file).await { + tracing::warn!("{}", err); } } - Ok(None) => break, - Err(err) => { - tracing::warn!( - "Failed to read report directory {}: {}", - file_.display(), - err - ); - break; - } + } + Ok(None) => break, + Err(err) => { + tracing::warn!( + "Failed to read report directory {}: {}", + file_.display(), + err + ); + break; } } } - Err(err) => { - tracing::warn!( - "Failed to read report directory {}: {}", - file.display(), - err - ) - } - }; - } else if file.extension().map_or(false, |e| e == "t" || e == "d") { - if let Err(err) = scheduler.add_path(file).await { - tracing::warn!("{}", err); } + Err(err) => { + tracing::warn!( + "Failed to read report directory {}: {}", + file.display(), + err + ) + } + }; + } else if file.extension().map_or(false, |e| e == "t" || e == "d") { + if let Err(err) = scheduler.add_path(file).await { + tracing::warn!("{}", err); } } - Ok(None) => { - break; - } - Err(err) => { - tracing::warn!( - "Failed to read report directory {}: {}", - path.display(), - err - ); - break; - } + } + Ok(None) => { + break; + } + Err(err) => { + tracing::warn!( + "Failed to read report directory {}: {}", + self.config.path.display(), + err + ); + break; } } } diff --git a/crates/smtp/src/reporting/spf.rs b/crates/smtp/src/reporting/spf.rs index 7e4cdb32..1f5c37e5 100644 --- a/crates/smtp/src/reporting/spf.rs +++ b/crates/smtp/src/reporting/spf.rs @@ -49,7 +49,11 @@ impl Session { // Generate report let config = &self.core.report.config.spf; - let from_addr = config.address.eval(self).await; + let from_addr = self + .core + .eval_if(&config.address, self) + .await + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); let mut report = Vec::with_capacity(128); self.new_auth_failure(AuthFailureType::Spf, rejected) .with_authentication_results( @@ -71,9 +75,20 @@ impl Session { ) .with_spf_dns(format!("txt : {} : v=SPF1", output.domain())) // TODO use DNS record .write_rfc5322( - (config.name.eval(self).await.as_str(), from_addr.as_str()), + ( + self.core + .eval_if(&config.name, self) + .await + .unwrap_or_else(|| "Mailer Daemon".to_string()) + .as_str(), + from_addr.as_str(), + ), rcpt, - config.subject.eval(self).await, + &self + .core + .eval_if(&config.subject, self) + .await + .unwrap_or_else(|| "SPF Report".to_string()), &mut report, ) .ok(); @@ -90,7 +105,7 @@ impl Session { // Send report self.core .send_report( - from_addr, + &from_addr, [rcpt].into_iter(), report, &config.sign, diff --git a/crates/smtp/src/reporting/tls.rs b/crates/smtp/src/reporting/tls.rs index c3c578c1..052d2757 100644 --- a/crates/smtp/src/reporting/tls.rs +++ b/crates/smtp/src/reporting/tls.rs @@ -93,7 +93,9 @@ impl GenerateTlsReport for Arc { let config = &core.report.config.tls; let mut report = TlsReport { organization_name: handle - .block_on(config.org_name.eval(&RecipientDomain::new(domain.as_str()))) + .block_on( + core.eval_if(&config.org_name, &RecipientDomain::new(domain.as_str())), + ) .clone(), date_range: DateRange { start_datetime: DateTime::from_timestamp(path.created as i64), @@ -101,9 +103,7 @@ impl GenerateTlsReport for Arc { }, contact_info: handle .block_on( - config - .contact_info - .eval(&RecipientDomain::new(domain.as_str())), + core.eval_if(&config.contact_info, &RecipientDomain::new(domain.as_str())), ) .clone(), report_id: format!( @@ -245,20 +245,24 @@ impl GenerateTlsReport for Arc { // Deliver report over SMTP if !rcpts.is_empty() { - let from_addr = - handle.block_on(config.address.eval(&RecipientDomain::new(domain.as_str()))); + let from_addr = handle + .block_on(core.eval_if(&config.address, &RecipientDomain::new(domain.as_str()))) + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); let mut message = Vec::with_capacity(path.size); let _ = report.write_rfc5322_from_bytes( &domain, - handle.block_on( - core.report - .config - .submitter - .eval(&RecipientDomain::new(domain.as_str())), - ), + &handle + .block_on(core.eval_if( + &core.report.config.submitter, + &RecipientDomain::new(domain.as_str()), + )) + .unwrap_or_else(|| "localhost".to_string()), ( handle - .block_on(config.name.eval(&RecipientDomain::new(domain.as_str()))) + .block_on( + core.eval_if(&config.name, &RecipientDomain::new(domain.as_str())), + ) + .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) .as_str(), from_addr.as_str(), ), @@ -269,7 +273,7 @@ impl GenerateTlsReport for Arc { // Send report handle.block_on(core.send_report( - from_addr, + &from_addr, rcpts.iter(), message, &config.sign, @@ -291,12 +295,12 @@ impl GenerateTlsReport for Arc { impl Scheduler { pub async fn schedule_tls(&mut self, event: Box, core: &SMTP) { let max_size = core - .report - .config - .tls - .max_size - .eval(&RecipientDomain::new(event.domain.as_str())) - .await; + .eval_if( + &core.report.config.tls.max_size, + &RecipientDomain::new(event.domain.as_str()), + ) + .await + .unwrap_or(25 * 1024 * 1024); let policy_hash = event.policy.to_hash(); let (path, pos, create) = match self.reports.entry(ReportType::Tls(event.domain)) { @@ -436,10 +440,10 @@ impl Scheduler { } } } - } else if path.size < *max_size { + } else if path.size < max_size { // Append to existing report path.size += - json_append(&path.path[pos].inner, &event.failure, *max_size - path.size).await; + json_append(&path.path[pos].inner, &event.failure, max_size - path.size).await; } } } diff --git a/crates/smtp/src/scripts/event_loop.rs b/crates/smtp/src/scripts/event_loop.rs index e2db65ab..845430f7 100644 --- a/crates/smtp/src/scripts/event_loop.rs +++ b/crates/smtp/src/scripts/event_loop.rs @@ -74,7 +74,7 @@ impl SMTP { match result { Ok(event) => match event { Event::IncludeScript { name, optional } => { - if let Some(script) = self.sieve.scripts.get(name.as_str()) { + if let Some(script) = self.shared.scripts.get(name.as_str()) { input = Input::script(name, script.clone()); } else if optional { input = false.into(); @@ -95,7 +95,7 @@ impl SMTP { } => { input = false.into(); 'outer: for list in lists { - if let Some(store) = self.sieve.lookup_stores.get(&list) { + if let Some(store) = self.shared.lookup_stores.get(&list) { for value in &values { if let Ok(LookupValue::Value { .. }) = handle.block_on( store.key_get::(LookupKey::Key( @@ -163,22 +163,19 @@ impl SMTP { ); match recipient { Recipient::Address(rcpt) => { - handle.block_on(message.add_recipient(rcpt, &self.queue.config)); + handle.block_on(message.add_recipient(rcpt, self)); } Recipient::Group(rcpt_list) => { for rcpt in rcpt_list { - handle - .block_on(message.add_recipient(rcpt, &self.queue.config)); + handle.block_on(message.add_recipient(rcpt, self)); } } Recipient::List(list) => { - if let Some(list) = self.sieve.lookup_stores.get(&list) { + if let Some(list) = self.shared.lookup_stores.get(&list) { if let LookupStore::Memory(list) = list { if let MemoryStore::List(list) = list.as_ref() { for rcpt in &list.set { - handle.block_on( - message.add_recipient(rcpt, &self.queue.config), - ); + handle.block_on(message.add_recipient(rcpt, self)); } } } diff --git a/crates/smtp/src/scripts/plugins/bayes.rs b/crates/smtp/src/scripts/plugins/bayes.rs index b7566dd1..ab5a147b 100644 --- a/crates/smtp/src/scripts/plugins/bayes.rs +++ b/crates/smtp/src/scripts/plugins/bayes.rs @@ -63,8 +63,8 @@ pub fn exec_untrain(ctx: PluginContext<'_>) -> Variable { fn train(ctx: PluginContext<'_>, is_train: bool) -> Variable { let span: &tracing::Span = ctx.span; let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()), - _ => Some(&ctx.core.queue.config.lookup_store), + Variable::String(v) if !v.is_empty() => ctx.core.shared.lookup_stores.get(v.as_ref()), + _ => Some(&ctx.core.shared.default_lookup_store), }; let store = if let Some(store) = store { @@ -162,8 +162,8 @@ fn train(ctx: PluginContext<'_>, is_train: bool) -> Variable { pub fn exec_classify(ctx: PluginContext<'_>) -> Variable { let span = ctx.span; let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()), - _ => Some(&ctx.core.queue.config.lookup_store), + Variable::String(v) if !v.is_empty() => ctx.core.shared.lookup_stores.get(v.as_ref()), + _ => Some(&ctx.core.shared.default_lookup_store), }; let store = if let Some(store) = store { store @@ -261,8 +261,8 @@ pub fn exec_is_balanced(ctx: PluginContext<'_>) -> Variable { let span = ctx.span; let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()), - _ => Some(&ctx.core.queue.config.lookup_store), + Variable::String(v) if !v.is_empty() => ctx.core.shared.lookup_stores.get(v.as_ref()), + _ => Some(&ctx.core.shared.default_lookup_store), }; let store = if let Some(store) = store { store diff --git a/crates/smtp/src/scripts/plugins/lookup.rs b/crates/smtp/src/scripts/plugins/lookup.rs index c4e1d4a9..27978d8a 100644 --- a/crates/smtp/src/scripts/plugins/lookup.rs +++ b/crates/smtp/src/scripts/plugins/lookup.rs @@ -61,8 +61,8 @@ pub fn register_local_domain(plugin_id: u32, fnc_map: &mut FunctionMap) -> Variable { let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()), - _ => Some(&ctx.core.queue.config.lookup_store), + Variable::String(v) if !v.is_empty() => ctx.core.shared.lookup_stores.get(v.as_ref()), + _ => Some(&ctx.core.shared.default_lookup_store), }; if let Some(store) = store { @@ -107,8 +107,8 @@ pub fn exec(ctx: PluginContext<'_>) -> Variable { pub fn exec_get(ctx: PluginContext<'_>) -> Variable { let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()), - _ => Some(&ctx.core.queue.config.lookup_store), + Variable::String(v) if !v.is_empty() => ctx.core.shared.lookup_stores.get(v.as_ref()), + _ => Some(&ctx.core.shared.default_lookup_store), }; if let Some(store) = store { @@ -136,8 +136,8 @@ pub fn exec_get(ctx: PluginContext<'_>) -> Variable { pub fn exec_set(ctx: PluginContext<'_>) -> Variable { let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.sieve.lookup_stores.get(v.as_ref()), - _ => Some(&ctx.core.queue.config.lookup_store), + Variable::String(v) if !v.is_empty() => ctx.core.shared.lookup_stores.get(v.as_ref()), + _ => Some(&ctx.core.shared.default_lookup_store), }; if let Some(store) = store { @@ -399,8 +399,8 @@ pub fn exec_local_domain(ctx: PluginContext<'_>) -> Variable { if !domain.is_empty() { let directory = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.sieve.directories.get(v.as_ref()), - _ => Some(&ctx.core.queue.config.directory), + Variable::String(v) if !v.is_empty() => ctx.core.shared.directories.get(v.as_ref()), + _ => Some(&ctx.core.shared.default_directory), }; if let Some(directory) = directory { diff --git a/crates/smtp/src/scripts/plugins/query.rs b/crates/smtp/src/scripts/plugins/query.rs index 98905b12..f66cb012 100644 --- a/crates/smtp/src/scripts/plugins/query.rs +++ b/crates/smtp/src/scripts/plugins/query.rs @@ -41,7 +41,7 @@ pub fn exec(ctx: PluginContext<'_>) -> Variable { // Obtain store name let store = ctx.arguments[0].to_string(); - let store = if let Some(store_) = ctx.core.sieve.lookup_stores.get(store.as_ref()) { + let store = if let Some(store_) = ctx.core.shared.lookup_stores.get(store.as_ref()) { store_ } else { tracing::warn!( diff --git a/crates/store/src/dispatch/lookup.rs b/crates/store/src/dispatch/lookup.rs index 355bdbe9..004f8484 100644 --- a/crates/store/src/dispatch/lookup.rs +++ b/crates/store/src/dispatch/lookup.rs @@ -21,6 +21,8 @@ * for more details. */ +use utils::expr; + use crate::{backend::memory::MemoryStore, Row}; #[allow(unused_imports)] use crate::{ @@ -239,3 +241,19 @@ impl From> for String { } } } + +impl<'x> From> for expr::Variable<'x> { + fn from(value: Value<'x>) -> Self { + match value { + Value::Integer(v) => expr::Variable::Integer(v), + Value::Bool(v) => expr::Variable::Integer(v as i64), + Value::Float(v) => expr::Variable::Float(v), + Value::Text(v) => expr::Variable::String(v), + Value::Blob(v) => expr::Variable::String(match v { + std::borrow::Cow::Borrowed(v) => String::from_utf8_lossy(v), + std::borrow::Cow::Owned(v) => String::from_utf8_lossy(&v).into_owned().into(), + }), + Value::Null => expr::Variable::String("".into()), + } + } +} diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index e8e6f0e9..cd25bbb3 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -38,6 +38,7 @@ parking_lot = "0.12" arc-swap = "1.6.0" futures = "0.3" proxy-header = { version = "0.1.0", features = ["tokio"] } +regex = "1.7.0" [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/utils/src/config/dynvalue.rs b/crates/utils/src/config/dynvalue.rs deleted file mode 100644 index ec0a03ea..00000000 --- a/crates/utils/src/config/dynvalue.rs +++ /dev/null @@ -1,191 +0,0 @@ -/* - * 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 std::borrow::Cow; - -use super::{ - utils::{AsKey, ParseValue}, - DynValue, KeyLookup, -}; - -impl ParseValue for DynValue { - #[allow(clippy::while_let_on_iterator)] - fn parse_value(key: impl AsKey, value: &str) -> super::Result { - let mut items = vec![]; - let mut buf = vec![]; - let mut iter = value.as_bytes().iter().peekable(); - - while let Some(&ch) = iter.next() { - if ch == b'$' && matches!(iter.peek(), Some(b'{')) { - iter.next(); - match iter.peek() { - Some(ch) if **ch == b'{' => { - buf.push(b'$'); - while let Some(&ch) = iter.next() { - if ch == b'}' { - break; - } else { - buf.push(ch); - } - } - } - Some(ch) => { - if !buf.is_empty() { - items.push(DynValue::String(String::from_utf8(buf).unwrap())); - buf = vec![]; - } - if ch.is_ascii_digit() { - while let Some(&ch) = iter.next() { - if ch.is_ascii_digit() { - buf.push(ch); - } else if ch == b'}' && !buf.is_empty() { - let str_num = std::str::from_utf8(&buf).unwrap(); - items.push(DynValue::Position(str_num.parse().map_err(|_| { - format!( - "Failed to parse position {str_num:?} in value {value:?} for key {}", - key.as_key() - ) - })?)); - buf.clear(); - break; - } else { - return Err(format!( - "Invalid dynamic string {value:?} for key {}", - key.as_key() - )); - } - } - } else { - while let Some(&ch) = iter.next() { - if ch == b'}' { - if !buf.is_empty() { - items.push(DynValue::Key(T::parse_value( - key.clone(), - std::str::from_utf8(&buf).unwrap_or_default(), - )?)); - buf.clear(); - break; - } else { - return Err(format!( - "Invalid dynamic string {value:?} for key {}", - key.as_key() - )); - } - } else { - buf.push(ch); - } - } - } - } - None => {} - } - } else { - buf.push(ch); - } - } - - if !buf.is_empty() { - let item = DynValue::String(String::from_utf8(buf).unwrap()); - if !items.is_empty() { - items.push(item); - } else { - return Ok(item); - } - } - - Ok(match items.len() { - 0 => DynValue::String(String::new()), - 1 => items.pop().unwrap(), - _ => DynValue::List(items), - }) - } -} - -impl DynValue { - pub fn apply<'x, 'y: 'x>( - &'x self, - captures: Vec, - keys: &'y impl KeyLookup, - ) -> Cow<'x, str> { - match self { - DynValue::String(value) => Cow::Borrowed(value.as_str()), - DynValue::Position(pos) => captures - .into_iter() - .nth(*pos) - .map(Cow::Owned) - .unwrap_or(Cow::Borrowed("")), - DynValue::List(items) => { - let mut result = String::new(); - - for item in items { - match item { - DynValue::String(value) => result.push_str(value), - DynValue::Position(pos) => { - if let Some(capture) = captures.get(*pos) { - result.push_str(capture); - } - } - DynValue::Key(key) => result.push_str(keys.key(key).as_ref()), - DynValue::List(_) => unreachable!(), - } - } - - Cow::Owned(result) - } - DynValue::Key(key) => keys.key(key), - } - } - - pub fn apply_borrowed<'x, 'y: 'x>( - &'x self, - captures: &'y [String], - keys: &'y impl KeyLookup, - ) -> Cow<'x, str> { - match self { - DynValue::String(value) => Cow::Borrowed(value.as_str()), - DynValue::Position(pos) => captures - .get(*pos) - .map(|v| Cow::Borrowed(v.as_str())) - .unwrap_or(Cow::Borrowed("")), - DynValue::List(items) => { - let mut result = String::new(); - - for item in items { - match item { - DynValue::String(value) => result.push_str(value), - DynValue::Position(pos) => { - if let Some(capture) = captures.get(*pos) { - result.push_str(capture); - } - } - DynValue::Key(key) => result.push_str(keys.key(key).as_ref()), - DynValue::List(_) => unreachable!(), - } - } - - Cow::Owned(result) - } - DynValue::Key(key) => keys.key(key), - } - } -} diff --git a/crates/utils/src/config/if_block.rs b/crates/utils/src/config/if_block.rs new file mode 100644 index 00000000..6ad29ca2 --- /dev/null +++ b/crates/utils/src/config/if_block.rs @@ -0,0 +1,181 @@ +/* + * 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 crate::expr::{Constant, Expression, Token, Variable}; + +use super::{utils::AsKey, Config}; + +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] +pub struct IfThen { + pub expr: Expression, + pub then: Expression, +} + +#[derive(Debug, Clone, Default)] +#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] +pub struct IfBlock { + pub key: String, + pub if_then: Vec, + pub default: Expression, +} + +impl IfBlock { + pub fn new>(value: T) -> Self { + Self { + key: String::new(), + if_then: Vec::new(), + default: Expression::from(value), + } + } + + pub async fn eval<'x, V, F, R>(&'x self, var: V, mut fnc: F) -> Variable<'x> + where + V: Fn(u32) -> Variable<'x>, + F: FnMut(u32, Vec>) -> R, + R: std::future::Future> + Send, + { + let mut captures = Vec::new(); + + for if_then in &self.if_then { + if if_then + .expr + .eval(&var, &mut fnc, &mut captures) + .await + .to_bool() + { + return if_then.then.eval(&var, &mut fnc, &mut captures).await; + } + } + + self.default.eval(&var, &mut fnc, &mut captures).await + } + + pub fn is_empty(&self) -> bool { + self.default.is_empty() && self.if_then.is_empty() + } +} + +impl Config { + pub fn parse_if_block( + &self, + prefix: impl AsKey, + token_map: impl Fn(&str) -> Result, + ) -> super::Result> { + let key = prefix.as_key(); + let prefix = prefix.as_prefix(); + + let mut found_if = false; + let mut found_else = ""; + let mut found_then = false; + + // Parse conditions + let mut if_block = IfBlock { + key, + ..Default::default() + }; + let mut last_array_pos = ""; + let key = &if_block.key; + + for (item, value) in &self.keys { + if let Some(suffix_) = item.strip_prefix(&prefix) { + if let Some((array_pos, suffix)) = suffix_.split_once('.') { + let if_key = suffix.split_once('.').map(|(v, _)| v).unwrap_or(suffix); + if if_key == "if" { + if array_pos != last_array_pos { + if !last_array_pos.is_empty() && !found_then { + return Err(format!( + "Missing 'then' in 'if' condition {} for property {:?}.", + last_array_pos.parse().unwrap_or(0) + 1, + key + )); + } + + if_block.if_then.push(IfThen { + expr: Expression::parse(key.as_str(), value, &token_map)?, + then: Expression::default(), + }); + + found_then = false; + last_array_pos = array_pos; + } + + found_if = true; + } else if if_key == "else" { + if found_else.is_empty() { + if found_if { + if_block.default = + Expression::parse(key.as_str(), value, &token_map)?; + found_else = array_pos; + } else { + return Err(format!( + "Found 'else' before 'if' for property {key:?}.", + )); + } + } else if array_pos != found_else { + return Err(format!("Multiple 'else' found for property {key:?}.")); + } + } else if if_key == "then" { + if found_else.is_empty() { + if array_pos == last_array_pos { + if !found_then { + if_block.if_then.last_mut().unwrap().then = + Expression::parse(key.as_str(), value, &token_map)?; + found_then = true; + } + } else { + return Err(format!( + "Found 'then' without 'if' for property {key:?}.", + )); + } + } else { + return Err(format!( + "Found 'then' in 'else' block for property {key:?}.", + )); + } + } + } else { + return Err(format!("Invalid property {item:?} found in 'if' block.")); + } + } else if item == key { + // There is a single value, parse and return + if_block.default = Expression::parse(key.as_str(), value, &token_map)?; + return Ok(Some(if_block)); + } + } + + if !found_if { + Ok(None) + } else if !found_then { + Err(format!( + "Missing 'then' in 'if' condition {} for property {:?}.", + last_array_pos.parse().unwrap_or(0) + 1, + key + )) + } else if found_else.is_empty() { + Err(format!("Missing 'else' for property {key:?}.")) + } else { + Ok(Some(if_block)) + } + } +} diff --git a/crates/utils/src/config/mod.rs b/crates/utils/src/config/mod.rs index 3069aaa6..6e9ea4fd 100644 --- a/crates/utils/src/config/mod.rs +++ b/crates/utils/src/config/mod.rs @@ -22,21 +22,14 @@ */ pub mod cron; -pub mod dynvalue; +pub mod if_block; pub mod ipmask; pub mod listener; pub mod parser; pub mod tls; pub mod utils; -use std::{ - borrow::Cow, - collections::BTreeMap, - fmt::Display, - net::{IpAddr, Ipv4Addr, SocketAddr}, - sync::Arc, - time::Duration, -}; +use std::{collections::BTreeMap, fmt::Display, net::SocketAddr, sync::Arc, time::Duration}; use ahash::{AHashMap, AHashSet}; use tokio::net::TcpSocket; @@ -48,7 +41,7 @@ use crate::{ UnwrapFailure, }; -use self::{ipmask::IpAddrMask, utils::ParseValue}; +use self::ipmask::IpAddrMask; #[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Config { @@ -107,38 +100,6 @@ pub enum ServerProtocol { ManageSieve, } -#[derive(Debug, Clone)] -pub enum DynValue { - String(String), - Position(usize), - Key(T), - List(Vec>), -} - -pub trait KeyLookup { - type Key: ParseValue; - - fn key(&self, key: &Self::Key) -> Cow<'_, str>; - fn key_as_int(&self, key: &Self::Key) -> i32; - fn key_as_ip(&self, key: &Self::Key) -> IpAddr; -} - -impl KeyLookup for () { - type Key = String; - - fn key(&self, _: &Self::Key) -> Cow<'_, str> { - "".into() - } - - fn key_as_int(&self, _: &Self::Key) -> i32 { - 0 - } - - fn key_as_ip(&self, _: &Self::Key) -> IpAddr { - IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)) - } -} - #[derive(Debug, Default, PartialEq, Eq, Clone)] pub struct Rate { pub requests: u64, diff --git a/crates/utils/src/config/utils.rs b/crates/utils/src/config/utils.rs index 4a5db0e7..2b99a186 100644 --- a/crates/utils/src/config/utils.rs +++ b/crates/utils/src/config/utils.rs @@ -34,6 +34,8 @@ use mail_auth::{ }; use smtp_proto::MtPriority; +use crate::expr::{Constant, Variable}; + use super::{Config, Rate}; impl Config { @@ -229,15 +231,15 @@ impl Config { } } -pub trait ParseValues: Sized + Default { - fn parse_values(key: impl AsKey, values: &Config) -> super::Result; - fn is_multivalue() -> bool; -} - pub trait ParseValue: Sized { fn parse_value(key: impl AsKey, value: &str) -> super::Result; } +pub trait ConstantValue: + ParseValue + for<'x> TryFrom> + Into + Sized +{ +} + pub trait ParseKey { fn parse_key(&self, key: impl AsKey) -> super::Result; } @@ -260,43 +262,6 @@ impl ParseKey for &String { } } -impl ParseValues for Vec { - fn is_multivalue() -> bool { - true - } - - fn parse_values(key: impl AsKey, values: &Config) -> super::Result { - let mut result = Vec::new(); - for (key, value) in values.values(key) { - result.push(T::parse_value(key, value)?); - } - Ok(result) - } -} - -impl ParseValues for T { - fn is_multivalue() -> bool { - false - } - - fn parse_values(key: impl AsKey, values: &Config) -> super::Result { - let mut iter = values.values(key); - if let Some((key, value)) = iter.next() { - let result = T::parse_value(key, value)?; - if iter.next().is_none() { - Ok(result) - } else { - Err(format!( - "Property {:?} cannot have multiple values.", - key.as_key() - )) - } - } else { - Ok(T::default()) - } - } -} - impl ParseValue for Option { fn parse_value(key: impl AsKey, value: &str) -> super::Result { if !value.is_empty() @@ -470,6 +435,35 @@ impl ParseValue for MtPriority { } } +impl<'x> TryFrom> for MtPriority { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::Integer(value) => match value { + 0 => Ok(MtPriority::Mixer), + 1 => Ok(MtPriority::Stanag4406), + 2 => Ok(MtPriority::Nsep), + _ => Err(()), + }, + Variable::String(value) => MtPriority::parse_value("", &value).map_err(|_| ()), + _ => Err(()), + } + } +} + +impl From for Constant { + fn from(value: MtPriority) -> Self { + Constant::Integer(match value { + MtPriority::Mixer => 0, + MtPriority::Stanag4406 => 1, + MtPriority::Nsep => 2, + }) + } +} + +impl ConstantValue for MtPriority {} + impl ParseValue for Canonicalization { fn parse_value(key: impl AsKey, value: &str) -> super::Result { match value { @@ -503,6 +497,37 @@ impl ParseValue for IpLookupStrategy { } } +impl<'x> TryFrom> for IpLookupStrategy { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::Integer(value) => match value { + 0 => Ok(IpLookupStrategy::Ipv4Only), + 1 => Ok(IpLookupStrategy::Ipv6Only), + 2 => Ok(IpLookupStrategy::Ipv6thenIpv4), + 3 => Ok(IpLookupStrategy::Ipv4thenIpv6), + _ => Err(()), + }, + Variable::String(value) => IpLookupStrategy::parse_value("", &value).map_err(|_| ()), + _ => Err(()), + } + } +} + +impl From for Constant { + fn from(value: IpLookupStrategy) -> Self { + Constant::Integer(match value { + IpLookupStrategy::Ipv4Only => 0, + IpLookupStrategy::Ipv6Only => 1, + IpLookupStrategy::Ipv6thenIpv4 => 2, + IpLookupStrategy::Ipv4thenIpv6 => 3, + }) + } +} + +impl ConstantValue for IpLookupStrategy {} + impl ParseValue for Algorithm { fn parse_value(key: impl AsKey, value: &str) -> super::Result { match value { @@ -568,6 +593,124 @@ impl ParseValue for Duration { } } +impl ConstantValue for Duration {} + +impl<'x> TryFrom> for Duration { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::Integer(value) if value > 0 => Ok(Duration::from_millis(value as u64)), + Variable::Float(value) if value > 0.0 => Ok(Duration::from_millis(value as u64)), + Variable::String(value) if !value.is_empty() => { + Duration::parse_value("", &value).map_err(|_| ()) + } + _ => Err(()), + } + } +} + +impl From for Constant { + fn from(value: Duration) -> Self { + Constant::Integer(value.as_millis() as i64) + } +} + +impl<'x> TryFrom> for Rate { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::Array(items) if items.len() == 2 => { + let requests = items[0].to_integer().ok_or(())?; + let period = items[1].to_integer().ok_or(())?; + + if requests > 0 && period > 0 { + Ok(Rate { + requests: requests as u64, + period: Duration::from_millis(period as u64), + }) + } else { + Err(()) + } + } + _ => Err(()), + } + } +} + +impl<'x> TryFrom> for Ipv4Addr { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::String(value) => value.parse().map_err(|_| ()), + _ => Err(()), + } + } +} + +impl<'x> TryFrom> for Ipv6Addr { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::String(value) => value.parse().map_err(|_| ()), + _ => Err(()), + } + } +} + +impl<'x> TryFrom> for IpAddr { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + match value { + Variable::String(value) => value.parse().map_err(|_| ()), + _ => Err(()), + } + } +} + +impl<'x, T: TryFrom>> TryFrom> for Vec +where + Result, ()>: FromIterator>>::Error>>, +{ + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + value + .into_array() + .into_iter() + .map(|v| T::try_from(v)) + .collect() + } +} + +pub struct NoConstants; + +impl<'x> TryFrom> for NoConstants { + type Error = (); + + fn try_from(_: Variable<'x>) -> Result { + Err(()) + } +} + +impl From for Constant { + fn from(_: NoConstants) -> Self { + Constant::Integer(0) + } +} + +impl ParseValue for NoConstants { + fn parse_value(_: impl AsKey, _: &str) -> super::Result { + Err("".to_string()) + } +} + +impl ConstantValue for NoConstants {} + impl ParseValue for Rate { fn parse_value(key: impl AsKey, value: &str) -> super::Result { if let Some((requests, period)) = value.split_once('/') { diff --git a/crates/utils/src/expr/eval.rs b/crates/utils/src/expr/eval.rs new file mode 100644 index 00000000..df889fc0 --- /dev/null +++ b/crates/utils/src/expr/eval.rs @@ -0,0 +1,569 @@ +/* + * Copyright (c) 2020-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 std::{borrow::Cow, cmp::Ordering, fmt::Display}; + +use super::{ + functions::FUNCTIONS, BinaryOperator, Constant, Expression, ExpressionItem, UnaryOperator, + Variable, +}; + +impl Expression { + pub async fn eval<'x, 'y, V, F, R>( + &'x self, + var: V, + mut fnc: F, + captures: &'y mut Vec, + ) -> Variable<'x> + where + V: Fn(u32) -> Variable<'x>, + F: FnMut(u32, Vec>) -> R, + R: std::future::Future> + Send, + { + let mut stack = Vec::new(); + let mut exprs = self.items.iter(); + + while let Some(expr) = exprs.next() { + match expr { + ExpressionItem::Variable(v) => { + stack.push(var(*v)); + } + ExpressionItem::Constant(val) => { + stack.push(Variable::from(val)); + } + ExpressionItem::Capture(v) => { + stack.push(Variable::String(Cow::Owned( + captures + .get(*v as usize) + .map(|v| v.as_str()) + .unwrap_or_default() + .to_string(), + ))); + } + ExpressionItem::UnaryOperator(op) => { + let value = stack.pop().unwrap_or_default(); + stack.push(match op { + UnaryOperator::Not => value.op_not(), + UnaryOperator::Minus => value.op_minus(), + }); + } + ExpressionItem::BinaryOperator(op) => { + let right = stack.pop().unwrap_or_default(); + let left = stack.pop().unwrap_or_default(); + stack.push(match op { + BinaryOperator::Add => left.op_add(right), + BinaryOperator::Subtract => left.op_subtract(right), + BinaryOperator::Multiply => left.op_multiply(right), + BinaryOperator::Divide => left.op_divide(right), + BinaryOperator::And => left.op_and(right), + BinaryOperator::Or => left.op_or(right), + BinaryOperator::Xor => left.op_xor(right), + BinaryOperator::Eq => left.op_eq(right), + BinaryOperator::Ne => left.op_ne(right), + BinaryOperator::Lt => left.op_lt(right), + BinaryOperator::Le => left.op_le(right), + BinaryOperator::Gt => left.op_gt(right), + BinaryOperator::Ge => left.op_ge(right), + }); + } + ExpressionItem::Function { id, num_args } => { + let num_args = *num_args as usize; + + let mut arguments = Variable::array(num_args); + for arg_num in 0..num_args { + arguments[num_args - arg_num - 1] = stack.pop().unwrap_or_default(); + } + + let result = if let Some((_, fnc, _)) = FUNCTIONS.get(*id as usize) { + (fnc)(arguments) + } else { + (fnc)(*id - FUNCTIONS.len() as u32, arguments).await + }; + + stack.push(result); + } + ExpressionItem::JmpIf { val, pos } => { + if stack.last().map_or(false, |v| v.to_bool()) == *val { + for _ in 0..*pos { + exprs.next(); + } + } + } + ExpressionItem::ArrayAccess => { + let index = stack + .pop() + .unwrap_or_default() + .to_usize() + .unwrap_or_default(); + let array = stack.pop().unwrap_or_default().into_array(); + stack.push(array.into_iter().nth(index).unwrap_or_default()); + } + ExpressionItem::ArrayBuild(num_items) => { + let num_items = *num_items as usize; + let mut items = Variable::array(num_items); + for arg_num in 0..num_items { + items[num_items - arg_num - 1] = stack.pop().unwrap_or_default(); + } + stack.push(Variable::Array(items)); + } + ExpressionItem::Regex(regex) => { + captures.clear(); + let value = stack.pop().unwrap_or_default().into_string(); + + for captures_ in regex.captures_iter(value.as_ref()) { + for capture in captures_.iter() { + captures.push(capture.map_or("", |m| m.as_str()).to_string()); + } + } + + stack.push(Variable::Integer(!captures.is_empty() as i64)); + } + } + } + + stack.pop().unwrap_or_default() + } + + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + pub fn items(&self) -> &[ExpressionItem] { + &self.items + } +} + +impl<'x> Variable<'x> { + pub fn op_add(self, other: Variable<'x>) -> Variable<'x> { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_add(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a + b), + (Variable::Integer(i), Variable::Float(f)) + | (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 + f), + (Variable::Array(a), Variable::Array(b)) => { + Variable::Array(a.into_iter().chain(b).collect::>()) + } + (Variable::Array(a), b) => { + Variable::Array(a.into_iter().chain([b]).collect::>()) + } + (a, Variable::Array(b)) => { + Variable::Array([a].into_iter().chain(b).collect::>()) + } + (Variable::String(a), b) => { + if !a.is_empty() { + Variable::String(format!("{}{}", a, b).into()) + } else { + b + } + } + (a, Variable::String(b)) => { + if !b.is_empty() { + Variable::String(format!("{}{}", a, b).into()) + } else { + a + } + } + } + } + + pub fn op_subtract(self, other: Variable<'x>) -> Variable<'x> { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_sub(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a - b), + (Variable::Integer(a), Variable::Float(b)) => Variable::Float(a as f64 - b), + (Variable::Float(a), Variable::Integer(b)) => Variable::Float(a - b as f64), + (Variable::Array(a), b) | (b, Variable::Array(a)) => { + Variable::Array(a.into_iter().filter(|v| v != &b).collect::>()) + } + (a, b) => a.parse_number().op_subtract(b.parse_number()), + } + } + + pub fn op_multiply(self, other: Variable<'x>) -> Variable<'x> { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => Variable::Integer(a.saturating_mul(b)), + (Variable::Float(a), Variable::Float(b)) => Variable::Float(a * b), + (Variable::Integer(i), Variable::Float(f)) + | (Variable::Float(f), Variable::Integer(i)) => Variable::Float(i as f64 * f), + (a, b) => a.parse_number().op_multiply(b.parse_number()), + } + } + + pub fn op_divide(self, other: Variable<'x>) -> Variable<'x> { + match (self, other) { + (Variable::Integer(a), Variable::Integer(b)) => { + Variable::Float(if b != 0 { a as f64 / b as f64 } else { 0.0 }) + } + (Variable::Float(a), Variable::Float(b)) => { + Variable::Float(if b != 0.0 { a / b } else { 0.0 }) + } + (Variable::Integer(a), Variable::Float(b)) => { + Variable::Float(if b != 0.0 { a as f64 / b } else { 0.0 }) + } + (Variable::Float(a), Variable::Integer(b)) => { + Variable::Float(if b != 0 { a / b as f64 } else { 0.0 }) + } + (a, b) => a.parse_number().op_divide(b.parse_number()), + } + } + + pub fn op_and(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self.to_bool() & other.to_bool())) + } + + pub fn op_or(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self.to_bool() | other.to_bool())) + } + + pub fn op_xor(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self.to_bool() ^ other.to_bool())) + } + + pub fn op_eq(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self == other)) + } + + pub fn op_ne(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self != other)) + } + + pub fn op_lt(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self < other)) + } + + pub fn op_le(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self <= other)) + } + + pub fn op_gt(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self > other)) + } + + pub fn op_ge(self, other: Variable) -> Variable { + Variable::Integer(i64::from(self >= other)) + } + + pub fn op_not(self) -> Variable<'static> { + Variable::Integer(i64::from(!self.to_bool())) + } + + pub fn op_minus(self) -> Variable<'static> { + match self { + Variable::Integer(n) => Variable::Integer(-n), + Variable::Float(n) => Variable::Float(-n), + _ => self.parse_number().op_minus(), + } + } + + pub fn parse_number(&self) -> Variable<'static> { + match self { + Variable::String(s) if !s.is_empty() => { + if let Ok(n) = s.parse::() { + Variable::Integer(n) + } else if let Ok(n) = s.parse::() { + Variable::Float(n) + } else { + Variable::Integer(0) + } + } + Variable::Integer(n) => Variable::Integer(*n), + Variable::Float(n) => Variable::Float(*n), + Variable::Array(l) => Variable::Integer(l.is_empty() as i64), + _ => Variable::Integer(0), + } + } + + #[inline(always)] + fn array(num_items: usize) -> Vec> { + let mut items = Vec::with_capacity(num_items); + for _ in 0..num_items { + items.push(Variable::Integer(0)); + } + items + } + + pub fn to_ref<'y: 'x>(&'y self) -> Variable<'x> { + match self { + Variable::String(s) => Variable::String(Cow::Borrowed(s.as_ref())), + Variable::Integer(n) => Variable::Integer(*n), + Variable::Float(n) => Variable::Float(*n), + Variable::Array(l) => Variable::Array(l.iter().map(|v| v.to_ref()).collect::>()), + } + } + + pub fn to_bool(&self) -> bool { + match self { + Variable::Float(f) => *f != 0.0, + Variable::Integer(n) => *n != 0, + Variable::String(s) => !s.is_empty(), + Variable::Array(a) => !a.is_empty(), + } + } + + pub fn to_string(&self) -> Cow<'_, str> { + match self { + Variable::String(s) => Cow::Borrowed(s.as_ref()), + Variable::Integer(n) => Cow::Owned(n.to_string()), + Variable::Float(n) => Cow::Owned(n.to_string()), + Variable::Array(l) => { + let mut result = String::with_capacity(self.len() * 10); + for item in l { + if !result.is_empty() { + result.push_str("\r\n"); + } + match item { + Variable::String(v) => result.push_str(v), + Variable::Integer(v) => result.push_str(&v.to_string()), + Variable::Float(v) => result.push_str(&v.to_string()), + Variable::Array(_) => {} + } + } + Cow::Owned(result) + } + } + } + + pub fn into_string(self) -> Cow<'x, str> { + match self { + Variable::String(s) => s, + Variable::Integer(n) => Cow::Owned(n.to_string()), + Variable::Float(n) => Cow::Owned(n.to_string()), + Variable::Array(l) => { + let mut result = String::with_capacity(l.len() * 10); + for item in l { + if !result.is_empty() { + result.push_str("\r\n"); + } + match item { + Variable::String(v) => result.push_str(v.as_ref()), + Variable::Integer(v) => result.push_str(&v.to_string()), + Variable::Float(v) => result.push_str(&v.to_string()), + Variable::Array(_) => {} + } + } + Cow::Owned(result) + } + } + } + + pub fn to_integer(&self) -> Option { + match self { + Variable::Integer(n) => Some(*n), + Variable::Float(n) => Some(*n as i64), + Variable::String(s) if !s.is_empty() => s.parse::().ok(), + _ => None, + } + } + + pub fn to_usize(&self) -> Option { + match self { + Variable::Integer(n) => Some(*n as usize), + Variable::Float(n) => Some(*n as usize), + Variable::String(s) if !s.is_empty() => s.parse::().ok(), + _ => None, + } + } + + pub fn len(&self) -> usize { + match self { + Variable::String(s) => s.len(), + Variable::Integer(_) | Variable::Float(_) => 2, + Variable::Array(l) => l.iter().map(|v| v.len() + 2).sum(), + } + } + + pub fn is_empty(&self) -> bool { + match self { + Variable::String(s) => s.is_empty(), + _ => false, + } + } + + pub fn as_array(&self) -> Option<&[Variable]> { + match self { + Variable::Array(l) => Some(l), + _ => None, + } + } + + pub fn into_array(self) -> Vec> { + match self { + Variable::Array(l) => l, + v if !v.is_empty() => vec![v], + _ => vec![], + } + } + + pub fn to_array(&self) -> Vec> { + match self { + Variable::Array(l) => l.iter().map(|v| v.to_ref()).collect::>(), + v if !v.is_empty() => vec![v.to_ref()], + _ => vec![], + } + } + + pub fn into_owned(self) -> Variable<'static> { + match self { + Variable::String(s) => Variable::String(Cow::Owned(s.into_owned())), + Variable::Integer(n) => Variable::Integer(n), + Variable::Float(n) => Variable::Float(n), + Variable::Array(l) => Variable::Array(l.into_iter().map(|v| v.into_owned()).collect()), + } + } +} + +impl PartialEq for Variable<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Integer(a), Self::Integer(b)) => a == b, + (Self::Float(a), Self::Float(b)) => a == b, + (Self::Integer(a), Self::Float(b)) | (Self::Float(b), Self::Integer(a)) => { + *a as f64 == *b + } + (Self::String(a), Self::String(b)) => a == b, + (Self::String(_), Self::Integer(_) | Self::Float(_)) => &self.parse_number() == other, + (Self::Integer(_) | Self::Float(_), Self::String(_)) => self == &other.parse_number(), + (Self::Array(a), Self::Array(b)) => a == b, + _ => false, + } + } +} + +impl Eq for Variable<'_> {} + +#[allow(clippy::non_canonical_partial_ord_impl)] +impl PartialOrd for Variable<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + match (self, other) { + (Self::Integer(a), Self::Integer(b)) => a.partial_cmp(b), + (Self::Float(a), Self::Float(b)) => a.partial_cmp(b), + (Self::Integer(a), Self::Float(b)) => (*a as f64).partial_cmp(b), + (Self::Float(a), Self::Integer(b)) => a.partial_cmp(&(*b as f64)), + (Self::String(a), Self::String(b)) => a.partial_cmp(b), + (Self::String(_), Self::Integer(_) | Self::Float(_)) => { + self.parse_number().partial_cmp(other) + } + (Self::Integer(_) | Self::Float(_), Self::String(_)) => { + self.partial_cmp(&other.parse_number()) + } + (Self::Array(a), Self::Array(b)) => a.partial_cmp(b), + (Self::Array(_) | Self::String(_), _) => Ordering::Greater.into(), + (_, Self::Array(_)) => Ordering::Less.into(), + } + } +} + +impl Ord for Variable<'_> { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.partial_cmp(other).unwrap_or(Ordering::Greater) + } +} + +impl Display for Variable<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Variable::String(v) => v.fmt(f), + Variable::Integer(v) => v.fmt(f), + Variable::Float(v) => v.fmt(f), + Variable::Array(v) => { + for (i, v) in v.iter().enumerate() { + if i > 0 { + f.write_str("\n")?; + } + v.fmt(f)?; + } + Ok(()) + } + } + } +} + +trait IntoBool { + fn into_bool(self) -> bool; +} + +impl IntoBool for f64 { + #[inline(always)] + fn into_bool(self) -> bool { + self != 0.0 + } +} + +impl IntoBool for i64 { + #[inline(always)] + fn into_bool(self) -> bool { + self != 0 + } +} + +impl<'x> From<&'x Constant> for Variable<'x> { + fn from(value: &'x Constant) -> Self { + match value { + Constant::Integer(i) => Variable::Integer(*i), + Constant::Float(f) => Variable::Float(*f), + Constant::String(s) => Variable::String(s.as_str().into()), + } + } +} + +impl<'x> TryFrom> for String { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + if let Variable::String(s) = value { + Ok(s.into_owned()) + } else { + Err(()) + } + } +} + +impl<'x> From> for bool { + fn from(val: Variable<'x>) -> Self { + val.to_bool() + } +} + +impl<'x> TryFrom> for i64 { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + value.to_integer().ok_or(()) + } +} + +impl<'x> TryFrom> for u64 { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + value.to_integer().map(|v| v as u64).ok_or(()) + } +} + +impl<'x> TryFrom> for usize { + type Error = (); + + fn try_from(value: Variable<'x>) -> Result { + value.to_usize().ok_or(()) + } +} diff --git a/crates/utils/src/expr/functions/array.rs b/crates/utils/src/expr/functions/array.rs new file mode 100644 index 00000000..12868067 --- /dev/null +++ b/crates/utils/src/expr/functions/array.rs @@ -0,0 +1,82 @@ +/* + * 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 crate::expr::Variable; + +pub(crate) fn fn_count(v: Vec) -> Variable { + match &v[0] { + Variable::Array(a) => a.len(), + v => { + if !v.is_empty() { + 1 + } else { + 0 + } + } + } + .into() +} + +pub(crate) fn fn_sort(mut v: Vec) -> Variable { + let is_asc = v[1].to_bool(); + let mut arr = v.remove(0).into_array(); + if is_asc { + arr.sort_unstable_by(|a, b| b.cmp(a)); + } else { + arr.sort_unstable(); + } + arr.into() +} + +pub(crate) fn fn_dedup(mut v: Vec) -> Variable { + let arr = v.remove(0).into_array(); + let mut result = Vec::with_capacity(arr.len()); + + for item in arr { + if !result.contains(&item) { + result.push(item); + } + } + + result.into() +} + +pub(crate) fn fn_is_intersect(v: Vec) -> Variable { + match (&v[0], &v[1]) { + (Variable::Array(a), Variable::Array(b)) => a.iter().any(|x| b.contains(x)), + (Variable::Array(a), item) | (item, Variable::Array(a)) => a.contains(item), + _ => false, + } + .into() +} + +pub(crate) fn fn_winnow(mut v: Vec) -> Variable { + match v.remove(0) { + Variable::Array(a) => a + .into_iter() + .filter(|i| !i.is_empty()) + .collect::>() + .into(), + v => v, + } +} diff --git a/crates/utils/src/expr/functions/email.rs b/crates/utils/src/expr/functions/email.rs new file mode 100644 index 00000000..42a6d318 --- /dev/null +++ b/crates/utils/src/expr/functions/email.rs @@ -0,0 +1,121 @@ +/* + * 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 std::borrow::Cow; + +use crate::expr::Variable; + +pub(crate) fn fn_is_email(v: Vec) -> Variable { + let mut last_ch = 0; + let mut in_quote = false; + let mut at_count = 0; + let mut dot_count = 0; + let mut lp_len = 0; + let mut value = 0; + + for ch in v[0].to_string().bytes() { + match ch { + b'0'..=b'9' + | b'a'..=b'z' + | b'A'..=b'Z' + | b'!' + | b'#' + | b'$' + | b'%' + | b'&' + | b'\'' + | b'*' + | b'+' + | b'-' + | b'/' + | b'=' + | b'?' + | b'^' + | b'_' + | b'`' + | b'{' + | b'|' + | b'}' + | b'~' + | 0x7f..=u8::MAX => { + value += 1; + } + b'.' if !in_quote => { + if last_ch != b'.' && last_ch != b'@' && value != 0 { + value += 1; + if at_count == 1 { + dot_count += 1; + } + } else { + return false.into(); + } + } + b'@' if !in_quote => { + at_count += 1; + lp_len = value; + value = 0; + } + b'>' | b':' | b',' | b' ' if in_quote => { + value += 1; + } + b'\"' if !in_quote || last_ch != b'\\' => { + in_quote = !in_quote; + } + b'\\' if in_quote && last_ch != b'\\' => (), + _ => { + if !in_quote { + return false.into(); + } + } + } + + last_ch = ch; + } + + (at_count == 1 && dot_count > 0 && lp_len > 0 && value > 0).into() +} + +pub(crate) fn fn_email_part(v: Vec) -> Variable { + let mut v = v.into_iter(); + let value = v.next().unwrap(); + let part = v.next().unwrap().into_string(); + + value.transform(|s| match s { + Cow::Borrowed(s) => s + .rsplit_once('@') + .map(|(u, d)| match part.as_ref() { + "local" => Variable::from(u.trim()), + "domain" => Variable::from(d.trim()), + _ => Variable::default(), + }) + .unwrap_or_default(), + Cow::Owned(s) => s + .rsplit_once('@') + .map(|(u, d)| match part.as_ref() { + "local" => Variable::from(u.trim().to_string()), + "domain" => Variable::from(d.trim().to_string()), + _ => Variable::default(), + }) + .unwrap_or_default(), + }) +} diff --git a/crates/utils/src/expr/functions/misc.rs b/crates/utils/src/expr/functions/misc.rs new file mode 100644 index 00000000..6947abf4 --- /dev/null +++ b/crates/utils/src/expr/functions/misc.rs @@ -0,0 +1,67 @@ +/* + * 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 std::net::IpAddr; + +use mail_auth::common::resolver::ToReverseName; + +use crate::expr::Variable; + +pub(crate) fn fn_is_empty(v: Vec) -> Variable { + match &v[0] { + Variable::String(s) => s.is_empty(), + Variable::Integer(_) | Variable::Float(_) => false, + Variable::Array(a) => a.is_empty(), + } + .into() +} + +pub(crate) fn fn_is_number(v: Vec) -> Variable { + matches!(&v[0], Variable::Integer(_) | Variable::Float(_)).into() +} + +pub(crate) fn fn_is_ip_addr(v: Vec) -> Variable { + v[0].to_string().parse::().is_ok().into() +} + +pub(crate) fn fn_is_ipv4_addr(v: Vec) -> Variable { + v[0].to_string() + .parse::() + .map_or(false, |ip| matches!(ip, IpAddr::V4(_))) + .into() +} + +pub(crate) fn fn_is_ipv6_addr(v: Vec) -> Variable { + v[0].to_string() + .parse::() + .map_or(false, |ip| matches!(ip, IpAddr::V6(_))) + .into() +} + +pub(crate) fn fn_ip_reverse_name(v: Vec) -> Variable { + v[0].to_string() + .parse::() + .map(|ip| ip.to_reverse_name()) + .unwrap_or_default() + .into() +} diff --git a/crates/utils/src/expr/functions/mod.rs b/crates/utils/src/expr/functions/mod.rs new file mode 100644 index 00000000..9effffac --- /dev/null +++ b/crates/utils/src/expr/functions/mod.rs @@ -0,0 +1,92 @@ +/* + * 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 std::borrow::Cow; + +use super::Variable; + +pub mod array; +pub mod email; +pub mod misc; +pub mod text; + +impl<'x> Variable<'x> { + fn transform(self, f: impl Fn(Cow<'x, str>) -> Variable<'x>) -> Variable<'x> { + match self { + Variable::String(s) => f(s), + Variable::Array(list) => Variable::Array( + list.into_iter() + .map(|v| match v { + Variable::String(s) => f(s), + v => f(v.into_string()), + }) + .collect::>(), + ), + v => f(v.into_string()), + } + } +} + +#[allow(clippy::type_complexity)] +pub(crate) const FUNCTIONS: &[(&str, fn(Vec) -> Variable, u32)] = &[ + ("count", array::fn_count, 1), + ("sort", array::fn_sort, 2), + ("dedup", array::fn_dedup, 1), + ("winnow", array::fn_winnow, 1), + ("is_intersect", array::fn_is_intersect, 2), + ("is_email", email::fn_is_email, 1), + ("email_part", email::fn_email_part, 2), + ("is_empty", misc::fn_is_empty, 1), + ("is_number", misc::fn_is_number, 1), + ("is_ip_addr", misc::fn_is_ip_addr, 1), + ("is_ipv4_addr", misc::fn_is_ipv4_addr, 1), + ("is_ipv6_addr", misc::fn_is_ipv6_addr, 1), + ("ip_reverse_name", misc::fn_ip_reverse_name, 1), + ("trim", text::fn_trim, 1), + ("trim_end", text::fn_trim_end, 1), + ("trim_start", text::fn_trim_start, 1), + ("len", text::fn_len, 1), + ("to_lowercase", text::fn_to_lowercase, 1), + ("to_uppercase", text::fn_to_uppercase, 1), + ("is_uppercase", text::fn_is_uppercase, 1), + ("is_lowercase", text::fn_is_lowercase, 1), + ("has_digits", text::fn_has_digits, 1), + ("count_spaces", text::fn_count_spaces, 1), + ("count_uppercase", text::fn_count_uppercase, 1), + ("count_lowercase", text::fn_count_lowercase, 1), + ("count_chars", text::fn_count_chars, 1), + ("contains", text::fn_contains, 2), + ("contains_ignore_case", text::fn_contains_ignore_case, 2), + ("eq_ignore_case", text::fn_eq_ignore_case, 2), + ("starts_with", text::fn_starts_with, 2), + ("ends_with", text::fn_ends_with, 2), + ("lines", text::fn_lines, 1), + ("substring", text::fn_substring, 3), + ("strip_prefix", text::fn_strip_prefix, 2), + ("strip_suffix", text::fn_strip_suffix, 2), + ("split", text::fn_split, 2), + ("rsplit", text::fn_rsplit, 2), + ("split_once", text::fn_split_once, 2), + ("rsplit_once", text::fn_rsplit_once, 2), + ("split_words", text::fn_split_words, 1), +]; diff --git a/crates/utils/src/expr/functions/text.rs b/crates/utils/src/expr/functions/text.rs new file mode 100644 index 00000000..9cbce608 --- /dev/null +++ b/crates/utils/src/expr/functions/text.rs @@ -0,0 +1,301 @@ +/* + * 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 std::borrow::Cow; + +use crate::expr::Variable; + +pub(crate) fn fn_trim(mut v: Vec) -> Variable { + v.remove(0).transform(|s| match s { + Cow::Borrowed(s) => Variable::from(s.trim()), + Cow::Owned(s) => Variable::from(s.trim().to_string()), + }) +} + +pub(crate) fn fn_trim_end(mut v: Vec) -> Variable { + v.remove(0).transform(|s| match s { + Cow::Borrowed(s) => Variable::from(s.trim_end()), + Cow::Owned(s) => Variable::from(s.trim_end().to_string()), + }) +} + +pub(crate) fn fn_trim_start(mut v: Vec) -> Variable { + v.remove(0).transform(|s| match s { + Cow::Borrowed(s) => Variable::from(s.trim_start()), + Cow::Owned(s) => Variable::from(s.trim_start().to_string()), + }) +} + +pub(crate) fn fn_len(v: Vec) -> Variable { + match &v[0] { + Variable::String(s) => s.len(), + Variable::Array(a) => a.len(), + v => v.to_string().len(), + } + .into() +} + +pub(crate) fn fn_to_lowercase(mut v: Vec) -> Variable { + v.remove(0).transform(|s| Variable::from(s.to_lowercase())) +} + +pub(crate) fn fn_to_uppercase(mut v: Vec) -> Variable { + v.remove(0).transform(|s| Variable::from(s.to_uppercase())) +} + +pub(crate) fn fn_is_uppercase(mut v: Vec) -> Variable { + v.remove(0).transform(|s| { + s.chars() + .filter(|c| c.is_alphabetic()) + .all(|c| c.is_uppercase()) + .into() + }) +} + +pub(crate) fn fn_is_lowercase(mut v: Vec) -> Variable { + v.remove(0).transform(|s| { + s.chars() + .filter(|c| c.is_alphabetic()) + .all(|c| c.is_lowercase()) + .into() + }) +} + +pub(crate) fn fn_has_digits(mut v: Vec) -> Variable { + v.remove(0) + .transform(|s| s.chars().any(|c| c.is_ascii_digit()).into()) +} + +pub(crate) fn fn_split_words(v: Vec) -> Variable { + v[0].to_string() + .split_whitespace() + .filter(|word| word.chars().all(|c| c.is_alphanumeric())) + .map(|word| Variable::from(word.to_string())) + .collect::>() + .into() +} + +pub(crate) fn fn_count_spaces(v: Vec) -> Variable { + v[0].to_string() + .as_ref() + .chars() + .filter(|c| c.is_whitespace()) + .count() + .into() +} + +pub(crate) fn fn_count_uppercase(v: Vec) -> Variable { + v[0].to_string() + .as_ref() + .chars() + .filter(|c| c.is_alphabetic() && c.is_uppercase()) + .count() + .into() +} + +pub(crate) fn fn_count_lowercase(v: Vec) -> Variable { + v[0].to_string() + .as_ref() + .chars() + .filter(|c| c.is_alphabetic() && c.is_lowercase()) + .count() + .into() +} + +pub(crate) fn fn_count_chars(v: Vec) -> Variable { + v[0].to_string().as_ref().chars().count().into() +} + +pub(crate) fn fn_eq_ignore_case(v: Vec) -> Variable { + v[0].to_string() + .eq_ignore_ascii_case(v[1].to_string().as_ref()) + .into() +} + +pub(crate) fn fn_contains(v: Vec) -> Variable { + match &v[0] { + Variable::String(s) => s.contains(v[1].to_string().as_ref()), + Variable::Array(arr) => arr.contains(&v[1]), + val => val.to_string().contains(v[1].to_string().as_ref()), + } + .into() +} + +pub(crate) fn fn_contains_ignore_case(v: Vec) -> Variable { + let needle = v[1].to_string(); + match &v[0] { + Variable::String(s) => s.to_lowercase().contains(&needle.to_lowercase()), + Variable::Array(arr) => arr.iter().any(|v| match v { + Variable::String(s) => s.eq_ignore_ascii_case(needle.as_ref()), + _ => false, + }), + val => val.to_string().contains(needle.as_ref()), + } + .into() +} + +pub(crate) fn fn_starts_with(v: Vec) -> Variable { + v[0].to_string() + .starts_with(v[1].to_string().as_ref()) + .into() +} + +pub(crate) fn fn_ends_with(v: Vec) -> Variable { + v[0].to_string().ends_with(v[1].to_string().as_ref()).into() +} + +pub(crate) fn fn_lines(mut v: Vec) -> Variable { + match v.remove(0) { + Variable::String(s) => s + .lines() + .map(|s| Variable::from(s.to_string())) + .collect::>() + .into(), + val => val, + } +} + +pub(crate) fn fn_substring(v: Vec) -> Variable { + v[0].to_string() + .chars() + .skip(v[1].to_usize().unwrap_or_default()) + .take(v[2].to_usize().unwrap_or_default()) + .collect::() + .into() +} + +pub(crate) fn fn_strip_prefix(v: Vec) -> Variable { + let mut v = v.into_iter(); + let value = v.next().unwrap(); + let prefix = v.next().unwrap().into_string(); + + value.transform(|s| match s { + Cow::Borrowed(s) => s + .strip_prefix(prefix.as_ref()) + .map(Variable::from) + .unwrap_or_default(), + Cow::Owned(s) => s + .strip_prefix(prefix.as_ref()) + .map(|s| Variable::from(s.to_string())) + .unwrap_or_default(), + }) +} + +pub(crate) fn fn_strip_suffix(v: Vec) -> Variable { + let mut v = v.into_iter(); + let value = v.next().unwrap(); + let suffix = v.next().unwrap().into_string(); + + value.transform(|s| match s { + Cow::Borrowed(s) => s + .strip_suffix(suffix.as_ref()) + .map(Variable::from) + .unwrap_or_default(), + Cow::Owned(s) => s + .strip_suffix(suffix.as_ref()) + .map(|s| Variable::from(s.to_string())) + .unwrap_or_default(), + }) +} + +pub(crate) fn fn_split(v: Vec) -> Variable { + let mut v = v.into_iter(); + let value = v.next().unwrap().into_string(); + let arg = v.next().unwrap().into_string(); + + match value { + Cow::Borrowed(s) => s + .split(arg.as_ref()) + .map(Variable::from) + .collect::>() + .into(), + Cow::Owned(s) => s + .split(arg.as_ref()) + .map(|s| Variable::from(s.to_string())) + .collect::>() + .into(), + } +} + +pub(crate) fn fn_rsplit(v: Vec) -> Variable { + let mut v = v.into_iter(); + let value = v.next().unwrap().into_string(); + let arg = v.next().unwrap().into_string(); + + match value { + Cow::Borrowed(s) => s + .rsplit(arg.as_ref()) + .map(Variable::from) + .collect::>() + .into(), + Cow::Owned(s) => s + .rsplit(arg.as_ref()) + .map(|s| Variable::from(s.to_string())) + .collect::>() + .into(), + } +} + +pub(crate) fn fn_split_once(v: Vec) -> Variable { + let mut v = v.into_iter(); + let value = v.next().unwrap().into_string(); + let arg = v.next().unwrap().into_string(); + + match value { + Cow::Borrowed(s) => s + .split_once(arg.as_ref()) + .map(|(a, b)| Variable::Array(vec![Variable::from(a), Variable::from(b)])) + .unwrap_or_default(), + Cow::Owned(s) => s + .split_once(arg.as_ref()) + .map(|(a, b)| { + Variable::Array(vec![ + Variable::from(a.to_string()), + Variable::from(b.to_string()), + ]) + }) + .unwrap_or_default(), + } +} + +pub(crate) fn fn_rsplit_once(v: Vec) -> Variable { + let mut v = v.into_iter(); + let value = v.next().unwrap().into_string(); + let arg = v.next().unwrap().into_string(); + + match value { + Cow::Borrowed(s) => s + .rsplit_once(arg.as_ref()) + .map(|(a, b)| Variable::Array(vec![Variable::from(a), Variable::from(b)])) + .unwrap_or_default(), + Cow::Owned(s) => s + .rsplit_once(arg.as_ref()) + .map(|(a, b)| { + Variable::Array(vec![ + Variable::from(a.to_string()), + Variable::from(b.to_string()), + ]) + }) + .unwrap_or_default(), + } +} diff --git a/crates/utils/src/expr/mod.rs b/crates/utils/src/expr/mod.rs new file mode 100644 index 00000000..18db2710 --- /dev/null +++ b/crates/utils/src/expr/mod.rs @@ -0,0 +1,308 @@ +/* + * Copyright (c) 2020-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 std::borrow::Cow; + +use regex::Regex; + +use crate::config::utils::AsKey; + +use self::{parser::ExpressionParser, tokenizer::Tokenizer}; + +pub mod eval; +pub mod functions; +pub mod parser; +pub mod tokenizer; + +#[derive(Debug, PartialEq, Eq, Clone, Default)] +pub struct Expression { + pub items: Vec, +} + +#[derive(Debug, Clone)] +pub enum ExpressionItem { + Variable(u32), + Capture(u32), + Constant(Constant), + BinaryOperator(BinaryOperator), + UnaryOperator(UnaryOperator), + Regex(Regex), + JmpIf { val: bool, pos: u32 }, + Function { id: u32, num_args: u32 }, + ArrayAccess, + ArrayBuild(u32), +} + +#[derive(Debug)] +pub enum Variable<'x> { + String(Cow<'x, str>), + Integer(i64), + Float(f64), + Array(Vec>), +} + +impl Default for Variable<'_> { + fn default() -> Self { + Variable::Integer(0) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Constant { + Integer(i64), + Float(f64), + String(String), +} + +impl Eq for Constant {} + +impl From for Constant { + fn from(value: String) -> Self { + Constant::String(value) + } +} + +impl From for Constant { + fn from(value: bool) -> Self { + Constant::Integer(value as i64) + } +} + +impl From for Constant { + fn from(value: i64) -> Self { + Constant::Integer(value) + } +} + +impl From for Constant { + fn from(value: i32) -> Self { + Constant::Integer(value as i64) + } +} + +impl From for Constant { + fn from(value: i16) -> Self { + Constant::Integer(value as i64) + } +} + +impl From for Constant { + fn from(value: f64) -> Self { + Constant::Float(value) + } +} + +impl From for Constant { + fn from(value: usize) -> Self { + Constant::Integer(value as i64) + } +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum BinaryOperator { + Add, + Subtract, + Multiply, + Divide, + + And, + Or, + Xor, + + Eq, + Ne, + Lt, + Le, + Gt, + Ge, +} + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub enum UnaryOperator { + Not, + Minus, +} + +#[derive(Debug, Clone)] +pub enum Token { + Variable(u32), + Capture(u32), + Function { + name: Cow<'static, str>, + id: u32, + num_args: u32, + }, + Constant(Constant), + Regex(Regex), + BinaryOperator(BinaryOperator), + UnaryOperator(UnaryOperator), + OpenParen, + CloseParen, + OpenBracket, + CloseBracket, + Comma, +} + +impl From for Variable<'_> { + fn from(value: usize) -> Self { + Variable::Integer(value as i64) + } +} + +impl From for Variable<'_> { + fn from(value: i64) -> Self { + Variable::Integer(value) + } +} + +impl From for Variable<'_> { + fn from(value: i32) -> Self { + Variable::Integer(value as i64) + } +} + +impl From for Variable<'_> { + fn from(value: i16) -> Self { + Variable::Integer(value as i64) + } +} + +impl From for Variable<'_> { + fn from(value: f64) -> Self { + Variable::Float(value) + } +} + +impl<'x> From<&'x str> for Variable<'x> { + fn from(value: &'x str) -> Self { + Variable::String(Cow::Borrowed(value)) + } +} + +impl From for Variable<'_> { + fn from(value: String) -> Self { + Variable::String(Cow::Owned(value)) + } +} + +impl<'x> From>> for Variable<'x> { + fn from(value: Vec>) -> Self { + Variable::Array(value) + } +} + +impl From for Variable<'_> { + fn from(value: bool) -> Self { + Variable::Integer(value as i64) + } +} + +impl Expression { + pub fn parse( + key: impl AsKey, + expr: &str, + token_map: impl Fn(&str) -> Result, + ) -> crate::config::Result { + ExpressionParser::new(Tokenizer::new(expr, token_map)) + .parse() + .map_err(|e| { + format!( + "Failed to parse expression {:?} for key {:?}: {}", + expr, + key.as_key(), + e + ) + }) + } +} + +impl> From for Expression { + fn from(value: T) -> Self { + Expression { + items: vec![ExpressionItem::Constant(value.into())], + } + } +} + +impl PartialEq for ExpressionItem { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Variable(l0), Self::Variable(r0)) => l0 == r0, + (Self::Constant(l0), Self::Constant(r0)) => l0 == r0, + (Self::BinaryOperator(l0), Self::BinaryOperator(r0)) => l0 == r0, + (Self::UnaryOperator(l0), Self::UnaryOperator(r0)) => l0 == r0, + (Self::Regex(_), Self::Regex(_)) => true, + ( + Self::JmpIf { + val: l_val, + pos: l_pos, + }, + Self::JmpIf { + val: r_val, + pos: r_pos, + }, + ) => l_val == r_val && l_pos == r_pos, + ( + Self::Function { + id: l_id, + num_args: l_num_args, + }, + Self::Function { + id: r_id, + num_args: r_num_args, + }, + ) => l_id == r_id && l_num_args == r_num_args, + (Self::ArrayBuild(l0), Self::ArrayBuild(r0)) => l0 == r0, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + +impl Eq for ExpressionItem {} + +impl PartialEq for Token { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (Self::Variable(l0), Self::Variable(r0)) => l0 == r0, + ( + Self::Function { + name: l_name, + id: l_id, + num_args: l_num_args, + }, + Self::Function { + name: r_name, + id: r_id, + num_args: r_num_args, + }, + ) => l_name == r_name && l_id == r_id && l_num_args == r_num_args, + (Self::Constant(l0), Self::Constant(r0)) => l0 == r0, + (Self::Regex(_), Self::Regex(_)) => true, + (Self::BinaryOperator(l0), Self::BinaryOperator(r0)) => l0 == r0, + (Self::UnaryOperator(l0), Self::UnaryOperator(r0)) => l0 == r0, + _ => core::mem::discriminant(self) == core::mem::discriminant(other), + } + } +} + +impl Eq for Token {} diff --git a/crates/utils/src/expr/parser.rs b/crates/utils/src/expr/parser.rs new file mode 100644 index 00000000..32730133 --- /dev/null +++ b/crates/utils/src/expr/parser.rs @@ -0,0 +1,293 @@ +/* + * Copyright (c) 2020-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 super::{tokenizer::Tokenizer, BinaryOperator, Expression, ExpressionItem, Token}; + +pub struct ExpressionParser<'x, F> +where + F: Fn(&str) -> Result, +{ + pub(crate) tokenizer: Tokenizer<'x, F>, + pub(crate) output: Vec, + operator_stack: Vec<(Token, Option)>, + arg_count: Vec, +} + +pub(crate) const ID_ARRAY_ACCESS: u32 = u32::MAX; +pub(crate) const ID_ARRAY_BUILD: u32 = u32::MAX - 1; + +impl<'x, F> ExpressionParser<'x, F> +where + F: Fn(&str) -> Result, +{ + pub fn new(tokenizer: Tokenizer<'x, F>) -> Self { + Self { + tokenizer, + output: Vec::new(), + operator_stack: Vec::new(), + arg_count: Vec::new(), + } + } + + pub fn parse(mut self) -> Result { + let mut last_is_var_or_fnc = false; + + while let Some(token) = self.tokenizer.next()? { + let mut is_var_or_fnc = false; + match token { + Token::Variable(v) => { + self.inc_arg_count(); + is_var_or_fnc = true; + self.output.push(ExpressionItem::Variable(v)) + } + Token::Constant(c) => { + self.inc_arg_count(); + self.output.push(ExpressionItem::Constant(c)) + } + Token::Capture(c) => { + self.inc_arg_count(); + self.output.push(ExpressionItem::Capture(c)) + } + Token::UnaryOperator(uop) => { + self.operator_stack.push((Token::UnaryOperator(uop), None)) + } + Token::OpenParen => self.operator_stack.push((token, None)), + Token::CloseParen | Token::CloseBracket => { + let expect_token = if matches!(token, Token::CloseParen) { + Token::OpenParen + } else { + Token::OpenBracket + }; + loop { + match self.operator_stack.pop() { + Some((t, _)) if t == expect_token => { + break; + } + Some((Token::BinaryOperator(bop), jmp_pos)) => { + self.update_jmp_pos(jmp_pos); + self.output.push(ExpressionItem::BinaryOperator(bop)) + } + Some((Token::UnaryOperator(uop), _)) => { + self.output.push(ExpressionItem::UnaryOperator(uop)) + } + _ => return Err("Mismatched parentheses".to_string()), + } + } + + match self.operator_stack.last() { + Some((Token::Function { id, num_args, name }, _)) => { + let got_args = self.arg_count.pop().unwrap(); + if got_args != *num_args as i32 { + return Err(if *id != u32::MAX { + format!( + "Expression function {:?} expected {} arguments, got {}", + name, num_args, got_args + ) + } else { + "Missing array index".to_string() + }); + } + + let expr = match *id { + ID_ARRAY_ACCESS => ExpressionItem::ArrayAccess, + ID_ARRAY_BUILD => ExpressionItem::ArrayBuild(*num_args), + id => ExpressionItem::Function { + id, + num_args: *num_args, + }, + }; + + self.operator_stack.pop(); + self.output.push(expr); + } + Some((Token::Regex(regex), _)) => { + if self.arg_count.pop().unwrap() != 1 { + return Err("Expression function \"matches\" expected 2 arguments" + .to_string()); + } + self.output.push(ExpressionItem::Regex(regex.clone())); + self.operator_stack.pop(); + } + _ => {} + } + + is_var_or_fnc = true; + } + Token::BinaryOperator(bop) => { + self.dec_arg_count(); + while let Some((top_token, prev_jmp_pos)) = self.operator_stack.last() { + match top_token { + Token::BinaryOperator(top_bop) => { + if bop.precedence() <= top_bop.precedence() { + let top_bop = *top_bop; + let jmp_pos = *prev_jmp_pos; + self.update_jmp_pos(jmp_pos); + self.operator_stack.pop(); + self.output.push(ExpressionItem::BinaryOperator(top_bop)); + } else { + break; + } + } + Token::UnaryOperator(top_uop) => { + let top_uop = *top_uop; + self.operator_stack.pop(); + self.output.push(ExpressionItem::UnaryOperator(top_uop)); + } + _ => break, + } + } + + // Add jump instruction for short-circuiting + let jmp_pos = match bop { + BinaryOperator::And => { + self.output + .push(ExpressionItem::JmpIf { val: false, pos: 0 }); + Some(self.output.len() - 1) + } + BinaryOperator::Or => { + self.output + .push(ExpressionItem::JmpIf { val: true, pos: 0 }); + Some(self.output.len() - 1) + } + _ => None, + }; + + self.operator_stack + .push((Token::BinaryOperator(bop), jmp_pos)); + } + Token::Function { id, name, num_args } => { + self.inc_arg_count(); + self.arg_count.push(0); + self.operator_stack + .push((Token::Function { id, name, num_args }, None)) + } + Token::Regex(regex) => { + self.inc_arg_count(); + self.arg_count.push(0); + self.operator_stack.push((Token::Regex(regex), None)) + } + Token::OpenBracket => { + // Array functions + let (id, num_args, arg_count) = if last_is_var_or_fnc { + (ID_ARRAY_ACCESS, 2, 1) + } else { + self.inc_arg_count(); + (ID_ARRAY_BUILD, 0, 0) + }; + self.arg_count.push(arg_count); + self.operator_stack.push(( + Token::Function { + id, + name: "array".into(), + num_args, + }, + None, + )); + self.operator_stack.push((token, None)); + } + Token::Comma => { + while let Some((token, jmp_pos)) = self.operator_stack.last() { + match token { + Token::OpenParen => break, + Token::BinaryOperator(bop) => { + let bop = *bop; + let jmp_pos = *jmp_pos; + self.update_jmp_pos(jmp_pos); + self.output.push(ExpressionItem::BinaryOperator(bop)); + self.operator_stack.pop(); + } + Token::UnaryOperator(uop) => { + self.output.push(ExpressionItem::UnaryOperator(*uop)); + self.operator_stack.pop(); + } + _ => break, + } + } + } + } + last_is_var_or_fnc = is_var_or_fnc; + } + + while let Some((token, jmp_pos)) = self.operator_stack.pop() { + match token { + Token::BinaryOperator(bop) => { + self.update_jmp_pos(jmp_pos); + self.output.push(ExpressionItem::BinaryOperator(bop)) + } + Token::UnaryOperator(uop) => self.output.push(ExpressionItem::UnaryOperator(uop)), + _ => return Err("Invalid token on the operator stack".to_string()), + } + } + + if self.operator_stack.is_empty() { + Ok(Expression { items: self.output }) + } else { + Err("Invalid expression".to_string()) + } + } + + fn inc_arg_count(&mut self) { + if let Some(x) = self.arg_count.last_mut() { + *x = x.saturating_add(1); + let op_pos = self.operator_stack.len().saturating_sub(2); + match self.operator_stack.get_mut(op_pos) { + Some((Token::Function { num_args, id, .. }, _)) if *id == ID_ARRAY_BUILD => { + *num_args += 1; + } + _ => {} + } + } + } + + fn dec_arg_count(&mut self) { + if let Some(x) = self.arg_count.last_mut() { + *x = x.saturating_sub(1); + } + } + + fn update_jmp_pos(&mut self, jmp_pos: Option) { + if let Some(jmp_pos) = jmp_pos { + let cur_pos = self.output.len(); + if let ExpressionItem::JmpIf { pos, .. } = &mut self.output[jmp_pos] { + *pos = (cur_pos - jmp_pos) as u32; + } else { + #[cfg(test)] + panic!("Invalid jump position"); + } + } + } +} + +impl BinaryOperator { + fn precedence(&self) -> i32 { + match self { + BinaryOperator::Multiply | BinaryOperator::Divide => 7, + BinaryOperator::Add | BinaryOperator::Subtract => 6, + BinaryOperator::Gt | BinaryOperator::Ge | BinaryOperator::Lt | BinaryOperator::Le => 5, + BinaryOperator::Eq | BinaryOperator::Ne => 4, + BinaryOperator::Xor => 3, + BinaryOperator::And => 2, + BinaryOperator::Or => 1, + } + } +} diff --git a/crates/utils/src/expr/tokenizer.rs b/crates/utils/src/expr/tokenizer.rs new file mode 100644 index 00000000..cb08cf6a --- /dev/null +++ b/crates/utils/src/expr/tokenizer.rs @@ -0,0 +1,338 @@ +/* + * Copyright (c) 2020-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 std::{borrow::Cow, iter::Peekable, slice::Iter}; + +use regex::Regex; +use smtp_proto::IntoString; + +use super::{functions::FUNCTIONS, BinaryOperator, Constant, Token, UnaryOperator}; + +pub struct Tokenizer<'x, F> +where + F: Fn(&str) -> Result, +{ + pub(crate) iter: Peekable>, + token_map: F, + buf: Vec, + depth: u32, + next_token: Vec, + has_number: bool, + has_dot: bool, + has_alpha: bool, + is_start: bool, + is_eof: bool, +} + +impl<'x, F> Tokenizer<'x, F> +where + F: Fn(&str) -> Result, +{ + #[allow(clippy::should_implement_trait)] + pub fn new(expr: &'x str, token_map: F) -> Self { + Self { + iter: expr.as_bytes().iter().peekable(), + buf: Vec::new(), + depth: 0, + next_token: Vec::with_capacity(2), + has_number: false, + has_dot: false, + has_alpha: false, + is_start: true, + is_eof: false, + token_map, + } + } + + #[allow(clippy::should_implement_trait)] + pub fn next(&mut self) -> Result, String> { + if let Some(token) = self.next_token.pop() { + return Ok(Some(token)); + } else if self.is_eof { + return Ok(None); + } + + while let Some(&ch) = self.iter.next() { + match ch { + b'A'..=b'Z' | b'a'..=b'z' | b'_' | b'$' => { + self.buf.push(ch); + self.has_alpha = true; + } + b'0'..=b'9' => { + self.buf.push(ch); + self.has_number = true; + } + b'.' => { + self.buf.push(ch); + self.has_dot = true; + } + b'}' => { + self.is_eof = true; + break; + } + b'-' if self.buf.last().map_or(false, |c| *c == b'[') => { + self.buf.push(ch); + } + b':' if self.buf.contains(&b'.') => { + self.buf.push(ch); + } + b']' if self.buf.contains(&b'[') => { + self.buf.push(b']'); + } + b'*' if self.buf.last().map_or(false, |&c| c == b'[' || c == b'.') => { + self.buf.push(ch); + } + _ => { + let (prev_token, ch) = if ch == b'(' && self.buf.eq(b"matches") { + // Parse regular expressions + let stop_ch = self.find_char(&[b'\"', b'\''])?; + let regex_str = self.parse_string(stop_ch)?; + let regex = Regex::new(®ex_str).map_err(|e| { + format!("Invalid regular expression {:?}: {}", regex_str, e) + })?; + self.has_alpha = false; + self.buf.clear(); + self.find_char(&[b','])?; + (Token::Regex(regex).into(), b'(') + } else if !self.buf.is_empty() { + self.is_start = false; + (self.parse_buf()?.into(), ch) + } else { + (None, ch) + }; + let token = match ch { + b'&' => { + if matches!(self.iter.peek(), Some(b'&')) { + self.iter.next(); + } + Token::BinaryOperator(BinaryOperator::And) + } + b'|' => { + if matches!(self.iter.peek(), Some(b'|')) { + self.iter.next(); + } + Token::BinaryOperator(BinaryOperator::Or) + } + b'!' => { + if matches!(self.iter.peek(), Some(b'=')) { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Ne) + } else { + Token::UnaryOperator(UnaryOperator::Not) + } + } + b'^' => Token::BinaryOperator(BinaryOperator::Xor), + b'(' => { + self.depth += 1; + Token::OpenParen + } + b')' => { + if self.depth == 0 { + return Err("Unmatched close parenthesis".to_string()); + } + self.depth -= 1; + Token::CloseParen + } + b'+' => Token::BinaryOperator(BinaryOperator::Add), + b'*' => Token::BinaryOperator(BinaryOperator::Multiply), + b'/' => Token::BinaryOperator(BinaryOperator::Divide), + b'-' => { + if self.is_start { + Token::UnaryOperator(UnaryOperator::Minus) + } else { + Token::BinaryOperator(BinaryOperator::Subtract) + } + } + b'=' => match self.iter.next() { + Some(b'=') => Token::BinaryOperator(BinaryOperator::Eq), + Some(b'>') => Token::BinaryOperator(BinaryOperator::Ge), + Some(b'<') => Token::BinaryOperator(BinaryOperator::Le), + _ => Token::BinaryOperator(BinaryOperator::Eq), + }, + b'>' => match self.iter.peek() { + Some(b'=') => { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Ge) + } + _ => Token::BinaryOperator(BinaryOperator::Gt), + }, + b'<' => match self.iter.peek() { + Some(b'=') => { + self.iter.next(); + Token::BinaryOperator(BinaryOperator::Le) + } + _ => Token::BinaryOperator(BinaryOperator::Lt), + }, + b',' => Token::Comma, + b'[' => Token::OpenBracket, + b']' => Token::CloseBracket, + b' ' | b'\r' | b'\n' => { + if prev_token.is_some() { + return Ok(prev_token); + } else { + continue; + } + } + b'\"' | b'\'' => Token::Constant(Constant::String(self.parse_string(ch)?)), + _ => { + return Err(format!("Invalid character {:?}", char::from(ch),)); + } + }; + self.is_start = matches!( + token, + Token::OpenParen | Token::Comma | Token::BinaryOperator(_) + ); + + return if prev_token.is_some() { + self.next_token.push(token); + Ok(prev_token) + } else { + Ok(Some(token)) + }; + } + } + } + + if self.depth > 0 { + Err("Unmatched open parenthesis".to_string()) + } else if !self.buf.is_empty() { + self.parse_buf().map(Some) + } else { + Ok(None) + } + } + + fn find_char(&mut self, chars: &[u8]) -> Result { + for &ch in self.iter.by_ref() { + if !ch.is_ascii_whitespace() { + return if chars.contains(&ch) { + Ok(ch) + } else { + Err(format!( + "Expected {:?}, found invalid character {:?}", + char::from(chars[0]), + char::from(ch), + )) + }; + } + } + + Err("Unexpected end of expression".to_string()) + } + + fn parse_string(&mut self, stop_ch: u8) -> Result { + let mut buf = Vec::with_capacity(16); + let mut last_ch = 0; + let mut found_end = false; + + for &ch in self.iter.by_ref() { + if last_ch != b'\\' { + if ch != stop_ch { + buf.push(ch); + } else { + found_end = true; + break; + } + } else { + match ch { + b'n' => { + buf.push(b'\n'); + } + b'r' => { + buf.push(b'\r'); + } + b't' => { + buf.push(b'\t'); + } + _ => { + buf.push(ch); + } + } + } + + last_ch = ch; + } + + if found_end { + String::from_utf8(buf).map_err(|_| "Invalid UTF-8".to_string()) + } else { + Err("Unterminated string".to_string()) + } + } + + fn parse_buf(&mut self) -> Result { + let buf = std::mem::take(&mut self.buf).into_string(); + if self.has_number && !self.has_alpha { + self.has_number = false; + if self.has_dot { + self.has_dot = false; + + buf.parse::() + .map(|f| Token::Constant(Constant::Float(f))) + .map_err(|_| format!("Invalid float value {}", buf,)) + } else { + buf.parse::() + .map(|i| Token::Constant(Constant::Integer(i))) + .map_err(|_| format!("Invalid integer value {}", buf,)) + } + } else { + let has_dot = self.has_dot; + let has_number = self.has_number; + + self.has_alpha = false; + self.has_number = false; + self.has_dot = false; + + if !has_number && !has_dot && [4, 5].contains(&buf.len()) { + if buf == "true" { + return Ok(Token::Constant(Constant::Integer(1))); + } else if buf == "false" { + return Ok(Token::Constant(Constant::Integer(0))); + } + } + + if let Some(regex_capture) = buf.strip_prefix('$').and_then(|v| v.parse::().ok()) { + Ok(Token::Capture(regex_capture)) + } else if let Some((idx, (name, _, num_args))) = FUNCTIONS + .iter() + .enumerate() + .find(|(_, (name, _, _))| name == &buf) + { + Ok(Token::Function { + name: Cow::Borrowed(*name), + id: idx as u32, + num_args: *num_args, + }) + } else { + (self.token_map)(&buf).map(|t| match t { + Token::Function { name, id, num_args } => Token::Function { + name, + id: id + FUNCTIONS.len() as u32, + num_args, + }, + t => t, + }) + } + } + } +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 03371ee8..961fac56 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -28,6 +28,7 @@ use config::Config; pub mod acme; pub mod codec; pub mod config; +pub mod expr; pub mod ipc; pub mod listener; pub mod map; diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 8a090366..f5329a84 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -6,7 +6,7 @@ resolver = "2" [features] #default = ["sqlite", "foundationdb", "postgres", "mysql", "rocks", "elastic", "s3", "redis"] -default = ["sqlite", "foundationdb", "postgres", "mysql", "rocks", "elastic", "s3", "redis"] +default = ["sqlite", "postgres", "mysql"] sqlite = ["store/sqlite"] foundationdb = ["store/foundation"] postgres = ["store/postgres"] diff --git a/tests/resources/smtp/config/if-blocks.toml b/tests/resources/smtp/config/if-blocks.toml index 05bd7507..ad70d794 100644 --- a/tests/resources/smtp/config/if-blocks.toml +++ b/tests/resources/smtp/config/if-blocks.toml @@ -1,40 +1,34 @@ durations = [ - {if = "sender", eq = "jdoe", then = "5d"}, - {any-of = [{if = "priority", eq = -1}, {if = "rcpt", starts-with = "jane"}], then = "1h"}, + {if = "sender = 'jdoe'", then = "5d"}, + {if = "priority = -1 | starts_with(rcpt, 'jane')", then = "1h"}, {else = false} ] string-list = [ - {if = "sender", eq = "jdoe", then = ["From", "To", "Date"]}, - {any-of = [{if = "priority", eq = -1}, {if = "rcpt", starts-with = "jane"}], then = "Other-ID"}, - {else = []} + {if = "sender = 'jdoe'", then = "['From', 'To', 'Date']"}, + {if = "priority = -1 | starts_with(rcpt, 'jane')", then = "'Other-ID'"}, + {else = "[]"} ] string-list-bis = [ - {if = "sender", eq = "jdoe", then = ["From", "To", "Date"]}, - {any-of = [{if = "priority", eq = -1}, {if = "rcpt", starts-with = "jane"}], then = []}, - {else = ["ID-Bis"]} + {if = "sender = 'jdoe'", then = "['From', 'To', 'Date']"}, + {if = "priority = -1 | starts_with(rcpt, 'jane')", then = "[]"}, + {else = "['ID-Bis']"} ] -single-value = "hello world" - -bad-multi-value = [ - {if = "sender", eq = "jdoe", then = 100}, - {any-of = [{if = "priority", eq = -1}, {if = "rcpt", starts-with = "jane"}], then = [1, 2, 3]}, - {else = 2} -] +single-value = "'hello world'" bad-if-without-then = [ - {if = "sender", eq = "jdoe"}, + {if = "sender = 'jdoe'"}, {else = 1} ] bad-if-without-else = [ - {if = "sender", eq = "jdoe", then = 1} + {if = "sender = 'jdoe'", then = 1} ] bad-multiple-else = [ - {if = "sender", eq = "jdoe", then = 1}, + {if = "sender = 'jdoe'", then = 1}, {else = 1}, {else = 2} ] diff --git a/tests/resources/smtp/config/rules-dynvalue.toml b/tests/resources/smtp/config/rules-dynvalue.toml index 1f83e6c6..e38aa8d6 100644 --- a/tests/resources/smtp/config/rules-dynvalue.toml +++ b/tests/resources/smtp/config/rules-dynvalue.toml @@ -9,96 +9,55 @@ remote-ip = "A:B:C::D:E" mx = "mx.somedomain.com" authenticated-as = "john@foobar.org" priority = -4 -listener = 123 +listener = "smtp" helo-domain = "hi-domain.net" [eval."eq"] test = [ - {if = "sender", eq = "bill@foo.net", then = "${0}"}, + {if = "sender = 'bill@foo.net'", then = "sender"}, {else = false} ] expect = "bill@foo.net" [eval."starts-with"] test = [ - {if = "rcpt-domain", starts-with = "foo", then = "${0}${{0}}"}, + {if = "starts_with(rcpt_domain, 'foo')", then = "'mx.' + rcpt_domain"}, {else = false} ] -expect = "foo.example.org${0}" +expect = "mx.foo.example.org" [eval."regex"] test = [ - {if = "rcpt", matches = "^([^.]+)@([^.]+)\.(.+)$", then = "${1}+${2}@${3}"}, + {if = "matches('^([^.]+)@([^.]+)\.(.+)$', rcpt)", then = "$1 + '+' + $2 + '@' + $3"}, {else = false} ] expect = "user+foo@example.org" [eval."regex-full"] test = [ - {if = "rcpt", matches = "^([^.]+)@([^.]+)\.(.+)$", then = "${0}"}, + {if = "matches('^([^.]+)@([^.]+)\.(.+)$', rcpt)", then = "rcpt"}, {else = false} ] expect = "user@foo.example.org" [eval."envelope-match"] test = [ - {if = "authenticated-as", matches = "^([^.]+)@(.+)$", then = "rcpt ${rcpt} listener ${listener} ip ${local-ip} priority ${priority}"}, + {if = "matches('^([^.]+)@(.+)$', authenticated_as)", then = "'rcpt ' + rcpt + ' listener ' + listener + ' ip ' + local_ip + ' priority ' + priority"}, {else = false} ] -expect = "rcpt user@foo.example.org listener 123 ip 192.168.9.3 priority -4" +expect = "rcpt user@foo.example.org listener smtp ip 192.168.9.3 priority -4" [eval."static-match"] test = [ - {if = "authenticated-as", matches = "^([^.]+)@(.+)$", then = "hello world"}, + {if = "matches('^([^.]+)@(.+)$', authenticated_as)", then = "'hello world'"}, {else = false} ] expect = "hello world" [eval."no-match"] test = [ - {if = "authenticated-as", matches = "^([^.]+)@([^.]+)\.(.+)$org", then = "${1}+${2}@${3}"}, + {if = "matches('^([^.]+)@([^.]+)\.(.+)$org', authenticated_as)", then = "'test'"}, {else = false} ] expect = false -[store."list_mx/domains"] -type = "memory" -format = "list" -values = ["mx"] - -[store."list_foo/domains"] -type = "memory" -format = "list" -values = ["foo"] - -[store."list_123/domains"] -type = "memory" -format = "list" -values = ["123"] - -[maybe-eval."dyn_mx"] -test = [ - {if = "mx", matches = "([^.]+)\.(.+)$", then = "list_${1}/domains"}, - {else = false} -] -expect = "mx" - -[maybe-eval."dyn_foo"] -test = [ - {if = "sender-domain", matches = "([^.]+)\.(.+)$", then = "list_${1}/domains"}, - {else = false} -] -expect = "foo" - -[maybe-eval."static_mx"] -test = "list_mx/domains" -expect = "mx" - -[maybe-eval."static_foo"] -test = "list_foo/domains" -expect = "foo" - -[maybe-eval."dyn_123"] -test = "list_${listener}/domains" -expect = "123" - diff --git a/tests/resources/smtp/config/rules-eval.toml b/tests/resources/smtp/config/rules-eval.toml index 1a658753..e74c7dcd 100644 --- a/tests/resources/smtp/config/rules-eval.toml +++ b/tests/resources/smtp/config/rules-eval.toml @@ -8,163 +8,27 @@ remote-ip = "A:B:C::D:E" mx = "mx.somedomain.com" authenticated-as = "john@foobar.org" priority = -4 -listener = 123 +listener = "smtp" helo-domain = "hi-domain.net" [rule] -"eq-true" = {if = "rcpt-domain", eq = "example.org"} -"eq-false" = {if = "rcpt-domain", eq = "example.com"} -"listener-eq-true" = {if = "listener", eq = "smtp"} -"listener-eq-false" = {if = "listener", eq = "smtps"} -"ip-eq-true" = {if = "local-ip", eq = "192.168.9.0/24"} -"ip-eq-false" = {if = "remote-ip", eq = "A:B:C::D:F/128"} -"ne-true" = {if = "authenticated-as", ne = ""} -"ne-false" = {if = "authenticated-as", ne = "john@foobar.org"} -"starts-with-true" = {if = "mx", starts-with = "mx.some"} -"starts-with-false" = {if = "mx", starts-with = "enchilada"} -"ends-with-true" = {if = "sender", ends-with = "@foo.net"} -"ends-with-false" = {if = "sender", ends-with = "chimichanga"} -"in-list-true" = {if = "sender-domain", in-list = "list/domains"} -"in-list-false" = {if = "rcpt-domain", in-list = "list/domains"} -"not-in-list-true" = {if = "rcpt-domain", not-in-list = "list/domains"} -"not-in-list-false" = {if = "sender-domain", not-in-list = "list/domains"} -"regex-true" = {if = "sender", matches = "^(.+)@(.+)$"} -"regex-false" = {if = "mx", matches = "/^\\S+@\\S+\\.\\S+$/"} - -"any-of-true" = { any-of = [ - {if = "authenticated-as", ne = "john@foobar.org"}, - {if = "rcpt-domain", eq = "example.org"}, - {if = "mx", starts-with = "mx.some"}, -]} -"any-of-false" = { any-of = [ - {if = "authenticated-as", eq = "something else"}, - {if = "rcpt-domain", eq = "something else"}, - {if = "mx", starts-with = "something else"}, -]} -"all-of-true" = { all-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} -]} -"all-of-false" = { all-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "something else"} -]} -"none-of-true" = { none-of = [ - {if = "authenticated-as", eq = "something else"}, - {if = "rcpt-domain", eq = "something else"}, - {if = "mx", starts-with = "something else"}, -]} -"none-of-false" = { none-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} -]} -nested-any-of-true = { any-of = [ - { all-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "something else"} - ]}, - { none-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} - ]}, - { any-of = [ - {if = "authenticated-as", ne = "john@foobar.org"}, - {if = "rcpt-domain", eq = "example.org"}, - {if = "mx", starts-with = "mx.some"}, - ]} -]} -nested-any-of-false = { any-of = [ - { none-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} - ]}, - { all-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "something else"} - ]}, - { any-of = [ - {if = "authenticated-as", eq = "something else"}, - {if = "rcpt-domain", eq = "something else"}, - {if = "mx", starts-with = "something else"}, - ]} -]} -nested-all-of-true = { all-of = [ - { any-of = [ - {if = "authenticated-as", ne = "john@foobar.org"}, - {if = "rcpt-domain", eq = "example.org"}, - {if = "mx", starts-with = "mx.some"}, - ]}, - { all-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} - ]}, - { none-of = [ - {if = "authenticated-as", eq = "something else"}, - {if = "rcpt-domain", eq = "something else"}, - {if = "mx", starts-with = "something else"}, - ]} -]} -nested-all-of-false = { all-of = [ - { any-of = [ - {if = "authenticated-as", ne = "john@foobar.org"}, - {if = "rcpt-domain", eq = "example.org"}, - {if = "mx", starts-with = "mx.some"}, - ]}, - { all-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} - ]}, - { none-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} - ]} -]} -nested-none-of-true = { none-of = [ - { none-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} - ]}, - { all-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "something else"} - ]}, - { any-of = [ - {if = "authenticated-as", eq = "something else"}, - {if = "rcpt-domain", eq = "something else"}, - {if = "mx", starts-with = "something else"}, - ]} -]} -nested-none-of-false = { none-of = [ - { any-of = [ - {if = "authenticated-as", ne = "john@foobar.org"}, - {if = "rcpt-domain", eq = "example.org"}, - {if = "mx", starts-with = "mx.some"}, - ]}, - { all-of = [ - {if = "rcpt-domain", eq = "example.org"}, - {if = "listener", eq = "smtp"}, - {if = "mx", starts-with = "mx.some"} - ]}, - { none-of = [ - {if = "authenticated-as", eq = "something else"}, - {if = "rcpt-domain", eq = "something else"}, - {if = "mx", starts-with = "something else"}, - ]} -]} - -[store."list/domains"] -type = "memory" -format = "list" -values = ["mydomain1.org", "foo.net", "otherdomain.net"] +"eq-true" = "rcpt_domain = 'example.org'" +"eq-false" = "rcpt_domain = 'example.com'" +"listener-eq-true" = "listener = 'smtp'" +"listener-eq-false" = "listener = 'smtps'" +"ip-eq-true" = "local_ip = '192.168.9.3'" +"ip-eq-false" = "remote_ip = 'A:B:C::D:E'" +"ne-true" = "!is_empty(authenticated_as)" +"ne-false" = "authenticated_as != 'john@foobar.org'" +"starts-with-true" = "starts_with(mx, 'mx.some')" +"starts-with-false" = "starts_with(mx, 'enchilada')" +"ends-with-true" = "ends_with(sender, '@foo.net')" +"ends-with-false" = "ends_with(sender, 'chimichanga')" +"regex-true" = "matches('^(.+)@(.+)$', sender)" +"regex-false" = "matches('/^\\S+@\\S+\\.\\S+$/', mx)" +"any-of-true" = "authenticated_as != 'john@foobar.org' | rcpt_domain = 'example.org' | starts_with(mx, 'mx.some')" +"any-of-false" = "authenticated_as = 'something else' | rcpt_domain = 'something else' | starts_with(mx, 'something else')" +"all-of-true" = "rcpt_domain = 'example.org' & listener = 'smtp' & starts_with(mx, 'mx.some')" +"all-of-false" = "rcpt_domain = 'example.org' & listener = 'smtp' & starts_with(mx, 'something else')" +"none-of-true" = "!(authenticated_as = 'something else' | rcpt_domain = 'something else' | starts_with(mx, 'something else'))" +"none-of-false" = "!(rcpt_domain = 'example.org' | listener = 'smtp' | starts_with(mx, 'mx.some'))" diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index c4280b1d..f2f35bd5 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -27,7 +27,6 @@ pub mod ldap; pub mod smtp; pub mod sql; -use ::smtp::core::Lookup; use directory::{ backend::internal::manage::ManageDirectory, core::config::ConfigDirectory, AddressMapping, Directories, Principal, @@ -37,7 +36,7 @@ use rustls::ServerConfig; use rustls_pemfile::{certs, pkcs8_private_keys}; use rustls_pki_types::PrivateKeyDer; use std::{borrow::Cow, io::BufReader, path::PathBuf, sync::Arc}; -use store::{config::ConfigStore, LookupStore, Store, Stores}; +use store::{config::ConfigStore, LookupKey, LookupStore, LookupValue, Store, Stores}; use tokio_rustls::TlsAcceptor; use utils::config::Servers; @@ -600,18 +599,23 @@ async fn lookup_local() { ("suffix", "coco", false), ] { assert_eq!( - Lookup::from(lookups.get(&format!("local/{lookup}")).unwrap().clone()) - .contains(item) - .await - .unwrap(), + matches!( + lookups + .get(&format!("local/{lookup}")) + .unwrap() + .key_get::(LookupKey::Key(item.as_bytes().to_vec())) + .await + .unwrap(), + LookupValue::Value { .. } + ), expect, "failed for {lookup}, item {item}" ); } } -#[test] -fn address_mappings() { +#[tokio::test] +async fn address_mappings() { const MAPPINGS: &str = r#" [enable] catch-all = true @@ -640,13 +644,13 @@ fn address_mappings() { let subaddressing = AddressMapping::from_config(&config, (test, "subaddressing")).unwrap(); assert_eq!( - subaddressing.to_subaddress(ADDR), + subaddressing.to_subaddress(ADDR).await, config.value_require((test, "expected-sub")).unwrap(), "failed subaddress for {test:?}" ); assert_eq!( - catch_all.to_catch_all(ADDR), + catch_all.to_catch_all(ADDR).await, config .property_require::>((test, "expected-catch")) .unwrap() diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index e3dd5990..87e69bf2 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -21,10 +21,8 @@ * for more details. */ -use ahash::AHashMap; use directory::{backend::internal::manage::ManageDirectory, Principal, QueryBy, Type}; use mail_send::Credentials; -use smtp::core::Lookup; use store::{LookupStore, Store}; use crate::directory::{map_account_ids, DirectoryTest}; @@ -45,12 +43,6 @@ async fn sql_directory() { for directory_id in ["sqlite", "postgresql", "mysql"] { // Parse config let mut config = DirectoryTest::new(directory_id.into()).await; - let lookups = config - .stores - .lookup_stores - .iter() - .map(|(k, v)| (k.clone(), Lookup::from(v.clone()))) - .collect::>(); println!("Testing SQL directory {:?}", directory_id); let handle = config.directories.directories.remove(directory_id).unwrap(); @@ -122,14 +114,6 @@ async fn sql_directory() { .link_test_address("robert", "@catchall.org", "alias") .await; - // Text lookup - assert!(lookups - .get(&format!("{}/domains", directory_id)) - .unwrap() - .contains("example.org") - .await - .unwrap()); - // Test authentication assert_eq!( handle @@ -239,6 +223,7 @@ async fn sql_directory() { handle.email_to_ids("info@example.org").await.unwrap(), map_account_ids(base_store, vec!["bill", "jane", "john"]).await ); + let todo = "test regex subaddressing"; assert_eq!( handle.email_to_ids("jane+alias@example.org").await.unwrap(), map_account_ids(base_store, vec!["jane"]).await diff --git a/tests/src/smtp/config.rs b/tests/src/smtp/config.rs index a2278c4b..08ac1497 100644 --- a/tests/src/smtp/config.rs +++ b/tests/src/smtp/config.rs @@ -21,38 +21,26 @@ * for more details. */ -use std::{ - borrow::Cow, - fs, - net::{IpAddr, Ipv4Addr}, - path::PathBuf, - sync::Arc, - time::Duration, -}; +use std::{fs, net::IpAddr, path::PathBuf, sync::Arc, time::Duration}; -use store::{ - backend::memory::{LookupList, MemoryStore}, - config::ConfigStore, - LookupStore, -}; +use store::config::ConfigStore; use tokio::net::TcpSocket; use utils::{ config::{ - ipmask::IpAddrMask, Config, DynValue, KeyLookup, Listener, Rate, Server, ServerProtocol, + if_block::{IfBlock, IfThen}, + Config, Listener, Rate, Server, ServerProtocol, }, + expr::{BinaryOperator, Constant, Expression, ExpressionItem, UnaryOperator}, listener::TcpAcceptor, }; -use ahash::AHashMap; - use smtp::{ config::{ - condition::ConfigCondition, if_block::ConfigIf, throttle::ConfigThrottle, Condition, - ConditionMatch, Conditions, ConfigContext, EnvelopeKey, IfBlock, IfThen, StringMatch, - Throttle, THROTTLE_AUTH_AS, THROTTLE_REMOTE_IP, THROTTLE_SENDER_DOMAIN, + map_expr_token, throttle::ConfigThrottle, ConfigContext, Throttle, THROTTLE_AUTH_AS, + THROTTLE_REMOTE_IP, THROTTLE_SENDER_DOMAIN, }, - core::Lookup, + core::{eval::*, ResolveVariable}, }; use super::add_test_certs; @@ -67,134 +55,10 @@ struct TestEnvelope { pub helo_domain: String, pub authenticated_as: String, pub mx: String, - pub listener_id: u16, + pub listener_id: String, pub priority: i16, } -#[test] -fn parse_conditions() { - let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - file.push("resources"); - file.push("smtp"); - file.push("config"); - file.push("rules.toml"); - - let config = Config::new(&fs::read_to_string(file).unwrap()).unwrap(); - let servers = vec![Server { - id: "smtp".to_string(), - internal_id: 123, - ..Default::default() - }]; - let mut context = ConfigContext::new(&servers); - let list = LookupStore::Query(Arc::new(store::QueryStore { - store: MemoryStore::List(LookupList::default()).into(), - query: "abc".into(), - })); - context - .stores - .lookup_stores - .insert("test-list".to_string(), list.clone()); - - let mut conditions = config.parse_conditions(&context).unwrap(); - let expected_rules = AHashMap::from_iter([ - ( - "simple".to_string(), - Conditions { - conditions: vec![Condition::Match { - key: EnvelopeKey::Listener, - value: ConditionMatch::UInt(123), - not: false, - }], - }, - ), - ( - "is-authenticated".to_string(), - Conditions { - conditions: vec![Condition::Match { - key: EnvelopeKey::AuthenticatedAs, - value: ConditionMatch::String(StringMatch::Equal("".to_string())), - not: true, - }], - }, - ), - ( - "expanded".to_string(), - Conditions { - conditions: vec![ - Condition::Match { - key: EnvelopeKey::SenderDomain, - value: ConditionMatch::String(StringMatch::StartsWith( - "example".to_string(), - )), - not: false, - }, - Condition::JumpIfFalse { positions: 1 }, - Condition::Match { - key: EnvelopeKey::Sender, - value: ConditionMatch::Lookup(list.into()), - not: false, - }, - ], - }, - ), - ( - "my-nested-rule".to_string(), - Conditions { - conditions: vec![ - Condition::Match { - key: EnvelopeKey::RecipientDomain, - value: ConditionMatch::String(StringMatch::Equal( - "example.org".to_string(), - )), - not: false, - }, - Condition::JumpIfTrue { positions: 9 }, - Condition::Match { - key: EnvelopeKey::RemoteIp, - value: ConditionMatch::IpAddrMask(IpAddrMask::V4 { - addr: "192.168.0.0".parse().unwrap(), - mask: u32::MAX << (32 - 24), - }), - not: false, - }, - Condition::JumpIfTrue { positions: 7 }, - Condition::Match { - key: EnvelopeKey::Recipient, - value: ConditionMatch::String(StringMatch::StartsWith( - "no-reply@".to_string(), - )), - not: false, - }, - Condition::JumpIfFalse { positions: 5 }, - Condition::Match { - key: EnvelopeKey::Sender, - value: ConditionMatch::String(StringMatch::EndsWith( - "@domain.org".to_string(), - )), - not: false, - }, - Condition::JumpIfFalse { positions: 3 }, - Condition::Match { - key: EnvelopeKey::Priority, - value: ConditionMatch::Int(1), - not: true, - }, - Condition::JumpIfTrue { positions: 1 }, - Condition::Match { - key: EnvelopeKey::Priority, - value: ConditionMatch::Int(-2), - not: false, - }, - ], - }, - ), - ]); - - for (key, rule) in expected_rules { - assert_eq!(Some(rule), conditions.remove(&key), "failed for {key}"); - } -} - #[test] fn parse_if_blocks() { let mut file = PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -206,163 +70,209 @@ fn parse_if_blocks() { let config = Config::new(&fs::read_to_string(file).unwrap()).unwrap(); // Create context and add some conditions - let context = ConfigContext::new(&[]); let available_keys = vec![ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, ]; assert_eq!( config - .parse_if_block::>("durations", &context, &available_keys) + .parse_if_block("durations", |name| { + map_expr_token::(name, &available_keys) + }) .unwrap() .unwrap(), IfBlock { + key: "durations".to_string(), if_then: vec![ IfThen { - conditions: Conditions { - conditions: vec![Condition::Match { - key: EnvelopeKey::Sender, - value: ConditionMatch::String(StringMatch::Equal("jdoe".to_string())), - not: false - }] - }, - then: Duration::from_secs(5 * 86400).into() - }, - IfThen { - conditions: Conditions { - conditions: vec![ - Condition::Match { - key: EnvelopeKey::Priority, - value: ConditionMatch::Int(-1), - not: false - }, - Condition::JumpIfTrue { positions: 1 }, - Condition::Match { - key: EnvelopeKey::Recipient, - value: ConditionMatch::String(StringMatch::StartsWith( - "jane".to_string() - )), - not: false - } + expr: Expression { + items: vec![ + ExpressionItem::Variable(2), + ExpressionItem::Constant(Constant::String("jdoe".to_string())), + ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] }, - then: Duration::from_secs(3600).into() + then: Expression { + items: vec![ExpressionItem::Constant(Constant::Integer(432000000))] + } + }, + IfThen { + expr: Expression { + items: vec![ + ExpressionItem::Variable(10), + ExpressionItem::Constant(Constant::Integer(1)), + ExpressionItem::UnaryOperator(UnaryOperator::Minus), + ExpressionItem::BinaryOperator(BinaryOperator::Eq), + ExpressionItem::JmpIf { val: true, pos: 4 }, + ExpressionItem::Variable(0), + ExpressionItem::Constant(Constant::String("jane".to_string())), + ExpressionItem::Function { + id: 29, + num_args: 2 + }, + ExpressionItem::BinaryOperator(BinaryOperator::Or) + ] + }, + then: Expression { + items: vec![ExpressionItem::Constant(Constant::Integer(3600000))] + } } ], - default: None + default: Expression { + items: vec![ExpressionItem::Constant(Constant::Integer(0))] + } } ); assert_eq!( config - .parse_if_block::>("string-list", &context, &available_keys) + .parse_if_block("string-list", |name| { + map_expr_token::(name, &available_keys) + }) .unwrap() .unwrap(), IfBlock { + key: "string-list".to_string(), if_then: vec![ IfThen { - conditions: Conditions { - conditions: vec![Condition::Match { - key: EnvelopeKey::Sender, - value: ConditionMatch::String(StringMatch::Equal("jdoe".to_string())), - not: false - }] - }, - then: vec!["From".to_string(), "To".to_string(), "Date".to_string()] - }, - IfThen { - conditions: Conditions { - conditions: vec![ - Condition::Match { - key: EnvelopeKey::Priority, - value: ConditionMatch::Int(-1), - not: false - }, - Condition::JumpIfTrue { positions: 1 }, - Condition::Match { - key: EnvelopeKey::Recipient, - value: ConditionMatch::String(StringMatch::StartsWith( - "jane".to_string() - )), - not: false - } + expr: Expression { + items: vec![ + ExpressionItem::Variable(2), + ExpressionItem::Constant(Constant::String("jdoe".to_string())), + ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] }, - then: vec!["Other-ID".to_string()] + then: Expression { + items: vec![ + ExpressionItem::Constant(Constant::String("From".to_string())), + ExpressionItem::Constant(Constant::String("To".to_string())), + ExpressionItem::Constant(Constant::String("Date".to_string())), + ExpressionItem::ArrayBuild(3) + ] + } + }, + IfThen { + expr: Expression { + items: vec![ + ExpressionItem::Variable(10), + ExpressionItem::Constant(Constant::Integer(1)), + ExpressionItem::UnaryOperator(UnaryOperator::Minus), + ExpressionItem::BinaryOperator(BinaryOperator::Eq), + ExpressionItem::JmpIf { val: true, pos: 4 }, + ExpressionItem::Variable(0), + ExpressionItem::Constant(Constant::String("jane".to_string())), + ExpressionItem::Function { + id: 29, + num_args: 2 + }, + ExpressionItem::BinaryOperator(BinaryOperator::Or) + ] + }, + then: Expression { + items: vec![ExpressionItem::Constant(Constant::String( + "Other-ID".to_string() + ))] + } } ], - default: vec![] + default: Expression { + items: vec![ExpressionItem::ArrayBuild(0)] + } } ); assert_eq!( config - .parse_if_block::>("string-list-bis", &context, &available_keys) + .parse_if_block("string-list-bis", |name| { + map_expr_token::(name, &available_keys) + }) .unwrap() .unwrap(), IfBlock { + key: "string-list-bis".to_string(), if_then: vec![ IfThen { - conditions: Conditions { - conditions: vec![Condition::Match { - key: EnvelopeKey::Sender, - value: ConditionMatch::String(StringMatch::Equal("jdoe".to_string())), - not: false - }] - }, - then: vec!["From".to_string(), "To".to_string(), "Date".to_string()] - }, - IfThen { - conditions: Conditions { - conditions: vec![ - Condition::Match { - key: EnvelopeKey::Priority, - value: ConditionMatch::Int(-1), - not: false - }, - Condition::JumpIfTrue { positions: 1 }, - Condition::Match { - key: EnvelopeKey::Recipient, - value: ConditionMatch::String(StringMatch::StartsWith( - "jane".to_string() - )), - not: false - } + expr: Expression { + items: vec![ + ExpressionItem::Variable(2), + ExpressionItem::Constant(Constant::String("jdoe".to_string())), + ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] }, - then: vec![] + then: Expression { + items: vec![ + ExpressionItem::Constant(Constant::String("From".to_string())), + ExpressionItem::Constant(Constant::String("To".to_string())), + ExpressionItem::Constant(Constant::String("Date".to_string())), + ExpressionItem::ArrayBuild(3) + ] + } + }, + IfThen { + expr: Expression { + items: vec![ + ExpressionItem::Variable(10), + ExpressionItem::Constant(Constant::Integer(1)), + ExpressionItem::UnaryOperator(UnaryOperator::Minus), + ExpressionItem::BinaryOperator(BinaryOperator::Eq), + ExpressionItem::JmpIf { val: true, pos: 4 }, + ExpressionItem::Variable(0), + ExpressionItem::Constant(Constant::String("jane".to_string())), + ExpressionItem::Function { + id: 29, + num_args: 2 + }, + ExpressionItem::BinaryOperator(BinaryOperator::Or) + ] + }, + then: Expression { + items: vec![ExpressionItem::ArrayBuild(0)] + } } ], - default: vec!["ID-Bis".to_string()] + default: Expression { + items: vec![ + ExpressionItem::Constant(Constant::String("ID-Bis".to_string())), + ExpressionItem::ArrayBuild(1) + ] + } } ); assert_eq!( config - .parse_if_block::("single-value", &context, &available_keys) + .parse_if_block("single-value", |name| { + map_expr_token::(name, &available_keys) + }) .unwrap() .unwrap(), IfBlock { + key: "single-value".to_string(), if_then: vec![], - default: "hello world".to_string() + default: Expression { + items: vec![ExpressionItem::Constant(Constant::String( + "hello world".to_string() + ))] + } } ); for bad_rule in [ - "bad-multi-value", "bad-if-without-then", "bad-if-without-else", "bad-multiple-else", ] { - if let Ok(value) = config.parse_if_block::(bad_rule, &context, &available_keys) { + if let Ok(value) = config.parse_if_block(bad_rule, |name| { + map_expr_token::(name, &available_keys) + }) { panic!("Condition {bad_rule:?} had unexpected result {value:?}"); } } @@ -377,37 +287,27 @@ fn parse_throttle() { file.push("throttle.toml"); let available_keys = vec![ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, ]; 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) + .parse_throttle("throttle", &available_keys, u16::MAX) .unwrap(); assert_eq!( throttle, vec![ Throttle { - conditions: Conditions { - conditions: vec![Condition::Match { - key: EnvelopeKey::RemoteIp, - value: ConditionMatch::IpAddrMask(IpAddrMask::V4 { - addr: "127.0.0.1".parse().unwrap(), - mask: u32::MAX - }), - not: false - }] - }, + expr: Expression::default(), keys: THROTTLE_REMOTE_IP | THROTTLE_AUTH_AS, concurrency: 100.into(), rate: Rate { @@ -417,7 +317,7 @@ fn parse_throttle() { .into() }, Throttle { - conditions: Conditions { conditions: vec![] }, + expr: Expression::default(), keys: THROTTLE_SENDER_DOMAIN, concurrency: 10000.into(), rate: None @@ -588,24 +488,49 @@ async fn eval_if() { ]; let mut context = ConfigContext::new(&servers); context.stores = config.parse_stores().await.unwrap(); - let conditions = config.parse_conditions(&context).unwrap(); let envelope = TestEnvelope::from_config(&config); - for (key, conditions) in conditions { + for (key, expr) in &config.keys { + if !key.starts_with("rule.") { + continue; + } + //println!("============= Testing {:?} ==================", key); let (_, expected_result) = key.rsplit_once('-').unwrap(); assert_eq!( IfBlock { + key: key.to_string(), if_then: vec![IfThen { - conditions, - then: true + expr: Expression::parse(key.as_str(), expr, |name| { + map_expr_token::( + name, + &[ + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, + V_MX, + ], + ) + }) + .unwrap(), + then: Expression::from(true), }], - default: false, + default: Expression::from(false), } - .eval(&envelope) - .await, - &expected_result.parse::().unwrap(), + .eval( + |name| { envelope.resolve_variable(name) }, + |_, _| async { Default::default() } + ) + .await + .to_bool(), + expected_result.parse::().unwrap(), "failed for {key:?}" ); } @@ -628,117 +553,60 @@ async fn eval_dynvalue() { for test_name in config.sub_keys("eval", "") { //println!("============= Testing {:?} ==================", key); let if_block = config - .parse_if_block::>>( - ("eval", test_name, "test"), - &context, - &[ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, - EnvelopeKey::Mx, - ], - ) + .parse_if_block(("eval", test_name, "test"), |name| { + map_expr_token::( + name, + &[ + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, + V_MX, + ], + ) + }) .unwrap() .unwrap(); let expected = config .property_require::>(("eval", test_name, "expect")) - .unwrap() - .map(Cow::Owned); + .unwrap(); assert_eq!( - if_block - .eval_and_capture(&envelope) - .await - .into_value(&envelope), + String::try_from( + if_block + .eval( + |name| { envelope.resolve_variable(name) }, + |_, _| async { Default::default() } + ) + .await + ) + .ok(), expected, "failed for test {test_name:?}" ); } - let wrapped_stores = context - .stores - .lookup_stores - .iter() - .map(|(k, v)| (k.clone(), Arc::new(v.clone()))) - .collect::>(); - - for test_name in config.sub_keys("maybe-eval", "") { - //println!("============= Testing {:?} ==================", key); - let if_block = config - .parse_if_block::>>( - ("maybe-eval", test_name, "test"), - &context, - &[ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, - EnvelopeKey::Mx, - ], - ) - .unwrap() - .unwrap() - .map_if_block(&wrapped_stores, ("maybe-eval", test_name, "test"), "test") - .unwrap(); - let expected = config - .value_require(("maybe-eval", test_name, "expect")) - .unwrap(); - - let lookup: Lookup = if_block - .eval_and_capture(&envelope) - .await - .into_value(&envelope) - .unwrap() - .as_ref() - .clone() - .into(); - - assert!(lookup.contains(expected).await.unwrap()); - } } -impl KeyLookup for TestEnvelope { - type Key = EnvelopeKey; - - fn key(&self, key: &Self::Key) -> std::borrow::Cow<'_, str> { - match key { - EnvelopeKey::Recipient => self.rcpt.as_str().into(), - EnvelopeKey::RecipientDomain => self.rcpt_domain.as_str().into(), - EnvelopeKey::Sender => self.sender.as_str().into(), - EnvelopeKey::SenderDomain => self.sender_domain.as_str().into(), - EnvelopeKey::AuthenticatedAs => self.authenticated_as.as_str().into(), - EnvelopeKey::Listener => self.listener_id.to_string().into(), - EnvelopeKey::RemoteIp => self.remote_ip.to_string().into(), - EnvelopeKey::LocalIp => self.local_ip.to_string().into(), - EnvelopeKey::Priority => self.priority.to_string().into(), - EnvelopeKey::Mx => self.mx.as_str().into(), - EnvelopeKey::HeloDomain => self.helo_domain.as_str().into(), - } - } - - fn key_as_int(&self, key: &Self::Key) -> i32 { - match key { - EnvelopeKey::Priority => self.priority as i32, - EnvelopeKey::Listener => self.listener_id as i32, - _ => unreachable!(), - } - } - - fn key_as_ip(&self, key: &Self::Key) -> IpAddr { - match key { - EnvelopeKey::RemoteIp => self.remote_ip, - EnvelopeKey::LocalIp => self.local_ip, - _ => IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), +impl ResolveVariable for TestEnvelope { + fn resolve_variable(&self, variable: u32) -> utils::expr::Variable<'_> { + match variable { + V_RECIPIENT => self.rcpt.as_str().into(), + V_RECIPIENT_DOMAIN => self.rcpt_domain.as_str().into(), + V_SENDER => self.sender.as_str().into(), + V_SENDER_DOMAIN => self.sender_domain.as_str().into(), + V_AUTHENTICATED_AS => self.authenticated_as.as_str().into(), + V_LISTENER => self.listener_id.to_string().into(), + V_REMOTE_IP => self.remote_ip.to_string().into(), + V_LOCAL_IP => self.local_ip.to_string().into(), + V_PRIORITY => self.priority.to_string().into(), + V_MX => self.mx.as_str().into(), + V_HELO_DOMAIN => self.helo_domain.as_str().into(), + _ => Default::default(), } } } diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index 84362f79..8623be18 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -12,7 +12,7 @@ use ahash::AHashMap; use mail_auth::{dmarc::Policy, DkimResult, DmarcResult, IprevResult, SpfResult, MX}; use sieve::runtime::Variable; use smtp::{ - config::{scripts::ConfigSieve, ConfigContext, IfBlock}, + config::{scripts::ConfigSieve, ConfigContext}, core::{Session, SessionAddress, SMTP}, inbound::AuthResult, scripts::{ @@ -22,7 +22,7 @@ use smtp::{ }; use store::config::ConfigStore; use tokio::runtime::Handle; -use utils::config::Config; +use utils::config::{if_block::IfBlock, Config}; use crate::smtp::{TestConfig, TestSMTP}; diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index 16c42ee5..f73cda54 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -24,14 +24,14 @@ use directory::core::config::ConfigDirectory; use smtp_proto::{AUTH_LOGIN, AUTH_PLAIN}; use store::{Store, Stores}; -use utils::config::{Config, DynValue, Servers}; +use utils::config::{if_block::IfBlock, Config, Servers}; use crate::smtp::{ session::{TestSession, VerifyResponse}, ParseTestConfig, TestConfig, }; use smtp::{ - config::{ConfigContext, EnvelopeKey, IfBlock}, + config::ConfigContext, core::{Session, State, SMTP}, }; @@ -70,28 +70,26 @@ async fn auth() { config.require = r"[{if = 'remote-ip', eq = '10.0.0.1', then = true}, {else = false}]" - .parse_if(&ctx); + .parse_if(); config.directory = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 'local'}, {else = false}]" - .parse_if::>>(&ctx) - .map_if_block(&ctx.directory.directories, "", "") - .unwrap(); + .parse_if(); config.errors_max = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 2}, {else = 3}]" - .parse_if(&ctx); - config.errors_wait = "'100ms'".parse_if(&ctx); + .parse_if(); + config.errors_wait = "'100ms'".parse_if(); config.mechanisms = format!( "[{{if = 'remote-ip', eq = '10.0.0.1', then = {}}}, {{else = 0}}]", AUTH_PLAIN | AUTH_LOGIN ) .as_str() - .parse_if(&ctx); + .parse_if(); config.must_match_sender = IfBlock::new(true); core.session.config.extensions.future_release = r"[{if = 'authenticated-as', ne = '', then = '1d'}, {else = false}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); // EHLO should not advertise plain text auth without TLS let mut session = Session::test(core); diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index 77ed1cad..2aead711 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -23,17 +23,14 @@ use directory::core::config::ConfigDirectory; use store::{Store, Stores}; -use utils::config::{Config, Servers}; +use utils::config::{if_block::IfBlock, Config, Servers}; use crate::smtp::{ inbound::{TestMessage, TestQueueEvent}, session::{load_test_message, TestSession, VerifyResponse}, ParseTestConfig, TestConfig, TestSMTP, }; -use smtp::{ - config::{ConfigContext, IfBlock, MaybeDynValue}, - core::{Session, SMTP}, -}; +use smtp::core::{Session, SMTP}; const DIRECTORY: &str = r#" [directory."local"] @@ -84,14 +81,12 @@ async fn data() { .await .unwrap(); let config = &mut core.session.config.rcpt; - config.directory = IfBlock::new(Some(MaybeDynValue::Static( - directory.directories.get("local").unwrap().clone(), - ))); + config.directory = IfBlock::new("local".to_string()); let config = &mut core.session.config; config.data.add_auth_results = "[{if = 'remote-ip', eq = '10.0.0.3', then = true}, {else = false}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.data.add_date = config.data.add_auth_results.clone(); config.data.add_message_id = config.data.add_auth_results.clone(); config.data.add_received = config.data.add_auth_results.clone(); @@ -100,7 +95,7 @@ async fn data() { config.data.max_received_headers = IfBlock::new(3); config.data.max_messages = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 1}, {else = 100}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); core.queue.config.quota = r"[[queue.quota]] match = {if = 'sender', eq = 'john@doe.org'} @@ -117,7 +112,7 @@ async fn data() { key = ['rcpt'] size = 450 " - .parse_quota(&ConfigContext::new(&[])); + .parse_quota(); // Test queue message builder let mut session = Session::test(core); diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index 7469f97f..b4ac0a93 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -35,7 +35,7 @@ use mail_auth::{ spf::Spf, }; use store::{Store, Stores}; -use utils::config::{Config, DynValue, Rate, Servers}; +use utils::config::{if_block::IfBlock, Config, Servers}; use crate::smtp::{ inbound::{sign::TextConfigContext, TestMessage, TestQueueEvent, TestReportingEvent}, @@ -43,9 +43,7 @@ use crate::smtp::{ ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{ - AggregateFrequency, ConfigContext, EnvelopeKey, IfBlock, MaybeDynValue, VerifyStrategy, - }, + config::{AggregateFrequency, ConfigContext, VerifyStrategy}, core::{Session, SMTP}, }; @@ -139,9 +137,7 @@ async fn dmarc() { .await .unwrap(); let config = &mut core.session.config.rcpt; - config.directory = IfBlock::new(Some(MaybeDynValue::Static( - directory.directories.get("local").unwrap().clone(), - ))); + config.directory = IfBlock::new("local".to_string()); let config = &mut core.session.config; config.data.add_auth_results = IfBlock::new(true); @@ -152,10 +148,7 @@ async fn dmarc() { config.data.add_received_spf = IfBlock::new(true); let config = &mut core.report.config; - config.dkim.send = IfBlock::new(Some(Rate { - requests: 1, - period: Duration::from_secs(1), - })); + config.dkim.send = "[1, 1s]".parse_if(); config.dmarc.send = config.dkim.send.clone(); config.spf.send = config.dkim.send.clone(); config.dmarc_aggregate.send = IfBlock::new(AggregateFrequency::Daily); @@ -163,27 +156,18 @@ async fn dmarc() { let config = &mut core.mail_auth; config.spf.verify_ehlo = "[{if = 'remote-ip', eq = '10.0.0.2', then = 'strict'}, { else = 'relaxed' }]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.spf.verify_mail_from = config.spf.verify_ehlo.clone(); config.dmarc.verify = IfBlock::new(VerifyStrategy::Strict); config.arc.verify = config.dmarc.verify.clone(); config.dkim.verify = "[{if = 'sender-domain', eq = 'test.net', then = 'relaxed'}, { else = 'strict' }]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); let config = &mut core.report.config; - config.spf.sign = "['rsa']" - .parse_if::>>(&ctx) - .map_if_block(&ctx.signers, "", "") - .unwrap(); - config.dmarc.sign = "['rsa']" - .parse_if::>>(&ctx) - .map_if_block(&ctx.signers, "", "") - .unwrap(); - config.dkim.sign = "['rsa']" - .parse_if::>>(&ctx) - .map_if_block(&ctx.signers, "", "") - .unwrap(); + config.spf.sign = "['rsa']".parse_if(); + config.dmarc.sign = "['rsa']".parse_if(); + config.dkim.sign = "['rsa']".parse_if(); // SPF must pass let core = Arc::new(core); diff --git a/tests/src/smtp/inbound/ehlo.rs b/tests/src/smtp/inbound/ehlo.rs index f1aa8361..b214a575 100644 --- a/tests/src/smtp/inbound/ehlo.rs +++ b/tests/src/smtp/inbound/ehlo.rs @@ -24,15 +24,13 @@ use std::time::{Duration, Instant}; use mail_auth::{common::parse::TxtRecordParser, spf::Spf, SpfResult}; +use utils::config::if_block::IfBlock; use crate::smtp::{ session::{TestSession, VerifyResponse}, ParseTestConfig, TestConfig, }; -use smtp::{ - config::{ConfigContext, IfBlock}, - core::{Session, SMTP}, -}; +use smtp::core::{Session, SMTP}; #[tokio::test] async fn ehlo() { @@ -51,16 +49,16 @@ async fn ehlo() { let config = &mut core.session.config; config.data.max_message_size = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 1024}, {else = 2048}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.extensions.future_release = r"[{if = 'remote-ip', eq = '10.0.0.1', then = '1h'}, {else = false}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.extensions.mt_priority = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 'nsep'}, {else = false}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); core.mail_auth.spf.verify_ehlo = r"[{if = 'remote-ip', eq = '10.0.0.2', then = 'strict'}, {else = 'relaxed'}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.ehlo.reject_non_fqdn = IfBlock::new(true); // Reject non-FQDN domains diff --git a/tests/src/smtp/inbound/limits.rs b/tests/src/smtp/inbound/limits.rs index c8475fdc..33607e95 100644 --- a/tests/src/smtp/inbound/limits.rs +++ b/tests/src/smtp/inbound/limits.rs @@ -29,10 +29,7 @@ use crate::smtp::{ session::{TestSession, VerifyResponse}, ParseTestConfig, TestConfig, }; -use smtp::{ - config::ConfigContext, - core::{Session, SMTP}, -}; +use smtp::core::{Session, SMTP}; #[tokio::test] async fn limits() { @@ -40,13 +37,13 @@ async fn limits() { let config = &mut core.session.config; config.transfer_limit = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 10}, {else = 1024}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.timeout = r"[{if = 'remote-ip', eq = '10.0.0.2', then = '500ms'}, {else = '30m'}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.duration = r"[{if = 'remote-ip', eq = '10.0.0.3', then = '500ms'}, {else = '60m'}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); let (_tx, rx) = watch::channel(true); // Exceed max line length diff --git a/tests/src/smtp/inbound/mail.rs b/tests/src/smtp/inbound/mail.rs index 0dfc35b7..2bda8ca6 100644 --- a/tests/src/smtp/inbound/mail.rs +++ b/tests/src/smtp/inbound/mail.rs @@ -28,13 +28,14 @@ use std::{ use mail_auth::{common::parse::TxtRecordParser, spf::Spf, IprevResult, SpfResult}; use smtp_proto::{MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_REQUIRETLS}; +use utils::config::if_block::IfBlock; use crate::smtp::{ session::{TestSession, VerifyResponse}, ParseTestConfig, TestConfig, }; use smtp::{ - config::{ConfigContext, IfBlock, VerifyStrategy}, + config::VerifyStrategy, core::{Session, SMTP}, }; @@ -72,32 +73,32 @@ async fn mail() { core.mail_auth.spf.verify_ehlo = IfBlock::new(VerifyStrategy::Relaxed); core.mail_auth.spf.verify_mail_from = r"[{if = 'remote-ip', eq = '10.0.0.2', then = 'strict'}, {else = 'relaxed'}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); core.mail_auth.iprev.verify = r"[{if = 'remote-ip', eq = '10.0.0.2', then = 'strict'}, {else = 'relaxed'}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.extensions.future_release = r"[{if = 'remote-ip', eq = '10.0.0.2', then = '1d'}, {else = false}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.extensions.deliver_by = r"[{if = 'remote-ip', eq = '10.0.0.2', then = '1d'}, {else = false}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.extensions.requiretls = r"[{if = 'remote-ip', eq = '10.0.0.2', then = true}, {else = false}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.extensions.mt_priority = r"[{if = 'remote-ip', eq = '10.0.0.2', then = 'nsep'}, {else = false}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.data.max_message_size = r"[{if = 'remote-ip', eq = '10.0.0.2', then = 2048}, {else = 1024}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.throttle.mail_from = r"[[throttle]] match = {if = 'remote-ip', eq = '10.0.0.1'} key = 'sender' rate = '2/1s' " - .parse_throttle(&ConfigContext::new(&[])); + .parse_throttle(); // Be rude and do not say EHLO let core = Arc::new(core); diff --git a/tests/src/smtp/inbound/milter.rs b/tests/src/smtp/inbound/milter.rs index 8595b30a..2f38d24b 100644 --- a/tests/src/smtp/inbound/milter.rs +++ b/tests/src/smtp/inbound/milter.rs @@ -27,7 +27,7 @@ use mail_auth::AuthenticatedMessage; use mail_parser::MessageParser; use serde::Deserialize; use smtp::{ - config::{ConfigContext, IfBlock, Milter}, + config::Milter, core::{Session, SessionData, SMTP}, inbound::milter::{ receiver::{FrameResult, Receiver}, @@ -39,6 +39,7 @@ use tokio::{ net::{TcpListener, TcpStream}, sync::watch, }; +use utils::config::if_block::IfBlock; use crate::smtp::{ inbound::{TestMessage, TestQueueEvent}, @@ -79,7 +80,7 @@ async fn milter_session() { options.version = 6 tls = false "# - .parse_milters(&ConfigContext::new(&[])); + .parse_milters(); // Build session let mut session = Session::test(core); diff --git a/tests/src/smtp/inbound/rcpt.rs b/tests/src/smtp/inbound/rcpt.rs index e14c228c..c0ca8d80 100644 --- a/tests/src/smtp/inbound/rcpt.rs +++ b/tests/src/smtp/inbound/rcpt.rs @@ -26,16 +26,13 @@ use std::time::Duration; use directory::core::config::ConfigDirectory; use smtp_proto::{RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_SUCCESS}; use store::{Store, Stores}; -use utils::config::{Config, Servers}; +use utils::config::{if_block::IfBlock, Config, Servers}; use crate::smtp::{ session::{TestSession, VerifyResponse}, ParseTestConfig, TestConfig, }; -use smtp::{ - config::{ConfigContext, IfBlock, MaybeDynValue}, - core::{Session, State, SMTP}, -}; +use smtp::core::{Session, State, SMTP}; const DIRECTORY: &str = r#" [directory."local"] @@ -78,30 +75,28 @@ async fn rcpt() { .await .unwrap(); let config = &mut core.session.config.rcpt; - config.directory = IfBlock::new(Some(MaybeDynValue::Static( - directory.directories.get("local").unwrap().clone(), - ))); + config.directory = IfBlock::new("local".to_string()); config.max_recipients = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 3}, {else = 5}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.relay = r"[{if = 'remote-ip', eq = '10.0.0.1', then = false}, {else = true}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config_ext.dsn = r"[{if = 'remote-ip', eq = '10.0.0.1', then = false}, {else = true}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.errors_max = r"[{if = 'remote-ip', eq = '10.0.0.1', then = 3}, {else = 100}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.errors_wait = r"[{if = 'remote-ip', eq = '10.0.0.1', then = '5ms'}, {else = '1s'}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); core.session.config.throttle.rcpt_to = r"[[throttle]] match = {if = 'remote-ip', eq = '10.0.0.1'} key = 'sender' rate = '2/1s' " - .parse_throttle(&ConfigContext::new(&[])); + .parse_throttle(); // RCPT without MAIL FROM let mut session = Session::test(core); diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs index c1cfe9a9..16070a68 100644 --- a/tests/src/smtp/inbound/rewrite.rs +++ b/tests/src/smtp/inbound/rewrite.rs @@ -24,11 +24,11 @@ use crate::smtp::{inbound::sign::TextConfigContext, session::TestSession, TestConfig}; use directory::core::config::ConfigDirectory; use smtp::{ - config::{if_block::ConfigIf, scripts::ConfigSieve, ConfigContext, EnvelopeKey, IfBlock}, - core::{Session, SMTP}, + config::{map_expr_token, scripts::ConfigSieve, ConfigContext}, + core::{eval::*, Session, SMTP}, }; use store::{Store, Stores}; -use utils::config::{Config, DynValue, Servers}; +use utils::config::{if_block::IfBlock, utils::NoConstants, Config, Servers}; const CONFIG: &str = r#" [session.mail] @@ -95,12 +95,7 @@ async fn address_rewrite() { .unwrap();*/ // Prepare config - let available_keys = [ - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - ]; + let available_keys = &[V_SENDER, V_SENDER_DOMAIN, V_RECIPIENT, V_RECIPIENT_DOMAIN]; let mut core = SMTP::test(); let mut ctx = ConfigContext::new(&[]).parse_signatures(); let settings = Config::new(CONFIG).unwrap(); @@ -111,31 +106,27 @@ async fn address_rewrite() { core.sieve = settings.parse_sieve(&mut ctx).unwrap(); let config = &mut core.session.config; config.mail.script = settings - .parse_if_block::>("session.mail.script", &ctx, &available_keys) + .parse_if_block("session.mail.script", |name| { + map_expr_token::(name, available_keys) + }) .unwrap() - .unwrap_or_default() - .map_if_block(&ctx.scripts, "session.mail.script", "script") - .unwrap(); + .unwrap_or_default(); config.mail.rewrite = settings - .parse_if_block::>>( - "session.mail.rewrite", - &ctx, - &available_keys, - ) + .parse_if_block("session.mail.rewrite", |name| { + map_expr_token::(name, available_keys) + }) .unwrap() .unwrap_or_default(); config.rcpt.script = settings - .parse_if_block::>("session.rcpt.script", &ctx, &available_keys) + .parse_if_block("session.rcpt.script", |name| { + map_expr_token::(name, available_keys) + }) .unwrap() - .unwrap_or_default() - .map_if_block(&ctx.scripts, "session.rcpt.script", "script") - .unwrap(); + .unwrap_or_default(); config.rcpt.rewrite = settings - .parse_if_block::>>( - "session.rcpt.rewrite", - &ctx, - &available_keys, - ) + .parse_if_block("session.rcpt.rewrite", |name| { + map_expr_token::(name, available_keys) + }) .unwrap() .unwrap_or_default(); config.rcpt.relay = IfBlock::new(true); diff --git a/tests/src/smtp/inbound/scripts.rs b/tests/src/smtp/inbound/scripts.rs index af09e9bd..bfc340ac 100644 --- a/tests/src/smtp/inbound/scripts.rs +++ b/tests/src/smtp/inbound/scripts.rs @@ -31,13 +31,13 @@ use crate::smtp::{ }; use directory::core::config::ConfigDirectory; use smtp::{ - config::{scripts::ConfigSieve, session::ConfigSession, ConfigContext, EnvelopeKey, IfBlock}, - core::{Session, SMTP}, + config::{scripts::ConfigSieve, session::ConfigSession, ConfigContext}, + core::{eval::V_REMOTE_IP, Session, SMTP}, scripts::ScriptResult, }; use store::{config::ConfigStore, Store}; use tokio::runtime::Handle; -use utils::config::{Config, Servers}; +use utils::config::{if_block::IfBlock, Config, Servers}; const CONFIG: &str = r#" [store."sql"] @@ -137,14 +137,14 @@ async fn sieve_scripts() { .parse_directory(&ctx.stores, &Servers::default(), Store::default()) .await .unwrap(); - let pipes = config.parse_pipes(&ctx, &[EnvelopeKey::RemoteIp]).unwrap(); + let pipes = config.parse_pipes(&[V_REMOTE_IP]).unwrap(); core.sieve = config.parse_sieve(&mut ctx).unwrap(); let config = &mut core.session.config; - config.connect.script = IfBlock::new(ctx.scripts.get("stage_connect").cloned()); - config.ehlo.script = IfBlock::new(ctx.scripts.get("stage_ehlo").cloned()); - config.mail.script = IfBlock::new(ctx.scripts.get("stage_mail").cloned()); - config.rcpt.script = IfBlock::new(ctx.scripts.get("stage_rcpt").cloned()); - config.data.script = IfBlock::new(ctx.scripts.get("stage_data").cloned()); + config.connect.script = IfBlock::new("stage_connect".to_string()); + config.ehlo.script = IfBlock::new("stage_ehlo".to_string()); + config.mail.script = IfBlock::new("stage_mail".to_string()); + config.rcpt.script = IfBlock::new("stage_rcpt".to_string()); + config.data.script = IfBlock::new("stage_data".to_string()); config.rcpt.relay = IfBlock::new(true); config.data.pipe_commands = pipes; let core = Arc::new(core); diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs index 81588751..7ba4d55f 100644 --- a/tests/src/smtp/inbound/sign.rs +++ b/tests/src/smtp/inbound/sign.rs @@ -29,7 +29,7 @@ use mail_auth::{ spf::Spf, }; use store::{Store, Stores}; -use utils::config::{Config, DynValue, Servers}; +use utils::config::{if_block::IfBlock, Config, Servers}; use crate::smtp::{ inbound::{TestMessage, TestQueueEvent}, @@ -37,9 +37,7 @@ use crate::smtp::{ ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{ - auth::ConfigAuth, ConfigContext, EnvelopeKey, IfBlock, MaybeDynValue, VerifyStrategy, - }, + config::{auth::ConfigAuth, ConfigContext, VerifyStrategy}, core::{Session, SMTP}, }; @@ -158,9 +156,7 @@ async fn sign_and_seal() { .await .unwrap(); let config = &mut core.session.config.rcpt; - config.directory = IfBlock::new(Some(MaybeDynValue::Static( - directory.directories.get("local").unwrap().clone(), - ))); + config.directory = IfBlock::new("local".to_string()); let config = &mut core.session.config; config.data.add_auth_results = IfBlock::new(true); @@ -177,14 +173,8 @@ async fn sign_and_seal() { config.dkim.verify = config.spf.verify_ehlo.clone(); config.arc.verify = config.spf.verify_ehlo.clone(); config.dmarc.verify = config.spf.verify_ehlo.clone(); - config.dkim.sign = "['rsa']" - .parse_if::>>(&ctx) - .map_if_block(&ctx.signers, "", "") - .unwrap(); - config.arc.seal = "'ed'" - .parse_if::>>(&ctx) - .map_if_block(&ctx.sealers, "", "") - .unwrap(); + config.dkim.sign = "['rsa']".parse_if(); + config.arc.seal = "'ed'".parse_if(); // Test DKIM signing let mut session = Session::test(core); diff --git a/tests/src/smtp/inbound/throttle.rs b/tests/src/smtp/inbound/throttle.rs index 45ae7c29..2e20873b 100644 --- a/tests/src/smtp/inbound/throttle.rs +++ b/tests/src/smtp/inbound/throttle.rs @@ -24,10 +24,7 @@ use std::time::Duration; use crate::smtp::{session::TestSession, ParseTestConfig, TestConfig}; -use smtp::{ - config::ConfigContext, - core::{Session, SessionAddress, SMTP}, -}; +use smtp::core::{Session, SessionAddress, SMTP}; #[tokio::test] async fn throttle_inbound() { @@ -39,17 +36,17 @@ async fn throttle_inbound() { concurrency = 2 rate = '3/1s' " - .parse_throttle(&ConfigContext::new(&[])); + .parse_throttle(); config.throttle.mail_from = r"[[throttle]] key = 'sender' rate = '2/1s' " - .parse_throttle(&ConfigContext::new(&[])); + .parse_throttle(); config.throttle.rcpt_to = r"[[throttle]] key = ['remote-ip', 'rcpt'] rate = '2/1s' " - .parse_throttle(&ConfigContext::new(&[])); + .parse_throttle(); // Test connection concurrency limit let mut session = Session::test(core); diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs index 5f0368a0..debe9c2e 100644 --- a/tests/src/smtp/inbound/vrfy.rs +++ b/tests/src/smtp/inbound/vrfy.rs @@ -23,14 +23,14 @@ use directory::core::config::ConfigDirectory; use store::{Store, Stores}; -use utils::config::{Config, Servers}; +use utils::config::{if_block::IfBlock, Config, Servers}; use crate::smtp::{ session::{TestSession, VerifyResponse}, ParseTestConfig, TestConfig, }; use smtp::{ - config::{ConfigContext, IfBlock, MaybeDynValue}, + config::ConfigContext, core::{Session, SMTP}, }; @@ -72,17 +72,15 @@ async fn vrfy_expn() { .await .unwrap(); let config = &mut core.session.config.rcpt; - config.directory = IfBlock::new(Some(MaybeDynValue::Static( - directory.directories.get("local").unwrap().clone(), - ))); + config.directory = IfBlock::new("local".to_string()); let config = &mut core.session.config.extensions; config.vrfy = r"[{if = 'remote-ip', eq = '10.0.0.1', then = true}, {else = false}]" - .parse_if(&ctx); + .parse_if(); config.expn = r"[{if = 'remote-ip', eq = '10.0.0.1', then = true}, {else = false}]" - .parse_if(&ctx); + .parse_if(); // EHLO should not avertise VRFY/EXPN to 10.0.0.2 let mut session = Session::test(core); diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index 096ed17b..64c292d1 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -26,7 +26,7 @@ use std::time::Duration; use directory::core::config::ConfigDirectory; use smtp_proto::{AUTH_LOGIN, AUTH_PLAIN}; use store::{config::ConfigStore, Store}; -use utils::config::{Config, DynValue, Servers}; +use utils::config::{if_block::IfBlock, Config, Servers}; use crate::{ directory::DirectoryStore, @@ -37,7 +37,7 @@ use crate::{ store::TempDir, }; use smtp::{ - config::{ConfigContext, EnvelopeKey, IfBlock}, + config::{session::Mechanism, ConfigContext}, core::{Session, SMTP}, }; @@ -142,19 +142,13 @@ async fn lookup_sql() { // Enable AUTH let config = &mut core.session.config.auth; - config.directory = r"'sql'" - .parse_if::>>(&ctx) - .map_if_block(&ctx.directory.directories, "", "") - .unwrap(); - config.mechanisms = IfBlock::new(AUTH_PLAIN | AUTH_LOGIN); + config.directory = r"'sql'".parse_if(); + config.mechanisms = IfBlock::new(Mechanism::from(AUTH_PLAIN | AUTH_LOGIN)); config.errors_wait = IfBlock::new(Duration::from_millis(5)); // Enable VRFY/EXPN/RCPT let config = &mut core.session.config.rcpt; - config.directory = r"'sql'" - .parse_if::>>(&ctx) - .map_if_block(&ctx.directory.directories, "", "") - .unwrap(); + config.directory = r"'sql'".parse_if(); config.relay = IfBlock::new(false); config.errors_wait = IfBlock::new(Duration::from_millis(5)); @@ -162,7 +156,7 @@ async fn lookup_sql() { core.session.config.extensions.requiretls = r"[{if = 'remote-ip', in-list = 'sql/is_ip_allowed', then = true}, {else = false}]" - .parse_if(&ctx); + .parse_if(); let mut session = Session::test(core); session.data.remote_ip = "10.0.0.50".parse().unwrap(); session.eval_session_params().await; diff --git a/tests/src/smtp/lookup/utils.rs b/tests/src/smtp/lookup/utils.rs index 27740f24..7bcaf2ee 100644 --- a/tests/src/smtp/lookup/utils.rs +++ b/tests/src/smtp/lookup/utils.rs @@ -25,7 +25,7 @@ use std::time::{Duration, Instant}; use mail_auth::{IpLookupStrategy, MX}; -use ::smtp::{config::IfBlock, core::SMTP, outbound::NextHop}; +use ::smtp::{core::SMTP, outbound::NextHop}; use mail_parser::DateTime; use smtp::{ config::AggregateFrequency, @@ -35,8 +35,9 @@ use smtp::{ }, queue::RecipientDomain, }; +use utils::config::if_block::IfBlock; -use crate::smtp::TestConfig; +use crate::smtp::{ParseTestConfig, TestConfig}; #[tokio::test] async fn lookup_ip() { @@ -53,8 +54,24 @@ async fn lookup_ip() { "10.0.0.4".parse().unwrap(), ]; let mut core = SMTP::test(); - core.queue.config.source_ip.ipv4 = IfBlock::new(ipv4.clone()); - core.queue.config.source_ip.ipv6 = IfBlock::new(ipv6.clone()); + core.queue.config.source_ip.ipv4 = format!( + "[{}]", + ipv4.iter() + .map(|ip| format!("\"{}\"", ip)) + .collect::>() + .join(",") + ) + .as_str() + .parse_if(); + core.queue.config.source_ip.ipv6 = format!( + "[{}]", + ipv6.iter() + .map(|ip| format!("\"{}\"", ip)) + .collect::>() + .join(",") + ) + .as_str() + .parse_if(); core.resolvers.dns.ipv4_add( "mx.foobar.org", vec![ diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index fb580507..484bfedb 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -32,14 +32,13 @@ use mail_auth::MX; use mail_parser::DateTime; use reqwest::{header::AUTHORIZATION, StatusCode}; use store::{Store, Stores}; -use utils::config::{Config, ServerProtocol, Servers}; +use utils::config::{if_block::IfBlock, Config, ServerProtocol, Servers}; use crate::smtp::{ inbound::TestQueueEvent, management::send_manage_request, outbound::start_test_server, session::TestSession, TestConfig, TestSMTP, }; use smtp::{ - config::IfBlock, core::{management::Message, Session, SMTP}, queue::{ manager::{Queue, SpawnQueue}, @@ -99,13 +98,13 @@ async fn manage_queue() { .parse_directory(&Stores::default(), &Servers::default(), Store::default()) .await .unwrap(); - core.queue.config.directory = directory.directories.get("local").unwrap().clone(); + core.shared.default_directory = directory.directories.get("local").unwrap().clone(); core.session.config.rcpt.relay = IfBlock::new(true); core.session.config.rcpt.max_recipients = IfBlock::new(100); - core.session.config.extensions.future_release = IfBlock::new(Some(Duration::from_secs(86400))); + core.session.config.extensions.future_release = IfBlock::new(Duration::from_secs(86400)); core.session.config.extensions.dsn = IfBlock::new(true); - core.queue.config.retry = IfBlock::new(vec![Duration::from_secs(1000)]); - core.queue.config.notify = IfBlock::new(vec![Duration::from_secs(2000)]); + core.queue.config.retry = IfBlock::new(Duration::from_secs(1000)); + core.queue.config.notify = IfBlock::new(Duration::from_secs(2000)); core.queue.config.expire = IfBlock::new(Duration::from_secs(3000)); let local_qr = core.init_test_queue("smtp_manage_queue_local"); let core = Arc::new(core); diff --git a/tests/src/smtp/management/report.rs b/tests/src/smtp/management/report.rs index a85b7eb1..7871be40 100644 --- a/tests/src/smtp/management/report.rs +++ b/tests/src/smtp/management/report.rs @@ -36,13 +36,13 @@ use mail_auth::{ }; use store::{Store, Stores}; use tokio::sync::mpsc; -use utils::config::{Config, ServerProtocol, Servers}; +use utils::config::{if_block::IfBlock, Config, ServerProtocol, Servers}; use crate::smtp::{ make_temp_dir, management::send_manage_request, outbound::start_test_server, TestConfig, }; use smtp::{ - config::{AggregateFrequency, IfBlock}, + config::AggregateFrequency, core::{management::Report, SMTP}, reporting::{ scheduler::{Scheduler, SpawnReport}, @@ -77,7 +77,7 @@ async fn manage_reports() { let mut core = SMTP::test(); let temp_dir = make_temp_dir("smtp_report_management_test", true); let config = &mut core.report.config; - config.path = IfBlock::new(temp_dir.temp_dir.clone()); + config.path = temp_dir.temp_dir.clone(); config.hash = IfBlock::new(16); config.dmarc_aggregate.max_size = IfBlock::new(1024); config.tls.max_size = IfBlock::new(1024); @@ -86,7 +86,7 @@ async fn manage_reports() { .parse_directory(&Stores::default(), &Servers::default(), Store::default()) .await .unwrap(); - core.queue.config.directory = directory.directories.get("local").unwrap().clone(); + core.shared.default_directory = directory.directories.get("local").unwrap().clone(); let (report_tx, report_rx) = mpsc::channel(1024); core.report.tx = report_tx; let core = Arc::new(core); diff --git a/tests/src/smtp/mod.rs b/tests/src/smtp/mod.rs index 2600c891..bf58b970 100644 --- a/tests/src/smtp/mod.rs +++ b/tests/src/smtp/mod.rs @@ -23,7 +23,6 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; -use ahash::AHashMap; use dashmap::DashMap; use directory::{AddressMapping, Directory, DirectoryInner}; use mail_auth::{ @@ -39,21 +38,24 @@ use tokio::sync::mpsc; use smtp::{ config::{ - if_block::ConfigIf, queue::ConfigQueue, scripts::SieveContext, session::ConfigSession, - throttle::ConfigThrottle, AggregateReport, ArcAuthConfig, Auth, ConfigContext, Connect, - Data, DkimAuthConfig, DmarcAuthConfig, Dsn, Ehlo, EnvelopeKey, Extensions, IfBlock, - IpRevAuthConfig, Mail, MailAuthConfig, Milter, QueueConfig, QueueOutboundSourceIp, - QueueOutboundTimeout, QueueOutboundTls, QueueQuotas, QueueThrottle, Rcpt, Report, - ReportAnalysis, ReportConfig, SessionConfig, SessionThrottle, SpfAuthConfig, Throttle, - VerifyStrategy, + map_expr_token, + queue::ConfigQueue, + scripts::SieveContext, + session::{ConfigSession, Mechanism}, + throttle::ConfigThrottle, + AggregateReport, ArcAuthConfig, Auth, Connect, Data, DkimAuthConfig, DmarcAuthConfig, Dsn, + Ehlo, Extensions, IpRevAuthConfig, Mail, MailAuthConfig, Milter, QueueConfig, + QueueOutboundSourceIp, QueueOutboundTimeout, QueueOutboundTls, QueueQuotas, QueueThrottle, + Rcpt, Report, ReportAnalysis, ReportConfig, SessionConfig, SessionThrottle, SpfAuthConfig, + Throttle, VerifyStrategy, }, core::{ - throttle::ThrottleKeyHasherBuilder, QueueCore, ReportCore, Resolvers, SessionCore, - SieveCore, TlsConnectors, SMTP, + eval::*, throttle::ThrottleKeyHasherBuilder, QueueCore, ReportCore, Resolvers, SessionCore, + Shared, SieveCore, TlsConnectors, SMTP, }, outbound::dane::DnssecResolver, }; -use utils::config::{utils::ParseValues, Config}; +use utils::config::{if_block::IfBlock, Config}; pub mod config; pub mod inbound; @@ -65,92 +67,86 @@ pub mod reporting; pub mod session; pub trait ParseTestConfig { - fn parse_if(&self, ctx: &ConfigContext) -> IfBlock; - fn parse_throttle(&self, ctx: &ConfigContext) -> Vec; - fn parse_quota(&self, ctx: &ConfigContext) -> QueueQuotas; - fn parse_queue_throttle(&self, ctx: &ConfigContext) -> QueueThrottle; - fn parse_milters(&self, ctx: &ConfigContext) -> Vec; + fn parse_if(&self) -> IfBlock; + fn parse_throttle(&self) -> Vec; + fn parse_quota(&self) -> QueueQuotas; + fn parse_queue_throttle(&self) -> QueueThrottle; + fn parse_milters(&self) -> Vec; } impl ParseTestConfig for &str { - fn parse_if(&self, ctx: &ConfigContext) -> IfBlock { + fn parse_if(&self) -> IfBlock { Config::new(&format!("test = {self}\n")) .unwrap() - .parse_if_block( - "test", - ctx, - &[ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Mx, - EnvelopeKey::HeloDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, - ], - ) + .parse_if_block("test", |name| { + map_expr_token::( + name, + &[ + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_MX, + V_HELO_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, + ], + ) + }) .unwrap() .unwrap() } - fn parse_throttle(&self, ctx: &ConfigContext) -> Vec { + fn parse_throttle(&self) -> Vec { Config::new(self) .unwrap() .parse_throttle( "throttle", - ctx, &[ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Mx, - EnvelopeKey::HeloDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_MX, + V_HELO_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, ], u16::MAX, ) .unwrap() } - fn parse_quota(&self, ctx: &ConfigContext) -> QueueQuotas { - Config::new(self).unwrap().parse_queue_quota(ctx).unwrap() + fn parse_quota(&self) -> QueueQuotas { + Config::new(self).unwrap().parse_queue_quota().unwrap() } - fn parse_queue_throttle(&self, ctx: &ConfigContext) -> QueueThrottle { - Config::new(self) - .unwrap() - .parse_queue_throttle(ctx) - .unwrap() + fn parse_queue_throttle(&self) -> QueueThrottle { + Config::new(self).unwrap().parse_queue_throttle().unwrap() } - fn parse_milters(&self, ctx: &ConfigContext) -> Vec { + fn parse_milters(&self) -> Vec { Config::new(self) .unwrap() - .parse_milters( - ctx, - &[ - EnvelopeKey::Recipient, - EnvelopeKey::RecipientDomain, - EnvelopeKey::Sender, - EnvelopeKey::SenderDomain, - EnvelopeKey::Mx, - EnvelopeKey::HeloDomain, - EnvelopeKey::AuthenticatedAs, - EnvelopeKey::Listener, - EnvelopeKey::RemoteIp, - EnvelopeKey::LocalIp, - EnvelopeKey::Priority, - ], - ) + .parse_milters(&[ + V_RECIPIENT, + V_RECIPIENT_DOMAIN, + V_SENDER, + V_SENDER_DOMAIN, + V_MX, + V_HELO_DOMAIN, + V_AUTHENTICATED_AS, + V_LISTENER, + V_REMOTE_IP, + V_LOCAL_IP, + V_PRIORITY, + ]) .unwrap() } } @@ -161,6 +157,7 @@ pub trait TestConfig { impl TestConfig for SMTP { fn test() -> Self { + let store = Store::default(); SMTP { worker_pool: rayon::ThreadPoolBuilder::new() .num_threads(num_cpus::get()) @@ -184,6 +181,23 @@ impl TestConfig for SMTP { report: ReportCore::test(), sieve: SieveCore::test(), delivery_tx: mpsc::channel(1).0, + shared: Shared { + scripts: Default::default(), + signers: Default::default(), + sealers: Default::default(), + directories: Default::default(), + lookup_stores: Default::default(), + relay_hosts: Default::default(), + default_directory: Arc::new(Directory { + store: DirectoryInner::Internal(store.clone()), + catch_all: AddressMapping::Disable, + subaddressing: AddressMapping::Disable, + cache: None, + blocked_ips: Arc::new(Default::default()), + }), + default_lookup_store: LookupStore::Store(store.clone()), + default_data_store: store, + }, } } } @@ -213,10 +227,10 @@ impl TestConfig for SessionConfig { rcpt_to: vec![], }, connect: Connect { - script: IfBlock::new(None), + script: IfBlock::default(), }, ehlo: Ehlo { - script: IfBlock::new(None), + script: IfBlock::default(), require: IfBlock::new(true), reject_non_fqdn: IfBlock::new(false), }, @@ -224,17 +238,17 @@ impl TestConfig for SessionConfig { pipelining: IfBlock::new(true), chunking: IfBlock::new(true), requiretls: IfBlock::new(true), - no_soliciting: IfBlock::new("domain.org".to_string().into()), - future_release: IfBlock::new(None), - deliver_by: IfBlock::new(None), - mt_priority: IfBlock::new(None), + no_soliciting: IfBlock::new("domain.org".to_string()), + future_release: IfBlock::default(), + deliver_by: IfBlock::default(), + mt_priority: IfBlock::default(), dsn: IfBlock::new(true), expn: IfBlock::new(true), vrfy: IfBlock::new(true), }, auth: Auth { - directory: IfBlock::new(None), - mechanisms: IfBlock::new(AUTH_PLAIN | AUTH_LOGIN), + directory: IfBlock::default(), + mechanisms: IfBlock::new(Mechanism::from(AUTH_PLAIN | AUTH_LOGIN)), require: IfBlock::new(false), errors_max: IfBlock::new(10), errors_wait: IfBlock::new(Duration::from_secs(1)), @@ -242,20 +256,20 @@ impl TestConfig for SessionConfig { must_match_sender: IfBlock::new(false), }, mail: Mail { - script: IfBlock::new(None), - rewrite: IfBlock::new(None), + script: IfBlock::default(), + rewrite: IfBlock::default(), }, rcpt: Rcpt { - script: IfBlock::new(None), + script: IfBlock::default(), relay: IfBlock::new(false), - directory: IfBlock::new(None), + directory: IfBlock::default(), errors_max: IfBlock::new(3), errors_wait: IfBlock::new(Duration::from_secs(1)), max_recipients: IfBlock::new(3), - rewrite: IfBlock::new(None), + rewrite: IfBlock::default(), }, data: Data { - script: IfBlock::new(None), + script: IfBlock::default(), max_messages: IfBlock::new(10), max_message_size: IfBlock::new(1024 * 1024), max_received_headers: IfBlock::new(10), @@ -298,20 +312,19 @@ impl TestConfig for QueueCore { impl TestConfig for QueueConfig { fn test() -> Self { - let store = Store::default(); Self { path: Default::default(), hash: IfBlock::new(10), - retry: IfBlock::new(vec![Duration::from_secs(10)]), - notify: IfBlock::new(vec![Duration::from_secs(20)]), + retry: IfBlock::new(Duration::from_secs(10)), + notify: IfBlock::new(Duration::from_secs(20)), expire: IfBlock::new(Duration::from_secs(10)), hostname: IfBlock::new("mx.example.org".to_string()), next_hop: Default::default(), max_mx: IfBlock::new(5), max_multihomed: IfBlock::new(5), source_ip: QueueOutboundSourceIp { - ipv4: IfBlock::new(vec![]), - ipv6: IfBlock::new(vec![]), + ipv4: IfBlock::default(), + ipv6: IfBlock::default(), }, ip_strategy: IfBlock::new(IpLookupStrategy::Ipv4thenIpv6), tls: QueueOutboundTls { @@ -345,15 +358,6 @@ impl TestConfig for QueueConfig { rcpt: vec![], rcpt_domain: vec![], }, - directory: Arc::new(Directory { - store: DirectoryInner::Internal(store.clone()), - catch_all: AddressMapping::Disable, - subaddressing: AddressMapping::Disable, - cache: None, - blocked_ips: Arc::new(Default::default()), - }), - lookup_store: LookupStore::Store(store.clone()), - data_store: store, } } } @@ -443,13 +447,10 @@ impl TestConfig for SieveCore { fn test() -> Self { SieveCore { runtime: Runtime::new_with_context(SieveContext::default()), - scripts: AHashMap::new(), from_addr: "MAILER-DAEMON@example.org".to_string(), from_name: "Mailer Daemon".to_string(), return_path: "".to_string(), sign: vec![], - directories: Default::default(), - lookup_stores: Default::default(), } } } @@ -511,7 +512,7 @@ pub trait TestSMTP { impl TestSMTP for SMTP { fn init_test_queue(&mut self, test_name: &str) -> QueueReceiver { let _temp_dir = make_temp_dir(test_name, true); - self.queue.config.path = IfBlock::new(_temp_dir.temp_dir.clone()); + self.queue.config.path = _temp_dir.temp_dir.clone(); let (queue_tx, queue_rx) = mpsc::channel(128); self.queue.tx = queue_tx; diff --git a/tests/src/smtp/outbound/dane.rs b/tests/src/smtp/outbound/dane.rs index 05bd8625..31626ee3 100644 --- a/tests/src/smtp/outbound/dane.rs +++ b/tests/src/smtp/outbound/dane.rs @@ -45,7 +45,7 @@ use mail_auth::{ Resolver, MX, }; use rustls_pki_types::CertificateDer; -use utils::config::ServerProtocol; +use utils::config::{if_block::IfBlock, ServerProtocol}; use crate::smtp::{ inbound::{TestMessage, TestQueueEvent, TestReportingEvent}, @@ -54,7 +54,7 @@ use crate::smtp::{ TestConfig, TestSMTP, }; use smtp::{ - config::{AggregateFrequency, IfBlock, RequireOptional}, + config::{AggregateFrequency, RequireOptional}, core::{Resolvers, Session, SMTP}, outbound::dane::{DnssecResolver, Tlsa, TlsaEntry}, queue::{manager::Queue, DeliveryAttempt, Error, ErrorDetails, Status}, diff --git a/tests/src/smtp/outbound/extensions.rs b/tests/src/smtp/outbound/extensions.rs index 364e387d..f8c2c18e 100644 --- a/tests/src/smtp/outbound/extensions.rs +++ b/tests/src/smtp/outbound/extensions.rs @@ -28,7 +28,7 @@ use std::{ use mail_auth::MX; use smtp_proto::{MAIL_REQUIRETLS, MAIL_RET_HDRS, MAIL_SMTPUTF8, RCPT_NOTIFY_NEVER}; -use utils::config::ServerProtocol; +use utils::config::{if_block::IfBlock, ServerProtocol}; use crate::smtp::{ inbound::{TestMessage, TestQueueEvent}, @@ -37,7 +37,6 @@ use crate::smtp::{ TestConfig, TestSMTP, }; use smtp::{ - config::IfBlock, core::{Session, SMTP}, queue::{manager::Queue, DeliveryAttempt}, }; diff --git a/tests/src/smtp/outbound/ip_lookup.rs b/tests/src/smtp/outbound/ip_lookup.rs index c09241fb..4b9e4310 100644 --- a/tests/src/smtp/outbound/ip_lookup.rs +++ b/tests/src/smtp/outbound/ip_lookup.rs @@ -27,14 +27,13 @@ use std::{ }; use mail_auth::{IpLookupStrategy, MX}; -use utils::config::ServerProtocol; +use utils::config::{if_block::IfBlock, ServerProtocol}; use crate::smtp::{ inbound::TestQueueEvent, outbound::start_test_server, session::TestSession, TestConfig, TestSMTP, }; use smtp::{ - config::IfBlock, core::{Session, SMTP}, queue::{manager::Queue, DeliveryAttempt}, }; diff --git a/tests/src/smtp/outbound/lmtp.rs b/tests/src/smtp/outbound/lmtp.rs index 9a9898ee..60c3af19 100644 --- a/tests/src/smtp/outbound/lmtp.rs +++ b/tests/src/smtp/outbound/lmtp.rs @@ -33,11 +33,11 @@ use crate::smtp::{ ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{remote::ConfigHost, ConfigContext, IfBlock}, + config::ConfigContext, core::{Session, SMTP}, queue::{manager::Queue, DeliveryAttempt, Event, WorkerResult}, }; -use utils::config::{Config, ServerProtocol}; +use utils::config::{if_block::IfBlock, Config, ServerProtocol}; const REMOTE: &str = " [remote.lmtp] @@ -81,23 +81,20 @@ async fn lmtp_delivery() { let mut ctx = ConfigContext::new(&[]); 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}]" - .parse_if::>(&ctx) - .into_relay_host(&ctx) - .unwrap(); + .parse_if(); core.session.config.rcpt.relay = IfBlock::new(true); core.session.config.rcpt.max_recipients = IfBlock::new(100); core.session.config.extensions.dsn = IfBlock::new(true); let config = &mut core.queue.config; - config.retry = IfBlock::new(vec![Duration::from_millis(100)]); + config.retry = IfBlock::new(Duration::from_millis(100)); config.notify = "[{if = 'rcpt-domain', eq = 'foobar.org', then = ['100ms', '200ms']}, {else = ['100ms']}]" - .parse_if(&ctx); + .parse_if(); config.expire = "[{if = 'rcpt-domain', eq = 'foobar.org', then = '400ms'}, {else = '500ms'}]" - .parse_if(&ctx); + .parse_if(); config.timeout.data = IfBlock::new(Duration::from_millis(50)); let core = Arc::new(core); diff --git a/tests/src/smtp/outbound/mta_sts.rs b/tests/src/smtp/outbound/mta_sts.rs index 5663f870..bba8e923 100644 --- a/tests/src/smtp/outbound/mta_sts.rs +++ b/tests/src/smtp/outbound/mta_sts.rs @@ -32,7 +32,7 @@ use mail_auth::{ report::tlsrpt::ResultType, MX, }; -use utils::config::ServerProtocol; +use utils::config::{if_block::IfBlock, ServerProtocol}; use crate::smtp::{ inbound::{TestMessage, TestQueueEvent, TestReportingEvent}, @@ -41,7 +41,7 @@ use crate::smtp::{ TestConfig, TestSMTP, }; use smtp::{ - config::{AggregateFrequency, IfBlock, RequireOptional}, + config::{AggregateFrequency, RequireOptional}, core::{Session, SMTP}, outbound::mta_sts::{lookup::STS_TEST_POLICY, Policy}, queue::{manager::Queue, DeliveryAttempt}, diff --git a/tests/src/smtp/outbound/smtp.rs b/tests/src/smtp/outbound/smtp.rs index 6ad958f6..c18f92b2 100644 --- a/tests/src/smtp/outbound/smtp.rs +++ b/tests/src/smtp/outbound/smtp.rs @@ -27,7 +27,7 @@ use std::{ }; use mail_auth::MX; -use utils::config::ServerProtocol; +use utils::config::{if_block::IfBlock, ServerProtocol}; use crate::smtp::{ inbound::{TestMessage, TestQueueEvent}, @@ -36,7 +36,7 @@ use crate::smtp::{ ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{ConfigContext, IfBlock}, + config::ConfigContext, core::{Session, SMTP}, queue::{manager::Queue, DeliveryAttempt, Event, WorkerResult}, }; @@ -109,14 +109,14 @@ async fn smtp_delivery() { core.session.config.rcpt.max_recipients = IfBlock::new(100); core.session.config.extensions.dsn = IfBlock::new(true); let config = &mut core.queue.config; - config.retry = IfBlock::new(vec![Duration::from_millis(100)]); + config.retry = IfBlock::new(Duration::from_millis(100)); config.notify = "[{if = 'rcpt-domain', eq = 'foobar.org', then = ['100ms', '200ms']}, {if = 'rcpt-domain', eq = 'foobar.com', then = ['500ms', '600ms']}, {else = ['100ms']}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.expire = "[{if = 'rcpt-domain', eq = 'foobar.org', then = '650ms'}, {else = '750ms'}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); let core = Arc::new(core); let mut queue = Queue::default(); diff --git a/tests/src/smtp/outbound/throttle.rs b/tests/src/smtp/outbound/throttle.rs index 4bfd46f7..0519f9d5 100644 --- a/tests/src/smtp/outbound/throttle.rs +++ b/tests/src/smtp/outbound/throttle.rs @@ -28,13 +28,13 @@ use std::{ }; use mail_auth::MX; +use utils::config::if_block::IfBlock; use crate::smtp::{ inbound::TestQueueEvent, queue::manager::new_message, session::TestSession, ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{ConfigContext, IfBlock}, core::{Session, SMTP}, queue::{manager::Queue, DeliveryAttempt, Message, QueueEnvelope}, }; @@ -86,9 +86,9 @@ async fn throttle_outbound() { let mut core = SMTP::test(); let mut local_qr = core.init_test_queue("smtp_throttle_outbound"); core.session.config.rcpt.relay = IfBlock::new(true); - core.queue.config.throttle = THROTTLE.parse_queue_throttle(&ConfigContext::new(&[])); - core.queue.config.retry = IfBlock::new(vec![Duration::from_secs(86400)]); - core.queue.config.notify = IfBlock::new(vec![Duration::from_secs(86400)]); + core.queue.config.throttle = THROTTLE.parse_queue_throttle(); + core.queue.config.retry = IfBlock::new(Duration::from_secs(86400)); + core.queue.config.notify = IfBlock::new(Duration::from_secs(86400)); core.queue.config.expire = IfBlock::new(Duration::from_secs(86400)); let core = Arc::new(core); @@ -106,15 +106,14 @@ async fn throttle_outbound() { let mut in_flight = vec![]; let throttle = &core.queue.config.throttle; for t in &throttle.sender { - core.queue - .is_allowed( - t, - &QueueEnvelope::test(&test_message, "", ""), - &mut in_flight, - &span, - ) - .await - .unwrap(); + core.is_allowed( + t, + &QueueEnvelope::test(&test_message, "", ""), + &mut in_flight, + &span, + ) + .await + .unwrap(); } assert!(!in_flight.is_empty()); @@ -130,15 +129,14 @@ async fn throttle_outbound() { // Expect rate limit throttle for sender domain 'foobar.net' test_message.return_path_domain = "foobar.net".to_string(); for t in &throttle.sender { - core.queue - .is_allowed( - t, - &QueueEnvelope::test(&test_message, "", ""), - &mut in_flight, - &span, - ) - .await - .unwrap(); + core.is_allowed( + t, + &QueueEnvelope::test(&test_message, "", ""), + &mut in_flight, + &span, + ) + .await + .unwrap(); } assert!(in_flight.is_empty()); session @@ -161,15 +159,14 @@ async fn throttle_outbound() { // Expect concurrency throttle for recipient domain 'example.org' test_message.return_path_domain = "test.net".to_string(); for t in &throttle.rcpt { - core.queue - .is_allowed( - t, - &QueueEnvelope::test(&test_message, "example.org", ""), - &mut in_flight, - &span, - ) - .await - .unwrap(); + core.is_allowed( + t, + &QueueEnvelope::test(&test_message, "example.org", ""), + &mut in_flight, + &span, + ) + .await + .unwrap(); } assert!(!in_flight.is_empty()); session @@ -188,15 +185,14 @@ async fn throttle_outbound() { // Expect rate limit throttle for recipient domain 'example.org' for t in &throttle.rcpt { - core.queue - .is_allowed( - t, - &QueueEnvelope::test(&test_message, "example.net", ""), - &mut in_flight, - &span, - ) - .await - .unwrap(); + core.is_allowed( + t, + &QueueEnvelope::test(&test_message, "example.net", ""), + &mut in_flight, + &span, + ) + .await + .unwrap(); } assert!(in_flight.is_empty()); session @@ -235,15 +231,14 @@ async fn throttle_outbound() { Instant::now() + Duration::from_secs(10), ); for t in &throttle.host { - core.queue - .is_allowed( - t, - &QueueEnvelope::test(&test_message, "test.org", "mx.test.org"), - &mut in_flight, - &span, - ) - .await - .unwrap(); + core.is_allowed( + t, + &QueueEnvelope::test(&test_message, "test.org", "mx.test.org"), + &mut in_flight, + &span, + ) + .await + .unwrap(); } assert!(!in_flight.is_empty()); session @@ -270,15 +265,14 @@ async fn throttle_outbound() { Instant::now() + Duration::from_secs(10), ); for t in &throttle.host { - core.queue - .is_allowed( - t, - &QueueEnvelope::test(&test_message, "example.net", "mx.test.net"), - &mut in_flight, - &span, - ) - .await - .unwrap(); + core.is_allowed( + t, + &QueueEnvelope::test(&test_message, "example.net", "mx.test.net"), + &mut in_flight, + &span, + ) + .await + .unwrap(); } assert!(in_flight.is_empty()); session diff --git a/tests/src/smtp/outbound/tls.rs b/tests/src/smtp/outbound/tls.rs index 6f52b73c..dc8788b2 100644 --- a/tests/src/smtp/outbound/tls.rs +++ b/tests/src/smtp/outbound/tls.rs @@ -27,7 +27,7 @@ use std::{ }; use mail_auth::MX; -use utils::config::ServerProtocol; +use utils::config::{if_block::IfBlock, ServerProtocol}; use crate::smtp::{ inbound::{TestMessage, TestQueueEvent}, @@ -36,7 +36,7 @@ use crate::smtp::{ TestConfig, TestSMTP, }; use smtp::{ - config::{IfBlock, RequireOptional}, + config::RequireOptional, core::{Session, SMTP}, queue::{manager::Queue, DeliveryAttempt}, }; diff --git a/tests/src/smtp/queue/dsn.rs b/tests/src/smtp/queue/dsn.rs index eb25de82..50fbc7b6 100644 --- a/tests/src/smtp/queue/dsn.rs +++ b/tests/src/smtp/queue/dsn.rs @@ -29,14 +29,13 @@ use std::{ use smtp_proto::{Response, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_SUCCESS}; use tokio::{fs::File, io::AsyncReadExt}; -use utils::config::DynValue; use crate::smtp::{ inbound::{sign::TextConfigContext, TestQueueEvent}, ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{ConfigContext, EnvelopeKey}, + config::ConfigContext, core::SMTP, queue::{ DeliveryAttempt, Domain, Error, ErrorDetails, HostResponse, Message, Recipient, Schedule, @@ -110,21 +109,18 @@ async fn generate_dsn() { let mut core = SMTP::test(); let ctx = ConfigContext::new(&[]).parse_signatures(); let config = &mut core.queue.config.dsn; - config.sign = "['rsa']" - .parse_if::>>(&ctx) - .map_if_block(&ctx.signers, "", "") - .unwrap(); + config.sign = "['rsa']".parse_if(); // Create temp dir for queue let mut qr = core.init_test_queue("smtp_dsn_test"); // Disabled DSN - core.queue.send_dsn(&mut attempt).await; + core.send_dsn(&mut attempt).await; qr.assert_empty_queue(); // Failure DSN attempt.message.recipients[0].flags = flags; - core.queue.send_dsn(&mut attempt).await; + core.send_dsn(&mut attempt).await; compare_dsn(qr.read_event().await.unwrap_message(), "failure.eml").await; // Success DSN @@ -143,7 +139,7 @@ async fn generate_dsn() { flags, orcpt: None, }); - core.queue.send_dsn(&mut attempt).await; + core.send_dsn(&mut attempt).await; compare_dsn(qr.read_event().await.unwrap_message(), "success.eml").await; // Delay DSN @@ -155,7 +151,7 @@ async fn generate_dsn() { flags, orcpt: "jdoe@example.org".to_string().into(), }); - core.queue.send_dsn(&mut attempt).await; + core.send_dsn(&mut attempt).await; compare_dsn(qr.read_event().await.unwrap_message(), "delay.eml").await; // Mixed DSN @@ -163,7 +159,7 @@ async fn generate_dsn() { rcpt.flags = flags; } attempt.message.domains[0].notify.due = Instant::now(); - core.queue.send_dsn(&mut attempt).await; + core.send_dsn(&mut attempt).await; compare_dsn(qr.read_event().await.unwrap_message(), "mixed.eml").await; // Load queue diff --git a/tests/src/smtp/queue/retry.rs b/tests/src/smtp/queue/retry.rs index c2fb9967..792c3763 100644 --- a/tests/src/smtp/queue/retry.rs +++ b/tests/src/smtp/queue/retry.rs @@ -32,10 +32,10 @@ use crate::smtp::{ ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{ConfigContext, IfBlock}, core::{Session, SMTP}, queue::{manager::Queue, DeliveryAttempt, Event, WorkerResult}, }; +use utils::config::if_block::IfBlock; #[tokio::test] async fn queue_retry() { @@ -54,20 +54,16 @@ async fn queue_retry() { let config = &mut core.session.config.rcpt; config.relay = IfBlock::new(true); let config = &mut core.session.config.extensions; - config.deliver_by = IfBlock::new(Some(Duration::from_secs(86400))); - config.future_release = IfBlock::new(Some(Duration::from_secs(86400))); + config.deliver_by = IfBlock::new(Duration::from_secs(86400)); + config.future_release = IfBlock::new(Duration::from_secs(86400)); let config = &mut core.queue.config; - config.retry = IfBlock::new(vec![ - Duration::from_millis(100), - Duration::from_millis(200), - Duration::from_millis(300), - ]); + config.retry = "[100ms, 200ms, 300ms]".parse_if(); config.notify = "[{if = 'sender-domain', eq = 'test.org', then = ['150ms', '200ms']}, {else = ['15h', '22h']}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); config.expire = "[{if = 'sender-domain', eq = 'test.org', then = '600ms'}, {else = '1d'}]" - .parse_if(&ConfigContext::new(&[])); + .parse_if(); // Create test message let core = Arc::new(core); diff --git a/tests/src/smtp/reporting/analyze.rs b/tests/src/smtp/reporting/analyze.rs index 66639acd..25d988a2 100644 --- a/tests/src/smtp/reporting/analyze.rs +++ b/tests/src/smtp/reporting/analyze.rs @@ -27,9 +27,10 @@ use crate::smtp::{ inbound::TestQueueEvent, make_temp_dir, session::TestSession, TestConfig, TestSMTP, }; use smtp::{ - config::{AddressMatch, IfBlock}, + config::AddressMatch, core::{Session, SMTP}, }; +use utils::config::if_block::IfBlock; #[tokio::test] async fn report_analyze() { diff --git a/tests/src/smtp/reporting/dmarc.rs b/tests/src/smtp/reporting/dmarc.rs index f3cde714..a9b358b0 100644 --- a/tests/src/smtp/reporting/dmarc.rs +++ b/tests/src/smtp/reporting/dmarc.rs @@ -32,7 +32,7 @@ use mail_auth::{ dmarc::Dmarc, report::{ActionDisposition, Disposition, DmarcResult, Record, Report}, }; -use utils::config::DynValue; +use utils::config::if_block::IfBlock; use crate::smtp::{ inbound::{sign::TextConfigContext, TestMessage, TestQueueEvent}, @@ -41,7 +41,7 @@ use crate::smtp::{ ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{AggregateFrequency, ConfigContext, EnvelopeKey, IfBlock}, + config::{AggregateFrequency, ConfigContext}, core::SMTP, reporting::{ dmarc::GenerateDmarcReport, @@ -64,18 +64,14 @@ async fn report_dmarc() { let ctx = ConfigContext::new(&[]).parse_signatures(); let temp_dir = make_temp_dir("smtp_report_dmarc_test", true); let config = &mut core.report.config; - config.path = IfBlock::new(temp_dir.temp_dir.clone()); + config.path = temp_dir.temp_dir.clone(); config.hash = IfBlock::new(16); - config.dmarc_aggregate.sign = "['rsa']" - .parse_if::>>(&ctx) - .map_if_block(&ctx.signers, "", "") - .unwrap(); + config.dmarc_aggregate.sign = "['rsa']".parse_if(); config.dmarc_aggregate.max_size = IfBlock::new(4096); config.submitter = IfBlock::new("mx.example.org".to_string()); config.dmarc_aggregate.address = IfBlock::new("reports@example.org".to_string()); - config.dmarc_aggregate.org_name = IfBlock::new("Foobar, Inc.".to_string().into()); - config.dmarc_aggregate.contact_info = - IfBlock::new("https://foobar.org/contact".to_string().into()); + config.dmarc_aggregate.org_name = IfBlock::new("Foobar, Inc.".to_string()); + config.dmarc_aggregate.contact_info = IfBlock::new("https://foobar.org/contact".to_string()); let mut scheduler = Scheduler::default(); // Authorize external report for foobar.org diff --git a/tests/src/smtp/reporting/scheduler.rs b/tests/src/smtp/reporting/scheduler.rs index 179cafaa..d06971e7 100644 --- a/tests/src/smtp/reporting/scheduler.rs +++ b/tests/src/smtp/reporting/scheduler.rs @@ -30,10 +30,11 @@ use mail_auth::{ report::{ActionDisposition, Alignment, Disposition, DmarcResult, PolicyPublished, Record}, }; use tokio::fs; +use utils::config::if_block::IfBlock; use crate::smtp::{make_temp_dir, TestConfig}; use smtp::{ - config::{AggregateFrequency, IfBlock}, + config::AggregateFrequency, core::SMTP, reporting::{ dmarc::DmarcFormat, @@ -55,7 +56,7 @@ async fn report_scheduler() { let mut core = SMTP::test(); let temp_dir = make_temp_dir("smtp_report_scheduler_test", true); let config = &mut core.report.config; - config.path = IfBlock::new(temp_dir.temp_dir.clone()); + config.path = temp_dir.temp_dir.clone(); config.hash = IfBlock::new(16); config.dmarc_aggregate.max_size = IfBlock::new(500); config.tls.max_size = IfBlock::new(550); diff --git a/tests/src/smtp/reporting/tls.rs b/tests/src/smtp/reporting/tls.rs index 627a3a89..3a4a21ad 100644 --- a/tests/src/smtp/reporting/tls.rs +++ b/tests/src/smtp/reporting/tls.rs @@ -29,7 +29,7 @@ use mail_auth::{ mta_sts::TlsRpt, report::tlsrpt::{FailureDetails, PolicyType, ResultType, TlsReport}, }; -use utils::config::DynValue; +use utils::config::if_block::IfBlock; use crate::smtp::{ inbound::{sign::TextConfigContext, TestMessage, TestQueueEvent}, @@ -38,7 +38,7 @@ use crate::smtp::{ ParseTestConfig, TestConfig, TestSMTP, }; use smtp::{ - config::{AggregateFrequency, ConfigContext, EnvelopeKey, IfBlock}, + config::{AggregateFrequency, ConfigContext}, core::SMTP, reporting::{ scheduler::{ReportType, Scheduler}, @@ -61,17 +61,14 @@ async fn report_tls() { let ctx = ConfigContext::new(&[]).parse_signatures(); let temp_dir = make_temp_dir("smtp_report_tls_test", true); let config = &mut core.report.config; - config.path = IfBlock::new(temp_dir.temp_dir.clone()); + config.path = temp_dir.temp_dir.clone(); config.hash = IfBlock::new(16); - config.tls.sign = "['rsa']" - .parse_if::>>(&ctx) - .map_if_block(&ctx.signers, "", "") - .unwrap(); + config.tls.sign = "['rsa']".parse_if(); config.tls.max_size = IfBlock::new(4096); config.submitter = IfBlock::new("mx.example.org".to_string()); config.tls.address = IfBlock::new("reports@example.org".to_string()); - config.tls.org_name = IfBlock::new("Foobar, Inc.".to_string().into()); - config.tls.contact_info = IfBlock::new("https://foobar.org/contact".to_string().into()); + config.tls.org_name = IfBlock::new("Foobar, Inc.".to_string()); + config.tls.contact_info = IfBlock::new("https://foobar.org/contact".to_string()); let mut scheduler = Scheduler::default(); // Create temp dir for queue