From 1c2c96b89d8fd57d0f876db5565c0a7af1b1caa1 Mon Sep 17 00:00:00 2001 From: mdecimus <11444311+mdecimus@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:49:37 +0100 Subject: [PATCH] Bootstrap from registry - part 1 --- Cargo.lock | 1 + crates/common/Cargo.toml | 1 + crates/common/src/addresses.rs | 2 +- crates/common/src/auth/oauth/config.rs | 6 +- crates/common/src/config/groupware.rs | 2 +- crates/common/src/config/imap.rs | 54 -- crates/common/src/config/inner.rs | 5 +- crates/common/src/config/jmap/settings.rs | 431 ------------ .../{jmap => mailstore}/capabilities.rs | 87 ++- crates/common/src/config/mailstore/email.rs | 240 +++++++ crates/common/src/config/mailstore/imap.rs | 40 ++ crates/common/src/config/mailstore/jmap.rs | 89 +++ .../src/config/{jmap => mailstore}/mod.rs | 6 +- crates/common/src/config/mailstore/scripts.rs | 256 ++++++++ .../src/config/{ => mailstore}/spamfilter.rs | 28 +- crates/common/src/config/mod.rs | 17 +- crates/common/src/config/network.rs | 97 ++- crates/common/src/config/scripts.rs | 393 ----------- crates/common/src/config/server/listener.rs | 6 +- crates/common/src/config/server/tls.rs | 6 +- crates/common/src/config/smtp/auth.rs | 24 +- crates/common/src/config/smtp/mod.rs | 2 +- crates/common/src/config/smtp/queue.rs | 44 +- crates/common/src/config/smtp/report.rs | 38 +- crates/common/src/config/smtp/resolver.rs | 4 +- crates/common/src/config/smtp/session.rs | 106 +-- crates/common/src/config/smtp/throttle.rs | 4 +- crates/common/src/config/storage.rs | 3 - crates/common/src/config/telemetry.rs | 8 +- crates/common/src/core.rs | 2 +- crates/common/src/enterprise/config.rs | 15 +- crates/common/src/enterprise/llm.rs | 2 +- crates/common/src/expr/eval.rs | 26 +- crates/common/src/expr/if_block.rs | 228 +++---- crates/common/src/expr/mod.rs | 14 +- crates/common/src/expr/tokenizer.rs | 52 +- crates/common/src/lib.rs | 23 +- crates/common/src/listener/acme/cache.rs | 6 +- crates/common/src/listener/blocked.rs | 18 +- crates/common/src/listener/listen.rs | 2 +- crates/common/src/manager/boot.rs | 7 +- crates/common/src/manager/bootstrap.rs | 98 +++ crates/common/src/manager/config.rs | 611 ------------------ crates/common/src/manager/mod.rs | 3 +- crates/common/src/manager/reload.rs | 11 +- crates/common/src/manager/webadmin.rs | 11 +- crates/coordinator/src/dispatch.rs | 7 +- crates/coordinator/src/lib.rs | 3 +- crates/jmap-proto/src/request/capability.rs | 46 +- crates/registry/src/schema/mod.rs | 21 + crates/registry/src/schema/prelude.rs | 1 + crates/registry/src/types/error.rs | 132 ++-- crates/registry/src/types/mod.rs | 7 + crates/store/src/dispatch/registry.rs | 9 +- crates/store/src/registry/mod.rs | 7 + tests/Cargo.toml | 4 +- 56 files changed, 1328 insertions(+), 2038 deletions(-) delete mode 100644 crates/common/src/config/imap.rs delete mode 100644 crates/common/src/config/jmap/settings.rs rename crates/common/src/config/{jmap => mailstore}/capabilities.rs (77%) create mode 100644 crates/common/src/config/mailstore/email.rs create mode 100644 crates/common/src/config/mailstore/imap.rs create mode 100644 crates/common/src/config/mailstore/jmap.rs rename crates/common/src/config/{jmap => mailstore}/mod.rs (66%) create mode 100644 crates/common/src/config/mailstore/scripts.rs rename crates/common/src/config/{ => mailstore}/spamfilter.rs (97%) delete mode 100644 crates/common/src/config/scripts.rs create mode 100644 crates/common/src/manager/bootstrap.rs delete mode 100644 crates/common/src/manager/config.rs diff --git a/Cargo.lock b/Cargo.lock index e7da8efe..2a9fa8cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1103,6 +1103,7 @@ dependencies = [ "pwhash", "rcgen 0.12.1", "regex", + "registry", "reqwest", "ring", "rkyv", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index e9d97ded..5e3bbe32 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -12,6 +12,7 @@ trc = { path = "../trc" } directory = { path = "../directory" } coordinator = { path = "../coordinator" } types = { path = "../types" } +registry = { path = "../registry" } jmap_proto = { path = "../jmap-proto" } imap_proto = { path = "../imap-proto" } sieve-rs = { version = "0.7", features = ["rkyv", "serde"] } diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs index 06949669..b7e24bf6 100644 --- a/crates/common/src/addresses.rs +++ b/crates/common/src/addresses.rs @@ -135,7 +135,7 @@ impl Server { } impl AddressMapping { - pub fn parse(config: &mut Config, key: impl AsKey) -> Self { + pub fn parse(bp: &mut Bootstrap, key: impl AsKey) -> Self { let key = key.as_key(); if let Some(value) = config.value(key.as_str()) { match value { diff --git a/crates/common/src/auth/oauth/config.rs b/crates/common/src/auth/oauth/config.rs index 69454e3d..aa10570a 100644 --- a/crates/common/src/auth/oauth/config.rs +++ b/crates/common/src/auth/oauth/config.rs @@ -46,7 +46,7 @@ pub struct OAuthConfig { } impl OAuthConfig { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let oidc_signature_algorithm = match config.value("oauth.oidc.signature-algorithm") { Some(alg) => match alg.to_uppercase().as_str() { "HS256" => SignatureAlgorithm::HS256, @@ -220,7 +220,7 @@ impl Default for OAuthConfig { } } -fn parse_rsa_key(config: &mut Config) -> Option<(Secret, AlgorithmParameters)> { +fn parse_rsa_key(bp: &mut Bootstrap) -> Option<(Secret, AlgorithmParameters)> { let rsa_key_pair = match build_rsa_keypair(config.value_require("oauth.oidc.signature-key")?) { Ok(key) => key, Err(err) => { @@ -258,7 +258,7 @@ fn parse_rsa_key(config: &mut Config) -> Option<(Secret, AlgorithmParameters)> { } fn parse_ecdsa_key( - config: &mut Config, + bp: &mut Bootstrap, oidc_signature_algorithm: SignatureAlgorithm, ) -> Option<(Secret, AlgorithmParameters)> { let (alg, curve) = match oidc_signature_algorithm { diff --git a/crates/common/src/config/groupware.rs b/crates/common/src/config/groupware.rs index 28ceb36d..bb1f74b1 100644 --- a/crates/common/src/config/groupware.rs +++ b/crates/common/src/config/groupware.rs @@ -77,7 +77,7 @@ pub enum CalendarTemplateVariable { } impl GroupwareConfig { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { GroupwareConfig { max_request_size: config .property("dav.request.max-size") diff --git a/crates/common/src/config/imap.rs b/crates/common/src/config/imap.rs deleted file mode 100644 index ae3c6cde..00000000 --- a/crates/common/src/config/imap.rs +++ /dev/null @@ -1,54 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::time::Duration; - -use utils::config::{Config, Rate}; - -#[derive(Default, Clone)] -pub struct ImapConfig { - pub max_request_size: usize, - pub max_auth_failures: u32, - pub allow_plain_auth: bool, - - pub timeout_auth: Duration, - pub timeout_unauth: Duration, - pub timeout_idle: Duration, - - pub rate_requests: Option, - pub rate_concurrent: Option, -} - -impl ImapConfig { - pub fn parse(config: &mut Config) -> Self { - ImapConfig { - max_request_size: config - .property_or_default("imap.request.max-size", "52428800") - .unwrap_or(52428800), - max_auth_failures: config - .property_or_default("imap.auth.max-failures", "3") - .unwrap_or(3), - timeout_auth: config - .property_or_default("imap.timeout.authenticated", "30m") - .unwrap_or_else(|| Duration::from_secs(1800)), - timeout_unauth: config - .property_or_default("imap.timeout.anonymous", "1m") - .unwrap_or_else(|| Duration::from_secs(60)), - timeout_idle: config - .property_or_default("imap.timeout.idle", "30m") - .unwrap_or_else(|| Duration::from_secs(1800)), - rate_requests: config - .property_or_default::>("imap.rate-limit.requests", "2000/1m") - .unwrap_or_default(), - rate_concurrent: config - .property::>("imap.rate-limit.concurrent") - .unwrap_or_default(), - allow_plain_auth: config - .property_or_default("imap.auth.allow-plain-text", "false") - .unwrap_or(false), - } - } -} diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index 8d6da9ba..519b0751 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -27,12 +27,11 @@ use std::{ }; use utils::{ cache::{Cache, CacheWithTtl}, - config::Config, snowflake::SnowflakeIdGenerator, }; impl Data { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { // Parse certificates let mut certificates = AHashMap::new(); let mut subject_names = AHashSet::new(); @@ -79,7 +78,7 @@ impl Data { } impl Caches { - pub fn parse(config: &mut Config) -> Self { + 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; diff --git a/crates/common/src/config/jmap/settings.rs b/crates/common/src/config/jmap/settings.rs deleted file mode 100644 index 971ad86c..00000000 --- a/crates/common/src/config/jmap/settings.rs +++ /dev/null @@ -1,431 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::config::groupware::GroupwareConfig; -use ahash::{AHashMap, AHashSet}; -use jmap_proto::request::capability::BaseCapabilities; -use nlp::language::Language; -use std::{str::FromStr, time::Duration}; -use store::{search::SearchField, write::SearchIndex}; -use types::{collection::Collection, special_use::SpecialUse}; -use utils::{ - config::{Config, Rate, cron::SimpleCron, utils::ParseValue}, - map::bitmap::Bitmap, -}; - -#[derive(Default, Clone)] -pub struct JmapConfig { - pub default_language: Language, - pub query_max_results: usize, - pub snippet_max_results: usize, - - pub changes_max_results: Option, - pub changes_max_history: Option, - pub share_notification_max_history: Option, - - pub request_max_size: usize, - pub request_max_calls: usize, - pub request_max_concurrent: Option, - - pub get_max_objects: usize, - pub set_max_objects: usize, - - pub upload_max_size: usize, - pub upload_max_concurrent: Option, - - pub upload_tmp_quota_size: usize, - pub upload_tmp_quota_amount: usize, - pub upload_tmp_ttl: u64, - - pub mailbox_max_depth: usize, - pub mailbox_name_max_len: usize, - pub mail_attachments_max_size: usize, - pub mail_parse_max_items: usize, - pub mail_max_size: usize, - pub mail_autoexpunge_after: Option, - pub email_submission_autoexpunge_after: Option, - - pub contact_parse_max_items: usize, - pub calendar_parse_max_items: usize, - - pub sieve_max_script_name: usize, - pub max_objects: [u32; Collection::MAX], - - pub rate_authenticated: Option, - pub rate_anonymous: Option, - - pub event_source_throttle: Duration, - pub push_attempt_interval: Duration, - pub push_attempts_max: u32, - pub push_retry_interval: Duration, - pub push_timeout: Duration, - pub push_verify_timeout: Duration, - pub push_throttle: Duration, - - pub web_socket_throttle: Duration, - pub web_socket_timeout: Duration, - pub web_socket_heartbeat: Duration, - - pub fallback_admin: Option<(String, String)>, - pub master_user: Option<(String, String)>, - - pub default_folders: Vec, - pub shared_folder: String, - - pub http_headers: Vec<(hyper::header::HeaderName, hyper::header::HeaderValue)>, - pub http_use_forwarded: bool, - - pub encrypt: bool, - pub encrypt_append: bool, - - pub index_batch_size: usize, - pub index_fields: AHashMap>, - - pub capabilities: BaseCapabilities, - pub account_purge_frequency: SimpleCron, -} - -#[derive(Clone, Debug)] -pub struct DefaultFolder { - pub name: String, - pub aliases: Vec, - pub special_use: SpecialUse, - pub subscribe: bool, - pub create: bool, -} - -impl JmapConfig { - pub fn parse(config: &mut Config, groupware_config: &GroupwareConfig) -> Self { - // Parse HTTP headers - let mut http_headers = config - .values("http.headers") - .map(|(_, v)| { - if let Some((k, v)) = v.split_once(':') { - Ok(( - hyper::header::HeaderName::from_str(k.trim()).map_err(|err| { - format!("Invalid header found in property \"http.headers\": {}", err) - })?, - hyper::header::HeaderValue::from_str(v.trim()).map_err(|err| { - format!("Invalid header found in property \"http.headers\": {}", err) - })?, - )) - } else { - Err(format!( - "Invalid header found in property \"http.headers\": {}", - v - )) - } - }) - .collect::, String>>() - .map_err(|e| config.new_parse_error("http.headers", e)) - .unwrap_or_default(); - - // Parse default folders - let mut default_folders = Vec::new(); - let mut shared_folder = "Shared Folders".to_string(); - for key in config.sub_keys("email.folders", ".name") { - match SpecialUse::parse_value(&key) { - Ok(SpecialUse::Shared) => { - if let Some(value) = config.value(("email.folders", key.as_str(), "name")) { - shared_folder = value.to_string(); - } - } - Ok(special_use) => { - let subscribe = config - .property_or_default(("email.folders", key.as_str(), "subscribe"), "true") - .unwrap_or(true); - let create = config - .property_or_default(("email.folders", key.as_str(), "create"), "true") - .unwrap_or(true) - | [SpecialUse::Inbox, SpecialUse::Trash, SpecialUse::Junk] - .contains(&special_use); - if let Some(name) = config - .value(("email.folders", key.as_str(), "name")) - .map(|name| name.trim()) - .filter(|name| !name.is_empty()) - { - default_folders.push(DefaultFolder { - name: name.to_string(), - aliases: config - .value(("email.folders", key.as_str(), "aliases")) - .unwrap_or_default() - .split(',') - .map(|s| s.trim().to_string()) - .filter(|s| !s.is_empty()) - .collect(), - special_use, - subscribe, - create, - }); - } - } - Err(err) => { - config.new_parse_error(key, err); - } - } - } - for (special_use, name) in [ - (SpecialUse::Inbox, "Inbox"), - (SpecialUse::Trash, "Deleted Items"), - (SpecialUse::Junk, "Junk Mail"), - (SpecialUse::Drafts, "Drafts"), - (SpecialUse::Sent, "Sent Items"), - ] { - if !default_folders.iter().any(|f| f.special_use == special_use) { - default_folders.push(DefaultFolder { - name: name.to_string(), - aliases: Vec::new(), - special_use, - subscribe: true, - create: true, - }); - } - } - - // Add permissive CORS headers - if config - .property::("http.permissive-cors") - .unwrap_or(false) - { - http_headers.push(( - hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN, - hyper::header::HeaderValue::from_static("*"), - )); - http_headers.push(( - hyper::header::ACCESS_CONTROL_ALLOW_HEADERS, - hyper::header::HeaderValue::from_static( - "Authorization, Content-Type, Accept, X-Requested-With", - ), - )); - http_headers.push(( - hyper::header::ACCESS_CONTROL_ALLOW_METHODS, - hyper::header::HeaderValue::from_static( - "POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS", - ), - )); - } - - // Add HTTP Strict Transport Security - if config.property::("http.hsts").unwrap_or(false) { - http_headers.push(( - hyper::header::STRICT_TRANSPORT_SECURITY, - hyper::header::HeaderValue::from_static("max-age=31536000; includeSubDomains"), - )); - } - - let mut jmap = JmapConfig { - default_language: Language::from_iso_639( - config - .value("storage.search-index.default-language") - .unwrap_or("en"), - ) - .unwrap_or(Language::English), - query_max_results: config - .property("jmap.protocol.query.max-results") - .unwrap_or(5000), - changes_max_results: config - .property_or_default::>("jmap.protocol.changes.max-results", "5000") - .unwrap_or_default(), - changes_max_history: config - .property_or_default::>("changes.max-history", "10000") - .unwrap_or_default(), - share_notification_max_history: config - .property_or_default::>("sharing.max-history", "30d") - .unwrap_or_default(), - snippet_max_results: config - .property("jmap.protocol.search-snippet.max-results") - .unwrap_or(100), - request_max_size: config - .property("jmap.protocol.request.max-size") - .unwrap_or(10000000), - request_max_calls: config - .property("jmap.protocol.request.max-calls") - .unwrap_or(16), - request_max_concurrent: config - .property_or_default::>("jmap.protocol.request.max-concurrent", "4") - .unwrap_or(Some(4)), - get_max_objects: config - .property("jmap.protocol.get.max-objects") - .unwrap_or(500), - set_max_objects: config - .property("jmap.protocol.set.max-objects") - .unwrap_or(500), - upload_max_size: config - .property("jmap.protocol.upload.max-size") - .unwrap_or(50000000), - upload_max_concurrent: config - .property_or_default::>("jmap.protocol.upload.max-concurrent", "4") - .unwrap_or(Some(4)), - upload_tmp_quota_size: config - .property("jmap.protocol.upload.quota.size") - .unwrap_or(50000000), - upload_tmp_quota_amount: config - .property("jmap.protocol.upload.quota.files") - .unwrap_or(1000), - upload_tmp_ttl: config - .property_or_default::("jmap.protocol.upload.ttl", "1h") - .unwrap_or_else(|| Duration::from_secs(3600)) - .as_secs(), - mailbox_max_depth: config.property("jmap.mailbox.max-depth").unwrap_or(10), - mailbox_name_max_len: config - .property("jmap.mailbox.max-name-length") - .unwrap_or(255), - mail_attachments_max_size: config - .property("jmap.email.max-attachment-size") - .unwrap_or(50000000), - mail_max_size: config.property("jmap.email.max-size").unwrap_or(75000000), - mail_parse_max_items: config.property("jmap.email.parse.max-items").unwrap_or(10), - mail_autoexpunge_after: config - .property_or_default::>("email.auto-expunge", "30d") - .map(|d| d.map(|d| d.as_secs())) - .unwrap_or_default(), - email_submission_autoexpunge_after: config - .property_or_default::>("email-submission.auto-expunge", "3d") - .map(|d| d.map(|d| d.as_secs())) - .unwrap_or_default(), - sieve_max_script_name: config - .property("sieve.untrusted.limits.name-length") - .unwrap_or(512), - max_objects: [u32::MAX; Collection::MAX], - capabilities: BaseCapabilities::default(), - rate_authenticated: config - .property_or_default::>("http.rate-limit.account", "1000/1m") - .unwrap_or_default(), - rate_anonymous: config - .property_or_default::>("http.rate-limit.anonymous", "100/1m") - .unwrap_or_default(), - event_source_throttle: config - .property_or_default("jmap.event-source.throttle", "1s") - .unwrap_or_else(|| Duration::from_secs(1)), - web_socket_throttle: config - .property_or_default("jmap.web-socket.throttle", "1s") - .unwrap_or_else(|| Duration::from_secs(1)), - web_socket_timeout: config - .property_or_default("jmap.web-socket.timeout", "10m") - .unwrap_or_else(|| Duration::from_secs(10 * 60)), - web_socket_heartbeat: config - .property_or_default("jmap.web-socket.heartbeat", "1m") - .unwrap_or_else(|| Duration::from_secs(60)), - encrypt: config - .property_or_default("email.encryption.enable", "true") - .unwrap_or(true), - encrypt_append: config - .property_or_default("email.encryption.append", "false") - .unwrap_or(false), - http_use_forwarded: config.property("http.use-x-forwarded").unwrap_or(false), - http_headers, - push_attempt_interval: config - .property_or_default("jmap.push.attempts.interval", "1m") - .unwrap_or_else(|| Duration::from_secs(60)), - push_attempts_max: config - .property_or_default("jmap.push.attempts.max", "3") - .unwrap_or(3), - push_retry_interval: config - .property_or_default("jmap.push.retry.interval", "1s") - .unwrap_or_else(|| Duration::from_secs(1)), - push_timeout: config - .property_or_default("jmap.push.timeout.request", "10s") - .unwrap_or_else(|| Duration::from_secs(10)), - push_verify_timeout: config - .property_or_default("jmap.push.timeout.verify", "1m") - .unwrap_or_else(|| Duration::from_secs(60)), - push_throttle: config - .property_or_default("jmap.push.throttle", "1s") - .unwrap_or_else(|| Duration::from_secs(1)), - account_purge_frequency: config - .property_or_default::("account.purge.frequency", "0 0 *") - .unwrap_or_else(|| SimpleCron::parse_value("0 0 *").unwrap()), - fallback_admin: config - .value("authentication.fallback-admin.user") - .and_then(|u| { - config - .value("authentication.fallback-admin.secret") - .map(|p| (u.to_string(), p.to_string())) - }), - master_user: config.value("authentication.master.user").and_then(|u| { - config - .value("authentication.master.secret") - .map(|p| (u.to_string(), p.to_string())) - }), - contact_parse_max_items: config - .property("jmap.contact.parse.max-items") - .unwrap_or(10), - calendar_parse_max_items: config - .property("jmap.calendar.parse.max-items") - .unwrap_or(10), - index_batch_size: config - .property("storage.search-index.batch-size") - .unwrap_or(100), - index_fields: AHashMap::new(), - default_folders, - shared_folder, - }; - - // Parse index fields - for index in [ - SearchIndex::Email, - SearchIndex::Contacts, - SearchIndex::Calendar, - SearchIndex::Tracing, - ] { - let mut fields = AHashSet::new(); - let index_name = match index { - SearchIndex::Email => "email", - SearchIndex::Contacts => "contacts", - SearchIndex::Calendar => "calendar", - SearchIndex::Tracing => "tracing", - _ => unreachable!(), - }; - - if !config - .property_or_default::( - &format!("storage.search-index.{index_name}.enabled"), - "true", - ) - .unwrap_or(true) - { - continue; - } - - for (_, field) in config - .properties::(&format!("storage.search-index.{index_name}.fields")) - { - fields.insert(field); - } - jmap.index_fields.insert(index, fields); - } - - for collection in Bitmap::::all() { - let key = format!("object-quota.{}", collection.as_config_case()); - jmap.max_objects[collection as usize] = - if let Some(value) = config.property::(&key) { - value - } else { - match collection { - Collection::Mailbox => 250, - Collection::SieveScript => 100, - Collection::Identity => 20, - Collection::EmailSubmission => 500, - Collection::PushSubscription => 15, - Collection::Calendar => 250, - Collection::AddressBook => 250, - Collection::Principal - | Collection::None - | Collection::CalendarEventNotification - | Collection::CalendarEvent - | Collection::ContactCard - | Collection::FileNode - | Collection::Email - | Collection::Thread => u32::MAX, - } - }; - } - - // Add capabilities - jmap.add_capabilities(config, groupware_config); - jmap - } -} diff --git a/crates/common/src/config/jmap/capabilities.rs b/crates/common/src/config/mailstore/capabilities.rs similarity index 77% rename from crates/common/src/config/jmap/capabilities.rs rename to crates/common/src/config/mailstore/capabilities.rs index bdb7f5e4..e2fde2c0 100644 --- a/crates/common/src/config/jmap/capabilities.rs +++ b/crates/common/src/config/mailstore/capabilities.rs @@ -4,8 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::settings::JmapConfig; -use crate::config::groupware::GroupwareConfig; +use crate::{config::mailstore::jmap::JmapConfig, manager::bootstrap::Bootstrap}; use ahash::AHashSet; use calcard::icalendar::ICalendarDuration; use chrono::{DateTime, Utc}; @@ -19,24 +18,26 @@ use jmap_proto::{ }, types::date::UTCDate, }; -use types::{collection::Collection, type_state::DataType}; -use utils::{config::Config, map::vec_map::VecMap}; +use registry::{ + schema::structs::{Calendar, Email, SieveUserInterpreter}, + types::EnumType, +}; +use types::type_state::DataType; +use utils::map::vec_map::VecMap; impl JmapConfig { - pub fn add_capabilities(&mut self, config: &mut Config, groupware_config: &GroupwareConfig) { + pub async fn add_capabilities(&mut self, bp: &mut Bootstrap) { // Add core capabilities self.capabilities.session.append( Capability::Core, Capabilities::Core(CoreCapabilities { - max_size_upload: self.upload_max_size, - max_concurrent_upload: self.upload_max_concurrent.unwrap_or(u32::MAX as u64) - as usize, - max_size_request: self.request_max_size, - max_concurrent_requests: self.request_max_concurrent.unwrap_or(u32::MAX as u64) - as usize, - max_calls_in_request: self.request_max_calls, - max_objects_in_get: self.get_max_objects, - max_objects_in_set: self.set_max_objects, + max_size_upload: self.upload_max_size as u64, + max_concurrent_upload: self.upload_max_concurrent.unwrap_or(u32::MAX as u64), + max_size_request: self.request_max_size as u64, + max_concurrent_requests: self.request_max_concurrent.unwrap_or(u32::MAX as u64), + max_calls_in_request: self.request_max_calls as u64, + max_objects_in_get: self.get_max_objects as u64, + max_objects_in_set: self.set_max_objects as u64, collation_algorithms: vec![ "i;ascii-numeric".to_string(), "i;ascii-casemap".to_string(), @@ -46,6 +47,7 @@ impl JmapConfig { ); // Add email capabilities + let email = bp.setting_infallible::().await; self.capabilities.session.append( Capability::Mail, Capabilities::Empty(EmptyCapabilities::default()), @@ -54,9 +56,9 @@ impl JmapConfig { Capability::Mail, Capabilities::Mail(MailCapabilities { max_mailboxes_per_email: None, - max_mailbox_depth: self.mailbox_max_depth, - max_size_mailbox_name: self.mailbox_name_max_len, - max_size_attachments_per_email: self.mail_attachments_max_size, + max_mailbox_depth: email.max_mailbox_depth, + max_size_mailbox_name: email.max_mailbox_name_length, + max_size_attachments_per_email: email.max_attachment_size, email_query_sort_options: vec![ EmailComparator::ReceivedAt, EmailComparator::Size, @@ -85,7 +87,11 @@ impl JmapConfig { max_date_time: UTCDate::from_timestamp(DateTime::::MAX_UTC.timestamp()), max_expanded_query_duration: ICalendarDuration::from_seconds(86400 * 365) .to_string(), - max_participants_per_event: groupware_config.max_ical_attendees_per_instance.into(), + max_participants_per_event: bp + .setting_infallible::() + .await + .max_attendees + .into(), may_create_calendar: true, }), ); @@ -188,25 +194,16 @@ impl JmapConfig { ); // Add Sieve capabilities - let mut notification_methods = Vec::new(); - - for (_, uri) in config.values("sieve.untrusted.notification-uris") { - notification_methods.push(uri.to_string()); - } - if notification_methods.is_empty() { - notification_methods.push("mailto".to_string()); - } - - let mut capabilities: AHashSet = - AHashSet::from_iter(sieve::compiler::grammar::Capability::all().iter().cloned()); - - for (_, capability) in config.values("sieve.untrusted.disabled-capabilities") { - capabilities.remove(&sieve::compiler::grammar::Capability::parse(capability)); - } - - let mut extensions = capabilities + let sieve = bp.setting_infallible::().await; + let disabled_capabilities = sieve + .disable_capabilities .into_iter() + .map(|v| v.as_str()) + .collect::>(); + let mut extensions = sieve::compiler::grammar::Capability::all() + .iter() .map(|c| c.to_string()) + .filter(|c| !disabled_capabilities.contains(c.as_str())) .collect::>(); extensions.sort_unstable(); @@ -217,17 +214,13 @@ impl JmapConfig { self.capabilities.account.insert( Capability::Sieve, Capabilities::SieveAccount(SieveAccountCapabilities { - max_script_name: self.sieve_max_script_name, - max_script_size: config - .property("sieve.untrusted.max-script-size") - .unwrap_or(1024 * 1024), - max_scripts: self.max_objects[Collection::SieveScript as usize] as usize, - max_redirects: config - .property("sieve.untrusted.max-redirects") - .unwrap_or(1), + max_script_name: sieve.max_script_name_length as u64, + max_script_size: sieve.max_script_size, + max_scripts: sieve.max_scripts.unwrap_or(u32::MAX as u64), + max_redirects: sieve.max_redirects, extensions, - notification_methods: if !notification_methods.is_empty() { - notification_methods.into() + notification_methods: if !sieve.allowed_notify_uris.is_empty() { + sieve.allowed_notify_uris.into() } else { None }, @@ -243,8 +236,8 @@ impl JmapConfig { self.capabilities.account.insert( Capability::Blob, Capabilities::Blob(BlobCapabilities { - max_size_blob_set: (self.request_max_size * 3 / 4) - 512, - max_data_sources: self.request_max_calls, + max_size_blob_set: (self.request_max_size as u64 * 3 / 4) - 512, + max_data_sources: self.request_max_calls as u64, supported_type_names: vec![ DataType::Email, DataType::Thread, diff --git a/crates/common/src/config/mailstore/email.rs b/crates/common/src/config/mailstore/email.rs new file mode 100644 index 00000000..2cbe5f7d --- /dev/null +++ b/crates/common/src/config/mailstore/email.rs @@ -0,0 +1,240 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::manager::bootstrap::Bootstrap; +use ahash::{AHashMap, AHashSet}; +use nlp::language::Language; +use registry::{ + schema::{ + enums::{SearchCalendarField, SearchContactField, SearchEmailField}, + structs::{ + AddressBook, Calendar, DataRetention, Email, Jmap, Search, SieveUserInterpreter, + }, + }, + types::EnumType, +}; +use std::time::Duration; +use store::{ + search::{CalendarSearchField, ContactSearchField, EmailSearchField, SearchField}, + write::SearchIndex, +}; +use types::{collection::Collection, special_use::SpecialUse}; +use utils::config::cron::SimpleCron; + +pub struct EmailConfig { + pub default_language: Language, + + pub mailbox_max_depth: usize, + pub mailbox_name_max_len: usize, + + pub mail_attachments_max_size: usize, + pub mail_max_size: usize, + pub mail_autoexpunge_after: Option, + pub email_submission_autoexpunge_after: Option, + + pub changes_max_history: Option, + pub share_notification_max_history: Option, + + pub sieve_max_script_name: usize, + + pub default_folders: Vec, + pub shared_folder: String, + + pub encrypt: bool, + pub encrypt_append: bool, + + pub index_batch_size: usize, + pub index_fields: AHashMap>, + + pub max_objects: [u32; Collection::MAX], + + pub account_purge_frequency: SimpleCron, +} + +#[derive(Clone, Debug)] +pub struct DefaultFolder { + pub name: String, + pub aliases: Vec, + pub special_use: SpecialUse, + pub subscribe: bool, + pub create: bool, +} + +impl EmailConfig { + pub async fn parse(bp: &mut Bootstrap) -> Self { + let email = bp.setting_infallible::().await; + let dr = bp.setting_infallible::().await; + let sieve = bp.setting_infallible::().await; + let search = bp.setting_infallible::().await; + let jmap = bp.setting_infallible::().await; + let calendar = bp.setting_infallible::().await; + let address_book = bp.setting_infallible::().await; + + // Parse default object quotas + let mut max_objects = [u32::MAX; Collection::MAX]; + for (collection, max) in [ + (Collection::Mailbox, email.max_mailboxes), + (Collection::SieveScript, sieve.max_scripts), + (Collection::Identity, email.max_identities), + (Collection::EmailSubmission, email.max_submissions), + (Collection::PushSubscription, jmap.max_subscriptions), + (Collection::Calendar, calendar.max_calendars), + (Collection::CalendarEvent, calendar.max_events), + (Collection::AddressBook, address_book.max_address_books), + (Collection::ContactCard, address_book.max_contacts), + ] { + if let Some(max) = max { + max_objects[collection as usize] = max as u32; + } + } + + // Parse default folders + let mut default_folders = Vec::new(); + let mut shared_folder = "Shared Folders".to_string(); + for (special_use, folder) in email.default_folders { + let special_use = match special_use { + registry::schema::enums::SpecialUse::Inbox => SpecialUse::Inbox, + registry::schema::enums::SpecialUse::Trash => SpecialUse::Trash, + registry::schema::enums::SpecialUse::Junk => SpecialUse::Junk, + registry::schema::enums::SpecialUse::Drafts => SpecialUse::Drafts, + registry::schema::enums::SpecialUse::Archive => SpecialUse::Archive, + registry::schema::enums::SpecialUse::Sent => SpecialUse::Sent, + registry::schema::enums::SpecialUse::Important => SpecialUse::Important, + registry::schema::enums::SpecialUse::Memos => SpecialUse::Memos, + registry::schema::enums::SpecialUse::Scheduled => SpecialUse::Scheduled, + registry::schema::enums::SpecialUse::Snoozed => SpecialUse::Snoozed, + registry::schema::enums::SpecialUse::Shared => { + shared_folder = folder.name; + continue; + } + }; + default_folders.push(DefaultFolder { + name: folder.name, + aliases: folder.aliases, + special_use, + subscribe: folder.subscribe, + create: folder.create + || matches!( + special_use, + SpecialUse::Inbox | SpecialUse::Trash | SpecialUse::Junk + ), + }); + } + for (special_use, name) in [ + (SpecialUse::Inbox, "Inbox"), + (SpecialUse::Trash, "Deleted Items"), + (SpecialUse::Junk, "Junk Mail"), + (SpecialUse::Drafts, "Drafts"), + (SpecialUse::Sent, "Sent Items"), + ] { + if !default_folders.iter().any(|f| f.special_use == special_use) { + default_folders.push(DefaultFolder { + name: name.to_string(), + aliases: Vec::new(), + special_use, + subscribe: true, + create: true, + }); + } + } + + // Search Index settings + let mut index_fields = AHashMap::new(); + if search.index_email { + index_fields.insert( + SearchIndex::Email, + search + .index_email_fields + .into_iter() + .map(|field| { + SearchField::Email(match field { + SearchEmailField::From => EmailSearchField::From, + SearchEmailField::To => EmailSearchField::To, + SearchEmailField::Cc => EmailSearchField::Cc, + SearchEmailField::Bcc => EmailSearchField::Bcc, + SearchEmailField::Subject => EmailSearchField::Subject, + SearchEmailField::Body => EmailSearchField::Body, + SearchEmailField::Attachment => EmailSearchField::Attachment, + SearchEmailField::ReceivedAt => EmailSearchField::ReceivedAt, + SearchEmailField::SentAt => EmailSearchField::SentAt, + SearchEmailField::Size => EmailSearchField::Size, + SearchEmailField::HasAttachment => EmailSearchField::HasAttachment, + SearchEmailField::Headers => EmailSearchField::Headers, + }) + }) + .collect(), + ); + } + if search.index_contacts { + index_fields.insert( + SearchIndex::Contacts, + search + .index_contact_fields + .into_iter() + .map(|field| { + SearchField::Contact(match field { + SearchContactField::Member => ContactSearchField::Member, + SearchContactField::Kind => ContactSearchField::Kind, + SearchContactField::Name => ContactSearchField::Name, + SearchContactField::Nickname => ContactSearchField::Nickname, + SearchContactField::Organization => ContactSearchField::Organization, + SearchContactField::Email => ContactSearchField::Email, + SearchContactField::Phone => ContactSearchField::Phone, + SearchContactField::OnlineService => ContactSearchField::OnlineService, + SearchContactField::Address => ContactSearchField::Address, + SearchContactField::Note => ContactSearchField::Note, + SearchContactField::Uid => ContactSearchField::Uid, + }) + }) + .collect(), + ); + } + if search.index_calendar { + index_fields.insert( + SearchIndex::Calendar, + search + .index_calendar_fields + .into_iter() + .map(|field| { + SearchField::Calendar(match field { + SearchCalendarField::Title => CalendarSearchField::Title, + SearchCalendarField::Description => CalendarSearchField::Description, + SearchCalendarField::Location => CalendarSearchField::Location, + SearchCalendarField::Owner => CalendarSearchField::Owner, + SearchCalendarField::Attendee => CalendarSearchField::Attendee, + SearchCalendarField::Start => CalendarSearchField::Start, + SearchCalendarField::Uid => CalendarSearchField::Uid, + }) + }) + .collect(), + ); + } + + EmailConfig { + default_language: Language::from_iso_639(search.default_language.as_str()) + .unwrap_or(Language::English), + mailbox_max_depth: email.max_mailbox_depth as usize, + mailbox_name_max_len: email.max_mailbox_name_length as usize, + mail_attachments_max_size: email.max_attachment_size as usize, + mail_max_size: email.max_message_size as usize, + mail_autoexpunge_after: dr.expunge_trash_after.map(|d| d.into_inner().as_secs()), + email_submission_autoexpunge_after: dr + .expunge_submissions_after + .map(|d| d.into_inner().as_secs()), + changes_max_history: dr.max_changes_history.map(|v| v as usize), + share_notification_max_history: dr.expunge_share_notify_after.map(|v| v.into_inner()), + sieve_max_script_name: sieve.max_script_name_length as usize, + encrypt: email.encrypt_at_rest, + encrypt_append: email.encrypt_on_append, + index_batch_size: search.index_batch_size as usize, + index_fields, + max_objects, + account_purge_frequency: dr.expunge_schedule.into(), + default_folders, + shared_folder, + } + } +} diff --git a/crates/common/src/config/mailstore/imap.rs b/crates/common/src/config/mailstore/imap.rs new file mode 100644 index 00000000..caf63a15 --- /dev/null +++ b/crates/common/src/config/mailstore/imap.rs @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::manager::bootstrap::Bootstrap; +use registry::schema::structs::{Imap, Rate}; +use std::time::Duration; + +#[derive(Default, Clone)] +pub struct ImapConfig { + pub max_request_size: usize, + pub max_auth_failures: u32, + pub allow_plain_auth: bool, + + pub timeout_auth: Duration, + pub timeout_unauth: Duration, + pub timeout_idle: Duration, + + pub rate_requests: Option, + pub rate_concurrent: Option, +} + +impl ImapConfig { + pub async fn parse(bp: &mut Bootstrap) -> Self { + let imap = bp.setting_infallible::().await; + + ImapConfig { + max_request_size: imap.max_request_size as usize, + max_auth_failures: imap.max_auth_failures as u32, + timeout_auth: imap.timeout_authenticated.into_inner(), + timeout_unauth: imap.timeout_anonymous.into_inner(), + timeout_idle: imap.timeout_idle.into_inner(), + rate_requests: imap.max_request_rate, + rate_concurrent: imap.max_concurrent, + allow_plain_auth: imap.allow_plain_text_auth, + } + } +} diff --git a/crates/common/src/config/mailstore/jmap.rs b/crates/common/src/config/mailstore/jmap.rs new file mode 100644 index 00000000..548ab9da --- /dev/null +++ b/crates/common/src/config/mailstore/jmap.rs @@ -0,0 +1,89 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::manager::bootstrap::Bootstrap; +use jmap_proto::request::capability::BaseCapabilities; +use registry::schema::structs::Jmap; +use std::time::Duration; + +#[derive(Default, Clone)] +pub struct JmapConfig { + pub query_max_results: usize, + pub snippet_max_results: usize, + pub changes_max_results: usize, + + pub request_max_size: usize, + pub request_max_calls: usize, + pub request_max_concurrent: Option, + + pub get_max_objects: usize, + pub set_max_objects: usize, + + pub upload_max_size: usize, + pub upload_max_concurrent: Option, + + pub upload_tmp_quota_size: usize, + pub upload_tmp_quota_amount: usize, + pub upload_tmp_ttl: u64, + + pub mail_parse_max_items: usize, + pub contact_parse_max_items: usize, + pub calendar_parse_max_items: usize, + + pub event_source_throttle: Duration, + pub push_attempt_interval: Duration, + pub push_attempts_max: u32, + pub push_retry_interval: Duration, + pub push_timeout: Duration, + pub push_verify_timeout: Duration, + pub push_throttle: Duration, + + pub web_socket_throttle: Duration, + pub web_socket_timeout: Duration, + pub web_socket_heartbeat: Duration, + + pub capabilities: BaseCapabilities, +} + +impl JmapConfig { + pub async fn parse(bp: &mut Bootstrap) -> Self { + let jmap = bp.setting_infallible::().await; + + let mut jmap = JmapConfig { + query_max_results: jmap.query_max_results as usize, + changes_max_results: jmap.changes_max_results as usize, + snippet_max_results: jmap.snippet_max_results as usize, + request_max_size: jmap.max_request_size as usize, + request_max_calls: jmap.max_method_calls as usize, + request_max_concurrent: jmap.max_concurrent_requests, + get_max_objects: jmap.get_max_results as usize, + set_max_objects: jmap.set_max_objects as usize, + upload_max_size: jmap.max_upload_size as usize, + upload_max_concurrent: jmap.max_concurrent_uploads, + upload_tmp_quota_size: jmap.upload_quota as usize, + upload_tmp_quota_amount: jmap.max_upload_count as usize, + upload_tmp_ttl: jmap.upload_ttl.into_inner().as_secs(), + mail_parse_max_items: jmap.parse_limit_email as usize, + contact_parse_max_items: jmap.parse_limit_contact as usize, + calendar_parse_max_items: jmap.parse_limit_event as usize, + event_source_throttle: jmap.event_source_throttle.into_inner(), + web_socket_throttle: jmap.websocket_throttle.into_inner(), + web_socket_timeout: jmap.websocket_timeout.into_inner(), + web_socket_heartbeat: jmap.websocket_heartbeat.into_inner(), + push_attempt_interval: jmap.push_attempt_wait.into_inner(), + push_attempts_max: jmap.push_max_attempts as u32, + push_retry_interval: jmap.push_retry_wait.into_inner(), + push_timeout: jmap.push_request_timeout.into_inner(), + push_verify_timeout: jmap.push_verify_timeout.into_inner(), + push_throttle: jmap.push_throttle.into_inner(), + capabilities: BaseCapabilities::default(), + }; + + // Add capabilities + jmap.add_capabilities(bp); + jmap + } +} diff --git a/crates/common/src/config/jmap/mod.rs b/crates/common/src/config/mailstore/mod.rs similarity index 66% rename from crates/common/src/config/jmap/mod.rs rename to crates/common/src/config/mailstore/mod.rs index c5882d20..cd528066 100644 --- a/crates/common/src/config/jmap/mod.rs +++ b/crates/common/src/config/mailstore/mod.rs @@ -5,4 +5,8 @@ */ pub mod capabilities; -pub mod settings; +pub mod email; +pub mod imap; +pub mod jmap; +pub mod scripts; +pub mod spamfilter; diff --git a/crates/common/src/config/mailstore/scripts.rs b/crates/common/src/config/mailstore/scripts.rs new file mode 100644 index 00000000..54fd2eb2 --- /dev/null +++ b/crates/common/src/config/mailstore/scripts.rs @@ -0,0 +1,256 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + VERSION_PUBLIC, + config::smtp::SMTP_RCPT_TO_VARS, + expr::{if_block::IfBlock, tokenizer::TokenMap}, + manager::bootstrap::Bootstrap, + scripts::{ + functions::{register_functions_trusted, register_functions_untrusted}, + plugins::RegisterSievePlugins, + }, +}; +use ahash::AHashMap; +use registry::{ + schema::{ + prelude::{Object, Property}, + structs::{ + SieveSystemInterpreter, SieveSystemScript, SieveUserInterpreter, SieveUserScript, + }, + }, + types::EnumType, +}; +use sieve::{Compiler, Runtime, Sieve, compiler::grammar::Capability}; +use std::sync::Arc; + +pub struct Scripting { + pub untrusted_compiler: Compiler, + pub untrusted_runtime: Runtime, + pub trusted_runtime: Runtime, + pub from_addr: IfBlock, + pub from_name: IfBlock, + pub return_path: IfBlock, + pub sign: IfBlock, + pub trusted_scripts: AHashMap>, + pub untrusted_scripts: AHashMap>, +} + +impl Scripting { + pub async fn parse(bp: &mut Bootstrap) -> Self { + // Parse untrusted compiler + let untrusted = bp.setting_infallible::().await; + let mut fnc_map_untrusted = register_functions_untrusted().register_plugins_untrusted(); + let untrusted_compiler = Compiler::new() + .with_max_script_size(untrusted.max_script_size as usize) + .with_max_string_size(untrusted.max_string_length as usize) + .with_max_variable_name_size(untrusted.max_var_name_length as usize) + .with_max_nested_blocks(untrusted.max_nested_blocks as usize) + .with_max_nested_tests(untrusted.max_nested_tests as usize) + .with_max_nested_foreverypart(untrusted.max_nested_for_every as usize) + .with_max_match_variables(untrusted.max_match_vars as usize) + .with_max_local_variables(untrusted.max_local_vars as usize) + .with_max_header_size(untrusted.max_header_size as usize) + .with_max_includes(untrusted.max_includes as usize) + .register_functions(&mut fnc_map_untrusted); + + // Parse untrusted runtime + let untrusted_runtime = Runtime::new() + .with_functions(&mut fnc_map_untrusted) + .with_max_nested_includes(untrusted.max_nested_includes as usize) + .with_cpu_limit(untrusted.max_cpu_cycles as usize) + .with_max_variable_size(untrusted.max_var_size as usize) + .with_max_redirects(untrusted.max_redirects as usize) + .with_max_received_headers(untrusted.max_received_headers as usize) + .with_max_header_size(untrusted.max_header_size as usize) + .with_max_out_messages(untrusted.max_out_messages as usize) + .with_default_vacation_expiry(untrusted.default_expiry_vacation.into_inner().as_secs()) + .with_default_duplicate_expiry( + untrusted.default_expiry_duplicate.into_inner().as_secs(), + ) + .with_capability(Capability::Expressions) + .without_capabilities( + untrusted + .disable_capabilities + .iter() + .map(|cap| cap.as_str()), + ) + .with_valid_notification_uris(untrusted.allowed_notify_uris) + .with_protected_headers(untrusted.protected_headers) + .with_vacation_default_subject(untrusted.default_subject) + .with_vacation_subject_prefix(untrusted.default_subject_prefix) + .with_env_variable("name", "Stalwart Server") + .with_env_variable("version", VERSION_PUBLIC) + .with_env_variable("location", "MS") + .with_env_variable("phase", "during"); + + // Parse trusted compiler and runtime + let mut fnc_map_trusted = register_functions_trusted().register_plugins_trusted(); + + // Allocate compiler and runtime + let trusted = bp.setting_infallible::().await; + let trusted_compiler = Compiler::new() + .with_max_string_size(52428800) + .with_max_variable_name_size(100) + .with_max_nested_blocks(50) + .with_max_nested_tests(50) + .with_max_nested_foreverypart(10) + .with_max_local_variables(8192) + .with_max_header_size(10240) + .with_max_includes(10) + .with_no_capability_check(trusted.no_capability_check) + .register_functions(&mut fnc_map_trusted); + + let mut trusted_runtime = Runtime::new() + .without_capabilities([ + Capability::FileInto, + Capability::Vacation, + Capability::VacationSeconds, + Capability::Fcc, + Capability::Mailbox, + Capability::MailboxId, + Capability::MboxMetadata, + Capability::ServerMetadata, + Capability::ImapSieve, + Capability::Duplicate, + ]) + .with_capability(Capability::Expressions) + .with_capability(Capability::While) + .with_max_variable_size(trusted.max_var_size as usize) + .with_max_header_size(10240) + .with_valid_notification_uri("mailto") + //.with_valid_ext_lists(stores.in_memory_stores.keys().map(|k| k.to_string())) + .with_functions(&mut fnc_map_trusted) + .with_max_redirects(trusted.max_redirects as usize) + .with_max_out_messages(trusted.max_out_messages as usize) + .with_cpu_limit(trusted.max_cpu_cycles as usize) + .with_max_nested_includes(trusted.max_nested_includes as usize) + .with_max_received_headers(trusted.max_received_headers as usize) + .with_default_duplicate_expiry(trusted.duplicate_expiry.into_inner().as_secs()); + trusted_runtime.set_local_hostname(bp.node.hostname.clone()); + + // Parse trusted scripts + let mut trusted_scripts = AHashMap::new(); + for script in bp.list_infallible::().await { + match trusted_compiler.compile(script.object.contents.as_bytes()) { + Ok(compiled) => { + trusted_scripts.insert(script.object.name, compiled.into()); + } + Err(err) => { + bp.build_error( + script.id, + format!("Failed to compile system Sieve script: {err}"), + ); + } + } + } + + // Parse untrusted scripts + let mut untrusted_scripts = AHashMap::new(); + for script in bp.list_infallible::().await { + match untrusted_compiler.compile(script.object.contents.as_bytes()) { + Ok(compiled) => { + untrusted_scripts.insert(script.object.name, compiled.into()); + } + Err(err) => { + bp.build_error( + script.id, + format!("Failed to compile user global Sieve script: {err}"), + ); + } + } + } + + let token_map = TokenMap::default().with_variables(SMTP_RCPT_TO_VARS); + + let mut scripting = Scripting { + untrusted_compiler, + untrusted_runtime, + trusted_runtime, + untrusted_scripts, + trusted_scripts, + ..Default::default() + }; + + for (property, from, to) in [ + ( + Property::DefaultFromAddress, + trusted.default_from_address, + &mut scripting.from_addr, + ), + ( + Property::DefaultFromName, + trusted.default_from_name, + &mut scripting.from_name, + ), + ( + Property::DefaultReturnPath, + trusted.default_return_path, + &mut scripting.return_path, + ), + ( + Property::DkimSignDomain, + trusted.dkim_sign_domain, + &mut scripting.sign, + ), + ] { + if let Some(if_block) = IfBlock::try_parse( + bp, + Object::SieveSystemScript.singleton(), + property, + from, + &token_map, + ) { + *to = if_block; + } + } + + scripting + } +} + +impl Default for Scripting { + fn default() -> Self { + let script = SieveSystemInterpreter::default(); + + Scripting { + untrusted_compiler: Compiler::new(), + untrusted_runtime: Runtime::new(), + trusted_runtime: Runtime::new(), + from_addr: IfBlock::new_default::<()>( + Property::DefaultFromAddress, + script.default_from_address, + ), + from_name: IfBlock::new_default::<()>( + Property::DefaultFromName, + script.default_from_name, + ), + return_path: IfBlock::new_default::<()>( + Property::DefaultReturnPath, + script.default_return_path, + ), + sign: IfBlock::new_default::<()>(Property::DkimSignDomain, script.dkim_sign_domain), + untrusted_scripts: AHashMap::new(), + trusted_scripts: AHashMap::new(), + } + } +} + +impl Clone for Scripting { + fn clone(&self) -> Self { + Self { + untrusted_compiler: self.untrusted_compiler.clone(), + untrusted_runtime: self.untrusted_runtime.clone(), + trusted_runtime: self.trusted_runtime.clone(), + from_addr: self.from_addr.clone(), + from_name: self.from_name.clone(), + return_path: self.return_path.clone(), + sign: self.sign.clone(), + trusted_scripts: self.trusted_scripts.clone(), + untrusted_scripts: self.untrusted_scripts.clone(), + } + } +} diff --git a/crates/common/src/config/spamfilter.rs b/crates/common/src/config/mailstore/spamfilter.rs similarity index 97% rename from crates/common/src/config/spamfilter.rs rename to crates/common/src/config/mailstore/spamfilter.rs index 2c228a05..f8908575 100644 --- a/crates/common/src/config/spamfilter.rs +++ b/crates/common/src/config/mailstore/spamfilter.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{Variable, functions::ResolveVariable, if_block::IfBlock, tokenizer::TokenMap}; +use crate::{expr::if_block::IfBlock, manager::bootstrap::Bootstrap}; use ahash::AHashSet; use mail_auth::common::resolver::ToReverseName; use nlp::classifier::model::{CcfhClassifier, FhClassifier}; @@ -13,11 +13,7 @@ use std::{ time::Duration, }; use tokio::net::lookup_host; -use utils::{ - cache::CacheItemWeight, - config::{Config, utils::ParseValue}, - glob::GlobMap, -}; +use utils::{cache::CacheItemWeight, config::utils::ParseValue, glob::GlobMap}; #[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default)] pub enum SpamClassifier { @@ -174,7 +170,7 @@ pub struct DnsBlServer { } impl SpamFilterConfig { - pub async fn parse(config: &mut Config) -> Self { + pub async fn parse(bp: &mut Bootstrap) -> Self { SpamFilterConfig { enabled: config .property_or_default("spam-filter.enable", "true") @@ -200,7 +196,7 @@ impl SpamFilterConfig { } impl SpamFilterRules { - pub fn parse(config: &mut Config) -> SpamFilterRules { + pub fn parse(bp: &mut Bootstrap) -> SpamFilterRules { let mut rules = vec![]; for id in config.sub_keys("spam-filter.rule", ".scope") { if let Some(rule) = SpamFilterRule::parse(config, id) { @@ -234,7 +230,7 @@ struct SpamFilterRule { } impl SpamFilterRule { - pub fn parse(config: &mut Config, id: String) -> Option { + pub fn parse(bp: &mut Bootstrap, id: String) -> Option { let id = id.as_str(); if !config .property_or_default(("spam-filter.rule", id, "enable"), "true") @@ -263,7 +259,7 @@ impl SpamFilterRule { } impl DnsBlConfig { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let mut servers = vec![]; for id in config.sub_keys("spam-filter.dnsbl.server", ".scope") { if let Some(server) = DnsBlServer::parse(config, id) { @@ -290,7 +286,7 @@ impl DnsBlConfig { } impl DnsBlServer { - pub fn parse(config: &mut Config, id: String) -> Option { + pub fn parse(bp: &mut Bootstrap, id: String) -> Option { let id_ = id.as_str(); if !config @@ -322,7 +318,7 @@ impl DnsBlServer { } impl SpamFilterLists { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let mut lists = SpamFilterLists { file_extensions: GlobMap::default(), scores: GlobMap::default(), @@ -397,7 +393,7 @@ impl SpamFilterLists { } impl PyzorConfig { - pub async fn parse(config: &mut Config) -> Option { + pub async fn parse(bp: &mut Bootstrap) -> Option { if !config .property_or_default("spam-filter.pyzor.enable", "true") .unwrap_or(true) @@ -452,7 +448,7 @@ impl PyzorConfig { } impl ClassifierConfig { - pub fn parse(config: &mut Config) -> Option { + pub fn parse(bp: &mut Bootstrap) -> Option { let ccfh = match config.value("spam-filter.classifier.model") { Some("ftrl-fh") | None => false, Some("ftrl-ccfh") => true, @@ -524,7 +520,7 @@ impl ClassifierConfig { } impl FtrlParameters { - pub fn parse(config: &mut Config, prefix: &str, default_features: usize) -> Self { + pub fn parse(bp: &mut Bootstrap, prefix: &str, default_features: usize) -> Self { let feature_hash_size: usize = config .property((prefix, "features")) .unwrap_or(default_features); @@ -561,7 +557,7 @@ impl SpamClassifier { } impl SpamFilterScoreConfig { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { SpamFilterScoreConfig { reject_threshold: config .property("spam-filter.score.reject") diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 61147eee..c59dcd26 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -4,13 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use self::{ - imap::ImapConfig, jmap::settings::JmapConfig, scripts::Scripting, smtp::SmtpConfig, - storage::Storage, -}; +use self::{mailstore::jmap::JmapConfig, smtp::SmtpConfig, storage::Storage}; use crate::{ Core, Network, Security, auth::oauth::config::OAuthConfig, expr::*, - listener::tls::AcmeProviders, manager::config::ConfigManager, + listener::tls::AcmeProviders, }; use arc_swap::ArcSwap; use coordinator::Coordinator; @@ -18,21 +15,17 @@ use directory::{Directories, Directory}; use groupware::GroupwareConfig; use hyper::HeaderMap; use ring::signature::{EcdsaKeyPair, RsaKeyPair}; -use spamfilter::SpamFilterConfig; use std::sync::Arc; use store::{BlobBackend, BlobStore, InMemoryStore, SearchStore, Store, Stores}; use telemetry::Metrics; -use utils::config::{Config, utils::AsKey}; +use utils::config::utils::AsKey; pub mod groupware; -pub mod imap; pub mod inner; -pub mod jmap; +pub mod mailstore; pub mod network; -pub mod scripts; pub mod server; pub mod smtp; -pub mod spamfilter; pub mod storage; pub mod telemetry; @@ -50,7 +43,7 @@ pub(crate) const CONNECTION_VARS: &[u32; 9] = &[ impl Core { pub async fn parse( - config: &mut Config, + bp: &mut Bootstrap, mut stores: Stores, config_manager: ConfigManager, ) -> Self { diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index e77d6e77..a68a8789 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -18,12 +18,21 @@ pub struct Network { pub server_name: String, pub report_domain: String, pub security: Security, + pub http: Http, pub contact_form: Option, - pub http_response_url: IfBlock, - pub http_allowed_endpoint: IfBlock, pub asn_geo_lookup: AsnGeoLookupConfig, } +#[derive(Clone)] +pub struct Http { + pub rate_authenticated: Option, + pub rate_anonymous: Option, + pub response_url: IfBlock, + pub allowed_endpoint: IfBlock, + pub response_headers: Vec<(hyper::header::HeaderName, hyper::header::HeaderValue)>, + pub use_forwarded: bool, +} + #[derive(Clone)] pub struct ContactForm { pub rcpt_to: Vec, @@ -110,12 +119,12 @@ impl Default for Network { security: Default::default(), contact_form: None, node_id: 1, - http_response_url: IfBlock::new::<()>( + http_response_url: IfBlock::new_default::<()>( "http.url", [], "protocol + '://' + config_get('server.hostname') + ':' + local_port", ), - http_allowed_endpoint: IfBlock::new::<()>("http.allowed-endpoint", [], "200"), + http_allowed_endpoint: IfBlock::new_default::<()>("http.allowed-endpoint", [], "200"), asn_geo_lookup: AsnGeoLookupConfig::Disabled, server_name: Default::default(), report_domain: Default::default(), @@ -125,7 +134,7 @@ impl Default for Network { } impl ContactForm { - pub fn parse(config: &mut Config) -> Option { + pub fn parse(bp: &mut Bootstrap) -> Option { if !config .property_or_default::("form.enable", "false") .unwrap_or_default() @@ -167,7 +176,7 @@ impl ContactForm { } impl FieldOrDefault { - pub fn parse(config: &mut Config, key: &str, default: &str) -> Self { + pub fn parse(bp: &mut Bootstrap, key: &str, default: &str) -> Self { FieldOrDefault { field: config.value((key, "field")).map(|s| s.to_string()), default: config @@ -179,7 +188,7 @@ impl FieldOrDefault { } impl Network { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let server_name = config .value("server.hostname") .map(|v| v.to_string()) @@ -300,6 +309,78 @@ impl Network { } } +impl Http { + pub fn parse(bp: &mut Bootstrap) -> Option { + // Parse HTTP headers + let mut http_headers = config + .values("http.headers") + .map(|(_, v)| { + if let Some((k, v)) = v.split_once(':') { + Ok(( + hyper::header::HeaderName::from_str(k.trim()).map_err(|err| { + format!("Invalid header found in property \"http.headers\": {}", err) + })?, + hyper::header::HeaderValue::from_str(v.trim()).map_err(|err| { + format!("Invalid header found in property \"http.headers\": {}", err) + })?, + )) + } else { + Err(format!( + "Invalid header found in property \"http.headers\": {}", + v + )) + } + }) + .collect::, String>>() + .map_err(|e| config.new_parse_error("http.headers", e)) + .unwrap_or_default(); + // Add permissive CORS headers + if config + .property::("http.permissive-cors") + .unwrap_or(false) + { + http_headers.push(( + hyper::header::ACCESS_CONTROL_ALLOW_ORIGIN, + hyper::header::HeaderValue::from_static("*"), + )); + http_headers.push(( + hyper::header::ACCESS_CONTROL_ALLOW_HEADERS, + hyper::header::HeaderValue::from_static( + "Authorization, Content-Type, Accept, X-Requested-With", + ), + )); + http_headers.push(( + hyper::header::ACCESS_CONTROL_ALLOW_METHODS, + hyper::header::HeaderValue::from_static( + "POST, GET, PATCH, PUT, DELETE, HEAD, OPTIONS", + ), + )); + } + + // Add HTTP Strict Transport Security + if config.property::("http.hsts").unwrap_or(false) { + http_headers.push(( + hyper::header::STRICT_TRANSPORT_SECURITY, + hyper::header::HeaderValue::from_static("max-age=31536000; includeSubDomains"), + )); + } + // http_use_forwarded: config.property("http.use-x-forwarded").unwrap_or(false), + + /* + + rate_authenticated: jmap + .property_or_default::>("http.rate-limit.account", "1000/1m") + .unwrap_or_default(), + rate_anonymous: jmap + .property_or_default::>("http.rate-limit.anonymous", "100/1m") + .unwrap_or_default(), + + */ + + todo!() + } +} + struct NodeList(AHashSet); impl ParseValue for NodeList { @@ -313,7 +394,7 @@ impl ParseValue for NodeList { } impl AsnGeoLookupConfig { - pub fn parse(config: &mut Config) -> Option { + pub fn parse(bp: &mut Bootstrap) -> Option { match config.value("asn.type")? { "dns" => AsnGeoLookupConfig::Dns { zone_ipv4: config.value_require_non_empty("asn.zone.ipv4")?.to_string(), diff --git a/crates/common/src/config/scripts.rs b/crates/common/src/config/scripts.rs deleted file mode 100644 index 2d82a239..00000000 --- a/crates/common/src/config/scripts.rs +++ /dev/null @@ -1,393 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::{sync::Arc, time::Duration}; - -use ahash::AHashMap; -use sieve::{Compiler, Runtime, Sieve, compiler::grammar::Capability}; -use store::Stores; -use utils::config::Config; - -use crate::{ - VERSION_PUBLIC, - scripts::{ - functions::{register_functions_trusted, register_functions_untrusted}, - plugins::RegisterSievePlugins, - }, -}; - -use super::{if_block::IfBlock, smtp::SMTP_RCPT_TO_VARS, tokenizer::TokenMap}; - -pub struct Scripting { - pub untrusted_compiler: Compiler, - pub untrusted_runtime: Runtime, - pub trusted_runtime: Runtime, - pub from_addr: IfBlock, - pub from_name: IfBlock, - pub return_path: IfBlock, - pub sign: IfBlock, - pub trusted_scripts: AHashMap>, - pub untrusted_scripts: AHashMap>, -} - -impl Scripting { - pub async fn parse(config: &mut Config, stores: &Stores) -> Self { - // Parse untrusted compiler - let mut fnc_map_untrusted = register_functions_untrusted().register_plugins_untrusted(); - let untrusted_compiler = Compiler::new() - .with_max_script_size( - config - .property("sieve.untrusted.limits.script-size") - .unwrap_or(1024 * 1024), - ) - .with_max_string_size( - config - .property("sieve.untrusted.limits.string-length") - .unwrap_or(4096), - ) - .with_max_variable_name_size( - config - .property("sieve.untrusted.limits.variable-name-length") - .unwrap_or(32), - ) - .with_max_nested_blocks( - config - .property("sieve.untrusted.limits.nested-blocks") - .unwrap_or(15), - ) - .with_max_nested_tests( - config - .property("sieve.untrusted.limits.nested-tests") - .unwrap_or(15), - ) - .with_max_nested_foreverypart( - config - .property("sieve.untrusted.limits.nested-foreverypart") - .unwrap_or(3), - ) - .with_max_match_variables( - config - .property("sieve.untrusted.limits.match-variables") - .unwrap_or(30), - ) - .with_max_local_variables( - config - .property("sieve.untrusted.limits.local-variables") - .unwrap_or(128), - ) - .with_max_header_size( - config - .property("sieve.untrusted.limits.header-size") - .unwrap_or(1024), - ) - .with_max_includes( - config - .property("sieve.untrusted.limits.includes") - .unwrap_or(3), - ) - .register_functions(&mut fnc_map_untrusted); - - // Parse untrusted runtime - let untrusted_runtime = Runtime::new() - .with_functions(&mut fnc_map_untrusted) - .with_max_nested_includes( - config - .property("sieve.untrusted.limits.nested-includes") - .unwrap_or(3), - ) - .with_cpu_limit( - config - .property("sieve.untrusted.limits.cpu") - .unwrap_or(5000), - ) - .with_max_variable_size( - config - .property("sieve.untrusted.limits.variable-size") - .unwrap_or(4096), - ) - .with_max_redirects( - config - .property("sieve.untrusted.limits.redirects") - .unwrap_or(1), - ) - .with_max_received_headers( - config - .property("sieve.untrusted.limits.received-headers") - .unwrap_or(10), - ) - .with_max_header_size( - config - .property("sieve.untrusted.limits.header-size") - .unwrap_or(1024), - ) - .with_max_out_messages( - config - .property("sieve.untrusted.limits.outgoing-messages") - .unwrap_or(3), - ) - .with_default_vacation_expiry( - config - .property::("sieve.untrusted.default-expiry.vacation") - .unwrap_or(Duration::from_secs(30 * 86400)) - .as_secs(), - ) - .with_default_duplicate_expiry( - config - .property::("sieve.untrusted.default-expiry.duplicate") - .unwrap_or(Duration::from_secs(7 * 86400)) - .as_secs(), - ) - .with_capability(Capability::Expressions) - .without_capabilities( - config - .values("sieve.untrusted.disable-capabilities") - .map(|(_, v)| v), - ) - .with_valid_notification_uris({ - let values = config - .values("sieve.untrusted.notification-uris") - .map(|(_, v)| v.to_string()) - .collect::>(); - if !values.is_empty() { - values - } else { - vec!["mailto".to_string()] - } - }) - .with_protected_headers({ - let values = config - .values("sieve.untrusted.protected-headers") - .map(|(_, v)| v.to_string()) - .collect::>(); - if !values.is_empty() { - values - } else { - vec![ - "Original-Subject".to_string(), - "Original-From".to_string(), - "Received".to_string(), - "Auto-Submitted".to_string(), - ] - } - }) - .with_vacation_default_subject( - config - .value("sieve.untrusted.vacation.default-subject") - .unwrap_or("Automated reply") - .to_string(), - ) - .with_vacation_subject_prefix( - config - .value("sieve.untrusted.vacation.subject-prefix") - .unwrap_or("Auto: ") - .to_string(), - ) - .with_env_variable("name", "Stalwart Server") - .with_env_variable("version", VERSION_PUBLIC) - .with_env_variable("location", "MS") - .with_env_variable("phase", "during"); - - // Parse trusted compiler and runtime - let mut fnc_map_trusted = register_functions_trusted().register_plugins_trusted(); - - // Allocate compiler and runtime - let trusted_compiler = Compiler::new() - .with_max_string_size(52428800) - .with_max_variable_name_size(100) - .with_max_nested_blocks(50) - .with_max_nested_tests(50) - .with_max_nested_foreverypart(10) - .with_max_local_variables(8192) - .with_max_header_size(10240) - .with_max_includes(10) - .with_no_capability_check( - config - .property_or_default("sieve.trusted.no-capability-check", "true") - .unwrap_or(true), - ) - .register_functions(&mut fnc_map_trusted); - - let mut trusted_runtime = Runtime::new() - .without_capabilities([ - Capability::FileInto, - Capability::Vacation, - Capability::VacationSeconds, - Capability::Fcc, - Capability::Mailbox, - Capability::MailboxId, - Capability::MboxMetadata, - Capability::ServerMetadata, - Capability::ImapSieve, - Capability::Duplicate, - ]) - .with_capability(Capability::Expressions) - .with_capability(Capability::While) - .with_max_variable_size( - config - .property_or_default("sieve.trusted.limits.variable-size", "52428800") - .unwrap_or(52428800), - ) - .with_max_header_size(10240) - .with_valid_notification_uri("mailto") - .with_valid_ext_lists(stores.in_memory_stores.keys().map(|k| k.to_string())) - .with_functions(&mut fnc_map_trusted) - .with_max_redirects( - config - .property_or_default("sieve.trusted.limits.redirects", "3") - .unwrap_or(3), - ) - .with_max_out_messages( - config - .property_or_default("sieve.trusted.limits.out-messages", "5") - .unwrap_or(5), - ) - .with_cpu_limit( - config - .property_or_default("sieve.trusted.limits.cpu", "1048576") - .unwrap_or(1048576), - ) - .with_max_nested_includes( - config - .property_or_default("sieve.trusted.limits.nested-includes", "5") - .unwrap_or(5), - ) - .with_max_received_headers( - config - .property_or_default("sieve.trusted.limits.received-headers", "50") - .unwrap_or(50), - ) - .with_default_duplicate_expiry( - config - .property_or_default::("sieve.trusted.limits.duplicate-expiry", "7d") - .unwrap_or_else(|| Duration::from_secs(604800)) - .as_secs(), - ); - - let hostname = config - .value("sieve.trusted.hostname") - .or_else(|| config.value("server.hostname")) - .unwrap_or("localhost") - .to_string(); - trusted_runtime.set_local_hostname(hostname.clone()); - - // Parse trusted scripts - let mut trusted_scripts = AHashMap::new(); - for id in config.sub_keys("sieve.trusted.scripts", ".contents") { - match trusted_compiler.compile( - config - .value(("sieve.trusted.scripts", id.as_str(), "contents")) - .unwrap() - .as_bytes(), - ) { - Ok(compiled) => { - trusted_scripts.insert(id, compiled.into()); - } - Err(err) => config.new_build_error( - ("sieve.trusted.scripts", id.as_str(), "contents"), - format!("Failed to compile trusted Sieve script: {err}"), - ), - } - } - - // Parse untrusted scripts - let mut untrusted_scripts = AHashMap::new(); - for id in config.sub_keys("sieve.untrusted.scripts", ".contents") { - match untrusted_compiler.compile( - config - .value(("sieve.untrusted.scripts", id.as_str(), "contents")) - .unwrap() - .as_bytes(), - ) { - Ok(compiled) => { - untrusted_scripts.insert(id, compiled.into()); - } - Err(err) => config.new_build_error( - ("sieve.untrusted.scripts", id.as_str(), "contents"), - format!("Failed to compile untrusted Sieve script: {err}"), - ), - } - } - - let token_map = TokenMap::default().with_variables(SMTP_RCPT_TO_VARS); - - Scripting { - untrusted_compiler, - untrusted_runtime, - trusted_runtime, - from_addr: IfBlock::try_parse(config, "sieve.trusted.from-addr", &token_map) - .unwrap_or_else(|| { - IfBlock::new::<()>( - "sieve.trusted.from-addr", - [], - "'MAILER-DAEMON@' + config_get('report.domain')", - ) - }), - from_name: IfBlock::try_parse(config, "sieve.trusted.from-name", &token_map) - .unwrap_or_else(|| { - IfBlock::new::<()>("sieve.trusted.from-name", [], "'Automated Message'") - }), - return_path: IfBlock::try_parse(config, "sieve.trusted.return-path", &token_map) - .unwrap_or_else(|| IfBlock::empty("sieve.trusted.return-path")), - sign: IfBlock::try_parse(config, "sieve.trusted.sign", &token_map).unwrap_or_else( - || { - IfBlock::new::<()>( - "sieve.trusted.sign", - [], - concat!( - "['rsa-' + config_get('report.domain'), ", - "'ed25519-' + config_get('report.domain')]" - ), - ) - }, - ), - untrusted_scripts, - trusted_scripts, - } - } -} - -impl Default for Scripting { - fn default() -> Self { - Scripting { - untrusted_compiler: Compiler::new(), - untrusted_runtime: Runtime::new(), - trusted_runtime: Runtime::new(), - from_addr: IfBlock::new::<()>( - "sieve.trusted.from-addr", - [], - "'MAILER-DAEMON@' + config_get('report.domain')", - ), - from_name: IfBlock::new::<()>("sieve.trusted.from-name", [], "'Mailer Daemon'"), - return_path: IfBlock::empty("sieve.trusted.return-path"), - sign: IfBlock::new::<()>( - "sieve.trusted.sign", - [], - concat!( - "['rsa-' + config_get('report.domain'), ", - "'ed25519-' + config_get('report.domain')]" - ), - ), - untrusted_scripts: AHashMap::new(), - trusted_scripts: AHashMap::new(), - } - } -} - -impl Clone for Scripting { - fn clone(&self) -> Self { - Self { - untrusted_compiler: self.untrusted_compiler.clone(), - untrusted_runtime: self.untrusted_runtime.clone(), - trusted_runtime: self.trusted_runtime.clone(), - from_addr: self.from_addr.clone(), - from_name: self.from_name.clone(), - return_path: self.return_path.clone(), - sign: self.sign.clone(), - trusted_scripts: self.trusted_scripts.clone(), - untrusted_scripts: self.untrusted_scripts.clone(), - } - } -} diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index 68e8d61f..17bf0064 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -32,7 +32,7 @@ use super::{ }; impl Listeners { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { // Parse ACME managers let mut servers = Listeners { span_id_gen: Arc::new( @@ -51,7 +51,7 @@ impl Listeners { servers } - fn parse_server(&mut self, config: &mut Config, id_: String) { + fn parse_server(&mut self, bp: &mut Bootstrap, id_: String) { // Parse protocol let id = id_.as_str(); let protocol = @@ -209,7 +209,7 @@ impl Listeners { }); } - pub fn parse_tcp_acceptors(&mut self, config: &mut Config, inner: Arc) { + pub fn parse_tcp_acceptors(&mut self, bp: &mut Bootstrap, inner: Arc) { let resolver = Arc::new(CertificateResolver::new(inner.clone())); for id_ in config.sub_keys("server.listener", ".protocol") { diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index e4e591f9..820602c0 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -44,7 +44,7 @@ pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13]; pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12]; impl AcmeProviders { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let mut providers = AHashMap::new(); // Parse ACME providers @@ -185,7 +185,7 @@ impl AcmeProviders { } #[allow(clippy::unnecessary_to_owned)] -fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option { +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)); @@ -316,7 +316,7 @@ fn build_dns_updater(config: &mut Config, acme_id: &str) -> Option { } pub(crate) fn parse_certificates( - config: &mut Config, + bp: &mut Bootstrap, certificates: &mut AHashMap>, subject_names: &mut AHashSet, ) { diff --git a/crates/common/src/config/smtp/auth.rs b/crates/common/src/config/smtp/auth.rs index 9e1e46e5..e881e263 100644 --- a/crates/common/src/config/smtp/auth.rs +++ b/crates/common/src/config/smtp/auth.rs @@ -104,8 +104,8 @@ impl Default for MailAuthConfig { fn default() -> Self { Self { dkim: DkimAuthConfig { - verify: IfBlock::new::("auth.dkim.verify", [], "relaxed"), - sign: IfBlock::new::<()>( + verify: IfBlock::new_default::("auth.dkim.verify", [], "relaxed"), + sign: IfBlock::new_default::<()>( "auth.dkim.sign", [( "is_local_domain('*', sender_domain)", @@ -116,15 +116,15 @@ impl Default for MailAuthConfig { strict: true, }, arc: ArcAuthConfig { - verify: IfBlock::new::("auth.arc.verify", [], "relaxed"), - seal: IfBlock::new::<()>( + verify: IfBlock::new_default::("auth.arc.verify", [], "relaxed"), + seal: IfBlock::new_default::<()>( "auth.arc.seal", [], "'rsa-' + config_get('report.domain')", ), }, spf: SpfAuthConfig { - verify_ehlo: IfBlock::new::( + verify_ehlo: IfBlock::new_default::( "auth.spf.verify.ehlo", [("local_port == 25", "relaxed")], #[cfg(not(feature = "test_mode"))] @@ -132,7 +132,7 @@ impl Default for MailAuthConfig { #[cfg(feature = "test_mode")] "relaxed", ), - verify_mail_from: IfBlock::new::( + verify_mail_from: IfBlock::new_default::( "auth.spf.verify.mail-from", [("local_port == 25", "relaxed")], #[cfg(not(feature = "test_mode"))] @@ -142,7 +142,7 @@ impl Default for MailAuthConfig { ), }, dmarc: DmarcAuthConfig { - verify: IfBlock::new::( + verify: IfBlock::new_default::( "auth.dmarc.verify", [("local_port == 25", "relaxed")], #[cfg(not(feature = "test_mode"))] @@ -152,7 +152,7 @@ impl Default for MailAuthConfig { ), }, iprev: IpRevAuthConfig { - verify: IfBlock::new::( + verify: IfBlock::new_default::( "auth.iprev.verify", [("local_port == 25", "relaxed")], #[cfg(not(feature = "test_mode"))] @@ -167,7 +167,7 @@ impl Default for MailAuthConfig { } impl MailAuthConfig { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let rcpt_vars = TokenMap::default() .with_variables(SMTP_RCPT_TO_VARS) .with_constants::(); @@ -236,7 +236,7 @@ impl MailAuthConfig { } } -pub fn build_signature(config: &mut Config, id: &str) -> Option<(DkimSigner, ArcSealer)> { +pub fn build_signature(bp: &mut Bootstrap, id: &str) -> Option<(DkimSigner, ArcSealer)> { match config.property_require::(("signature", id, "algorithm"))? { Algorithm::RsaSha256 => { let pk = config @@ -300,7 +300,7 @@ pub fn build_signature(config: &mut Config, id: &str) -> Option<(DkimSigner, Arc } } -fn parse_pem(config: &mut Config, key: impl AsKey) -> Option> { +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 { @@ -339,7 +339,7 @@ pub fn simple_pem_parse(contents: &str) -> Option> { } fn parse_signature>( - config: &mut Config, + bp: &mut Bootstrap, id: &str, key_dkim: T, key_arc: U, diff --git a/crates/common/src/config/smtp/mod.rs b/crates/common/src/config/smtp/mod.rs index 9dfafa36..518395ea 100644 --- a/crates/common/src/config/smtp/mod.rs +++ b/crates/common/src/config/smtp/mod.rs @@ -151,7 +151,7 @@ pub(crate) const SMTP_QUEUE_SENDER_VARS: &[u32; 8] = &[ ]; impl SmtpConfig { - pub async fn parse(config: &mut Config) -> Self { + pub async fn parse(bp: &mut Bootstrap) -> Self { Self { session: SessionConfig::parse(config), queue: QueueConfig::parse(config), diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index 78565f95..aba31629 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -189,7 +189,7 @@ pub enum RequireOptional { impl Default for QueueConfig { fn default() -> Self { Self { - route: IfBlock::new::<()>( + route: IfBlock::new_default::<()>( "queue.strategy.route", #[cfg(not(feature = "test_mode"))] [("is_local_domain('*', rcpt_domain)", "'local'")], @@ -197,7 +197,7 @@ impl Default for QueueConfig { [], "'mx'", ), - queue: IfBlock::new::<()>( + queue: IfBlock::new_default::<()>( "queue.strategy.schedule", #[cfg(not(feature = "test_mode"))] [ @@ -212,8 +212,8 @@ impl Default for QueueConfig { #[cfg(feature = "test_mode")] "'default'", ), - connection: IfBlock::new::<()>("queue.strategy.connection", [], "'default'"), - tls: IfBlock::new::<()>( + connection: IfBlock::new_default::<()>("queue.strategy.connection", [], "'default'"), + tls: IfBlock::new_default::<()>( "queue.strategy.tls", #[cfg(not(feature = "test_mode"))] [("retry_num > 0 && last_error == 'tls'", "'invalid-tls'")], @@ -222,13 +222,13 @@ impl Default for QueueConfig { "'default'", ), dsn: Dsn { - name: IfBlock::new::<()>("report.dsn.from-name", [], "'Mail Delivery Subsystem'"), - address: IfBlock::new::<()>( + name: IfBlock::new_default::<()>("report.dsn.from-name", [], "'Mail Delivery Subsystem'"), + address: IfBlock::new_default::<()>( "report.dsn.from-address", [], "'MAILER-DAEMON@' + config_get('report.domain')", ), - sign: IfBlock::new::<()>( + sign: IfBlock::new_default::<()>( "report.dsn.sign", [], "['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]", @@ -247,7 +247,7 @@ impl Default for QueueConfig { } impl QueueConfig { - pub fn parse(config: &mut Config) -> Self { + 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); @@ -291,7 +291,7 @@ impl QueueConfig { } fn parse_queue_strategies( - config: &mut Config, + bp: &mut Bootstrap, queues: &AHashMap, ) -> AHashMap { let mut entries = AHashMap::new(); @@ -313,7 +313,7 @@ fn parse_queue_strategies( } fn parse_queue_strategy( - config: &mut Config, + bp: &mut Bootstrap, id: &str, queues: &AHashMap, ) -> Option { @@ -370,7 +370,7 @@ fn parse_queue_strategy( }) } -fn parse_virtual_queues(config: &mut Config) -> AHashMap { +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) { @@ -387,7 +387,7 @@ fn parse_virtual_queues(config: &mut Config) -> AHashMap Option { +fn parse_virtual_queue(bp: &mut Bootstrap, id: &str) -> Option { Some(VirtualQueue { threads: config .property_require::(("queue.virtual", id, "threads-per-node")) @@ -395,7 +395,7 @@ fn parse_virtual_queue(config: &mut Config, id: &str) -> Option { }) } -fn parse_routing_strategies(config: &mut Config) -> AHashMap { +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) { @@ -405,7 +405,7 @@ fn parse_routing_strategies(config: &mut Config) -> AHashMap Option { +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"))?, @@ -453,7 +453,7 @@ fn parse_route(config: &mut Config, id: &str) -> Option { } } -fn parse_tls_strategies(config: &mut Config) -> AHashMap { +fn parse_tls_strategies(bp: &mut Bootstrap) -> AHashMap { let mut entries = AHashMap::new(); for key in config.sub_keys_with_suffixes( "queue.tls", @@ -472,7 +472,7 @@ fn parse_tls_strategies(config: &mut Config) -> AHashMap { entries } -fn parse_tls(config: &mut Config, id: &str) -> Option { +fn parse_tls(bp: &mut Bootstrap, id: &str) -> Option { Some(TlsStrategy { dane: config .property::(("queue.tls", id, "dane")) @@ -495,7 +495,7 @@ fn parse_tls(config: &mut Config, id: &str) -> Option { }) } -fn parse_connection_strategies(config: &mut Config) -> AHashMap { +fn parse_connection_strategies(bp: &mut Bootstrap) -> AHashMap { let mut entries = AHashMap::new(); for key in config.sub_keys_with_suffixes( "queue.connection", @@ -516,7 +516,7 @@ fn parse_connection_strategies(config: &mut Config) -> AHashMap Option { +fn parse_connection(bp: &mut Bootstrap, id: &str) -> Option { let mut source_ipv4 = Vec::new(); let mut source_ipv6 = Vec::new(); @@ -558,7 +558,7 @@ fn parse_connection(config: &mut Config, id: &str) -> Option }) } -fn parse_inbound_rate_limiters(config: &mut Config) -> QueueRateLimiters { +fn parse_inbound_rate_limiters(bp: &mut Bootstrap) -> QueueRateLimiters { let mut throttle = QueueRateLimiters::default(); let all_throttles = parse_queue_rate_limiter( config, @@ -605,7 +605,7 @@ fn parse_inbound_rate_limiters(config: &mut Config) -> QueueRateLimiters { throttle } -fn parse_outbound_rate_limiters(config: &mut Config) -> QueueRateLimiters { +fn parse_outbound_rate_limiters(bp: &mut Bootstrap) -> QueueRateLimiters { // Parse throttle let mut throttle = QueueRateLimiters::default(); @@ -643,7 +643,7 @@ fn parse_outbound_rate_limiters(config: &mut Config) -> QueueRateLimiters { throttle } -fn parse_queue_quota(config: &mut Config) -> QueueQuotas { +fn parse_queue_quota(bp: &mut Bootstrap) -> QueueQuotas { let mut capacities = QueueQuotas { sender: Vec::new(), rcpt: Vec::new(), @@ -677,7 +677,7 @@ fn parse_queue_quota(config: &mut Config) -> QueueQuotas { capacities } -fn parse_queue_quota_item(config: &mut Config, prefix: impl AsKey, id: &str) -> Option { +fn parse_queue_quota_item(bp: &mut Bootstrap, prefix: impl AsKey, id: &str) -> Option { let prefix = prefix.as_key(); // Skip disabled throttles diff --git a/crates/common/src/config/smtp/report.rs b/crates/common/src/config/smtp/report.rs index 5af5d485..c2006aa9 100644 --- a/crates/common/src/config/smtp/report.rs +++ b/crates/common/src/config/smtp/report.rs @@ -68,7 +68,7 @@ pub enum AggregateFrequency { } impl ReportConfig { - pub fn parse(config: &mut Config) -> Self { + 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); @@ -79,7 +79,7 @@ impl ReportConfig { &TokenMap::default().with_variables(RCPT_DOMAIN_VARS), ) .unwrap_or_else(|| { - IfBlock::new::<()>("report.submitter", [], "config_get('server.hostname')") + IfBlock::new_default::<()>("report.submitter", [], "config_get('server.hostname')") }), analysis: ReportAnalysis { addresses: config @@ -112,15 +112,19 @@ impl ReportConfig { } impl Report { - pub fn parse(config: &mut Config, id: &str, token_map: &TokenMap) -> Self { + pub fn parse(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Self { let mut report = Self { - name: IfBlock::new::<()>(format!("report.{id}.from-name"), [], "'Report Subsystem'"), - address: IfBlock::new::<()>( + 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::<()>( + subject: IfBlock::new_default::<()>( format!("report.{id}.subject"), [], format!( @@ -128,12 +132,12 @@ impl Report { id.to_ascii_uppercase() ), ), - sign: IfBlock::new::<()>( + sign: IfBlock::new_default::<()>( format!("report.{id}.sign"), [], "['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]", ), - send: IfBlock::new::<()>(format!("report.{id}.send"), [], "[1, 1d]"), + send: IfBlock::new_default::<()>(format!("report.{id}.send"), [], "[1, 1d]"), }; for (value, key) in [ (&mut report.name, "from-name"), @@ -152,37 +156,41 @@ impl Report { } impl AggregateReport { - pub fn parse(config: &mut Config, id: &str, token_map: &TokenMap) -> Self { + 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::<()>( + name: IfBlock::new_default::<()>( format!("report.{id}.aggregate.from-name"), [], format!("'{} Aggregate Report'", id.to_ascii_uppercase()), ), - address: IfBlock::new::<()>( + address: IfBlock::new_default::<()>( format!("report.{id}.aggregate.from-address"), [], format!("'noreply-{id}@' + config_get('report.domain')"), ), - org_name: IfBlock::new::<()>( + 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::( + send: IfBlock::new_default::( format!("report.{id}.aggregate.send"), [], "daily", ), - sign: IfBlock::new::<()>( + sign: IfBlock::new_default::<()>( format!("report.{id}.aggregate.sign"), [], "['rsa-' + config_get('report.domain'), 'ed25519-' + config_get('report.domain')]", ), - max_size: IfBlock::new::<()>(format!("report.{id}.aggregate.max-size"), [], "26214400"), + max_size: IfBlock::new_default::<()>( + format!("report.{id}.aggregate.max-size"), + [], + "26214400", + ), }; for (value, key, token_map) in [ diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index bfd418aa..86e70b65 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -102,7 +102,7 @@ impl CacheItemWeight for Policy { } impl Resolvers { - pub async fn parse(config: &mut Config) -> Self { + 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()), @@ -245,7 +245,7 @@ impl Resolvers { } impl Policy { - pub fn try_parse(config: &mut Config) -> Option { + pub fn try_parse(bp: &mut Bootstrap) -> Option { let mode = config .property_or_default::>("session.mta-sts.mode", "testing") .unwrap_or_default()?; diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index 18e8a167..6ec0edf4 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -190,7 +190,7 @@ pub enum Stage { } impl SessionConfig { - pub fn parse(config: &mut Config) -> Self { + 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); @@ -448,7 +448,7 @@ impl SessionConfig { } } -fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option { +fn parse_milter(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Option { let hostname = config .value_require(("session.milter", id, "hostname"))? .to_string(); @@ -456,7 +456,7 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option(format!("session.milter.{id}.enable"), [], "false") + IfBlock::new_default::<()>(format!("session.milter.{id}.enable"), [], "false") }), id: Arc::new(id.into()), addrs: format!("{}:{}", hostname, port) @@ -515,7 +515,7 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option Option { +fn parse_hooks(bp: &mut Bootstrap, id: &str, token_map: &TokenMap) -> Option { let mut headers = HeaderMap::new(); for (header, value) in config @@ -563,7 +563,7 @@ fn parse_hooks(config: &mut Config, id: &str, token_map: &TokenMap) -> Option(format!("session.hook.{id}.enable"), [], "false") + IfBlock::new_default::<()>(format!("session.hook.{id}.enable"), [], "false") }), id: id.to_string(), url: config @@ -589,7 +589,7 @@ fn parse_hooks(config: &mut Config, id: &str, token_map: &TokenMap) -> Option AHashSet { +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")) { @@ -626,17 +626,17 @@ fn parse_stages(config: &mut Config, prefix: &str, id: &str) -> AHashSet impl Default for SessionConfig { fn default() -> Self { Self { - timeout: IfBlock::new::<()>("session.timeout", [], "5m"), - duration: IfBlock::new::<()>("session.duration", [], "10m"), - transfer_limit: IfBlock::new::<()>("session.transfer-limit", [], "262144000"), + timeout: IfBlock::new_default::<()>("session.timeout", [], "5m"), + duration: IfBlock::new_default::<()>("session.duration", [], "10m"), + transfer_limit: IfBlock::new_default::<()>("session.transfer-limit", [], "262144000"), connect: Connect { - hostname: IfBlock::new::<()>( + hostname: IfBlock::new_default::<()>( "server.connect.hostname", [], "config_get('server.hostname')", ), script: IfBlock::empty("session.connect.script"), - greeting: IfBlock::new::<()>( + greeting: IfBlock::new_default::<()>( "session.connect.greeting", [], "config_get('server.hostname') + ' Stalwart ESMTP at your service'", @@ -644,15 +644,15 @@ impl Default for SessionConfig { }, ehlo: Ehlo { script: IfBlock::empty("session.ehlo.script"), - require: IfBlock::new::<()>("session.ehlo.require", [], "true"), - reject_non_fqdn: IfBlock::new::<()>( + require: IfBlock::new_default::<()>("session.ehlo.require", [], "true"), + reject_non_fqdn: IfBlock::new_default::<()>( "session.ehlo.reject-non-fqdn", [("local_port == 25", "true")], "false", ), }, auth: Auth { - directory: IfBlock::new::<()>( + directory: IfBlock::new_default::<()>( "session.auth.directory", #[cfg(feature = "test_mode")] [], @@ -660,7 +660,7 @@ impl Default for SessionConfig { [("local_port != 25", "'*'")], "false", ), - mechanisms: IfBlock::new::( + mechanisms: IfBlock::new_default::( "session.auth.mechanisms", [ ( @@ -671,7 +671,7 @@ impl Default for SessionConfig { ], "false", ), - require: IfBlock::new::<()>( + require: IfBlock::new_default::<()>( "session.auth.require", #[cfg(feature = "test_mode")] [], @@ -679,14 +679,18 @@ impl Default for SessionConfig { [("local_port != 25", "true")], "false", ), - must_match_sender: IfBlock::new::<()>("session.auth.must-match-sender", [], "true"), - errors_max: IfBlock::new::<()>("session.auth.errors.total", [], "3"), - errors_wait: IfBlock::new::<()>("session.auth.errors.wait", [], "5s"), + 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"), }, mail: Mail { script: IfBlock::empty("session.mail.script"), rewrite: IfBlock::empty("session.mail.rewrite"), - is_allowed: IfBlock::new::<()>( + is_allowed: IfBlock::new_default::<()>( "session.mail.is-allowed", [], "!is_empty(authenticated_as) || !key_exists('blocked-domains', sender_domain)", @@ -694,12 +698,12 @@ impl Default for SessionConfig { }, rcpt: Rcpt { script: IfBlock::empty("session.rcpt.script"), - relay: IfBlock::new::<()>( + relay: IfBlock::new_default::<()>( "session.rcpt.relay", [("!is_empty(authenticated_as)", "true")], "false", ), - directory: IfBlock::new::<()>( + directory: IfBlock::new_default::<()>( "session.rcpt.directory", [], #[cfg(feature = "test_mode")] @@ -708,48 +712,56 @@ impl Default for SessionConfig { "'*'", ), rewrite: IfBlock::empty("session.rcpt.rewrite"), - errors_max: IfBlock::new::<()>("session.rcpt.errors.total", [], "5"), - errors_wait: IfBlock::new::<()>("session.rcpt.errors.wait", [], "5s"), - max_recipients: IfBlock::new::<()>("session.rcpt.max-recipients", [], "100"), + 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, }, data: Data { script: IfBlock::empty("session.data.script"), - spam_filter: IfBlock::new::<()>("session.data.spam-filter", [], "true"), - max_messages: IfBlock::new::<()>("session.data.limits.messages", [], "10"), - max_message_size: IfBlock::new::<()>("session.data.limits.size", [], "104857600"), - max_received_headers: IfBlock::new::<()>( + spam_filter: IfBlock::new_default::<()>("session.data.spam-filter", [], "true"), + max_messages: IfBlock::new_default::<()>("session.data.limits.messages", [], "10"), + max_message_size: IfBlock::new_default::<()>( + "session.data.limits.size", + [], + "104857600", + ), + max_received_headers: IfBlock::new_default::<()>( "session.data.limits.received-headers", [], "50", ), - add_received: IfBlock::new::<()>( + add_received: IfBlock::new_default::<()>( "session.data.add-headers.received", [("local_port == 25", "true")], "false", ), - add_received_spf: IfBlock::new::<()>( + add_received_spf: IfBlock::new_default::<()>( "session.data.add-headers.received-spf", [("local_port == 25", "true")], "false", ), - add_return_path: IfBlock::new::<()>( + add_return_path: IfBlock::new_default::<()>( "session.data.add-headers.return-path", [("local_port == 25", "true")], "false", ), - add_auth_results: IfBlock::new::<()>( + add_auth_results: IfBlock::new_default::<()>( "session.data.add-headers.auth-results", [("local_port == 25", "true")], "false", ), - add_message_id: IfBlock::new::<()>( + add_message_id: IfBlock::new_default::<()>( "session.data.add-headers.message-id", [("local_port == 25", "true")], "false", ), - add_date: IfBlock::new::<()>( + add_date: IfBlock::new_default::<()>( "session.data.add-headers.date", [("local_port == 25", "true")], "false", @@ -757,36 +769,40 @@ impl Default for SessionConfig { add_delivered_to: false, }, extensions: Extensions { - pipelining: IfBlock::new::<()>("session.extensions.pipelining", [], "true"), - chunking: IfBlock::new::<()>("session.extensions.chunking", [], "true"), - requiretls: IfBlock::new::<()>("session.extensions.requiretls", [], "true"), - dsn: IfBlock::new::<()>( + 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::<()>( + vrfy: IfBlock::new_default::<()>( "session.extensions.vrfy", [("!is_empty(authenticated_as)", "true")], "false", ), - expn: IfBlock::new::<()>( + expn: IfBlock::new_default::<()>( "session.extensions.expn", [("!is_empty(authenticated_as)", "true")], "false", ), - no_soliciting: IfBlock::new::<()>("session.extensions.no-soliciting", [], "''"), - future_release: IfBlock::new::<()>( + 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::<()>( + deliver_by: IfBlock::new_default::<()>( "session.extensions.deliver-by", [("!is_empty(authenticated_as)", "15d")], "false", ), - mt_priority: IfBlock::new::( + mt_priority: IfBlock::new_default::( "session.extensions.mt-priority", [("!is_empty(authenticated_as)", "mixer")], "false", diff --git a/crates/common/src/config/smtp/throttle.rs b/crates/common/src/config/smtp/throttle.rs index d056c218..64e3ed96 100644 --- a/crates/common/src/config/smtp/throttle.rs +++ b/crates/common/src/config/smtp/throttle.rs @@ -11,7 +11,7 @@ use crate::expr::{Expression, tokenizer::TokenMap}; use super::*; pub fn parse_queue_rate_limiter( - config: &mut Config, + bp: &mut Bootstrap, prefix: impl AsKey, token_map: &TokenMap, available_rate_limiter_keys: u16, @@ -35,7 +35,7 @@ pub fn parse_queue_rate_limiter( } fn parse_queue_rate_limiter_item( - config: &mut Config, + bp: &mut Bootstrap, prefix: impl AsKey, rate_limiter_id: &str, token_map: &TokenMap, diff --git a/crates/common/src/config/storage.rs b/crates/common/src/config/storage.rs index 282c5df4..29f7023b 100644 --- a/crates/common/src/config/storage.rs +++ b/crates/common/src/config/storage.rs @@ -10,8 +10,6 @@ use directory::Directory; use std::sync::Arc; use store::{BlobStore, InMemoryStore, PurgeSchedule, SearchStore, Store}; -use crate::manager::config::ConfigManager; - #[derive(Default, Clone)] pub struct Storage { pub data: Store, @@ -22,7 +20,6 @@ pub struct Storage { pub directory: Arc, pub directories: AHashMap>, pub purge_schedules: Vec, - pub config: ConfigManager, pub stores: AHashMap, pub blobs: AHashMap, diff --git a/crates/common/src/config/telemetry.rs b/crates/common/src/config/telemetry.rs index f891163f..87ad1f30 100644 --- a/crates/common/src/config/telemetry.rs +++ b/crates/common/src/config/telemetry.rs @@ -135,7 +135,7 @@ pub struct PrometheusMetrics { } impl Telemetry { - pub fn parse(config: &mut Config, stores: &Stores) -> Self { + pub fn parse(bp: &mut Bootstrap, stores: &Stores) -> Self { let mut telemetry = Telemetry { tracers: Tracers::parse(config, stores), metrics: Interests::default(), @@ -160,7 +160,7 @@ impl Telemetry { } impl Tracers { - pub fn parse(config: &mut Config, stores: &Stores) -> Self { + pub fn parse(bp: &mut Bootstrap, stores: &Stores) -> Self { // Parse custom logging levels let mut custom_levels = AHashMap::new(); for event_name in config @@ -595,7 +595,7 @@ impl Tracers { } impl Metrics { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let mut metrics = Metrics { prometheus: None, otel: None, @@ -743,7 +743,7 @@ impl Metrics { } fn parse_webhook( - config: &mut Config, + bp: &mut Bootstrap, id: &str, global_interests: &mut Interests, ) -> Option { diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs index 7a39ad86..f3fad87d 100644 --- a/crates/common/src/core.rs +++ b/crates/common/src/core.rs @@ -8,6 +8,7 @@ use crate::{ Inner, Server, auth::{AccessToken, ResourceToken, TenantInfo}, config::{ + mailstore::spamfilter::SpamClassifier, smtp::{ auth::{ArcSealer, DkimSigner, LazySignature, ResolvedSignature, build_signature}, queue::{ @@ -15,7 +16,6 @@ use crate::{ QueueStrategy, RequireOptional, RoutingStrategy, TlsStrategy, VirtualQueue, }, }, - spamfilter::SpamClassifier, }, ipc::{BroadcastEvent, PushEvent, PushNotification}, manager::SPAM_CLASSIFIER_KEY, diff --git a/crates/common/src/enterprise/config.rs b/crates/common/src/enterprise/config.rs index 2768344e..5324f7c0 100644 --- a/crates/common/src/enterprise/config.rs +++ b/crates/common/src/enterprise/config.rs @@ -12,10 +12,7 @@ use super::{ AlertContent, AlertContentToken, AlertMethod, Enterprise, MetricAlert, MetricStore, SpamFilterLlmConfig, TraceStore, Undelete, license::LicenseKey, llm::AiApiConfig, }; -use crate::{ - expr::{Expression, tokenizer::TokenMap}, - manager::config::ConfigManager, -}; +use crate::expr::{Expression, tokenizer::TokenMap}; use ahash::AHashMap; use directory::{Type, backend::internal::manage::ManageDirectory}; use std::{sync::Arc, time::Duration}; @@ -32,7 +29,7 @@ use utils::{ impl Enterprise { pub async fn parse( - config: &mut Config, + bp: &mut Bootstrap, config_manager: &ConfigManager, stores: &Stores, data: &Store, @@ -237,7 +234,7 @@ impl Enterprise { } impl SpamFilterLlmConfig { - pub fn parse(config: &mut Config, models: &AHashMap>) -> Option { + pub fn parse(bp: &mut Bootstrap, models: &AHashMap>) -> Option { if !config .property_or_default::("spam-filter.llm.enable", "false") .unwrap_or_default() @@ -298,7 +295,7 @@ impl SpamFilterLlmConfig { } } -pub fn parse_metric_alerts(config: &mut Config) -> Vec { +pub fn parse_metric_alerts(bp: &mut Bootstrap) -> Vec { let mut alerts = Vec::new(); for metric_id in config.sub_keys("metrics.alerts", ".enable") { @@ -310,7 +307,7 @@ pub fn parse_metric_alerts(config: &mut Config) -> Vec { alerts } -fn parse_metric_alert(config: &mut Config, id: String) -> Option { +fn parse_metric_alert(bp: &mut Bootstrap, id: String) -> Option { if !config.property_or_default::(("metrics.alerts", id.as_str(), "enable"), "false")? { return None; } @@ -417,7 +414,7 @@ fn parse_metric_alert(config: &mut Config, id: String) -> Option { alert.into() } -fn parse_alert_content(key: impl AsKey, config: &mut Config) -> Option { +fn parse_alert_content(key: impl AsKey, bp: &mut Bootstrap) -> Option { let mut tokens = Vec::new(); let mut value = config.value(key)?.chars().peekable(); let mut buf = String::new(); diff --git a/crates/common/src/enterprise/llm.rs b/crates/common/src/enterprise/llm.rs index a2ec1e7c..509c81de 100644 --- a/crates/common/src/enterprise/llm.rs +++ b/crates/common/src/enterprise/llm.rs @@ -199,7 +199,7 @@ impl AiApiConfig { } } - pub fn parse(config: &mut Config, id: &str) -> Option { + pub fn parse(bp: &mut Bootstrap, id: &str) -> Option { let url = config.value(("enterprise.ai", id, "url"))?.to_string(); let api_type = match config.value(("enterprise.ai", id, "type"))? { "chat" => ApiType::ChatCompletion, diff --git a/crates/common/src/expr/eval.rs b/crates/common/src/expr/eval.rs index 41d8b5b8..3ea55f71 100644 --- a/crates/common/src/expr/eval.rs +++ b/crates/common/src/expr/eval.rs @@ -4,20 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{cmp::Ordering, fmt::Display}; - -use compact_str::{CompactString, ToCompactString, format_compact}; -use hyper::StatusCode; -use trc::EvalEvent; - -use crate::Server; - use super::{ BinaryOperator, Constant, Expression, ExpressionItem, Setting, StringCow, UnaryOperator, Variable, functions::{FUNCTIONS, ResolveVariable}, if_block::IfBlock, }; +use crate::Server; +use compact_str::{CompactString, ToCompactString, format_compact}; +use hyper::StatusCode; +use std::{cmp::Ordering, fmt::Display}; +use trc::EvalEvent; impl Server { pub async fn eval_if<'x, R: TryFrom>, V: ResolveVariable>( @@ -214,21 +211,10 @@ impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec { stack.push(self.core.core.network.server_name.as_str().into()) } - Setting::ReportDomain => { + Setting::Domain => { stack.push(self.core.core.network.report_domain.as_str().into()) } Setting::NodeId => stack.push(self.core.core.network.node_id.into()), - Setting::Other(key) => stack.push( - self.core - .core - .storage - .config - .get(key) - .await? - .unwrap_or_default() - .to_compact_string() - .into(), - ), }, ExpressionItem::UnaryOperator(op) => { let value = stack.pop().unwrap_or_default(); diff --git a/crates/common/src/expr/if_block.rs b/crates/common/src/expr/if_block.rs index c1de2ed6..224f8457 100644 --- a/crates/common/src/expr/if_block.rs +++ b/crates/common/src/expr/if_block.rs @@ -4,16 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use compact_str::CompactString; -use utils::config::{Config, utils::AsKey}; - -use crate::expr::{Constant, Expression}; - use super::{ ConstantValue, ExpressionItem, parser::ExpressionParser, tokenizer::{TokenMap, Tokenizer}, }; +use crate::{ + expr::{Constant, Expression}, + manager::bootstrap::Bootstrap, +}; +use compact_str::CompactString; +use registry::{ + schema::{prelude::Property, structs}, + types::id::Id, +}; #[derive(Debug, Clone, PartialEq, Eq)] pub struct IfThen { @@ -23,37 +27,34 @@ pub struct IfThen { #[derive(Debug, Clone, PartialEq, Eq)] pub struct IfBlock { - pub key: String, + pub property: Property, pub if_then: Vec, pub default: Expression, } impl IfBlock { - pub fn new( - key: impl Into, - if_thens: impl IntoIterator, - default: impl AsRef, - ) -> Self { + pub fn new_default(property: Property, expr: structs::Expression) -> Self { let token_map = TokenMap::default() .with_all_variables() .with_constants::(); Self { - key: key.into(), - if_then: if_thens + property, + if_then: expr + .match_ .into_iter() - .map(|(if_, then)| IfThen { - expr: Expression::parse(&token_map, if_), - then: Expression::parse(&token_map, then), + .map(|match_| IfThen { + expr: Expression::parse(&token_map, &match_.if_), + then: Expression::parse(&token_map, &match_.then), }) .collect(), - default: Expression::parse(&token_map, default.as_ref()), + default: Expression::parse(&token_map, &expr.else_), } } - pub fn empty(key: impl Into) -> Self { + pub fn empty(property: Property) -> Self { Self { - key: key.into(), + property, if_then: Default::default(), default: Expression { items: Default::default(), @@ -67,24 +68,6 @@ impl IfBlock { } impl Expression { - pub fn try_parse( - config: &mut Config, - key: impl AsKey, - token_map: &TokenMap, - ) -> Option { - if let Some(expr) = config.value(key.as_key()) { - match ExpressionParser::new(Tokenizer::new(expr, token_map)).parse() { - Ok(expr) => Some(expr), - Err(err) => { - config.new_parse_error(key, err); - None - } - } - } else { - None - } - } - fn parse(token_map: &TokenMap, expr: &str) -> Self { ExpressionParser::new(Tokenizer::new(expr, token_map)) .parse() @@ -94,129 +77,96 @@ impl Expression { impl IfBlock { pub fn try_parse( - config: &mut Config, - prefix: impl AsKey, + bp: &mut Bootstrap, + id: Id, + property: Property, + expr: structs::Expression, token_map: &TokenMap, ) -> Option { - let key = prefix.as_key(); - // Parse conditions let mut if_block = IfBlock { - key, - if_then: Default::default(), + property, + if_then: Vec::with_capacity(expr.match_.len()), default: Expression { items: Default::default(), }, }; - // Try first with a single value - if config.contains_key(if_block.key.as_str()) { - if_block.default = Expression::try_parse(config, &if_block.key, token_map)?; - return Some(if_block); + if expr.else_.is_empty() { + if !expr.match_.is_empty() { + bp.invalid_property(id, property, "Missing 'else' block in 'if' expression"); + } + return None; } - // Collect prefixes - let prefix = prefix.as_prefix(); - let keys = config - .keys - .keys() - .filter(|k| k.starts_with(&prefix)) - .cloned() - .collect::>(); - let mut found_if = false; - let mut found_else = ""; - let mut found_then = false; - let mut last_array_pos = ""; + if expr + .match_ + .iter() + .any(|m| m.if_.is_empty() || m.then.is_empty()) + { + bp.invalid_property(id, property, "All 'if' and 'then' blocks must be non-empty"); + return None; + } - for item in &keys { - let suffix_ = item.strip_prefix(&prefix).unwrap(); - - if let Some((array_pos, suffix)) = suffix_.split_once('.') { - let if_key = suffix.split_once('.').map(|(v, _)| v).unwrap_or(suffix); - if if_key == "if" { - if array_pos != last_array_pos { - if !last_array_pos.is_empty() && !found_then { - config.new_parse_error( - if_block.key, - format!( - "Missing 'then' in 'if' condition {}.", - last_array_pos.parse().unwrap_or(0) + 1, - ), - ); - return None; - } - - if_block.if_then.push(IfThen { - expr: Expression::try_parse(config, item, token_map)?, - then: Expression::default(), - }); - - found_then = false; - last_array_pos = array_pos; - } - - found_if = true; - } else if if_key == "else" { - if found_else.is_empty() { - if found_if { - if_block.default = Expression::try_parse(config, item, token_map)?; - found_else = array_pos; - } else { - config.new_parse_error(if_block.key, "Found 'else' before 'if'"); - return None; - } - } else if array_pos != found_else { - config.new_parse_error(if_block.key, "Multiple 'else' found"); - return None; - } - } else if if_key == "then" { - if found_else.is_empty() { - if array_pos == last_array_pos { - if !found_then { - if_block.if_then.last_mut().unwrap().then = - Expression::try_parse(config, item, token_map)?; - found_then = true; - } - } else { - config.new_parse_error(if_block.key, "Found 'then' without 'if'"); - return None; - } - } else { - config.new_parse_error(if_block.key, "Found 'then' in 'else' block"); - return None; - } - } - } else { - config.new_parse_error( - if_block.key, - format!("Invalid property {item:?} found in 'if' block."), + match ExpressionParser::new(Tokenizer::new(&expr.else_, token_map)).parse() { + Ok(expr) => { + if_block.default = expr; + } + Err(err) => { + bp.invalid_property( + id, + property, + &format!("Error parsing 'else' expression: {}", err), ); return None; } } - if !found_if { - None - } else if !found_then { - config.new_parse_error( - if_block.key, - format!( - "Missing 'then' in 'if' condition {}", - last_array_pos.parse().unwrap_or(0) + 1, - ), - ); - None - } else if found_else.is_empty() { - config.new_parse_error(if_block.key, "Missing 'else'"); - None - } else { - Some(if_block) + for (num, match_) in expr.match_.into_iter().enumerate() { + match ExpressionParser::new(Tokenizer::new(&match_.if_, token_map)).parse() { + Ok(if_expr) => { + match ExpressionParser::new(Tokenizer::new(&match_.then, token_map)).parse() { + Ok(then_expr) => { + if_block.if_then.push(IfThen { + expr: if_expr, + then: then_expr, + }); + } + Err(err) => { + bp.invalid_property( + id, + property, + &format!( + "Error parsing 'then' expression in condition #{}: {}", + num + 1, + err + ), + ); + return None; + } + } + } + Err(err) => { + bp.invalid_property( + id, + property, + &format!( + "Error parsing 'if' expression in condition #{}: {}", + num + 1, + err + ), + ); + return None; + } + } } + + Some(if_block) } - pub fn into_default(self, key: impl Into) -> IfBlock { + pub fn into_default(self, property: Property) -> IfBlock { IfBlock { - key: key.into(), + property, if_then: Default::default(), default: self.default, } diff --git a/crates/common/src/expr/mod.rs b/crates/common/src/expr/mod.rs index 86dfab25..5ecf2ca8 100644 --- a/crates/common/src/expr/mod.rs +++ b/crates/common/src/expr/mod.rs @@ -233,20 +233,8 @@ pub enum Token { #[derive(Debug, Clone)] pub enum Setting { Hostname, - ReportDomain, + Domain, NodeId, - Other(CompactString), -} - -impl From for Setting { - fn from(value: CompactString) -> Self { - match value.as_str() { - "server.hostname" => Setting::Hostname, - "report.domain" => Setting::ReportDomain, - "cluster.node-id" => Setting::NodeId, - _ => Setting::Other(value), - } - } } impl From for Variable<'_> { diff --git a/crates/common/src/expr/tokenizer.rs b/crates/common/src/expr/tokenizer.rs index 91c3181a..bf7c3e6b 100644 --- a/crates/common/src/expr/tokenizer.rs +++ b/crates/common/src/expr/tokenizer.rs @@ -89,24 +89,40 @@ impl<'x> Tokenizer<'x> { self.buf.push(ch); } _ => { - let (prev_token, ch) = if ch == b'(' && self.buf.eq(b"matches") { - // Parse regular expressions - let stop_ch = self.find_char(b"\"'")?; - let regex_str = self.parse_string(stop_ch)?; - let regex = Regex::new(®ex_str).map_err(|e| { - format!("Invalid regular expression {:?}: {}", regex_str, e) - })?; - self.has_alpha = false; - self.buf.clear(); - self.find_char(b",")?; - (Token::Regex(regex).into(), b'(') - } else if ch == b'(' && self.buf.eq(b"config_get") { - // Parse setting - let stop_ch = self.find_char(b"\"'")?; - let setting_str = self.parse_string(stop_ch)?; - self.has_alpha = false; - self.buf.clear(); - (Token::Setting(Setting::from(setting_str)).into(), b'(') + let (prev_token, ch) = if ch == b'(' && !self.buf.is_empty() { + match self.buf.as_slice() { + b"matches" => { + // Parse regular expressions + let stop_ch = self.find_char(b"\"'")?; + let regex_str = self.parse_string(stop_ch)?; + let regex = Regex::new(®ex_str).map_err(|e| { + format!("Invalid regular expression {:?}: {}", regex_str, e) + })?; + self.has_alpha = false; + self.buf.clear(); + self.find_char(b",")?; + (Token::Regex(regex).into(), b'(') + } + b"default_domain" => { + self.has_alpha = false; + self.buf.clear(); + (Token::Setting(Setting::Domain).into(), b'(') + } + b"node_hostname" => { + self.has_alpha = false; + self.buf.clear(); + (Token::Setting(Setting::Hostname).into(), b'(') + } + b"node_id" => { + self.has_alpha = false; + self.buf.clear(); + (Token::Setting(Setting::NodeId).into(), b'(') + } + _ => { + self.is_start = false; + (self.parse_buf()?.into(), ch) + } + } } else if !self.buf.is_empty() { self.is_start = false; (self.parse_buf()?.into(), ch) diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index d81acd03..a8f9b6d3 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -6,21 +6,27 @@ #![warn(clippy::large_futures)] +use crate::{ + config::mailstore::{ + email::EmailConfig, + imap::ImapConfig, + scripts::Scripting, + spamfilter::{IpResolver, SpamClassifier, SpamFilterConfig}, + }, + ipc::TrainTaskController, +}; use ahash::{AHashMap, AHashSet}; use arc_swap::ArcSwap; use auth::{AccessToken, oauth::config::OAuthConfig, roles::RolePermissions}; use calcard::common::timezone::Tz; use config::{ groupware::GroupwareConfig, - imap::ImapConfig, - jmap::settings::JmapConfig, + mailstore::jmap::JmapConfig, network::Network, - scripts::Scripting, smtp::{ SmtpConfig, resolver::{Policy, Tlsa}, }, - spamfilter::{IpResolver, SpamFilterConfig}, storage::Storage, telemetry::Metrics, }; @@ -72,8 +78,6 @@ pub mod enterprise; pub use psl; -use crate::{config::spamfilter::SpamClassifier, ipc::TrainTaskController}; - pub static VERSION_PRIVATE: &str = env!("CARGO_PKG_VERSION"); pub static VERSION_PUBLIC: &str = "1.0.0"; @@ -342,11 +346,12 @@ pub struct Core { pub network: Network, pub acme: AcmeProviders, pub oauth: OAuthConfig, - pub smtp: SmtpConfig, + pub email: EmailConfig, pub jmap: JmapConfig, - pub groupware: GroupwareConfig, - pub spam: SpamFilterConfig, pub imap: ImapConfig, + pub smtp: SmtpConfig, + pub spam: SpamFilterConfig, + pub groupware: GroupwareConfig, pub metrics: Metrics, // SPDX-SnippetBegin diff --git a/crates/common/src/listener/acme/cache.rs b/crates/common/src/listener/acme/cache.rs index 7ff4c286..7e8e9334 100644 --- a/crates/common/src/listener/acme/cache.rs +++ b/crates/common/src/listener/acme/cache.rs @@ -4,14 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::AcmeProvider; +use crate::Server; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use trc::AddContext; use utils::config::ConfigKey; -use crate::Server; - -use super::AcmeProvider; - impl Server { pub(crate) async fn load_cert(&self, provider: &AcmeProvider) -> trc::Result>> { self.read_if_exists(provider, "cert", provider.domains.as_slice()) diff --git a/crates/common/src/listener/blocked.rs b/crates/common/src/listener/blocked.rs index 060789ff..0192cffd 100644 --- a/crates/common/src/listener/blocked.rs +++ b/crates/common/src/listener/blocked.rs @@ -4,9 +4,12 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{fmt::Debug, net::IpAddr}; - +use crate::{ + KV_RATE_LIMIT_AUTH, KV_RATE_LIMIT_LOITER, KV_RATE_LIMIT_RCPT, KV_RATE_LIMIT_SCAN, Server, + ip_to_bytes, ipc::BroadcastEvent, +}; use ahash::AHashSet; +use std::{fmt::Debug, net::IpAddr}; use utils::{ config::{ Config, ConfigKey, Rate, @@ -16,13 +19,10 @@ use utils::{ glob::GlobPattern, }; -use crate::{ - KV_RATE_LIMIT_AUTH, KV_RATE_LIMIT_LOITER, KV_RATE_LIMIT_RCPT, KV_RATE_LIMIT_SCAN, Server, - ip_to_bytes, ipc::BroadcastEvent, manager::config::MatchType, -}; - #[derive(Debug, Clone)] pub struct Security { + fallback_admin: Option<(String, String)>, + blocked_ip_networks: Vec, has_blocked_networks: bool, @@ -49,7 +49,7 @@ pub struct BlockedIps { } impl Security { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let mut allowed_ip_addresses = AHashSet::new(); let mut allowed_ip_networks = Vec::new(); @@ -280,7 +280,7 @@ impl Server { } impl BlockedIps { - pub fn parse(config: &mut Config) -> Self { + pub fn parse(bp: &mut Bootstrap) -> Self { let mut blocked_ip_addresses = AHashSet::new(); let mut blocked_ip_networks = Vec::new(); diff --git a/crates/common/src/listener/listen.rs b/crates/common/src/listener/listen.rs index da0b6f0a..d7bb1ca3 100644 --- a/crates/common/src/listener/listen.rs +++ b/crates/common/src/listener/listen.rs @@ -301,7 +301,7 @@ impl SocketOpts { } impl Listeners { - pub fn bind_and_drop_priv(&self, config: &mut Config) { + pub fn bind_and_drop_priv(&self, bp: &mut Bootstrap) { // Bind as root for server in &self.servers { for listener in &server.listeners { diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index 3907cc5f..f459758e 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -4,12 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{ - WEBADMIN_KEY, - backup::BackupParams, - config::{ConfigManager, Patterns}, - console::store_console, -}; +use super::{WEBADMIN_KEY, backup::BackupParams, console::store_console}; use crate::{ Caches, Core, Data, IPC_CHANNEL_BUFFER, Inner, Ipc, config::{network::AsnGeoLookupConfig, server::Listeners, telemetry::Telemetry}, diff --git a/crates/common/src/manager/bootstrap.rs b/crates/common/src/manager/bootstrap.rs new file mode 100644 index 00000000..4677e78a --- /dev/null +++ b/crates/common/src/manager/bootstrap.rs @@ -0,0 +1,98 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use registry::{ + schema::{prelude::Property, structs::Node}, + types::{ + ObjectType, + error::{Error, ValidationError, Warning}, + id::Id, + }, +}; +use store::{RegistryStore, registry::RegistryObject}; + +pub struct Bootstrap { + pub registry: RegistryStore, + pub errors: Vec, + pub warnings: Vec, + pub has_fatal_errors: bool, + pub node: Node, +} + +impl Bootstrap { + pub fn new(registry: RegistryStore) -> Self { + Self { + registry, + errors: Vec::new(), + warnings: Vec::new(), + has_fatal_errors: false, + node: Node::default(), + } + } + + pub async fn setting(&mut self) -> trc::Result { + let object_id = T::object().singleton(); + + if let Some(setting) = self.registry.get::(object_id).await? { + let mut errors = Vec::new(); + if setting.validate(&mut errors) { + return Ok(setting); + } + self.errors.push(Error::Validation { object_id, errors }); + } + + Ok(T::default()) + } + + pub async fn setting_infallible(&mut self) -> T { + match self.setting::().await { + Ok(setting) => setting, + Err(err) => { + if !self.has_fatal_errors { + self.errors.push(Error::Internal { + object_id: Some(T::object().singleton()), + error: err, + }); + self.has_fatal_errors = true; + } + T::default() + } + } + } + + pub async fn list_infallible(&mut self) -> Vec> { + match self.registry.list::().await { + Ok(objects) => objects, + Err(err) => { + if !self.has_fatal_errors { + self.errors.push(Error::Internal { + object_id: None, + error: err, + }); + self.has_fatal_errors = true; + } + Vec::new() + } + } + } + + pub fn build_error(&mut self, id: Id, message: impl Into) { + self.errors.push(Error::Build { + object_id: id, + message: message.into(), + }); + } + + pub fn invalid_property(&mut self, id: Id, property: Property, value: impl Into) { + self.errors.push(Error::Validation { + object_id: id, + errors: vec![ValidationError::Invalid { + property, + value: value.into(), + }], + }); + } +} diff --git a/crates/common/src/manager/config.rs b/crates/common/src/manager/config.rs deleted file mode 100644 index da17745a..00000000 --- a/crates/common/src/manager/config.rs +++ /dev/null @@ -1,611 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use std::{ - collections::{BTreeMap, btree_map::Entry}, - path::PathBuf, - sync::Arc, -}; - -use ahash::AHashMap; -use arc_swap::ArcSwap; -use store::{ - Deserialize, IterateParams, Store, ValueKey, - write::{BatchBuilder, ValueClass}, -}; -use trc::AddContext; -use types::semver::Semver; -use utils::{ - config::{Config, ConfigKey}, - glob::GlobPattern, -}; - -#[derive(Default)] -pub struct ConfigManager { - pub cfg_local: ArcSwap>, - pub cfg_local_path: PathBuf, - pub cfg_local_patterns: Arc, - pub cfg_store: Store, -} - -#[derive(Default)] -pub struct Patterns { - patterns: Vec, -} - -#[derive(Debug, PartialEq, Eq)] -enum Pattern { - Include(MatchType), - Exclude(MatchType), -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MatchType { - Equal(String), - StartsWith(String), - EndsWith(String), - Matches(GlobPattern), - All, -} - -pub(crate) struct ExternalSpamRules { - pub version: Semver, - pub keys: Vec, -} - -impl ConfigManager { - pub async fn build_config(&self, prefix: &str) -> trc::Result { - let mut config = Config { - keys: self.cfg_local.load().as_ref().clone(), - ..Default::default() - }; - config.resolve_all_macros().await; - self.extend_config(&mut config, prefix) - .await - .map(|_| config) - } - - pub(crate) async fn extend_config(&self, config: &mut Config, prefix: &str) -> trc::Result<()> { - for (key, value) in self.db_list(prefix, false).await? { - config.keys.entry(key).or_insert(value); - } - - Ok(()) - } - - pub async fn get(&self, key: impl AsRef) -> trc::Result> { - let key = key.as_ref(); - match self.cfg_local.load().get(key) { - Some(value) => Ok(Some(value.to_string())), - None => { - self.cfg_store - .get_value(ValueKey::from(ValueClass::Config( - key.to_string().into_bytes(), - ))) - .await - } - } - } - - pub async fn list( - &self, - prefix: &str, - strip_prefix: bool, - ) -> trc::Result> { - let mut results = self.db_list(prefix, strip_prefix).await?; - for (key, value) in self.cfg_local.load().iter() { - if prefix.is_empty() || (!strip_prefix && key.starts_with(prefix)) { - results.insert(key.clone(), value.clone()); - } else if let Some(key) = key.strip_prefix(prefix) { - results.insert(key.to_string(), value.clone()); - } - } - - Ok(results) - } - - pub async fn group( - &self, - prefix: &str, - suffix: &str, - ) -> trc::Result>> { - let mut grouped = AHashMap::new(); - - let mut list = self.list(prefix, true).await?; - for key in list.keys() { - if let Some(key) = key.strip_suffix(suffix) { - grouped.insert(key.to_string(), AHashMap::new()); - } - } - - for (name, entries) in &mut grouped { - let prefix = format!("{name}."); - for (key, value) in &mut list { - if let Some(key) = key.strip_prefix(&prefix) { - entries.insert(key.to_string(), std::mem::take(value)); - } - } - } - - Ok(grouped) - } - - pub async fn db_list( - &self, - prefix: &str, - strip_prefix: bool, - ) -> trc::Result> { - let key = prefix.as_bytes(); - let from_key = ValueKey::from(ValueClass::Config(key.to_vec())); - let to_key = ValueKey::from(ValueClass::Config( - key.iter() - .copied() - .chain([u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX]) - .collect::>(), - )); - let mut results = BTreeMap::new(); - let patterns = self.cfg_local_patterns.clone(); - self.cfg_store - .iterate( - IterateParams::new(from_key, to_key).ascending(), - |key, value| { - let mut key = std::str::from_utf8(key).map_err(|_| { - trc::Error::corrupted_key(key, value.into(), trc::location!()) - })?; - - if !patterns.is_local_key(key) { - if strip_prefix && !prefix.is_empty() { - key = key.strip_prefix(prefix).unwrap_or(key); - } - - results.insert(key.to_string(), String::deserialize(value)?); - } - - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - - Ok(results) - } - - pub async fn set(&self, keys: I, overwrite: bool) -> trc::Result<()> - where - I: IntoIterator, - T: Into, - { - let mut batch = BatchBuilder::new(); - let mut local_batch = Vec::new(); - - for key in keys { - let key = key.into(); - - if overwrite || self.get(&key.key).await?.is_none() || key.key.starts_with("version.") { - if self.cfg_local_patterns.is_local_key(&key.key) { - local_batch.push(key); - } else { - batch.set(ValueClass::Config(key.key.into_bytes()), key.value); - } - } - } - - if !batch.is_empty() { - self.cfg_store.write(batch.build_all()).await?; - } - - if !local_batch.is_empty() { - let mut local = self.cfg_local.load().as_ref().clone(); - let mut has_changes = false; - - for key in local_batch { - match local.entry(key.key) { - Entry::Vacant(v) => { - v.insert(key.value); - has_changes = true; - } - Entry::Occupied(mut v) => { - if v.get() != &key.value { - v.insert(key.value); - has_changes = true; - } - } - } - } - if has_changes { - self.update_local(local).await?; - } - } - - Ok(()) - } - - pub async fn clear(&self, key: impl AsRef) -> trc::Result<()> { - let key = key.as_ref(); - - if self.cfg_local_patterns.is_local_key(key) { - let mut local = self.cfg_local.load().as_ref().clone(); - if local.remove(key).is_some() { - self.update_local(local).await - } else { - Ok(()) - } - } else { - let mut batch = BatchBuilder::new(); - batch.clear(ValueClass::Config(key.to_string().into_bytes())); - self.cfg_store.write(batch.build_all()).await.map(|_| ()) - } - } - - pub async fn clear_prefix(&self, key: impl AsRef) -> trc::Result<()> { - let key = key.as_ref(); - - // Delete local keys - let local = self.cfg_local.load(); - if local.keys().any(|k| k.starts_with(key)) { - let mut local = local.as_ref().clone(); - local.retain(|k, _| !k.starts_with(key)); - self.update_local(local).await?; - } - - // Delete db keys - self.cfg_store - .delete_range( - ValueKey::from(ValueClass::Config(key.as_bytes().to_vec())), - ValueKey::from(ValueClass::Config( - key.as_bytes() - .iter() - .copied() - .chain([u8::MAX, u8::MAX, u8::MAX, u8::MAX, u8::MAX]) - .collect::>(), - )), - ) - .await - } - - async fn update_local(&self, map: BTreeMap) -> trc::Result<()> { - let mut cfg_text = String::with_capacity(1024); - for (key, value) in &map { - cfg_text.push_str(key); - cfg_text.push_str(" = "); - if value == "true" || value == "false" || value.parse::().is_ok() { - cfg_text.push_str(value); - } else { - let mut needs_escape = false; - let mut has_lf = false; - - for ch in value.chars() { - match ch { - '"' | '\\' => { - needs_escape = true; - if has_lf { - break; - } - } - '\n' => { - has_lf = true; - if needs_escape { - break; - } - } - _ => {} - } - } - - if has_lf || (value.len() > 50 && needs_escape) { - cfg_text.push_str("'''"); - cfg_text.push_str(value); - cfg_text.push_str("'''"); - } else { - cfg_text.push('"'); - if needs_escape { - for ch in value.chars() { - if ch == '\\' || ch == '"' { - cfg_text.push('\\'); - } - cfg_text.push(ch); - } - } else { - cfg_text.push_str(value); - } - cfg_text.push('"'); - } - } - cfg_text.push('\n'); - } - - self.cfg_local.store(map.into()); - - tokio::fs::write(&self.cfg_local_path, cfg_text) - .await - .map_err(|err| { - trc::EventType::Config(trc::ConfigEvent::WriteError) - .reason(err) - .details("Failed to write local configuration") - .ctx(trc::Key::Path, self.cfg_local_path.display().to_string()) - }) - } - - pub async fn update_spam_rules( - &self, - force_update: bool, - overwrite: bool, - ) -> trc::Result> { - let current_version = self - .get("version.spam-filter") - .await? - .and_then(|v| Semver::try_from(v.as_str()).ok()); - let is_update = current_version.is_some(); - - let mut external = self.fetch_spam_rules().await.map_err(|reason| { - trc::EventType::Config(trc::ConfigEvent::FetchError) - .caused_by(trc::location!()) - .details("Failed to update spam filter rules") - .ctx(trc::Key::Reason, reason) - })?; - - if current_version.is_none_or(|v| external.version > v || force_update) { - if is_update { - // Delete previous STWT_* rules - let mut rule_settings = AHashMap::new(); - for prefix in [ - "spam-filter.rule.stwt_", - "spam-filter.dnsbl.server.stwt_", - "http-lookup.stwt_", - ] { - for (key, value) in self.list(prefix, false).await? { - if key.ends_with(".enable") { - rule_settings.insert(key, value); - } - } - - self.clear_prefix(prefix).await?; - } - - // Update keys - if !rule_settings.is_empty() { - for key in &mut external.keys { - if let Some(value) = rule_settings.remove(&key.key) { - key.value = value; - } - } - } - - if !overwrite { - // Do not overwrite ASN or LLM settings - external.keys.retain(|key| { - !key.key.starts_with("spam-filter.llm.") && !key.key.starts_with("asn.") - }); - } - } - - self.set(external.keys, overwrite).await?; - - trc::event!( - Config(trc::ConfigEvent::ImportExternal), - Version = external.version.to_string(), - Id = "spam-filter", - ); - - Ok(Some(external.version)) - } else { - trc::event!( - Config(trc::ConfigEvent::AlreadyUpToDate), - Version = external.version.to_string(), - Id = "spam-filter", - ); - - Ok(None) - } - } - - pub(crate) async fn fetch_spam_rules(&self) -> Result { - let config = String::from_utf8(self.fetch_resource("spam-filter").await?) - .map_err(|err| format!("Configuration file has invalid UTF-8: {err}"))?; - let config = Config::new(config) - .map_err(|err| format!("Failed to parse external configuration: {err}"))?; - - // Import configuration - let mut external = ExternalSpamRules { - version: Semver::default(), - keys: Vec::new(), - }; - let mut required_semver = Semver::default(); - let server_semver: Semver = env!("CARGO_PKG_VERSION").try_into().unwrap(); - for (key, value) in config.keys { - if key == "version.spam-filter" { - external.version = value.as_str().try_into().unwrap_or_default(); - external.keys.push(ConfigKey::from((key, value))); - } else if key == "version.server" { - required_semver = value.as_str().try_into().unwrap_or_default(); - } else if key.starts_with("spam-filter.") - || key.starts_with("http-lookup.") - || key.starts_with("lookup.") - || key.starts_with("asn.") - { - external.keys.push(ConfigKey::from((key, value))); - } - } - - if !required_semver.is_valid() { - Err("External spam filter rules do not contain a valid server version".to_string()) - } else if required_semver > server_semver { - Err(format!( - "External spam filter rules require server version {required_semver}, but this is version {server_semver}", - )) - } else if external.version.is_valid() { - Ok(external) - } else { - Err("External spam filter rules do not contain a version key".to_string()) - } - } - - pub async fn get_services(&self) -> trc::Result> { - let mut result = Vec::new(); - - for listener in self - .group("server.listener.", ".protocol") - .await - .unwrap_or_default() - .into_values() - { - let is_tls = listener - .get("tls.implicit") - .is_some_and(|tls| tls == "true"); - let protocol = listener - .get("protocol") - .map(|s| s.as_str()) - .unwrap_or_default(); - let port = listener - .get("bind") - .or_else(|| { - listener.iter().find_map(|(key, value)| { - if key.starts_with("bind.") { - Some(value) - } else { - None - } - }) - }) - .and_then(|s| s.rsplit_once(':').and_then(|(_, p)| p.parse::().ok())) - .unwrap_or_default(); - - if port > 0 { - result.push((protocol.to_string(), port, is_tls)); - } - } - - // Sort by name, then tls and finally port - result.sort_unstable_by(|a, b| { - a.0.cmp(&b.0) - .then_with(|| b.2.cmp(&a.2)) - .then_with(|| a.1.cmp(&b.1)) - }); - - Ok(result) - } -} - -impl Patterns { - pub fn parse(config: &mut Config) -> Self { - let mut cfg_local_patterns = Vec::new(); - for (key, value) in &config.keys { - if !key.starts_with("config.local-keys") { - if cfg_local_patterns.is_empty() { - continue; - } else { - break; - } - }; - let value = value.trim(); - let (value, is_include) = value - .strip_prefix('!') - .map_or((value, true), |value| (value, false)); - let value = value.trim().to_ascii_lowercase(); - if value.is_empty() { - continue; - } - let match_type = MatchType::parse(&value); - - cfg_local_patterns.push(if is_include { - Pattern::Include(match_type) - } else { - Pattern::Exclude(match_type) - }); - } - - if cfg_local_patterns.is_empty() { - cfg_local_patterns = vec![ - Pattern::Include(MatchType::StartsWith("store.".to_string())), - Pattern::Include(MatchType::StartsWith("directory.".to_string())), - Pattern::Include(MatchType::StartsWith("tracer.".to_string())), - Pattern::Exclude(MatchType::StartsWith("server.blocked-ip.".to_string())), - Pattern::Exclude(MatchType::StartsWith("server.allowed-ip.".to_string())), - Pattern::Include(MatchType::StartsWith("server.".to_string())), - Pattern::Include(MatchType::StartsWith("certificate.".to_string())), - Pattern::Include(MatchType::StartsWith("config.local-keys.".to_string())), - Pattern::Include(MatchType::StartsWith( - "authentication.fallback-admin.".to_string(), - )), - Pattern::Include(MatchType::StartsWith("cluster.".to_string())), - Pattern::Include(MatchType::Equal("storage.data".to_string())), - Pattern::Include(MatchType::Equal("storage.blob".to_string())), - Pattern::Include(MatchType::Equal("storage.lookup".to_string())), - Pattern::Include(MatchType::Equal("storage.fts".to_string())), - Pattern::Include(MatchType::Equal("storage.directory".to_string())), - Pattern::Include(MatchType::Equal("enterprise.license-key".to_string())), - ]; - } else if !cfg_local_patterns.contains(&Pattern::Include(MatchType::StartsWith( - "config.local-keys.".to_string(), - ))) { - cfg_local_patterns.push(Pattern::Include(MatchType::StartsWith( - "config.local-keys.".to_string(), - ))); - } - - Patterns { - patterns: cfg_local_patterns, - } - } - - pub fn is_local_key(&self, key: &str) -> bool { - let mut is_local = false; - - for pattern in &self.patterns { - match pattern { - Pattern::Include(pattern) => { - if !is_local && pattern.matches(key) { - is_local = true; - } - } - Pattern::Exclude(pattern) => { - if pattern.matches(key) { - return false; - } - } - } - } - - is_local - } -} - -impl MatchType { - pub fn parse(value: &str) -> Self { - if value == "*" { - MatchType::All - } else if let Some(value) = value.strip_suffix('*') { - MatchType::StartsWith(value.to_string()) - } else if let Some(value) = value.strip_prefix('*') { - MatchType::EndsWith(value.to_string()) - } else if value.contains('*') { - MatchType::Matches(GlobPattern::compile(value, false)) - } else { - MatchType::Equal(value.to_string()) - } - } - - pub fn matches(&self, value: &str) -> bool { - match self { - MatchType::Equal(pattern) => value == pattern, - MatchType::StartsWith(pattern) => value.starts_with(pattern), - MatchType::EndsWith(pattern) => value.ends_with(pattern), - MatchType::Matches(pattern) => pattern.matches(value), - MatchType::All => true, - } - } -} - -impl Clone for ConfigManager { - fn clone(&self) -> Self { - Self { - cfg_local: ArcSwap::from_pointee(self.cfg_local.load().as_ref().clone()), - cfg_local_path: self.cfg_local_path.clone(), - cfg_local_patterns: self.cfg_local_patterns.clone(), - cfg_store: self.cfg_store.clone(), - } - } -} diff --git a/crates/common/src/manager/mod.rs b/crates/common/src/manager/mod.rs index 2abe43f2..b18a755e 100644 --- a/crates/common/src/manager/mod.rs +++ b/crates/common/src/manager/mod.rs @@ -4,7 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use self::config::ConfigManager; use crate::USER_AGENT; use hyper::HeaderMap; use std::time::Duration; @@ -12,7 +11,7 @@ use utils::HttpLimitResponse; pub mod backup; pub mod boot; -pub mod config; +pub mod bootstrap; pub mod console; pub mod reload; pub mod restore; diff --git a/crates/common/src/manager/reload.rs b/crates/common/src/manager/reload.rs index bcc3df7f..7fa35e0d 100644 --- a/crates/common/src/manager/reload.rs +++ b/crates/common/src/manager/reload.rs @@ -4,11 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use ahash::AHashMap; -use arc_swap::ArcSwap; -use store::Stores; -use utils::config::Config; - use crate::{ Core, Server, config::{ @@ -17,8 +12,10 @@ use crate::{ }, listener::blocked::{BLOCKED_IP_KEY, BlockedIps}, }; - -use super::config::{ConfigManager, Patterns}; +use ahash::AHashMap; +use arc_swap::ArcSwap; +use store::Stores; +use utils::config::Config; pub struct ReloadResult { pub config: Config, diff --git a/crates/common/src/manager/webadmin.rs b/crates/common/src/manager/webadmin.rs index 142ebc39..516cb478 100644 --- a/crates/common/src/manager/webadmin.rs +++ b/crates/common/src/manager/webadmin.rs @@ -4,20 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::WEBADMIN_KEY; +use crate::Core; +use ahash::AHashMap; +use arc_swap::ArcSwap; use std::{ borrow::Cow, io::{self, Cursor, Read}, path::PathBuf, }; - -use ahash::AHashMap; -use arc_swap::ArcSwap; use store::BlobStore; -use crate::Core; - -use super::WEBADMIN_KEY; - pub struct WebAdminManager { bundle_path: TempDir, routes: ArcSwap>>, diff --git a/crates/coordinator/src/dispatch.rs b/crates/coordinator/src/dispatch.rs index 89b1ff51..e3c22ca7 100644 --- a/crates/coordinator/src/dispatch.rs +++ b/crates/coordinator/src/dispatch.rs @@ -4,9 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -#[cfg(feature = "redis")] -use crate::PubSubStream; -use crate::{Coordinator, Msg}; +use crate::{Coordinator, Msg, PubSubStream}; #[allow(unused_variables)] impl Coordinator { @@ -58,7 +56,6 @@ impl PubSubStream { PubSubStream::Zenoh(stream) => stream.next().await, #[cfg(feature = "kafka")] PubSubStream::Kafka(stream) => stream.next().await, - #[cfg(not(any(feature = "redis", feature = "nats")))] PubSubStream::Unimplemented => None, } } @@ -75,7 +72,6 @@ impl Msg { Msg::Zenoh(msg) => msg.as_slice(), #[cfg(feature = "kafka")] Msg::Kafka(msg) => msg.as_slice(), - #[cfg(not(any(feature = "redis", feature = "nats")))] Msg::Unimplemented => &[], } } @@ -90,7 +86,6 @@ impl Msg { Msg::Zenoh(_) => "", #[cfg(feature = "kafka")] Msg::Kafka(_) => "", - #[cfg(not(any(feature = "redis", feature = "nats")))] Msg::Unimplemented => "", } } diff --git a/crates/coordinator/src/lib.rs b/crates/coordinator/src/lib.rs index 73f530fc..4a8760fe 100644 --- a/crates/coordinator/src/lib.rs +++ b/crates/coordinator/src/lib.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +#[allow(unused_imports)] use std::sync::Arc; pub mod backend; @@ -34,7 +35,6 @@ pub enum PubSubStream { Zenoh(crate::backend::zenoh::pubsub::ZenohPubSubStream), #[cfg(feature = "kafka")] Kafka(crate::backend::kafka::pubsub::KafkaPubSubStream), - #[cfg(not(any(feature = "redis", feature = "nats")))] Unimplemented, } @@ -47,6 +47,5 @@ pub enum Msg { Zenoh(Vec), #[cfg(feature = "kafka")] Kafka(Vec), - #[cfg(not(any(feature = "redis", feature = "nats")))] Unimplemented, } diff --git a/crates/jmap-proto/src/request/capability.rs b/crates/jmap-proto/src/request/capability.rs index bd9ffeff..1c8cfa7a 100644 --- a/crates/jmap-proto/src/request/capability.rs +++ b/crates/jmap-proto/src/request/capability.rs @@ -115,19 +115,19 @@ pub enum Capabilities { #[derive(Debug, Clone, serde::Serialize)] pub struct CoreCapabilities { #[serde(rename(serialize = "maxSizeUpload"))] - pub max_size_upload: usize, + pub max_size_upload: u64, #[serde(rename(serialize = "maxConcurrentUpload"))] - pub max_concurrent_upload: usize, + pub max_concurrent_upload: u64, #[serde(rename(serialize = "maxSizeRequest"))] - pub max_size_request: usize, + pub max_size_request: u64, #[serde(rename(serialize = "maxConcurrentRequests"))] - pub max_concurrent_requests: usize, + pub max_concurrent_requests: u64, #[serde(rename(serialize = "maxCallsInRequest"))] - pub max_calls_in_request: usize, + pub max_calls_in_request: u64, #[serde(rename(serialize = "maxObjectsInGet"))] - pub max_objects_in_get: usize, + pub max_objects_in_get: u64, #[serde(rename(serialize = "maxObjectsInSet"))] - pub max_objects_in_set: usize, + pub max_objects_in_set: u64, #[serde(rename(serialize = "collationAlgorithms"))] pub collation_algorithms: Vec, } @@ -149,13 +149,13 @@ pub struct SieveSessionCapabilities { #[derive(Debug, Clone, serde::Serialize)] pub struct SieveAccountCapabilities { #[serde(rename(serialize = "maxSizeScriptName"))] - pub max_script_name: usize, + pub max_script_name: u64, #[serde(rename(serialize = "maxSizeScript"))] - pub max_script_size: usize, + pub max_script_size: u64, #[serde(rename(serialize = "maxNumberScripts"))] - pub max_scripts: usize, + pub max_scripts: u64, #[serde(rename(serialize = "maxNumberRedirects"))] - pub max_redirects: usize, + pub max_redirects: u64, #[serde(rename(serialize = "sieveExtensions"))] pub extensions: Vec, #[serde(rename(serialize = "notificationMethods"))] @@ -167,13 +167,13 @@ pub struct SieveAccountCapabilities { #[derive(Debug, Clone, serde::Serialize)] pub struct MailCapabilities { #[serde(rename(serialize = "maxMailboxesPerEmail"))] - pub max_mailboxes_per_email: Option, + pub max_mailboxes_per_email: Option, #[serde(rename(serialize = "maxMailboxDepth"))] - pub max_mailbox_depth: usize, + pub max_mailbox_depth: u64, #[serde(rename(serialize = "maxSizeMailboxName"))] - pub max_size_mailbox_name: usize, + pub max_size_mailbox_name: u64, #[serde(rename(serialize = "maxSizeAttachmentsPerEmail"))] - pub max_size_attachments_per_email: usize, + pub max_size_attachments_per_email: u64, #[serde(rename(serialize = "emailQuerySortOptions"))] pub email_query_sort_options: Vec, #[serde(rename(serialize = "mayCreateTopLevelMailbox"))] @@ -183,7 +183,7 @@ pub struct MailCapabilities { #[derive(Debug, Clone, serde::Serialize)] pub struct SubmissionCapabilities { #[serde(rename(serialize = "maxDelayedSend"))] - pub max_delayed_send: usize, + pub max_delayed_send: u64, #[serde(rename(serialize = "submissionExtensions"))] pub submission_extensions: VecMap>, } @@ -191,9 +191,9 @@ pub struct SubmissionCapabilities { #[derive(Debug, Clone, serde::Serialize)] pub struct BlobCapabilities { #[serde(rename(serialize = "maxSizeBlobSet"))] - pub max_size_blob_set: usize, + pub max_size_blob_set: u64, #[serde(rename(serialize = "maxDataSources"))] - pub max_data_sources: usize, + pub max_data_sources: u64, #[serde(rename(serialize = "supportedTypeNames"))] pub supported_type_names: Vec, #[serde(rename(serialize = "supportedDigestAlgorithms"))] @@ -203,7 +203,7 @@ pub struct BlobCapabilities { #[derive(Debug, Clone, serde::Serialize)] pub struct CalendarCapabilities { #[serde(rename(serialize = "maxCalendarsPerEvent"))] - pub max_calendars_per_event: Option, + pub max_calendars_per_event: Option, #[serde(rename(serialize = "minDateTime"))] pub min_date_time: UTCDate, #[serde(rename(serialize = "maxDateTime"))] @@ -211,7 +211,7 @@ pub struct CalendarCapabilities { #[serde(rename(serialize = "maxExpandedQueryDuration"))] pub max_expanded_query_duration: String, #[serde(rename(serialize = "maxParticipantsPerEvent"))] - pub max_participants_per_event: Option, + pub max_participants_per_event: Option, #[serde(rename(serialize = "mayCreateCalendar"))] pub may_create_calendar: bool, } @@ -219,7 +219,7 @@ pub struct CalendarCapabilities { #[derive(Debug, Clone, serde::Serialize)] pub struct ContactsCapabilities { #[serde(rename(serialize = "maxAddressBooksPerCard"))] - pub max_address_books_per_card: Option, + pub max_address_books_per_card: Option, #[serde(rename(serialize = "mayCreateAddressBook"))] pub may_create_address_book: bool, } @@ -260,9 +260,9 @@ pub struct PrincipalCalendarCapabilities { #[derive(Debug, Clone, serde::Serialize)] pub struct FileNodeCapabilities { #[serde(rename(serialize = "maxFileNodeDepth"))] - pub max_file_node_depth: Option, + pub max_file_node_depth: Option, #[serde(rename(serialize = "maxSizeFileNodeName"))] - pub max_size_file_node_name: usize, + pub max_size_file_node_name: u64, #[serde(rename(serialize = "fileNodeQuerySortOptions"))] pub file_node_query_sort_options: Vec, #[serde(rename(serialize = "mayCreateTopLevelFileNode"))] diff --git a/crates/registry/src/schema/mod.rs b/crates/registry/src/schema/mod.rs index e1746054..3b5ebc8b 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 utils::config::cron::SimpleCron; + #[allow(clippy::derivable_impls)] pub mod enums; pub mod enums_impl; @@ -14,3 +16,22 @@ pub mod properties_impl; pub mod structs; #[allow(clippy::derivable_impls)] pub mod structs_impl; + +impl From for SimpleCron { + fn from(value: prelude::Cron) -> Self { + match value { + prelude::Cron::Daily(cron) => SimpleCron::Day { + hour: cron.hour as u32, + minute: cron.minute as u32, + }, + prelude::Cron::Weekly(cron) => SimpleCron::Week { + day: cron.day as u32, + hour: cron.hour as u32, + minute: cron.minute as u32, + }, + prelude::Cron::Hourly(cron) => SimpleCron::Hour { + minute: cron.minute as u32, + }, + } + } +} diff --git a/crates/registry/src/schema/prelude.rs b/crates/registry/src/schema/prelude.rs index 50832109..83151eeb 100644 --- a/crates/registry/src/schema/prelude.rs +++ b/crates/registry/src/schema/prelude.rs @@ -9,6 +9,7 @@ pub use crate::schema::enums::*; pub use crate::schema::properties::*; pub use crate::schema::structs::*; pub use crate::types::EnumType; +pub use crate::types::ObjectType; pub use crate::types::datetime::UTCDateTime; pub use crate::types::duration::Duration; pub use crate::types::error::*; diff --git a/crates/registry/src/types/error.rs b/crates/registry/src/types/error.rs index 9355ed1c..a7669ea0 100644 --- a/crates/registry/src/types/error.rs +++ b/crates/registry/src/types/error.rs @@ -4,83 +4,93 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::schema::prelude::Property; +use crate::{schema::prelude::Property, types::id::Id}; +use std::fmt::Display; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValidationErrorType { - Invalid, - Required, - MinItems(usize), - MaxItems(usize), - MaxLength(usize), - MinLength(usize), - MaxValue(i64), - MinValue(i64), +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ValidationError { + Invalid { property: Property, value: String }, + Required { property: Property }, + MaxLength { property: Property, required: usize }, + MinLength { property: Property, required: usize }, + MaxValue { property: Property, required: i64 }, + MinValue { property: Property, required: i64 }, } -pub struct ValidationError { - pub property: Property, - pub typ: ValidationErrorType, +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Error { + Validation { + object_id: Id, + errors: Vec, + }, + Build { + object_id: Id, + message: String, + }, + Internal { + object_id: Option, + error: trc::Error, + }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Warning { + pub object_id: Id, + pub property: Option, + pub message: String, } impl ValidationError { - pub fn new(property: Property, typ: ValidationErrorType) -> Self { - Self { property, typ } - } - pub fn required(property: Property) -> Self { - Self { + Self::Required { property } + } + + pub fn invalid(property: Property, value: impl Display) -> Self { + Self::Invalid { property, - typ: ValidationErrorType::Required, + value: value.to_string(), } } - pub fn invalid(property: Property) -> Self { + pub fn min_items(property: Property, required: usize) -> Self { + Self::MinLength { property, required } + } + + pub fn max_items(property: Property, required: usize) -> Self { + Self::MaxLength { property, required } + } + + pub fn max_length(property: Property, required: usize) -> Self { + Self::MaxLength { property, required } + } + + pub fn min_length(property: Property, required: usize) -> Self { + Self::MinLength { property, required } + } + + pub fn max_value(property: Property, required: i64) -> Self { + Self::MaxValue { property, required } + } + + pub fn min_value(property: Property, required: i64) -> Self { + Self::MinValue { property, required } + } +} + +impl Warning { + pub fn new(object_id: Id, message: impl Display) -> Self { Self { - property, - typ: ValidationErrorType::Invalid, + object_id, + property: None, + message: message.to_string(), } } - pub fn min_items(property: Property, value: usize) -> Self { + pub fn for_property(object_id: Id, property: Property, message: impl Display) -> Self { Self { - property, - typ: ValidationErrorType::MinItems(value), - } - } - - pub fn max_items(property: Property, value: usize) -> Self { - Self { - property, - typ: ValidationErrorType::MaxItems(value), - } - } - - pub fn max_length(property: Property, value: usize) -> Self { - Self { - property, - typ: ValidationErrorType::MaxLength(value), - } - } - - pub fn min_length(property: Property, value: usize) -> Self { - Self { - property, - typ: ValidationErrorType::MinLength(value), - } - } - - pub fn max_value(property: Property, value: i64) -> Self { - Self { - property, - typ: ValidationErrorType::MaxValue(value), - } - } - - pub fn min_value(property: Property, value: i64) -> Self { - Self { - property, - typ: ValidationErrorType::MinValue(value), + object_id, + property: Some(property), + message: message.to_string(), } } } diff --git a/crates/registry/src/types/mod.rs b/crates/registry/src/types/mod.rs index 98567f79..7e3e06c4 100644 --- a/crates/registry/src/types/mod.rs +++ b/crates/registry/src/types/mod.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{pickle::Pickle, schema::prelude::Object, types::error::ValidationError}; + pub mod datetime; pub mod duration; pub mod error; @@ -18,3 +20,8 @@ pub trait EnumType: Sized { fn from_id(id: u16) -> Option; fn to_id(&self) -> u16; } + +pub trait ObjectType: Pickle + Default + Clone + Send + Sync { + fn object() -> Object; + fn validate(&self, errors: &mut Vec) -> bool; +} diff --git a/crates/store/src/dispatch/registry.rs b/crates/store/src/dispatch/registry.rs index b6d90965..874d2d8e 100644 --- a/crates/store/src/dispatch/registry.rs +++ b/crates/store/src/dispatch/registry.rs @@ -4,16 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use registry::{schema::prelude::Registry, types::id::Id}; - -use crate::RegistryStore; +use crate::{RegistryStore, registry::RegistryObject}; +use registry::types::{ObjectType, id::Id}; impl RegistryStore { - pub async fn get(&self, id: Id) -> trc::Result> { + pub async fn get(&self, id: Id) -> trc::Result> { todo!() } - pub async fn get_or_default(&self, id: Id) -> trc::Result { + pub async fn list(&self) -> trc::Result>> { todo!() } diff --git a/crates/store/src/registry/mod.rs b/crates/store/src/registry/mod.rs index 69b01f91..e2da0869 100644 --- a/crates/store/src/registry/mod.rs +++ b/crates/store/src/registry/mod.rs @@ -3,3 +3,10 @@ * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ + +use registry::types::{ObjectType, id::Id}; + +pub struct RegistryObject { + pub id: Id, + pub object: T, +} diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 460ecd33..15e8eaf3 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,8 +5,8 @@ edition = "2024" [features] #default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "nats", "azure", "foundationdb"] -default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "foundationdb"] -#default = ["rocks", "foundationdb"] +#default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis", "foundationdb"] +default = ["rocks"] sqlite = ["store/sqlite"] foundationdb = ["store/foundation", "common/foundation"] postgres = ["store/postgres"]