From da85cad9b72fa61422df92c81c75174041c61606 Mon Sep 17 00:00:00 2001 From: mdecimus <11444311+mdecimus@users.noreply.github.com> Date: Sun, 25 Jan 2026 19:31:08 +0100 Subject: [PATCH] Bootstrap from registry - part 2 --- crates/common/src/addresses.rs | 13 +- crates/common/src/config/inner.rs | 5 +- crates/common/src/config/mailstore/scripts.rs | 77 +- .../common/src/config/mailstore/spamfilter.rs | 674 ++++++------------ crates/common/src/config/mod.rs | 12 - crates/common/src/config/network.rs | 26 +- crates/common/src/config/server/listener.rs | 361 ++++------ crates/common/src/config/server/mod.rs | 17 +- crates/common/src/config/server/tls.rs | 29 +- crates/common/src/config/smtp/auth.rs | 47 +- crates/common/src/config/smtp/mod.rs | 99 --- crates/common/src/config/smtp/queue.rs | 104 +-- crates/common/src/config/smtp/report.rs | 71 +- crates/common/src/config/smtp/session.rs | 137 ++-- crates/common/src/enterprise/alerts.rs | 3 +- crates/common/src/expr/eval.rs | 57 +- crates/common/src/expr/functions/misc.rs | 8 +- crates/common/src/expr/functions/mod.rs | 3 +- crates/common/src/expr/if_block.rs | 111 ++- crates/common/src/expr/mod.rs | 101 +-- crates/common/src/expr/parser.rs | 11 +- crates/common/src/expr/tokenizer.rs | 147 ++-- crates/common/src/listener/mod.rs | 17 +- crates/common/src/manager/bootstrap.rs | 17 + crates/http-proto/src/context.rs | 26 +- crates/registry/src/schema/prelude.rs | 9 + crates/registry/src/types/duration.rs | 5 +- crates/registry/src/types/id.rs | 165 +++-- crates/smtp/src/core/throttle.rs | 52 +- crates/smtp/src/inbound/session.rs | 38 +- crates/smtp/src/outbound/lookup.rs | 6 +- crates/smtp/src/queue/mod.rs | 58 +- crates/spam-filter/src/modules/expression.rs | 161 +++-- tests/src/smtp/config.rs | 118 +-- tests/src/smtp/lookup/sql.rs | 22 +- 35 files changed, 1148 insertions(+), 1659 deletions(-) diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs index b7e24bf6..8ef0fdb2 100644 --- a/crates/common/src/addresses.rs +++ b/crates/common/src/addresses.rs @@ -5,15 +5,14 @@ */ use directory::{Directory, backend::RcptType}; +use registry::schema::enums::ExpressionVariable; use std::borrow::Cow; use utils::config::{Config, utils::AsKey}; use crate::{ Server, config::smtp::session::AddressMapping, - expr::{ - V_RECIPIENT, Variable, functions::ResolveVariable, if_block::IfBlock, tokenizer::TokenMap, - }, + expr::{Variable, functions::ResolveVariable, if_block::IfBlock, tokenizer::TokenMap}, }; impl Server { @@ -153,9 +152,9 @@ impl AddressMapping { config, key, &TokenMap::default().with_variables_map([ - ("address", V_RECIPIENT), - ("email", V_RECIPIENT), - ("rcpt", V_RECIPIENT), + ("address", ExpressionVariable::Rcpt), + ("email", ExpressionVariable::Rcpt), + ("rcpt", ExpressionVariable::Rcpt), ]), ) { AddressMapping::Custom(if_block) @@ -168,7 +167,7 @@ impl AddressMapping { struct Address<'x>(&'x str); impl ResolveVariable for Address<'_> { - fn resolve_variable(&'_ self, _: u32) -> crate::expr::Variable<'_> { + fn resolve_variable(&'_ self, _: ExpressionVariable) -> crate::expr::Variable<'_> { Variable::from(self.0) } diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index 519b0751..def9ad45 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -9,10 +9,7 @@ use crate::{ CacheSwap, Caches, Data, DavResource, DavResources, MailboxCache, MessageStoreCache, MessageUidCache, TlsConnectors, auth::{AccessToken, roles::RolePermissions}, - config::{ - smtp::resolver::{Policy, Tlsa}, - spamfilter::SpamClassifier, - }, + config::smtp::resolver::{Policy, Tlsa}, listener::blocked::BlockedIps, manager::webadmin::WebAdminManager, }; diff --git a/crates/common/src/config/mailstore/scripts.rs b/crates/common/src/config/mailstore/scripts.rs index 54fd2eb2..84ca66ad 100644 --- a/crates/common/src/config/mailstore/scripts.rs +++ b/crates/common/src/config/mailstore/scripts.rs @@ -6,8 +6,7 @@ use crate::{ VERSION_PUBLIC, - config::smtp::SMTP_RCPT_TO_VARS, - expr::{if_block::IfBlock, tokenizer::TokenMap}, + expr::if_block::IfBlock, manager::bootstrap::Bootstrap, scripts::{ functions::{register_functions_trusted, register_functions_untrusted}, @@ -17,7 +16,7 @@ use crate::{ use ahash::AHashMap; use registry::{ schema::{ - prelude::{Object, Property}, + prelude::Object, structs::{ SieveSystemInterpreter, SieveSystemScript, SieveUserInterpreter, SieveUserScript, }, @@ -103,7 +102,6 @@ impl Scripting { .with_max_includes(10) .with_no_capability_check(trusted.no_capability_check) .register_functions(&mut fnc_map_trusted); - let mut trusted_runtime = Runtime::new() .without_capabilities([ Capability::FileInto, @@ -164,51 +162,29 @@ impl Scripting { } } - let token_map = TokenMap::default().with_variables(SMTP_RCPT_TO_VARS); - - let mut scripting = Scripting { + Scripting { untrusted_compiler, untrusted_runtime, trusted_runtime, untrusted_scripts, trusted_scripts, - ..Default::default() - }; - - for (property, from, to) in [ - ( - Property::DefaultFromAddress, - trusted.default_from_address, - &mut scripting.from_addr, - ), - ( - Property::DefaultFromName, - trusted.default_from_name, - &mut scripting.from_name, - ), - ( - Property::DefaultReturnPath, - trusted.default_return_path, - &mut scripting.return_path, - ), - ( - Property::DkimSignDomain, - trusted.dkim_sign_domain, - &mut scripting.sign, - ), - ] { - if let Some(if_block) = IfBlock::try_parse( - bp, + from_addr: bp.compile_expr( Object::SieveSystemScript.singleton(), - property, - from, - &token_map, - ) { - *to = if_block; - } + &trusted.ctx_default_from_address(), + ), + from_name: bp.compile_expr( + Object::SieveSystemScript.singleton(), + &trusted.ctx_default_from_name(), + ), + return_path: bp.compile_expr( + Object::SieveSystemScript.singleton(), + &trusted.ctx_default_return_path(), + ), + sign: bp.compile_expr( + Object::SieveSystemScript.singleton(), + &trusted.ctx_dkim_sign_domain(), + ), } - - scripting } } @@ -220,19 +196,10 @@ impl Default for Scripting { untrusted_compiler: Compiler::new(), untrusted_runtime: Runtime::new(), trusted_runtime: Runtime::new(), - from_addr: IfBlock::new_default::<()>( - Property::DefaultFromAddress, - script.default_from_address, - ), - from_name: IfBlock::new_default::<()>( - Property::DefaultFromName, - script.default_from_name, - ), - return_path: IfBlock::new_default::<()>( - Property::DefaultReturnPath, - script.default_return_path, - ), - sign: IfBlock::new_default::<()>(Property::DkimSignDomain, script.dkim_sign_domain), + from_addr: IfBlock::new_default(script.ctx_default_from_address()), + from_name: IfBlock::new_default(script.ctx_default_from_name()), + return_path: IfBlock::new_default(script.ctx_default_return_path()), + sign: IfBlock::new_default(script.ctx_dkim_sign_domain()), untrusted_scripts: AHashMap::new(), trusted_scripts: AHashMap::new(), } diff --git a/crates/common/src/config/mailstore/spamfilter.rs b/crates/common/src/config/mailstore/spamfilter.rs index f8908575..08034e53 100644 --- a/crates/common/src/config/mailstore/spamfilter.rs +++ b/crates/common/src/config/mailstore/spamfilter.rs @@ -4,14 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{expr::if_block::IfBlock, manager::bootstrap::Bootstrap}; +use crate::{ + expr::{Variable, functions::ResolveVariable, if_block::IfBlock}, + manager::bootstrap::Bootstrap, +}; use ahash::AHashSet; use mail_auth::common::resolver::ToReverseName; use nlp::classifier::model::{CcfhClassifier, FhClassifier}; +use registry::schema::{ + enums::{ExpressionVariable, ModelSize}, + prelude::Object, + structs::{ + self, SpamDnsblServer, SpamDnsblSettings, SpamFileExtension, SpamPyzor, SpamRule, + SpamSettings, SpamTag, + }, +}; use std::{ net::{IpAddr, SocketAddr}, time::Duration, }; +use store::registry::RegistryObject; use tokio::net::lookup_host; use utils::{cache::CacheItemWeight, config::utils::ParseValue, glob::GlobMap}; @@ -171,35 +183,32 @@ pub struct DnsBlServer { impl SpamFilterConfig { pub async fn parse(bp: &mut Bootstrap) -> Self { + let spam = bp.setting_infallible::().await; + SpamFilterConfig { - enabled: config - .property_or_default("spam-filter.enable", "true") - .unwrap_or(true), - card_is_ham: config - .property_or_default("spam-filter.card-is-ham.enable", "true") - .unwrap_or(true), - trusted_reply: config - .property_or_default("spam-filter.trusted-reply.enable", "true") - .unwrap_or(true), - dnsbl: DnsBlConfig::parse(config), - rules: SpamFilterRules::parse(config), - lists: SpamFilterLists::parse(config), - pyzor: PyzorConfig::parse(config).await, - classifier: ClassifierConfig::parse(config), - scores: SpamFilterScoreConfig::parse(config), - grey_list_expiry: config - .property::>("spam-filter.grey-list.duration") - .unwrap_or_default() - .map(|d| d.as_secs()), + enabled: spam.enable, + card_is_ham: spam.trust_contacts, + trusted_reply: spam.trust_replies, + dnsbl: DnsBlConfig::parse(bp).await, + rules: SpamFilterRules::parse(bp).await, + lists: SpamFilterLists::parse(bp).await, + pyzor: PyzorConfig::parse(bp).await, + classifier: ClassifierConfig::parse(bp).await, + scores: SpamFilterScoreConfig { + reject_threshold: spam.score_reject as f32, + discard_threshold: spam.score_discard as f32, + spam_threshold: spam.score_spam as f32, + }, + grey_list_expiry: spam.greylist_for.map(|d| d.into_inner().as_secs()), } } } impl SpamFilterRules { - pub fn parse(bp: &mut Bootstrap) -> SpamFilterRules { + pub async fn parse(bp: &mut Bootstrap) -> SpamFilterRules { let mut rules = vec![]; - for id in config.sub_keys("spam-filter.rule", ".scope") { - if let Some(rule) = SpamFilterRule::parse(config, id) { + for rule in bp.list_infallible::().await { + if let Some(rule) = SpamFilterRule::parse(bp, rule) { rules.push(rule); } } @@ -230,162 +239,160 @@ struct SpamFilterRule { } impl SpamFilterRule { - pub fn parse(bp: &mut Bootstrap, id: String) -> Option { - let id = id.as_str(); - if !config - .property_or_default(("spam-filter.rule", id, "enable"), "true") - .unwrap_or(true) - { - return None; + pub fn parse(bp: &mut Bootstrap, obj: RegistryObject) -> Option { + match obj.object { + SpamRule::Any(rule) if rule.enable => SpamFilterRule { + rule: bp.compile_expr(obj.id, &rule.ctx_condition()), + scope: Element::Any, + priority: rule.priority as i32, + } + .into(), + SpamRule::Url(rule) if rule.enable => SpamFilterRule { + rule: bp.compile_expr(obj.id, &rule.ctx_condition()), + scope: Element::Url, + priority: rule.priority as i32, + } + .into(), + SpamRule::Domain(rule) if rule.enable => SpamFilterRule { + rule: bp.compile_expr(obj.id, &rule.ctx_condition()), + scope: Element::Domain, + priority: rule.priority as i32, + } + .into(), + SpamRule::Email(rule) if rule.enable => SpamFilterRule { + rule: bp.compile_expr(obj.id, &rule.ctx_condition()), + scope: Element::Email, + priority: rule.priority as i32, + } + .into(), + SpamRule::Ip(rule) if rule.enable => SpamFilterRule { + rule: bp.compile_expr(obj.id, &rule.ctx_condition()), + scope: Element::Ip, + priority: rule.priority as i32, + } + .into(), + SpamRule::Header(rule) if rule.enable => SpamFilterRule { + rule: bp.compile_expr(obj.id, &rule.ctx_condition()), + scope: Element::Header, + priority: rule.priority as i32, + } + .into(), + SpamRule::Body(rule) if rule.enable => SpamFilterRule { + rule: bp.compile_expr(obj.id, &rule.ctx_condition()), + scope: Element::Body, + priority: rule.priority as i32, + } + .into(), + _ => None, } - let priority = config - .property_or_default(("spam-filter.rule", id, "priority"), "0") - .unwrap_or(0); - let scope = config - .property_or_default::(("spam-filter.rule", id, "scope"), "any") - .unwrap_or_default(); - - SpamFilterRule { - rule: IfBlock::try_parse( - config, - ("spam-filter.rule", id, "condition"), - &scope.token_map(), - )?, - scope, - priority, - } - .into() } } impl DnsBlConfig { - pub fn parse(bp: &mut Bootstrap) -> Self { + pub async fn parse(bp: &mut Bootstrap) -> Self { let mut servers = vec![]; - for id in config.sub_keys("spam-filter.dnsbl.server", ".scope") { - if let Some(server) = DnsBlServer::parse(config, id) { + for server in bp.list_infallible::().await { + if let Some(server) = DnsBlServer::parse(bp, server) { servers.push(server); } } + let dnsbl = bp.setting_infallible::().await; DnsBlConfig { - max_ip_checks: config - .property_or_default("spam-filter.dnsbl.max-check.ip", "50") - .unwrap_or(20), - max_domain_checks: config - .property_or_default("spam-filter.dnsbl.max-check.domain", "50") - .unwrap_or(20), - max_email_checks: config - .property_or_default("spam-filter.dnsbl.max-check.email", "50") - .unwrap_or(20), - max_url_checks: config - .property_or_default("spam-filter.dnsbl.max-check.url", "50") - .unwrap_or(20), + max_ip_checks: dnsbl.ip_limit as usize, + max_domain_checks: dnsbl.domain_limit as usize, + max_email_checks: dnsbl.email_limit as usize, + max_url_checks: dnsbl.url_limit as usize, servers, } } } impl DnsBlServer { - pub fn parse(bp: &mut Bootstrap, id: String) -> Option { - let id_ = id.as_str(); - - if !config - .property_or_default(("spam-filter.dnsbl.server", id_, "enable"), "true") - .unwrap_or(true) - { - return None; + pub fn parse(bp: &mut Bootstrap, obj: RegistryObject) -> Option { + match obj.object { + SpamDnsblServer::Any(server) if server.enable => DnsBlServer { + zone: bp.compile_expr(obj.id, &server.ctx_zone()), + tags: bp.compile_expr(obj.id, &server.ctx_tag()), + scope: Element::Any, + id: server.name, + } + .into(), + SpamDnsblServer::Url(server) if server.enable => DnsBlServer { + zone: bp.compile_expr(obj.id, &server.ctx_zone()), + tags: bp.compile_expr(obj.id, &server.ctx_tag()), + scope: Element::Url, + id: server.name, + } + .into(), + SpamDnsblServer::Domain(server) if server.enable => DnsBlServer { + zone: bp.compile_expr(obj.id, &server.ctx_zone()), + tags: bp.compile_expr(obj.id, &server.ctx_tag()), + scope: Element::Domain, + id: server.name, + } + .into(), + SpamDnsblServer::Email(server) if server.enable => DnsBlServer { + zone: bp.compile_expr(obj.id, &server.ctx_zone()), + tags: bp.compile_expr(obj.id, &server.ctx_tag()), + scope: Element::Email, + id: server.name, + } + .into(), + SpamDnsblServer::Ip(server) if server.enable => DnsBlServer { + zone: bp.compile_expr(obj.id, &server.ctx_zone()), + tags: bp.compile_expr(obj.id, &server.ctx_tag()), + scope: Element::Ip, + id: server.name, + } + .into(), + SpamDnsblServer::Header(server) if server.enable => DnsBlServer { + zone: bp.compile_expr(obj.id, &server.ctx_zone()), + tags: bp.compile_expr(obj.id, &server.ctx_tag()), + scope: Element::Header, + id: server.name, + } + .into(), + SpamDnsblServer::Body(server) if server.enable => DnsBlServer { + zone: bp.compile_expr(obj.id, &server.ctx_zone()), + tags: bp.compile_expr(obj.id, &server.ctx_tag()), + scope: Element::Body, + id: server.name, + } + .into(), + _ => None, } - - let scope = - config.property_require::(("spam-filter.dnsbl.server", id_, "scope"))?; - - DnsBlServer { - zone: IfBlock::try_parse( - config, - ("spam-filter.dnsbl.server", id_, "zone"), - &scope.token_map(), - )?, - scope, - tags: IfBlock::try_parse( - config, - ("spam-filter.dnsbl.server", id_, "tag"), - &Element::Ip.token_map(), - )?, - id, - } - .into() } } impl SpamFilterLists { - pub fn parse(bp: &mut Bootstrap) -> Self { + pub async fn parse(bp: &mut Bootstrap) -> Self { let mut lists = SpamFilterLists { file_extensions: GlobMap::default(), scores: GlobMap::default(), }; - // Parse local lists - let mut errors = vec![]; - for (key, value) in config.iterate_prefix("spam-filter.list") { - if let Some((id, key)) = key - .split_once('.') - .filter(|(id, key)| !id.is_empty() && !key.is_empty()) - { - match id { - "scores" => { - let action = match value.to_lowercase().as_str() { - "reject" => SpamFilterAction::Reject, - "discard" => SpamFilterAction::Discard, - score => match score.parse() { - Ok(score) => SpamFilterAction::Allow(score), - Err(err) => { - errors.push(( - format!("spam-filter.list.{id}.{key}"), - format!("Invalid score: {}", err), - )); - continue; - } - }, - }; - lists.scores.insert(key, action); - } - "file-extensions" => { - let mut ext = FileExtension::default(); - - for part in value.split('|') { - let part = part.trim(); - match part { - "AR" => { - ext.is_archive = true; - } - "NZ" => { - ext.is_nz = true; - } - "BAD" => { - ext.is_bad = true; - } - other => { - if other.contains('/') { - ext.known_types.insert(other.to_string()); - } else if !other.is_empty() { - errors.push(( - format!("spam-filter.list.{id}.{key}"), - format!("Invalid file extension: {}", other), - )); - } - } - } - } - - lists.file_extensions.insert(key, ext); - } - _ => (), - } + for tag in bp.list_infallible::().await { + match tag.object { + SpamTag::Score(tag) => lists + .scores + .insert(&tag.tag, SpamFilterAction::Allow(tag.score as f32)), + SpamTag::Discard(tag) => lists.scores.insert(&tag.tag, SpamFilterAction::Discard), + SpamTag::Reject(tag) => lists.scores.insert(&tag.tag, SpamFilterAction::Reject), } } - for (key, error) in errors { - config.new_parse_error(key, error); + for ext in bp.list_infallible::().await { + let ext = ext.object; + lists.file_extensions.insert( + &ext.extension, + FileExtension { + known_types: ext.content_types.into_iter().collect(), + is_bad: ext.is_bad, + is_archive: ext.is_archive, + is_nz: ext.is_nz, + }, + ); } lists @@ -394,34 +401,29 @@ impl SpamFilterLists { impl PyzorConfig { pub async fn parse(bp: &mut Bootstrap) -> Option { - if !config - .property_or_default("spam-filter.pyzor.enable", "true") - .unwrap_or(true) - { + let pyzor = bp.setting_infallible::().await; + + if !pyzor.enable { return None; } - let port = config - .property_or_default::("spam-filter.pyzor.port", "24441") - .unwrap_or(24441); - let host = config - .value("spam-filter.pyzor.host") - .unwrap_or("public.pyzor.org"); + let port = pyzor.port; + let host = pyzor.host; let address = match lookup_host(format!("{host}:{port}")) .await .map(|mut a| a.next()) { Ok(Some(address)) => address, Ok(None) => { - config.new_build_error( - "spam-filter.pyzor.host", + bp.build_error( + Object::SpamPyzor.singleton(), "Invalid address: No addresses found.", ); return None; } Err(err) => { - config.new_build_error( - "spam-filter.pyzor.host", + bp.build_error( + Object::SpamPyzor.singleton(), format!("Invalid address: {}", err), ); return None; @@ -430,122 +432,81 @@ impl PyzorConfig { PyzorConfig { address, - timeout: config - .property_or_default::("spam-filter.pyzor.timeout", "5s") - .unwrap_or(Duration::from_secs(5)), - min_count: config - .property_or_default("spam-filter.pyzor.count", "5") - .unwrap_or(5), - min_wl_count: config - .property_or_default("spam-filter.pyzor.wl-count", "10") - .unwrap_or(10), - ratio: config - .property_or_default("spam-filter.pyzor.ratio", "0.2") - .unwrap_or(0.2), + timeout: pyzor.timeout.into_inner(), + min_count: pyzor.block_count, + min_wl_count: pyzor.allow_count, + ratio: pyzor.ratio, } .into() } } impl ClassifierConfig { - pub fn parse(bp: &mut Bootstrap) -> Option { - let ccfh = match config.value("spam-filter.classifier.model") { - Some("ftrl-fh") | None => false, - Some("ftrl-ccfh") => true, - Some("disabled" | "disable") => return None, - Some(other) => { - config.new_build_error( - "spam-filter.classifier.model", - format!("Invalid model type: {}", other), - ); - return None; - } - }; + pub async fn parse(bp: &mut Bootstrap) -> Option { + let classifier = bp.setting_infallible::().await; + let w_params; + let i_params; + let log_scale; + let l2_normalize; - let w_params = FtrlParameters::parse(config, "spam-filter.classifier.parameters", 20); - let i_params = if ccfh { - Some(FtrlParameters::parse( - config, - "spam-filter.classifier.parameters.ccfh", - w_params.feature_hash_size - 2, - )) - } else { - None - }; + match classifier.model { + structs::SpamClassifierModel::FtrlFh(model) => { + log_scale = model.feature_log_scale; + l2_normalize = model.feature_l2_normalize; + w_params = FtrlParameters::parse(&model.parameters); + i_params = None; + } + structs::SpamClassifierModel::FtrlCcfh(model) => { + log_scale = model.feature_log_scale; + l2_normalize = model.feature_l2_normalize; + w_params = FtrlParameters::parse(&model.parameters); + i_params = Some(FtrlParameters::parse(&model.indicator_parameters)); + } + structs::SpamClassifierModel::None => return None, + } ClassifierConfig { w_params, i_params, - reservoir_capacity: config - .property_or_default("spam-filter.classifier.samples.reservoir-capacity", "1024") - .unwrap_or(1024), - auto_learn_card_is_ham: config - .property_or_default("spam-filter.card-is-ham.learn", "true") - .unwrap_or(true), - auto_learn_reply_ham: config - .property_or_default("spam-filter.trusted-reply.learn", "true") - .unwrap_or(true), - auto_learn_spam_trap: config - .property_or_default("spam-filter.classifier.auto-learn.spam-trap", "true") - .unwrap_or(true), - auto_learn_spam_rbl_count: config - .property_or_default("spam-filter.classifier.auto-learn.spam-rbl-count", "2") - .unwrap_or(2), - hold_samples_for: config - .property_or_default::("spam-filter.classifier.samples.hold-for", "180d") - .unwrap_or(Duration::from_secs(180 * 24 * 60 * 60)) - .as_secs(), - min_ham_samples: config - .property_or_default("spam-filter.classifier.samples.min-ham", "100") - .unwrap_or(100), - min_spam_samples: config - .property_or_default("spam-filter.classifier.samples.min-spam", "100") - .unwrap_or(100), - train_frequency: config - .property_or_default::>( - "spam-filter.classifier.training.frequency", - "12h", - ) - .unwrap_or(Some(Duration::from_secs(12 * 60 * 60))) - .map(|d| d.as_secs()), - log_scale: config - .property_or_default("spam-filter.classifier.features.log-scale", "true") - .unwrap_or(true), - l2_normalize: config - .property_or_default("spam-filter.classifier.features.l2-normalize", "true") - .unwrap_or(true), + reservoir_capacity: classifier.reservoir_capacity as usize, + auto_learn_card_is_ham: classifier.learn_ham_from_card, + auto_learn_reply_ham: classifier.learn_ham_from_reply, + auto_learn_spam_trap: classifier.learn_spam_from_traps, + auto_learn_spam_rbl_count: classifier.learn_spam_from_rbl_hits as u32, + hold_samples_for: classifier.hold_samples_for.into_inner().as_secs(), + min_ham_samples: classifier.min_ham_samples, + min_spam_samples: classifier.min_spam_samples, + train_frequency: classifier.train_frequency.map(|d| d.into_inner().as_secs()), + log_scale, + l2_normalize, } .into() } } impl FtrlParameters { - pub fn parse(bp: &mut Bootstrap, prefix: &str, default_features: usize) -> Self { - let feature_hash_size: usize = config - .property((prefix, "features")) - .unwrap_or(default_features); - - if !(16..=28).contains(&feature_hash_size) { - config.new_build_error( - (prefix, "features"), - "Feature size must be between 2^16 and 2^28.", - ); - } - + pub fn parse(params: &structs::FtrlParameters) -> Self { + let hash_size = match params.num_features { + ModelSize::V16 => 16, + ModelSize::V17 => 17, + ModelSize::V18 => 18, + ModelSize::V19 => 19, + ModelSize::V20 => 20, + ModelSize::V21 => 21, + ModelSize::V22 => 22, + ModelSize::V23 => 23, + ModelSize::V24 => 24, + ModelSize::V25 => 25, + ModelSize::V26 => 26, + ModelSize::V27 => 27, + ModelSize::V28 => 28, + }; FtrlParameters { - feature_hash_size: 1 << feature_hash_size, - alpha: config - .property_or_default((prefix, "alpha"), "2.0") - .unwrap_or(2.0), - beta: config - .property_or_default((prefix, "beta"), "1.0") - .unwrap_or(1.0), - l1_ratio: config - .property_or_default((prefix, "l1"), "0.001") - .unwrap_or(0.001), - l2_ratio: config - .property_or_default((prefix, "l2"), "0.0001") - .unwrap_or(0.0001), + feature_hash_size: 1 << hash_size, + alpha: params.alpha, + beta: params.beta, + l1_ratio: params.l1_ratio, + l2_ratio: params.l2_ratio, } } } @@ -556,22 +517,6 @@ impl SpamClassifier { } } -impl SpamFilterScoreConfig { - pub fn parse(bp: &mut Bootstrap) -> Self { - SpamFilterScoreConfig { - reject_threshold: config - .property("spam-filter.score.reject") - .unwrap_or_default(), - discard_threshold: config - .property("spam-filter.score.discard") - .unwrap_or_default(), - spam_threshold: config - .property_or_default("spam-filter.score.spam", "5.0") - .unwrap_or(5.0), - } - } -} - impl ParseValue for Element { fn parse_value(value: &str) -> utils::config::Result { match value { @@ -611,170 +556,7 @@ impl Location { } } -pub const V_SPAM_REMOTE_IP: u32 = 100; -pub const V_SPAM_REMOTE_IP_PTR: u32 = 101; -pub const V_SPAM_EHLO_DOMAIN: u32 = 102; -pub const V_SPAM_AUTH_AS: u32 = 103; -pub const V_SPAM_ASN: u32 = 104; -pub const V_SPAM_COUNTRY: u32 = 105; -pub const V_SPAM_IS_TLS: u32 = 106; -pub const V_SPAM_ENV_FROM: u32 = 108; -pub const V_SPAM_ENV_FROM_LOCAL: u32 = 109; -pub const V_SPAM_ENV_FROM_DOMAIN: u32 = 110; -pub const V_SPAM_ENV_TO: u32 = 111; -pub const V_SPAM_FROM: u32 = 112; -pub const V_SPAM_FROM_NAME: u32 = 113; -pub const V_SPAM_FROM_LOCAL: u32 = 114; -pub const V_SPAM_FROM_DOMAIN: u32 = 115; -pub const V_SPAM_REPLY_TO: u32 = 116; -pub const V_SPAM_REPLY_TO_NAME: u32 = 117; -pub const V_SPAM_REPLY_TO_LOCAL: u32 = 118; -pub const V_SPAM_REPLY_TO_DOMAIN: u32 = 119; -pub const V_SPAM_TO: u32 = 120; -pub const V_SPAM_TO_NAME: u32 = 121; -pub const V_SPAM_TO_LOCAL: u32 = 122; -pub const V_SPAM_TO_DOMAIN: u32 = 123; -pub const V_SPAM_CC: u32 = 124; -pub const V_SPAM_CC_NAME: u32 = 125; -pub const V_SPAM_CC_LOCAL: u32 = 126; -pub const V_SPAM_CC_DOMAIN: u32 = 127; -pub const V_SPAM_BCC: u32 = 128; -pub const V_SPAM_BCC_NAME: u32 = 129; -pub const V_SPAM_BCC_LOCAL: u32 = 130; -pub const V_SPAM_BCC_DOMAIN: u32 = 131; -pub const V_SPAM_BODY_TEXT: u32 = 132; -pub const V_SPAM_BODY_HTML: u32 = 133; -pub const V_SPAM_BODY_RAW: u32 = 134; -pub const V_SPAM_SUBJECT: u32 = 135; -pub const V_SPAM_SUBJECT_THREAD: u32 = 136; -pub const V_SPAM_LOCATION: u32 = 137; -pub const V_WORDS_SUBJECT: u32 = 138; -pub const V_WORDS_BODY: u32 = 139; - -pub const V_RCPT_EMAIL: u32 = 0; -pub const V_RCPT_NAME: u32 = 1; -pub const V_RCPT_LOCAL: u32 = 2; -pub const V_RCPT_DOMAIN: u32 = 3; -pub const V_RCPT_DOMAIN_SLD: u32 = 4; - -pub const V_URL_FULL: u32 = 0; -pub const V_URL_PATH_QUERY: u32 = 1; -pub const V_URL_PATH: u32 = 2; -pub const V_URL_QUERY: u32 = 3; -pub const V_URL_SCHEME: u32 = 4; -pub const V_URL_AUTHORITY: u32 = 5; -pub const V_URL_HOST: u32 = 6; -pub const V_URL_HOST_SLD: u32 = 7; -pub const V_URL_PORT: u32 = 8; - -pub const V_HEADER_NAME: u32 = 0; -pub const V_HEADER_NAME_LOWER: u32 = 1; -pub const V_HEADER_VALUE: u32 = 2; -pub const V_HEADER_VALUE_LOWER: u32 = 3; -pub const V_HEADER_PROPERTY: u32 = 4; -pub const V_HEADER_RAW: u32 = 5; -pub const V_HEADER_RAW_LOWER: u32 = 6; - -pub const V_IP: u32 = 0; -pub const V_IP_REVERSE: u32 = 1; -pub const V_IP_OCTETS: u32 = 2; -pub const V_IP_IS_V4: u32 = 3; -pub const V_IP_IS_V6: u32 = 4; - impl Element { - pub fn token_map(&self) -> TokenMap { - let map = TokenMap::default().with_variables_map([ - ("remote_ip", V_SPAM_REMOTE_IP), - ("remote_ip.ptr", V_SPAM_REMOTE_IP_PTR), - ("ehlo_domain", V_SPAM_EHLO_DOMAIN), - ("auth_as", V_SPAM_AUTH_AS), - ("asn", V_SPAM_ASN), - ("country", V_SPAM_COUNTRY), - ("is_tls", V_SPAM_IS_TLS), - ("env_from", V_SPAM_ENV_FROM), - ("env_from.local", V_SPAM_ENV_FROM_LOCAL), - ("env_from.domain", V_SPAM_ENV_FROM_DOMAIN), - ("env_to", V_SPAM_ENV_TO), - ("from", V_SPAM_FROM), - ("from.name", V_SPAM_FROM_NAME), - ("from.local", V_SPAM_FROM_LOCAL), - ("from.domain", V_SPAM_FROM_DOMAIN), - ("reply_to", V_SPAM_REPLY_TO), - ("reply_to.name", V_SPAM_REPLY_TO_NAME), - ("reply_to.local", V_SPAM_REPLY_TO_LOCAL), - ("reply_to.domain", V_SPAM_REPLY_TO_DOMAIN), - ("to", V_SPAM_TO), - ("to.name", V_SPAM_TO_NAME), - ("to.local", V_SPAM_TO_LOCAL), - ("to.domain", V_SPAM_TO_DOMAIN), - ("cc", V_SPAM_CC), - ("cc.name", V_SPAM_CC_NAME), - ("cc.local", V_SPAM_CC_LOCAL), - ("cc.domain", V_SPAM_CC_DOMAIN), - ("bcc", V_SPAM_BCC), - ("bcc.name", V_SPAM_BCC_NAME), - ("bcc.local", V_SPAM_BCC_LOCAL), - ("bcc.domain", V_SPAM_BCC_DOMAIN), - ("body", V_SPAM_BODY_TEXT), - ("body.text", V_SPAM_BODY_TEXT), - ("body.html", V_SPAM_BODY_HTML), - ("body.words", V_WORDS_BODY), - ("body.raw", V_SPAM_BODY_RAW), - ("subject", V_SPAM_SUBJECT), - ("subject.thread", V_SPAM_SUBJECT_THREAD), - ("subject.words", V_WORDS_SUBJECT), - ("location", V_SPAM_LOCATION), - ]); - - match self { - Element::Url => map.with_variables_map([ - ("url", V_URL_FULL), - ("value", V_URL_FULL), - ("path_query", V_URL_PATH_QUERY), - ("path", V_URL_PATH), - ("query", V_URL_QUERY), - ("scheme", V_URL_SCHEME), - ("authority", V_URL_AUTHORITY), - ("host", V_URL_HOST), - ("sld", V_URL_HOST_SLD), - ("port", V_URL_PORT), - ]), - Element::Email => map.with_variables_map([ - ("email", V_RCPT_EMAIL), - ("value", V_RCPT_EMAIL), - ("name", V_RCPT_NAME), - ("local", V_RCPT_LOCAL), - ("domain", V_RCPT_DOMAIN), - ("sld", V_RCPT_DOMAIN_SLD), - ]), - Element::Ip => map.with_variables_map([ - ("ip", V_IP), - ("value", V_IP), - ("input", V_IP), - ("reverse_ip", V_IP_REVERSE), - ("ip_reverse", V_IP_REVERSE), - ("octets", V_IP_OCTETS), - ("is_v4", V_IP_IS_V4), - ("is_v6", V_IP_IS_V6), - ]), - Element::Header => map.with_variables_map([ - ("name", V_HEADER_NAME), - ("name_lower", V_HEADER_NAME_LOWER), - ("value", V_HEADER_VALUE), - ("value_lower", V_HEADER_VALUE_LOWER), - ("email", V_HEADER_VALUE), - ("email_lower", V_HEADER_VALUE_LOWER), - ("attributes", V_HEADER_PROPERTY), - ("raw", V_HEADER_RAW), - ("raw_lower", V_HEADER_RAW_LOWER), - ]), - Element::Body | Element::Domain => { - map.with_variables_map([("input", 0), ("value", 0), ("result", 0)]) - } - Element::Any => map, - } - } - pub fn as_str(&self) -> &'static str { match self { Element::Url => "url", @@ -796,13 +578,13 @@ pub struct IpResolver { } impl ResolveVariable for IpResolver { - fn resolve_variable(&self, variable: u32) -> Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> { match variable { - V_IP => self.ip_string.as_str().into(), - V_IP_REVERSE => self.reverse.as_str().into(), - V_IP_OCTETS => self.octets.clone(), - V_IP_IS_V4 => Variable::Integer(self.ip.is_ipv4() as _), - V_IP_IS_V6 => Variable::Integer(self.ip.is_ipv6() as _), + ExpressionVariable::Ip | ExpressionVariable::Value => self.ip_string.as_str().into(), + ExpressionVariable::IpReverse => self.reverse.as_str().into(), + ExpressionVariable::Octets => self.octets.clone(), + ExpressionVariable::IsV4 => Variable::Integer(self.ip.is_ipv4() as _), + ExpressionVariable::IsV6 => Variable::Integer(self.ip.is_ipv6() as _), _ => Variable::Integer(0), } } diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index c59dcd26..371bb75f 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -29,18 +29,6 @@ pub mod smtp; pub mod storage; pub mod telemetry; -pub(crate) const CONNECTION_VARS: &[u32; 9] = &[ - V_LISTENER, - V_REMOTE_IP, - V_REMOTE_PORT, - V_LOCAL_IP, - V_LOCAL_PORT, - V_PROTOCOL, - V_TLS, - V_ASN, - V_COUNTRY, -]; - impl Core { pub async fn parse( bp: &mut Bootstrap, diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index a68a8789..b16d66dc 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -100,17 +100,17 @@ pub struct FieldOrDefault { } pub(crate) const HTTP_VARS: &[u32; 11] = &[ - V_LISTENER, - V_REMOTE_IP, - V_REMOTE_PORT, - V_LOCAL_IP, - V_LOCAL_PORT, - V_PROTOCOL, - V_TLS, - V_URL, - V_URL_PATH, - V_HEADERS, - V_METHOD, + ExpressionVariable::Listener, + ExpressionVariable::RemoteIp, + ExpressionVariable::RemotePort, + ExpressionVariable::LocalIp, + ExpressionVariable::LocalPort, + ExpressionVariable::Protocol, + ExpressionVariable::IsTls, + ExpressionVariable::Url, + ExpressionVariable::UrlPath, + ExpressionVariable::Headers, + ExpressionVariable::Method, ]; impl Default for Network { @@ -119,12 +119,12 @@ impl Default for Network { security: Default::default(), contact_form: None, node_id: 1, - http_response_url: IfBlock::new_default::<()>( + http_response_url: IfBlock::new_default( "http.url", [], "protocol + '://' + config_get('server.hostname') + ':' + local_port", ), - http_allowed_endpoint: IfBlock::new_default::<()>("http.allowed-endpoint", [], "200"), + http_allowed_endpoint: IfBlock::new_default("http.allowed-endpoint", [], "200"), asn_geo_lookup: AsnGeoLookupConfig::Disabled, server_name: Default::default(), report_domain: Default::default(), diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index 17bf0064..bea2b62f 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -4,67 +4,78 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{net::SocketAddr, sync::Arc, time::Duration}; - -use rustls::{ - ALL_VERSIONS, ServerConfig, SupportedCipherSuite, - crypto::ring::{ALL_CIPHER_SUITES, default_provider}, -}; - -use tokio::net::TcpSocket; -use tokio_rustls::TlsAcceptor; -use utils::{ - config::{ - Config, - utils::{AsKey, ParseValue}, - }, - snowflake::SnowflakeIdGenerator, -}; - -use crate::{ - Inner, - listener::{TcpAcceptor, tls::CertificateResolver}, -}; - use super::{ Listener, Listeners, ServerProtocol, TcpListener, tls::{TLS12_VERSION, TLS13_VERSION}, }; +use crate::{ + Inner, + listener::{TcpAcceptor, tls::CertificateResolver}, + manager::bootstrap::Bootstrap, +}; +use registry::schema::{ + enums::{NetworkListenerProtocol, TlsCipherSuite, TlsVersion}, + structs::NetworkListener, +}; +use rustls::{ + ALL_VERSIONS, ServerConfig, SupportedCipherSuite, + crypto::ring::{ALL_CIPHER_SUITES, cipher_suite::*, default_provider}, +}; +use std::sync::Arc; +use store::registry::RegistryObject; +use tokio::net::TcpSocket; +use tokio_rustls::TlsAcceptor; +use utils::snowflake::SnowflakeIdGenerator; impl Listeners { - pub fn parse(bp: &mut Bootstrap) -> Self { + pub async fn parse(bp: &mut Bootstrap) -> Self { // Parse ACME managers let mut servers = Listeners { - span_id_gen: Arc::new( - config - .property::("cluster.node-id") - .map(SnowflakeIdGenerator::with_node_id) - .unwrap_or_default(), - ), + span_id_gen: Arc::new(SnowflakeIdGenerator::with_node_id(bp.node_id())), ..Default::default() }; // Parse servers - for id in config.sub_keys("server.listener", ".protocol") { - servers.parse_server(config, id); + let node_id = bp.node_id(); + for listener in bp.list_infallible::().await { + if bp.validate(listener.id, &listener.object) + && (listener.object.enable_for_nodes.is_empty() + || listener + .object + .enable_for_nodes + .iter() + .any(|n| n.id() == node_id)) + && !listener + .object + .disable_for_nodes + .iter() + .any(|n| n.id() == node_id) + { + servers.parse_server(bp, listener); + } } servers } - fn parse_server(&mut self, bp: &mut Bootstrap, id_: String) { + fn parse_server(&mut self, bp: &mut Bootstrap, listener: RegistryObject) { + let id = listener.id; + let listener = listener.object; + // Parse protocol - let id = id_.as_str(); - let protocol = - if let Some(protocol) = config.property_require(("server.listener", id, "protocol")) { - protocol - } else { - return; - }; + let protocol = match listener.protocol { + NetworkListenerProtocol::Smtp => ServerProtocol::Smtp, + NetworkListenerProtocol::Lmtp => ServerProtocol::Lmtp, + NetworkListenerProtocol::Http => ServerProtocol::Http, + NetworkListenerProtocol::Imap => ServerProtocol::Imap, + NetworkListenerProtocol::Pop3 => ServerProtocol::Pop3, + NetworkListenerProtocol::ManageSieve => ServerProtocol::ManageSieve, + }; // Build listeners let mut listeners = Vec::new(); - for (_, addr) in config.properties::(("server.listener", id, "bind")) { + for addr in &listener.bind { // Parse bind address and build socket + let addr = addr.0; let socket = match if addr.is_ipv4() { TcpSocket::new_v4() } else { @@ -72,187 +83,128 @@ impl Listeners { } { Ok(socket) => socket, Err(err) => { - config.new_build_error( - ("server.listener", id, "bind"), - format!("Failed to create socket: {err}"), - ); + bp.build_error(id, format!("Failed to create socket: {err}")); return; } }; - // Set socket options - for option in [ - "reuse-addr", - "reuse-port", - "send-buffer-size", - "recv-buffer-size", - "tos", - ] { - if let Some(value) = config.value_or_else( - ("server.listener", id, "socket", option), - ("server.socket", option), - ) { - let value = value.to_string(); - let key = ("server.listener", id, "socket", option); - let result = match option { - "reuse-addr" => socket - .set_reuseaddr(config.try_parse_value(key, &value).unwrap_or(true)), - #[cfg(not(target_env = "msvc"))] - "reuse-port" => socket - .set_reuseport(config.try_parse_value(key, &value).unwrap_or(false)), - "send-buffer-size" => { - if let Some(value) = config.try_parse_value(key, &value) { - socket.set_send_buffer_size(value) - } else { - continue; - } - } - "recv-buffer-size" => { - if let Some(value) = config.try_parse_value(key, &value) { - socket.set_recv_buffer_size(value) - } else { - continue; - } - } - "tos" => { - if let Some(value) = config.try_parse_value(key, &value) { - socket.set_tos(value) - } else { - continue; - } - } - _ => continue, - }; + if let Err(err) = socket.set_reuseaddr(listener.socket_reuse_address) { + bp.build_error(id, format!("Failed to set SO_REUSEADDR: {err}")); + return; + } - if let Err(err) = result { - config.new_build_error(key, format!("Failed to set socket option: {err}")); - } + #[cfg(not(target_env = "msvc"))] + if let Err(err) = socket.set_reuseport(listener.socket_reuse_port) { + bp.build_error(id, format!("Failed to set SO_REUSEPORT: {err}")); + return; + } + + if let Some(send_size) = listener.socket_send_buffer_size { + if let Err(err) = socket.set_send_buffer_size(send_size as u32) { + bp.build_error(id, format!("Failed to set SO_SNDBUF: {err}")); + return; } } - // Set default options - if !config.contains_key(("server.listener", id, "socket.reuse-addr")) { - let _ = socket.set_reuseaddr(true); + if let Some(recv_size) = listener.socket_receive_buffer_size { + if let Err(err) = socket.set_recv_buffer_size(recv_size as u32) { + bp.build_error(id, format!("Failed to set SO_RCVBUF: {err}")); + return; + } + } + + if let Some(tos) = listener.socket_tos { + if let Err(err) = socket.set_tos(tos as u32) { + bp.build_error(id, format!("Failed to set IP_TOS: {err}")); + return; + } } listeners.push(TcpListener { socket, addr, - ttl: config - .property_or_else::>( - ("server.listener", id, "socket.ttl"), - "server.socket.ttl", - "false", - ) - .unwrap_or_default(), - backlog: config - .property_or_else::>( - ("server.listener", id, "socket.backlog"), - "server.socket.backlog", - "1024", - ) - .unwrap_or_default(), - linger: config - .property_or_else::>( - ("server.listener", id, "socket.linger"), - "server.socket.linger", - "false", - ) - .unwrap_or_default(), - nodelay: config - .property_or_else( - ("server.listener", id, "socket.nodelay"), - "server.socket.nodelay", - "true", - ) - .unwrap_or(true), + ttl: listener.socket_ttl.map(|v| v as u32), + backlog: listener.socket_backlog.map(|v| v as u32), + linger: listener.socket_linger.map(|d| d.into_inner()), + nodelay: listener.socket_no_delay, }); } - if listeners.is_empty() { - config.new_build_error( - ("server.listener", id), - "No 'bind' directive found for listener", - ); - return; - } - - // Parse proxy networks - let mut proxy_networks = Vec::new(); - let proxy_keys = if config - .value(("server.listener", id, "proxy.trusted-networks")) - .is_some() - || config.has_prefix(("server.listener", id, "proxy.trusted-networks")) - { - ("server.listener", id, "proxy.trusted-networks").as_key() - } else { - "server.proxy.trusted-networks".as_key() - }; - for (_, network) in config.properties(proxy_keys) { - proxy_networks.push(network); - } - let span_id_gen = self.span_id_gen.clone(); self.servers.push(Listener { - max_connections: config - .property_or_else( - ("server.listener", id, "max-connections"), - "server.max-connections", - "8192", - ) - .unwrap_or(8192), - id: id_, + max_connections: listener.max_connections.unwrap_or(bp.node.max_connections), + id: listener.name.clone(), protocol, listeners, - proxy_networks, + proxy_networks: if !listener.override_proxy_trusted_networks.is_empty() { + listener.override_proxy_trusted_networks.clone() + } else { + bp.node.proxy_trusted_networks.clone() + }, span_id_gen, }); + self.parsed_listeners.push(RegistryObject { + id, + object: listener, + }); } - pub fn parse_tcp_acceptors(&mut self, bp: &mut Bootstrap, inner: Arc) { + pub async fn parse_tcp_acceptors(&mut self, bp: &mut Bootstrap, inner: Arc) { let resolver = Arc::new(CertificateResolver::new(inner.clone())); - for id_ in config.sub_keys("server.listener", ".protocol") { - let id = id_.as_str(); + for listener in std::mem::take(&mut self.parsed_listeners) { + let id = listener.id; + let listener = listener.object; + // Build TLS config - let acceptor = if config - .property_or_default(("server.listener", id, "tls.enable"), "true") - .unwrap_or(true) - { + let acceptor = if listener.tls_enable { // Parse protocol versions let mut tls_v2 = true; let mut tls_v3 = true; - let mut proto_err = None; - for (_, protocol) in config.values_or_else( - ("server.listener", id, "tls.disable-protocols"), - "server.tls.disable-protocols", - ) { - match protocol { - "TLSv1.2" | "0x0303" => tls_v2 = false, - "TLSv1.3" | "0x0304" => tls_v3 = false, - protocol => { - proto_err = format!("Unsupported TLS protocol {protocol:?}").into(); + + for disabled in listener.tls_disable_protocols { + match disabled { + TlsVersion::Tls12 => { + tls_v2 = false; + } + TlsVersion::Tls13 => { + tls_v3 = false; } } } - if let Some(proto_err) = proto_err { - config.new_parse_error( - ("server.listener", id, "tls.disable-protocols"), - proto_err, - ); - } - // Parse cipher suites let mut disabled_ciphers: Vec = Vec::new(); - let cipher_keys = - if config.has_prefix(("server.listener", id, "tls.disable-ciphers")) { - ("server.listener", id, "tls.disable-ciphers").as_key() - } else { - "server.tls.disable-ciphers".as_key() - }; - for (_, protocol) in config.properties::(cipher_keys) { - disabled_ciphers.push(protocol); + for disabled in listener.tls_disable_cipher_suites { + disabled_ciphers.push(match disabled { + TlsCipherSuite::Tls13Aes256GcmSha384 => { + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + } + TlsCipherSuite::Tls13Aes128GcmSha256 => { + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + } + TlsCipherSuite::Tls13Chacha20Poly1305Sha256 => { + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + } + TlsCipherSuite::TlsEcdheEcdsaWithAes256GcmSha384 => { + TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 + } + TlsCipherSuite::TlsEcdheEcdsaWithAes128GcmSha256 => { + TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 + } + TlsCipherSuite::TlsEcdheEcdsaWithChacha20Poly1305Sha256 => { + TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256 + } + TlsCipherSuite::TlsEcdheRsaWithAes256GcmSha384 => { + TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 + } + TlsCipherSuite::TlsEcdheRsaWithAes128GcmSha256 => { + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + } + TlsCipherSuite::TlsEcdheRsaWithChacha20Poly1305Sha256 => { + TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256 + } + }); } // Build cert provider @@ -278,56 +230,25 @@ impl Listeners { .with_no_client_auth() .with_cert_resolver(resolver.clone()), Err(err) => { - config.new_build_error( - ("server.listener", id, "tls"), - format!("Failed to build TLS server config: {err}"), - ); + bp.build_error(id, format!("Failed to build TLS server config: {err}")); return; } }; - server_config.ignore_client_order = config - .property_or_else( - ("server.listener", id, "tls.ignore-client-order"), - "server.tls.ignore-client-order", - "true", - ) - .unwrap_or(true); + server_config.ignore_client_order = listener.tls_ignore_client_order; // Build acceptor let default_config = Arc::new(server_config); TcpAcceptor::Tls { acceptor: TlsAcceptor::from(default_config.clone()), config: default_config, - implicit: config - .property_or_default(("server.listener", id, "tls.implicit"), "false") - .unwrap_or(false), + implicit: listener.tls_implicit, } } else { TcpAcceptor::Plain }; - self.tcp_acceptors.insert(id_, acceptor); - } - } -} - -impl ParseValue for ServerProtocol { - fn parse_value(value: &str) -> Result { - if value.eq_ignore_ascii_case("smtp") { - Ok(Self::Smtp) - } else if value.eq_ignore_ascii_case("lmtp") { - Ok(Self::Lmtp) - } else if value.eq_ignore_ascii_case("imap") { - Ok(Self::Imap) - } else if value.eq_ignore_ascii_case("http") | value.eq_ignore_ascii_case("https") { - Ok(Self::Http) - } else if value.eq_ignore_ascii_case("managesieve") { - Ok(Self::ManageSieve) - } else if value.eq_ignore_ascii_case("pop3") { - Ok(Self::Pop3) - } else { - Err(format!("Invalid server protocol type {:?}.", value,)) + self.tcp_acceptors.insert(listener.name, acceptor); } } } diff --git a/crates/common/src/config/server/mod.rs b/crates/common/src/config/server/mod.rs index 7dc94816..7a53098b 100644 --- a/crates/common/src/config/server/mod.rs +++ b/crates/common/src/config/server/mod.rs @@ -4,14 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{fmt::Display, net::SocketAddr, sync::Arc, time::Duration}; - -use ahash::AHashMap; -use serde::{Deserialize, Serialize}; -use tokio::net::TcpSocket; -use utils::{config::ipmask::IpAddrMask, snowflake::SnowflakeIdGenerator}; - use crate::listener::TcpAcceptor; +use ahash::AHashMap; +use registry::{schema::structs::NetworkListener, types::ipmask::IpAddrOrMask}; +use serde::{Deserialize, Serialize}; +use std::{fmt::Display, net::SocketAddr, sync::Arc, time::Duration}; +use store::registry::RegistryObject; +use tokio::net::TcpSocket; +use utils::snowflake::SnowflakeIdGenerator; pub mod listener; pub mod tls; @@ -21,6 +21,7 @@ pub struct Listeners { pub servers: Vec, pub tcp_acceptors: AHashMap, pub span_id_gen: Arc, + parsed_listeners: Vec>, } #[derive(Debug, Default)] @@ -28,7 +29,7 @@ pub struct Listener { pub id: String, pub protocol: ServerProtocol, pub listeners: Vec, - pub proxy_networks: Vec, + pub proxy_networks: Vec, pub max_connections: u64, pub span_id_gen: Arc, } diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index 820602c0..0876ec99 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -4,13 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{ - io::Cursor, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, - sync::Arc, - time::Duration, +use crate::{ + listener::{ + acme::{ + AcmeProvider, ChallengeSettings, EabSettings, + directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, + }, + tls::AcmeProviders, + }, + manager::bootstrap::Bootstrap, }; - use ahash::{AHashMap, AHashSet}; use base64::{ Engine, @@ -26,20 +29,18 @@ use rustls::{ }; use rustls_pemfile::{Item, certs, read_one}; use rustls_pki_types::PrivateKeyDer; -use utils::config::Config; +use std::{ + io::Cursor, + net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + sync::Arc, + time::Duration, +}; use x509_parser::{ certificate::X509Certificate, der_parser::asn1_rs::FromDer, extensions::{GeneralName, ParsedExtension}, }; -use crate::listener::{ - acme::{ - AcmeProvider, ChallengeSettings, EabSettings, directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, - }, - tls::AcmeProviders, -}; - pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13]; pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12]; diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index e881e263..e00f2200 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -4,26 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{sync::Arc, time::Duration}; - +use super::*; +use crate::expr::{self, Constant, if_block::IfBlock, tokenizer::TokenMap}; use ahash::AHashMap; use mail_auth::{ common::crypto::{Algorithm, Ed25519Key, HashAlgorithm, RsaKey, Sha256, SigningKey}, dkim::{Canonicalization, Done}, }; use mail_parser::decoders::base64::base64_decode; +use registry::schema::enums::ExpressionConstant; +use std::{sync::Arc, time::Duration}; use utils::config::{ Config, utils::{AsKey, ParseValue}, }; -use crate::{ - config::CONNECTION_VARS, - expr::{self, Constant, ConstantValue, if_block::IfBlock, tokenizer::TokenMap}, -}; - -use super::*; - #[derive(Clone)] pub struct MailAuthConfig { pub dkim: DkimAuthConfig, @@ -105,7 +100,7 @@ impl Default for MailAuthConfig { Self { dkim: DkimAuthConfig { verify: IfBlock::new_default::("auth.dkim.verify", [], "relaxed"), - sign: IfBlock::new_default::<()>( + sign: IfBlock::new_default( "auth.dkim.sign", [( "is_local_domain('*', sender_domain)", @@ -117,7 +112,7 @@ impl Default for MailAuthConfig { }, arc: ArcAuthConfig { verify: IfBlock::new_default::("auth.arc.verify", [], "relaxed"), - seal: IfBlock::new_default::<()>( + seal: IfBlock::new_default( "auth.arc.seal", [], "'rsa-' + config_get('report.domain')", @@ -427,10 +422,10 @@ impl<'x> TryFrom> for VerifyStrategy { fn try_from(value: expr::Variable<'x>) -> Result { match value { - expr::Variable::Integer(c) => match c { - 2 => Ok(VerifyStrategy::Relaxed), - 3 => Ok(VerifyStrategy::Strict), - 4 => Ok(VerifyStrategy::Disable), + expr::Variable::Constant(c) => match c { + ExpressionConstant::Relaxed => Ok(VerifyStrategy::Relaxed), + ExpressionConstant::Strict => Ok(VerifyStrategy::Strict), + ExpressionConstant::Disable => Ok(VerifyStrategy::Disable), _ => Err(()), }, _ => Err(()), @@ -438,16 +433,6 @@ impl<'x> TryFrom> for VerifyStrategy { } } -impl From for Constant { - fn from(value: VerifyStrategy) -> Self { - Constant::Integer(match value { - VerifyStrategy::Relaxed => 2, - VerifyStrategy::Strict => 3, - VerifyStrategy::Disable => 4, - }) - } -} - impl VerifyStrategy { #[inline(always)] pub fn verify(&self) -> bool { @@ -471,18 +456,6 @@ impl ParseValue for VerifyStrategy { } } -impl ConstantValue for VerifyStrategy { - fn add_constants(token_map: &mut TokenMap) { - token_map - .add_constant("relaxed", VerifyStrategy::Relaxed) - .add_constant("strict", VerifyStrategy::Strict) - .add_constant("disable", VerifyStrategy::Disable) - .add_constant("disabled", VerifyStrategy::Disable) - .add_constant("never", VerifyStrategy::Disable) - .add_constant("none", VerifyStrategy::Disable); - } -} - impl ParseValue for DkimCanonicalization { fn parse_value(value: &str) -> Result { if let Some((headers, body)) = value.split_once('/') { diff --git a/crates/common/src/config/smtp/mod.rs b/crates/common/src/config/smtp/mod.rs index 518395ea..586f404a 100644 --- a/crates/common/src/config/smtp/mod.rs +++ b/crates/common/src/config/smtp/mod.rs @@ -51,105 +51,6 @@ pub const THROTTLE_REMOTE_IP: u16 = 1 << 7; pub const THROTTLE_LOCAL_IP: u16 = 1 << 8; pub const THROTTLE_HELO_DOMAIN: u16 = 1 << 9; -pub(crate) const RCPT_DOMAIN_VARS: &[u32; 1] = &[V_RECIPIENT_DOMAIN]; - -pub(crate) const SMTP_EHLO_VARS: &[u32; 10] = &[ - V_LISTENER, - V_REMOTE_IP, - V_REMOTE_PORT, - V_LOCAL_IP, - V_LOCAL_PORT, - V_PROTOCOL, - V_TLS, - V_HELO_DOMAIN, - V_ASN, - V_COUNTRY, -]; -pub(crate) const SMTP_MAIL_FROM_VARS: &[u32; 12] = &[ - V_LISTENER, - V_REMOTE_IP, - V_REMOTE_PORT, - V_LOCAL_IP, - V_LOCAL_PORT, - V_PROTOCOL, - V_TLS, - V_SENDER, - V_SENDER_DOMAIN, - V_AUTHENTICATED_AS, - V_ASN, - V_COUNTRY, -]; -pub(crate) const SMTP_RCPT_TO_VARS: &[u32; 17] = &[ - V_SENDER, - V_SENDER_DOMAIN, - V_RECIPIENTS, - V_RECIPIENT, - V_RECIPIENT_DOMAIN, - V_AUTHENTICATED_AS, - V_LISTENER, - V_REMOTE_IP, - V_REMOTE_PORT, - V_LOCAL_IP, - V_LOCAL_PORT, - V_PROTOCOL, - V_TLS, - V_PRIORITY, - V_HELO_DOMAIN, - V_ASN, - V_COUNTRY, -]; -pub(crate) const SMTP_QUEUE_HOST_VARS: &[u32; 20] = &[ - V_SENDER, - V_SENDER_DOMAIN, - V_RECIPIENT_DOMAIN, - V_RECIPIENT, - V_RECIPIENTS, - V_MX, - V_PRIORITY, - V_REMOTE_IP, - V_LOCAL_IP, - V_QUEUE_RETRY_NUM, - V_QUEUE_NOTIFY_NUM, - V_QUEUE_EXPIRES_IN, - V_QUEUE_LAST_STATUS, - V_QUEUE_LAST_ERROR, - V_QUEUE_NAME, - V_QUEUE_AGE, - V_RECEIVED_FROM_IP, - V_RECEIVED_VIA_PORT, - V_SOURCE, - V_SIZE, -]; -pub(crate) const SMTP_QUEUE_RCPT_VARS: &[u32; 17] = &[ - V_RECIPIENT, - V_RECIPIENT_DOMAIN, - V_RECIPIENTS, - V_SENDER, - V_SENDER_DOMAIN, - V_PRIORITY, - V_QUEUE_RETRY_NUM, - V_QUEUE_NOTIFY_NUM, - V_QUEUE_EXPIRES_IN, - V_QUEUE_LAST_STATUS, - V_QUEUE_LAST_ERROR, - V_QUEUE_NAME, - V_QUEUE_AGE, - V_RECEIVED_FROM_IP, - V_RECEIVED_VIA_PORT, - V_SOURCE, - V_SIZE, -]; -pub(crate) const SMTP_QUEUE_SENDER_VARS: &[u32; 8] = &[ - V_SENDER, - V_SENDER_DOMAIN, - V_PRIORITY, - V_QUEUE_RETRY_NUM, - V_QUEUE_NOTIFY_NUM, - V_QUEUE_EXPIRES_IN, - V_QUEUE_LAST_STATUS, - V_QUEUE_LAST_ERROR, -]; - impl SmtpConfig { pub async fn parse(bp: &mut Bootstrap) -> Self { Self { diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index aba31629..3b5c61e5 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -13,6 +13,7 @@ use crate::{ use ahash::AHashMap; use mail_auth::IpLookupStrategy; use mail_send::Credentials; +use registry::schema::enums::ExpressionConstant; use std::{ fmt::Display, hash::{Hash, Hasher}, @@ -189,7 +190,7 @@ pub enum RequireOptional { impl Default for QueueConfig { fn default() -> Self { Self { - route: IfBlock::new_default::<()>( + route: IfBlock::new_default( "queue.strategy.route", #[cfg(not(feature = "test_mode"))] [("is_local_domain('*', rcpt_domain)", "'local'")], @@ -197,7 +198,7 @@ impl Default for QueueConfig { [], "'mx'", ), - queue: IfBlock::new_default::<()>( + queue: IfBlock::new_default( "queue.strategy.schedule", #[cfg(not(feature = "test_mode"))] [ @@ -212,8 +213,8 @@ impl Default for QueueConfig { #[cfg(feature = "test_mode")] "'default'", ), - connection: IfBlock::new_default::<()>("queue.strategy.connection", [], "'default'"), - tls: IfBlock::new_default::<()>( + connection: IfBlock::new_default("queue.strategy.connection", [], "'default'"), + tls: IfBlock::new_default( "queue.strategy.tls", #[cfg(not(feature = "test_mode"))] [("retry_num > 0 && last_error == 'tls'", "'invalid-tls'")], @@ -222,13 +223,13 @@ impl Default for QueueConfig { "'default'", ), dsn: Dsn { - name: IfBlock::new_default::<()>("report.dsn.from-name", [], "'Mail Delivery Subsystem'"), - address: IfBlock::new_default::<()>( + name: IfBlock::new_default("report.dsn.from-name", [], "'Mail Delivery Subsystem'"), + address: IfBlock::new_default( "report.dsn.from-address", [], "'MAILER-DAEMON@' + config_get('report.domain')", ), - sign: IfBlock::new_default::<()>( + sign: IfBlock::new_default( "report.dsn.sign", [], "['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]", @@ -579,7 +580,9 @@ fn parse_inbound_rate_limiters(bp: &mut Bootstrap) -> QueueRateLimiters { || t.expr.items().iter().any(|c| { matches!( c, - ExpressionItem::Variable(V_RECIPIENT | V_RECIPIENT_DOMAIN) + ExpressionItem::Variable( + ExpressionVariable::Rcpt | ExpressionVariable::RcptDomain + ) ) }) { @@ -591,7 +594,10 @@ fn parse_inbound_rate_limiters(bp: &mut Bootstrap) -> QueueRateLimiters { matches!( c, ExpressionItem::Variable( - V_SENDER | V_SENDER_DOMAIN | V_HELO_DOMAIN | V_AUTHENTICATED_AS + ExpressionVariable::Sender + | ExpressionVariable::SenderDomain + | ExpressionVariable::HeloDomain + | ExpressionVariable::AuthenticatedAs ) ) }) @@ -622,17 +628,23 @@ fn parse_outbound_rate_limiters(bp: &mut Bootstrap) -> QueueRateLimiters { ); for t in all_throttles { if (t.keys & (THROTTLE_MX | THROTTLE_REMOTE_IP | THROTTLE_LOCAL_IP)) != 0 - || t.expr - .items() - .iter() - .any(|c| matches!(c, ExpressionItem::Variable(V_MX | V_REMOTE_IP | V_LOCAL_IP))) + || t.expr.items().iter().any(|c| { + matches!( + c, + ExpressionItem::Variable( + ExpressionVariable::Mx + | ExpressionVariable::RemoteIp + | ExpressionVariable::LocalIp + ) + ) + }) { throttle.remote.push(t); } else if (t.keys & (THROTTLE_RCPT_DOMAIN)) != 0 || t.expr .items() .iter() - .any(|c| matches!(c, ExpressionItem::Variable(V_RECIPIENT_DOMAIN))) + .any(|c| matches!(c, ExpressionItem::Variable(ExpressionVariable::RcptDomain))) { throttle.rcpt.push(t); } else { @@ -657,7 +669,7 @@ fn parse_queue_quota(bp: &mut Bootstrap) -> QueueQuotas { .expr .items() .iter() - .any(|c| matches!(c, ExpressionItem::Variable(V_RECIPIENT))) + .any(|c| matches!(c, ExpressionItem::Variable(ExpressionVariable::Rcpt))) { capacities.rcpt.push(quota); } else if (quota.keys & THROTTLE_RCPT_DOMAIN) != 0 @@ -665,7 +677,7 @@ fn parse_queue_quota(bp: &mut Bootstrap) -> QueueQuotas { .expr .items() .iter() - .any(|c| matches!(c, ExpressionItem::Variable(V_RECIPIENT_DOMAIN))) + .any(|c| matches!(c, ExpressionItem::Variable(ExpressionVariable::RcptDomain))) { capacities.rcpt_domain.push(quota); } else { @@ -766,47 +778,24 @@ impl<'x> TryFrom> for RequireOptional { fn try_from(value: Variable<'x>) -> Result { match value { - Variable::Integer(2) => Ok(RequireOptional::Optional), - Variable::Integer(1) => Ok(RequireOptional::Require), - Variable::Integer(0) => Ok(RequireOptional::Disable), + Variable::Constant(ExpressionConstant::Optional) => Ok(RequireOptional::Optional), + Variable::Constant(ExpressionConstant::Require) => Ok(RequireOptional::Require), + Variable::Constant(ExpressionConstant::Disable) => Ok(RequireOptional::Disable), _ => Err(()), } } } -impl From for Constant { - fn from(value: RequireOptional) -> Self { - Constant::Integer(match value { - RequireOptional::Optional => 2, - RequireOptional::Require => 1, - RequireOptional::Disable => 0, - }) - } -} - -impl ConstantValue for RequireOptional { - fn add_constants(token_map: &mut crate::expr::tokenizer::TokenMap) { - token_map - .add_constant("optional", RequireOptional::Optional) - .add_constant("require", RequireOptional::Require) - .add_constant("required", RequireOptional::Require) - .add_constant("disable", RequireOptional::Disable) - .add_constant("disabled", RequireOptional::Disable) - .add_constant("none", RequireOptional::Disable) - .add_constant("false", RequireOptional::Disable); - } -} - impl<'x> TryFrom> for IpLookupStrategy { type Error = (); fn try_from(value: Variable<'x>) -> Result { match value { - Variable::Integer(value) => match value { - 2 => Ok(IpLookupStrategy::Ipv4Only), - 3 => Ok(IpLookupStrategy::Ipv6Only), - 4 => Ok(IpLookupStrategy::Ipv6thenIpv4), - 5 => Ok(IpLookupStrategy::Ipv4thenIpv6), + Variable::Constant(value) => match value { + ExpressionConstant::Ipv4Only => Ok(IpLookupStrategy::Ipv4Only), + ExpressionConstant::Ipv6Only => Ok(IpLookupStrategy::Ipv6Only), + ExpressionConstant::Ipv6ThenIpv4 => Ok(IpLookupStrategy::Ipv6thenIpv4), + ExpressionConstant::Ipv4ThenIpv6 => Ok(IpLookupStrategy::Ipv4thenIpv6), _ => Err(()), }, Variable::String(value) => { @@ -817,27 +806,6 @@ impl<'x> TryFrom> for IpLookupStrategy { } } -impl From for Constant { - fn from(value: IpLookupStrategy) -> Self { - Constant::Integer(match value { - IpLookupStrategy::Ipv4Only => 2, - IpLookupStrategy::Ipv6Only => 3, - IpLookupStrategy::Ipv6thenIpv4 => 4, - IpLookupStrategy::Ipv4thenIpv6 => 5, - }) - } -} - -impl ConstantValue for IpLookupStrategy { - fn add_constants(token_map: &mut crate::expr::tokenizer::TokenMap) { - token_map - .add_constant("ipv4_only", IpLookupStrategy::Ipv4Only) - .add_constant("ipv6_only", IpLookupStrategy::Ipv6Only) - .add_constant("ipv6_then_ipv4", IpLookupStrategy::Ipv6thenIpv4) - .add_constant("ipv4_then_ipv6", IpLookupStrategy::Ipv4thenIpv6); - } -} - impl std::fmt::Debug for RelayConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("RelayConfig") diff --git a/crates/common/src/config/smtp/report.rs b/crates/common/src/config/smtp/report.rs index c2006aa9..739f08a5 100644 --- a/crates/common/src/config/smtp/report.rs +++ b/crates/common/src/config/smtp/report.rs @@ -4,13 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Duration; - -use utils::config::{Config, utils::ParseValue}; - -use crate::expr::{Constant, ConstantValue, Variable, if_block::IfBlock, tokenizer::TokenMap}; - use super::*; +use crate::expr::{Constant, Variable, if_block::IfBlock, tokenizer::TokenMap}; +use registry::schema::enums::ExpressionConstant; +use std::time::Duration; +use utils::config::{Config, utils::ParseValue}; #[derive(Clone)] pub struct ReportConfig { @@ -79,7 +77,7 @@ impl ReportConfig { &TokenMap::default().with_variables(RCPT_DOMAIN_VARS), ) .unwrap_or_else(|| { - IfBlock::new_default::<()>("report.submitter", [], "config_get('server.hostname')") + IfBlock::new_default("report.submitter", [], "config_get('server.hostname')") }), analysis: ReportAnalysis { addresses: config @@ -114,17 +112,13 @@ impl ReportConfig { impl Report { pub fn parse(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Self { let mut report = Self { - name: IfBlock::new_default::<()>( - format!("report.{id}.from-name"), - [], - "'Report Subsystem'", - ), - address: IfBlock::new_default::<()>( + name: IfBlock::new_default(format!("report.{id}.from-name"), [], "'Report Subsystem'"), + address: IfBlock::new_default( format!("report.{id}.from-address"), [], format!("'noreply-{id}@' + config_get('report.domain')"), ), - subject: IfBlock::new_default::<()>( + subject: IfBlock::new_default( format!("report.{id}.subject"), [], format!( @@ -132,12 +126,12 @@ impl Report { id.to_ascii_uppercase() ), ), - sign: IfBlock::new_default::<()>( + sign: IfBlock::new_default( format!("report.{id}.sign"), [], "['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]", ), - send: IfBlock::new_default::<()>(format!("report.{id}.send"), [], "[1, 1d]"), + send: IfBlock::new_default(format!("report.{id}.send"), [], "[1, 1d]"), }; for (value, key) in [ (&mut report.name, "from-name"), @@ -160,17 +154,17 @@ impl AggregateReport { let rcpt_vars = TokenMap::default().with_variables(RCPT_DOMAIN_VARS); let mut report = Self { - name: IfBlock::new_default::<()>( + name: IfBlock::new_default( format!("report.{id}.aggregate.from-name"), [], format!("'{} Aggregate Report'", id.to_ascii_uppercase()), ), - address: IfBlock::new_default::<()>( + address: IfBlock::new_default( format!("report.{id}.aggregate.from-address"), [], format!("'noreply-{id}@' + config_get('report.domain')"), ), - org_name: IfBlock::new_default::<()>( + org_name: IfBlock::new_default( format!("report.{id}.aggregate.org-name"), [], "config_get('report.domain')", @@ -181,12 +175,12 @@ impl AggregateReport { [], "daily", ), - sign: IfBlock::new_default::<()>( + sign: IfBlock::new_default( format!("report.{id}.aggregate.sign"), [], "['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]", ), - max_size: IfBlock::new_default::<()>( + max_size: IfBlock::new_default( format!("report.{id}.aggregate.max-size"), [], "26214400", @@ -233,47 +227,20 @@ impl ParseValue for AggregateFrequency { } } -impl From for Constant { - fn from(value: AggregateFrequency) -> Self { - match value { - AggregateFrequency::Never => 0.into(), - AggregateFrequency::Hourly => 2.into(), - AggregateFrequency::Daily => 3.into(), - AggregateFrequency::Weekly => 4.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(2) => Ok(AggregateFrequency::Hourly), - Variable::Integer(3) => Ok(AggregateFrequency::Daily), - Variable::Integer(4) => Ok(AggregateFrequency::Weekly), + Variable::Constant(ExpressionConstant::Disable) => Ok(AggregateFrequency::Never), + Variable::Constant(ExpressionConstant::Hourly) => Ok(AggregateFrequency::Hourly), + Variable::Constant(ExpressionConstant::Daily) => Ok(AggregateFrequency::Daily), + Variable::Constant(ExpressionConstant::Weekly) => Ok(AggregateFrequency::Weekly), _ => Err(()), } } } -impl ConstantValue for AggregateFrequency { - fn add_constants(token_map: &mut crate::expr::tokenizer::TokenMap) { - token_map - .add_constant("never", AggregateFrequency::Never) - .add_constant("hourly", AggregateFrequency::Hourly) - .add_constant("hour", AggregateFrequency::Hourly) - .add_constant("daily", AggregateFrequency::Daily) - .add_constant("day", AggregateFrequency::Daily) - .add_constant("weekly", AggregateFrequency::Weekly) - .add_constant("week", AggregateFrequency::Weekly) - .add_constant("never", AggregateFrequency::Never) - .add_constant("disable", AggregateFrequency::Never) - .add_constant("false", AggregateFrequency::Never); - } -} - impl ParseValue for AddressMatch { fn parse_value(value: &str) -> Result { if let Some(value) = value.strip_prefix('*').map(|v| v.trim()) { diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index 6ec0edf4..2cdd24a6 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -17,13 +17,11 @@ use hyper::{ HeaderMap, header::{AUTHORIZATION, CONTENT_TYPE, HeaderName, HeaderValue}, }; +use registry::schema::enums::ExpressionConstant; use smtp_proto::*; use utils::config::{Config, utils::ParseValue}; -use crate::{ - config::CONNECTION_VARS, - expr::{if_block::IfBlock, tokenizer::TokenMap, *}, -}; +use crate::expr::{if_block::IfBlock, tokenizer::TokenMap, *}; use self::resolver::Policy; @@ -456,7 +454,7 @@ fn parse_milter(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Option(format!("session.milter.{id}.enable"), [], "false") + IfBlock::new_default(format!("session.milter.{id}.enable"), [], "false") }), id: Arc::new(id.into()), addrs: format!("{}:{}", hostname, port) @@ -563,7 +561,7 @@ fn parse_hooks(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Option(format!("session.hook.{id}.enable"), [], "false") + IfBlock::new_default(format!("session.hook.{id}.enable"), [], "false") }), id: id.to_string(), url: config @@ -626,17 +624,17 @@ fn parse_stages(bp: &mut Bootstrap, prefix: &str, id: &str) -> AHashSet { impl Default for SessionConfig { fn default() -> Self { Self { - timeout: IfBlock::new_default::<()>("session.timeout", [], "5m"), - duration: IfBlock::new_default::<()>("session.duration", [], "10m"), - transfer_limit: IfBlock::new_default::<()>("session.transfer-limit", [], "262144000"), + timeout: IfBlock::new_default("session.timeout", [], "5m"), + duration: IfBlock::new_default("session.duration", [], "10m"), + transfer_limit: IfBlock::new_default("session.transfer-limit", [], "262144000"), connect: Connect { - hostname: IfBlock::new_default::<()>( + hostname: IfBlock::new_default( "server.connect.hostname", [], "config_get('server.hostname')", ), script: IfBlock::empty("session.connect.script"), - greeting: IfBlock::new_default::<()>( + greeting: IfBlock::new_default( "session.connect.greeting", [], "config_get('server.hostname') + ' Stalwart ESMTP at your service'", @@ -644,15 +642,15 @@ impl Default for SessionConfig { }, ehlo: Ehlo { script: IfBlock::empty("session.ehlo.script"), - require: IfBlock::new_default::<()>("session.ehlo.require", [], "true"), - reject_non_fqdn: IfBlock::new_default::<()>( + require: IfBlock::new_default("session.ehlo.require", [], "true"), + reject_non_fqdn: IfBlock::new_default( "session.ehlo.reject-non-fqdn", [("local_port == 25", "true")], "false", ), }, auth: Auth { - directory: IfBlock::new_default::<()>( + directory: IfBlock::new_default( "session.auth.directory", #[cfg(feature = "test_mode")] [], @@ -671,7 +669,7 @@ impl Default for SessionConfig { ], "false", ), - require: IfBlock::new_default::<()>( + require: IfBlock::new_default( "session.auth.require", #[cfg(feature = "test_mode")] [], @@ -679,18 +677,18 @@ impl Default for SessionConfig { [("local_port != 25", "true")], "false", ), - must_match_sender: IfBlock::new_default::<()>( + must_match_sender: IfBlock::new_default( "session.auth.must-match-sender", [], "true", ), - errors_max: IfBlock::new_default::<()>("session.auth.errors.total", [], "3"), - errors_wait: IfBlock::new_default::<()>("session.auth.errors.wait", [], "5s"), + errors_max: IfBlock::new_default("session.auth.errors.total", [], "3"), + errors_wait: IfBlock::new_default("session.auth.errors.wait", [], "5s"), }, mail: Mail { script: IfBlock::empty("session.mail.script"), rewrite: IfBlock::empty("session.mail.rewrite"), - is_allowed: IfBlock::new_default::<()>( + is_allowed: IfBlock::new_default( "session.mail.is-allowed", [], "!is_empty(authenticated_as) || !key_exists('blocked-domains', sender_domain)", @@ -698,12 +696,12 @@ impl Default for SessionConfig { }, rcpt: Rcpt { script: IfBlock::empty("session.rcpt.script"), - relay: IfBlock::new_default::<()>( + relay: IfBlock::new_default( "session.rcpt.relay", [("!is_empty(authenticated_as)", "true")], "false", ), - directory: IfBlock::new_default::<()>( + directory: IfBlock::new_default( "session.rcpt.directory", [], #[cfg(feature = "test_mode")] @@ -712,9 +710,9 @@ impl Default for SessionConfig { "'*'", ), rewrite: IfBlock::empty("session.rcpt.rewrite"), - errors_max: IfBlock::new_default::<()>("session.rcpt.errors.total", [], "5"), - errors_wait: IfBlock::new_default::<()>("session.rcpt.errors.wait", [], "5s"), - max_recipients: IfBlock::new_default::<()>( + errors_max: IfBlock::new_default("session.rcpt.errors.total", [], "5"), + errors_wait: IfBlock::new_default("session.rcpt.errors.wait", [], "5s"), + max_recipients: IfBlock::new_default( "session.rcpt.max-recipients", [], "100", @@ -724,44 +722,44 @@ impl Default for SessionConfig { }, data: Data { script: IfBlock::empty("session.data.script"), - spam_filter: IfBlock::new_default::<()>("session.data.spam-filter", [], "true"), - max_messages: IfBlock::new_default::<()>("session.data.limits.messages", [], "10"), - max_message_size: IfBlock::new_default::<()>( + spam_filter: IfBlock::new_default("session.data.spam-filter", [], "true"), + max_messages: IfBlock::new_default("session.data.limits.messages", [], "10"), + max_message_size: IfBlock::new_default( "session.data.limits.size", [], "104857600", ), - max_received_headers: IfBlock::new_default::<()>( + max_received_headers: IfBlock::new_default( "session.data.limits.received-headers", [], "50", ), - add_received: IfBlock::new_default::<()>( + add_received: IfBlock::new_default( "session.data.add-headers.received", [("local_port == 25", "true")], "false", ), - add_received_spf: IfBlock::new_default::<()>( + add_received_spf: IfBlock::new_default( "session.data.add-headers.received-spf", [("local_port == 25", "true")], "false", ), - add_return_path: IfBlock::new_default::<()>( + add_return_path: IfBlock::new_default( "session.data.add-headers.return-path", [("local_port == 25", "true")], "false", ), - add_auth_results: IfBlock::new_default::<()>( + add_auth_results: IfBlock::new_default( "session.data.add-headers.auth-results", [("local_port == 25", "true")], "false", ), - add_message_id: IfBlock::new_default::<()>( + add_message_id: IfBlock::new_default( "session.data.add-headers.message-id", [("local_port == 25", "true")], "false", ), - add_date: IfBlock::new_default::<()>( + add_date: IfBlock::new_default( "session.data.add-headers.date", [("local_port == 25", "true")], "false", @@ -769,35 +767,35 @@ impl Default for SessionConfig { add_delivered_to: false, }, extensions: Extensions { - pipelining: IfBlock::new_default::<()>("session.extensions.pipelining", [], "true"), - chunking: IfBlock::new_default::<()>("session.extensions.chunking", [], "true"), - requiretls: IfBlock::new_default::<()>("session.extensions.requiretls", [], "true"), - dsn: IfBlock::new_default::<()>( + pipelining: IfBlock::new_default("session.extensions.pipelining", [], "true"), + chunking: IfBlock::new_default("session.extensions.chunking", [], "true"), + requiretls: IfBlock::new_default("session.extensions.requiretls", [], "true"), + dsn: IfBlock::new_default( "session.extensions.dsn", [("!is_empty(authenticated_as)", "true")], "false", ), - vrfy: IfBlock::new_default::<()>( + vrfy: IfBlock::new_default( "session.extensions.vrfy", [("!is_empty(authenticated_as)", "true")], "false", ), - expn: IfBlock::new_default::<()>( + expn: IfBlock::new_default( "session.extensions.expn", [("!is_empty(authenticated_as)", "true")], "false", ), - no_soliciting: IfBlock::new_default::<()>( + no_soliciting: IfBlock::new_default( "session.extensions.no-soliciting", [], "''", ), - future_release: IfBlock::new_default::<()>( + future_release: IfBlock::new_default( "session.extensions.future-release", [("!is_empty(authenticated_as)", "7d")], "false", ), - deliver_by: IfBlock::new_default::<()>( + deliver_by: IfBlock::new_default( "session.extensions.deliver-by", [("!is_empty(authenticated_as)", "15d")], "false", @@ -873,13 +871,13 @@ impl<'x> TryFrom> for Mechanism { fn try_from(value: Variable<'x>) -> Result { match value { - Variable::Integer(value) => Ok(Mechanism(value as u64)), + Variable::Constant(value) => Mechanism::try_from(value), Variable::Array(items) => { let mut mechanism = 0; for item in items { match item { - Variable::Integer(value) => mechanism |= value as u64, + Variable::Constant(value) => mechanism |= Mechanism::try_from(value)?.0, _ => return Err(()), } } @@ -891,19 +889,17 @@ impl<'x> TryFrom> for Mechanism { } } -impl From for Constant { - fn from(value: Mechanism) -> Self { - Constant::Integer(value.0 as i64) - } -} +impl TryFrom for Mechanism { + type Error = (); -impl ConstantValue for Mechanism { - fn add_constants(token_map: &mut crate::expr::tokenizer::TokenMap) { - token_map - .add_constant("login", Mechanism(AUTH_LOGIN)) - .add_constant("plain", Mechanism(AUTH_PLAIN)) - .add_constant("xoauth2", Mechanism(AUTH_XOAUTH2)) - .add_constant("oauthbearer", Mechanism(AUTH_OAUTHBEARER)); + fn try_from(value: ExpressionConstant) -> Result { + match value { + ExpressionConstant::Login => Ok(Mechanism(AUTH_LOGIN)), + ExpressionConstant::Plain => Ok(Mechanism(AUTH_PLAIN)), + ExpressionConstant::Xoauth2 => Ok(Mechanism(AUTH_XOAUTH2)), + ExpressionConstant::Oauthbearer => Ok(Mechanism(AUTH_OAUTHBEARER)), + _ => Err(()), + } } } @@ -924,10 +920,10 @@ impl<'x> TryFrom> for MtPriority { fn try_from(value: Variable<'x>) -> Result { match value { - Variable::Integer(value) => match value { - 2 => Ok(MtPriority::Mixer), - 3 => Ok(MtPriority::Stanag4406), - 4 => Ok(MtPriority::Nsep), + Variable::Constant(value) => match value { + ExpressionConstant::Mixer => Ok(MtPriority::Mixer), + ExpressionConstant::Stanag4406 => Ok(MtPriority::Stanag4406), + ExpressionConstant::Nsep => Ok(MtPriority::Nsep), _ => Err(()), }, Variable::String(value) => MtPriority::parse_value(value.as_str()).map_err(|_| ()), @@ -935,22 +931,3 @@ impl<'x> TryFrom> for MtPriority { } } } - -impl From for Constant { - fn from(value: MtPriority) -> Self { - Constant::Integer(match value { - MtPriority::Mixer => 2, - MtPriority::Stanag4406 => 3, - MtPriority::Nsep => 4, - }) - } -} - -impl ConstantValue for MtPriority { - fn add_constants(token_map: &mut TokenMap) { - token_map - .add_constant("mixer", MtPriority::Mixer) - .add_constant("stanag4406", MtPriority::Stanag4406) - .add_constant("nsep", MtPriority::Nsep); - } -} diff --git a/crates/common/src/enterprise/alerts.rs b/crates/common/src/enterprise/alerts.rs index 022f92ff..11f7ce86 100644 --- a/crates/common/src/enterprise/alerts.rs +++ b/crates/common/src/enterprise/alerts.rs @@ -15,6 +15,7 @@ use mail_builder::{ address::{Address, EmailAddress}, }, }; +use registry::schema::enums::ExpressionVariable; use trc::{Collector, MetricType, TOTAL_EVENT_COUNT, TelemetryEvent}; use super::{AlertContent, AlertContentToken, AlertMethod}; @@ -108,7 +109,7 @@ impl Server { } impl ResolveVariable for CollectorResolver { - fn resolve_variable(&self, variable: u32) -> Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> { if (variable as usize) < TOTAL_EVENT_COUNT { Variable::Integer(Collector::read_event_metric(variable as usize) as i64) } else if let Some(metric_type) = diff --git a/crates/common/src/expr/eval.rs b/crates/common/src/expr/eval.rs index 3ea55f71..23b8052e 100644 --- a/crates/common/src/expr/eval.rs +++ b/crates/common/src/expr/eval.rs @@ -5,7 +5,7 @@ */ use super::{ - BinaryOperator, Constant, Expression, ExpressionItem, Setting, StringCow, UnaryOperator, + BinaryOperator, Constant, Expression, ExpressionItem, StringCow, SystemVariable, UnaryOperator, Variable, functions::{FUNCTIONS, ResolveVariable}, if_block::IfBlock, @@ -13,8 +13,9 @@ use super::{ use crate::Server; use compact_str::{CompactString, ToCompactString, format_compact}; use hyper::StatusCode; +use registry::types::EnumType; use std::{cmp::Ordering, fmt::Display}; -use trc::EvalEvent; +use trc::{Collector, EvalEvent, MetricType, TOTAL_EVENT_COUNT}; impl Server { pub async fn eval_if<'x, R: TryFrom>, V: ResolveVariable>( @@ -27,7 +28,7 @@ impl Server { trc::event!( Eval(EvalEvent::Result), SpanId = session_id, - Id = if_block.key.clone(), + Id = if_block.property.as_str(), Result = "" ); @@ -48,7 +49,7 @@ impl Server { trc::event!( Eval(EvalEvent::Result), SpanId = session_id, - Id = if_block.key.clone(), + Id = if_block.property.as_str(), Result = format!("{result:?}"), ); @@ -58,7 +59,7 @@ impl Server { trc::event!( Eval(EvalEvent::Result), SpanId = session_id, - Id = if_block.key.clone(), + Id = if_block.property.as_str(), Result = "", ); @@ -70,7 +71,7 @@ impl Server { trc::event!( Eval(EvalEvent::Error), SpanId = session_id, - Id = if_block.key.clone(), + Id = if_block.property.as_str(), CausedBy = err, ); @@ -83,7 +84,7 @@ impl Server { &'x self, expr: &'x Expression, resolver: &'x V, - expr_id: &str, + expr_id: &'static str, session_id: u64, ) -> Option { if expr.is_empty() { @@ -104,7 +105,7 @@ impl Server { trc::event!( Eval(EvalEvent::Result), SpanId = session_id, - Id = expr_id.to_compact_string(), + Id = expr_id, Result = format!("{result:?}"), ); @@ -114,7 +115,7 @@ impl Server { trc::event!( Eval(EvalEvent::Error), SpanId = session_id, - Id = expr_id.to_compact_string(), + Id = expr_id, Details = "Failed to convert result", ); @@ -126,7 +127,7 @@ impl Server { trc::event!( Eval(EvalEvent::Error), SpanId = session_id, - Id = expr_id.to_compact_string(), + Id = expr_id, CausedBy = err, ); @@ -207,14 +208,25 @@ impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec match setting { - Setting::Hostname => { + ExpressionItem::System(setting) => match setting { + SystemVariable::Hostname => { stack.push(self.core.core.network.server_name.as_str().into()) } - Setting::Domain => { + SystemVariable::Domain => { stack.push(self.core.core.network.report_domain.as_str().into()) } - Setting::NodeId => stack.push(self.core.core.network.node_id.into()), + SystemVariable::NodeId => stack.push(self.core.core.network.node_id.into()), + SystemVariable::Metric(variable) => { + stack.push(if *variable < TOTAL_EVENT_COUNT { + Variable::Integer(Collector::read_event_metric(*variable) as i64) + } else if let Some(metric_type) = + MetricType::from_code(*variable as u64 - TOTAL_EVENT_COUNT as u64) + { + Variable::Float(Collector::read_metric(metric_type)) + } else { + Variable::Integer(0) + }); + } }, ExpressionItem::UnaryOperator(op) => { let value = stack.pop().unwrap_or_default(); @@ -347,6 +359,8 @@ impl<'x> Variable<'x> { a } } + (a, Variable::Constant(_)) => a, + (Variable::Constant(_), b) => b, } } @@ -471,6 +485,7 @@ impl<'x> Variable<'x> { Variable::String(s) => Variable::String(StringCow::Borrowed(s.as_str())), Variable::Integer(n) => Variable::Integer(*n), Variable::Float(n) => Variable::Float(*n), + Variable::Constant(c) => Variable::Constant(*c), Variable::Array(l) => Variable::Array(l.iter().map(|v| v.to_ref()).collect::>()), } } @@ -481,6 +496,7 @@ impl<'x> Variable<'x> { Variable::Integer(n) => *n != 0, Variable::String(s) => !s.is_empty(), Variable::Array(a) => !a.is_empty(), + Variable::Constant(_) => true, } } @@ -500,10 +516,12 @@ impl<'x> Variable<'x> { Variable::Integer(v) => result.push_str(&v.to_compact_string()), Variable::Float(v) => result.push_str(&v.to_compact_string()), Variable::Array(_) => {} + Variable::Constant(c) => result.push_str(c.as_str()), } } StringCow::Owned(result) } + Variable::Constant(c) => StringCow::Borrowed(c.as_str()), } } @@ -523,10 +541,12 @@ impl<'x> Variable<'x> { Variable::Integer(v) => result.push_str(&v.to_compact_string()), Variable::Float(v) => result.push_str(&v.to_compact_string()), Variable::Array(_) => {} + Variable::Constant(c) => result.push_str(c.as_str()), } } StringCow::Owned(result) } + Variable::Constant(c) => StringCow::Borrowed(c.as_str()), } } @@ -553,6 +573,7 @@ impl<'x> Variable<'x> { Variable::String(s) => s.len(), Variable::Integer(_) | Variable::Float(_) => 2, Variable::Array(l) => l.iter().map(|v| v.len() + 2).sum(), + Variable::Constant(c) => c.as_str().len(), } } @@ -591,6 +612,7 @@ impl<'x> Variable<'x> { Variable::String(s) => Variable::String(StringCow::Owned(s.into_owned())), Variable::Integer(n) => Variable::Integer(n), Variable::Float(n) => Variable::Float(n), + Variable::Constant(c) => Variable::Constant(c), Variable::Array(l) => Variable::Array(l.into_iter().map(|v| v.into_owned()).collect()), } } @@ -632,7 +654,10 @@ impl PartialOrd for Variable<'_> { } (Self::Array(a), Self::Array(b)) => a.partial_cmp(b), (Self::Array(_) | Self::String(_), _) => Ordering::Greater.into(), - (_, Self::Array(_)) => Ordering::Less.into(), + (Self::Constant(a), Self::Constant(b)) => a.to_id().partial_cmp(&b.to_id()), + (_, Self::Array(_) | Self::Constant(_)) | (Self::Constant(_), _) => { + Ordering::Less.into() + } } } } @@ -658,6 +683,7 @@ impl Display for Variable<'_> { } Ok(()) } + Variable::Constant(c) => c.as_str().fmt(f), } } } @@ -668,6 +694,7 @@ impl<'x> From<&'x Constant> for Variable<'x> { Constant::Integer(i) => Variable::Integer(*i), Constant::Float(f) => Variable::Float(*f), Constant::String(s) => Variable::String(StringCow::Borrowed(s.as_str())), + Constant::Static(c) => Variable::Constant(*c), } } } diff --git a/crates/common/src/expr/functions/misc.rs b/crates/common/src/expr/functions/misc.rs index 9550ee1a..692767c7 100644 --- a/crates/common/src/expr/functions/misc.rs +++ b/crates/common/src/expr/functions/misc.rs @@ -4,17 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::net::IpAddr; - +use crate::expr::Variable; use compact_str::CompactString; use mail_auth::common::resolver::ToReverseName; - -use crate::expr::Variable; +use std::net::IpAddr; pub(crate) fn fn_is_empty(v: Vec) -> Variable { match &v[0] { Variable::String(s) => s.is_empty(), - Variable::Integer(_) | Variable::Float(_) => false, + Variable::Integer(_) | Variable::Float(_) | Variable::Constant(_) => false, Variable::Array(a) => a.is_empty(), } .into() diff --git a/crates/common/src/expr/functions/mod.rs b/crates/common/src/expr/functions/mod.rs index 431b9a8f..4f190ef2 100644 --- a/crates/common/src/expr/functions/mod.rs +++ b/crates/common/src/expr/functions/mod.rs @@ -5,6 +5,7 @@ */ use super::{StringCow, Variable}; +use registry::schema::enums::ExpressionVariable; pub mod array; pub mod asynch; @@ -13,7 +14,7 @@ pub mod misc; pub mod text; pub trait ResolveVariable: Sync + Send { - fn resolve_variable(&self, variable: u32) -> Variable<'_>; + fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_>; fn resolve_global(&self, variable: &str) -> Variable<'_>; } diff --git a/crates/common/src/expr/if_block.rs b/crates/common/src/expr/if_block.rs index 224f8457..c7d57ff1 100644 --- a/crates/common/src/expr/if_block.rs +++ b/crates/common/src/expr/if_block.rs @@ -5,7 +5,7 @@ */ use super::{ - ConstantValue, ExpressionItem, + ExpressionItem, parser::ExpressionParser, tokenizer::{TokenMap, Tokenizer}, }; @@ -15,7 +15,10 @@ use crate::{ }; use compact_str::CompactString; use registry::{ - schema::{prelude::Property, structs}, + schema::{ + prelude::{ExpressionContext, Property}, + structs, + }, types::id::Id, }; @@ -33,22 +36,24 @@ pub struct IfBlock { } impl IfBlock { - pub fn new_default(property: Property, expr: structs::Expression) -> Self { - let token_map = TokenMap::default() - .with_all_variables() - .with_constants::(); + pub fn new_default(expr_ctx: ExpressionContext<'_>) -> Self { + let token_map = TokenMap::default(); - Self { - property, - if_then: expr - .match_ - .into_iter() - .map(|match_| IfThen { - expr: Expression::parse(&token_map, &match_.if_), - then: Expression::parse(&token_map, &match_.then), - }) - .collect(), - default: Expression::parse(&token_map, &expr.else_), + if let Some(default) = &expr_ctx.default { + Self { + property: expr_ctx.property, + if_then: default + .match_ + .iter() + .map(|match_| IfThen { + expr: Expression::parse(&token_map, &match_.if_), + then: Expression::parse(&token_map, &match_.then), + }) + .collect(), + default: Expression::parse(&token_map, &default.else_), + } + } else { + Self::empty(expr_ctx.property) } } @@ -75,17 +80,37 @@ impl Expression { } } -impl IfBlock { - pub fn try_parse( - bp: &mut Bootstrap, +impl Bootstrap { + pub fn compile_expr(&mut self, id: Id, expr_ctx: &ExpressionContext<'_>) -> IfBlock { + if expr_ctx.expr.else_.is_empty() && expr_ctx.expr.match_.is_empty() { + return IfBlock::empty(expr_ctx.property); + } + + if let Some(if_block) = self.try_compile_expr(id, expr_ctx, &expr_ctx.expr) { + if_block + } else { + self.compile_default_expr(id, expr_ctx) + } + } + + pub fn compile_default_expr(&mut self, id: Id, expr_ctx: &ExpressionContext<'_>) -> IfBlock { + if let Some(default) = &expr_ctx.default { + self.try_compile_expr(id, expr_ctx, default) + .expect("Valid default expression") + } else { + IfBlock::empty(expr_ctx.property) + } + } + + pub fn try_compile_expr( + &mut self, id: Id, - property: Property, - expr: structs::Expression, - token_map: &TokenMap, + expr_ctx: &ExpressionContext<'_>, + expr: &structs::Expression, ) -> Option { // Parse conditions let mut if_block = IfBlock { - property, + property: expr_ctx.property, if_then: Vec::with_capacity(expr.match_.len()), default: Expression { items: Default::default(), @@ -94,7 +119,11 @@ impl IfBlock { if expr.else_.is_empty() { if !expr.match_.is_empty() { - bp.invalid_property(id, property, "Missing 'else' block in 'if' expression"); + self.invalid_property( + id, + expr_ctx.property, + "Missing 'else' block in 'if' expression", + ); } return None; } @@ -104,28 +133,36 @@ impl IfBlock { .iter() .any(|m| m.if_.is_empty() || m.then.is_empty()) { - bp.invalid_property(id, property, "All 'if' and 'then' blocks must be non-empty"); + self.invalid_property( + id, + expr_ctx.property, + "All 'if' and 'then' blocks must be non-empty", + ); return None; } - match ExpressionParser::new(Tokenizer::new(&expr.else_, token_map)).parse() { + let token_map = TokenMap::default() + .with_variables(expr_ctx.allowed_variables) + .with_constants(expr_ctx.allowed_constants); + + match ExpressionParser::new(Tokenizer::new(&expr.else_, &token_map)).parse() { Ok(expr) => { if_block.default = expr; } Err(err) => { - bp.invalid_property( + self.invalid_property( id, - property, + expr_ctx.property, &format!("Error parsing 'else' expression: {}", err), ); return None; } } - for (num, match_) in expr.match_.into_iter().enumerate() { - match ExpressionParser::new(Tokenizer::new(&match_.if_, token_map)).parse() { + for (num, match_) in expr.match_.iter().enumerate() { + match ExpressionParser::new(Tokenizer::new(&match_.if_, &token_map)).parse() { Ok(if_expr) => { - match ExpressionParser::new(Tokenizer::new(&match_.then, token_map)).parse() { + match ExpressionParser::new(Tokenizer::new(&match_.then, &token_map)).parse() { Ok(then_expr) => { if_block.if_then.push(IfThen { expr: if_expr, @@ -133,9 +170,9 @@ impl IfBlock { }); } Err(err) => { - bp.invalid_property( + self.invalid_property( id, - property, + expr_ctx.property, &format!( "Error parsing 'then' expression in condition #{}: {}", num + 1, @@ -147,9 +184,9 @@ impl IfBlock { } } Err(err) => { - bp.invalid_property( + self.invalid_property( id, - property, + expr_ctx.property, &format!( "Error parsing 'if' expression in condition #{}: {}", num + 1, @@ -163,7 +200,9 @@ impl IfBlock { Some(if_block) } +} +impl IfBlock { pub fn into_default(self, property: Property) -> IfBlock { IfBlock { property, diff --git a/crates/common/src/expr/mod.rs b/crates/common/src/expr/mod.rs index 5ecf2ca8..27748c55 100644 --- a/crates/common/src/expr/mod.rs +++ b/crates/common/src/expr/mod.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use self::tokenizer::TokenMap; use compact_str::CompactString; use regex::Regex; +use registry::schema::enums::{ExpressionConstant, ExpressionVariable}; use std::{ borrow::Cow, fmt::{Display, Formatter}, @@ -15,76 +15,6 @@ use std::{ }; use utils::config::{Rate, utils::ParseValue}; -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_REMOTE_PORT: u32 = 9; -pub const V_LOCAL_IP: u32 = 10; -pub const V_LOCAL_PORT: u32 = 11; -pub const V_PRIORITY: u32 = 12; -pub const V_PROTOCOL: u32 = 13; -pub const V_TLS: u32 = 14; -pub const V_RECIPIENTS: u32 = 15; -pub const V_QUEUE_RETRY_NUM: u32 = 16; -pub const V_QUEUE_NOTIFY_NUM: u32 = 17; -pub const V_QUEUE_EXPIRES_IN: u32 = 18; -pub const V_QUEUE_LAST_STATUS: u32 = 19; -pub const V_QUEUE_LAST_ERROR: u32 = 20; -pub const V_URL: u32 = 21; -pub const V_URL_PATH: u32 = 22; -pub const V_HEADERS: u32 = 23; -pub const V_METHOD: u32 = 24; -pub const V_ASN: u32 = 25; -pub const V_COUNTRY: u32 = 26; -pub const V_RECEIVED_VIA_PORT: u32 = 27; -pub const V_RECEIVED_FROM_IP: u32 = 28; -pub const V_QUEUE_NAME: u32 = 29; -pub const V_SOURCE: u32 = 30; -pub const V_SIZE: u32 = 31; -pub const V_QUEUE_AGE: u32 = 32; - -pub const VARIABLES_MAP: &[(&str, u32)] = &[ - ("rcpt", V_RECIPIENT), - ("rcpt_domain", V_RECIPIENT_DOMAIN), - ("sender", V_SENDER), - ("sender_domain", V_SENDER_DOMAIN), - ("mx", V_MX), - ("helo_domain", V_HELO_DOMAIN), - ("authenticated_as", V_AUTHENTICATED_AS), - ("listener", V_LISTENER), - ("remote_ip", V_REMOTE_IP), - ("local_ip", V_LOCAL_IP), - ("priority", V_PRIORITY), - ("local_port", V_LOCAL_PORT), - ("remote_port", V_REMOTE_PORT), - ("protocol", V_PROTOCOL), - ("is_tls", V_TLS), - ("recipients", V_RECIPIENTS), - ("retry_num", V_QUEUE_RETRY_NUM), - ("notify_num", V_QUEUE_NOTIFY_NUM), - ("expires_in", V_QUEUE_EXPIRES_IN), - ("last_status", V_QUEUE_LAST_STATUS), - ("last_error", V_QUEUE_LAST_ERROR), - ("url", V_URL), - ("url_path", V_URL_PATH), - ("headers", V_HEADERS), - ("method", V_METHOD), - ("asn", V_ASN), - ("country", V_COUNTRY), - ("received_via_port", V_RECEIVED_VIA_PORT), - ("received_from_ip", V_RECEIVED_FROM_IP), - ("queue_name", V_QUEUE_NAME), - ("source", V_SOURCE), - ("size", V_SIZE), - ("queue_age", V_QUEUE_AGE), -]; - pub mod eval; pub mod functions; pub mod if_block; @@ -98,9 +28,9 @@ pub struct Expression { #[derive(Debug, Clone)] pub enum ExpressionItem { - Variable(u32), + Variable(ExpressionVariable), Global(CompactString), - Setting(Setting), + System(SystemVariable), Capture(u32), Constant(Constant), BinaryOperator(BinaryOperator), @@ -118,6 +48,7 @@ pub enum Variable<'x> { Integer(i64), Float(f64), Array(Vec>), + Constant(ExpressionConstant), } #[derive(Debug, Clone)] @@ -134,6 +65,7 @@ impl Default for Variable<'_> { #[derive(Debug, PartialEq, Clone)] pub enum Constant { + Static(ExpressionConstant), Integer(i64), Float(f64), String(CompactString), @@ -210,7 +142,7 @@ pub enum UnaryOperator { #[derive(Debug, Clone)] pub enum Token { - Variable(u32), + Variable(ExpressionVariable), Global(CompactString), Capture(u32), Function { @@ -219,7 +151,7 @@ pub enum Token { num_args: u32, }, Constant(Constant), - Setting(Setting), + System(SystemVariable), Regex(Regex), BinaryOperator(BinaryOperator), UnaryOperator(UnaryOperator), @@ -231,10 +163,11 @@ pub enum Token { } #[derive(Debug, Clone)] -pub enum Setting { +pub enum SystemVariable { Hostname, Domain, NodeId, + Metric(usize), } impl From for Variable<'_> { @@ -380,18 +313,6 @@ impl PartialEq for Token { impl Eq for Token {} -pub struct NoConstants; - -pub trait ConstantValue: - ParseValue + for<'x> TryFrom> + Into + Sized -{ - fn add_constants(token_map: &mut TokenMap); -} - -impl ConstantValue for () { - fn add_constants(_: &mut TokenMap) {} -} - impl From<()> for Constant { fn from(_: ()) -> Self { Constant::Integer(0) @@ -406,10 +327,6 @@ impl<'x> TryFrom> for () { } } -impl ConstantValue for Duration { - fn add_constants(_: &mut TokenMap) {} -} - impl<'x> TryFrom> for Duration { type Error = (); diff --git a/crates/common/src/expr/parser.rs b/crates/common/src/expr/parser.rs index 9a206409..4800f0e1 100644 --- a/crates/common/src/expr/parser.rs +++ b/crates/common/src/expr/parser.rs @@ -109,14 +109,11 @@ impl<'x> ExpressionParser<'x> { self.output.push(ExpressionItem::Regex(regex.clone())); self.operator_stack.pop(); } - Some((Token::Setting(setting), _)) => { + Some((Token::System(setting), _)) => { if self.arg_count.pop().unwrap() != 0 { - return Err( - "Expression function \"config_get\" expected 1 argument" - .to_string(), - ); + return Err("Expression function expected 1 argument".to_string()); } - self.output.push(ExpressionItem::Setting(setting.clone())); + self.output.push(ExpressionItem::System(setting.clone())); self.operator_stack.pop(); } _ => {} @@ -166,7 +163,7 @@ impl<'x> ExpressionParser<'x> { self.operator_stack .push((Token::BinaryOperator(bop), jmp_pos)); } - token @ (Token::Function { .. } | Token::Regex(_) | Token::Setting(_)) => { + token @ (Token::Function { .. } | Token::Regex(_) | Token::System(_)) => { self.inc_arg_count(); self.arg_count.push(0); self.operator_stack.push((token, None)) diff --git a/crates/common/src/expr/tokenizer.rs b/crates/common/src/expr/tokenizer.rs index bf7c3e6b..9db459bd 100644 --- a/crates/common/src/expr/tokenizer.rs +++ b/crates/common/src/expr/tokenizer.rs @@ -4,16 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Cow, iter::Peekable, slice::Iter, time::Duration}; - -use ahash::AHashMap; -use regex::Regex; -use utils::config::utils::ParseValue; - use super::{ functions::{ASYNC_FUNCTIONS, FUNCTIONS}, *, }; +use ahash::AHashSet; +use regex::Regex; +use registry::{schema::enums::ExpressionConstant, types::EnumType}; +use std::{borrow::Cow, iter::Peekable, slice::Iter, time::Duration}; +use trc::{EventType, MetricType, TOTAL_EVENT_COUNT}; +use utils::config::utils::ParseValue; pub struct Tokenizer<'x> { pub(crate) iter: Peekable>, @@ -30,7 +30,8 @@ pub struct Tokenizer<'x> { #[derive(Debug, Default, Clone)] pub struct TokenMap { - pub tokens: AHashMap, Token>, + pub variables: AHashSet, + pub constants: AHashSet, } impl<'x> Tokenizer<'x> { @@ -103,20 +104,38 @@ impl<'x> Tokenizer<'x> { self.find_char(b",")?; (Token::Regex(regex).into(), b'(') } - b"default_domain" => { + b"metric" => { + let stop_ch = self.find_char(b"\"'")?; + let metric_str = self.parse_string(stop_ch)?; + let metric = EventType::try_parse(&metric_str) + .map(|e| e.id()) + .or_else(|| { + MetricType::try_parse(&metric_str) + .map(|m| m.code() as usize + TOTAL_EVENT_COUNT) + }) + .ok_or_else(|| { + format!("Invalid metric name {:?}", metric_str) + })?; self.has_alpha = false; self.buf.clear(); - (Token::Setting(Setting::Domain).into(), b'(') + (Token::System(SystemVariable::Metric(metric)).into(), b'(') } - b"node_hostname" => { + b"system" => { + let stop_ch = self.find_char(b"\"'")?; + let var = match self.parse_string(stop_ch)?.as_str() { + "default_domain" => SystemVariable::Domain, + "hostname" => SystemVariable::Hostname, + "node_id" => SystemVariable::NodeId, + other => { + return Err(format!( + "Invalid system variable name {:?}", + other + )); + } + }; self.has_alpha = false; self.buf.clear(); - (Token::Setting(Setting::Hostname).into(), b'(') - } - b"node_id" => { - self.has_alpha = false; - self.buf.clear(); - (Token::Setting(Setting::NodeId).into(), b'(') + (Token::System(var).into(), b'(') } _ => { self.is_start = false; @@ -347,8 +366,22 @@ impl<'x> Tokenizer<'x> { id: *idx + FUNCTIONS.len() as u32, num_args: *num_args, }) - } else if let Some(token) = self.token_map.tokens.get(buf.as_str()) { - Ok(token.clone()) + } else if let Some(variable) = ExpressionVariable::parse(buf.as_str()) { + if self.token_map.variables.is_empty() + || self.token_map.variables.contains(&variable) + { + Ok(Token::Variable(variable)) + } else { + Err(format!("Variable {:?} not allowed in this context", buf)) + } + } else if let Some(constant) = ExpressionConstant::parse(buf.as_str()) { + if self.token_map.constants.is_empty() + || self.token_map.constants.contains(&constant) + { + Ok(Token::Constant(Constant::Static(constant))) + } else { + Err(format!("Constant {:?} not allowed in this context", buf)) + } } else if let Ok(duration) = Duration::parse_value(&buf) { Ok(Token::Constant(Constant::Integer( duration.as_millis() as i64 @@ -361,83 +394,13 @@ impl<'x> Tokenizer<'x> { } impl TokenMap { - pub fn with_all_variables(self) -> Self { - self.with_variables(&[ - V_RECIPIENT, - V_RECIPIENT_DOMAIN, - V_SENDER, - V_SENDER_DOMAIN, - V_MX, - V_HELO_DOMAIN, - V_AUTHENTICATED_AS, - V_LISTENER, - V_REMOTE_IP, - V_REMOTE_PORT, - V_LOCAL_IP, - V_LOCAL_PORT, - V_PRIORITY, - V_PROTOCOL, - V_TLS, - V_QUEUE_RETRY_NUM, - V_QUEUE_NOTIFY_NUM, - V_QUEUE_EXPIRES_IN, - V_QUEUE_LAST_STATUS, - V_QUEUE_LAST_ERROR, - V_QUEUE_NAME, - V_QUEUE_AGE, - V_ASN, - V_COUNTRY, - V_RECEIVED_FROM_IP, - V_RECEIVED_VIA_PORT, - V_SOURCE, - V_SIZE, - ]) - } - - pub fn with_variables(mut self, variables: &[u32]) -> Self { - for (name, idx) in VARIABLES_MAP { - if variables.contains(idx) { - self.tokens - .insert(Cow::Borrowed(name), Token::Variable(*idx)); - } - } - + pub fn with_variables(mut self, variables: &[ExpressionVariable]) -> Self { + self.variables.extend(variables.iter().copied()); self } - pub fn with_variables_map(mut self, vars: I) -> Self - where - I: IntoIterator, - V: Into>, - { - for (name, idx) in vars { - self.tokens.insert(name.into(), Token::Variable(idx)); - } - - self - } - - pub fn set_constants(mut self, consts: I) -> Self - where - I: IntoIterator, - T: Into, - { - for (name, constant) in consts { - self.tokens - .insert(Cow::Borrowed(name), Token::Constant(constant.into())); - } - - self - } - - pub fn with_constants(mut self) -> Self { - T::add_constants(&mut self); - self - } - - pub fn add_constant(&mut self, name: &'static str, constant: impl Into) -> &mut Self { - self.tokens - .insert(Cow::Borrowed(name), Token::Constant(constant.into())); + pub fn with_constants(mut self, constants: &[ExpressionConstant]) -> Self { + self.constants.extend(constants.iter().copied()); self } } diff --git a/crates/common/src/listener/mod.rs b/crates/common/src/listener/mod.rs index f7db6acb..96d1ee9d 100644 --- a/crates/common/src/listener/mod.rs +++ b/crates/common/src/listener/mod.rs @@ -7,6 +7,7 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc, time::Instant}; use compact_str::ToCompactString; +use registry::schema::enums::ExpressionVariable; use rustls::ServerConfig; use std::fmt::Debug; use tokio::{ @@ -222,15 +223,15 @@ pub trait SessionManager: Sync + Send + 'static + Clone { } impl ResolveVariable for SessionData { - fn resolve_variable(&self, variable: u32) -> crate::expr::Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> crate::expr::Variable<'_> { match variable { - V_REMOTE_IP => self.remote_ip.to_compact_string().into(), - V_REMOTE_PORT => self.remote_port.into(), - V_LOCAL_IP => self.local_ip.to_compact_string().into(), - V_LOCAL_PORT => self.local_port.into(), - V_LISTENER => self.instance.id.as_str().into(), - V_PROTOCOL => self.protocol.as_str().into(), - V_TLS => self.stream.is_tls().into(), + ExpressionVariable::RemoteIp => self.remote_ip.to_compact_string().into(), + ExpressionVariable::RemotePort => self.remote_port.into(), + ExpressionVariable::LocalIp => self.local_ip.to_compact_string().into(), + ExpressionVariable::LocalPort => self.local_port.into(), + ExpressionVariable::Listener => self.instance.id.as_str().into(), + ExpressionVariable::Protocol => self.protocol.as_str().into(), + ExpressionVariable::IsTls => self.stream.is_tls().into(), _ => crate::expr::Variable::default(), } } diff --git a/crates/common/src/manager/bootstrap.rs b/crates/common/src/manager/bootstrap.rs index 4677e78a..d24c1f0c 100644 --- a/crates/common/src/manager/bootstrap.rs +++ b/crates/common/src/manager/bootstrap.rs @@ -95,4 +95,21 @@ impl Bootstrap { }], }); } + + pub fn validate(&mut self, id: Id, object: &impl ObjectType) -> bool { + let mut errors = Vec::new(); + if object.validate(&mut errors) { + true + } else { + self.errors.push(Error::Validation { + object_id: id, + errors, + }); + false + } + } + + pub fn node_id(&self) -> u64 { + self.node.node_id + } } diff --git a/crates/http-proto/src/context.rs b/crates/http-proto/src/context.rs index d288f047..4f6335dc 100644 --- a/crates/http-proto/src/context.rs +++ b/crates/http-proto/src/context.rs @@ -49,19 +49,21 @@ impl<'x> HttpContext<'x> { } impl ResolveVariable for HttpContext<'_> { - fn resolve_variable(&self, variable: u32) -> Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> { match variable { - V_REMOTE_IP => self.session.remote_ip.to_compact_string().into(), - V_REMOTE_PORT => self.session.remote_port.into(), - V_LOCAL_IP => self.session.local_ip.to_compact_string().into(), - V_LOCAL_PORT => self.session.local_port.into(), - V_TLS => self.session.is_tls.into(), - V_PROTOCOL => if self.session.is_tls { "https" } else { "http" }.into(), - V_LISTENER => self.session.instance.id.as_str().into(), - V_URL => self.req.uri().to_compact_string().into(), - V_URL_PATH => self.req.uri().path().into(), - V_METHOD => self.req.method().as_str().into(), - V_HEADERS => self + ExpressionVariable::RemoteIp => self.session.remote_ip.to_compact_string().into(), + ExpressionVariable::RemotePort => self.session.remote_port.into(), + ExpressionVariable::LocalIp => self.session.local_ip.to_compact_string().into(), + ExpressionVariable::LocalPort => self.session.local_port.into(), + ExpressionVariable::IsTls => self.session.is_tls.into(), + ExpressionVariable::Protocol => { + if self.session.is_tls { "https" } else { "http" }.into() + } + ExpressionVariable::Listener => self.session.instance.id.as_str().into(), + ExpressionVariable::Url => self.req.uri().to_compact_string().into(), + ExpressionVariable::UrlPath => self.req.uri().path().into(), + ExpressionVariable::Method => self.req.method().as_str().into(), + ExpressionVariable::Headers => self .req .headers() .iter() diff --git a/crates/registry/src/schema/prelude.rs b/crates/registry/src/schema/prelude.rs index 83151eeb..6fbe25f6 100644 --- a/crates/registry/src/schema/prelude.rs +++ b/crates/registry/src/schema/prelude.rs @@ -20,3 +20,12 @@ pub use crate::types::socketaddr::SocketAddr; pub use serde::{Deserialize, Serialize}; pub use std::collections::HashMap; pub use std::str::FromStr; + +#[derive(Debug)] +pub struct ExpressionContext<'x> { + pub expr: &'x Expression, + pub default: Option, + pub property: Property, + pub allowed_variables: &'static [ExpressionVariable], + pub allowed_constants: &'static [ExpressionConstant], +} diff --git a/crates/registry/src/types/duration.rs b/crates/registry/src/types/duration.rs index 973e4fa6..1c038dca 100644 --- a/crates/registry/src/types/duration.rs +++ b/crates/registry/src/types/duration.rs @@ -4,10 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{fmt::Display, str::FromStr}; use crate::pickle::{Pickle, PickledStream}; +use std::{fmt::Display, str::FromStr}; -#[derive(Debug, Clone, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[repr(transparent)] pub struct Duration(pub std::time::Duration); impl Duration { diff --git a/crates/registry/src/types/id.rs b/crates/registry/src/types/id.rs index 860c0e66..fc53c6dd 100644 --- a/crates/registry/src/types/id.rs +++ b/crates/registry/src/types/id.rs @@ -7,78 +7,40 @@ use crate::{ pickle::{Pickle, PickledStream}, schema::prelude::Object, + types::EnumType, }; use std::str::FromStr; use utils::codec::base32_custom::{BASE32_ALPHABET, BASE32_INVERSE}; #[derive(Debug, PartialEq, Clone, Copy, Eq, Hash)] -#[repr(transparent)] -pub struct Id(u64); +pub struct Id { + object: Object, + id: u64, +} impl Id { pub fn new(object: Object, id: u64) -> Self { - Id(id & (u64::MAX >> 16) | ((object as u64) << 48)) + Self { object, id } } pub fn id(&self) -> u64 { - self.0 + self.id + } + + pub fn object(&self) -> Object { + self.object } pub fn is_valid(&self) -> bool { - self.0 != u64::MAX + self.id != u64::MAX } - // From https://github.com/archer884/crockford by J/A - // License: MIT/Apache 2.0 pub fn as_string(&self) -> String { - match self.0 { - 0 => "a".to_string(), - mut n => { - // Used for the initial shift. - const QUAD_SHIFT: usize = 60; - const QUAD_RESET: usize = 4; - - // Used for all subsequent shifts. - const FIVE_SHIFT: usize = 59; - const FIVE_RESET: usize = 5; - - // After we clear the four most significant bits, the four least significant bits will be - // replaced with 0001. We can then know to stop once the four most significant bits are, - // likewise, 0001. - const STOP_BIT: u64 = 1 << QUAD_SHIFT; - - let mut buf = String::with_capacity(7); - - // Start by getting the most significant four bits. We get four here because these would be - // leftovers when starting from the least significant bits. In either case, tag the four least - // significant bits with our stop bit. - match (n >> QUAD_SHIFT) as usize { - // Eat leading zero-bits. This should not be done if the first four bits were non-zero. - // Additionally, we *must* do this in increments of five bits. - 0 => { - n <<= QUAD_RESET; - n |= 1; - n <<= n.leading_zeros() / 5 * 5; - } - - // Write value of first four bytes. - i => { - n <<= QUAD_RESET; - n |= 1; - buf.push(char::from(BASE32_ALPHABET[i])); - } - } - - // From now until we reach the stop bit, take the five most significant bits and then shift - // left by five bits. - while n != STOP_BIT { - buf.push(char::from(BASE32_ALPHABET[(n >> FIVE_SHIFT) as usize])); - n <<= FIVE_RESET; - } - - buf - } - } + let mut out = String::with_capacity(14); + encode(self.object.to_id() as u64, &mut out); + out.push(':'); + encode(self.id, &mut out); + out } } @@ -88,7 +50,7 @@ impl Object { } pub fn singleton(&self) -> Id { - Id::new(*self, u64::MAX) + Id::new(*self, 20080258862541) } } @@ -96,24 +58,85 @@ impl FromStr for Id { type Err = (); fn from_str(s: &str) -> Result { - let mut id = 0; + match s.split_once(':') { + Some((obj_str, id_str)) => { + let object_id = decode(obj_str).ok_or(())?; + let object = Object::from_id(object_id as u16).ok_or(())?; + let id = decode(id_str).ok_or(())?; + Ok(Id::new(object, id)) + } + None => Err(()), + } + } +} - for &ch in s.as_bytes() { - let i = BASE32_INVERSE[ch as usize]; - if i != u8::MAX { - id = (id << 5) | i as u64; - } else { - return Err(()); +fn decode(s: &str) -> Option { + let mut n = 0u64; + + for &ch in s.as_bytes() { + let i = BASE32_INVERSE[ch as usize]; + if i != u8::MAX { + n = (n << 5) | i as u64; + } else { + return None; + } + } + + Some(n) +} + +// From https://github.com/archer884/crockford by J/A +// License: MIT/Apache 2.0 +fn encode(n: u64, out: &mut String) { + match n { + 0 => out.push('a'), + mut n => { + // Used for the initial shift. + const QUAD_SHIFT: usize = 60; + const QUAD_RESET: usize = 4; + + // Used for all subsequent shifts. + const FIVE_SHIFT: usize = 59; + const FIVE_RESET: usize = 5; + + // After we clear the four most significant bits, the four least significant bits will be + // replaced with 0001. We can then know to stop once the four most significant bits are, + // likewise, 0001. + const STOP_BIT: u64 = 1 << QUAD_SHIFT; + + // Start by getting the most significant four bits. We get four here because these would be + // leftovers when starting from the least significant bits. In either case, tag the four least + // significant bits with our stop bit. + match (n >> QUAD_SHIFT) as usize { + // Eat leading zero-bits. This should not be done if the first four bits were non-zero. + // Additionally, we *must* do this in increments of five bits. + 0 => { + n <<= QUAD_RESET; + n |= 1; + n <<= n.leading_zeros() / 5 * 5; + } + + // Write value of first four bytes. + i => { + n <<= QUAD_RESET; + n |= 1; + out.push(char::from(BASE32_ALPHABET[i])); + } + } + + // From now until we reach the stop bit, take the five most significant bits and then shift + // left by five bits. + while n != STOP_BIT { + out.push(char::from(BASE32_ALPHABET[(n >> FIVE_SHIFT) as usize])); + n <<= FIVE_RESET; } } - - Ok(Id(id)) } } impl Default for Id { fn default() -> Self { - Id(u64::MAX) + Id::new(Object::Account, u64::MAX) } } @@ -144,12 +167,18 @@ impl std::fmt::Display for Id { impl Pickle for Id { fn pickle(&self, out: &mut Vec) { - out.extend_from_slice(&self.0.to_le_bytes()); + out.extend_from_slice(&self.object.to_id().to_le_bytes()); + out.extend_from_slice(&self.id.to_le_bytes()); } fn unpickle(data: &mut PickledStream<'_>) -> Option { + let mut arr = [0u8; 2]; + arr.copy_from_slice(data.read_bytes(2)?); + let object = Object::from_id(u16::from_le_bytes(arr))?; let mut arr = [0u8; 8]; arr.copy_from_slice(data.read_bytes(8)?); - Some(Id(u64::from_le_bytes(arr))) + let id = u64::from_le_bytes(arr); + + Some(Id { object, id }) } } diff --git a/crates/smtp/src/core/throttle.rs b/crates/smtp/src/core/throttle.rs index 83fa48fa..81c0917c 100644 --- a/crates/smtp/src/core/throttle.rs +++ b/crates/smtp/src/core/throttle.rs @@ -25,17 +25,21 @@ impl NewKey for QueueQuota { let mut hasher = blake3::Hasher::new(); if (self.keys & THROTTLE_RCPT) != 0 { - hasher.update(e.resolve_variable(V_RECIPIENT).to_string().as_bytes()); + hasher.update( + e.resolve_variable(ExpressionVariable::Rcpt) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_RCPT_DOMAIN) != 0 { hasher.update( - e.resolve_variable(V_RECIPIENT_DOMAIN) + e.resolve_variable(ExpressionVariable::RcptDomain) .to_string() .as_bytes(), ); } if (self.keys & THROTTLE_SENDER) != 0 { - let sender = e.resolve_variable(V_SENDER).into_string(); + let sender = e.resolve_variable(ExpressionVariable::Sender).into_string(); hasher.update( if !sender.is_empty() { sender.as_ref() @@ -46,7 +50,9 @@ impl NewKey for QueueQuota { ); } if (self.keys & THROTTLE_SENDER_DOMAIN) != 0 { - let sender_domain = e.resolve_variable(V_SENDER_DOMAIN).into_string(); + let sender_domain = e + .resolve_variable(ExpressionVariable::SenderDomain) + .into_string(); hasher.update( if !sender_domain.is_empty() { sender_domain.as_ref() @@ -76,17 +82,21 @@ impl NewKey for QueueRateLimiter { let mut hasher = blake3::Hasher::new(); if (self.keys & THROTTLE_RCPT) != 0 { - hasher.update(e.resolve_variable(V_RECIPIENT).to_string().as_bytes()); + hasher.update( + e.resolve_variable(ExpressionVariable::Rcpt) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_RCPT_DOMAIN) != 0 { hasher.update( - e.resolve_variable(V_RECIPIENT_DOMAIN) + e.resolve_variable(ExpressionVariable::RcptDomain) .to_string() .as_bytes(), ); } if (self.keys & THROTTLE_SENDER) != 0 { - let sender = e.resolve_variable(V_SENDER).into_string(); + let sender = e.resolve_variable(ExpressionVariable::Sender).into_string(); hasher.update( if !sender.is_empty() { sender.as_ref() @@ -97,7 +107,9 @@ impl NewKey for QueueRateLimiter { ); } if (self.keys & THROTTLE_SENDER_DOMAIN) != 0 { - let sender_domain = e.resolve_variable(V_SENDER_DOMAIN).into_string(); + let sender_domain = e + .resolve_variable(ExpressionVariable::SenderDomain) + .into_string(); hasher.update( if !sender_domain.is_empty() { sender_domain.as_ref() @@ -108,26 +120,38 @@ impl NewKey for QueueRateLimiter { ); } if (self.keys & THROTTLE_HELO_DOMAIN) != 0 { - hasher.update(e.resolve_variable(V_HELO_DOMAIN).to_string().as_bytes()); + hasher.update( + e.resolve_variable(ExpressionVariable::HeloDomain) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_AUTH_AS) != 0 { hasher.update( - e.resolve_variable(V_AUTHENTICATED_AS) + e.resolve_variable(ExpressionVariable::AuthenticatedAs) .to_string() .as_bytes(), ); } if (self.keys & THROTTLE_LISTENER) != 0 { - hasher.update(e.resolve_variable(V_LISTENER).to_string().as_bytes()); + hasher.update( + e.resolve_variable(ExpressionVariable::Listener) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_MX) != 0 { - hasher.update(e.resolve_variable(V_MX).to_string().as_bytes()); + hasher.update( + e.resolve_variable(ExpressionVariable::Mx) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_REMOTE_IP) != 0 { - hasher.update(e.resolve_variable(V_REMOTE_IP).to_string().as_bytes()); + hasher.update(e.resolve_variable(ExpressionVariable::RemoteIp).to_string().as_bytes()); } if (self.keys & THROTTLE_LOCAL_IP) != 0 { - hasher.update(e.resolve_variable(V_LOCAL_IP).to_string().as_bytes()); + hasher.update(e.resolve_variable(ExpressionVariable::LocalIp).to_string().as_bytes()); } hasher.update(&self.rate.period.as_secs().to_be_bytes()[..]); hasher.update(&self.rate.requests.to_be_bytes()[..]); diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index aba24aa7..f479016c 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -539,54 +539,56 @@ impl Session { } impl ResolveVariable for Session { - fn resolve_variable(&self, variable: u32) -> expr::Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'_> { match variable { - V_RECIPIENT => self + ExpressionVariable::Rcpt => self .data .rcpt_to .last() .map(|r| r.address_lcase.as_str()) .unwrap_or_default() .into(), - V_RECIPIENT_DOMAIN => self + ExpressionVariable::RcptDomain => self .data .rcpt_to .last() .map(|r| r.domain.as_str()) .unwrap_or_default() .into(), - V_RECIPIENTS => self + ExpressionVariable::Recipients => self .data .rcpt_to .iter() .map(|r| Variable::from(r.address_lcase.as_str())) .collect::>() .into(), - V_SENDER => self + ExpressionVariable::Sender => self .data .mail_from .as_ref() .map(|m| m.address_lcase.as_str()) .unwrap_or_default() .into(), - V_SENDER_DOMAIN => self + ExpressionVariable::SenderDomain => self .data .mail_from .as_ref() .map(|m| m.domain.as_str()) .unwrap_or_default() .into(), - V_HELO_DOMAIN => self.data.helo_domain.as_str().into(), - V_AUTHENTICATED_AS => self.authenticated_as().unwrap_or_default().into(), - V_LISTENER => self.instance.id.as_str().into(), - V_REMOTE_IP => self.data.remote_ip_str.as_str().into(), - V_REMOTE_PORT => self.data.remote_port.into(), - V_LOCAL_IP => self.data.local_ip_str.as_str().into(), - V_LOCAL_PORT => self.data.local_port.into(), - V_TLS => self.stream.is_tls().into(), - V_PRIORITY => self.data.priority.to_compact_string().into(), - V_PROTOCOL => self.instance.protocol.as_str().into(), - V_ASN => self + ExpressionVariable::HeloDomain => self.data.helo_domain.as_str().into(), + ExpressionVariable::AuthenticatedAs => { + self.authenticated_as().unwrap_or_default().into() + } + ExpressionVariable::Listener => self.instance.id.as_str().into(), + ExpressionVariable::RemoteIp => self.data.remote_ip_str.as_str().into(), + ExpressionVariable::RemotePort => self.data.remote_port.into(), + ExpressionVariable::LocalIp => self.data.local_ip_str.as_str().into(), + ExpressionVariable::LocalPort => self.data.local_port.into(), + ExpressionVariable::IsTls => self.stream.is_tls().into(), + ExpressionVariable::Priority => self.data.priority.to_compact_string().into(), + ExpressionVariable::Protocol => self.instance.protocol.as_str().into(), + ExpressionVariable::Asn => self .data .asn_geo_data .asn @@ -594,7 +596,7 @@ impl ResolveVariable for Session { .map(|a| a.id) .unwrap_or_default() .into(), - V_COUNTRY => self + ExpressionVariable::Country => self .data .asn_geo_data .country diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index f7e5c998..dde0caa4 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -9,7 +9,7 @@ use crate::queue::{Error, ErrorDetails, HostResponse, Status}; use common::{ Server, config::smtp::queue::{ConnectionStrategy, IpAndHost, MxConfig}, - expr::{V_MX, functions::ResolveVariable}, + expr::{ExpressionVariable::Mx, functions::ResolveVariable}, }; use mail_auth::{IpLookupStrategy, MX}; use rand::{Rng, seq::SliceRandom}; @@ -165,7 +165,9 @@ impl DnsLookup for Server { details: Error::DnsError( format!( "No IP addresses found for {:?}.", - envelope.resolve_variable(V_MX).to_string() + envelope + .resolve_variable(ExpressionVariable::Mx) + .to_string() ) .into_boxed_str(), ), diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs index fa1eb162..31b62605 100644 --- a/crates/smtp/src/queue/mod.rs +++ b/crates/smtp/src/queue/mod.rs @@ -280,30 +280,30 @@ impl<'x> QueueEnvelope<'x> { } impl<'x> ResolveVariable for QueueEnvelope<'x> { - fn resolve_variable(&self, variable: u32) -> expr::Variable<'x> { + fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'x> { match variable { - V_SENDER => self.message.return_path.as_ref().into(), - V_SENDER_DOMAIN => self.message.return_path.domain_part().into(), - V_RECIPIENT_DOMAIN => self.domain.into(), - V_RECIPIENT => self.rcpt.address.as_ref().into(), - V_RECIPIENTS => self + ExpressionVariable::Sender => self.message.return_path.as_ref().into(), + ExpressionVariable::SenderDomain => self.message.return_path.domain_part().into(), + ExpressionVariable::RcptDomain => self.domain.into(), + ExpressionVariable::Rcpt => self.rcpt.address.as_ref().into(), + ExpressionVariable::Recipients => self .message .recipients .iter() .map(|r| Variable::from(r.address.as_ref())) .collect::>() .into(), - V_QUEUE_RETRY_NUM => self.rcpt.retry.inner.into(), - V_QUEUE_NOTIFY_NUM => self.rcpt.notify.inner.into(), - V_QUEUE_EXPIRES_IN => match &self.rcpt.expires { + ExpressionVariable::RetryNum => self.rcpt.retry.inner.into(), + ExpressionVariable::NotifyNum => self.rcpt.notify.inner.into(), + ExpressionVariable::ExpiresIn => match &self.rcpt.expires { QueueExpiry::Ttl(time) => (*time + self.message.created).saturating_sub(now()), QueueExpiry::Attempts(count) => { (count.saturating_sub(self.rcpt.retry.inner)) as u64 } } .into(), - V_QUEUE_LAST_STATUS => self.rcpt.status.to_compact_string().into(), - V_QUEUE_LAST_ERROR => match &self.rcpt.status { + ExpressionVariable::LastStatus => self.rcpt.status.to_compact_string().into(), + ExpressionVariable::LastError => match &self.rcpt.status { Status::Scheduled | Status::Completed(_) => "none", Status::TemporaryFailure(err) | Status::PermanentFailure(err) => { match &err.details { @@ -320,9 +320,9 @@ impl<'x> ResolveVariable for QueueEnvelope<'x> { } } .into(), - V_QUEUE_NAME => self.rcpt.queue.as_str().into(), - V_QUEUE_AGE => now().saturating_sub(self.message.created).into(), - V_SOURCE => if (self.message.flags & FROM_AUTHENTICATED) != 0 { + ExpressionVariable::QueueName => self.rcpt.queue.as_str().into(), + ExpressionVariable::QueueAge => now().saturating_sub(self.message.created).into(), + ExpressionVariable::Source => if (self.message.flags & FROM_AUTHENTICATED) != 0 { "authenticated" } else if (self.message.flags & FROM_UNAUTHENTICATED_DMARC) != 0 { "dmarc_pass" @@ -338,13 +338,15 @@ impl<'x> ResolveVariable for QueueEnvelope<'x> { "unknown" } .into(), - V_MX => self.mx.into(), - V_PRIORITY => self.message.priority.into(), - V_REMOTE_IP => self.remote_ip.to_compact_string().into(), - V_LOCAL_IP => self.local_ip.to_compact_string().into(), - V_RECEIVED_FROM_IP => self.message.received_from_ip.to_compact_string().into(), - V_RECEIVED_VIA_PORT => self.message.received_via_port.into(), - V_SIZE => self.message.size.into(), + ExpressionVariable::Mx => self.mx.into(), + ExpressionVariable::Priority => self.message.priority.into(), + ExpressionVariable::RemoteIp => self.remote_ip.to_compact_string().into(), + ExpressionVariable::LocalIp => self.local_ip.to_compact_string().into(), + ExpressionVariable::ReceivedFromIp => { + self.message.received_from_ip.to_compact_string().into() + } + ExpressionVariable::ReceivedViaPort => self.message.received_via_port.into(), + ExpressionVariable::Size => self.message.size.into(), _ => "".into(), } } @@ -355,17 +357,17 @@ impl<'x> ResolveVariable for QueueEnvelope<'x> { } impl ResolveVariable for Message { - fn resolve_variable(&self, variable: u32) -> expr::Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'_> { match variable { - V_SENDER => self.return_path.as_ref().into(), - V_SENDER_DOMAIN => self.return_path.domain_part().into(), - V_RECIPIENTS => self + ExpressionVariable::Sender => self.return_path.as_ref().into(), + ExpressionVariable::SenderDomain => self.return_path.domain_part().into(), + ExpressionVariable::Recipients => self .recipients .iter() .map(|r| Variable::from(r.address.as_ref())) .collect::>() .into(), - V_PRIORITY => self.priority.into(), + ExpressionVariable::Priority => self.priority.into(), _ => "".into(), } } @@ -384,9 +386,9 @@ impl<'x> RecipientDomain<'x> { } impl<'x> ResolveVariable for RecipientDomain<'x> { - fn resolve_variable(&self, variable: u32) -> expr::Variable<'x> { + fn resolve_variable(&self, variable: ExpressionVariable) -> expr::Variable<'x> { match variable { - V_RECIPIENT_DOMAIN => self.0.into(), + ExpressionVariable::RcptDomain => self.0.into(), _ => "".into(), } } diff --git a/crates/spam-filter/src/modules/expression.rs b/crates/spam-filter/src/modules/expression.rs index 37fec886..514f130c 100644 --- a/crates/spam-filter/src/modules/expression.rs +++ b/crates/spam-filter/src/modules/expression.rs @@ -6,7 +6,7 @@ use common::{ config::spamfilter::*, - expr::{StringCow, Variable, functions::ResolveVariable}, + expr::{Expression, StringCow, Variable, functions::ResolveVariable}, }; use compact_str::{CompactString, ToCompactString, format_compact}; use mail_parser::{Header, HeaderValue}; @@ -31,25 +31,28 @@ impl<'x, T: ResolveVariable> SpamFilterResolver<'x, T> { } impl ResolveVariable for SpamFilterResolver<'_, T> { - fn resolve_variable(&self, variable: u32) -> Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> { match variable { - 0..100 => self.item.resolve_variable(variable), - V_SPAM_REMOTE_IP => self.ctx.input.remote_ip.to_compact_string().into(), - V_SPAM_REMOTE_IP_PTR => self + ExpressionVariable::RemoteIp => self.ctx.input.remote_ip.to_compact_string().into(), + ExpressionVariable::RemoteIpPtr => self .ctx .output .iprev_ptr .as_deref() .unwrap_or_default() .into(), - V_SPAM_EHLO_DOMAIN => self.ctx.output.ehlo_host.fqdn.as_str().into(), - V_SPAM_AUTH_AS => self.ctx.input.authenticated_as.unwrap_or_default().into(), - V_SPAM_ASN => self.ctx.input.asn.unwrap_or_default().into(), - V_SPAM_COUNTRY => self.ctx.input.country.unwrap_or_default().into(), - V_SPAM_IS_TLS => self.ctx.input.is_tls.into(), - V_SPAM_ENV_FROM => self.ctx.output.env_from_addr.address.as_str().into(), - V_SPAM_ENV_FROM_LOCAL => self.ctx.output.env_from_addr.local_part.as_str().into(), - V_SPAM_ENV_FROM_DOMAIN => self + ExpressionVariable::HeloDomain => self.ctx.output.ehlo_host.fqdn.as_str().into(), + ExpressionVariable::AuthenticatedAs => { + self.ctx.input.authenticated_as.unwrap_or_default().into() + } + ExpressionVariable::Asn => self.ctx.input.asn.unwrap_or_default().into(), + ExpressionVariable::Country => self.ctx.input.country.unwrap_or_default().into(), + ExpressionVariable::IsTls => self.ctx.input.is_tls.into(), + ExpressionVariable::EnvFrom => self.ctx.output.env_from_addr.address.as_str().into(), + ExpressionVariable::EnvFromLocal => { + self.ctx.output.env_from_addr.local_part.as_str().into() + } + ExpressionVariable::EnvFromDomain => self .ctx .output .env_from_addr @@ -57,7 +60,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .fqdn .as_str() .into(), - V_SPAM_ENV_TO => self + ExpressionVariable::EnvTo => self .ctx .output .env_to_addr @@ -65,8 +68,8 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|e| Variable::from(e.address.as_str())) .collect::>() .into(), - V_SPAM_FROM => self.ctx.output.from.email.address.as_str().into(), - V_SPAM_FROM_NAME => self + ExpressionVariable::From => self.ctx.output.from.email.address.as_str().into(), + ExpressionVariable::FromName => self .ctx .output .from @@ -74,9 +77,11 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .as_deref() .unwrap_or_default() .into(), - V_SPAM_FROM_LOCAL => self.ctx.output.from.email.local_part.as_str().into(), - V_SPAM_FROM_DOMAIN => self.ctx.output.from.email.domain_part.fqdn.as_str().into(), - V_SPAM_REPLY_TO => self + ExpressionVariable::FromLocal => self.ctx.output.from.email.local_part.as_str().into(), + ExpressionVariable::FromDomain => { + self.ctx.output.from.email.domain_part.fqdn.as_str().into() + } + ExpressionVariable::ReplyTo => self .ctx .output .reply_to @@ -84,7 +89,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| r.email.address.as_str()) .unwrap_or_default() .into(), - V_SPAM_REPLY_TO_NAME => self + ExpressionVariable::ReplyToName => self .ctx .output .reply_to @@ -92,7 +97,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .and_then(|r| r.name.as_deref()) .unwrap_or_default() .into(), - V_SPAM_REPLY_TO_LOCAL => self + ExpressionVariable::ReplyToLocal => self .ctx .output .reply_to @@ -100,7 +105,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| r.email.local_part.as_str()) .unwrap_or_default() .into(), - V_SPAM_REPLY_TO_DOMAIN => self + ExpressionVariable::ReplyToDomain => self .ctx .output .reply_to @@ -108,7 +113,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| r.email.domain_part.fqdn.as_str()) .unwrap_or_default() .into(), - V_SPAM_TO => self + ExpressionVariable::To => self .ctx .output .recipients_to @@ -116,7 +121,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.address.as_str())) .collect::>() .into(), - V_SPAM_TO_NAME => self + ExpressionVariable::ToName => self .ctx .output .recipients_to @@ -124,7 +129,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .filter_map(|r| Variable::from(r.name.as_deref()?).into()) .collect::>() .into(), - V_SPAM_TO_LOCAL => self + ExpressionVariable::ToLocal => self .ctx .output .recipients_to @@ -132,7 +137,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.local_part.as_str())) .collect::>() .into(), - V_SPAM_TO_DOMAIN => self + ExpressionVariable::ToDomain => self .ctx .output .recipients_to @@ -140,7 +145,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.domain_part.fqdn.as_str())) .collect::>() .into(), - V_SPAM_CC => self + ExpressionVariable::Cc => self .ctx .output .recipients_cc @@ -148,7 +153,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.address.as_str())) .collect::>() .into(), - V_SPAM_CC_NAME => self + ExpressionVariable::CcName => self .ctx .output .recipients_cc @@ -156,7 +161,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .filter_map(|r| Variable::from(r.name.as_deref()?).into()) .collect::>() .into(), - V_SPAM_CC_LOCAL => self + ExpressionVariable::CcLocal => self .ctx .output .recipients_cc @@ -164,7 +169,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.local_part.as_str())) .collect::>() .into(), - V_SPAM_CC_DOMAIN => self + ExpressionVariable::CcDomain => self .ctx .output .recipients_cc @@ -172,7 +177,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.domain_part.fqdn.as_str())) .collect::>() .into(), - V_SPAM_BCC => self + ExpressionVariable::Bcc => self .ctx .output .recipients_bcc @@ -180,7 +185,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.address.as_str())) .collect::>() .into(), - V_SPAM_BCC_NAME => self + ExpressionVariable::BccName => self .ctx .output .recipients_bcc @@ -188,7 +193,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .filter_map(|r| Variable::from(r.name.as_deref()?).into()) .collect::>() .into(), - V_SPAM_BCC_LOCAL => self + ExpressionVariable::BccLocal => self .ctx .output .recipients_bcc @@ -196,7 +201,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.local_part.as_str())) .collect::>() .into(), - V_SPAM_BCC_DOMAIN => self + ExpressionVariable::BccDomain => self .ctx .output .recipients_bcc @@ -204,8 +209,10 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .map(|r| Variable::from(r.email.domain_part.fqdn.as_str())) .collect::>() .into(), - V_SPAM_BODY_TEXT => self.ctx.text_body().unwrap_or_default().into(), - V_SPAM_BODY_HTML => self + ExpressionVariable::Body | ExpressionVariable::BodyText => { + self.ctx.text_body().unwrap_or_default().into() + } + ExpressionVariable::BodyHtml => self .ctx .input .message @@ -221,13 +228,13 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { }) .unwrap_or_default() .into(), - V_SPAM_BODY_RAW => Variable::from(CompactString::from_utf8_lossy( + ExpressionVariable::BodyRaw => Variable::from(CompactString::from_utf8_lossy( self.ctx.input.message.raw_message(), )), - V_SPAM_SUBJECT => self.ctx.output.subject_lc.as_str().into(), - V_SPAM_SUBJECT_THREAD => self.ctx.output.subject_thread_lc.as_str().into(), - V_SPAM_LOCATION => self.location.as_str().into(), - V_WORDS_SUBJECT => self + ExpressionVariable::Subject => self.ctx.output.subject_lc.as_str().into(), + ExpressionVariable::SubjectThread => self.ctx.output.subject_thread_lc.as_str().into(), + ExpressionVariable::Location => self.location.as_str().into(), + ExpressionVariable::SubjectWords => self .ctx .output .subject_tokens @@ -241,7 +248,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { }) .collect::>() .into(), - V_WORDS_BODY => self + ExpressionVariable::BodyWords => self .ctx .input .message @@ -263,7 +270,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { }) .unwrap_or_default() .into(), - _ => Variable::Integer(0), + variable => self.item.resolve_variable(variable), } } @@ -278,13 +285,17 @@ pub(crate) struct EmailHeader<'x> { } impl ResolveVariable for EmailHeader<'_> { - fn resolve_variable(&self, variable: u32) -> Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> { match variable { - V_HEADER_NAME => self.header.name().into(), - V_HEADER_NAME_LOWER => CompactString::from_str_to_lowercase(self.header.name()).into(), - V_HEADER_VALUE | V_HEADER_VALUE_LOWER | V_HEADER_PROPERTY => match &self.header.value { + ExpressionVariable::Name => self.header.name().into(), + ExpressionVariable::NameLower => { + CompactString::from_str_to_lowercase(self.header.name()).into() + } + ExpressionVariable::Value + | ExpressionVariable::ValueLower + | ExpressionVariable::Attributes => match &self.header.value { HeaderValue::Text(text) => { - if variable == V_HEADER_VALUE_LOWER { + if variable == ExpressionVariable::ValueLower { CompactString::from_str_to_lowercase(text).into() } else { text.as_ref().into() @@ -293,7 +304,7 @@ impl ResolveVariable for EmailHeader<'_> { HeaderValue::TextList(list) => Variable::Array( list.iter() .map(|text| { - Variable::String(if variable == V_HEADER_VALUE_LOWER { + Variable::String(if variable == ExpressionVariable::ValueLower { StringCow::Owned(CompactString::from_str_to_lowercase(text)) } else { StringCow::Borrowed(text.as_ref()) @@ -306,7 +317,7 @@ impl ResolveVariable for EmailHeader<'_> { .iter() .filter_map(|a| { a.address.as_ref().map(|text| { - Variable::String(if variable == V_HEADER_VALUE_LOWER { + Variable::String(if variable == ExpressionVariable::ValueLower { StringCow::Owned(CompactString::from_str_to_lowercase(text)) } else { StringCow::Borrowed(text.as_ref()) @@ -319,7 +330,7 @@ impl ResolveVariable for EmailHeader<'_> { .iter() .filter_map(|a| { a.name.as_ref().map(|text| { - Variable::String(if variable == V_HEADER_VALUE_LOWER { + Variable::String(if variable == ExpressionVariable::ValueLower { StringCow::Owned(CompactString::from_str_to_lowercase(text)) } else { StringCow::Borrowed(text.as_ref()) @@ -332,7 +343,7 @@ impl ResolveVariable for EmailHeader<'_> { CompactString::new(date_time.to_rfc3339()).into() } HeaderValue::ContentType(ct) => { - if variable != V_HEADER_PROPERTY { + if variable != ExpressionVariable::Attributes { if let Some(st) = ct.subtype() { format_compact!("{}/{}", ct.ctype(), st).into() } else { @@ -355,7 +366,7 @@ impl ResolveVariable for EmailHeader<'_> { } } HeaderValue::Received(_) => { - if variable == V_HEADER_VALUE_LOWER { + if variable == ExpressionVariable::ValueLower { CompactString::from_str_to_lowercase(self.raw.trim()).into() } else { self.raw.trim().into() @@ -363,8 +374,8 @@ impl ResolveVariable for EmailHeader<'_> { } HeaderValue::Empty => "".into(), }, - V_HEADER_RAW => self.raw.into(), - V_HEADER_RAW_LOWER => CompactString::from_str_to_lowercase(self.raw).into(), + ExpressionVariable::Raw => self.raw.into(), + ExpressionVariable::RawLower => CompactString::from_str_to_lowercase(self.raw).into(), _ => Variable::Integer(0), } } @@ -375,13 +386,15 @@ impl ResolveVariable for EmailHeader<'_> { } impl ResolveVariable for Recipient { - fn resolve_variable(&self, variable: u32) -> Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> { match variable { - V_RCPT_EMAIL => Variable::from(self.email.address.as_str()), - V_RCPT_NAME => Variable::from(self.name.as_deref().unwrap_or_default()), - V_RCPT_LOCAL => Variable::from(self.email.local_part.as_str()), - V_RCPT_DOMAIN => Variable::from(self.email.domain_part.fqdn.as_str()), - V_RCPT_DOMAIN_SLD => Variable::from(self.email.domain_part.sld_or_default()), + ExpressionVariable::Email | ExpressionVariable::Value => { + Variable::from(self.email.address.as_str()) + } + ExpressionVariable::Name => Variable::from(self.name.as_deref().unwrap_or_default()), + ExpressionVariable::Local => Variable::from(self.email.local_part.as_str()), + ExpressionVariable::Domain => Variable::from(self.email.domain_part.fqdn.as_str()), + ExpressionVariable::Sld => Variable::from(self.email.domain_part.sld_or_default()), _ => Variable::Integer(0), } } @@ -392,52 +405,54 @@ impl ResolveVariable for Recipient { } impl ResolveVariable for UrlParts<'_> { - fn resolve_variable(&self, variable: u32) -> Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> Variable<'_> { match variable { - V_URL_FULL => Variable::from(self.url.as_str()), - V_URL_PATH_QUERY => Variable::from( + ExpressionVariable::Url | ExpressionVariable::Value => { + Variable::from(self.url.as_str()) + } + ExpressionVariable::PathQuery => Variable::from( self.url_parsed .as_ref() .and_then(|p| p.parts.path_and_query().map(|p| p.as_str())) .unwrap_or_default(), ), - V_URL_PATH => Variable::from( + ExpressionVariable::UrlPath => Variable::from( self.url_parsed .as_ref() .map(|p| p.parts.path()) .unwrap_or_default(), ), - V_URL_QUERY => Variable::from( + ExpressionVariable::Query => Variable::from( self.url_parsed .as_ref() .and_then(|p| p.parts.query()) .unwrap_or_default(), ), - V_URL_SCHEME => Variable::from( + ExpressionVariable::Scheme => Variable::from( self.url_parsed .as_ref() .and_then(|p| p.parts.scheme_str()) .unwrap_or_default(), ), - V_URL_AUTHORITY => Variable::from( + ExpressionVariable::Authority => Variable::from( self.url_parsed .as_ref() .and_then(|p| p.parts.authority().map(|a| a.as_str())) .unwrap_or_default(), ), - V_URL_HOST => Variable::from( + ExpressionVariable::Host => Variable::from( self.url_parsed .as_ref() .map(|p| p.host.fqdn.as_str()) .unwrap_or_default(), ), - V_URL_HOST_SLD => Variable::from( + ExpressionVariable::Sld => Variable::from( self.url_parsed .as_ref() .map(|p| p.host.sld_or_default()) .unwrap_or_default(), ), - V_URL_PORT => Variable::Integer( + ExpressionVariable::Port => Variable::Integer( self.url_parsed .as_ref() .and_then(|p| p.parts.port_u16()) @@ -455,7 +470,7 @@ impl ResolveVariable for UrlParts<'_> { pub struct StringResolver<'x>(pub &'x str); impl ResolveVariable for StringResolver<'_> { - fn resolve_variable(&self, _: u32) -> Variable<'_> { + fn resolve_variable(&self, _: ExpressionVariable) -> Variable<'_> { Variable::from(self.0) } @@ -467,7 +482,7 @@ impl ResolveVariable for StringResolver<'_> { pub struct StringListResolver<'x>(pub &'x [String]); impl ResolveVariable for StringListResolver<'_> { - fn resolve_variable(&self, _: u32) -> Variable<'_> { + fn resolve_variable(&self, _: ExpressionVariable) -> Variable<'_> { Variable::Array(self.0.iter().map(|v| Variable::from(v.as_str())).collect()) } diff --git a/tests/src/smtp/config.rs b/tests/src/smtp/config.rs index 2198fd97..992a19ee 100644 --- a/tests/src/smtp/config.rs +++ b/tests/src/smtp/config.rs @@ -50,15 +50,15 @@ fn parse_if_blocks() { // Create context and add some conditions let token_map = TokenMap::default().with_variables(&[ - V_RECIPIENT, - V_RECIPIENT_DOMAIN, - V_SENDER, - V_SENDER_DOMAIN, - V_AUTHENTICATED_AS, - V_LISTENER, - V_REMOTE_IP, - V_LOCAL_IP, - V_PRIORITY, + ExpressionVariable::Rcpt, + ExpressionVariable::RcptDomain, + ExpressionVariable::Sender, + ExpressionVariable::SenderDomain, + ExpressionVariable::AuthenticatedAs, + ExpressionVariable::Listener, + ExpressionVariable::RemoteIp, + ExpressionVariable::LocalIp, + ExpressionVariable::Priority, ]); assert_eq!( @@ -69,7 +69,7 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(V_SENDER), + ExpressionItem::Variable(ExpressionVariable::Sender), ExpressionItem::Constant(Constant::String("jdoe".into())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] @@ -81,12 +81,12 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(V_PRIORITY), + ExpressionItem::Variable(ExpressionVariable::Priority), ExpressionItem::Constant(Constant::Integer(1)), ExpressionItem::UnaryOperator(UnaryOperator::Minus), ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(V_RECIPIENT), + ExpressionItem::Variable(ExpressionVariable::Rcpt), ExpressionItem::Constant(Constant::String("jane".into())), ExpressionItem::Function { id: 29, @@ -114,7 +114,7 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(V_SENDER), + ExpressionItem::Variable(ExpressionVariable::Sender), ExpressionItem::Constant(Constant::String("jdoe".into())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] @@ -131,12 +131,12 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(V_PRIORITY), + ExpressionItem::Variable(ExpressionVariable::Priority), ExpressionItem::Constant(Constant::Integer(1)), ExpressionItem::UnaryOperator(UnaryOperator::Minus), ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(V_RECIPIENT), + ExpressionItem::Variable(ExpressionVariable::Rcpt), ExpressionItem::Constant(Constant::String("jane".into())), ExpressionItem::Function { id: 29, @@ -166,7 +166,7 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(V_SENDER), + ExpressionItem::Variable(ExpressionVariable::Sender), ExpressionItem::Constant(Constant::String("jdoe".into())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] @@ -183,12 +183,12 @@ fn parse_if_blocks() { IfThen { expr: Expression { items: vec![ - ExpressionItem::Variable(V_PRIORITY), + ExpressionItem::Variable(ExpressionVariable::Priority), ExpressionItem::Constant(Constant::Integer(1)), ExpressionItem::UnaryOperator(UnaryOperator::Minus), ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, - ExpressionItem::Variable(V_RECIPIENT), + ExpressionItem::Variable(ExpressionVariable::Rcpt), ExpressionItem::Constant(Constant::String("jane".into())), ExpressionItem::Function { id: 29, @@ -248,15 +248,15 @@ fn parse_throttles() { &mut config, "throttle", &TokenMap::default().with_variables(&[ - V_RECIPIENT, - V_RECIPIENT_DOMAIN, - V_SENDER, - V_SENDER_DOMAIN, - V_AUTHENTICATED_AS, - V_LISTENER, - V_REMOTE_IP, - V_LOCAL_IP, - V_PRIORITY, + ExpressionVariable::Rcpt, + ExpressionVariable::RcptDomain, + ExpressionVariable::Sender, + ExpressionVariable::SenderDomain, + ExpressionVariable::AuthenticatedAs, + ExpressionVariable::Listener, + ExpressionVariable::RemoteIp, + ExpressionVariable::LocalIp, + ExpressionVariable::Priority, ]), u16::MAX, ); @@ -408,16 +408,16 @@ async fn eval_if() { let mut config = Config::new(fs::read_to_string(file).unwrap()).unwrap(); let envelope = TestEnvelope::from_config(&mut config); let token_map = TokenMap::default().with_variables(&[ - 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, + ExpressionVariable::Rcpt, + ExpressionVariable::RcptDomain, + ExpressionVariable::Sender, + ExpressionVariable::SenderDomain, + ExpressionVariable::AuthenticatedAs, + ExpressionVariable::Listener, + ExpressionVariable::RemoteIp, + ExpressionVariable::LocalIp, + ExpressionVariable::Priority, + ExpressionVariable::Mx, ]); let core = Server::default(); @@ -461,16 +461,16 @@ async fn eval_dynvalue() { let mut config = Config::new(fs::read_to_string(file).unwrap()).unwrap(); let envelope = TestEnvelope::from_config(&mut config); let token_map = TokenMap::default().with_variables(&[ - 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, + ExpressionVariable::Rcpt, + ExpressionVariable::RcptDomain, + ExpressionVariable::Sender, + ExpressionVariable::SenderDomain, + ExpressionVariable::AuthenticatedAs, + ExpressionVariable::Listener, + ExpressionVariable::RemoteIp, + ExpressionVariable::LocalIp, + ExpressionVariable::Priority, + ExpressionVariable::Mx, ]); let core = Server::default(); @@ -495,19 +495,19 @@ async fn eval_dynvalue() { } impl ResolveVariable for TestEnvelope { - fn resolve_variable(&self, variable: u32) -> Variable<'_> { + fn resolve_variable(&self, variable: ExpressionVariable) -> 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_compact_string().into(), - V_REMOTE_IP => self.remote_ip.to_compact_string().into(), - V_LOCAL_IP => self.local_ip.to_compact_string().into(), - V_PRIORITY => self.priority.to_compact_string().into(), - V_MX => self.mx.as_str().into(), - V_HELO_DOMAIN => self.helo_domain.as_str().into(), + ExpressionVariable::Rcpt => self.rcpt.as_str().into(), + ExpressionVariable::RcptDomain => self.rcpt_domain.as_str().into(), + ExpressionVariable::Sender => self.sender.as_str().into(), + ExpressionVariable::SenderDomain => self.sender_domain.as_str().into(), + ExpressionVariable::AuthenticatedAs => self.authenticated_as.as_str().into(), + ExpressionVariable::Listener => self.listener_id.to_compact_string().into(), + ExpressionVariable::RemoteIp => self.remote_ip.to_compact_string().into(), + ExpressionVariable::LocalIp => self.local_ip.to_compact_string().into(), + ExpressionVariable::Priority => self.priority.to_compact_string().into(), + ExpressionVariable::Mx => self.mx.as_str().into(), + ExpressionVariable::HeloDomain => self.helo_domain.as_str().into(), _ => Default::default(), } } diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index 49d1822a..09c71242 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -202,17 +202,17 @@ async fn lookup_sql() { // Test expression functions let token_map = TokenMap::default().with_variables(&[ - 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, + ExpressionVariable::Rcpt, + ExpressionVariable::RcptDomain, + ExpressionVariable::Sender, + ExpressionVariable::SenderDomain, + ExpressionVariable::Mx, + ExpressionVariable::HeloDomain, + ExpressionVariable::AuthenticatedAs, + ExpressionVariable::Listener, + ExpressionVariable::RemoteIp, + ExpressionVariable::LocalIp, + ExpressionVariable::Priority, ]); for test_name in ["sql", "dns", "key_get", "counter_get"] { let e =