/* * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC * * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ #![warn(clippy::large_futures)] 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, network::Network, scripts::Scripting, smtp::{ SmtpConfig, resolver::{Policy, Tlsa}, }, spamfilter::{IpResolver, SpamFilterConfig}, storage::Storage, telemetry::Metrics, }; use ipc::{BroadcastEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent}; use listener::{asn::AsnGeoLookupData, blocked::Security, tls::AcmeProviders}; use mail_auth::{MX, Txt}; use manager::webadmin::{Resource, WebAdminManager}; use nlp::bayes::{TokenHash, Weights}; use parking_lot::{Mutex, RwLock}; use rustls::sign::CertifiedKey; use std::{ hash::{BuildHasher, Hash, Hasher}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::{Arc, atomic::AtomicBool}, time::{Duration, Instant}, }; use store::rand::{Rng, distr::Alphanumeric}; use tinyvec::TinyVec; use tokio::sync::{Notify, Semaphore, mpsc}; use tokio_rustls::TlsConnector; use types::{acl::AclGrant, special_use::SpecialUse}; use utils::{ cache::{Cache, CacheItemWeight, CacheWithTtl}, snowflake::SnowflakeIdGenerator, }; pub mod addresses; pub mod auth; pub mod config; pub mod core; pub mod dns; pub mod expr; pub mod i18n; pub mod ipc; pub mod listener; pub mod manager; pub mod scripts; pub mod sharing; pub mod storage; pub mod telemetry; // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] pub mod enterprise; // SPDX-SnippetEnd pub use psl; pub static VERSION_PRIVATE: &str = env!("CARGO_PKG_VERSION"); pub static VERSION_PUBLIC: &str = "1.0.0"; pub static USER_AGENT: &str = "Stalwart/1.0.0"; pub static DAEMON_NAME: &str = concat!("Stalwart v", env!("CARGO_PKG_VERSION"),); pub static PROD_ID: &str = "-//Stalwart Labs LLC//Stalwart Server//EN"; /* Schema history: 1 - v0.12.0 2 - v0.12.4 3 - v0.13.0 */ pub const DATABASE_SCHEMA_VERSION: u32 = 3; pub const LONG_1D_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24); pub const LONG_1Y_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24 * 365); pub const IPC_CHANNEL_BUFFER: usize = 1024; pub const KV_ACME: u8 = 0; pub const KV_OAUTH: u8 = 1; pub const KV_RATE_LIMIT_RCPT: u8 = 2; pub const KV_RATE_LIMIT_SCAN: u8 = 3; pub const KV_RATE_LIMIT_LOITER: u8 = 4; pub const KV_RATE_LIMIT_AUTH: u8 = 5; pub const KV_RATE_LIMIT_SMTP: u8 = 6; pub const KV_RATE_LIMIT_CONTACT: u8 = 7; pub const KV_RATE_LIMIT_HTTP_AUTHENTICATED: u8 = 8; pub const KV_RATE_LIMIT_HTTP_ANONYMOUS: u8 = 9; pub const KV_RATE_LIMIT_IMAP: u8 = 10; pub const KV_REPUTATION_IP: u8 = 12; pub const KV_REPUTATION_FROM: u8 = 13; pub const KV_REPUTATION_DOMAIN: u8 = 14; pub const KV_REPUTATION_ASN: u8 = 15; pub const KV_GREYLIST: u8 = 16; pub const KV_BAYES_MODEL_GLOBAL: u8 = 17; pub const KV_BAYES_MODEL_USER: u8 = 18; pub const KV_TRUSTED_REPLY: u8 = 19; pub const KV_LOCK_PURGE_ACCOUNT: u8 = 20; pub const KV_LOCK_QUEUE_MESSAGE: u8 = 21; pub const KV_LOCK_QUEUE_REPORT: u8 = 22; pub const KV_LOCK_TASK: u8 = 23; pub const KV_LOCK_HOUSEKEEPER: u8 = 24; pub const KV_LOCK_DAV: u8 = 25; pub const KV_SIEVE_ID: u8 = 26; #[derive(Clone)] pub struct Server { pub inner: Arc, pub core: Arc, } pub struct Inner { pub shared_core: ArcSwap, pub data: Data, pub cache: Caches, pub ipc: Ipc, } pub struct Data { pub tls_certificates: ArcSwap>>, pub tls_self_signed_cert: Option>, pub blocked_ips: RwLock>, pub asn_geo_data: AsnGeoLookupData, pub jmap_id_gen: SnowflakeIdGenerator, pub queue_id_gen: SnowflakeIdGenerator, pub span_id_gen: SnowflakeIdGenerator, pub queue_status: AtomicBool, pub webadmin: WebAdminManager, pub logos: Mutex>>>>, pub smtp_connectors: TlsConnectors, } pub struct Caches { pub access_tokens: Cache>, pub http_auth: Cache, pub permissions: Cache>, pub messages: Cache>, pub files: Cache>, pub contacts: Cache>, pub events: Cache>, pub scheduling: Cache>, pub bayes: CacheWithTtl, pub dns_txt: CacheWithTtl, pub dns_mx: CacheWithTtl>>, pub dns_ptr: CacheWithTtl>>, pub dns_ipv4: CacheWithTtl>>, pub dns_ipv6: CacheWithTtl>>, pub dns_tlsa: CacheWithTtl>, pub dbs_mta_sts: CacheWithTtl>, pub dns_rbl: CacheWithTtl>>, } #[derive(Debug, Clone)] pub struct CacheSwap(pub Arc>); #[derive(Debug, Clone)] pub struct MessageStoreCache { pub emails: Arc, pub mailboxes: Arc, pub update_lock: Arc, pub last_change_id: u64, pub size: u64, } #[derive(Debug, Clone)] pub struct MailboxesCache { pub change_id: u64, pub index: AHashMap, pub items: Vec, pub size: u64, } #[derive(Debug, Clone)] pub struct MessagesCache { pub change_id: u64, pub items: Vec, pub index: AHashMap, pub keywords: Vec, pub size: u64, } #[derive(Debug, Clone)] pub struct MessageCache { pub document_id: u32, pub mailboxes: TinyVec<[MessageUidCache; 2]>, pub keywords: u128, pub thread_id: u32, pub change_id: u64, } #[derive(Debug, Default, Clone, Copy)] pub struct MessageUidCache { pub mailbox_id: u32, pub uid: u32, } #[derive(Debug, Clone)] pub struct MailboxCache { pub document_id: u32, pub name: String, pub path: String, pub role: SpecialUse, pub parent_id: u32, pub sort_order: u32, pub subscribers: TinyVec<[u32; 4]>, pub uid_validity: u32, pub acls: TinyVec<[AclGrant; 2]>, } #[derive(Debug, Clone)] pub struct HttpAuthCache { pub account_id: u32, pub revision: u64, pub expires: Instant, } pub struct Ipc { pub state_tx: mpsc::Sender, pub housekeeper_tx: mpsc::Sender, pub task_tx: Arc, pub queue_tx: mpsc::Sender, pub report_tx: mpsc::Sender, pub broadcast_tx: Option>, } pub struct TlsConnectors { pub pki_verify: TlsConnector, pub dummy_verify: TlsConnector, } pub struct NameWrapper(pub String); #[derive(Debug, Clone)] pub struct DavResources { pub base_path: String, pub paths: AHashSet, pub resources: Vec, pub item_change_id: u64, pub container_change_id: u64, pub highest_change_id: u64, pub size: u64, pub update_lock: Arc, } #[derive(Debug, Clone)] pub struct DavPath { pub path: String, pub parent_id: Option, pub hierarchy_seq: u32, pub resource_idx: usize, } #[derive(Debug, Clone)] pub struct DavResource { pub document_id: u32, pub data: DavResourceMetadata, } #[derive(Debug, Clone, Copy)] pub struct DavResourcePath<'x> { pub path: &'x DavPath, pub resource: &'x DavResource, } #[derive(Debug, Clone)] pub enum DavResourceMetadata { File { name: String, size: Option, parent_id: Option, acls: TinyVec<[AclGrant; 2]>, }, Calendar { name: String, acls: TinyVec<[AclGrant; 2]>, tz: Tz, }, CalendarEvent { names: TinyVec<[DavName; 2]>, start: i64, duration: u32, }, CalendarScheduling { names: TinyVec<[DavName; 2]>, }, AddressBook { name: String, acls: TinyVec<[AclGrant; 2]>, }, ContactCard { names: TinyVec<[DavName; 2]>, }, } #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] #[rkyv(derive(Debug))] pub struct DavName { pub name: String, pub parent_id: u32, } #[derive(Clone, Default)] pub struct Core { pub storage: Storage, pub sieve: Scripting, pub network: Network, pub acme: AcmeProviders, pub oauth: OAuthConfig, pub smtp: SmtpConfig, pub jmap: JmapConfig, pub groupware: GroupwareConfig, pub spam: SpamFilterConfig, pub imap: ImapConfig, pub metrics: Metrics, // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] pub enterprise: Option, // SPDX-SnippetEnd } impl CacheItemWeight for CacheSwap { fn weight(&self) -> u64 { std::mem::size_of::>() as u64 + self.0.load().weight() } } impl CacheItemWeight for MessageStoreCache { fn weight(&self) -> u64 { self.size } } impl CacheItemWeight for HttpAuthCache { fn weight(&self) -> u64 { std::mem::size_of::() as u64 } } impl CacheItemWeight for DavResources { fn weight(&self) -> u64 { self.size } } pub trait IntoString: Sized { fn into_string(self) -> String; } impl IntoString for Vec { fn into_string(self) -> String { String::from_utf8(self) .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) } } #[derive(Debug, Clone, Eq)] pub struct ThrottleKey { pub hash: [u8; 32], } impl PartialEq for ThrottleKey { fn eq(&self, other: &Self) -> bool { self.hash == other.hash } } impl std::hash::Hash for ThrottleKey { fn hash(&self, state: &mut H) { self.hash.hash(state); } } impl AsRef<[u8]> for ThrottleKey { fn as_ref(&self) -> &[u8] { &self.hash } } #[derive(Default)] pub struct ThrottleKeyHasher { hash: u64, } impl Hasher for ThrottleKeyHasher { fn finish(&self) -> u64 { self.hash } fn write(&mut self, bytes: &[u8]) { debug_assert!( bytes.len() >= std::mem::size_of::(), "ThrottleKeyHasher: input too short {bytes:?}" ); self.hash = bytes .get(0..std::mem::size_of::()) .map_or(0, |b| u64::from_ne_bytes(b.try_into().unwrap())); } } #[derive(Clone, Default)] pub struct ThrottleKeyHasherBuilder {} impl BuildHasher for ThrottleKeyHasherBuilder { type Hasher = ThrottleKeyHasher; fn build_hasher(&self) -> Self::Hasher { ThrottleKeyHasher::default() } } #[cfg(feature = "test_mode")] #[allow(clippy::derivable_impls)] impl Default for Server { fn default() -> Self { Self { inner: Default::default(), core: Default::default(), } } } #[cfg(feature = "test_mode")] #[allow(clippy::derivable_impls)] impl Default for Inner { fn default() -> Self { Self { shared_core: Default::default(), data: Default::default(), ipc: Default::default(), cache: Default::default(), } } } #[cfg(feature = "test_mode")] #[allow(clippy::derivable_impls)] impl Default for Caches { fn default() -> Self { Self { access_tokens: Cache::new(1024, 10 * 1024 * 1024), http_auth: Cache::new(1024, 10 * 1024 * 1024), permissions: Cache::new(1024, 10 * 1024 * 1024), messages: Cache::new(1024, 25 * 1024 * 1024), files: Cache::new(1024, 10 * 1024 * 1024), contacts: Cache::new(1024, 10 * 1024 * 1024), events: Cache::new(1024, 10 * 1024 * 1024), scheduling: Cache::new(1024, 10 * 1024 * 1024), bayes: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_rbl: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_txt: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_mx: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_ptr: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_ipv4: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_ipv6: CacheWithTtl::new(1024, 10 * 1024 * 1024), dns_tlsa: CacheWithTtl::new(1024, 10 * 1024 * 1024), dbs_mta_sts: CacheWithTtl::new(1024, 10 * 1024 * 1024), } } } #[cfg(feature = "test_mode")] impl Default for Ipc { fn default() -> Self { Self { state_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0, housekeeper_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0, task_tx: Default::default(), queue_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0, report_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0, broadcast_tx: None, } } } pub fn ip_to_bytes(ip: &IpAddr) -> Vec { match ip { IpAddr::V4(ip) => ip.octets().to_vec(), IpAddr::V6(ip) => ip.octets().to_vec(), } } pub fn ip_to_bytes_prefix(prefix: u8, ip: &IpAddr) -> Vec { match ip { IpAddr::V4(ip) => { let mut buf = Vec::with_capacity(5); buf.push(prefix); buf.extend_from_slice(&ip.octets()); buf } IpAddr::V6(ip) => { let mut buf = Vec::with_capacity(17); buf.push(prefix); buf.extend_from_slice(&ip.octets()); buf } } } impl DavResourcePath<'_> { #[inline(always)] pub fn document_id(&self) -> u32 { self.resource.document_id } #[inline(always)] pub fn parent_id(&self) -> Option { self.path.parent_id } #[inline(always)] pub fn path(&self) -> &str { self.path.path.as_str() } #[inline(always)] pub fn is_container(&self) -> bool { self.resource.is_container() } #[inline(always)] pub fn hierarchy_seq(&self) -> u32 { self.path.hierarchy_seq } #[inline(always)] pub fn size(&self) -> u32 { self.resource.size().unwrap_or_default() } } impl DavResources { pub fn by_path(&self, name: &str) -> Option> { self.paths.get(name).map(|path| DavResourcePath { path, resource: &self.resources[path.resource_idx], }) } pub fn container_resource_by_id(&self, id: u32) -> Option<&DavResource> { self.resources .iter() .find(|res| res.document_id == id && res.is_container()) } pub fn container_resource_path_by_id(&self, id: u32) -> Option> { self.resources .iter() .enumerate() .find(|(_, resource)| resource.document_id == id && resource.is_container()) .and_then(|(idx, resource)| { self.paths .iter() .find(|path| path.resource_idx == idx) .map(|path| DavResourcePath { path, resource }) }) } pub fn subtree(&self, search_path: &str) -> impl Iterator> { let prefix = format!("{search_path}/"); self.paths.iter().filter_map(move |path| { if path.path.starts_with(&prefix) || path.path == search_path { Some(DavResourcePath { path, resource: &self.resources[path.resource_idx], }) } else { None } }) } pub fn subtree_with_depth( &self, search_path: &str, depth: usize, ) -> impl Iterator> { let prefix = format!("{search_path}/"); self.paths.iter().filter_map(move |path| { if path .path .strip_prefix(&prefix) .is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() < depth) || path.path.as_str() == search_path { Some(DavResourcePath { path, resource: &self.resources[path.resource_idx], }) } else { None } }) } pub fn tree_with_depth(&self, depth: usize) -> impl Iterator> { self.paths.iter().filter_map(move |path| { if path.path.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth { Some(DavResourcePath { path, resource: &self.resources[path.resource_idx], }) } else { None } }) } pub fn children(&self, parent_id: u32) -> impl Iterator> { self.paths .iter() .filter(move |item| item.parent_id.is_some_and(|id| id == parent_id)) .map(|path| DavResourcePath { path, resource: &self.resources[path.resource_idx], }) } pub fn children_ids(&self, parent_id: u32) -> impl Iterator { self.paths .iter() .filter(move |item| item.parent_id.is_some_and(|id| id == parent_id)) .map(|path| self.resources[path.resource_idx].document_id) } pub fn format_resource(&self, resource: DavResourcePath<'_>) -> String { if resource.resource.is_container() { format!("{}{}/", self.base_path, resource.path.path) } else { format!("{}{}", self.base_path, resource.path.path) } } pub fn format_collection(&self, name: &str) -> String { format!("{}{name}/", self.base_path) } pub fn format_item(&self, name: &str) -> String { format!("{}{}", self.base_path, name) } } const SCHEDULE_INBOX_ID: u32 = u32::MAX - 1; impl DavResource { pub fn is_child_of(&self, parent_id: u32) -> bool { match &self.data { DavResourceMetadata::File { parent_id: id, .. } => id.is_some_and(|id| id == parent_id), DavResourceMetadata::CalendarEvent { names, .. } => { names.iter().any(|name| name.parent_id == parent_id) } DavResourceMetadata::ContactCard { names } => { names.iter().any(|name| name.parent_id == parent_id) } DavResourceMetadata::CalendarScheduling { names } => { names.is_empty() && parent_id == SCHEDULE_INBOX_ID } _ => false, } } pub fn parent_id(&self) -> Option { match &self.data { DavResourceMetadata::File { parent_id, .. } => *parent_id, DavResourceMetadata::CalendarEvent { names, .. } => { names.first().map(|name| name.parent_id) } DavResourceMetadata::ContactCard { names } => names.first().map(|name| name.parent_id), DavResourceMetadata::CalendarScheduling { names } if names.is_empty() => { Some(SCHEDULE_INBOX_ID) } _ => None, } } pub fn child_names(&self) -> Option<&[DavName]> { match &self.data { DavResourceMetadata::CalendarEvent { names, .. } => Some(names.as_slice()), DavResourceMetadata::ContactCard { names } => Some(names.as_slice()), DavResourceMetadata::CalendarScheduling { names } if !names.is_empty() => { Some(names.as_slice()) } _ => None, } } pub fn container_name(&self) -> Option<&str> { match &self.data { DavResourceMetadata::File { name, .. } => Some(name.as_str()), DavResourceMetadata::Calendar { name, .. } => Some(name.as_str()), DavResourceMetadata::AddressBook { name, .. } => Some(name.as_str()), DavResourceMetadata::CalendarScheduling { names } if names.is_empty() => { Some(if self.document_id == SCHEDULE_INBOX_ID { "inbox" } else { "outbox" }) } _ => None, } } pub fn has_hierarchy_changes(&self, other: &DavResource) -> bool { match (&self.data, &other.data) { ( DavResourceMetadata::File { name: a, parent_id: c, .. }, DavResourceMetadata::File { name: b, parent_id: d, .. }, ) => a != b || c != d, ( DavResourceMetadata::Calendar { name: a, .. }, DavResourceMetadata::Calendar { name: b, .. }, ) => a != b, ( DavResourceMetadata::AddressBook { name: a, .. }, DavResourceMetadata::AddressBook { name: b, .. }, ) => a != b, ( DavResourceMetadata::CalendarEvent { names: a, .. }, DavResourceMetadata::CalendarEvent { names: b, .. }, ) => a != b, ( DavResourceMetadata::ContactCard { names: a, .. }, DavResourceMetadata::ContactCard { names: b, .. }, ) => a != b, ( DavResourceMetadata::CalendarScheduling { names: a, .. }, DavResourceMetadata::CalendarScheduling { names: b, .. }, ) => a != b, _ => unreachable!(), } } pub fn event_time_range(&self) -> Option<(i64, i64)> { match &self.data { DavResourceMetadata::CalendarEvent { start, duration, .. } => Some((*start, *start + *duration as i64)), _ => None, } } pub fn timezone(&self) -> Option { match &self.data { DavResourceMetadata::Calendar { tz, .. } => Some(*tz), _ => None, } } pub fn is_container(&self) -> bool { match &self.data { DavResourceMetadata::File { size, .. } => size.is_none(), DavResourceMetadata::Calendar { .. } | DavResourceMetadata::AddressBook { .. } => true, DavResourceMetadata::CalendarScheduling { names } => names.is_empty(), _ => false, } } pub fn size(&self) -> Option { match &self.data { DavResourceMetadata::File { size, .. } => *size, _ => None, } } pub fn acls(&self) -> Option<&[AclGrant]> { match &self.data { DavResourceMetadata::File { acls, .. } => Some(acls.as_slice()), DavResourceMetadata::Calendar { acls, .. } => Some(acls.as_slice()), DavResourceMetadata::AddressBook { acls, .. } => Some(acls.as_slice()), _ => None, } } } impl Hash for DavPath { fn hash(&self, state: &mut H) { self.path.hash(state); } } impl PartialEq for DavPath { fn eq(&self, other: &Self) -> bool { self.path == other.path } } impl Eq for DavPath {} impl std::borrow::Borrow for DavPath { fn borrow(&self) -> &str { &self.path } } impl std::hash::Hash for DavResource { fn hash(&self, state: &mut H) { self.document_id.hash(state); } } impl PartialEq for DavResource { fn eq(&self, other: &Self) -> bool { self.document_id == other.document_id } } impl Eq for DavResource {} impl std::borrow::Borrow for DavResource { fn borrow(&self) -> &u32 { &self.document_id } } impl DavName { pub fn new(name: String, parent_id: u32) -> Self { Self { name, parent_id } } pub fn new_with_rand_name(parent_id: u32) -> Self { Self { name: store::rand::rng() .sample_iter(Alphanumeric) .take(10) .map(char::from) .collect::(), parent_id, } } } impl CacheSwap { pub fn new(value: Arc) -> Self { Self(Arc::new(ArcSwap::new(value))) } pub fn load_full(&self) -> Arc { self.0.load_full() } pub fn update(&self, value: Arc) { self.0.store(value); } } impl MailboxCache { pub fn parent_id(&self) -> Option { if self.parent_id != u32::MAX { Some(self.parent_id) } else { None } } pub fn sort_order(&self) -> Option { if self.sort_order != u32::MAX { Some(self.sort_order) } else { None } } pub fn is_root(&self) -> bool { self.parent_id == u32::MAX } } pub const DEFAULT_LOGO_RAW: &str = r#" "#; pub const DEFAULT_LOGO_BASE64: &str = "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 AElFTkSuQmCC";