diff --git a/Cargo.lock b/Cargo.lock index 2a9fa8cf..40d322ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1071,6 +1071,7 @@ dependencies = [ "dns-update", "futures", "hashify", + "hickory-proto 0.24.4", "hostname", "hyper 1.8.1", "idna", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 5e3bbe32..0eb7f99b 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -76,6 +76,7 @@ indexmap = "2.7.1" tinyvec = "1.9.0" compact_str = { version = "0.9.0", features = ["rkyv", "serde"] } lz4_flex = { version = "0.12", features = ["frame"], default-features = false } +hickory-proto = "0.24" [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/common/src/auth/oauth/config.rs b/crates/common/src/auth/oauth/config.rs index aa10570a..50766225 100644 --- a/crates/common/src/auth/oauth/config.rs +++ b/crates/common/src/auth/oauth/config.rs @@ -104,7 +104,7 @@ impl OAuthConfig { | SignatureAlgorithm::RS512 | SignatureAlgorithm::PS256 | SignatureAlgorithm::PS384 - | SignatureAlgorithm::PS512 => parse_rsa_key(config).unwrap_or_else(|| { + | SignatureAlgorithm::PS512 => parse_rsa_key(bp).unwrap_or_else(|| { ( Secret::Bytes(rand_key.clone()), AlgorithmParameters::OctetKey(OctetKeyParameters { diff --git a/crates/common/src/config/groupware.rs b/crates/common/src/config/groupware.rs index bb1f74b1..8bb08440 100644 --- a/crates/common/src/config/groupware.rs +++ b/crates/common/src/config/groupware.rs @@ -4,9 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{str::FromStr, time::Duration}; - -use utils::{config::Config, template::Template}; +use crate::manager::bootstrap::Bootstrap; +use registry::schema::structs::{ + AddressBook, Calendar, CalendarAlarm, CalendarScheduling, DataRetention, FileStorage, Sharing, + WebDav, +}; +use std::str::FromStr; +use utils::template::Template; #[derive(Debug, Clone, Default)] pub struct GroupwareConfig { @@ -77,119 +81,67 @@ pub enum CalendarTemplateVariable { } impl GroupwareConfig { - pub fn parse(bp: &mut Bootstrap) -> Self { + pub async fn parse(bp: &mut Bootstrap) -> Self { + let calendar = bp.setting_infallible::().await; + let alarm = bp.setting_infallible::().await; + let sched = bp.setting_infallible::().await; + let book = bp.setting_infallible::().await; + let dav = bp.setting_infallible::().await; + let file = bp.setting_infallible::().await; + let share = bp.setting_infallible::().await; + let dr = bp.setting_infallible::().await; + GroupwareConfig { - max_request_size: config - .property("dav.request.max-size") - .unwrap_or(25 * 1024 * 1024), - dead_property_size: config - .property_or_default::>("dav.property.max-size.dead", "1024") - .unwrap_or(Some(1024)), - live_property_size: config.property("dav.property.max-size.live").unwrap_or(250), - assisted_discovery: config - .property("dav.collection.assisted-discovery") - .unwrap_or(true), - max_lock_timeout: config - .property::("dav.lock.max-timeout") - .map(|d| d.as_secs()) - .unwrap_or(3600), - max_locks_per_user: config.property("dav.locks.max-per-user").unwrap_or(10), - max_results: config.property("dav.response.max-results").unwrap_or(2000), - default_calendar_name: config - .property_or_default::>("calendar.default.href-name", "default") - .unwrap_or_default(), - default_calendar_display_name: config - .property_or_default::>( - "calendar.default.display-name", - "Stalwart Calendar", - ) - .unwrap_or_default(), - default_addressbook_name: config - .property_or_default::>("contacts.default.href-name", "default") - .unwrap_or_default(), - default_addressbook_display_name: config - .property_or_default::>( - "contacts.default.display-name", - "Stalwart Address Book", - ) - .unwrap_or_default(), - max_ical_size: config.property("calendar.max-size").unwrap_or(512 * 1024), - max_ical_instances: config - .property("calendar.max-recurrence-expansions") - .unwrap_or(3000), - max_ical_attendees_per_instance: config - .property("calendar.max-attendees-per-instance") - .unwrap_or(20), - max_vcard_size: config.property("contacts.max-size").unwrap_or(512 * 1024), - max_file_size: config - .property("file-storage.max-size") - .unwrap_or(25 * 1024 * 1024), - alarms_enabled: config.property("calendar.alarms.enabled").unwrap_or(true), - alarms_minimum_interval: config - .property_or_default::("calendar.alarms.minimum-interval", "1h") - .unwrap_or(Duration::from_secs(60 * 60)) - .as_secs() as i64, - alarms_allow_external_recipients: config - .property("calendar.alarms.allow-external-recipients") - .unwrap_or(false), - alarms_from_name: config - .value("calendar.alarms.from.name") - .unwrap_or("Stalwart Calendar") - .to_string(), - alarms_from_email: config - .value("calendar.alarms.from.email") - .map(|s| s.to_string()), + max_request_size: dav.request_max_size as usize, + dead_property_size: dav.dead_property_max_size.map(|v| v as usize), + live_property_size: dav.live_property_max_size as usize, + assisted_discovery: dav.enable_assisted_discovery, + max_lock_timeout: dav.max_lock_timeout.into_inner().as_secs(), + max_locks_per_user: dav.max_locks as usize, + max_results: dav.max_results as usize, + default_calendar_name: calendar.default_href_name, + default_calendar_display_name: calendar.default_display_name, + default_addressbook_name: book.default_href_name, + default_addressbook_display_name: book.default_display_name, + max_ical_size: calendar.max_i_calendar_size as usize, + max_ical_instances: calendar.max_recurrence_expansions as usize, + max_ical_attendees_per_instance: calendar.max_attendees as usize, + max_vcard_size: book.max_v_card_size as usize, + max_file_size: file.max_size as usize, + alarms_enabled: alarm.enable, + alarms_minimum_interval: alarm.min_trigger_interval.into_inner().as_secs() as i64, + alarms_allow_external_recipients: alarm.allow_external_rcpts, + alarms_from_name: alarm.from_name, + alarms_from_email: alarm.from_email, alarms_template: Template::parse(include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/../../resources/html-templates/calendar-alarm.html.min" ))) .expect("Failed to parse calendar template"), - itip_enabled: config - .property("calendar.scheduling.enable") - .unwrap_or(true), - itip_auto_add: config - .property("calendar.scheduling.inbound.auto-add") - .unwrap_or(false), - itip_inbound_max_ical_size: config - .property("calendar.scheduling.inbound.max-size") - .unwrap_or(512 * 1024), - itip_outbound_max_recipients: config - .property("calendar.scheduling.outbound.max-recipients") - .unwrap_or(100), - itip_inbox_auto_expunge: config - .property_or_default::>( - "calendar.scheduling.inbox.auto-expunge", - "30d", - ) - .map(|d| d.map(|d| d.as_secs())) - .unwrap_or(Some(30 * 24 * 60 * 60)), - itip_http_rsvp_url: if config - .property("calendar.scheduling.http-rsvp.enable") - .unwrap_or(true) - { - if let Some(url) = config - .value("calendar.scheduling.http-rsvp.url") + itip_enabled: sched.enable, + itip_auto_add: sched.auto_add_invitations, + itip_inbound_max_ical_size: sched.itip_max_size as usize, + itip_outbound_max_recipients: sched.max_recipients as usize, + itip_inbox_auto_expunge: dr + .expunge_scheduling_inbox_after + .map(|d| d.into_inner().as_secs()), + itip_http_rsvp_url: if sched.http_rsvp_enable { + if let Some(url) = sched + .http_rsvp_template + .as_deref() .map(|v| v.trim().trim_end_matches('/')) .filter(|v| !v.is_empty()) { Some(url.to_string()) } else { - Some(format!( - "https://{}/calendar/rsvp", - config.value("server.hostname").unwrap_or("localhost") - )) + Some(format!("https://{}/calendar/rsvp", bp.hostname())) } } else { None }, - max_shares_per_item: config.property("sharing.max-shares-per-item").unwrap_or(10), - allow_directory_query: config - .property("sharing.allow-directory-query") - .unwrap_or(false), - itip_http_rsvp_expiration: config - .property_or_default::("calendar.scheduling.http-rsvp.expiration", "90d") - .map(|d| d.as_secs()) - .unwrap_or(90 * 24 * 60 * 60), + max_shares_per_item: share.max_shares as usize, + allow_directory_query: share.allow_directory_queries, + itip_http_rsvp_expiration: sched.http_rsvp_link_expiry.into_inner().as_secs(), itip_template: Template::parse(include_str!(concat!( env!("CARGO_MANIFEST_DIR"), "/../../resources/html-templates/calendar-invite.html.min" diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index def9ad45..69765697 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -4,20 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::server::tls::{build_self_signed_cert, parse_certificates}; +use super::server::tls::build_self_signed_cert; use crate::{ CacheSwap, Caches, Data, DavResource, DavResources, MailboxCache, MessageStoreCache, MessageUidCache, TlsConnectors, auth::{AccessToken, roles::RolePermissions}, - config::smtp::resolver::{Policy, Tlsa}, + config::{ + mailstore::spamfilter::SpamClassifier, + smtp::resolver::{Policy, Tlsa}, + }, listener::blocked::BlockedIps, - manager::webadmin::WebAdminManager, + manager::{bootstrap::Bootstrap, webadmin::WebAdminManager}, }; use ahash::{AHashMap, AHashSet}; use arc_swap::ArcSwap; use mail_auth::{MX, Parameters, Txt}; use mail_send::smtp::tls::build_tls_connector; use parking_lot::RwLock; +use registry::schema::{prelude::Object, structs}; use std::{ net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::Arc, @@ -32,20 +36,20 @@ impl Data { // Parse certificates let mut certificates = AHashMap::new(); let mut subject_names = AHashSet::new(); - parse_certificates(config, &mut certificates, &mut subject_names); + bp.parse_certificates(&mut certificates, &mut subject_names); if subject_names.is_empty() { subject_names.insert("localhost".to_string()); } // Build and test snowflake id generator - let node_id = config - .property::("cluster.node-id") - .unwrap_or_else(store::rand::random); + let node_id = bp.node_id(); let id_generator = SnowflakeIdGenerator::with_node_id(node_id); if !id_generator.is_valid() { panic!("Invalid system time, panicking to avoid data corruption"); } + let todo = "TODO: WebAdminManager initialization"; + Data { spam_classifier: ArcSwap::from_pointee(SpamClassifier::default()), tls_certificates: ArcSwap::from_pointee(certificates), @@ -53,20 +57,23 @@ impl Data { subject_names.into_iter().collect::>(), ) .or_else(|err| { - config.new_build_error("certificate.self-signed", err); + bp.build_error( + Object::Certificate.singleton(), + format!("Failed to build self-signed TLS certificate: {err}"), + ); build_self_signed_cert(vec!["localhost".to_string()]) }) .ok() .map(Arc::new), - blocked_ips: RwLock::new(BlockedIps::parse(config).blocked_ip_addresses), + blocked_ips: RwLock::new(BlockedIps::parse(bp).blocked_ip_addresses), jmap_id_gen: id_generator.clone(), queue_id_gen: id_generator.clone(), span_id_gen: id_generator, queue_status: true.into(), - webadmin: config - .value("webadmin.path") - .map(|path| WebAdminManager::new(path.into())) - .unwrap_or_default(), + webadmin: Default::default(), /*config + .value("webadmin.path") + .map(|path| WebAdminManager::new(path.into())) + .unwrap_or_default(),*/ logos: Default::default(), smtp_connectors: TlsConnectors::default(), asn_geo_data: Default::default(), @@ -75,114 +82,64 @@ impl Data { } impl Caches { - pub fn parse(bp: &mut Bootstrap) -> Self { - const MB_50: u64 = 50 * 1024 * 1024; - const MB_10: u64 = 10 * 1024 * 1024; - const MB_5: u64 = 5 * 1024 * 1024; - const MB_1: u64 = 1024 * 1024; + pub async fn parse(bp: &mut Bootstrap) -> Self { + let cache = bp.setting_infallible::().await; Caches { - access_tokens: Cache::from_config( - config, - "access-token", - MB_10, + access_tokens: Cache::new( + cache.access_tokens, (std::mem::size_of::() + 255) as u64, ), - http_auth: Cache::from_config( - config, - "http-auth", - MB_1, - (50 + std::mem::size_of::()) as u64, - ), - permissions: Cache::from_config( - config, - "permission", - MB_5, + http_auth: Cache::new(cache.http_auth, (50 + std::mem::size_of::()) as u64), + permissions: Cache::new( + cache.permissions, std::mem::size_of::() as u64, ), - messages: Cache::from_config( - config, - "message", - MB_50, + messages: Cache::new( + cache.messages, (std::mem::size_of::() + std::mem::size_of::>() + (1024 * std::mem::size_of::()) + (15 * (std::mem::size_of::() + 60))) as u64, ), - files: Cache::from_config( - config, - "files", - MB_10, + files: Cache::new( + cache.files, (std::mem::size_of::() + (500 * std::mem::size_of::())) as u64, ), - events: Cache::from_config( - config, - "events", - MB_10, + events: Cache::new( + cache.events, (std::mem::size_of::() + (500 * std::mem::size_of::())) as u64, ), - contacts: Cache::from_config( - config, - "contacts", - MB_10, + contacts: Cache::new( + cache.contacts, (std::mem::size_of::() + (500 * std::mem::size_of::())) as u64, ), - scheduling: Cache::from_config( - config, - "scheduling", - MB_1, + scheduling: Cache::new( + cache.scheduling, (std::mem::size_of::() + (500 * std::mem::size_of::())) as u64, ), - dns_txt: CacheWithTtl::from_config( - config, - "dns.txt", - MB_5, - (std::mem::size_of::() + 255) as u64, - ), - dns_mx: CacheWithTtl::from_config( - config, - "dns.mx", - MB_5, - ((std::mem::size_of::() + 255) * 2) as u64, - ), - dns_ptr: CacheWithTtl::from_config( - config, - "dns.ptr", - MB_1, - (std::mem::size_of::() + 255) as u64, - ), - dns_ipv4: CacheWithTtl::from_config( - config, - "dns.ipv4", - MB_5, + dns_txt: CacheWithTtl::new(cache.dns_txt, (std::mem::size_of::() + 255) as u64), + dns_mx: CacheWithTtl::new(cache.dns_mx, ((std::mem::size_of::() + 255) * 2) as u64), + dns_ptr: CacheWithTtl::new(cache.dns_ptr, (std::mem::size_of::() + 255) as u64), + dns_ipv4: CacheWithTtl::new( + cache.dns_ipv4, ((std::mem::size_of::() + 255) * 2) as u64, ), - dns_ipv6: CacheWithTtl::from_config( - config, - "dns.ipv6", - MB_5, + dns_ipv6: CacheWithTtl::new( + cache.dns_ipv6, ((std::mem::size_of::() + 255) * 2) as u64, ), - dns_tlsa: CacheWithTtl::from_config( - config, - "dns.tlsa", - MB_1, - (std::mem::size_of::() + 255) as u64, - ), - dbs_mta_sts: CacheWithTtl::from_config( - config, - "dns.mta-sts", - MB_1, + dns_tlsa: CacheWithTtl::new(cache.dns_tlsa, (std::mem::size_of::() + 255) as u64), + dns_mta_sts: CacheWithTtl::new( + cache.dns_mta_sts, (std::mem::size_of::() + 255) as u64, ), - dns_rbl: CacheWithTtl::from_config( - config, - "dns.rbl", - MB_5, + dns_rbl: CacheWithTtl::new( + cache.dns_rbl, ((std::mem::size_of::() + 255) * 2) as u64, ), } diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 371bb75f..d34106b8 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -5,10 +5,7 @@ */ use self::{mailstore::jmap::JmapConfig, smtp::SmtpConfig, storage::Storage}; -use crate::{ - Core, Network, Security, auth::oauth::config::OAuthConfig, expr::*, - listener::tls::AcmeProviders, -}; +use crate::{Core, Network, Security, auth::oauth::config::OAuthConfig, expr::*}; use arc_swap::ArcSwap; use coordinator::Coordinator; use directory::{Directories, Directory}; @@ -18,7 +15,6 @@ use ring::signature::{EcdsaKeyPair, RsaKeyPair}; use std::sync::Arc; use store::{BlobBackend, BlobStore, InMemoryStore, SearchStore, Store, Stores}; use telemetry::Metrics; -use utils::config::utils::AsKey; pub mod groupware; pub mod inner; @@ -166,7 +162,7 @@ impl Core { ) } - let groupware = GroupwareConfig::parse(config); + let groupware = GroupwareConfig::parse(bp); Self { // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC @@ -175,14 +171,14 @@ impl Core { enterprise, // SPDX-SnippetEnd sieve: Scripting::parse(config, &stores).await, - network: Network::parse(config), - smtp: SmtpConfig::parse(config).await, + network: Network::parse(bp), + smtp: SmtpConfig::parse(bp).await, jmap: JmapConfig::parse(config, &groupware), - imap: ImapConfig::parse(config), - oauth: OAuthConfig::parse(config), - acme: AcmeProviders::parse(config), - metrics: Metrics::parse(config), - spam: SpamFilterConfig::parse(config).await, + imap: ImapConfig::parse(bp), + oauth: OAuthConfig::parse(bp), + acme: AcmeProviders::parse(bp), + metrics: Metrics::parse(bp), + spam: SpamFilterConfig::parse(bp).await, groupware, storage: Storage { data, diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index b16d66dc..b66eee2e 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -5,10 +5,13 @@ */ use super::*; -use crate::expr::{if_block::IfBlock, tokenizer::TokenMap}; +use crate::{ + expr::{if_block::IfBlock, tokenizer::TokenMap}, + manager::bootstrap::Bootstrap, +}; use ahash::AHashSet; use std::{hash::Hasher, time::Duration}; -use utils::config::{Config, Rate, http::parse_http_headers, utils::ParseValue}; +use utils::config::utils::ParseValue; use xxhash_rust::xxh3::Xxh3Builder; #[derive(Clone)] @@ -99,40 +102,6 @@ pub struct FieldOrDefault { pub default: String, } -pub(crate) const HTTP_VARS: &[u32; 11] = &[ - 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 { - fn default() -> Self { - Self { - security: Default::default(), - contact_form: None, - node_id: 1, - 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"), - asn_geo_lookup: AsnGeoLookupConfig::Disabled, - server_name: Default::default(), - report_domain: Default::default(), - roles: ClusterRoles::default(), - } - } -} - impl ContactForm { pub fn parse(bp: &mut Bootstrap) -> Option { if !config @@ -220,9 +189,9 @@ impl Network { node_id: config.property("cluster.node-id").unwrap_or(1), report_domain, server_name, - security: Security::parse(config), - contact_form: ContactForm::parse(config), - asn_geo_lookup: AsnGeoLookupConfig::parse(config).unwrap_or_default(), + security: Security::parse(bp), + contact_form: ContactForm::parse(bp), + asn_geo_lookup: AsnGeoLookupConfig::parse(bp).unwrap_or_default(), ..Default::default() }; let token_map = &TokenMap::default().with_variables(HTTP_VARS); diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index 0876ec99..c86e2ca4 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -4,23 +4,22 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ - listener::{ - acme::{ - AcmeProvider, ChallengeSettings, EabSettings, - directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, - }, - tls::AcmeProviders, - }, - manager::bootstrap::Bootstrap, -}; +use crate::{Server, listener::acme::AcmeProvider, manager::bootstrap::Bootstrap}; use ahash::{AHashMap, AHashSet}; -use base64::{ - Engine, - engine::general_purpose::{self, STANDARD}, +use dns_update::{ + Algorithm, DnsUpdater, TsigAlgorithm, + providers::{ovh::OvhEndpoint, rfc2136::DnsAddress}, }; -use dns_update::{DnsUpdater, TsigAlgorithm, providers::rfc2136::DnsAddress}; +use hickory_proto::rr::dnssec::KeyPair; use rcgen::generate_simple_self_signed; +use registry::{ + schema::{ + enums, + structs::{self, Certificate, DnsServer}, + }, + types::id::Id, +}; +use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; use rustls::{ SupportedProtocolVersion, crypto::ring::sign::any_supported_type, @@ -31,10 +30,11 @@ use rustls_pemfile::{Item, certs, read_one}; use rustls_pki_types::PrivateKeyDer; use std::{ io::Cursor, - net::{IpAddr, Ipv4Addr, Ipv6Addr, SocketAddr}, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, sync::Arc, - time::Duration, }; +use store::registry::RegistryObject; +use trc::AddContext; use x509_parser::{ certificate::X509Certificate, der_parser::asn1_rs::FromDer, @@ -44,296 +44,169 @@ use x509_parser::{ pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13]; pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12]; -impl AcmeProviders { - pub fn parse(bp: &mut Bootstrap) -> Self { - let mut providers = AHashMap::new(); +impl Server { + pub async fn build_acme_provider(&self, id: Id) -> trc::Result { + if let Some(server) = self + .registry() + .get::(id) + .await + .caused_by(trc::location!())? + { + Ok(AcmeProvider::new(RegistryObject { id, object: server })) + } else { + trc::bail!( + trc::AcmeEvent::Error + .into_err() + .id(id.to_string()) + .details("ACME provider not found") + ) + } + } - // Parse ACME providers - 'outer: for acme_id in config.sub_keys("acme", ".directory") { - let acme_id = acme_id.as_str(); - let directory = config - .value(("acme", acme_id, "directory")) - .unwrap_or(LETS_ENCRYPT_PRODUCTION_DIRECTORY) - .trim() - .to_string(); - let contact = config - .values(("acme", acme_id, "contact")) - .filter_map(|(_, v)| { - let v = v.trim().to_string(); - if !v.is_empty() { Some(v) } else { None } - }) - .collect::>(); - let renew_before: Duration = config - .property_or_default(("acme", acme_id, "renew-before"), "30d") - .unwrap_or_else(|| Duration::from_secs(30 * 24 * 60 * 60)); + pub async fn build_dns_updater(&self, id: Id) -> trc::Result { + let Some(server) = self + .registry() + .get::(id) + .await + .caused_by(trc::location!())? + else { + trc::bail!( + trc::DnsEvent::BuildError + .into_err() + .id(id.to_string()) + .details("DNS server settings not found") + ); + }; - if directory.is_empty() { - config.new_parse_error(format!("acme.{acme_id}.directory"), "Missing property"); - continue; - } - - if contact.is_empty() { - config.new_parse_error(format!("acme.{acme_id}.contact"), "Missing property"); - continue; - } - - // Parse challenge type - let challenge = match config - .value(("acme", acme_id, "challenge")) - .unwrap_or("tls-alpn-01") - { - "tls-alpn-01" => ChallengeSettings::TlsAlpn01, - "http-01" => ChallengeSettings::Http01, - "dns-01" => match build_dns_updater(config, acme_id) { - Some(updater) => ChallengeSettings::Dns01 { - updater, - origin: config - .value(("acme", acme_id, "origin")) - .map(|s| s.to_string()), - polling_interval: config - .property_or_default(("acme", acme_id, "polling-interval"), "15s") - .unwrap_or_else(|| Duration::from_secs(15)), - propagation_timeout: config - .property_or_default(("acme", acme_id, "propagation-timeout"), "1m") - .unwrap_or_else(|| Duration::from_secs(60)), - ttl: config - .property_or_default(("acme", acme_id, "ttl"), "5m") - .unwrap_or_else(|| Duration::from_secs(5 * 60)) - .as_secs() as u32, - }, - None => { - continue; - } + match server { + DnsServer::Tsig(server) => DnsUpdater::new_rfc2136_tsig( + match server.protocol { + enums::IpProtocol::Udp => DnsAddress::Tcp(SocketAddr::new( + server.host.into_inner(), + server.port as u16, + )), + enums::IpProtocol::Tcp => DnsAddress::Udp(SocketAddr::new( + server.host.into_inner(), + server.port as u16, + )), }, - _ => { - config - .new_parse_error(("acme", acme_id, "challenge"), "Invalid challenge type"); - continue; - } - }; - - // Domains covered by this ACME manager - let domains = config - .values(("acme", acme_id, "domains")) - .map(|(_, s)| s.trim().to_string()) - .collect::>(); - if !matches!(challenge, ChallengeSettings::Dns01 { .. }) - && domains.iter().any(|d| d.starts_with("*.")) - { - config.new_parse_error( - ("acme", acme_id, "domains"), - "Wildcard domains are only supported with DNS-01 challenge", - ); - continue 'outer; + server.key_name, + server.key, + match server.tsig_algorithm { + enums::TsigAlgorithm::HmacMd5 => TsigAlgorithm::HmacMd5, + enums::TsigAlgorithm::Gss => TsigAlgorithm::Gss, + enums::TsigAlgorithm::HmacSha1 => TsigAlgorithm::HmacSha1, + enums::TsigAlgorithm::HmacSha224 => TsigAlgorithm::HmacSha224, + enums::TsigAlgorithm::HmacSha256 => TsigAlgorithm::HmacSha256, + enums::TsigAlgorithm::HmacSha256128 => TsigAlgorithm::HmacSha256_128, + enums::TsigAlgorithm::HmacSha384 => TsigAlgorithm::HmacSha384, + enums::TsigAlgorithm::HmacSha384192 => TsigAlgorithm::HmacSha384_192, + enums::TsigAlgorithm::HmacSha512 => TsigAlgorithm::HmacSha512, + enums::TsigAlgorithm::HmacSha512256 => TsigAlgorithm::HmacSha512_256, + }, + ), + DnsServer::Sig0(server) => DnsUpdater::new_rfc2136_sig0( + match server.protocol { + enums::IpProtocol::Udp => DnsAddress::Tcp(SocketAddr::new( + server.host.into_inner(), + server.port as u16, + )), + enums::IpProtocol::Tcp => DnsAddress::Udp(SocketAddr::new( + server.host.into_inner(), + server.port as u16, + )), + }, + server.signer_name, + match server.sig0_algorithm { + enums::Sig0Algorithm::EcdsaP256Sha256 => KeyPair::ECDSA( + EcdsaKeyPair::from_pkcs8( + &ring::signature::ECDSA_P256_SHA256_ASN1_SIGNING, + server.key.as_bytes(), + &ring::rand::SystemRandom::new(), + ) + .map_err(|err| { + trc::DnsEvent::BuildError + .reason(err) + .details("Failed to build ECDSA P-256 key pair") + .id(id.to_string()) + })?, + ), + enums::Sig0Algorithm::EcdsaP384Sha384 => KeyPair::ECDSA( + EcdsaKeyPair::from_pkcs8( + &ring::signature::ECDSA_P384_SHA384_ASN1_SIGNING, + server.key.as_bytes(), + &ring::rand::SystemRandom::new(), + ) + .map_err(|err| { + trc::DnsEvent::BuildError + .reason(err) + .details("Failed to build ECDSA P-384 key pair") + .id(id.to_string()) + })?, + ), + enums::Sig0Algorithm::Ed25519 => KeyPair::ED25519( + Ed25519KeyPair::from_pkcs8(server.key.as_bytes()).map_err(|err| { + trc::DnsEvent::BuildError + .reason(err) + .details("Failed to build Ed25519 key pair") + .id(id.to_string()) + })?, + ), + }, + server.public_key, + match server.sig0_algorithm { + enums::Sig0Algorithm::EcdsaP256Sha256 => Algorithm::ECDSAP256SHA256, + enums::Sig0Algorithm::EcdsaP384Sha384 => Algorithm::ECDSAP384SHA384, + enums::Sig0Algorithm::Ed25519 => Algorithm::ED25519, + }, + ), + DnsServer::Cloudflare(server) => DnsUpdater::new_cloudflare( + server.secret, + server.email, + server.timeout.into_inner().into(), + ), + DnsServer::DigitalOcean(server) => { + DnsUpdater::new_digitalocean(server.secret, server.timeout.into_inner().into()) } + DnsServer::DeSEC(server) => { + DnsUpdater::new_desec(server.secret, server.timeout.into_inner().into()) + } + DnsServer::Ovh(server) => DnsUpdater::new_ovh( + server.application_key, + server.application_secret, + server.consumer_key, + match server.ovh_endpoint { + enums::OvhEndpoint::OvhEu => OvhEndpoint::OvhEu, + enums::OvhEndpoint::OvhCa => OvhEndpoint::OvhCa, + enums::OvhEndpoint::KimsufiEu => OvhEndpoint::KimsufiEu, + enums::OvhEndpoint::KimsufiCa => OvhEndpoint::KimsufiCa, + enums::OvhEndpoint::SoyoustartEu => OvhEndpoint::SoyoustartEu, + enums::OvhEndpoint::SoyoustartCa => OvhEndpoint::SoyoustartCa, + }, + server.timeout.into_inner().into(), + ), + } + .map_err(|err| { + trc::DnsEvent::BuildError + .reason(err) + .details("Failed to build DNS updater") + .id(id.to_string()) + }) + } +} - // Obtain EAB settings - let eab = if let (Some(eab_kid), Some(eab_hmac_key)) = ( - config - .value(("acme", acme_id, "eab.kid")) - .filter(|s| !s.is_empty()), - config - .value(("acme", acme_id, "eab.hmac-key")) - .filter(|s| !s.is_empty()), +impl Bootstrap { + pub(crate) async fn parse_certificates( + &mut self, + certificates: &mut AHashMap>, + subject_names: &mut AHashSet, + ) { + // Parse certificates + for cert_obj in self.list_infallible::().await { + match build_certified_key( + cert_obj.object.certificate.into_bytes(), + cert_obj.object.private_key.into_bytes(), ) { - if let Ok(hmac_key) = - general_purpose::URL_SAFE_NO_PAD.decode(eab_hmac_key.trim().as_bytes()) - { - EabSettings { - kid: eab_kid.to_string(), - hmac_key, - } - .into() - } else { - config.new_build_error( - format!("acme.{acme_id}.eab.hmac-key"), - "Failed to base64 decode HMAC key", - ); - None - } - } else { - None - }; - - // This ACME manager is the default when SNI is not available - let default = config - .property::(("acme", acme_id, "default")) - .unwrap_or_default(); - - if !domains.is_empty() { - match AcmeProvider::new( - acme_id.to_string(), - directory, - domains, - contact, - challenge, - eab, - renew_before, - default, - ) { - Ok(acme_provider) => { - providers.insert(acme_id.to_string(), acme_provider); - } - Err(err) => { - config.new_build_error(format!("acme.{acme_id}"), err.to_string()); - } - } - } - } - - AcmeProviders { providers } - } -} - -#[allow(clippy::unnecessary_to_owned)] -fn build_dns_updater(bp: &mut Bootstrap, acme_id: &str) -> Option { - let timeout = config - .property_or_default(("acme", acme_id, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)); - - match config.value_require(("acme", acme_id, "provider"))? { - "rfc2136-tsig" => { - let algorithm: TsigAlgorithm = config - .value_require(("acme", acme_id, "tsig-algorithm"))? - .parse() - .map_err(|_| { - config.new_parse_error(("acme", acme_id, "tsig-algorithm"), "Invalid algorithm") - }) - .ok()?; - let key = STANDARD - .decode(config.value_require(("acme", acme_id, "secret"))?.trim()) - .map_err(|_| { - config.new_parse_error( - ("acme", acme_id, "secret"), - "Failed to base64 decode secret", - ) - }) - .ok()?; - let host = config.property_require::(("acme", acme_id, "host"))?; - let port = config - .property_or_default::(("acme", acme_id, "port"), "53") - .unwrap_or(53); - let addr = if config.value(("acme", acme_id, "protocol")) == Some("tcp") { - DnsAddress::Tcp(SocketAddr::new(host, port)) - } else { - DnsAddress::Udp(SocketAddr::new(host, port)) - }; - - DnsUpdater::new_rfc2136_tsig( - addr, - config - .value_require(("acme", acme_id, "key"))? - .trim() - .to_string(), - key, - algorithm, - ) - .map_err(|err| { - config.new_build_error( - ("acme", acme_id, "provider"), - format!("Failed to create RFC2136-TSIG DNS updater: {err}"), - ) - }) - .ok() - } - "cloudflare" => DnsUpdater::new_cloudflare( - config - .value_require(("acme", acme_id, "secret"))? - .trim() - .to_string(), - config.value(("acme", acme_id, "user")).map(|s| s.trim()), - timeout.into(), - ) - .map_err(|err| { - config.new_build_error( - ("acme", acme_id, "provider"), - format!("Failed to create Cloudflare DNS updater: {err}"), - ) - }) - .ok(), - "digitalocean" => DnsUpdater::new_digitalocean( - config - .value_require(("acme", acme_id, "secret"))? - .trim() - .to_string(), - timeout.into(), - ) - .map_err(|err| { - config.new_build_error( - ("acme", acme_id, "provider"), - format!("Failed to create DigitalOcean DNS updater: {err}"), - ) - }) - .ok(), - "desec" => DnsUpdater::new_desec( - config - .value_require(("acme", acme_id, "secret"))? - .trim() - .to_string(), - timeout.into(), - ) - .map_err(|err| { - config.new_build_error( - ("acme", acme_id, "provider"), - format!("Failed to create Desec DNS updater: {err}"), - ) - }) - .ok(), - "ovh" => DnsUpdater::new_ovh( - config - .value_require(("acme", acme_id, "key")) - .map(|s| s.trim())? - .to_string(), - config - .value_require(("acme", acme_id, "secret"))? - .trim() - .to_string(), - config - .value_require(("acme", acme_id, "consumer-key"))? - .trim() - .to_string(), - config - .value_require(("acme", acme_id, "ovh-endpoint"))? - .parse() - .map_err(|_| { - config - .new_parse_error(("acme", acme_id, "ovh-endpoint"), "Invalid OVH endpoint") - }) - .ok()?, - timeout.into(), - ) - .map_err(|err| { - config.new_build_error( - ("acme", acme_id, "provider"), - format!("Failed to create OVH DNS updater: {err}"), - ) - }) - .ok(), - _ => { - config.new_parse_error(("acme", acme_id, "provider"), "Unsupported provider"); - None - } - } -} - -pub(crate) fn parse_certificates( - bp: &mut Bootstrap, - certificates: &mut AHashMap>, - subject_names: &mut AHashSet, -) { - // Parse certificates - for cert_id in config.sub_keys("certificate", ".cert") { - let cert_id = cert_id.as_str(); - let key_cert = ("certificate", cert_id, "cert"); - let key_pk = ("certificate", cert_id, "private-key"); - - let cert = config - .value_require(key_cert) - .map(|s| s.as_bytes().to_vec()); - let pk = config.value_require(key_pk).map(|s| s.as_bytes().to_vec()); - - if let (Some(cert), Some(pk)) = (cert, pk) { - match build_certified_key(cert, pk) { Ok(cert) => { match cert .end_entity_cert() @@ -378,11 +251,7 @@ pub(crate) fn parse_certificates( } // Add custom SNIs - names.extend( - config - .values(("certificate", cert_id, "subjects")) - .map(|(_, v)| v.trim().to_string()), - ); + names.extend(cert_obj.object.subject_alternative_names); // Add domain names subject_names.extend(names.iter().cloned()); @@ -399,17 +268,18 @@ pub(crate) fn parse_certificates( } // Add default certificate - if config - .property::(("certificate", cert_id, "default")) - .unwrap_or_default() - { + if cert_obj.object.default { certificates.insert("*".to_string(), cert.clone()); } } - Err(err) => config.new_build_error(format!("certificate.{cert_id}"), err), + Err(err) => { + self.build_error(cert_obj.id, format!("Invalid certificate: {err}")); + } } } - Err(err) => config.new_build_error(format!("certificate.{cert_id}"), err), + Err(err) => { + self.build_error(cert_obj.id, format!("Invalid certificate: {err}")); + } } } } diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index e00f2200..65c5d616 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -4,20 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::*; -use crate::expr::{self, Constant, if_block::IfBlock, tokenizer::TokenMap}; -use ahash::AHashMap; +use crate::{ + expr::{self, if_block::IfBlock}, + manager::bootstrap::Bootstrap, +}; use mail_auth::{ - common::crypto::{Algorithm, Ed25519Key, HashAlgorithm, RsaKey, Sha256, SigningKey}, + common::crypto::{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 registry::{ + schema::{ + enums::{self, ExpressionConstant}, + prelude::Object, + structs::{Dkim1Signature, DkimSignature, SenderAuth}, + }, + types::ObjectType, }; +use rustls_pki_types::{PrivateKeyDer, PrivatePkcs1KeyDer, PrivatePkcs8KeyDer, pem::PemObject}; +use std::sync::Arc; +use utils::config::utils::ParseValue; #[derive(Clone)] pub struct MailAuthConfig { @@ -26,14 +32,6 @@ pub struct MailAuthConfig { pub spf: SpfAuthConfig, pub dmarc: DmarcAuthConfig, pub iprev: IpRevAuthConfig, - pub signatures: AHashMap>>, -} - -#[allow(clippy::large_enum_variant)] -pub enum LazySignature { - Resolved(ResolvedSignature), - Pending(Config), - Failed, } #[derive(Clone)] @@ -79,12 +77,6 @@ pub enum VerifyStrategy { Disable, } -#[derive(Debug, Clone)] -pub struct DkimCanonicalization { - pub headers: Canonicalization, - pub body: Canonicalization, -} - pub enum DkimSigner { RsaSha256(mail_auth::dkim::DkimSigner, Done>), Ed25519Sha256(mail_auth::dkim::DkimSigner), @@ -95,212 +87,180 @@ pub enum ArcSealer { Ed25519Sha256(mail_auth::arc::ArcSealer), } -impl Default for MailAuthConfig { - fn default() -> Self { - Self { +impl MailAuthConfig { + pub async fn parse(bp: &mut Bootstrap) -> Self { + let auth = bp.setting_infallible::().await; + + MailAuthConfig { dkim: DkimAuthConfig { - verify: IfBlock::new_default::("auth.dkim.verify", [], "relaxed"), - sign: IfBlock::new_default( - "auth.dkim.sign", - [( - "is_local_domain('*', sender_domain)", - "['rsa-' + sender_domain, 'ed25519-' + sender_domain]", - )], - "false", - ), - strict: true, + verify: bp.compile_expr(Object::SenderAuth.singleton(), &auth.ctx_dkim_verify()), + sign: bp.compile_expr(Object::SenderAuth.singleton(), &auth.ctx_dkim_sign_domain()), + strict: auth.dkim_strict, }, arc: ArcAuthConfig { - verify: IfBlock::new_default::("auth.arc.verify", [], "relaxed"), - seal: IfBlock::new_default( - "auth.arc.seal", - [], - "'rsa-' + config_get('report.domain')", - ), + verify: bp.compile_expr(Object::SenderAuth.singleton(), &auth.ctx_arc_verify()), + seal: bp.compile_expr(Object::SenderAuth.singleton(), &auth.ctx_arc_seal_domain()), }, spf: SpfAuthConfig { - verify_ehlo: IfBlock::new_default::( - "auth.spf.verify.ehlo", - [("local_port == 25", "relaxed")], - #[cfg(not(feature = "test_mode"))] - "disable", - #[cfg(feature = "test_mode")] - "relaxed", - ), - verify_mail_from: IfBlock::new_default::( - "auth.spf.verify.mail-from", - [("local_port == 25", "relaxed")], - #[cfg(not(feature = "test_mode"))] - "disable", - #[cfg(feature = "test_mode")] - "relaxed", - ), + verify_ehlo: bp + .compile_expr(Object::SenderAuth.singleton(), &auth.ctx_spf_ehlo_verify()), + verify_mail_from: bp + .compile_expr(Object::SenderAuth.singleton(), &auth.ctx_spf_from_verify()), }, dmarc: DmarcAuthConfig { - verify: IfBlock::new_default::( - "auth.dmarc.verify", - [("local_port == 25", "relaxed")], - #[cfg(not(feature = "test_mode"))] - "disable", - #[cfg(feature = "test_mode")] - "relaxed", - ), + verify: bp.compile_expr(Object::SenderAuth.singleton(), &auth.ctx_dmarc_verify()), }, iprev: IpRevAuthConfig { - verify: IfBlock::new_default::( - "auth.iprev.verify", - [("local_port == 25", "relaxed")], - #[cfg(not(feature = "test_mode"))] - "disable", - #[cfg(feature = "test_mode")] - "relaxed", + verify: bp.compile_expr( + Object::SenderAuth.singleton(), + &auth.ctx_reverse_ip_verify(), ), }, - signatures: Default::default(), } } } -impl MailAuthConfig { - pub fn parse(bp: &mut Bootstrap) -> Self { - let rcpt_vars = TokenMap::default() - .with_variables(SMTP_RCPT_TO_VARS) - .with_constants::(); - let conn_vars = TokenMap::default() - .with_variables(CONNECTION_VARS) - .with_constants::(); - let mut mail_auth = Self::default(); +impl DkimSigner { + pub fn new(selector: String, domain: String, signature: DkimSignature) -> trc::Result { + match signature { + DkimSignature::Dkim1Ed25519Sha256(signature) => { + let mut errors = vec![]; + if !signature.validate(&mut errors) { + return Err(trc::DkimEvent::BuildError + .reason("DKIM signature validation failed") + .details( + errors + .into_iter() + .map(|v| trc::Value::from(v.to_string())) + .collect::>(), + )); + } - for (value, key, token_map) in [ - (&mut mail_auth.dkim.verify, "auth.dkim.verify", &rcpt_vars), - (&mut mail_auth.dkim.sign, "auth.dkim.sign", &rcpt_vars), - (&mut mail_auth.arc.verify, "auth.arc.verify", &rcpt_vars), - (&mut mail_auth.arc.seal, "auth.arc.seal", &rcpt_vars), - ( - &mut mail_auth.spf.verify_ehlo, - "auth.spf.verify.ehlo", - &conn_vars, - ), - ( - &mut mail_auth.spf.verify_mail_from, - "auth.spf.verify.mail-from", - &conn_vars, - ), - (&mut mail_auth.dmarc.verify, "auth.dmarc.verify", &rcpt_vars), - (&mut mail_auth.iprev.verify, "auth.iprev.verify", &conn_vars), - ] { - if let Some(if_block) = IfBlock::try_parse(config, key, token_map) { - *value = if_block; + let private_key = simple_pem_parse(&signature.private_key).ok_or_else(|| { + trc::DkimEvent::BuildError + .reason("Failed to parse ED25519 private key PEM") + .details("Invalid PEM format") + })?; + let key = + Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key).map_err(|err| { + trc::DkimEvent::BuildError + .reason(err) + .details("Failed to build ED25519 key") + })?; + + Ok(DkimSigner::Ed25519Sha256(build_dkim1_signer( + domain, selector, signature, key, + ))) + } + DkimSignature::Dkim1RsaSha256(signature) => { + let mut errors = vec![]; + if !signature.validate(&mut errors) { + return Err(trc::DkimEvent::BuildError + .reason("DKIM signature validation failed") + .details( + errors + .into_iter() + .map(|v| trc::Value::from(v.to_string())) + .collect::>(), + )); + } + + let key = PrivatePkcs1KeyDer::from_pem_slice(signature.private_key.as_bytes()) + .map(PrivateKeyDer::Pkcs1) + .or_else(|_| { + PrivatePkcs8KeyDer::from_pem_slice(signature.private_key.as_bytes()) + .map(PrivateKeyDer::Pkcs8) + }) + .map_err(|err| { + trc::DkimEvent::BuildError + .reason(err) + .details("Failed to build RSA key") + }) + .and_then(|key| { + RsaKey::::from_key_der(key).map_err(|err| { + trc::DkimEvent::BuildError + .reason(err) + .details("Failed to build RSA key") + }) + })?; + + Ok(DkimSigner::RsaSha256(build_dkim1_signer( + domain, selector, signature, key, + ))) } } - mail_auth.dkim.strict = config - .property_or_default("auth.dkim.strict", "true") - .unwrap_or(true); + } +} - // Parse signatures - let mut signatures: AHashMap<&str, Config> = AHashMap::new(); - let mut current_id = None; - for (k, v) in config.keys.iter() { - if let Some(prefix) = k.strip_prefix("signature.") { - if let Some(id) = prefix.strip_suffix(".algorithm") { - current_id = Some(id); +impl ArcSealer { + pub fn new(selector: String, domain: String, signature: DkimSignature) -> trc::Result { + match signature { + DkimSignature::Dkim1Ed25519Sha256(signature) => { + let mut errors = vec![]; + if !signature.validate(&mut errors) { + return Err(trc::DkimEvent::BuildError + .reason("DKIM signature validation failed") + .details( + errors + .into_iter() + .map(|v| trc::Value::from(v.to_string())) + .collect::>(), + )); } - #[allow(clippy::unwrap_or_default)] - if let Some(current_id) = current_id { - signatures - .entry(current_id) - .or_insert_with(Config::default) - .keys - .insert(k.to_string(), v.to_string()); + + let private_key = simple_pem_parse(&signature.private_key).ok_or_else(|| { + trc::DkimEvent::BuildError + .reason("Failed to parse ED25519 private key PEM") + .details("Invalid PEM format") + })?; + let key = + Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key).map_err(|err| { + trc::DkimEvent::BuildError + .reason(err) + .details("Failed to build ED25519 key") + })?; + + Ok(ArcSealer::Ed25519Sha256(build_dkim1_sealer( + domain, selector, signature, key, + ))) + } + DkimSignature::Dkim1RsaSha256(signature) => { + let mut errors = vec![]; + if !signature.validate(&mut errors) { + return Err(trc::DkimEvent::BuildError + .reason("DKIM signature validation failed") + .details( + errors + .into_iter() + .map(|v| trc::Value::from(v.to_string())) + .collect::>(), + )); } - } else if !signatures.is_empty() { - break; + + let key = PrivatePkcs1KeyDer::from_pem_slice(signature.private_key.as_bytes()) + .map(PrivateKeyDer::Pkcs1) + .or_else(|_| { + PrivatePkcs8KeyDer::from_pem_slice(signature.private_key.as_bytes()) + .map(PrivateKeyDer::Pkcs8) + }) + .map_err(|err| { + trc::DkimEvent::BuildError + .reason(err) + .details("Failed to build RSA key") + }) + .and_then(|key| { + RsaKey::::from_key_der(key).map_err(|err| { + trc::DkimEvent::BuildError + .reason(err) + .details("Failed to build RSA key") + }) + })?; + + Ok(ArcSealer::RsaSha256(build_dkim1_sealer( + domain, selector, signature, key, + ))) } } - mail_auth.signatures = signatures - .into_iter() - .map(|(id, config)| { - ( - id.to_string(), - Arc::new(ArcSwap::from_pointee(LazySignature::Pending(config))), - ) - }) - .collect(); - - mail_auth - } -} - -pub fn build_signature(bp: &mut Bootstrap, id: &str) -> Option<(DkimSigner, ArcSealer)> { - match config.property_require::(("signature", id, "algorithm"))? { - Algorithm::RsaSha256 => { - let pk = config - .value_require(("signature", id, "private-key"))? - .trim() - .to_string(); - let key = RsaKey::::from_rsa_pem(&pk) - .or_else(|_| RsaKey::::from_pkcs8_pem(&pk)) - .map_err(|err| { - config.new_build_error( - ("signature", id, "private-key"), - format!("Failed to build RSA key: {err}",), - ) - }) - .ok()?; - let key_clone = RsaKey::::from_rsa_pem(&pk) - .or_else(|_| RsaKey::::from_pkcs8_pem(&pk)) - .map_err(|err| { - config.new_build_error( - ("signature", id, "private-key"), - format!("Failed to build RSA key: {err}",), - ) - }) - .ok()?; - let (signer, sealer) = parse_signature(config, id, key_clone, key)?; - (DkimSigner::RsaSha256(signer), ArcSealer::RsaSha256(sealer)).into() - } - Algorithm::Ed25519Sha256 => { - let private_key = parse_pem(config, ("signature", id, "private-key"))?; - let key = Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key) - .map_err(|err| { - config.new_build_error( - ("signature", id), - format!("Failed to build ED25519 key for signature {id:?}: {err}"), - ) - }) - .ok()?; - let key_clone = Ed25519Key::from_pkcs8_maybe_unchecked_der(&private_key) - .map_err(|err| { - config.new_build_error( - ("signature", id), - format!("Failed to build ED25519 key for signature {id:?}: {err}"), - ) - }) - .ok()?; - - let (signer, sealer) = parse_signature(config, id, key_clone, key)?; - ( - DkimSigner::Ed25519Sha256(signer), - ArcSealer::Ed25519Sha256(sealer), - ) - .into() - } - Algorithm::RsaSha1 => { - config.new_build_error( - ("signature", id), - format!("Could not build signature {id:?}: SHA1 signatures are deprecated.",), - ); - None - } - } -} - -fn parse_pem(bp: &mut Bootstrap, key: impl AsKey) -> Option> { - if let Some(der) = simple_pem_parse(config.value_require(key.clone())?) { - Some(der) - } else { - config.new_build_error(key, "Failed to base64 decode key."); - None } } @@ -333,88 +293,109 @@ pub fn simple_pem_parse(contents: &str) -> Option> { base64_decode(&base64) } -fn parse_signature>( - bp: &mut Bootstrap, - id: &str, - key_dkim: T, - key_arc: U, -) -> Option<( - mail_auth::dkim::DkimSigner, - mail_auth::arc::ArcSealer, -)> { - let domain = config - .value_require(("signature", id, "domain"))? - .to_string(); - let selector = config - .value_require(("signature", id, "selector"))? - .to_string(); - let mut headers = config - .values(("signature", id, "headers")) - .filter_map(|(_, v)| { - if !v.is_empty() { - v.to_string().into() - } else { - None - } - }) - .collect::>(); - if headers.is_empty() { - headers = vec![ - "From".to_string(), - "To".to_string(), - "Date".to_string(), - "Subject".to_string(), - "Message-ID".to_string(), - ]; - } - - let mut signer = mail_auth::dkim::DkimSigner::from_key(key_dkim) - .domain(&domain) - .selector(&selector) - .headers(headers.clone()); - if !headers - .iter() - .any(|h| h.eq_ignore_ascii_case("DKIM-Signature")) - { - headers.push("DKIM-Signature".to_string()); - } - let mut sealer = mail_auth::arc::ArcSealer::from_key(key_arc) +fn build_dkim1_signer( + domain: String, + selector: String, + signature: Dkim1Signature, + key: T, +) -> mail_auth::dkim::DkimSigner { + let mut signer = mail_auth::dkim::DkimSigner::from_key(key) .domain(domain) .selector(selector) - .headers(headers); + .headers(signature.headers) + .reporting(signature.report); - if let Some(c) = config.property::(("signature", id, "canonicalization")) - { - signer = signer - .body_canonicalization(c.body) - .header_canonicalization(c.headers); - sealer = sealer - .body_canonicalization(c.body) - .header_canonicalization(c.headers); + match signature.canonicalization { + enums::DkimCanonicalization::RelaxedRelaxed => { + signer = signer + .body_canonicalization(Canonicalization::Relaxed) + .header_canonicalization(Canonicalization::Relaxed); + } + enums::DkimCanonicalization::SimpleSimple => { + signer = signer + .body_canonicalization(Canonicalization::Simple) + .header_canonicalization(Canonicalization::Simple); + } + enums::DkimCanonicalization::RelaxedSimple => { + signer = signer + .body_canonicalization(Canonicalization::Simple) + .header_canonicalization(Canonicalization::Relaxed); + } + enums::DkimCanonicalization::SimpleRelaxed => { + signer = signer + .body_canonicalization(Canonicalization::Relaxed) + .header_canonicalization(Canonicalization::Simple); + } } - if let Some(c) = config.property::(("signature", id, "expire")) { - signer = signer.expiration(c.as_secs()); - sealer = sealer.expiration(c.as_secs()); + if let Some(expire) = signature.expire { + signer = signer.expiration(expire.into_inner().as_secs()); } - if let Some(true) = config.property::(("signature", id, "report")) { - signer = signer.reporting(true); - } - - if let Some(auid) = config.property::(("signature", id, "auid")) { + if let Some(auid) = signature.auid { signer = signer.agent_user_identifier(auid); } - if let Some(atps) = config.property::(("signature", id, "third-party")) { + if let Some(atps) = signature.third_party { signer = signer.atps(atps); } - if let Some(atpsh) = config.property::(("signature", id, "third-party-algo")) { - signer = signer.atpsh(atpsh); + if let Some(atpsh) = signature.third_party_hash { + signer = signer.atpsh(match atpsh { + enums::DkimHash::Sha256 => HashAlgorithm::Sha256, + enums::DkimHash::Sha1 => HashAlgorithm::Sha1, + }); + } + signer +} + +fn build_dkim1_sealer>( + domain: String, + selector: String, + mut signature: Dkim1Signature, + key: T, +) -> mail_auth::arc::ArcSealer { + if !signature + .headers + .iter() + .any(|h| h.eq_ignore_ascii_case("DKIM-Signature")) + { + signature.headers.push("DKIM-Signature".to_string()); } - Some((signer, sealer)) + let mut sealer = mail_auth::arc::ArcSealer::from_key(key) + .domain(domain) + .selector(selector) + .headers(signature.headers); + + match signature.canonicalization { + enums::DkimCanonicalization::RelaxedRelaxed => { + sealer = sealer + .body_canonicalization(Canonicalization::Relaxed) + .header_canonicalization(Canonicalization::Relaxed); + } + enums::DkimCanonicalization::SimpleSimple => { + sealer = sealer + .body_canonicalization(Canonicalization::Simple) + .header_canonicalization(Canonicalization::Simple); + } + enums::DkimCanonicalization::RelaxedSimple => { + sealer = sealer + .body_canonicalization(Canonicalization::Simple) + .header_canonicalization(Canonicalization::Relaxed); + } + enums::DkimCanonicalization::SimpleRelaxed => { + sealer = sealer + .body_canonicalization(Canonicalization::Relaxed) + .header_canonicalization(Canonicalization::Simple); + } + } + + if let Some(expire) = signature.expire { + sealer = sealer.expiration(expire.into_inner().as_secs()); + } + + sealer } impl<'x> TryFrom> for VerifyStrategy { @@ -455,29 +436,3 @@ impl ParseValue for VerifyStrategy { } } } - -impl ParseValue for DkimCanonicalization { - fn parse_value(value: &str) -> Result { - if let Some((headers, body)) = value.split_once('/') { - Ok(DkimCanonicalization { - headers: Canonicalization::parse_value(headers.trim())?, - body: Canonicalization::parse_value(body.trim())?, - }) - } else { - let c = Canonicalization::parse_value(value)?; - Ok(DkimCanonicalization { - headers: c, - body: c, - }) - } - } -} - -impl Default for DkimCanonicalization { - fn default() -> Self { - Self { - headers: Canonicalization::Relaxed, - body: Canonicalization::Relaxed, - } - } -} diff --git a/crates/common/src/config/smtp/mod.rs b/crates/common/src/config/smtp/mod.rs index 586f404a..e9ae765a 100644 --- a/crates/common/src/config/smtp/mod.rs +++ b/crates/common/src/config/smtp/mod.rs @@ -4,25 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use utils::config::{Config, Rate}; - pub mod auth; pub mod queue; pub mod report; pub mod resolver; pub mod session; -pub mod throttle; - -use crate::expr::{Expression, tokenizer::TokenMap}; use self::{ auth::MailAuthConfig, queue::QueueConfig, report::ReportConfig, resolver::Resolvers, session::SessionConfig, }; - use super::*; +use crate::{expr::Expression, manager::bootstrap::Bootstrap}; +use registry::schema::structs::Rate; -#[derive(Default, Clone)] +#[derive(Clone)] pub struct SmtpConfig { pub session: SessionConfig, pub queue: QueueConfig, @@ -32,7 +28,7 @@ pub struct SmtpConfig { } #[derive(Debug, Default, Clone)] -#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] +//#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] pub struct QueueRateLimiter { pub id: String, pub expr: Expression, @@ -54,11 +50,11 @@ pub const THROTTLE_HELO_DOMAIN: u16 = 1 << 9; impl SmtpConfig { pub async fn parse(bp: &mut Bootstrap) -> Self { Self { - session: SessionConfig::parse(config), - queue: QueueConfig::parse(config), - resolvers: Resolvers::parse(config).await, - mail_auth: MailAuthConfig::parse(config), - report: ReportConfig::parse(config), + session: SessionConfig::parse(bp).await, + queue: QueueConfig::parse(bp).await, + resolvers: Resolvers::parse(bp).await, + mail_auth: MailAuthConfig::parse(bp).await, + report: ReportConfig::parse(bp).await, } } } diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index 3b5c61e5..610e96f3 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use self::throttle::parse_queue_rate_limiter; use super::*; use crate::{ config::server::ServerProtocol, @@ -13,15 +12,22 @@ use crate::{ use ahash::AHashMap; use mail_auth::IpLookupStrategy; use mail_send::Credentials; -use registry::schema::enums::ExpressionConstant; +use registry::schema::{ + enums::{self, ExpressionConstant, ExpressionVariable, MtaRequiredOrOptional}, + prelude::Object, + structs::{ + DsnReportSettings, MtaConnectionStrategy, MtaDeliveryExpiration, MtaDeliverySchedule, + MtaInboundThrottle, MtaOutboundStrategy, MtaOutboundThrottle, MtaQueueQuota, MtaRoute, + MtaTlsStrategy, MtaVirtualQueue, + }, +}; use std::{ fmt::Display, hash::{Hash, Hasher}, net::IpAddr, time::Duration, }; -use throttle::parse_queue_rate_limiter_key; -use utils::config::{Config, utils::ParseValue}; +use utils::config::utils::ParseValue; #[derive( Debug, @@ -187,483 +193,376 @@ pub enum RequireOptional { Disable, } -impl Default for QueueConfig { - fn default() -> Self { - Self { - route: IfBlock::new_default( - "queue.strategy.route", - #[cfg(not(feature = "test_mode"))] - [("is_local_domain('*', rcpt_domain)", "'local'")], - #[cfg(feature = "test_mode")] - [], - "'mx'", - ), - queue: IfBlock::new_default( - "queue.strategy.schedule", - #[cfg(not(feature = "test_mode"))] - [ - ("is_local_domain('*', rcpt_domain)", "'local'"), - ("source == 'dsn'", "'dsn'"), - ("source == 'report'", "'report'"), - ], - #[cfg(feature = "test_mode")] - [], - #[cfg(not(feature = "test_mode"))] - "'remote'", - #[cfg(feature = "test_mode")] - "'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'")], - #[cfg(feature = "test_mode")] - [], - "'default'", +impl QueueConfig { + pub async fn parse(bp: &mut Bootstrap) -> Self { + let st = bp.setting_infallible::().await; + let dsn = bp.setting_infallible::().await; + + let mut queue = QueueConfig { + route: bp.compile_expr(Object::MtaOutboundStrategy.singleton(), &st.ctx_route()), + queue: bp.compile_expr(Object::MtaOutboundStrategy.singleton(), &st.ctx_schedule()), + connection: bp.compile_expr( + Object::MtaOutboundStrategy.singleton(), + &st.ctx_connection(), ), + tls: bp.compile_expr(Object::MtaOutboundStrategy.singleton(), &st.ctx_tls()), dsn: Dsn { - 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')", + name: bp.compile_expr(Object::DsnReportSettings.singleton(), &dsn.ctx_from_name()), + address: bp.compile_expr( + Object::DsnReportSettings.singleton(), + &dsn.ctx_from_address(), ), - sign: IfBlock::new_default( - "report.dsn.sign", - [], - "['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]", + sign: bp.compile_expr( + Object::DsnReportSettings.singleton(), + &dsn.ctx_dkim_sign_domain(), ), }, - inbound_limiters: QueueRateLimiters::default(), - outbound_limiters: QueueRateLimiters::default(), - quota: QueueQuotas::default(), + inbound_limiters: QueueRateLimiters::parse_inbound(bp).await, + outbound_limiters: QueueRateLimiters::parse_outbound(bp).await, + quota: QueueQuotas::parse(bp).await, queue_strategy: Default::default(), - virtual_queues: Default::default(), connection_strategy: Default::default(), routing_strategy: Default::default(), tls_strategy: Default::default(), - } - } -} + virtual_queues: Default::default(), + }; -impl QueueConfig { - pub fn parse(bp: &mut Bootstrap) -> Self { - let mut queue = QueueConfig::default(); - let rcpt_vars = TokenMap::default().with_variables(SMTP_QUEUE_RCPT_VARS); - let sender_vars = TokenMap::default().with_variables(SMTP_QUEUE_SENDER_VARS); - let host_vars = TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS); - - for (value, key, token_map) in [ - (&mut queue.route, "queue.strategy.route", &rcpt_vars), - (&mut queue.queue, "queue.strategy.schedule", &rcpt_vars), - ( - &mut queue.connection, - "queue.strategy.connection", - &host_vars, - ), - (&mut queue.tls, "queue.strategy.tls", &host_vars), - (&mut queue.dsn.name, "report.dsn.from-name", &sender_vars), - ( - &mut queue.dsn.address, - "report.dsn.from-address", - &sender_vars, - ), - (&mut queue.dsn.sign, "report.dsn.sign", &sender_vars), - ] { - if let Some(if_block) = IfBlock::try_parse(config, key, token_map) { - *value = if_block; + // Parse virtual queues + let mut queue_id_to_name = AHashMap::new(); + for obj in bp.list_infallible::().await { + if bp.validate(obj.id, &obj.object) + && let Some(queue_name) = QueueName::new(&obj.object.name) + { + queue_id_to_name.insert(obj.id, queue_name); + queue.virtual_queues.insert( + queue_name, + VirtualQueue { + threads: obj.object.threads_per_node as usize, + }, + ); } } - // Parse strategies - queue.virtual_queues = parse_virtual_queues(config); - queue.queue_strategy = parse_queue_strategies(config, &queue.virtual_queues); - queue.connection_strategy = parse_connection_strategies(config); - queue.routing_strategy = parse_routing_strategies(config); - queue.tls_strategy = parse_tls_strategies(config); + // Parse queue strategies + for obj in bp.list_infallible::().await { + if !bp.validate(obj.id, &obj.object) { + continue; + } + let virtual_queue = if let Some(name) = queue_id_to_name.get(&obj.object.queue_id) { + *name + } else { + bp.build_error( + obj.id, + format!("Virtual queue ID '{}' does not exist.", obj.object.queue_id), + ); + continue; + }; + queue.queue_strategy.insert( + obj.object.name, + QueueStrategy { + retry: obj + .object + .retry + .into_iter() + .map(|d| d.into_inner().as_secs()) + .collect(), + notify: obj + .object + .notify + .into_iter() + .map(|d| d.into_inner().as_secs()) + .collect(), + expiry: match obj.object.expiry { + MtaDeliveryExpiration::Ttl(exp) => { + QueueExpiry::Ttl(exp.expire.into_inner().as_secs()) + } + MtaDeliveryExpiration::Attempts(exp) => { + QueueExpiry::Attempts(exp.max_attempts as u32) + } + }, + virtual_queue, + }, + ); + } + + // Parse connection strategies + for obj in bp.list_infallible::().await { + if !bp.validate(obj.id, &obj.object) { + continue; + } + + let mut source_ipv4 = Vec::new(); + let mut source_ipv6 = Vec::new(); + + for ip_host in obj.object.source_ips { + let ip_host = IpAndHost { + ip: ip_host.source_ip.into_inner(), + host: ip_host.ehlo_hostname, + }; + if ip_host.ip.is_ipv4() { + source_ipv4.push(ip_host); + } else { + source_ipv6.push(ip_host); + } + } + + queue.connection_strategy.insert( + obj.object.name, + ConnectionStrategy { + source_ipv4, + source_ipv6, + ehlo_hostname: obj.object.ehlo_hostname, + timeout_connect: obj.object.connect_timeout.into_inner(), + timeout_greeting: obj.object.greeting_timeout.into_inner(), + timeout_ehlo: obj.object.ehlo_timeout.into_inner(), + timeout_mail: obj.object.mail_from_timeout.into_inner(), + timeout_rcpt: obj.object.rcpt_to_timeout.into_inner(), + timeout_data: obj.object.data_timeout.into_inner(), + }, + ); + } + + // Parse routing strategies + for obj in bp.list_infallible::().await { + if !bp.validate(obj.id, &obj.object) { + continue; + } + + match obj.object { + MtaRoute::Mx(route) => { + queue.routing_strategy.insert( + route.name, + RoutingStrategy::Mx(MxConfig { + max_mx: route.max_mx_hosts as usize, + max_multi_homed: route.max_multihomed as usize, + ip_lookup_strategy: match route.ip_lookup_strategy { + enums::MtaIpStrategy::V4ThenV6 => IpLookupStrategy::Ipv4thenIpv6, + enums::MtaIpStrategy::V6ThenV4 => IpLookupStrategy::Ipv6thenIpv4, + enums::MtaIpStrategy::V4Only => IpLookupStrategy::Ipv4Only, + enums::MtaIpStrategy::V6Only => IpLookupStrategy::Ipv6Only, + }, + }), + ); + } + MtaRoute::Relay(route) => { + queue.routing_strategy.insert( + route.name, + RoutingStrategy::Relay(RelayConfig { + address: route.address, + port: route.port as u16, + protocol: match route.protocol { + enums::MtaProtocol::Smtp => ServerProtocol::Smtp, + enums::MtaProtocol::Lmtp => ServerProtocol::Lmtp, + }, + auth: route + .auth_username + .and_then(|user| route.auth_secret.map(|secret| (user, secret))) + .map(|(user, secret)| Credentials::new(user, secret)), + tls_implicit: route.implicit_tls, + tls_allow_invalid_certs: route.allow_invalid_certs, + }), + ); + } + MtaRoute::Local(route) => { + queue + .routing_strategy + .insert(route.name, RoutingStrategy::Local); + } + } + } + + // Parse TLS strategies + for obj in bp.list_infallible::().await { + if !bp.validate(obj.id, &obj.object) { + continue; + } + + queue.tls_strategy.insert( + obj.object.name, + TlsStrategy { + dane: match obj.object.dane { + MtaRequiredOrOptional::Optional => RequireOptional::Optional, + MtaRequiredOrOptional::Require => RequireOptional::Require, + MtaRequiredOrOptional::Disable => RequireOptional::Disable, + }, + mta_sts: match obj.object.mta_sts { + MtaRequiredOrOptional::Optional => RequireOptional::Optional, + MtaRequiredOrOptional::Require => RequireOptional::Require, + MtaRequiredOrOptional::Disable => RequireOptional::Disable, + }, + tls: match obj.object.start_tls { + MtaRequiredOrOptional::Optional => RequireOptional::Optional, + MtaRequiredOrOptional::Require => RequireOptional::Require, + MtaRequiredOrOptional::Disable => RequireOptional::Disable, + }, + allow_invalid_certs: obj.object.allow_invalid_certs, + timeout_tls: obj.object.tls_timeout.into_inner(), + timeout_mta_sts: obj.object.mta_sts_timeout.into_inner(), + }, + ); + } - // Parse rate limiters - queue.inbound_limiters = parse_inbound_rate_limiters(config); - queue.outbound_limiters = parse_outbound_rate_limiters(config); - queue.quota = parse_queue_quota(config); queue } } -fn parse_queue_strategies( - bp: &mut Bootstrap, - queues: &AHashMap, -) -> AHashMap { - let mut entries = AHashMap::new(); - for key in config.sub_keys_with_suffixes( - "queue.schedule", - &[ - ".queue-name", - ".retry", - ".notify", - ".expire", - ".max-attempts", - ], - ) { - if let Some(strategy) = parse_queue_strategy(config, &key, queues) { - entries.insert(key, strategy); - } - } - entries -} +impl QueueRateLimiters { + async fn parse_inbound(bp: &mut Bootstrap) -> QueueRateLimiters { + let mut throttle = QueueRateLimiters::default(); -fn parse_queue_strategy( - bp: &mut Bootstrap, - id: &str, - queues: &AHashMap, -) -> Option { - let virtual_queue = config - .property_require::(("queue.schedule", id, "queue-name")) - .unwrap_or_default(); - if virtual_queue != DEFAULT_QUEUE_NAME && !queues.contains_key(&virtual_queue) { - config.new_parse_error( - ("queue.schedule", id, "queue-name"), - format!("Virtual queue '{virtual_queue}' does not exist."), - ); - return None; - } - let mut retry: Vec = config - .properties::(("queue.schedule", id, "retry")) - .into_iter() - .map(|(_, d)| d.as_secs()) - .collect(); - let mut notify: Vec = config - .properties::(("queue.schedule", id, "notify")) - .into_iter() - .map(|(_, d)| d.as_secs()) - .collect(); - if retry.is_empty() { - config.new_parse_error( - ("queue.schedule", id, "retry"), - "At least one 'retry' duration must be specified.".to_string(), - ); - retry.push(60 * 60); // Default to 1 minute - } - if notify.is_empty() { - notify.push(10000 * 86400); // Disable notifications by default - } - - Some(QueueStrategy { - retry, - notify, - expiry: match ( - config.property::(("queue.schedule", id, "expire")), - config.property::(("queue.schedule", id, "max-attempts")), - ) { - (Some(duration), None) => QueueExpiry::Ttl(duration.as_secs()), - (None, Some(count)) => QueueExpiry::Attempts(count), - (Some(_), Some(_)) => { - config.new_parse_error( - ("queue.schedule", id, "expire"), - "Cannot specify both 'expire' and 'max-attempts'.".to_string(), - ); - return None; + for obj in bp.list_infallible::().await { + if !bp.validate(obj.id, &obj.object) || !obj.object.enable { + continue; } - (None, None) => QueueExpiry::Ttl(60 * 60 * 24 * 3), // Default to 3 days - }, - virtual_queue, - }) -} -fn parse_virtual_queues(bp: &mut Bootstrap) -> AHashMap { - let mut entries = AHashMap::new(); - for key in config.sub_keys("queue.virtual", ".threads-per-node") { - if let Some(queue_name) = QueueName::new(&key) { - if let Some(queue) = parse_virtual_queue(config, &key) { - entries.insert(queue_name, queue); - } - } else { - config.new_parse_error( - ("queue.virtual", &key, "threads-per-node"), - format!("Invalid virtual queue name: {key:?}. Must be 1-8 bytes long."), - ); - } - } - entries -} + let limiter = QueueRateLimiter { + expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()).default, + id: obj.object.name, + keys: obj + .object + .key + .iter() + .map(|key| match key { + enums::MtaInboundThrottleKey::Rcpt => THROTTLE_RCPT, + enums::MtaInboundThrottleKey::RcptDomain => THROTTLE_RCPT_DOMAIN, + enums::MtaInboundThrottleKey::Sender => THROTTLE_SENDER, + enums::MtaInboundThrottleKey::SenderDomain => THROTTLE_SENDER_DOMAIN, + enums::MtaInboundThrottleKey::AuthenticatedAs => THROTTLE_AUTH_AS, + enums::MtaInboundThrottleKey::Listener => THROTTLE_LISTENER, + enums::MtaInboundThrottleKey::RemoteIp => THROTTLE_REMOTE_IP, + enums::MtaInboundThrottleKey::LocalIp => THROTTLE_LOCAL_IP, + enums::MtaInboundThrottleKey::HeloDomain => THROTTLE_HELO_DOMAIN, + }) + .fold(0, |acc, key| acc | key), + rate: obj.object.rate, + }; -fn parse_virtual_queue(bp: &mut Bootstrap, id: &str) -> Option { - Some(VirtualQueue { - threads: config - .property_require::(("queue.virtual", id, "threads-per-node")) - .unwrap_or(1), - }) -} - -fn parse_routing_strategies(bp: &mut Bootstrap) -> AHashMap { - let mut entries = AHashMap::new(); - for key in config.sub_keys("queue.route", ".type") { - if let Some(strategy) = parse_route(config, &key) { - entries.insert(key, strategy); - } - } - entries -} - -fn parse_route(bp: &mut Bootstrap, id: &str) -> Option { - match config.value_require_non_empty(("queue.route", id, "type"))? { - "relay" => RoutingStrategy::Relay(RelayConfig { - address: config.property_require(("queue.route", id, "address"))?, - port: config - .property_require(("queue.route", id, "port")) - .unwrap_or(25), - protocol: config - .property_require(("queue.route", id, "protocol")) - .unwrap_or(ServerProtocol::Smtp), - auth: if let (Some(username), Some(secret)) = ( - config.value(("queue.route", id, "auth.username")), - config.value(("queue.route", id, "auth.secret")), - ) { - Credentials::new(username.to_string(), secret.to_string()).into() + if (limiter.keys & (THROTTLE_RCPT | THROTTLE_RCPT_DOMAIN)) != 0 + || limiter.expr.items().iter().any(|c| { + matches!( + c, + ExpressionItem::Variable( + ExpressionVariable::Rcpt | ExpressionVariable::RcptDomain + ) + ) + }) + { + throttle.rcpt.push(limiter); + } else if (limiter.keys + & (THROTTLE_SENDER + | THROTTLE_SENDER_DOMAIN + | THROTTLE_HELO_DOMAIN + | THROTTLE_AUTH_AS)) + != 0 + || limiter.expr.items().iter().any(|c| { + matches!( + c, + ExpressionItem::Variable( + ExpressionVariable::Sender + | ExpressionVariable::SenderDomain + | ExpressionVariable::HeloDomain + | ExpressionVariable::AuthenticatedAs + ) + ) + }) + { + throttle.sender.push(limiter); } else { - None - }, - tls_implicit: config - .property(("queue.route", id, "tls.implicit")) - .unwrap_or(true), - tls_allow_invalid_certs: config - .property(("queue.route", id, "tls.allow-invalid-certs")) - .unwrap_or(false), - }) - .into(), - "local" => RoutingStrategy::Local.into(), - "mx" => RoutingStrategy::Mx(MxConfig { - max_mx: config - .property(("queue.route", id, "limits.mx")) - .unwrap_or(5), - max_multi_homed: config - .property(("queue.route", id, "limits.multihomed")) - .unwrap_or(2), - ip_lookup_strategy: config - .property(("queue.route", id, "ip-lookup")) - .unwrap_or(IpLookupStrategy::Ipv4thenIpv6), - }) - .into(), - invalid => { - let details = - format!("Invalid route type: {invalid:?}. Expected 'relay', 'local', or 'mx'."); - config.new_parse_error(("queue.route", id, "type"), details); - None + throttle.remote.push(limiter); + } } + + throttle + } + + async fn parse_outbound(bp: &mut Bootstrap) -> QueueRateLimiters { + // Parse throttle + let mut throttle = QueueRateLimiters::default(); + + for obj in bp.list_infallible::().await { + if !bp.validate(obj.id, &obj.object) || !obj.object.enable { + continue; + } + + let limiter = QueueRateLimiter { + expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()).default, + id: obj.object.name, + keys: obj + .object + .key + .iter() + .map(|key| match key { + enums::MtaOutboundThrottleKey::RcptDomain => THROTTLE_RCPT_DOMAIN, + enums::MtaOutboundThrottleKey::Sender => THROTTLE_SENDER, + enums::MtaOutboundThrottleKey::SenderDomain => THROTTLE_SENDER_DOMAIN, + enums::MtaOutboundThrottleKey::Mx => THROTTLE_MX, + enums::MtaOutboundThrottleKey::RemoteIp => THROTTLE_REMOTE_IP, + enums::MtaOutboundThrottleKey::LocalIp => THROTTLE_LOCAL_IP, + }) + .fold(0, |acc, key| acc | key), + rate: obj.object.rate, + }; + if (limiter.keys & (THROTTLE_MX | THROTTLE_REMOTE_IP | THROTTLE_LOCAL_IP)) != 0 + || limiter.expr.items().iter().any(|c| { + matches!( + c, + ExpressionItem::Variable( + ExpressionVariable::Mx + | ExpressionVariable::RemoteIp + | ExpressionVariable::LocalIp + ) + ) + }) + { + throttle.remote.push(limiter); + } else if (limiter.keys & (THROTTLE_RCPT_DOMAIN)) != 0 + || limiter + .expr + .items() + .iter() + .any(|c| matches!(c, ExpressionItem::Variable(ExpressionVariable::RcptDomain))) + { + throttle.rcpt.push(limiter); + } else { + throttle.sender.push(limiter); + } + } + + throttle } } -fn parse_tls_strategies(bp: &mut Bootstrap) -> AHashMap { - let mut entries = AHashMap::new(); - for key in config.sub_keys_with_suffixes( - "queue.tls", - &[ - ".allow-invalid-certs", - ".dane", - ".starttls", - ".timeout.tls", - ".timeout.mta-sts", - ], - ) { - if let Some(strategy) = parse_tls(config, &key) { - entries.insert(key, strategy); - } - } - entries -} - -fn parse_tls(bp: &mut Bootstrap, id: &str) -> Option { - Some(TlsStrategy { - dane: config - .property::(("queue.tls", id, "dane")) - .unwrap_or(RequireOptional::Optional), - mta_sts: config - .property::(("queue.tls", id, "mta-sts")) - .unwrap_or(RequireOptional::Optional), - tls: config - .property::(("queue.tls", id, "starttls")) - .unwrap_or(RequireOptional::Optional), - allow_invalid_certs: config - .property::(("queue.tls", id, "allow-invalid-certs")) - .unwrap_or(false), - timeout_tls: config - .property::(("queue.tls", id, "timeout.tls")) - .unwrap_or(Duration::from_secs(3 * 60)), - timeout_mta_sts: config - .property::(("queue.tls", id, "timeout.mta-sts")) - .unwrap_or(Duration::from_secs(5 * 60)), - }) -} - -fn parse_connection_strategies(bp: &mut Bootstrap) -> AHashMap { - let mut entries = AHashMap::new(); - for key in config.sub_keys_with_suffixes( - "queue.connection", - &[ - ".timeout.connect", - ".timeout.greeting", - ".timeout.ehlo", - ".timeout.mail-from", - ".timeout.rcpt-to", - ".timeout.data", - ".ehlo-hostname", - ], - ) { - if let Some(strategy) = parse_connection(config, &key) { - entries.insert(key, strategy); - } - } - entries -} - -fn parse_connection(bp: &mut Bootstrap, id: &str) -> Option { - let mut source_ipv4 = Vec::new(); - let mut source_ipv6 = Vec::new(); - - for (_, ip) in config.properties::(("queue.connection", id, "source-ips")) { - let ip_and_host = IpAndHost { - ip, - host: config.property::(("queue.source-ip", ip.to_string(), "ehlo-hostname")), +impl QueueQuotas { + async fn parse(bp: &mut Bootstrap) -> QueueQuotas { + let mut capacities = QueueQuotas { + sender: Vec::new(), + rcpt: Vec::new(), + rcpt_domain: Vec::new(), }; - if ip.is_ipv4() { - source_ipv4.push(ip_and_host); - } else { - source_ipv6.push(ip_and_host); - } - } + for obj in bp.list_infallible::().await { + if !bp.validate(obj.id, &obj.object) || !obj.object.enable { + continue; + } - Some(ConnectionStrategy { - source_ipv4, - source_ipv6, - ehlo_hostname: config.property::(("queue.connection", id, "ehlo-hostname")), - timeout_connect: config - .property::(("queue.connection", id, "timeout.connect")) - .unwrap_or(Duration::from_secs(5 * 60)), - timeout_greeting: config - .property::(("queue.connection", id, "timeout.greeting")) - .unwrap_or(Duration::from_secs(5 * 60)), - timeout_ehlo: config - .property::(("queue.connection", id, "timeout.ehlo")) - .unwrap_or(Duration::from_secs(5 * 60)), - timeout_mail: config - .property::(("queue.connection", id, "timeout.mail-from")) - .unwrap_or(Duration::from_secs(5 * 60)), - timeout_rcpt: config - .property::(("queue.connection", id, "timeout.rcpt-to")) - .unwrap_or(Duration::from_secs(5 * 60)), - timeout_data: config - .property::(("queue.connection", id, "timeout.data")) - .unwrap_or(Duration::from_secs(10 * 60)), - }) -} + let quota = QueueQuota { + expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()).default, + id: obj.object.name, + keys: obj + .object + .key + .iter() + .map(|key| match key { + enums::MtaQueueQuotaKey::Rcpt => THROTTLE_RCPT, + enums::MtaQueueQuotaKey::RcptDomain => THROTTLE_RCPT_DOMAIN, + enums::MtaQueueQuotaKey::Sender => THROTTLE_SENDER, + enums::MtaQueueQuotaKey::SenderDomain => THROTTLE_SENDER_DOMAIN, + }) + .fold(0, |acc, key| acc | key), + size: obj.object.size, + messages: obj.object.messages, + }; -fn parse_inbound_rate_limiters(bp: &mut Bootstrap) -> QueueRateLimiters { - let mut throttle = QueueRateLimiters::default(); - let all_throttles = parse_queue_rate_limiter( - config, - "queue.limiter.inbound", - &TokenMap::default().with_variables(SMTP_RCPT_TO_VARS), - THROTTLE_LISTENER - | THROTTLE_REMOTE_IP - | THROTTLE_LOCAL_IP - | THROTTLE_AUTH_AS - | THROTTLE_HELO_DOMAIN - | THROTTLE_RCPT - | THROTTLE_RCPT_DOMAIN - | THROTTLE_SENDER - | THROTTLE_SENDER_DOMAIN, - ); - for t in all_throttles { - if (t.keys & (THROTTLE_RCPT | THROTTLE_RCPT_DOMAIN)) != 0 - || t.expr.items().iter().any(|c| { - matches!( - c, - ExpressionItem::Variable( - ExpressionVariable::Rcpt | ExpressionVariable::RcptDomain - ) - ) - }) - { - throttle.rcpt.push(t); - } else if (t.keys - & (THROTTLE_SENDER | THROTTLE_SENDER_DOMAIN | THROTTLE_HELO_DOMAIN | THROTTLE_AUTH_AS)) - != 0 - || t.expr.items().iter().any(|c| { - matches!( - c, - ExpressionItem::Variable( - ExpressionVariable::Sender - | ExpressionVariable::SenderDomain - | ExpressionVariable::HeloDomain - | ExpressionVariable::AuthenticatedAs - ) - ) - }) - { - throttle.sender.push(t); - } else { - throttle.remote.push(t); - } - } - - throttle -} - -fn parse_outbound_rate_limiters(bp: &mut Bootstrap) -> QueueRateLimiters { - // Parse throttle - let mut throttle = QueueRateLimiters::default(); - - let all_throttles = parse_queue_rate_limiter( - config, - "queue.limiter.outbound", - &TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS), - THROTTLE_RCPT_DOMAIN - | THROTTLE_SENDER - | THROTTLE_SENDER_DOMAIN - | THROTTLE_MX - | THROTTLE_REMOTE_IP - | THROTTLE_LOCAL_IP, - ); - 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( - 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(ExpressionVariable::RcptDomain))) - { - throttle.rcpt.push(t); - } else { - throttle.sender.push(t); - } - } - - throttle -} - -fn parse_queue_quota(bp: &mut Bootstrap) -> QueueQuotas { - let mut capacities = QueueQuotas { - sender: Vec::new(), - rcpt: Vec::new(), - rcpt_domain: Vec::new(), - }; - - for quota_id in config.sub_keys("queue.quota", "") { - if let Some(quota) = parse_queue_quota_item(config, ("queue.quota", "a_id), "a_id) { if (quota.keys & THROTTLE_RCPT) != 0 || quota .expr @@ -684,92 +583,8 @@ fn parse_queue_quota(bp: &mut Bootstrap) -> QueueQuotas { capacities.sender.push(quota); } } - } - capacities -} - -fn parse_queue_quota_item(bp: &mut Bootstrap, prefix: impl AsKey, id: &str) -> Option { - let prefix = prefix.as_key(); - - // Skip disabled throttles - if !config - .property::((prefix.as_str(), "enable")) - .unwrap_or(true) - { - return None; - } - - let mut keys = 0; - for (key_, value) in config - .values((&prefix, "key")) - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect::>() - { - match parse_queue_rate_limiter_key(&value) { - Ok(key) => { - if (key - & (THROTTLE_RCPT_DOMAIN - | THROTTLE_RCPT - | THROTTLE_SENDER - | THROTTLE_SENDER_DOMAIN)) - != 0 - { - keys |= key; - } else { - let err = format!("Quota key {value:?} is not available in this context"); - config.new_build_error(key_, err); - } - } - Err(err) => { - config.new_parse_error(key_, err); - } - } - } - - let quota = QueueQuota { - id: id.to_string(), - expr: Expression::try_parse( - config, - (prefix.as_str(), "match"), - &TokenMap::default().with_variables(SMTP_QUEUE_HOST_VARS), - ) - .unwrap_or_default(), - keys, - size: config - .property::>((prefix.as_str(), "size")) - .filter(|&v| v.as_ref().is_some_and(|v| *v > 0)) - .unwrap_or_default(), - messages: config - .property::>((prefix.as_str(), "messages")) - .filter(|&v| v.as_ref().is_some_and(|v| *v > 0)) - .unwrap_or_default(), - }; - - // Validate - if quota.size.is_none() && quota.messages.is_none() { - config.new_parse_error( - prefix.as_str(), - concat!( - "Queue quota needs to define a ", - "valid 'size' and/or 'messages' property." - ) - .to_string(), - ); - None - } else { - Some(quota) - } -} - -impl ParseValue for RequireOptional { - fn parse_value(value: &str) -> Result { - match value { - "optional" => Ok(RequireOptional::Optional), - "require" | "required" => Ok(RequireOptional::Require), - "disable" | "disabled" | "none" | "false" => Ok(RequireOptional::Disable), - _ => Err(format!("Invalid TLS option value {:?}.", value,)), - } + capacities } } diff --git a/crates/common/src/config/smtp/report.rs b/crates/common/src/config/smtp/report.rs index 739f08a5..31c72a58 100644 --- a/crates/common/src/config/smtp/report.rs +++ b/crates/common/src/config/smtp/report.rs @@ -5,10 +5,17 @@ */ use super::*; -use crate::expr::{Constant, Variable, if_block::IfBlock, tokenizer::TokenMap}; -use registry::schema::enums::ExpressionConstant; +use crate::expr::{Variable, if_block::IfBlock}; +use registry::schema::{ + enums::ExpressionConstant, + prelude::Object, + structs::{ + DataRetention, DkimReportSettings, DmarcReportSettings, ReportSettings, SpfReportSettings, + TlsReportSettings, + }, +}; use std::time::Duration; -use utils::config::{Config, utils::ParseValue}; +use utils::config::utils::ParseValue; #[derive(Clone)] pub struct ReportConfig { @@ -66,152 +73,142 @@ pub enum AggregateFrequency { } impl ReportConfig { - pub fn parse(bp: &mut Bootstrap) -> Self { - let sender_vars = TokenMap::default().with_variables(SMTP_MAIL_FROM_VARS); - let rcpt_vars = TokenMap::default().with_variables(SMTP_RCPT_TO_VARS); + pub async fn parse(bp: &mut Bootstrap) -> Self { + let report = bp.setting_infallible::().await; + let dkim = bp.setting_infallible::().await; + let spf = bp.setting_infallible::().await; + let dmarc = bp.setting_infallible::().await; + let tls = bp.setting_infallible::().await; + let dr = bp.setting_infallible::().await; - Self { - submitter: IfBlock::try_parse( - config, - "report.submitter", - &TokenMap::default().with_variables(RCPT_DOMAIN_VARS), - ) - .unwrap_or_else(|| { - IfBlock::new_default("report.submitter", [], "config_get('server.hostname')") - }), + ReportConfig { + submitter: bp.compile_expr( + Object::ReportSettings.singleton(), + &report.ctx_outbound_report_submitter(), + ), analysis: ReportAnalysis { - addresses: config - .properties::("report.analysis.addresses") - .into_iter() - .map(|(_, m)| m) + addresses: report + .inbound_report_addresses + .iter() + .filter_map(|addr| AddressMatch::parse_value(addr).ok()) .collect(), - forward: config.property("report.analysis.forward").unwrap_or(true), - store: config - .property_or_default::>("report.analysis.store", "30d") - .unwrap_or_default(), + forward: report.inbound_report_forwarding, + store: dr.hold_mta_reports_for.map(|d| d.into_inner()), }, - dkim: Report::parse(config, "dkim", &rcpt_vars), - spf: Report::parse(config, "spf", &sender_vars), - dmarc: Report::parse(config, "dmarc", &rcpt_vars), - dmarc_aggregate: AggregateReport::parse( - config, - "dmarc", - &rcpt_vars.with_constants::(), - ), - tls: AggregateReport::parse( - config, - "tls", - &TokenMap::default() - .with_variables(SMTP_QUEUE_HOST_VARS) - .with_constants::(), - ), - } - } -} - -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( - format!("report.{id}.from-address"), - [], - format!("'noreply-{id}@' + config_get('report.domain')"), - ), - subject: IfBlock::new_default( - format!("report.{id}.subject"), - [], - format!( - "'{} Authentication Failure Report'", - id.to_ascii_uppercase() + dkim: Report { + name: bp.compile_expr( + Object::DkimReportSettings.singleton(), + &dkim.ctx_from_name(), ), - ), - 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]"), - }; - for (value, key) in [ - (&mut report.name, "from-name"), - (&mut report.address, "from-address"), - (&mut report.subject, "subject"), - (&mut report.sign, "sign"), - (&mut report.send, "send"), - ] { - if let Some(if_block) = IfBlock::try_parse(config, ("report", id, key), token_map) { - *value = if_block; - } + address: bp.compile_expr( + Object::DkimReportSettings.singleton(), + &dkim.ctx_from_address(), + ), + subject: bp + .compile_expr(Object::DkimReportSettings.singleton(), &dkim.ctx_subject()), + sign: bp.compile_expr( + Object::DkimReportSettings.singleton(), + &dkim.ctx_dkim_sign_domain(), + ), + send: bp.compile_expr( + Object::DkimReportSettings.singleton(), + &dkim.ctx_send_frequency(), + ), + }, + spf: Report { + name: bp.compile_expr(Object::SpfReportSettings.singleton(), &spf.ctx_from_name()), + address: bp.compile_expr( + Object::SpfReportSettings.singleton(), + &spf.ctx_from_address(), + ), + subject: bp.compile_expr(Object::SpfReportSettings.singleton(), &spf.ctx_subject()), + sign: bp.compile_expr( + Object::SpfReportSettings.singleton(), + &spf.ctx_dkim_sign_domain(), + ), + send: bp.compile_expr( + Object::SpfReportSettings.singleton(), + &spf.ctx_send_frequency(), + ), + }, + dmarc: Report { + name: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_failure_from_name(), + ), + address: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_failure_from_address(), + ), + subject: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_failure_subject(), + ), + sign: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_failure_dkim_sign_domain(), + ), + send: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_failure_send_frequency(), + ), + }, + dmarc_aggregate: AggregateReport { + name: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_aggregate_from_name(), + ), + address: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_aggregate_from_address(), + ), + org_name: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_aggregate_org_name(), + ), + contact_info: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_aggregate_contact_info(), + ), + send: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_aggregate_send_frequency(), + ), + sign: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_aggregate_dkim_sign_domain(), + ), + max_size: bp.compile_expr( + Object::DmarcReportSettings.singleton(), + &dmarc.ctx_aggregate_max_report_size(), + ), + }, + tls: AggregateReport { + name: bp.compile_expr(Object::TlsReportSettings.singleton(), &tls.ctx_from_name()), + address: bp.compile_expr( + Object::TlsReportSettings.singleton(), + &tls.ctx_from_address(), + ), + org_name: bp + .compile_expr(Object::TlsReportSettings.singleton(), &tls.ctx_org_name()), + contact_info: bp.compile_expr( + Object::TlsReportSettings.singleton(), + &tls.ctx_contact_info(), + ), + send: bp.compile_expr( + Object::TlsReportSettings.singleton(), + &tls.ctx_send_frequency(), + ), + sign: bp.compile_expr( + Object::TlsReportSettings.singleton(), + &tls.ctx_dkim_sign_domain(), + ), + max_size: bp.compile_expr( + Object::TlsReportSettings.singleton(), + &tls.ctx_max_report_size(), + ), + }, } - - report - } -} - -impl AggregateReport { - pub fn parse(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Self { - let rcpt_vars = TokenMap::default().with_variables(RCPT_DOMAIN_VARS); - - let mut report = Self { - name: IfBlock::new_default( - format!("report.{id}.aggregate.from-name"), - [], - format!("'{} Aggregate Report'", id.to_ascii_uppercase()), - ), - address: IfBlock::new_default( - format!("report.{id}.aggregate.from-address"), - [], - format!("'noreply-{id}@' + config_get('report.domain')"), - ), - org_name: IfBlock::new_default( - format!("report.{id}.aggregate.org-name"), - [], - "config_get('report.domain')", - ), - contact_info: IfBlock::empty(format!("report.{id}.aggregate.contact-info")), - send: IfBlock::new_default::( - format!("report.{id}.aggregate.send"), - [], - "daily", - ), - sign: IfBlock::new_default( - format!("report.{id}.aggregate.sign"), - [], - "['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]", - ), - max_size: IfBlock::new_default( - format!("report.{id}.aggregate.max-size"), - [], - "26214400", - ), - }; - - for (value, key, token_map) in [ - (&mut report.name, "aggregate.from-name", &rcpt_vars), - (&mut report.address, "aggregate.from-address", &rcpt_vars), - (&mut report.org_name, "aggregate.org-name", &rcpt_vars), - ( - &mut report.contact_info, - "aggregate.contact-info", - &rcpt_vars, - ), - (&mut report.send, "aggregate.send", token_map), - (&mut report.sign, "aggregate.sign", &rcpt_vars), - (&mut report.max_size, "aggregate.max-size", &rcpt_vars), - ] { - if let Some(if_block) = IfBlock::try_parse(config, ("report", id, key), token_map) { - *value = if_block; - } - } - - report - } -} - -impl Default for ReportConfig { - fn default() -> Self { - Self::parse(&mut Config::default()) } } diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index 86e70b65..f46c576a 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -4,13 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{ - fmt::Display, - hash::{DefaultHasher, Hash, Hasher}, - net::{IpAddr, Ipv4Addr, SocketAddr}, - time::Duration, -}; - +use crate::{Server, manager::bootstrap::Bootstrap}; use mail_auth::{ MessageAuthenticator, hickory_resolver::{ @@ -20,13 +14,19 @@ use mail_auth::{ system_conf::read_system_conf, }, }; -use serde::{Deserialize, Serialize}; -use utils::{ - cache::CacheItemWeight, - config::{Config, utils::ParseValue}, +use registry::schema::{ + enums::{DnsResolverProtocol, PolicyEnforcement}, + prelude::Object, + structs::{DnsResolver, MtaSts}, }; - -use crate::Server; +use serde::{Deserialize, Serialize}; +use std::{ + fmt::Display, + hash::{DefaultHasher, Hash, Hasher}, + net::SocketAddr, + sync::Arc, +}; +use utils::{cache::CacheItemWeight, config::utils::ParseValue}; pub struct Resolvers { pub dns: MessageAuthenticator, @@ -103,124 +103,89 @@ impl CacheItemWeight for Policy { impl Resolvers { pub async fn parse(bp: &mut Bootstrap) -> Self { - let (resolver_config, mut opts) = match config.value("resolver.type").unwrap_or("system") { - "cloudflare" => (ResolverConfig::cloudflare(), ResolverOpts::default()), - "cloudflare-tls" => (ResolverConfig::cloudflare_tls(), ResolverOpts::default()), - "quad9" => (ResolverConfig::quad9(), ResolverOpts::default()), - "quad9-tls" => (ResolverConfig::quad9_tls(), ResolverOpts::default()), - "google" => (ResolverConfig::google(), ResolverOpts::default()), - "system" => read_system_conf() - .map_err(|err| { - config.new_build_error( - "resolver.type", + let mut resolver_config: ResolverConfig; + let mut opts = ResolverOpts::default(); + + match bp.setting_infallible::().await { + DnsResolver::System(resolver) => match read_system_conf() { + Ok((config, options)) => { + resolver_config = config; + opts = options; + opts.num_concurrent_reqs = resolver.concurrency as usize; + opts.timeout = resolver.timeout.into_inner(); + opts.preserve_intermediates = resolver.preserve_intermediates; + opts.try_tcp_on_error = resolver.tcp_on_error; + opts.attempts = resolver.attempts as usize; + opts.edns0 = resolver.enable_edns; + } + Err(err) => { + bp.build_error( + Object::DnsResolver.singleton(), format!("Failed to read system DNS config: {err}"), - ) - }) - .unwrap_or_else(|_| (ResolverConfig::cloudflare(), ResolverOpts::default())), - "custom" => { - let mut resolver_config = ResolverConfig::default(); - for url in config - .values("resolver.custom") - .map(|(_, v)| v.to_string()) - .collect::>() - { - let (proto, host) = if let Some((proto, host)) = url - .split_once("://") - .map(|(a, b)| (a.to_string(), b.to_string())) - { - ( - match proto.as_str() { - "udp" => ProtocolConfig::Udp, - "tcp" => ProtocolConfig::Tcp, - "tls" => ProtocolConfig::Tls { - server_name: host.clone().into(), - }, - _ => { - config.new_parse_error( - "resolver.custom", - format!("Invalid custom resolver protocol {url:?}"), - ); - ProtocolConfig::Udp - } - }, - host.to_string(), - ) - } else { - (ProtocolConfig::Udp, url) - }; - - let (host, port) = if let Some(host) = host.strip_prefix('[') { - let (host, maybe_port) = host.rsplit_once(']').unwrap_or_default(); - - ( - host, - maybe_port - .rsplit_once(':') - .map(|(_, port)| port) - .unwrap_or("53"), - ) - } else if let Some((host, port)) = host.split_once(':') { - (host, port) - } else { - (host.as_str(), "53") - }; - - let port = port - .parse::() - .map_err(|err| { - config.new_parse_error( - "resolver.custom", - format!("Invalid custom resolver port {port:?}: {err}"), - ); - }) - .unwrap_or(53); - - let host = host - .parse::() - .map_err(|err| { - config.new_parse_error( - "resolver.custom", - format!("Invalid custom resolver IP {host:?}: {err}"), - ) - }) - .unwrap_or(IpAddr::V4(Ipv4Addr::new(8, 8, 8, 8))); - resolver_config - .add_name_server(NameServerConfig::new(SocketAddr::new(host, port), proto)); - } - if !resolver_config.name_servers().is_empty() { - (resolver_config, ResolverOpts::default()) - } else { - config.new_parse_error( - "resolver.custom", - "At least one custom resolver must be specified.", ); - (ResolverConfig::cloudflare(), ResolverOpts::default()) + resolver_config = ResolverConfig::cloudflare(); } + }, + DnsResolver::Custom(resolver) => { + resolver_config = ResolverConfig::default(); + + for server in resolver.servers { + resolver_config.add_name_server(NameServerConfig::new( + SocketAddr::new(server.address.into_inner(), server.port as u16), + match server.protocol { + DnsResolverProtocol::Udp => ProtocolConfig::Udp, + DnsResolverProtocol::Tcp => ProtocolConfig::Tcp, + DnsResolverProtocol::Tls => ProtocolConfig::Tls { + server_name: Arc::from(server.address.to_string()), + }, + }, + )); + } + + opts.num_concurrent_reqs = resolver.concurrency as usize; + opts.timeout = resolver.timeout.into_inner(); + opts.preserve_intermediates = resolver.preserve_intermediates; + opts.try_tcp_on_error = resolver.tcp_on_error; + opts.attempts = resolver.attempts as usize; + opts.edns0 = resolver.enable_edns; } - other => { - let err = format!("Unknown resolver type {other:?}."); - config.new_parse_error("resolver.custom", err); - (ResolverConfig::cloudflare(), ResolverOpts::default()) + DnsResolver::Cloudflare(resolver) => { + resolver_config = if resolver.use_tls { + ResolverConfig::cloudflare_tls() + } else { + ResolverConfig::cloudflare() + }; + + opts.num_concurrent_reqs = resolver.concurrency as usize; + opts.timeout = resolver.timeout.into_inner(); + opts.preserve_intermediates = resolver.preserve_intermediates; + opts.try_tcp_on_error = resolver.tcp_on_error; + opts.attempts = resolver.attempts as usize; + opts.edns0 = resolver.enable_edns; + } + DnsResolver::Quad9(resolver) => { + resolver_config = if resolver.use_tls { + ResolverConfig::quad9_tls() + } else { + ResolverConfig::quad9() + }; + opts.num_concurrent_reqs = resolver.concurrency as usize; + opts.timeout = resolver.timeout.into_inner(); + opts.preserve_intermediates = resolver.preserve_intermediates; + opts.try_tcp_on_error = resolver.tcp_on_error; + opts.attempts = resolver.attempts as usize; + opts.edns0 = resolver.enable_edns; + } + DnsResolver::Google(resolver) => { + resolver_config = ResolverConfig::google(); + opts.num_concurrent_reqs = resolver.concurrency as usize; + opts.timeout = resolver.timeout.into_inner(); + opts.preserve_intermediates = resolver.preserve_intermediates; + opts.try_tcp_on_error = resolver.tcp_on_error; + opts.attempts = resolver.attempts as usize; + opts.edns0 = resolver.enable_edns; } - }; - if let Some(concurrency) = config.property("resolver.concurrency") { - opts.num_concurrent_reqs = concurrency; } - if let Some(timeout) = config.property("resolver.timeout") { - opts.timeout = timeout; - } - if let Some(preserve) = config.property("resolver.preserve-intermediates") { - opts.preserve_intermediates = preserve; - } - if let Some(try_tcp_on_error) = config.property("resolver.try-tcp-on-error") { - opts.try_tcp_on_error = try_tcp_on_error; - } - if let Some(attempts) = config.property("resolver.attempts") { - opts.attempts = attempts; - } - opts.edns0 = config - .property_or_default("resolver.edns", "true") - .unwrap_or(true); // We already have a cache, so disable the built-in cache opts.cache_size = 0; @@ -245,37 +210,37 @@ impl Resolvers { } impl Policy { - pub fn try_parse(bp: &mut Bootstrap) -> Option { - let mode = config - .property_or_default::>("session.mta-sts.mode", "testing") - .unwrap_or_default()?; - let max_age = config - .property_or_default::("session.mta-sts.max-age", "7d") - .unwrap_or_else(|| Duration::from_secs(604800)) - .as_secs(); - let mut mx = Vec::new(); + pub async fn try_parse(bp: &mut Bootstrap) -> Option { + let mta = bp.setting_infallible::().await; + if !mta.mx_hosts.is_empty() { + let mut policy = Policy { + id: Default::default(), + mode: match mta.mode { + PolicyEnforcement::Enforce => Mode::Enforce, + PolicyEnforcement::Testing => Mode::Testing, + PolicyEnforcement::Disable => Mode::None, + }, + mx: mta + .mx_hosts + .into_iter() + .map(|mx| { + if let Some(mx) = mx.strip_prefix("*.") { + MxPattern::StartsWith(mx.to_string()) + } else { + MxPattern::Equals(mx) + } + }) + .collect(), + max_age: mta.max_age.into_inner().as_secs(), + }; - for (_, item) in config.values("session.mta-sts.mx") { - if let Some(item) = item.strip_prefix("*.") { - mx.push(MxPattern::StartsWith(item.to_string())); - } else { - mx.push(MxPattern::Equals(item.to_string())); - } - } - - let mut policy = Self { - id: Default::default(), - mode, - mx, - max_age, - }; - - if !policy.mx.is_empty() { policy.mx.sort_unstable(); policy.id = policy.hash().to_string(); - } - policy.into() + Some(policy) + } else { + None + } } pub fn try_build(mut self, names: I) -> Option diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index 2cdd24a6..6fa036a9 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -4,28 +4,25 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use self::resolver::Policy; +use super::*; +use crate::expr::if_block::IfBlock; +use ahash::AHashSet; +use hyper::HeaderMap; +use registry::schema::{ + enums::{self, ExpressionConstant, MtaStage}, + prelude::Object, + structs::{ + HttpAuth, MtaExtensions, MtaHook, MtaInboundSession, MtaMilter, MtaStageAuth, + MtaStageConnect, MtaStageData, MtaStageEhlo, MtaStageMail, MtaStageRcpt, + }, +}; +use smtp_proto::*; use std::{ net::{SocketAddr, ToSocketAddrs}, - str::FromStr, time::Duration, }; - -use ahash::AHashSet; -use base64::{Engine, engine::general_purpose::STANDARD}; - -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::expr::{if_block::IfBlock, tokenizer::TokenMap, *}; - -use self::resolver::Policy; - -use super::*; +use utils::config::{http::build_http_headers, utils::ParseValue}; #[derive(Clone)] pub struct SessionConfig { @@ -76,7 +73,6 @@ pub struct Extensions { #[derive(Clone)] pub struct Auth { - pub directory: IfBlock, pub mechanisms: IfBlock, pub require: IfBlock, pub must_match_sender: IfBlock, @@ -95,19 +91,11 @@ pub struct Mail { pub struct Rcpt { pub script: IfBlock, pub relay: IfBlock, - pub directory: IfBlock, + pub is_local: IfBlock, pub rewrite: IfBlock, - - // Errors pub errors_max: IfBlock, pub errors_wait: IfBlock, - - // Limits pub max_recipients: IfBlock, - - // Catch-all and sub-addressing - pub catch_all: AddressMapping, - pub subaddressing: AddressMapping, } #[derive(Debug, Default, Clone)] @@ -122,13 +110,9 @@ pub enum AddressMapping { pub struct Data { pub script: IfBlock, pub spam_filter: IfBlock, - - // Limits pub max_messages: IfBlock, pub max_message_size: IfBlock, pub max_received_headers: IfBlock, - - // Headers pub add_received: IfBlock, pub add_received_spf: IfBlock, pub add_return_path: IfBlock, @@ -188,627 +172,241 @@ pub enum Stage { } impl SessionConfig { - pub fn parse(bp: &mut Bootstrap) -> Self { - let has_conn_vars = TokenMap::default().with_variables(CONNECTION_VARS); - let has_ehlo_hars = TokenMap::default().with_variables(SMTP_EHLO_VARS); - let has_sender_vars = TokenMap::default().with_variables(SMTP_MAIL_FROM_VARS); - let has_rcpt_vars = TokenMap::default().with_variables(SMTP_RCPT_TO_VARS); - let mt_priority_vars = has_sender_vars.clone().with_constants::(); - let mechanisms_vars = has_ehlo_hars.clone().with_constants::(); + pub async fn parse(bp: &mut Bootstrap) -> Self { + let session = bp.setting_infallible::().await; + let connect = bp.setting_infallible::().await; + let auth = bp.setting_infallible::().await; + let ehlo = bp.setting_infallible::().await; + let mail = bp.setting_infallible::().await; + let rcpt = bp.setting_infallible::().await; + let data = bp.setting_infallible::().await; + let ext = bp.setting_infallible::().await; - let mut session = SessionConfig::default(); - session.rcpt.catch_all = AddressMapping::parse(config, "session.rcpt.catch-all"); - session.rcpt.subaddressing = AddressMapping::parse(config, "session.rcpt.sub-addressing"); - session.milters = config - .sub_keys("session.milter", ".hostname") - .into_iter() - .filter_map(|id| parse_milter(config, &id, &has_rcpt_vars)) - .collect(); - session.hooks = config - .sub_keys("session.hook", ".url") - .into_iter() - .filter_map(|id| parse_hooks(config, &id, &has_rcpt_vars)) - .collect(); - session.mta_sts_policy = Policy::try_parse(config); - - for (value, key, token_map) in [ - (&mut session.duration, "session.duration", &has_conn_vars), - ( - &mut session.transfer_limit, - "session.transfer-limit", - &has_conn_vars, + SessionConfig { + timeout: bp.compile_expr( + Object::MtaInboundSession.singleton(), + &session.ctx_timeout(), ), - (&mut session.timeout, "session.timeout", &has_conn_vars), - ( - &mut session.connect.script, - "session.connect.script", - &has_conn_vars, + duration: bp.compile_expr( + Object::MtaInboundSession.singleton(), + &session.ctx_max_duration(), ), - ( - &mut session.connect.hostname, - "session.connect.hostname", - &has_conn_vars, + transfer_limit: bp.compile_expr( + Object::MtaInboundSession.singleton(), + &session.ctx_transfer_limit(), ), - ( - &mut session.connect.greeting, - "session.connect.greeting", - &has_conn_vars, - ), - ( - &mut session.extensions.pipelining, - "session.extensions.pipelining", - &has_sender_vars, - ), - ( - &mut session.extensions.dsn, - "session.extensions.dsn", - &has_sender_vars, - ), - ( - &mut session.extensions.vrfy, - "session.extensions.vrfy", - &has_sender_vars, - ), - ( - &mut session.extensions.expn, - "session.extensions.expn", - &has_sender_vars, - ), - ( - &mut session.extensions.chunking, - "session.extensions.chunking", - &has_sender_vars, - ), - ( - &mut session.extensions.requiretls, - "session.extensions.requiretls", - &has_sender_vars, - ), - ( - &mut session.extensions.no_soliciting, - "session.extensions.no-soliciting", - &has_sender_vars, - ), - ( - &mut session.extensions.future_release, - "session.extensions.future-release", - &has_sender_vars, - ), - ( - &mut session.extensions.deliver_by, - "session.extensions.deliver-by", - &has_sender_vars, - ), - ( - &mut session.extensions.mt_priority, - "session.extensions.mt-priority", - &mt_priority_vars, - ), - ( - &mut session.ehlo.script, - "session.ehlo.script", - &has_conn_vars, - ), - ( - &mut session.ehlo.require, - "session.ehlo.require", - &has_conn_vars, - ), - ( - &mut session.ehlo.reject_non_fqdn, - "session.ehlo.reject-non-fqdn", - &has_conn_vars, - ), - ( - &mut session.auth.directory, - "session.auth.directory", - &has_ehlo_hars, - ), - ( - &mut session.auth.mechanisms, - "session.auth.mechanisms", - &mechanisms_vars, - ), - ( - &mut session.auth.require, - "session.auth.require", - &has_ehlo_hars, - ), - ( - &mut session.auth.errors_max, - "session.auth.errors.total", - &has_ehlo_hars, - ), - ( - &mut session.auth.errors_wait, - "session.auth.errors.wait", - &has_ehlo_hars, - ), - ( - &mut session.auth.must_match_sender, - "session.auth.must-match-sender", - &has_sender_vars, - ), - ( - &mut session.mail.script, - "session.mail.script", - &has_sender_vars, - ), - ( - &mut session.mail.rewrite, - "session.mail.rewrite", - &has_sender_vars, - ), - ( - &mut session.mail.is_allowed, - "session.mail.is-allowed", - &has_sender_vars, - ), - ( - &mut session.rcpt.script, - "session.rcpt.script", - &has_rcpt_vars, - ), - ( - &mut session.rcpt.relay, - "session.rcpt.relay", - &has_rcpt_vars, - ), - ( - &mut session.rcpt.directory, - "session.rcpt.directory", - &has_rcpt_vars, - ), - ( - &mut session.rcpt.errors_max, - "session.rcpt.errors.total", - &has_sender_vars, - ), - ( - &mut session.rcpt.errors_wait, - "session.rcpt.errors.wait", - &has_sender_vars, - ), - ( - &mut session.rcpt.max_recipients, - "session.rcpt.max-recipients", - &has_sender_vars, - ), - ( - &mut session.rcpt.rewrite, - "session.rcpt.rewrite", - &has_rcpt_vars, - ), - ( - &mut session.data.script, - "session.data.script", - &has_rcpt_vars, - ), - ( - &mut session.data.max_messages, - "session.data.limits.messages", - &has_rcpt_vars, - ), - ( - &mut session.data.max_message_size, - "session.data.limits.size", - &has_rcpt_vars, - ), - ( - &mut session.data.max_received_headers, - "session.data.limits.received-headers", - &has_rcpt_vars, - ), - ( - &mut session.data.spam_filter, - "session.data.spam-filter", - &has_rcpt_vars, - ), - ( - &mut session.data.add_received, - "session.data.add-headers.received", - &has_rcpt_vars, - ), - ( - &mut session.data.add_received_spf, - "session.data.add-headers.received-spf", - &has_rcpt_vars, - ), - ( - &mut session.data.add_return_path, - "session.data.add-headers.return-path", - &has_rcpt_vars, - ), - ( - &mut session.data.add_auth_results, - "session.data.add-headers.auth-results", - &has_rcpt_vars, - ), - ( - &mut session.data.add_message_id, - "session.data.add-headers.message-id", - &has_rcpt_vars, - ), - ( - &mut session.data.add_date, - "session.data.add-headers.date", - &has_rcpt_vars, - ), - ] { - if let Some(if_block) = IfBlock::try_parse(config, key, token_map) { - *value = if_block; - } - } - session.data.add_delivered_to = config - .property_or_default("session.data.add-headers.delivered-to", "true") - .unwrap_or(true); - session - } -} - -fn parse_milter(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Option { - let hostname = config - .value_require(("session.milter", id, "hostname"))? - .to_string(); - let port = config.property_require(("session.milter", id, "port"))?; - Some(Milter { - enable: IfBlock::try_parse(config, ("session.milter", id, "enable"), token_map) - .unwrap_or_else(|| { - IfBlock::new_default(format!("session.milter.{id}.enable"), [], "false") - }), - id: Arc::new(id.into()), - addrs: format!("{}:{}", hostname, port) - .to_socket_addrs() - .map_err(|err| { - config.new_build_error( - ("session.milter", id, "hostname"), - format!("Unable to resolve milter hostname {hostname}: {err}"), - ) - }) - .ok()? - .collect(), - hostname, - port, - timeout_connect: config - .property_or_default(("session.milter", id, "timeout.connect"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)), - timeout_command: config - .property_or_default(("session.milter", id, "timeout.command"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)), - timeout_data: config - .property_or_default(("session.milter", id, "timeout.data"), "60s") - .unwrap_or_else(|| Duration::from_secs(60)), - tls: config - .property_or_default(("session.milter", id, "tls"), "false") - .unwrap_or_default(), - tls_allow_invalid_certs: config - .property_or_default(("session.milter", id, "allow-invalid-certs"), "false") - .unwrap_or_default(), - tempfail_on_error: config - .property_or_default(("session.milter", id, "options.tempfail-on-error"), "true") - .unwrap_or(true), - max_frame_len: config - .property_or_default( - ("session.milter", id, "options.max-response-size"), - "52428800", - ) - .unwrap_or(52428800), - protocol_version: match config - .property_or_default::(("session.milter", id, "options.version"), "6") - .unwrap_or(6) - { - 6 => MilterVersion::V6, - 2 => MilterVersion::V2, - v => { - config.new_parse_error( - ("session.milter", id, "options.version"), - format!("Unsupported milter protocol version {v}"), - ); - MilterVersion::V6 - } - }, - flags_actions: config.property(("session.milter", id, "options.flags.actions")), - flags_protocol: config.property(("session.milter", id, "options.flags.protocol")), - run_on_stage: parse_stages(config, "session.milter", id), - }) -} - -fn parse_hooks(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Option { - let mut headers = HeaderMap::new(); - - for (header, value) in config - .values(("session.hook", id, "headers")) - .map(|(_, v)| { - if let Some((k, v)) = v.split_once(':') { - Ok(( - HeaderName::from_str(k.trim()).map_err(|err| { - format!( - "Invalid header found in property \"session.hook.{id}.headers\": {err}", - ) - })?, - HeaderValue::from_str(v.trim()).map_err(|err| { - format!( - "Invalid header found in property \"session.hook.{id}.headers\": {err}", - ) - })?, - )) - } else { - Err(format!( - "Invalid header found in property \"session.hook.{id}.headers\": {v}", - )) - } - }) - .collect::, String>>() - .map_err(|e| config.new_parse_error(("session.hook", id, "headers"), e)) - .unwrap_or_default() - { - headers.insert(header, value); - } - - headers.insert(CONTENT_TYPE, "application/json".parse().unwrap()); - if let (Some(name), Some(secret)) = ( - config.value(("session.hook", id, "auth.username")), - config.value(("session.hook", id, "auth.secret")), - ) { - headers.insert( - AUTHORIZATION, - format!("Basic {}", STANDARD.encode(format!("{}:{}", name, secret))) - .parse() - .unwrap(), - ); - } - - Some(MTAHook { - enable: IfBlock::try_parse(config, ("session.hook", id, "enable"), token_map) - .unwrap_or_else(|| { - IfBlock::new_default(format!("session.hook.{id}.enable"), [], "false") - }), - id: id.to_string(), - url: config - .value_require(("session.hook", id, "url"))? - .to_string(), - timeout: config - .property_or_default(("session.hook", id, "timeout"), "30s") - .unwrap_or_else(|| Duration::from_secs(30)), - tls_allow_invalid_certs: config - .property_or_default(("session.hook", id, "allow-invalid-certs"), "false") - .unwrap_or_default(), - tempfail_on_error: config - .property_or_default(("session.hook", id, "options.tempfail-on-error"), "true") - .unwrap_or(true), - run_on_stage: parse_stages(config, "session.hook", id), - max_response_size: config - .property_or_default( - ("session.hook", id, "options.max-response-size"), - "52428800", - ) - .unwrap_or(52428800), - headers, - }) -} - -fn parse_stages(bp: &mut Bootstrap, prefix: &str, id: &str) -> AHashSet { - let mut stages = AHashSet::default(); - let mut invalid = Vec::new(); - for (_, value) in config.values((prefix, id, "stages")) { - let value = value.to_ascii_lowercase(); - let state = match value.as_str() { - "connect" => Stage::Connect, - "ehlo" => Stage::Ehlo, - "auth" => Stage::Auth, - "mail" => Stage::Mail, - "rcpt" => Stage::Rcpt, - "data" => Stage::Data, - _ => { - invalid.push(value); - continue; - } - }; - stages.insert(state); - } - - if !invalid.is_empty() { - config.new_parse_error( - (prefix, id, "stages"), - format!("Invalid stages: {}", invalid.join(", ")), - ); - } - - if stages.is_empty() { - stages.insert(Stage::Data); - } - - stages -} - -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"), connect: Connect { - hostname: IfBlock::new_default( - "server.connect.hostname", - [], - "config_get('server.hostname')", - ), - script: IfBlock::empty("session.connect.script"), - greeting: IfBlock::new_default( - "session.connect.greeting", - [], - "config_get('server.hostname') + ' Stalwart ESMTP at your service'", + hostname: bp + .compile_expr(Object::MtaStageConnect.singleton(), &connect.ctx_hostname()), + script: bp.compile_expr(Object::MtaStageConnect.singleton(), &connect.ctx_script()), + greeting: bp.compile_expr( + Object::MtaStageConnect.singleton(), + &connect.ctx_smtp_greeting(), ), }, ehlo: Ehlo { - script: IfBlock::empty("session.ehlo.script"), - require: IfBlock::new_default("session.ehlo.require", [], "true"), - reject_non_fqdn: IfBlock::new_default( - "session.ehlo.reject-non-fqdn", - [("local_port == 25", "true")], - "false", + script: bp.compile_expr(Object::MtaStageEhlo.singleton(), &ehlo.ctx_script()), + require: bp.compile_expr(Object::MtaStageEhlo.singleton(), &ehlo.ctx_require()), + reject_non_fqdn: bp.compile_expr( + Object::MtaStageEhlo.singleton(), + &ehlo.ctx_reject_non_fqdn(), ), }, auth: Auth { - directory: IfBlock::new_default( - "session.auth.directory", - #[cfg(feature = "test_mode")] - [], - #[cfg(not(feature = "test_mode"))] - [("local_port != 25", "'*'")], - "false", + mechanisms: bp.compile_expr( + Object::MtaStageAuth.singleton(), + &auth.ctx_sasl_mechanisms(), ), - mechanisms: IfBlock::new_default::( - "session.auth.mechanisms", - [ - ( - "local_port != 25 && is_tls", - "[plain, login, oauthbearer, xoauth2]", - ), - ("local_port != 25", "[oauthbearer, xoauth2]"), - ], - "false", + require: bp.compile_expr(Object::MtaStageAuth.singleton(), &auth.ctx_require()), + must_match_sender: bp.compile_expr( + Object::MtaStageAuth.singleton(), + &auth.ctx_must_match_sender(), ), - require: IfBlock::new_default( - "session.auth.require", - #[cfg(feature = "test_mode")] - [], - #[cfg(not(feature = "test_mode"))] - [("local_port != 25", "true")], - "false", - ), - 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: bp + .compile_expr(Object::MtaStageAuth.singleton(), &auth.ctx_max_failures()), + errors_wait: bp + .compile_expr(Object::MtaStageAuth.singleton(), &auth.ctx_wait_on_fail()), }, mail: Mail { - script: IfBlock::empty("session.mail.script"), - rewrite: IfBlock::empty("session.mail.rewrite"), - is_allowed: IfBlock::new_default( - "session.mail.is-allowed", - [], - "!is_empty(authenticated_as) || !key_exists('blocked-domains', sender_domain)", + script: bp.compile_expr(Object::MtaStageMail.singleton(), &mail.ctx_script()), + rewrite: bp.compile_expr(Object::MtaStageMail.singleton(), &mail.ctx_rewrite()), + is_allowed: bp.compile_expr( + Object::MtaStageMail.singleton(), + &mail.ctx_is_sender_allowed(), ), }, rcpt: Rcpt { - script: IfBlock::empty("session.rcpt.script"), - relay: IfBlock::new_default( - "session.rcpt.relay", - [("!is_empty(authenticated_as)", "true")], - "false", - ), - directory: IfBlock::new_default( - "session.rcpt.directory", - [], - #[cfg(feature = "test_mode")] - "false", - #[cfg(not(feature = "test_mode"))] - "'*'", - ), - 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( - "session.rcpt.max-recipients", - [], - "100", - ), - catch_all: AddressMapping::Enable, - subaddressing: AddressMapping::Enable, + script: bp.compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_script()), + relay: bp + .compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_allow_relaying()), + is_local: bp.compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_is_local()), + rewrite: bp.compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_rewrite()), + errors_max: bp + .compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_max_failures()), + errors_wait: bp + .compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_wait_on_fail()), + max_recipients: bp + .compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_max_recipients()), }, 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( - "session.data.limits.size", - [], - "104857600", + script: bp.compile_expr(Object::MtaStageData.singleton(), &data.ctx_script()), + spam_filter: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_enable_spam_filter(), ), - max_received_headers: IfBlock::new_default( - "session.data.limits.received-headers", - [], - "50", + max_messages: bp + .compile_expr(Object::MtaStageData.singleton(), &data.ctx_max_messages()), + max_message_size: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_max_message_size(), ), - add_received: IfBlock::new_default( - "session.data.add-headers.received", - [("local_port == 25", "true")], - "false", + max_received_headers: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_max_received_headers(), ), - add_received_spf: IfBlock::new_default( - "session.data.add-headers.received-spf", - [("local_port == 25", "true")], - "false", + add_received: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_add_received_header(), ), - add_return_path: IfBlock::new_default( - "session.data.add-headers.return-path", - [("local_port == 25", "true")], - "false", + add_received_spf: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_add_received_spf_header(), ), - add_auth_results: IfBlock::new_default( - "session.data.add-headers.auth-results", - [("local_port == 25", "true")], - "false", + add_return_path: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_add_return_path_header(), ), - add_message_id: IfBlock::new_default( - "session.data.add-headers.message-id", - [("local_port == 25", "true")], - "false", + add_auth_results: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_add_auth_results_header(), ), - add_date: IfBlock::new_default( - "session.data.add-headers.date", - [("local_port == 25", "true")], - "false", + add_message_id: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_add_message_id_header(), ), - add_delivered_to: false, + add_date: bp.compile_expr( + Object::MtaStageData.singleton(), + &data.ctx_add_date_header(), + ), + add_delivered_to: data.add_delivered_to_header, }, 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( - "session.extensions.dsn", - [("!is_empty(authenticated_as)", "true")], - "false", - ), - vrfy: IfBlock::new_default( - "session.extensions.vrfy", - [("!is_empty(authenticated_as)", "true")], - "false", - ), - expn: IfBlock::new_default( - "session.extensions.expn", - [("!is_empty(authenticated_as)", "true")], - "false", - ), - no_soliciting: IfBlock::new_default( - "session.extensions.no-soliciting", - [], - "''", - ), - future_release: IfBlock::new_default( - "session.extensions.future-release", - [("!is_empty(authenticated_as)", "7d")], - "false", - ), - deliver_by: IfBlock::new_default( - "session.extensions.deliver-by", - [("!is_empty(authenticated_as)", "15d")], - "false", - ), - mt_priority: IfBlock::new_default::( - "session.extensions.mt-priority", - [("!is_empty(authenticated_as)", "mixer")], - "false", - ), + pipelining: bp + .compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_pipelining()), + chunking: bp.compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_chunking()), + requiretls: bp + .compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_require_tls()), + dsn: bp.compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_dsn()), + vrfy: bp.compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_vrfy()), + expn: bp.compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_expn()), + no_soliciting: bp + .compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_no_soliciting()), + future_release: bp + .compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_future_release()), + deliver_by: bp + .compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_deliver_by()), + mt_priority: bp + .compile_expr(Object::MtaExtensions.singleton(), &ext.ctx_mt_priority()), }, - mta_sts_policy: None, - milters: Default::default(), - hooks: Default::default(), + mta_sts_policy: Policy::try_parse(bp).await, + milters: bp + .list_infallible::() + .await + .into_iter() + .filter_map(|milter| { + let id = milter.id; + let milter = milter.object; + + if bp.validate(id, &milter) { + Some(Milter { + enable: bp.compile_expr(id, &milter.ctx_enable()), + id: Arc::new(milter.name.into()), + addrs: format!("{}:{}", milter.hostname, milter.port) + .to_socket_addrs() + .map_err(|err| { + bp.build_error( + id, + format!( + "Unable to resolve milter hostname {}: {}", + milter.hostname, err + ), + ) + }) + .ok()? + .collect(), + hostname: milter.hostname, + port: milter.port as u16, + timeout_connect: milter.timeout_connect.into_inner(), + timeout_command: milter.timeout_command.into_inner(), + timeout_data: milter.timeout_data.into_inner(), + tls: milter.use_tls, + tls_allow_invalid_certs: milter.allow_invalid_certs, + tempfail_on_error: milter.temp_fail_on_error, + max_frame_len: milter.max_response_size as usize, + protocol_version: match milter.protocol_version { + enums::MilterVersion::V2 => MilterVersion::V2, + enums::MilterVersion::V6 => MilterVersion::V6, + }, + flags_actions: milter.flags_action.map(|v| v as u32), + flags_protocol: milter.flags_protocol.map(|v| v as u32), + run_on_stage: milter.stages.into_iter().map(Stage::from).collect(), + }) + } else { + None + } + }) + .collect(), + hooks: bp + .list_infallible::() + .await + .into_iter() + .filter_map(|hook| { + let id = hook.id; + let hook = hook.object; + + if bp.validate(id, &hook) { + Some(MTAHook { + enable: bp.compile_expr(id, &hook.ctx_enable()), + id: hook.name, + url: hook.url, + timeout: hook.timeout.into_inner(), + headers: match hook.http_auth { + HttpAuth::None => build_http_headers( + hook.http_headers, + None, + None, + None, + "application/json".into(), + ), + HttpAuth::Basic(auth) => build_http_headers( + hook.http_headers, + auth.username.as_str().into(), + Some(auth.secret.as_str().into()), + None, + "application/json".into(), + ), + HttpAuth::Bearer(auth) => build_http_headers( + hook.http_headers, + None, + None, + Some(auth.bearer_token.as_str().into()), + "application/json".into(), + ), + } + .map_err(|err| { + bp.build_error(id, format!("Unable to build HTTP headers: {}", err)) + }) + .ok()?, + tls_allow_invalid_certs: hook.allow_invalid_certs, + tempfail_on_error: hook.temp_fail_on_error, + run_on_stage: hook.stages.into_iter().map(Stage::from).collect(), + max_response_size: hook.max_response_size as usize, + }) + } else { + None + } + }) + .collect(), } } } @@ -931,3 +529,16 @@ impl<'x> TryFrom> for MtPriority { } } } + +impl From for Stage { + fn from(value: MtaStage) -> Self { + match value { + MtaStage::Connect => Stage::Connect, + MtaStage::Ehlo => Stage::Ehlo, + MtaStage::Auth => Stage::Auth, + MtaStage::Mail => Stage::Mail, + MtaStage::Rcpt => Stage::Rcpt, + MtaStage::Data => Stage::Data, + } + } +} diff --git a/crates/common/src/config/smtp/throttle.rs b/crates/common/src/config/smtp/throttle.rs deleted file mode 100644 index 64e3ed96..00000000 --- a/crates/common/src/config/smtp/throttle.rs +++ /dev/null @@ -1,101 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use utils::config::{Config, Rate, utils::AsKey}; - -use crate::expr::{Expression, tokenizer::TokenMap}; - -use super::*; - -pub fn parse_queue_rate_limiter( - bp: &mut Bootstrap, - prefix: impl AsKey, - token_map: &TokenMap, - available_rate_limiter_keys: u16, -) -> Vec { - let prefix_ = prefix.as_key(); - let mut rate_limiters = Vec::new(); - for rate_limiter_id in config.sub_keys(prefix, "") { - let rate_limiter_id = rate_limiter_id.as_str(); - if let Some(rate_limiter) = parse_queue_rate_limiter_item( - config, - (&prefix_, rate_limiter_id), - rate_limiter_id, - token_map, - available_rate_limiter_keys, - ) { - rate_limiters.push(rate_limiter); - } - } - - rate_limiters -} - -fn parse_queue_rate_limiter_item( - bp: &mut Bootstrap, - prefix: impl AsKey, - rate_limiter_id: &str, - token_map: &TokenMap, - available_rate_limiter_keys: u16, -) -> Option { - let prefix = prefix.as_key(); - - // Skip disabled rate_limiters - if !config - .property::((prefix.as_str(), "enable")) - .unwrap_or(true) - { - return None; - } - - let mut keys = 0; - for (key_, value) in config - .values((&prefix, "key")) - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect::>() - { - match parse_queue_rate_limiter_key(&value) { - Ok(key) => { - if (key & available_rate_limiter_keys) != 0 { - keys |= key; - } else { - let err = - format!("Rate limiter key {value:?} is not available in this context"); - config.new_build_error(key_, err); - } - } - Err(err) => { - config.new_parse_error(key_, err); - } - } - } - - Some(QueueRateLimiter { - id: rate_limiter_id.to_string(), - expr: Expression::try_parse(config, (prefix.as_str(), "match"), token_map) - .unwrap_or_default(), - keys, - rate: config - .property_require::((prefix.as_str(), "rate")) - .filter(|r| r.requests > 0)?, - }) -} - -pub(crate) fn parse_queue_rate_limiter_key(value: &str) -> Result { - match value { - "rcpt" => Ok(THROTTLE_RCPT), - "rcpt_domain" => Ok(THROTTLE_RCPT_DOMAIN), - "sender" => Ok(THROTTLE_SENDER), - "sender_domain" => Ok(THROTTLE_SENDER_DOMAIN), - "authenticated_as" => Ok(THROTTLE_AUTH_AS), - "listener" => Ok(THROTTLE_LISTENER), - "mx" => Ok(THROTTLE_MX), - "remote_ip" => Ok(THROTTLE_REMOTE_IP), - "local_ip" => Ok(THROTTLE_LOCAL_IP), - "helo_domain" => Ok(THROTTLE_HELO_DOMAIN), - _ => Err(format!("Invalid THROTTLE key {value:?}")), - } -} diff --git a/crates/common/src/config/storage.rs b/crates/common/src/config/storage.rs index 29f7023b..afafffc2 100644 --- a/crates/common/src/config/storage.rs +++ b/crates/common/src/config/storage.rs @@ -8,10 +8,11 @@ use ahash::AHashMap; use coordinator::Coordinator; use directory::Directory; use std::sync::Arc; -use store::{BlobStore, InMemoryStore, PurgeSchedule, SearchStore, Store}; +use store::{BlobStore, InMemoryStore, PurgeSchedule, RegistryStore, SearchStore, Store}; -#[derive(Default, Clone)] +#[derive(Clone)] pub struct Storage { + pub registry: RegistryStore, pub data: Store, pub blob: BlobStore, pub fts: SearchStore, diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index f3fad87d..0c20d498 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -29,7 +29,8 @@ use std::{ }; use store::{ BlobStore, Deserialize, InMemoryStore, IndexKey, IndexKeyPrefix, IterateParams, Key, LogKey, - SUBSPACE_LOGS, SearchStore, SerializeInfallible, Store, U32_LEN, U64_LEN, ValueKey, + RegistryStore, SUBSPACE_LOGS, SearchStore, SerializeInfallible, Store, U32_LEN, U64_LEN, + ValueKey, dispatch::DocumentSet, roaring::RoaringBitmap, write::{ @@ -48,6 +49,11 @@ use types::{ use utils::{map::bitmap::Bitmap, snowflake::SnowflakeIdGenerator}; impl Server { + #[inline(always)] + pub fn registry(&self) -> &RegistryStore { + &self.core.storage.registry + } + #[inline(always)] pub fn store(&self) -> &Store { &self.core.storage.data @@ -151,7 +157,7 @@ impl Server { let lazy_resolver_ = self.core.smtp.mail_auth.signatures.get(name)?; match lazy_resolver_.load().as_ref() { LazySignature::Resolved(resolved_signature) => Some(resolved_signature.clone()), - LazySignature::Pending(config) => { + LazySignature::Pending(bp) => { let mut config = config.clone(); if let Some((signer, sealer)) = build_signature(&mut config, name) { let resolved = ResolvedSignature { diff --git a/crates/common/src/enterprise/config.rs b/crates/common/src/enterprise/config.rs index 5324f7c0..423d6215 100644 --- a/crates/common/src/enterprise/config.rs +++ b/crates/common/src/enterprise/config.rs @@ -196,7 +196,7 @@ impl Enterprise { logo_url: config.value("enterprise.logo-url").map(|s| s.to_string()), trace_store, metrics_store, - metrics_alerts: parse_metric_alerts(config), + metrics_alerts: parse_metric_alerts(bp), spam_filter_llm: SpamFilterLlmConfig::parse(config, &ai_apis), ai_apis, template_calendar_alarm: None, diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index a8f9b6d3..6d91e6e8 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -175,7 +175,7 @@ pub struct Caches { pub dns_ipv4: CacheWithTtl>>, pub dns_ipv6: CacheWithTtl>>, pub dns_tlsa: CacheWithTtl>, - pub dbs_mta_sts: CacheWithTtl>, + pub dns_mta_sts: CacheWithTtl>, pub dns_rbl: CacheWithTtl>>, } @@ -496,7 +496,7 @@ impl Default for Caches { dns_ipv4: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_ipv6: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_tlsa: CacheWithTtl::new(1024, 10 * 1024 * 1024), - dbs_mta_sts: CacheWithTtl::new(1024, 10 * 1024 * 1024), + dns_mta_sts: CacheWithTtl::new(1024, 10 * 1024 * 1024), } } } diff --git a/crates/common/src/listener/acme/mod.rs b/crates/common/src/listener/acme/mod.rs index a6d9b978..ea8ed8b9 100644 --- a/crates/common/src/listener/acme/mod.rs +++ b/crates/common/src/listener/acme/mod.rs @@ -10,15 +10,14 @@ pub mod jose; pub mod order; pub mod resolver; -use std::{fmt::Debug, sync::Arc, time::Duration}; - +use self::directory::{Account, ChallengeType}; +use crate::Server; use arc_swap::ArcSwap; use dns_update::DnsUpdater; +use registry::schema::structs; use rustls::sign::CertifiedKey; - -use crate::Server; - -use self::directory::{Account, ChallengeType}; +use std::{fmt::Debug, sync::Arc, time::Duration}; +use store::registry::RegistryObject; pub struct AcmeProvider { pub id: String, @@ -56,37 +55,8 @@ pub struct StaticResolver { } impl AcmeProvider { - #[allow(clippy::too_many_arguments)] - pub fn new( - id: String, - directory_url: String, - domains: Vec, - contact: Vec, - challenge: ChallengeSettings, - eab: Option, - renew_before: Duration, - default: bool, - ) -> trc::Result { - Ok(AcmeProvider { - id, - directory_url, - contact: contact - .into_iter() - .map(|c| { - if !c.starts_with("mailto:") { - format!("mailto:{}", c) - } else { - c - } - }) - .collect(), - renew_before: chrono::Duration::from_std(renew_before).unwrap(), - domains, - account_key: Default::default(), - challenge, - eab, - default, - }) + pub fn new(obj: RegistryObject) -> Self { + todo!() } } diff --git a/crates/common/src/listener/acme/order.rs b/crates/common/src/listener/acme/order.rs index 6def86a4..ff5e69ef 100644 --- a/crates/common/src/listener/acme/order.rs +++ b/crates/common/src/listener/acme/order.rs @@ -12,7 +12,7 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use std::sync::Arc; use std::time::{Duration, Instant}; use store::dispatch::lookup::KeyValue; -use trc::{AcmeEvent, EventType}; +use trc::{AcmeEvent, DnsEvent, EventType}; use x509_parser::parse_x509_certificate; use crate::listener::acme::ChallengeSettings; @@ -255,7 +255,7 @@ impl Server { if let Err(err) = updater.delete(&name, &origin, DnsRecordType::TXT).await { // Errors are expected if the record does not exist trc::event!( - Acme(AcmeEvent::DnsRecordDeletionFailed), + Dns(DnsEvent::RecordDeletionFailed), Hostname = name.to_string(), Reason = err.to_string(), Details = origin.to_string(), @@ -275,7 +275,7 @@ impl Server { ) .await { - return Err(EventType::Acme(AcmeEvent::DnsRecordCreationFailed) + return Err(EventType::Dns(DnsEvent::RecordCreationFailed) .ctx(trc::Key::Id, provider.id.to_string()) .ctx(trc::Key::Hostname, name) .ctx(trc::Key::Details, origin) @@ -283,7 +283,7 @@ impl Server { } trc::event!( - Acme(AcmeEvent::DnsRecordCreated), + Dns(DnsEvent::RecordCreated), Hostname = name.to_string(), Details = origin.to_string(), Id = provider.id.to_string(), @@ -301,7 +301,7 @@ impl Server { break; } else { trc::event!( - Acme(AcmeEvent::DnsRecordNotPropagated), + Dns(DnsEvent::RecordNotPropagated), Id = provider.id.to_string(), Hostname = name.to_string(), Details = origin.to_string(), @@ -312,7 +312,7 @@ impl Server { } Err(err) => { trc::event!( - Acme(AcmeEvent::DnsRecordLookupFailed), + Dns(DnsEvent::RecordLookupFailed), Id = provider.id.to_string(), Hostname = name.to_string(), Details = origin.to_string(), @@ -326,14 +326,14 @@ impl Server { if did_propagate { trc::event!( - Acme(AcmeEvent::DnsRecordPropagated), + Dns(DnsEvent::RecordPropagated), Id = provider.id.to_string(), Hostname = name.to_string(), Details = origin.to_string(), ); } else { trc::event!( - Acme(AcmeEvent::DnsRecordPropagationTimeout), + Dns(DnsEvent::RecordPropagationTimeout), Id = provider.id.to_string(), Hostname = name.to_string(), Details = origin.to_string(), diff --git a/crates/common/src/listener/blocked.rs b/crates/common/src/listener/blocked.rs index 0192cffd..c9cbe140 100644 --- a/crates/common/src/listener/blocked.rs +++ b/crates/common/src/listener/blocked.rs @@ -78,7 +78,7 @@ impl Security { allowed_ip_addresses.insert(IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)); } - let blocked = BlockedIps::parse(config); + let blocked = BlockedIps::parse(bp); // Parse blocked HTTP paths let mut http_banned_paths = config diff --git a/crates/common/src/listener/tls.rs b/crates/common/src/listener/tls.rs index 9a25f823..d8df9f3b 100644 --- a/crates/common/src/listener/tls.rs +++ b/crates/common/src/listener/tls.rs @@ -33,11 +33,6 @@ use super::{ pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13]; pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12]; -#[derive(Default, Clone)] -pub struct AcmeProviders { - pub providers: AHashMap, -} - #[derive(Clone)] pub struct CertificateResolver { pub inner: Arc, diff --git a/crates/common/src/manager/bootstrap.rs b/crates/common/src/manager/bootstrap.rs index d24c1f0c..d27850c3 100644 --- a/crates/common/src/manager/bootstrap.rs +++ b/crates/common/src/manager/bootstrap.rs @@ -112,4 +112,8 @@ impl Bootstrap { pub fn node_id(&self) -> u64 { self.node.node_id } + + pub fn hostname(&self) -> &str { + &self.node.hostname + } } diff --git a/crates/registry/src/schema/mod.rs b/crates/registry/src/schema/mod.rs index 3b5ebc8b..f5b0564c 100644 --- a/crates/registry/src/schema/mod.rs +++ b/crates/registry/src/schema/mod.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{schema::prelude::Property, types::EnumType}; +use std::fmt::Display; use utils::config::cron::SimpleCron; #[allow(clippy::derivable_impls)] @@ -35,3 +37,9 @@ impl From for SimpleCron { } } } + +impl Display for Property { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_str()) + } +} diff --git a/crates/registry/src/types/error.rs b/crates/registry/src/types/error.rs index a7669ea0..6c21badb 100644 --- a/crates/registry/src/types/error.rs +++ b/crates/registry/src/types/error.rs @@ -94,3 +94,44 @@ impl Warning { } } } + +impl Display for ValidationError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ValidationError::Invalid { property, value } => { + write!(f, "Invalid value '{}' for property '{}'", value, property) + } + ValidationError::Required { property } => { + write!(f, "Property '{}' is required", property) + } + ValidationError::MaxLength { property, required } => { + write!( + f, + "Property '{}' exceeds maximum length of {}", + property, required + ) + } + ValidationError::MinLength { property, required } => { + write!( + f, + "Property '{}' is below minimum length of {}", + property, required + ) + } + ValidationError::MaxValue { property, required } => { + write!( + f, + "Property '{}' exceeds maximum value of {}", + property, required + ) + } + ValidationError::MinValue { property, required } => { + write!( + f, + "Property '{}' is below minimum value of {}", + property, required + ) + } + } + } +} diff --git a/crates/registry/src/types/ipaddr.rs b/crates/registry/src/types/ipaddr.rs index 9216b9ca..dba6dd8e 100644 --- a/crates/registry/src/types/ipaddr.rs +++ b/crates/registry/src/types/ipaddr.rs @@ -8,7 +8,8 @@ use std::{fmt::Display, net::Ipv4Addr, str::FromStr}; use crate::pickle::{Pickle, PickledStream}; -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(transparent)] pub struct IpAddr(pub std::net::IpAddr); impl IpAddr { diff --git a/crates/smtp/src/outbound/mta_sts/lookup.rs b/crates/smtp/src/outbound/mta_sts/lookup.rs index febaf7fa..00dddbee 100644 --- a/crates/smtp/src/outbound/mta_sts/lookup.rs +++ b/crates/smtp/src/outbound/mta_sts/lookup.rs @@ -50,7 +50,7 @@ impl MtaStsLookup for Server { Ok(record) => record, Err(err) => { // Return the cached policy in case of failure - return if let Some(value) = self.inner.cache.dbs_mta_sts.get(domain) { + return if let Some(value) = self.inner.cache.dns_mta_sts.get(domain) { Ok(value) } else { Err(err.into()) @@ -59,7 +59,7 @@ impl MtaStsLookup for Server { }; // Check if the policy has been cached - if let Some(value) = self.inner.cache.dbs_mta_sts.get(domain) + if let Some(value) = self.inner.cache.dns_mta_sts.get(domain) && value.id == record.id { return Ok(value); @@ -87,7 +87,7 @@ impl MtaStsLookup for Server { record.id.clone(), )?); - self.inner.cache.dbs_mta_sts.insert( + self.inner.cache.dns_mta_sts.insert( domain.to_string(), policy.clone(), Duration::from_secs(if (3600..31557600).contains(&policy.max_age) { diff --git a/crates/store/src/backend/elastic/main.rs b/crates/store/src/backend/elastic/main.rs index 63585dcc..cf916ff2 100644 --- a/crates/store/src/backend/elastic/main.rs +++ b/crates/store/src/backend/elastic/main.rs @@ -17,7 +17,8 @@ use utils::config::{Config, http::build_http_client, utils::AsKey}; impl ElasticSearchStore { pub async fn open(config: &mut Config, prefix: impl AsKey) -> Option { - let client = build_http_client(config, prefix.clone(), "application/json".into())?; + todo!() + /*let client = build_http_client(config, prefix.clone(), "application/json".into())?; let prefix = prefix.as_key(); let url = config .value_require((&prefix, "url"))? @@ -43,7 +44,7 @@ impl ElasticSearchStore { config.new_build_error(prefix.as_str(), err.to_string()); } - Some(es) + Some(es)*/ } pub async fn create_indexes( diff --git a/crates/store/src/backend/meili/main.rs b/crates/store/src/backend/meili/main.rs index 02472ee4..9e026ce5 100644 --- a/crates/store/src/backend/meili/main.rs +++ b/crates/store/src/backend/meili/main.rs @@ -18,7 +18,8 @@ use utils::config::{Config, http::build_http_client, utils::AsKey}; impl MeiliSearchStore { pub async fn open(config: &mut Config, prefix: impl AsKey) -> Option { - let client = build_http_client(config, prefix.clone(), "application/json".into())?; + todo!() + /*let client = build_http_client(config, prefix.clone(), "application/json".into())?; let prefix = prefix.as_key(); let url = config .value_require((&prefix, "url"))? @@ -55,7 +56,7 @@ impl MeiliSearchStore { task_poll_interval, task_poll_retries, task_fail_on_timeout, - }) + })*/ } pub async fn create_indexes(&self) -> trc::Result<()> { diff --git a/crates/trc/src/event/description.rs b/crates/trc/src/event/description.rs index e7837051..db538d86 100644 --- a/crates/trc/src/event/description.rs +++ b/crates/trc/src/event/description.rs @@ -54,6 +54,7 @@ impl EventType { EventType::Ai(event) => event.description(), EventType::WebDav(event) => event.description(), EventType::Calendar(event) => event.description(), + EventType::Dns(event) => event.description(), } } @@ -104,6 +105,7 @@ impl EventType { EventType::Ai(event) => event.explain(), EventType::WebDav(event) => event.explain(), EventType::Calendar(event) => event.explain(), + EventType::Dns(event) => event.explain(), } } } @@ -1221,13 +1223,6 @@ impl AcmeEvent { AcmeEvent::OrderValid => "ACME order valid", AcmeEvent::OrderInvalid => "ACME order invalid", AcmeEvent::RenewBackoff => "ACME renew backoff", - AcmeEvent::DnsRecordCreated => "ACME DNS record created", - AcmeEvent::DnsRecordCreationFailed => "ACME DNS record creation failed", - AcmeEvent::DnsRecordDeletionFailed => "ACME DNS record deletion failed", - AcmeEvent::DnsRecordNotPropagated => "ACME DNS record not propagated", - AcmeEvent::DnsRecordLookupFailed => "ACME DNS record lookup failed", - AcmeEvent::DnsRecordPropagated => "ACME DNS record propagated", - AcmeEvent::DnsRecordPropagationTimeout => "ACME DNS record propagation timeout", AcmeEvent::ClientSuppliedSni => "ACME client supplied SNI", AcmeEvent::ClientMissingSni => "ACME client missing SNI", AcmeEvent::TlsAlpnReceived => "ACME TLS ALPN received", @@ -1253,13 +1248,6 @@ impl AcmeEvent { AcmeEvent::OrderValid => "ACME order is valid", AcmeEvent::OrderInvalid => "ACME order is invalid", AcmeEvent::RenewBackoff => "ACME renew backoff", - AcmeEvent::DnsRecordCreated => "ACME DNS record has been created", - AcmeEvent::DnsRecordCreationFailed => "Failed to create ACME DNS record", - AcmeEvent::DnsRecordDeletionFailed => "Failed to delete ACME DNS record", - AcmeEvent::DnsRecordNotPropagated => "ACME DNS record has not propagated", - AcmeEvent::DnsRecordLookupFailed => "Failed to look up ACME DNS record", - AcmeEvent::DnsRecordPropagated => "ACME DNS record has propagated", - AcmeEvent::DnsRecordPropagationTimeout => "ACME DNS record propagation timeout", AcmeEvent::ClientSuppliedSni => "ACME client supplied SNI", AcmeEvent::ClientMissingSni => "ACME client missing SNI", AcmeEvent::TlsAlpnReceived => "ACME TLS ALPN received", @@ -1270,6 +1258,34 @@ impl AcmeEvent { } } +impl DnsEvent { + pub fn description(&self) -> &'static str { + match self { + DnsEvent::RecordCreated => "DNS record created", + DnsEvent::RecordCreationFailed => "DNS record creation failed", + DnsEvent::RecordDeletionFailed => "DNS record deletion failed", + DnsEvent::RecordNotPropagated => "DNS record not propagated", + DnsEvent::RecordLookupFailed => "DNS record lookup failed", + DnsEvent::RecordPropagated => "DNS record propagated", + DnsEvent::RecordPropagationTimeout => "DNS record propagation timeout", + DnsEvent::BuildError => "DNS updater build error", + } + } + + pub fn explain(&self) -> &'static str { + match self { + DnsEvent::RecordCreated => "DNS record has been created", + DnsEvent::RecordCreationFailed => "Failed to create DNS record", + DnsEvent::RecordDeletionFailed => "Failed to delete DNS record", + DnsEvent::RecordNotPropagated => "DNS record has not propagated", + DnsEvent::RecordLookupFailed => "Failed to look up DNS record", + DnsEvent::RecordPropagated => "DNS record has propagated", + DnsEvent::RecordPropagationTimeout => "DNS record propagation timeout", + DnsEvent::BuildError => "An error occurred while building the DNS updater", + } + } +} + impl PurgeEvent { pub fn description(&self) -> &'static str { match self { @@ -1399,6 +1415,7 @@ impl DkimEvent { DkimEvent::SignatureExpired => "DKIM signature expired", DkimEvent::SignatureLength => "DKIM signature length issue", DkimEvent::SignerNotFound => "DKIM signer not found", + DkimEvent::BuildError => "DKIM signature build error", } } @@ -1422,6 +1439,7 @@ impl DkimEvent { DkimEvent::SignatureExpired => "The DKIM signature has expired", DkimEvent::SignatureLength => "The DKIM signature length is incorrect", DkimEvent::SignerNotFound => "The DKIM signer was not found", + DkimEvent::BuildError => "An error occurred while building the DKIM signature", } } } diff --git a/crates/trc/src/event/level.rs b/crates/trc/src/event/level.rs index 226ceac3..de1e5043 100644 --- a/crates/trc/src/event/level.rs +++ b/crates/trc/src/event/level.rs @@ -265,7 +265,7 @@ impl EventType { ArcEvent::SealerNotFound => Level::Warn, }, EventType::Dkim(event) => match event { - DkimEvent::SignerNotFound => Level::Warn, + DkimEvent::SignerNotFound | DkimEvent::BuildError => Level::Warn, _ => Level::Debug, }, EventType::MailAuth(_) => Level::Debug, @@ -289,9 +289,7 @@ impl EventType { ServerEvent::StartupError | ServerEvent::ThreadError => Level::Error, }, EventType::Acme(event) => match event { - AcmeEvent::DnsRecordCreated - | AcmeEvent::DnsRecordPropagated - | AcmeEvent::TlsAlpnReceived + AcmeEvent::TlsAlpnReceived | AcmeEvent::AuthStart | AcmeEvent::AuthPending | AcmeEvent::AuthValid @@ -307,15 +305,18 @@ impl EventType { | AcmeEvent::AuthError | AcmeEvent::AuthTooManyAttempts | AcmeEvent::TokenNotFound - | AcmeEvent::DnsRecordPropagationTimeout - | AcmeEvent::TlsAlpnError - | AcmeEvent::DnsRecordCreationFailed => Level::Warn, + | AcmeEvent::TlsAlpnError => Level::Warn, AcmeEvent::RenewBackoff - | AcmeEvent::DnsRecordDeletionFailed | AcmeEvent::ClientSuppliedSni - | AcmeEvent::ClientMissingSni - | AcmeEvent::DnsRecordNotPropagated - | AcmeEvent::DnsRecordLookupFailed => Level::Debug, + | AcmeEvent::ClientMissingSni => Level::Debug, + }, + EventType::Dns(event) => match event { + DnsEvent::RecordCreated | DnsEvent::RecordPropagated => Level::Info, + DnsEvent::RecordPropagationTimeout | DnsEvent::RecordCreationFailed => Level::Warn, + DnsEvent::RecordDeletionFailed + | DnsEvent::RecordNotPropagated + | DnsEvent::RecordLookupFailed => Level::Debug, + DnsEvent::BuildError => Level::Error, }, EventType::Tls(event) => match event { TlsEvent::Handshake => Level::Info, diff --git a/crates/trc/src/event/mod.rs b/crates/trc/src/event/mod.rs index dc6170d5..cfff2d94 100644 --- a/crates/trc/src/event/mod.rs +++ b/crates/trc/src/event/mod.rs @@ -369,6 +369,69 @@ impl StoreEvent { } } +impl DnsEvent { + pub fn ctx(self, key: Key, value: impl Into) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(EventType::Dns(self)) + } +} + +impl AcmeEvent { + pub fn ctx(self, key: Key, value: impl Into) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(EventType::Acme(self)) + } +} + +impl DkimEvent { + pub fn ctx(self, key: Key, value: impl Into) -> Error { + self.into_err().ctx(key, value) + } + + #[inline(always)] + pub fn caused_by(self, error: impl Into) -> Error { + self.into_err().caused_by(error) + } + + #[inline(always)] + pub fn reason(self, error: impl Display) -> Error { + self.into_err().reason(error) + } + + #[inline(always)] + pub fn into_err(self) -> Error { + Error::new(EventType::Dkim(self)) + } +} + impl SecurityEvent { #[inline(always)] pub fn into_err(self) -> Error { diff --git a/crates/trc/src/ipc/metrics.rs b/crates/trc/src/ipc/metrics.rs index a61ed235..bf3d9cb0 100644 --- a/crates/trc/src/ipc/metrics.rs +++ b/crates/trc/src/ipc/metrics.rs @@ -420,15 +420,17 @@ impl EventType { | AcmeEvent::OrderCompleted | AcmeEvent::AuthError | AcmeEvent::AuthTooManyAttempts - | AcmeEvent::DnsRecordCreationFailed - | AcmeEvent::DnsRecordDeletionFailed - | AcmeEvent::DnsRecordPropagationTimeout | AcmeEvent::ClientMissingSni | AcmeEvent::TokenNotFound - | AcmeEvent::DnsRecordLookupFailed | AcmeEvent::OrderInvalid | AcmeEvent::Error, ) => true, + EventType::Dns( + DnsEvent::RecordCreationFailed + | DnsEvent::RecordDeletionFailed + | DnsEvent::RecordPropagationTimeout + | DnsEvent::RecordLookupFailed, + ) => true, EventType::Store( StoreEvent::AssertValueFailed | StoreEvent::FoundationdbError diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index c29a5d3c..43a2cc0f 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -190,6 +190,7 @@ pub enum EventType { Ai(AiEvent), WebDav(WebDavEvent), Calendar(CalendarEvent), + Dns(DnsEvent), } #[event_type] @@ -694,13 +695,6 @@ pub enum AcmeEvent { OrderValid, OrderInvalid, RenewBackoff, - DnsRecordCreated, - DnsRecordCreationFailed, - DnsRecordDeletionFailed, - DnsRecordNotPropagated, - DnsRecordLookupFailed, - DnsRecordPropagated, - DnsRecordPropagationTimeout, ClientSuppliedSni, ClientMissingSni, TlsAlpnReceived, @@ -709,6 +703,18 @@ pub enum AcmeEvent { Error, } +#[event_type] +pub enum DnsEvent { + RecordCreated, + RecordCreationFailed, + RecordDeletionFailed, + RecordNotPropagated, + RecordLookupFailed, + RecordPropagated, + RecordPropagationTimeout, + BuildError, +} + #[event_type] pub enum PurgeEvent { Started, @@ -774,6 +780,7 @@ pub enum DkimEvent { SignatureExpired, SignatureLength, SignerNotFound, + BuildError, } #[event_type] diff --git a/crates/trc/src/serializers/binary.rs b/crates/trc/src/serializers/binary.rs index eacce478..e4b0ddab 100644 --- a/crates/trc/src/serializers/binary.rs +++ b/crates/trc/src/serializers/binary.rs @@ -315,13 +315,13 @@ impl EventType { EventType::Acme(AcmeEvent::AuthValid) => 5, EventType::Acme(AcmeEvent::ClientMissingSni) => 6, EventType::Acme(AcmeEvent::ClientSuppliedSni) => 7, - EventType::Acme(AcmeEvent::DnsRecordCreated) => 8, - EventType::Acme(AcmeEvent::DnsRecordCreationFailed) => 9, - EventType::Acme(AcmeEvent::DnsRecordDeletionFailed) => 10, - EventType::Acme(AcmeEvent::DnsRecordLookupFailed) => 11, - EventType::Acme(AcmeEvent::DnsRecordNotPropagated) => 12, - EventType::Acme(AcmeEvent::DnsRecordPropagated) => 13, - EventType::Acme(AcmeEvent::DnsRecordPropagationTimeout) => 14, + EventType::Dns(DnsEvent::RecordCreated) => 8, + EventType::Dns(DnsEvent::RecordCreationFailed) => 9, + EventType::Dns(DnsEvent::RecordDeletionFailed) => 10, + EventType::Dns(DnsEvent::RecordLookupFailed) => 11, + EventType::Dns(DnsEvent::RecordNotPropagated) => 12, + EventType::Dns(DnsEvent::RecordPropagated) => 13, + EventType::Dns(DnsEvent::RecordPropagationTimeout) => 14, EventType::Acme(AcmeEvent::Error) => 15, EventType::Acme(AcmeEvent::OrderCompleted) => 16, EventType::Acme(AcmeEvent::OrderInvalid) => 17, @@ -898,6 +898,8 @@ impl EventType { EventType::Spam(SpamEvent::TrainStarted) => 588, EventType::Spam(SpamEvent::ModelLoaded) => 589, EventType::Store(StoreEvent::MeilisearchError) => 590, + EventType::Dns(DnsEvent::BuildError) => 591, + EventType::Dkim(DkimEvent::BuildError) => 592, } } @@ -911,13 +913,13 @@ impl EventType { 5 => Some(EventType::Acme(AcmeEvent::AuthValid)), 6 => Some(EventType::Acme(AcmeEvent::ClientMissingSni)), 7 => Some(EventType::Acme(AcmeEvent::ClientSuppliedSni)), - 8 => Some(EventType::Acme(AcmeEvent::DnsRecordCreated)), - 9 => Some(EventType::Acme(AcmeEvent::DnsRecordCreationFailed)), - 10 => Some(EventType::Acme(AcmeEvent::DnsRecordDeletionFailed)), - 11 => Some(EventType::Acme(AcmeEvent::DnsRecordLookupFailed)), - 12 => Some(EventType::Acme(AcmeEvent::DnsRecordNotPropagated)), - 13 => Some(EventType::Acme(AcmeEvent::DnsRecordPropagated)), - 14 => Some(EventType::Acme(AcmeEvent::DnsRecordPropagationTimeout)), + 8 => Some(EventType::Dns(DnsEvent::RecordCreated)), + 9 => Some(EventType::Dns(DnsEvent::RecordCreationFailed)), + 10 => Some(EventType::Dns(DnsEvent::RecordDeletionFailed)), + 11 => Some(EventType::Dns(DnsEvent::RecordLookupFailed)), + 12 => Some(EventType::Dns(DnsEvent::RecordNotPropagated)), + 13 => Some(EventType::Dns(DnsEvent::RecordPropagated)), + 14 => Some(EventType::Dns(DnsEvent::RecordPropagationTimeout)), 15 => Some(EventType::Acme(AcmeEvent::Error)), 16 => Some(EventType::Acme(AcmeEvent::OrderCompleted)), 17 => Some(EventType::Acme(AcmeEvent::OrderInvalid)), @@ -1534,6 +1536,8 @@ impl EventType { 588 => Some(EventType::Spam(SpamEvent::TrainStarted)), 589 => Some(EventType::Spam(SpamEvent::ModelLoaded)), 590 => Some(EventType::Store(StoreEvent::MeilisearchError)), + 591 => Some(EventType::Dns(DnsEvent::BuildError)), + 592 => Some(EventType::Dkim(DkimEvent::BuildError)), _ => None, } } diff --git a/crates/utils/src/cache.rs b/crates/utils/src/cache.rs index 7b41177c..4aa86564 100644 --- a/crates/utils/src/cache.rs +++ b/crates/utils/src/cache.rs @@ -33,23 +33,11 @@ pub struct TtlEntry { } impl Cache { - pub fn from_config( - config: &mut Config, - key: &str, - max_weight: u64, - estimated_weight: u64, - ) -> Self { - let weight_capacity = config - .property(("cache", key, "size")) - .unwrap_or(max_weight); - let estimated_items_capacity = config - .property(("cache", key, "capacity")) - .unwrap_or_else(|| weight_capacity as usize / estimated_weight as usize); - - Self::new(estimated_items_capacity, weight_capacity) + pub fn new(weight: u64, estimated_weight: u64) -> Self { + Self::new_estimated(weight as usize / estimated_weight as usize, weight) } - pub fn new(estimated_items_capacity: usize, weight_capacity: u64) -> Self { + pub fn new_estimated(estimated_items_capacity: usize, weight_capacity: u64) -> Self { Self(quick_cache::sync::Cache::with_weighter( estimated_items_capacity, weight_capacity, @@ -99,23 +87,11 @@ impl Cache { } impl CacheWithTtl { - pub fn from_config( - config: &mut Config, - key: &str, - max_weight: u64, - estimated_weight: u64, - ) -> Self { - let weight_capacity = config - .property(("cache", key, "size")) - .unwrap_or(max_weight); - let estimated_items_capacity = config - .property(("cache", key, "capacity")) - .unwrap_or_else(|| weight_capacity as usize / estimated_weight as usize); - - Self::new(estimated_items_capacity, weight_capacity) + pub fn new(weight: u64, estimated_weight: u64) -> Self { + Self::new_estimated(weight as usize / estimated_weight as usize, weight) } - pub fn new(estimated_items_capacity: usize, weight_capacity: u64) -> Self { + pub fn new_estimated(estimated_items_capacity: usize, weight_capacity: u64) -> Self { Self(quick_cache::sync::Cache::with_weighter( estimated_items_capacity, weight_capacity, diff --git a/crates/utils/src/config/http.rs b/crates/utils/src/config/http.rs index 4475ddc7..0652d06b 100644 --- a/crates/utils/src/config/http.rs +++ b/crates/utils/src/config/http.rs @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::config::{Config, utils::AsKey}; use base64::{Engine, engine::general_purpose}; use reqwest::{ Client, @@ -13,73 +12,57 @@ use reqwest::{ use std::{str::FromStr, time::Duration}; pub fn build_http_client( - config: &mut Config, - prefix: impl AsKey, + raw_headers: impl IntoIterator, + username: Option<&str>, + password: Option<&str>, + token: Option<&str>, content_type: Option<&str>, -) -> Option { - let mut headers = parse_http_headers(config, prefix.clone()); + timeout: Duration, + allow_invalid_certs: bool, +) -> Result { + let mut headers = build_http_headers(raw_headers, username, password, token, content_type)?; headers.insert(USER_AGENT, "Stalwart/1.0.0".parse().unwrap()); + match Client::builder() + .connect_timeout(timeout) + .danger_accept_invalid_certs(allow_invalid_certs) + .default_headers(headers) + .build() + { + Ok(client) => Ok(client), + Err(err) => Err(format!("Failed to build HTTP client: {}", err)), + } +} + +pub fn build_http_headers( + raw_headers: impl IntoIterator, + username: Option<&str>, + password: Option<&str>, + token: Option<&str>, + content_type: Option<&str>, +) -> Result { + let mut headers = HeaderMap::new(); + if let Some(content_type) = content_type { headers.insert(CONTENT_TYPE, HeaderValue::from_str(content_type).unwrap()); } - let prefix = prefix.as_key(); - match Client::builder() - .connect_timeout( - config - .property_or_default::((&prefix, "timeout"), "30s") - .unwrap_or(Duration::from_secs(30)), - ) - .danger_accept_invalid_certs( - config - .property_or_default::((&prefix, "tls.allow-invalid-certs"), "false") - .unwrap_or(false), - ) - .default_headers(headers) - .build() - { - Ok(client) => Some(client), - Err(err) => { - config.new_build_error(&prefix, format!("Failed to build HTTP client: {err}")); - None - } - } -} - -pub fn parse_http_headers(config: &mut Config, prefix: impl AsKey) -> HeaderMap { - let prefix = prefix.as_key(); - let mut headers = HeaderMap::new(); - - for (header, value) in config - .values((&prefix, "headers")) - .map(|(_, v)| { - if let Some((k, v)) = v.split_once(':') { - Ok(( - HeaderName::from_str(k.trim()).map_err(|err| { - format!("Invalid header found in property \"{prefix}.headers\": {err}",) - })?, - HeaderValue::from_str(v.trim()).map_err(|err| { - format!("Invalid header found in property \"{prefix}.headers\": {err}",) - })?, - )) - } else { - Err(format!( - "Invalid header found in property \"{prefix}.headers\": {v}", - )) - } + for (header, value) in raw_headers + .into_iter() + .map(|(k, v)| { + Ok(( + HeaderName::from_str(k.trim()) + .map_err(|err| format!("Invalid header {k:?}: {err}",))?, + HeaderValue::from_str(v.trim()) + .map_err(|err| format!("Invalid value {v:?}: {err}",))?, + )) }) - .collect::, String>>() - .map_err(|e| config.new_parse_error((&prefix, "headers"), e)) - .unwrap_or_default() + .collect::, String>>()? { headers.insert(header, value); } - if let (Some(name), Some(secret)) = ( - config.value((&prefix, "auth.username")), - config.value((&prefix, "auth.secret")), - ) { + if let (Some(name), Some(secret)) = (username, password) { headers.insert( AUTHORIZATION, format!( @@ -89,9 +72,9 @@ pub fn parse_http_headers(config: &mut Config, prefix: impl AsKey) -> HeaderMap .parse() .unwrap(), ); - } else if let Some(token) = config.value((&prefix, "auth.token")) { + } else if let Some(token) = token { headers.insert(AUTHORIZATION, format!("Bearer {}", token).parse().unwrap()); } - headers + Ok(headers) }