diff options
author | mdecimus <mauro@stalw.art> | 2024-09-26 14:49:46 +0200 |
---|---|---|
committer | mdecimus <mauro@stalw.art> | 2024-09-26 14:49:46 +0200 |
commit | ce8182ae07d3f5d341f0e58d3286e3ee20295c12 (patch) | |
tree | 8a6865308f5985eb68a589d64a79bd85e665e7e2 /crates | |
parent | 24967c1e86ab4333736c5e3d69ea4bb3af7f56f0 (diff) |
Core refactoring
Diffstat (limited to 'crates')
204 files changed, 5093 insertions, 3643 deletions
diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index 6f30e404..6dac6a47 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -11,6 +11,7 @@ store = { path = "../store" } trc = { path = "../trc" } directory = { path = "../directory" } jmap_proto = { path = "../jmap-proto" } +imap_proto = { path = "../imap-proto" } sieve-rs = { version = "0.5" } mail-parser = { version = "0.9", features = ["full_encoding", "ludicrous_mode"] } mail-builder = { version = "0.3", features = ["ludicrous_mode"] } @@ -59,6 +60,7 @@ zip = "2.1" pwhash = "1.0.0" xxhash-rust = { version = "0.8.5", features = ["xxh3"] } psl = "2" +dashmap = "6.0" [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs index 0303b7fc..d56385b1 100644 --- a/crates/common/src/addresses.rs +++ b/crates/common/src/addresses.rs @@ -14,10 +14,10 @@ use crate::{ expr::{ functions::ResolveVariable, if_block::IfBlock, tokenizer::TokenMap, Variable, V_RECIPIENT, }, - Core, + Server, }; -impl Core { +impl Server { pub async fn email_to_ids( &self, directory: &Directory, @@ -25,6 +25,7 @@ impl Core { session_id: u64, ) -> trc::Result<Vec<u32>> { let mut address = self + .core .smtp .session .rcpt @@ -38,6 +39,7 @@ impl Core { if !result.is_empty() { return Ok(result); } else if let Some(catch_all) = self + .core .smtp .session .rcpt @@ -62,6 +64,7 @@ impl Core { ) -> trc::Result<bool> { // Expand subaddress let mut address = self + .core .smtp .session .rcpt @@ -73,6 +76,7 @@ impl Core { if directory.rcpt(address.as_ref()).await? { return Ok(true); } else if let Some(catch_all) = self + .core .smtp .session .rcpt @@ -97,7 +101,8 @@ impl Core { ) -> trc::Result<Vec<String>> { directory .vrfy( - self.smtp + self.core + .smtp .session .rcpt .subaddressing @@ -116,7 +121,8 @@ impl Core { ) -> trc::Result<Vec<String>> { directory .expn( - self.smtp + self.core + .smtp .session .rcpt .subaddressing @@ -170,7 +176,7 @@ impl ResolveVariable for Address<'_> { impl AddressMapping { pub async fn to_subaddress<'x, 'y: 'x>( &'x self, - core: &Core, + core: &Server, address: &'y str, session_id: u64, ) -> Cow<'x, str> { @@ -198,7 +204,7 @@ impl AddressMapping { pub async fn to_catch_all<'x, 'y: 'x>( &'x self, - core: &Core, + core: &Server, address: &'y str, session_id: u64, ) -> Option<Cow<'x, str>> { diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 7ff882e4..113c33d8 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -25,11 +25,11 @@ use utils::map::{ vec_map::VecMap, }; -use crate::Core; +use crate::Server; use super::{roles::RolePermissions, AccessToken, ResourceToken, TenantInfo}; -impl Core { +impl Server { pub async fn build_access_token(&self, mut principal: Principal) -> trc::Result<AccessToken> { let mut role_permissions = RolePermissions::default(); @@ -75,8 +75,7 @@ impl Core { tenant = Some(TenantInfo { id: tenant_id, quota: self - .storage - .data + .store() .query(QueryBy::Id(tenant_id), false) .await .caused_by(trc::location!())? @@ -111,12 +110,7 @@ impl Core { } pub async fn get_access_token(&self, account_id: u32) -> trc::Result<AccessToken> { - let err = match self - .storage - .directory - .query(QueryBy::Id(account_id), true) - .await - { + let err = match self.directory().query(QueryBy::Id(account_id), true).await { Ok(Some(principal)) => { return self .update_access_token(self.build_access_token(principal).await?) @@ -129,7 +123,7 @@ impl Core { Err(err) => Err(err), }; - match &self.jmap.fallback_admin { + match &self.core.jmap.fallback_admin { Some((_, secret)) if account_id == u32::MAX => { self.update_access_token( self.build_access_token(Principal::fallback_admin(secret)) @@ -150,8 +144,7 @@ impl Core { .chain(access_token.member_of.iter().copied()) { for acl_item in self - .storage - .data + .store() .acl_query(AclQuery::HasAccess { grant_account_id }) .await .caused_by(trc::location!())? @@ -191,15 +184,15 @@ impl Core { } pub fn cache_access_token(&self, access_token: Arc<AccessToken>) { - self.security.access_tokens.insert_with_ttl( + self.inner.data.access_tokens.insert_with_ttl( access_token.primary_id(), access_token, - Instant::now() + self.jmap.session_cache_ttl, + Instant::now() + self.core.jmap.session_cache_ttl, ); } pub async fn get_cached_access_token(&self, primary_id: u32) -> trc::Result<Arc<AccessToken>> { - if let Some(access_token) = self.security.access_tokens.get_with_ttl(&primary_id) { + if let Some(access_token) = self.inner.data.access_tokens.get_with_ttl(&primary_id) { Ok(access_token) } else { // Refresh ACL token diff --git a/crates/common/src/auth/roles.rs b/crates/common/src/auth/roles.rs index 79a59cfd..1180ed79 100644 --- a/crates/common/src/auth/roles.rs +++ b/crates/common/src/auth/roles.rs @@ -13,7 +13,7 @@ use directory::{ }; use trc::AddContext; -use crate::Core; +use crate::Server; #[derive(Debug, Clone, Default)] pub struct RolePermissions { @@ -26,14 +26,14 @@ static ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> = LazyLock::new(admin_p static TENANT_ADMIN_PERMISSIONS: LazyLock<Arc<RolePermissions>> = LazyLock::new(tenant_admin_permissions); -impl Core { +impl Server { pub async fn get_role_permissions(&self, role_id: u32) -> trc::Result<Arc<RolePermissions>> { match role_id { ROLE_USER => Ok(USER_PERMISSIONS.clone()), ROLE_ADMIN => Ok(ADMIN_PERMISSIONS.clone()), ROLE_TENANT_ADMIN => Ok(TENANT_ADMIN_PERMISSIONS.clone()), role_id => { - if let Some(role_permissions) = self.security.permissions.get(&role_id) { + if let Some(role_permissions) = self.inner.data.permissions.get(&role_id) { Ok(role_permissions.clone()) } else { self.build_role_permissions(role_id).await @@ -81,15 +81,14 @@ impl Core { } role_id => { // Try with the cache - if let Some(role_permissions) = self.security.permissions.get(&role_id) { + if let Some(role_permissions) = self.inner.data.permissions.get(&role_id) { return_permissions.union(role_permissions.as_ref()); } else { let mut role_permissions = RolePermissions::default(); // Obtain principal let mut principal = self - .storage - .data + .store() .query(QueryBy::Id(role_id), true) .await .caused_by(trc::location!())? @@ -133,7 +132,8 @@ impl Core { role_ids = parent_role_ids.into_iter(); } else { // Cache role - self.security + self.inner + .data .permissions .insert(role_id, Arc::new(role_permissions)); } @@ -149,7 +149,8 @@ impl Core { // Cache role let return_permissions = Arc::new(return_permissions); - self.security + self.inner + .data .permissions .insert(role_id, return_permissions.clone()); Ok(return_permissions) diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs new file mode 100644 index 00000000..92528e26 --- /dev/null +++ b/crates/common/src/config/inner.rs @@ -0,0 +1,163 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::{sync::Arc, time::Duration}; + +use ahash::{AHashMap, AHashSet, RandomState}; +use arc_swap::ArcSwap; +use dashmap::DashMap; +use mail_send::smtp::tls::build_tls_connector; +use nlp::bayes::cache::BayesTokenCache; +use parking_lot::RwLock; +use utils::{ + config::Config, + lru_cache::{LruCache, LruCached}, + map::ttl_dashmap::{TtlDashMap, TtlMap}, + snowflake::SnowflakeIdGenerator, +}; + +use crate::{ + listener::blocked::BlockedIps, manager::webadmin::WebAdminManager, Data, + ThrottleKeyHasherBuilder, TlsConnectors, +}; + +use super::server::tls::{build_self_signed_cert, parse_certificates}; + +impl Data { + pub fn parse(config: &mut Config) -> Self { + // Parse certificates + let mut certificates = AHashMap::new(); + let mut subject_names = AHashSet::new(); + parse_certificates(config, &mut certificates, &mut subject_names); + if subject_names.is_empty() { + subject_names.insert("localhost".to_string()); + } + + // Parse capacities + let shard_amount = config + .property::<u64>("cache.shard") + .unwrap_or(32) + .next_power_of_two() as usize; + let capacity = config.property("cache.capacity").unwrap_or(100); + + // Parse id generator + let id_generator = config + .property::<u64>("cluster.node-id") + .map(SnowflakeIdGenerator::with_node_id) + .unwrap_or_default(); + + Data { + tls_certificates: ArcSwap::from_pointee(certificates), + tls_self_signed_cert: build_self_signed_cert( + subject_names.into_iter().collect::<Vec<_>>(), + ) + .or_else(|err| { + config.new_build_error("certificate.self-signed", err); + build_self_signed_cert(vec!["localhost".to_string()]) + }) + .ok() + .map(Arc::new), + access_tokens: TtlDashMap::with_capacity(capacity, shard_amount), + http_auth_cache: TtlDashMap::with_capacity(capacity, shard_amount), + blocked_ips: RwLock::new(BlockedIps::parse(config).blocked_ip_addresses), + blocked_ips_version: 0.into(), + permissions: Default::default(), + permissions_version: 0.into(), + jmap_id_gen: id_generator.clone(), + queue_id_gen: id_generator.clone(), + span_id_gen: id_generator, + webadmin: WebAdminManager::new(), + config_version: 0.into(), + jmap_limiter: DashMap::with_capacity_and_hasher_and_shard_amount( + capacity, + RandomState::default(), + shard_amount, + ), + imap_limiter: DashMap::with_capacity_and_hasher_and_shard_amount( + capacity, + RandomState::default(), + shard_amount, + ), + account_cache: LruCache::with_capacity( + config.property("cache.account.size").unwrap_or(2048), + ), + mailbox_cache: LruCache::with_capacity( + config.property("cache.mailbox.size").unwrap_or(2048), + ), + threads_cache: LruCache::with_capacity( + config.property("cache.thread.size").unwrap_or(2048), + ), + logos: Default::default(), + smtp_session_throttle: DashMap::with_capacity_and_hasher_and_shard_amount( + capacity, + ThrottleKeyHasherBuilder::default(), + shard_amount, + ), + smtp_queue_throttle: DashMap::with_capacity_and_hasher_and_shard_amount( + capacity, + ThrottleKeyHasherBuilder::default(), + shard_amount, + ), + smtp_connectors: TlsConnectors::default(), + bayes_cache: BayesTokenCache::new( + config + .property_or_default("cache.bayes.capacity", "8192") + .unwrap_or(8192), + config + .property_or_default("cache.bayes.ttl.positive", "1h") + .unwrap_or_else(|| Duration::from_secs(3600)), + config + .property_or_default("cache.bayes.ttl.negative", "1h") + .unwrap_or_else(|| Duration::from_secs(3600)), + ), + remote_lists: Default::default(), + } + } +} + +impl Default for Data { + fn default() -> Self { + Self { + tls_certificates: Default::default(), + tls_self_signed_cert: Default::default(), + access_tokens: Default::default(), + http_auth_cache: Default::default(), + blocked_ips: Default::default(), + blocked_ips_version: 0.into(), + permissions: Default::default(), + permissions_version: 0.into(), + remote_lists: Default::default(), + jmap_id_gen: Default::default(), + queue_id_gen: Default::default(), + span_id_gen: Default::default(), + webadmin: Default::default(), + config_version: Default::default(), + jmap_limiter: Default::default(), + imap_limiter: Default::default(), + account_cache: LruCache::with_capacity(2048), + mailbox_cache: LruCache::with_capacity(2048), + threads_cache: LruCache::with_capacity(2048), + logos: Default::default(), + smtp_session_throttle: Default::default(), + smtp_queue_throttle: Default::default(), + smtp_connectors: Default::default(), + bayes_cache: BayesTokenCache::new( + 8192, + Duration::from_secs(3600), + Duration::from_secs(3600), + ), + } + } +} + +impl Default for TlsConnectors { + fn default() -> Self { + TlsConnectors { + pki_verify: build_tls_connector(false), + dummy_verify: build_tls_connector(true), + } + } +} diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index edd5ed29..79b7c059 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -10,13 +10,10 @@ use arc_swap::ArcSwap; use directory::{Directories, Directory}; use store::{BlobBackend, BlobStore, FtsStore, LookupStore, Store, Stores}; use telemetry::Metrics; -use utils::{ - config::Config, - map::ttl_dashmap::{ADashMap, TtlDashMap, TtlMap}, -}; +use utils::config::Config; use crate::{ - expr::*, listener::tls::TlsManager, manager::config::ConfigManager, Core, Network, Security, + expr::*, listener::tls::AcmeProviders, manager::config::ConfigManager, Core, Network, Security, }; use self::{ @@ -25,6 +22,7 @@ use self::{ }; pub mod imap; +pub mod inner; pub mod jmap; pub mod network; pub mod scripts; @@ -165,18 +163,8 @@ impl Core { smtp: SmtpConfig::parse(config).await, jmap: JmapConfig::parse(config), imap: ImapConfig::parse(config), - tls: TlsManager::parse(config), + acme: AcmeProviders::parse(config), metrics: Metrics::parse(config), - security: Security { - access_tokens: TtlDashMap::with_capacity(100, 32), - permissions: ADashMap::with_capacity_and_hasher_and_shard_amount( - 100, - ahash::RandomState::new(), - 32, - ), - permissions_version: Default::default(), - logos: Default::default(), - }, storage: Storage { data, blob, @@ -194,7 +182,7 @@ impl Core { } } - pub fn into_shared(self) -> Arc<ArcSwap<Self>> { - Arc::new(ArcSwap::from_pointee(self)) + pub fn into_shared(self) -> ArcSwap<Self> { + ArcSwap::from_pointee(self) } } diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index c3829dbf..b7c3faa5 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -6,7 +6,6 @@ use crate::{ expr::{if_block::IfBlock, tokenizer::TokenMap}, - listener::blocked::{AllowedIps, BlockedIps}, Network, }; use utils::config::Config; @@ -30,8 +29,7 @@ pub(crate) const HTTP_VARS: &[u32; 11] = &[ impl Default for Network { fn default() -> Self { Self { - blocked_ips: Default::default(), - allowed_ips: Default::default(), + security: Default::default(), node_id: 0, http_response_url: IfBlock::new::<()>( "server.http.url", @@ -47,8 +45,7 @@ impl Network { pub fn parse(config: &mut Config) -> Self { let mut network = Network { node_id: config.property("cluster.node-id").unwrap_or_default(), - blocked_ips: BlockedIps::parse(config), - allowed_ips: AllowedIps::parse(config), + security: Security::parse(config), ..Default::default() }; let token_map = &TokenMap::default().with_variables(HTTP_VARS); diff --git a/crates/common/src/config/scripts.rs b/crates/common/src/config/scripts.rs index 5e68e435..aa91a2f7 100644 --- a/crates/common/src/config/scripts.rs +++ b/crates/common/src/config/scripts.rs @@ -11,8 +11,6 @@ use std::{ }; use ahash::AHashMap; -use nlp::bayes::cache::BayesTokenCache; -use parking_lot::RwLock; use sieve::{compiler::grammar::Capability, Compiler, Runtime, Sieve}; use store::Stores; use utils::config::Config; @@ -33,11 +31,6 @@ pub struct Scripting { pub untrusted_scripts: AHashMap<String, Arc<Sieve>>, } -pub struct ScriptCache { - pub bayes_cache: BayesTokenCache, - pub remote_lists: RwLock<AHashMap<String, RemoteList>>, -} - #[derive(Clone)] pub struct RemoteList { pub entries: HashSet<String>, @@ -364,25 +357,6 @@ impl Scripting { } } -impl ScriptCache { - pub fn parse(config: &mut Config) -> Self { - ScriptCache { - bayes_cache: BayesTokenCache::new( - config - .property_or_default("cache.bayes.capacity", "8192") - .unwrap_or(8192), - config - .property_or_default("cache.bayes.ttl.positive", "1h") - .unwrap_or_else(|| Duration::from_secs(3600)), - config - .property_or_default("cache.bayes.ttl.negative", "1h") - .unwrap_or_else(|| Duration::from_secs(3600)), - ), - remote_lists: Default::default(), - } - } -} - impl Default for Scripting { fn default() -> Self { Scripting { @@ -410,19 +384,6 @@ impl Default for Scripting { } } -impl Default for ScriptCache { - fn default() -> Self { - Self { - bayes_cache: BayesTokenCache::new( - 8192, - Duration::from_secs(3600), - Duration::from_secs(3600), - ), - remote_lists: Default::default(), - } - } -} - impl Clone for Scripting { fn clone(&self) -> Self { Self { diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index 130a6b91..651b841d 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -23,18 +23,18 @@ use utils::{ use crate::{ listener::{tls::CertificateResolver, TcpAcceptor}, - SharedCore, + Inner, }; use super::{ tls::{TLS12_VERSION, TLS13_VERSION}, - Listener, Server, ServerProtocol, Servers, + Listener, Listeners, ServerProtocol, TcpListener, }; -impl Servers { +impl Listeners { pub fn parse(config: &mut Config) -> Self { // Parse ACME managers - let mut servers = Servers { + let mut servers = Listeners { span_id_gen: Arc::new( config .property::<u64>("cluster.node-id") @@ -139,7 +139,7 @@ impl Servers { let _ = socket.set_reuseaddr(true); } - listeners.push(Listener { + listeners.push(TcpListener { socket, addr, ttl: config @@ -197,7 +197,7 @@ impl Servers { } let span_id_gen = self.span_id_gen.clone(); - self.servers.push(Server { + self.servers.push(Listener { max_connections: config .property_or_else( ("server.listener", id, "max-connections"), @@ -213,8 +213,8 @@ impl Servers { }); } - pub fn parse_tcp_acceptors(&mut self, config: &mut Config, core: SharedCore) { - let resolver = Arc::new(CertificateResolver::new(core.clone())); + pub fn parse_tcp_acceptors(&mut self, config: &mut Config, inner: Arc<Inner>) { + 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/mod.rs b/crates/common/src/config/server/mod.rs index 67c12c00..9040af39 100644 --- a/crates/common/src/config/server/mod.rs +++ b/crates/common/src/config/server/mod.rs @@ -17,24 +17,24 @@ pub mod listener; pub mod tls; #[derive(Default)] -pub struct Servers { - pub servers: Vec<Server>, +pub struct Listeners { + pub servers: Vec<Listener>, pub tcp_acceptors: AHashMap<String, TcpAcceptor>, pub span_id_gen: Arc<SnowflakeIdGenerator>, } #[derive(Debug, Default)] -pub struct Server { +pub struct Listener { pub id: String, pub protocol: ServerProtocol, - pub listeners: Vec<Listener>, + pub listeners: Vec<TcpListener>, pub proxy_networks: Vec<IpAddrMask>, pub max_connections: u64, pub span_id_gen: Arc<SnowflakeIdGenerator>, } #[derive(Debug)] -pub struct Listener { +pub struct TcpListener { pub socket: TcpSocket, pub addr: SocketAddr, pub backlog: Option<u32>, diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index a98d8583..dfe199d0 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -12,7 +12,6 @@ use std::{ }; use ahash::{AHashMap, AHashSet}; -use arc_swap::ArcSwap; use base64::{engine::general_purpose::STANDARD, Engine}; use dns_update::{providers::rfc2136::DnsAddress, DnsUpdater, TsigAlgorithm}; use rcgen::generate_simple_self_signed; @@ -33,20 +32,15 @@ use x509_parser::{ use crate::listener::{ acme::{directory::LETS_ENCRYPT_PRODUCTION_DIRECTORY, AcmeProvider, ChallengeSettings}, - tls::TlsManager, + tls::AcmeProviders, }; pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13]; pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12]; -impl TlsManager { +impl AcmeProviders { pub fn parse(config: &mut Config) -> Self { - let mut certificates = AHashMap::new(); - let mut acme_providers = AHashMap::new(); - let mut subject_names = AHashSet::new(); - - // Parse certificates - parse_certificates(config, &mut certificates, &mut subject_names); + let mut providers = AHashMap::new(); // Parse ACME providers 'outer: for acme_id in config @@ -140,9 +134,6 @@ impl TlsManager { .property::<bool>(("acme", acme_id, "default")) .unwrap_or_default(); - // Add domains for self-signed certificate - subject_names.extend(domains.iter().cloned()); - if !domains.is_empty() { match AcmeProvider::new( acme_id.to_string(), @@ -154,7 +145,7 @@ impl TlsManager { default, ) { Ok(acme_provider) => { - acme_providers.insert(acme_id.to_string(), acme_provider); + providers.insert(acme_id.to_string(), acme_provider); } Err(err) => { config.new_build_error(format!("acme.{acme_id}"), err.to_string()); @@ -163,21 +154,7 @@ impl TlsManager { } } - if subject_names.is_empty() { - subject_names.insert("localhost".to_string()); - } - - TlsManager { - certificates: ArcSwap::from_pointee(certificates), - acme_providers, - self_signed_cert: build_self_signed_cert(subject_names.into_iter().collect::<Vec<_>>()) - .or_else(|err| { - config.new_build_error("certificate.self-signed", err); - build_self_signed_cert(vec!["localhost".to_string()]) - }) - .ok() - .map(Arc::new), - } + AcmeProviders { providers } } } diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index ffe43328..b6e6802f 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -24,7 +24,7 @@ use mail_auth::{ use parking_lot::Mutex; use utils::config::{utils::ParseValue, Config}; -use crate::Core; +use crate::Server; pub struct Resolvers { pub dns: Resolver, @@ -307,15 +307,27 @@ impl Policy { } } -impl Core { +impl Server { pub fn build_mta_sts_policy(&self) -> Option<Policy> { - self.smtp.session.mta_sts_policy.clone().and_then(|policy| { - policy.try_build(self.tls.certificates.load().keys().filter(|key| { - !key.starts_with("mta-sts.") - && !key.starts_with("autoconfig.") - && !key.starts_with("autodiscover.") - })) - }) + self.core + .smtp + .session + .mta_sts_policy + .clone() + .and_then(|policy| { + policy.try_build( + self.inner + .data + .tls_certificates + .load() + .keys() + .filter(|key| { + !key.starts_with("mta-sts.") + && !key.starts_with("autoconfig.") + && !key.starts_with("autodiscover.") + }), + ) + }) } } diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs new file mode 100644 index 00000000..2ebf66dd --- /dev/null +++ b/crates/common/src/core.rs @@ -0,0 +1,335 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::{net::IpAddr, sync::Arc}; + +use directory::{ + backend::internal::manage::ManageDirectory, core::secret::verify_secret_hash, Directory, + Principal, QueryBy, Type, +}; +use mail_send::Credentials; +use sieve::Sieve; +use store::{ + write::{QueueClass, ValueClass}, + BlobStore, FtsStore, IterateParams, LookupStore, Store, ValueKey, +}; +use trc::AddContext; + +use crate::{ + config::smtp::{ + auth::{ArcSealer, DkimSigner}, + queue::RelayHost, + }, + ImapId, Inner, MailboxState, Server, +}; + +impl Server { + #[inline(always)] + pub fn store(&self) -> &Store { + &self.core.storage.data + } + + #[inline(always)] + pub fn blob_store(&self) -> &BlobStore { + &self.core.storage.blob + } + + #[inline(always)] + pub fn fts_store(&self) -> &FtsStore { + &self.core.storage.fts + } + + #[inline(always)] + pub fn lookup_store(&self) -> &LookupStore { + &self.core.storage.lookup + } + + #[inline(always)] + pub fn directory(&self) -> &Directory { + &self.core.storage.directory + } + + pub fn get_directory(&self, name: &str) -> Option<&Arc<Directory>> { + self.core.storage.directories.get(name) + } + + pub fn get_directory_or_default(&self, name: &str, session_id: u64) -> &Arc<Directory> { + self.core.storage.directories.get(name).unwrap_or_else(|| { + if !name.is_empty() { + trc::event!( + Eval(trc::EvalEvent::DirectoryNotFound), + Id = name.to_string(), + SpanId = session_id, + ); + } + + &self.core.storage.directory + }) + } + + pub fn get_lookup_store(&self, name: &str, session_id: u64) -> &LookupStore { + self.core.storage.lookups.get(name).unwrap_or_else(|| { + if !name.is_empty() { + trc::event!( + Eval(trc::EvalEvent::StoreNotFound), + Id = name.to_string(), + SpanId = session_id, + ); + } + + &self.core.storage.lookup + }) + } + + pub fn get_arc_sealer(&self, name: &str, session_id: u64) -> Option<&ArcSealer> { + self.core + .smtp + .mail_auth + .sealers + .get(name) + .map(|s| s.as_ref()) + .or_else(|| { + trc::event!( + Arc(trc::ArcEvent::SealerNotFound), + Id = name.to_string(), + SpanId = session_id, + ); + + None + }) + } + + pub fn get_dkim_signer(&self, name: &str, session_id: u64) -> Option<&DkimSigner> { + self.core + .smtp + .mail_auth + .signers + .get(name) + .map(|s| s.as_ref()) + .or_else(|| { + trc::event!( + Dkim(trc::DkimEvent::SignerNotFound), + Id = name.to_string(), + SpanId = session_id, + ); + + None + }) + } + + pub fn get_trusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> { + self.core.sieve.trusted_scripts.get(name).or_else(|| { + trc::event!( + Sieve(trc::SieveEvent::ScriptNotFound), + Id = name.to_string(), + SpanId = session_id, + ); + + None + }) + } + + pub fn get_untrusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> { + self.core.sieve.untrusted_scripts.get(name).or_else(|| { + trc::event!( + Sieve(trc::SieveEvent::ScriptNotFound), + Id = name.to_string(), + SpanId = session_id, + ); + + None + }) + } + + pub fn get_relay_host(&self, name: &str, session_id: u64) -> Option<&RelayHost> { + self.core.smtp.queue.relay_hosts.get(name).or_else(|| { + trc::event!( + Smtp(trc::SmtpEvent::RemoteIdNotFound), + Id = name.to_string(), + SpanId = session_id, + ); + + None + }) + } + + pub async fn authenticate( + &self, + directory: &Directory, + session_id: u64, + credentials: &Credentials<String>, + remote_ip: IpAddr, + return_member_of: bool, + ) -> trc::Result<Principal> { + // First try to authenticate the user against the default directory + let result = match directory + .query(QueryBy::Credentials(credentials), return_member_of) + .await + { + Ok(Some(principal)) => { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = credentials.login().to_string(), + AccountId = principal.id(), + SpanId = session_id, + Type = principal.typ().as_str(), + ); + + return Ok(principal); + } + Ok(None) => Ok(()), + Err(err) => { + if err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) { + return Err(err); + } else { + Err(err) + } + } + }; + + // Then check if the credentials match the fallback admin or master user + match ( + &self.core.jmap.fallback_admin, + &self.core.jmap.master_user, + credentials, + ) { + (Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret }) + if username == fallback_admin => + { + if verify_secret_hash(fallback_pass, secret).await? { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = username.clone(), + SpanId = session_id, + ); + + return Ok(Principal::fallback_admin(fallback_pass)); + } + } + (_, Some((master_user, master_pass)), Credentials::Plain { username, secret }) + if username.ends_with(master_user) => + { + if verify_secret_hash(master_pass, secret).await? { + let username = username.strip_suffix(master_user).unwrap(); + let username = username.strip_suffix('%').unwrap_or(username); + + if let Some(principal) = directory + .query(QueryBy::Name(username), return_member_of) + .await? + { + trc::event!( + Auth(trc::AuthEvent::Success), + AccountName = username.to_string(), + SpanId = session_id, + AccountId = principal.id(), + Type = principal.typ().as_str(), + ); + + return Ok(principal); + } + } + } + _ => {} + } + + if let Err(err) = result { + Err(err) + } else if self.has_auth_fail2ban() { + let login = credentials.login(); + if self.is_auth_fail2banned(remote_ip, login).await? { + Err(trc::SecurityEvent::AuthenticationBan + .into_err() + .ctx(trc::Key::RemoteIp, remote_ip) + .ctx(trc::Key::AccountName, login.to_string())) + } else { + Err(trc::AuthEvent::Failed + .ctx(trc::Key::RemoteIp, remote_ip) + .ctx(trc::Key::AccountName, login.to_string())) + } + } else { + Err(trc::AuthEvent::Failed + .ctx(trc::Key::RemoteIp, remote_ip) + .ctx(trc::Key::AccountName, credentials.login().to_string())) + } + } + + pub async fn total_queued_messages(&self) -> trc::Result<u64> { + let mut total = 0; + self.store() + .iterate( + IterateParams::new( + ValueKey::from(ValueClass::Queue(QueueClass::Message(0))), + ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))), + ) + .no_values(), + |_, _| { + total += 1; + + Ok(true) + }, + ) + .await + .map(|_| total) + } + + pub async fn total_accounts(&self) -> trc::Result<u64> { + self.store() + .count_principals(None, Type::Individual.into(), None) + .await + .caused_by(trc::location!()) + } + + pub async fn total_domains(&self) -> trc::Result<u64> { + self.store() + .count_principals(None, Type::Domain.into(), None) + .await + .caused_by(trc::location!()) + } +} + +pub trait BuildServer { + fn build_server(&self) -> Server; +} + +impl BuildServer for Arc<Inner> { + fn build_server(&self) -> Server { + Server { + inner: self.clone(), + core: self.shared_core.load_full(), + } + } +} + +trait CredentialsUsername { + fn login(&self) -> &str; +} + +impl CredentialsUsername for Credentials<String> { + fn login(&self) -> &str { + match self { + Credentials::Plain { username, .. } + | Credentials::XOauth2 { username, .. } + | Credentials::OAuthBearer { token: username } => username, + } + } +} + +impl MailboxState { + pub fn map_result_id(&self, document_id: u32, is_uid: bool) -> Option<(u32, ImapId)> { + if let Some(imap_id) = self.id_to_imap.get(&document_id) { + Some((if is_uid { imap_id.uid } else { imap_id.seqnum }, *imap_id)) + } else if is_uid { + self.next_state.as_ref().and_then(|s| { + s.next_state + .id_to_imap + .get(&document_id) + .map(|imap_id| (imap_id.uid, *imap_id)) + }) + } else { + None + } + } +} diff --git a/crates/common/src/enterprise/alerts.rs b/crates/common/src/enterprise/alerts.rs index a7d26404..6cff45e2 100644 --- a/crates/common/src/enterprise/alerts.rs +++ b/crates/common/src/enterprise/alerts.rs @@ -20,7 +20,7 @@ use trc::{Collector, MetricType, TelemetryEvent, TOTAL_EVENT_COUNT}; use super::{AlertContent, AlertContentToken, AlertMethod}; use crate::{ expr::{functions::ResolveVariable, Variable}, - Core, + Server, }; use std::fmt::Write; @@ -33,9 +33,9 @@ pub struct AlertMessage { struct CollectorResolver; -impl Core { +impl Server { pub async fn process_alerts(&self) -> Option<Vec<AlertMessage>> { - let alerts = &self.enterprise.as_ref()?.metrics_alerts; + let alerts = &self.core.enterprise.as_ref()?.metrics_alerts; if alerts.is_empty() { return None; } diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index 69608023..9f634564 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -25,7 +25,7 @@ use store::Store; use trc::{AddContext, EventType, MetricType}; use utils::config::cron::SimpleCron; -use crate::{expr::Expression, manager::webadmin::Resource, Core, HttpLimitResponse}; +use crate::{expr::Expression, manager::webadmin::Resource, Core, HttpLimitResponse, Server}; #[derive(Clone)] pub struct Enterprise { @@ -87,6 +87,14 @@ pub enum AlertContentToken { } impl Core { + pub fn is_enterprise_edition(&self) -> bool { + self.enterprise + .as_ref() + .map_or(false, |e| !e.license.is_expired()) + } +} + +impl Server { // WARNING: TAMPERING WITH THIS FUNCTION IS STRICTLY PROHIBITED // Any attempt to modify, bypass, or disable this license validation mechanism // constitutes a severe violation of the Stalwart Enterprise License Agreement. @@ -96,18 +104,20 @@ impl Core { // violators to the fullest extent of the law, including but not limited to claims // for copyright infringement, breach of contract, and fraud. + #[inline] pub fn is_enterprise_edition(&self) -> bool { - self.enterprise - .as_ref() - .map_or(false, |e| !e.license.is_expired()) + self.core.is_enterprise_edition() } pub fn licensed_accounts(&self) -> u32 { - self.enterprise.as_ref().map_or(0, |e| e.license.accounts) + self.core + .enterprise + .as_ref() + .map_or(0, |e| e.license.accounts) } pub fn log_license_details(&self) { - if let Some(enterprise) = &self.enterprise { + if let Some(enterprise) = &self.core.enterprise { trc::event!( Server(trc::ServerEvent::Licensing), Details = "Stalwart Enterprise Edition license key is valid", @@ -125,15 +135,14 @@ impl Core { if self.is_enterprise_edition() { let domain = psl::domain_str(domain).unwrap_or(domain); - let logo = { self.security.logos.lock().get(domain).cloned() }; + let logo = { self.inner.data.logos.lock().get(domain).cloned() }; if let Some(logo) = logo { Ok(logo) } else { // Try fetching the logo for the domain let logo_url = if let Some(mut principal) = self - .storage - .data + .store() .query(QueryBy::Name(domain), false) .await .caused_by(trc::location!())? @@ -146,8 +155,7 @@ impl Core { logo.into() } else if let Some(tenant_id) = principal.get_int(PrincipalField::Tenant) { if let Some(logo) = self - .storage - .data + .store() .query(QueryBy::Id(tenant_id as u32), false) .await .caused_by(trc::location!())? @@ -199,7 +207,8 @@ impl Core { logo = Resource::new(content_type, contents).into(); } - self.security + self.inner + .data .logos .lock() .insert(domain.to_string(), logo.clone()); @@ -212,7 +221,8 @@ impl Core { } fn default_logo_url(&self) -> Option<String> { - self.enterprise + self.core + .enterprise .as_ref() .and_then(|e| e.logo_url.as_ref().map(|l| l.to_string())) } diff --git a/crates/common/src/expr/eval.rs b/crates/common/src/expr/eval.rs index 371a0476..0e87c203 100644 --- a/crates/common/src/expr/eval.rs +++ b/crates/common/src/expr/eval.rs @@ -9,7 +9,7 @@ use std::{borrow::Cow, cmp::Ordering, fmt::Display}; use hyper::StatusCode; use trc::EvalEvent; -use crate::Core; +use crate::Server; use super::{ functions::{ResolveVariable, FUNCTIONS}, @@ -17,7 +17,7 @@ use super::{ BinaryOperator, Constant, Expression, ExpressionItem, UnaryOperator, Variable, }; -impl Core { +impl Server { pub async fn eval_if<'x, R: TryFrom<Variable<'x>>, V: ResolveVariable>( &self, if_block: &'x IfBlock, @@ -123,7 +123,7 @@ impl IfBlock { pub async fn eval<'x, V: ResolveVariable>( &'x self, resolver: &'x V, - core: &Core, + core: &Server, session_id: u64, ) -> trc::Result<Variable<'x>> { let mut captures = Vec::new(); @@ -152,7 +152,7 @@ impl Expression { async fn eval<'x, 'y, V: ResolveVariable>( &'x self, resolver: &'x V, - core: &Core, + core: &Server, captures: &'y mut Vec<String>, session_id: u64, ) -> trc::Result<Variable<'x>> { diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index 7e94f7b2..162929cc 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -4,11 +4,11 @@ use mail_auth::IpLookupStrategy; use store::{Deserialize, Rows, Value}; use trc::AddContext; -use crate::Core; +use crate::Server; use super::*; -impl Core { +impl Server { pub(crate) async fn eval_fnc<'x>( &self, fnc_id: u32, @@ -168,7 +168,8 @@ impl Core { let record_type = arguments.next_as_string(); if record_type.eq_ignore_ascii_case("ip") { - self.smtp + self.core + .smtp .resolvers .dns .ip_lookup(entry.as_ref(), IpLookupStrategy::Ipv4thenIpv6, 10) @@ -182,7 +183,8 @@ impl Core { .into() }) } else if record_type.eq_ignore_ascii_case("mx") { - self.smtp + self.core + .smtp .resolvers .dns .mx_lookup(entry.as_ref()) @@ -205,7 +207,8 @@ impl Core { .into() }) } else if record_type.eq_ignore_ascii_case("txt") { - self.smtp + self.core + .smtp .resolvers .dns .txt_raw_lookup(entry.as_ref()) @@ -213,7 +216,8 @@ impl Core { .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) .map(|result| Variable::from(String::from_utf8(result).unwrap_or_default())) } else if record_type.eq_ignore_ascii_case("ptr") { - self.smtp + self.core + .smtp .resolvers .dns .ptr_lookup(entry.parse::<IpAddr>().map_err(|err| { @@ -232,7 +236,8 @@ impl Core { .into() }) } else if record_type.eq_ignore_ascii_case("ipv4") { - self.smtp + self.core + .smtp .resolvers .dns .ipv4_lookup(entry.as_ref()) @@ -246,7 +251,8 @@ impl Core { .into() }) } else if record_type.eq_ignore_ascii_case("ipv6") { - self.smtp + self.core + .smtp .resolvers .dns .ipv6_lookup(entry.as_ref()) diff --git a/crates/common/src/expr/functions/mod.rs b/crates/common/src/expr/functions/mod.rs index 94646b36..3d6f0c28 100644 --- a/crates/common/src/expr/functions/mod.rs +++ b/crates/common/src/expr/functions/mod.rs @@ -14,7 +14,7 @@ pub mod email; pub mod misc; pub mod text; -pub trait ResolveVariable { +pub trait ResolveVariable: Sync + Send { fn resolve_variable(&self, variable: u32) -> Variable<'_>; } diff --git a/crates/common/src/ipc.rs b/crates/common/src/ipc.rs new file mode 100644 index 00000000..9b64a318 --- /dev/null +++ b/crates/common/src/ipc.rs @@ -0,0 +1,233 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use std::{borrow::Cow, sync::Arc, time::Instant}; + +use ahash::RandomState; +use jmap_proto::types::{state::StateChange, type_state::DataType}; +use mail_auth::{ + dmarc::Dmarc, + mta_sts::TlsRpt, + report::{tlsrpt::FailureDetails, Record}, +}; +use store::{BlobStore, LookupStore, Store}; +use tokio::sync::{mpsc, oneshot}; +use utils::{map::bitmap::Bitmap, BlobHash}; + +use crate::{ + config::smtp::{ + report::AggregateFrequency, + resolver::{Policy, Tlsa}, + }, + listener::limiter::ConcurrencyLimiter, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum DeliveryResult { + Success, + TemporaryFailure { + reason: Cow<'static, str>, + }, + PermanentFailure { + code: [u8; 3], + reason: Cow<'static, str>, + }, +} + +#[derive(Debug)] +pub enum DeliveryEvent { + Ingest { + message: IngestMessage, + result_tx: oneshot::Sender<Vec<DeliveryResult>>, + }, + Stop, +} + +#[derive(Debug)] +pub struct IngestMessage { + pub sender_address: String, + pub recipients: Vec<String>, + pub message_blob: BlobHash, + pub message_size: usize, + pub session_id: u64, +} + +pub enum HousekeeperEvent { + AcmeReschedule { + provider_id: String, + renew_at: Instant, + }, + Purge(PurgeType), + ReloadSettings, + Exit, +} + +pub enum PurgeType { + Data(Store), + Blobs { store: Store, blob_store: BlobStore }, + Lookup(LookupStore), + Account(Option<u32>), +} + +#[derive(Debug)] +pub enum StateEvent { + Subscribe { + account_id: u32, + types: Bitmap<DataType>, + tx: mpsc::Sender<StateChange>, + }, + Publish { + state_change: StateChange, + }, + UpdateSharedAccounts { + account_id: u32, + }, + UpdateSubscriptions { + account_id: u32, + subscriptions: Vec<UpdateSubscription>, + }, + Stop, +} + +#[derive(Debug)] +pub enum UpdateSubscription { + Unverified { + id: u32, + url: String, + code: String, + keys: Option<EncryptionKeys>, + }, + Verified(PushSubscription), +} + +#[derive(Debug)] +pub struct PushSubscription { + pub id: u32, + pub url: String, + pub expires: u64, + pub types: Bitmap<DataType>, + pub keys: Option<EncryptionKeys>, +} + +#[derive(Debug, Clone)] +pub struct EncryptionKeys { + pub p256dh: Vec<u8>, + pub auth: Vec<u8>, +} + +#[derive(Debug)] +pub enum QueueEvent { + Reload, + OnHold(OnHold<QueueEventLock>), + Stop, +} + +#[derive(Debug)] +pub struct OnHold<T> { + pub next_due: Option<u64>, + pub limiters: Vec<ConcurrencyLimiter>, + pub message: T, +} + +#[derive(Debug)] +pub struct QueueEventLock { + pub due: u64, + pub queue_id: u64, + pub lock_expiry: u64, +} + +#[derive(Debug)] +pub enum ReportingEvent { + Dmarc(Box<DmarcEvent>), + Tls(Box<TlsEvent>), + Stop, +} + +#[derive(Debug)] +pub struct DmarcEvent { + pub domain: String, + pub report_record: Record, + pub dmarc_record: Arc<Dmarc>, + pub interval: AggregateFrequency, +} + +#[derive(Debug)] +pub struct TlsEvent { + pub domain: String, + pub policy: PolicyType, + pub failure: Option<FailureDetails>, + pub tls_record: Arc<TlsRpt>, + pub interval: AggregateFrequency, +} + +#[derive(Debug, Hash, PartialEq, Eq)] +pub enum PolicyType { + Tlsa(Option<Arc<Tlsa>>), + Sts(Option<Arc<Policy>>), + None, +} + +pub trait ToHash { + fn to_hash(&self) -> u64; +} + +impl ToHash for Dmarc { + fn to_hash(&self) -> u64 { + RandomState::with_seeds(1, 9, 7, 9).hash_one(self) + } +} + +impl ToHash for PolicyType { + fn to_hash(&self) -> u64 { + RandomState::with_seeds(1, 9, 7, 9).hash_one(self) + } +} + +impl From<DmarcEvent> for ReportingEvent { + fn from(value: DmarcEvent) -> Self { + ReportingEvent::Dmarc(Box::new(value)) + } +} + +impl From<TlsEvent> for ReportingEvent { + fn from(value: TlsEvent) -> Self { + ReportingEvent::Tls(Box::new(value)) + } +} + +impl From<Arc<Tlsa>> for PolicyType { + fn from(value: Arc<Tlsa>) -> Self { + PolicyType::Tlsa(Some(value)) + } +} + +impl From<Arc<Policy>> for PolicyType { + fn from(value: Arc<Policy>) -> Self { + PolicyType::Sts(Some(value)) + } +} + +impl From<&Arc<Tlsa>> for PolicyType { + fn from(value: &Arc<Tlsa>) -> Self { + PolicyType::Tlsa(Some(value.clone())) + } +} + +impl From<&Arc<Policy>> for PolicyType { + fn from(value: &Arc<Policy>) -> Self { + PolicyType::Sts(Some(value.clone())) + } +} + +impl From<(&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)> for PolicyType { + fn from(value: (&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)) -> Self { + match value { + (Some(value), _) => PolicyType::Sts(Some(value.clone())), + (_, Some(value)) => PolicyType::Tlsa(Some(value.clone())), + _ => PolicyType::None, + } + } +} diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index e8dbf60e..f983917c 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -5,59 +5,52 @@ */ use std::{ - borrow::Cow, + collections::BTreeMap, + hash::{BuildHasher, Hasher}, net::IpAddr, sync::{atomic::AtomicU8, Arc}, }; -use ahash::AHashMap; +use ahash::{AHashMap, AHashSet, RandomState}; use arc_swap::ArcSwap; use auth::{roles::RolePermissions, AccessToken}; use config::{ imap::ImapConfig, jmap::settings::JmapConfig, - scripts::Scripting, - smtp::{ - auth::{ArcSealer, DkimSigner}, - queue::RelayHost, - SmtpConfig, - }, + scripts::{RemoteList, Scripting}, + smtp::SmtpConfig, storage::Storage, telemetry::Metrics, }; -use directory::{ - backend::internal::manage::ManageDirectory, core::secret::verify_secret_hash, Directory, - Principal, QueryBy, Type, -}; +use dashmap::DashMap; + use expr::if_block::IfBlock; use futures::StreamExt; -use listener::{ - blocked::{AllowedIps, BlockedIps}, - tls::TlsManager, -}; -use mail_send::Credentials; +use imap_proto::protocol::list::Attribute; +use ipc::{DeliveryEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent}; +use listener::{blocked::Security, limiter::ConcurrencyLimiter, tls::AcmeProviders}; -use manager::webadmin::Resource; -use parking_lot::Mutex; +use manager::webadmin::{Resource, WebAdminManager}; +use nlp::bayes::cache::BayesTokenCache; +use parking_lot::{Mutex, RwLock}; use reqwest::Response; -use sieve::Sieve; -use store::{ - write::{QueueClass, ValueClass}, - IterateParams, LookupStore, ValueKey, -}; -use tokio::sync::{mpsc, oneshot}; -use trc::AddContext; +use rustls::sign::CertifiedKey; +use tokio::sync::{mpsc, Notify}; +use tokio_rustls::TlsConnector; use utils::{ + lru_cache::LruCache, map::ttl_dashmap::{ADashMap, TtlDashMap}, - BlobHash, + snowflake::SnowflakeIdGenerator, }; pub mod addresses; pub mod auth; pub mod config; +pub mod core; #[cfg(feature = "enterprise")] pub mod enterprise; pub mod expr; +pub mod ipc; pub mod listener; pub mod manager; pub mod scripts; @@ -70,351 +63,219 @@ pub static DAEMON_NAME: &str = concat!("Stalwart Mail Server v", env!("CARGO_PKG pub const IPC_CHANNEL_BUFFER: usize = 1024; -pub type SharedCore = Arc<ArcSwap<Core>>; - #[derive(Clone, Default)] -pub struct Core { - pub storage: Storage, - pub sieve: Scripting, - pub network: Network, - pub tls: TlsManager, - pub smtp: SmtpConfig, - pub jmap: JmapConfig, - pub imap: ImapConfig, - pub metrics: Metrics, - pub security: Security, - #[cfg(feature = "enterprise")] - pub enterprise: Option<enterprise::Enterprise>, +pub struct Server { + pub inner: Arc<Inner>, + pub core: Arc<Core>, } -//TODO: temporary hack until OIDC is implemented #[derive(Default)] -pub struct Security { - pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>, +pub struct Inner { + pub shared_core: ArcSwap<Core>, + pub data: Data, + pub ipc: Ipc, +} + +pub struct Data { + pub tls_certificates: ArcSwap<AHashMap<String, Arc<CertifiedKey>>>, + pub tls_self_signed_cert: Option<Arc<CertifiedKey>>, + pub access_tokens: TtlDashMap<u32, Arc<AccessToken>>, + pub http_auth_cache: TtlDashMap<String, u32>, + + pub blocked_ips: RwLock<AHashSet<IpAddr>>, + pub blocked_ips_version: AtomicU8, + pub permissions: ADashMap<u32, Arc<RolePermissions>>, pub permissions_version: AtomicU8, -} -#[derive(Clone)] -pub struct Network { - pub node_id: u64, - pub blocked_ips: BlockedIps, - pub allowed_ips: AllowedIps, - pub http_response_url: IfBlock, - pub http_allowed_endpoint: IfBlock, -} + pub bayes_cache: BayesTokenCache, + pub remote_lists: RwLock<AHashMap<String, RemoteList>>, -#[derive(Debug)] -pub enum DeliveryEvent { - Ingest { - message: IngestMessage, - result_tx: oneshot::Sender<Vec<DeliveryResult>>, - }, - Stop, + pub jmap_id_gen: SnowflakeIdGenerator, + pub queue_id_gen: SnowflakeIdGenerator, + pub span_id_gen: SnowflakeIdGenerator, + + pub webadmin: WebAdminManager, + pub config_version: AtomicU8, + + pub jmap_limiter: DashMap<u32, Arc<ConcurrencyLimiters>, RandomState>, + pub imap_limiter: DashMap<u32, Arc<ConcurrencyLimiters>, RandomState>, + + pub account_cache: LruCache<AccountId, Arc<Account>>, + pub mailbox_cache: LruCache<MailboxId, Arc<MailboxState>>, + pub threads_cache: LruCache<u32, Arc<Threads>>, + + pub logos: Mutex<AHashMap<String, Option<Resource<Vec<u8>>>>>, + + pub smtp_session_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>, + pub smtp_queue_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>, + pub smtp_connectors: TlsConnectors, } pub struct Ipc { + pub state_tx: mpsc::Sender<StateEvent>, + pub housekeeper_tx: mpsc::Sender<HousekeeperEvent>, pub delivery_tx: mpsc::Sender<DeliveryEvent>, + pub index_tx: Arc<Notify>, + pub queue_tx: mpsc::Sender<QueueEvent>, + pub report_tx: mpsc::Sender<ReportingEvent>, } -#[derive(Debug)] -pub struct IngestMessage { - pub sender_address: String, - pub recipients: Vec<String>, - pub message_blob: BlobHash, - pub message_size: usize, - pub session_id: u64, +pub struct TlsConnectors { + pub pki_verify: TlsConnector, + pub dummy_verify: TlsConnector, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum DeliveryResult { - Success, - TemporaryFailure { - reason: Cow<'static, str>, - }, - PermanentFailure { - code: [u8; 3], - reason: Cow<'static, str>, - }, +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct AccountId { + pub account_id: u32, + pub primary_id: u32, } -pub trait IntoString: Sized { - fn into_string(self) -> String; +#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] +pub struct MailboxId { + pub account_id: u32, + pub mailbox_id: u32, } -impl IntoString for Vec<u8> { - 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, Default)] +pub struct Account { + pub account_id: u32, + pub prefix: Option<String>, + pub mailbox_names: BTreeMap<String, u32>, + pub mailbox_state: AHashMap<u32, Mailbox>, + pub state_email: Option<u64>, + pub state_mailbox: Option<u64>, } -impl Core { - pub fn get_directory(&self, name: &str) -> Option<&Arc<Directory>> { - self.storage.directories.get(name) - } - - pub fn get_directory_or_default(&self, name: &str, session_id: u64) -> &Arc<Directory> { - self.storage.directories.get(name).unwrap_or_else(|| { - if !name.is_empty() { - trc::event!( - Eval(trc::EvalEvent::DirectoryNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - } +#[derive(Debug, Default, Clone)] +pub struct Mailbox { + pub has_children: bool, + pub is_subscribed: bool, + pub special_use: Option<Attribute>, + pub total_messages: Option<u32>, + pub total_unseen: Option<u32>, + pub total_deleted: Option<u32>, + pub uid_validity: Option<u32>, + pub uid_next: Option<u32>, + pub size: Option<u32>, +} - &self.storage.directory - }) - } +#[derive(Debug, Clone, Default)] +pub struct MailboxState { + pub uid_next: u32, + pub uid_validity: u32, + pub uid_max: u32, + pub id_to_imap: AHashMap<u32, ImapId>, + pub uid_to_id: AHashMap<u32, u32>, + pub total_messages: usize, + pub modseq: Option<u64>, + pub next_state: Option<Box<NextMailboxState>>, +} - pub fn get_lookup_store(&self, name: &str, session_id: u64) -> &LookupStore { - self.storage.lookups.get(name).unwrap_or_else(|| { - if !name.is_empty() { - trc::event!( - Eval(trc::EvalEvent::StoreNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - } +#[derive(Debug, Clone)] +pub struct NextMailboxState { + pub next_state: MailboxState, + pub deletions: Vec<ImapId>, +} - &self.storage.lookup - }) - } +#[derive(Debug, Clone, Copy, Default)] +pub struct ImapId { + pub uid: u32, + pub seqnum: u32, +} - pub fn get_arc_sealer(&self, name: &str, session_id: u64) -> Option<&ArcSealer> { - self.smtp - .mail_auth - .sealers - .get(name) - .map(|s| s.as_ref()) - .or_else(|| { - trc::event!( - Arc(trc::ArcEvent::SealerNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - - None - }) - } +#[derive(Debug, Default)] +pub struct Threads { + pub threads: AHashMap<u32, u32>, + pub modseq: Option<u64>, +} - pub fn get_dkim_signer(&self, name: &str, session_id: u64) -> Option<&DkimSigner> { - self.smtp - .mail_auth - .signers - .get(name) - .map(|s| s.as_ref()) - .or_else(|| { - trc::event!( - Dkim(trc::DkimEvent::SignerNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - - None - }) - } +pub struct ConcurrencyLimiters { + pub concurrent_requests: ConcurrencyLimiter, + pub concurrent_uploads: ConcurrencyLimiter, +} - pub fn get_trusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> { - self.sieve.trusted_scripts.get(name).or_else(|| { - trc::event!( - Sieve(trc::SieveEvent::ScriptNotFound), - Id = name.to_string(), - SpanId = session_id, - ); +#[derive(Clone, Default)] +pub struct Core { + pub storage: Storage, + pub sieve: Scripting, + pub network: Network, + pub acme: AcmeProviders, + pub smtp: SmtpConfig, + pub jmap: JmapConfig, + pub imap: ImapConfig, + pub metrics: Metrics, + #[cfg(feature = "enterprise")] + pub enterprise: Option<enterprise::Enterprise>, +} - None - }) - } +#[derive(Clone)] +pub struct Network { + pub node_id: u64, + pub security: Security, + pub http_response_url: IfBlock, + pub http_allowed_endpoint: IfBlock, +} - pub fn get_untrusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc<Sieve>> { - self.sieve.untrusted_scripts.get(name).or_else(|| { - trc::event!( - Sieve(trc::SieveEvent::ScriptNotFound), - Id = name.to_string(), - SpanId = session_id, - ); +pub trait IntoString: Sized { + fn into_string(self) -> String; +} - None - }) +impl IntoString for Vec<u8> { + fn into_string(self) -> String { + String::from_utf8(self) + .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) } +} - pub fn get_relay_host(&self, name: &str, session_id: u64) -> Option<&RelayHost> { - self.smtp.queue.relay_hosts.get(name).or_else(|| { - trc::event!( - Smtp(trc::SmtpEvent::RemoteIdNotFound), - Id = name.to_string(), - SpanId = session_id, - ); +#[derive(Debug, Clone, Eq)] +pub struct ThrottleKey { + pub hash: [u8; 32], +} - None - }) +impl PartialEq for ThrottleKey { + fn eq(&self, other: &Self) -> bool { + self.hash == other.hash } +} - pub async fn authenticate( - &self, - directory: &Directory, - session_id: u64, - credentials: &Credentials<String>, - remote_ip: IpAddr, - return_member_of: bool, - ) -> trc::Result<Principal> { - // First try to authenticate the user against the default directory - let result = match directory - .query(QueryBy::Credentials(credentials), return_member_of) - .await - { - Ok(Some(principal)) => { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = credentials.login().to_string(), - AccountId = principal.id(), - SpanId = session_id, - Type = principal.typ().as_str(), - ); - - return Ok(principal); - } - Ok(None) => Ok(()), - Err(err) => { - if err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) { - return Err(err); - } else { - Err(err) - } - } - }; - - // Then check if the credentials match the fallback admin or master user - match ( - &self.jmap.fallback_admin, - &self.jmap.master_user, - credentials, - ) { - (Some((fallback_admin, fallback_pass)), _, Credentials::Plain { username, secret }) - if username == fallback_admin => - { - if verify_secret_hash(fallback_pass, secret).await? { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = username.clone(), - SpanId = session_id, - ); - - return Ok(Principal::fallback_admin(fallback_pass)); - } - } - (_, Some((master_user, master_pass)), Credentials::Plain { username, secret }) - if username.ends_with(master_user) => - { - if verify_secret_hash(master_pass, secret).await? { - let username = username.strip_suffix(master_user).unwrap(); - let username = username.strip_suffix('%').unwrap_or(username); - - if let Some(principal) = directory - .query(QueryBy::Name(username), return_member_of) - .await? - { - trc::event!( - Auth(trc::AuthEvent::Success), - AccountName = username.to_string(), - SpanId = session_id, - AccountId = principal.id(), - Type = principal.typ().as_str(), - ); - - return Ok(principal); - } - } - } - _ => {} - } - - if let Err(err) = result { - Err(err) - } else if self.has_auth_fail2ban() { - let login = credentials.login(); - if self.is_auth_fail2banned(remote_ip, login).await? { - Err(trc::SecurityEvent::AuthenticationBan - .into_err() - .ctx(trc::Key::RemoteIp, remote_ip) - .ctx(trc::Key::AccountName, login.to_string())) - } else { - Err(trc::AuthEvent::Failed - .ctx(trc::Key::RemoteIp, remote_ip) - .ctx(trc::Key::AccountName, login.to_string())) - } - } else { - Err(trc::AuthEvent::Failed - .ctx(trc::Key::RemoteIp, remote_ip) - .ctx(trc::Key::AccountName, credentials.login().to_string())) - } +impl std::hash::Hash for ThrottleKey { + fn hash<H: Hasher>(&self, state: &mut H) { + self.hash.hash(state); } +} - pub async fn total_queued_messages(&self) -> trc::Result<u64> { - let mut total = 0; - self.storage - .data - .iterate( - IterateParams::new( - ValueKey::from(ValueClass::Queue(QueueClass::Message(0))), - ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))), - ) - .no_values(), - |_, _| { - total += 1; - - Ok(true) - }, - ) - .await - .map(|_| total) +impl AsRef<[u8]> for ThrottleKey { + fn as_ref(&self) -> &[u8] { + &self.hash } +} - pub async fn total_accounts(&self) -> trc::Result<u64> { - self.storage - .data - .count_principals(None, Type::Individual.into(), None) - .await - .caused_by(trc::location!()) +#[derive(Default)] +pub struct ThrottleKeyHasher { + hash: u64, +} + +impl Hasher for ThrottleKeyHasher { + fn finish(&self) -> u64 { + self.hash } - pub async fn total_domains(&self) -> trc::Result<u64> { - self.storage - .data - .count_principals(None, Type::Domain.into(), None) - .await - .caused_by(trc::location!()) + fn write(&mut self, bytes: &[u8]) { + self.hash = u64::from_ne_bytes((&bytes[..std::mem::size_of::<u64>()]).try_into().unwrap()); } } -trait CredentialsUsername { - fn login(&self) -> &str; -} +#[derive(Clone, Default)] +pub struct ThrottleKeyHasherBuilder {} -impl CredentialsUsername for Credentials<String> { - fn login(&self) -> &str { - match self { - Credentials::Plain { username, .. } - | Credentials::XOauth2 { username, .. } - | Credentials::OAuthBearer { token: username } => username, - } - } -} +impl BuildHasher for ThrottleKeyHasherBuilder { + type Hasher = ThrottleKeyHasher; -impl Clone for Security { - fn clone(&self) -> Self { - Self { - access_tokens: self.access_tokens.clone(), - permissions: self.permissions.clone(), - permissions_version: AtomicU8::new( - self.permissions_version - .load(std::sync::atomic::Ordering::Relaxed), - ), - logos: Mutex::new(self.logos.lock().clone()), - } + fn build_hasher(&self) -> Self::Hasher { + ThrottleKeyHasher::default() } } @@ -448,3 +309,23 @@ impl HttpLimitResponse for Response { Ok(Some(bytes)) } } + +impl ConcurrencyLimiters { + pub fn is_active(&self) -> bool { + self.concurrent_requests.is_active() || self.concurrent_uploads.is_active() + } +} + +#[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, + delivery_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0, + index_tx: Default::default(), + queue_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0, + report_tx: mpsc::channel(IPC_CHANNEL_BUFFER).0, + } + } +} diff --git a/crates/common/src/listener/acme/cache.rs b/crates/common/src/listener/acme/cache.rs index 5c30ee43..c7269fc7 100644 --- a/crates/common/src/listener/acme/cache.rs +++ b/crates/common/src/listener/acme/cache.rs @@ -8,11 +8,11 @@ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use trc::AddContext; use utils::config::ConfigKey; -use crate::Core; +use crate::Server; use super::AcmeProvider; -impl Core { +impl Server { pub(crate) async fn load_cert(&self, provider: &AcmeProvider) -> trc::Result<Option<Vec<u8>>> { self.read_if_exists(provider, "cert", provider.domains.as_slice()) .await @@ -68,6 +68,7 @@ impl Core { items: &[String], ) -> trc::Result<Option<Vec<u8>>> { if let Some(content) = self + .core .storage .config .get(self.build_key(provider, class, items)) @@ -94,7 +95,8 @@ impl Core { items: &[String], contents: impl AsRef<[u8]>, ) -> trc::Result<()> { - self.storage + self.core + .storage .config .set([ConfigKey { key: self.build_key(provider, class, items), diff --git a/crates/common/src/listener/acme/mod.rs b/crates/common/src/listener/acme/mod.rs index f429984d..c0faef56 100644 --- a/crates/common/src/listener/acme/mod.rs +++ b/crates/common/src/listener/acme/mod.rs @@ -16,7 +16,7 @@ use arc_swap::ArcSwap; use dns_update::DnsUpdater; use rustls::sign::CertifiedKey; -use crate::Core; +use crate::Server; use self::directory::{Account, ChallengeType}; @@ -80,7 +80,7 @@ impl AcmeProvider { } } -impl Core { +impl Server { pub async fn init_acme(&self, provider: &AcmeProvider) -> trc::Result<Duration> { // Load account key from cache or generate a new one if let Some(account_key) = self.load_account(provider).await? { @@ -100,15 +100,17 @@ impl Core { } pub fn has_acme_tls_providers(&self) -> bool { - self.tls - .acme_providers + self.core + .acme + .providers .values() .any(|p| matches!(p.challenge, ChallengeSettings::TlsAlpn01)) } pub fn has_acme_http_providers(&self) -> bool { - self.tls - .acme_providers + self.core + .acme + .providers .values() .any(|p| matches!(p.challenge, ChallengeSettings::Http01)) } diff --git a/crates/common/src/listener/acme/order.rs b/crates/common/src/listener/acme/order.rs index 983198ea..38fc1290 100644 --- a/crates/common/src/listener/acme/order.rs +++ b/crates/common/src/listener/acme/order.rs @@ -13,12 +13,12 @@ use x509_parser::parse_x509_certificate; use crate::listener::acme::directory::Identifier; use crate::listener::acme::ChallengeSettings; -use crate::Core; +use crate::Server; use super::directory::{Account, AuthStatus, Directory, OrderStatus}; use super::AcmeProvider; -impl Core { +impl Server { pub(crate) async fn process_cert( &self, provider: &AcmeProvider, @@ -210,8 +210,7 @@ impl Core { match &provider.challenge { ChallengeSettings::TlsAlpn01 => { - self.storage - .lookup + self.lookup_store() .key_set( format!("acme:{domain}").into_bytes(), account.tls_alpn_key(challenge, domain.clone())?, @@ -220,8 +219,7 @@ impl Core { .await?; } ChallengeSettings::Http01 => { - self.storage - .lookup + self.lookup_store() .key_set( format!("acme:{}", challenge.token).into_bytes(), account.http_proof(challenge)?, @@ -289,7 +287,7 @@ impl Core { let wait_until = Instant::now() + *propagation_timeout; let mut did_propagate = false; while Instant::now() < wait_until { - match self.smtp.resolvers.dns.txt_raw_lookup(&name).await { + match self.core.smtp.resolvers.dns.txt_raw_lookup(&name).await { Ok(result) => { let result = std::str::from_utf8(&result).unwrap_or_default(); if result.contains(&dns_proof) { diff --git a/crates/common/src/listener/acme/resolver.rs b/crates/common/src/listener/acme/resolver.rs index 26d91cfe..fec948fc 100644 --- a/crates/common/src/listener/acme/resolver.rs +++ b/crates/common/src/listener/acme/resolver.rs @@ -16,14 +16,14 @@ use rustls_pki_types::{CertificateDer, PrivateKeyDer, PrivatePkcs8KeyDer}; use store::write::Bincode; use trc::AcmeEvent; -use crate::{listener::acme::directory::SerializedCert, Core}; +use crate::{listener::acme::directory::SerializedCert, Server}; use super::{directory::ACME_TLS_ALPN_NAME, AcmeProvider, StaticResolver}; -impl Core { +impl Server { pub(crate) fn set_cert(&self, provider: &AcmeProvider, cert: Arc<CertifiedKey>) { // Add certificates - let mut certificates = self.tls.certificates.load().as_ref().clone(); + let mut certificates = self.inner.data.tls_certificates.load().as_ref().clone(); for domain in provider.domains.iter() { certificates.insert( domain @@ -39,29 +39,12 @@ impl Core { certificates.insert("*".to_string(), cert); } - self.tls.certificates.store(certificates.into()); + self.inner.data.tls_certificates.store(certificates.into()); } -} - -impl ResolvesServerCert for StaticResolver { - fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> { - self.key.clone() - } -} - -pub(crate) fn build_acme_static_resolver(key: Option<Arc<CertifiedKey>>) -> Arc<ServerConfig> { - let mut challenge = ServerConfig::builder() - .with_no_client_auth() - .with_cert_resolver(Arc::new(StaticResolver { key })); - challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec()); - Arc::new(challenge) -} -impl Core { pub(crate) async fn build_acme_certificate(&self, domain: &str) -> Option<Arc<CertifiedKey>> { match self - .storage - .lookup + .lookup_store() .key_get::<Bincode<SerializedCert>>(format!("acme:{domain}").into_bytes()) .await { @@ -100,6 +83,20 @@ impl Core { } } +impl ResolvesServerCert for StaticResolver { + fn resolve(&self, _: ClientHello) -> Option<Arc<CertifiedKey>> { + self.key.clone() + } +} + +pub(crate) fn build_acme_static_resolver(key: Option<Arc<CertifiedKey>>) -> Arc<ServerConfig> { + let mut challenge = ServerConfig::builder() + .with_no_client_auth() + .with_cert_resolver(Arc::new(StaticResolver { key })); + challenge.alpn_protocols.push(ACME_TLS_ALPN_NAME.to_vec()); + Arc::new(challenge) +} + pub trait IsTlsAlpnChallenge { fn is_tls_alpn_challenge(&self) -> bool; } diff --git a/crates/common/src/listener/blocked.rs b/crates/common/src/listener/blocked.rs index a46a3ce1..20de1951 100644 --- a/crates/common/src/listener/blocked.rs +++ b/crates/common/src/listener/blocked.rs @@ -4,85 +4,45 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{fmt::Debug, net::IpAddr, sync::atomic::AtomicU8}; +use std::{fmt::Debug, net::IpAddr}; use ahash::AHashSet; -use parking_lot::RwLock; use utils::config::{ ipmask::{IpAddrMask, IpAddrOrMask}, utils::ParseValue, Config, ConfigKey, Rate, }; -use crate::Core; +use crate::Server; + +#[derive(Debug, Clone)] +pub struct Security { + blocked_ip_networks: Vec<IpAddrMask>, + has_blocked_networks: bool, + + allowed_ip_addresses: AHashSet<IpAddr>, + allowed_ip_networks: Vec<IpAddrMask>, + has_allowed_networks: bool, -pub struct BlockedIps { - pub ip_addresses: RwLock<AHashSet<IpAddr>>, - pub version: AtomicU8, - ip_networks: Vec<IpAddrMask>, - has_networks: bool, auth_fail_rate: Option<Rate>, rcpt_fail_rate: Option<Rate>, loiter_fail_rate: Option<Rate>, } -#[derive(Clone)] -pub struct AllowedIps { - ip_addresses: AHashSet<IpAddr>, - ip_networks: Vec<IpAddrMask>, - has_networks: bool, -} - pub const BLOCKED_IP_KEY: &str = "server.blocked-ip"; pub const BLOCKED_IP_PREFIX: &str = "server.blocked-ip."; pub const ALLOWED_IP_KEY: &str = "server.allowed-ip"; pub const ALLOWED_IP_PREFIX: &str = "server.allowed-ip."; -impl BlockedIps { - pub fn parse(config: &mut Config) -> Self { - let mut ip_addresses = AHashSet::new(); - let mut ip_networks = Vec::new(); - - for ip in config - .set_values(BLOCKED_IP_KEY) - .map(IpAddrOrMask::parse_value) - .collect::<Vec<_>>() - { - match ip { - Ok(IpAddrOrMask::Ip(ip)) => { - ip_addresses.insert(ip); - } - Ok(IpAddrOrMask::Mask(ip)) => { - ip_networks.push(ip); - } - Err(err) => { - config.new_parse_error(BLOCKED_IP_KEY, err); - } - } - } - - BlockedIps { - ip_addresses: RwLock::new(ip_addresses), - has_networks: !ip_networks.is_empty(), - ip_networks, - auth_fail_rate: config - .property_or_default::<Option<Rate>>("server.fail2ban.authentication", "100/1d") - .unwrap_or_default(), - rcpt_fail_rate: config - .property_or_default::<Option<Rate>>("server.fail2ban.invalid-rcpt", "35/1d") - .unwrap_or_default(), - loiter_fail_rate: config - .property_or_default::<Option<Rate>>("server.fail2ban.loitering", "150/1d") - .unwrap_or_default(), - version: 0.into(), - } - } +pub struct BlockedIps { + pub blocked_ip_addresses: AHashSet<IpAddr>, + pub blocked_ip_networks: Vec<IpAddrMask>, } -impl AllowedIps { +impl Security { pub fn parse(config: &mut Config) -> Self { - let mut ip_addresses = AHashSet::new(); - let mut ip_networks = Vec::new(); + let mut allowed_ip_addresses = AHashSet::new(); + let mut allowed_ip_networks = Vec::new(); for ip in config .set_values(ALLOWED_IP_KEY) @@ -91,10 +51,10 @@ impl AllowedIps { { match ip { Ok(IpAddrOrMask::Ip(ip)) => { - ip_addresses.insert(ip); + allowed_ip_addresses.insert(ip); } Ok(IpAddrOrMask::Mask(ip)) => { - ip_networks.push(ip); + allowed_ip_networks.push(ip); } Err(err) => { config.new_parse_error(ALLOWED_IP_KEY, err); @@ -105,25 +65,37 @@ impl AllowedIps { #[cfg(not(feature = "test_mode"))] { // Add loopback addresses - ip_addresses.insert(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)); - ip_addresses.insert(IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)); + allowed_ip_addresses.insert(IpAddr::V4(std::net::Ipv4Addr::LOCALHOST)); + allowed_ip_addresses.insert(IpAddr::V6(std::net::Ipv6Addr::LOCALHOST)); } - AllowedIps { - ip_addresses, - has_networks: !ip_networks.is_empty(), - ip_networks, + let blocked = BlockedIps::parse(config); + + Security { + has_blocked_networks: !blocked.blocked_ip_networks.is_empty(), + blocked_ip_networks: blocked.blocked_ip_networks, + has_allowed_networks: !allowed_ip_networks.is_empty(), + allowed_ip_addresses, + allowed_ip_networks, + auth_fail_rate: config + .property_or_default::<Option<Rate>>("server.fail2ban.authentication", "100/1d") + .unwrap_or_default(), + rcpt_fail_rate: config + .property_or_default::<Option<Rate>>("server.fail2ban.invalid-rcpt", "35/1d") + .unwrap_or_default(), + loiter_fail_rate: config + .property_or_default::<Option<Rate>>("server.fail2ban.loitering", "150/1d") + .unwrap_or_default(), } } } -impl Core { +impl Server { pub async fn is_rcpt_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> { - if let Some(rate) = &self.network.blocked_ips.rcpt_fail_rate { + if let Some(rate) = &self.core.network.security.rcpt_fail_rate { let is_allowed = self.is_ip_allowed(&ip) || self - .storage - .lookup + .lookup_store() .is_rate_allowed(format!("r:{ip}").as_bytes(), rate, false) .await? .is_none(); @@ -137,11 +109,10 @@ impl Core { } pub async fn is_loiter_fail2banned(&self, ip: IpAddr) -> trc::Result<bool> { - if let Some(rate) = &self.network.blocked_ips.loiter_fail_rate { + if let Some(rate) = &self.core.network.security.loiter_fail_rate { let is_allowed = self.is_ip_allowed(&ip) || self - .storage - .lookup + .lookup_store() .is_rate_allowed(format!("l:{ip}").as_bytes(), rate, false) .await? .is_none(); @@ -155,17 +126,15 @@ impl Core { } pub async fn is_auth_fail2banned(&self, ip: IpAddr, login: &str) -> trc::Result<bool> { - if let Some(rate) = &self.network.blocked_ips.auth_fail_rate { + if let Some(rate) = &self.core.network.security.auth_fail_rate { let is_allowed = self.is_ip_allowed(&ip) || (self - .storage - .lookup + .lookup_store() .is_rate_allowed(format!("b:{ip}").as_bytes(), rate, false) .await? .is_none() && self - .storage - .lookup + .lookup_store() .is_rate_allowed(format!("b:{login}").as_bytes(), rate, false) .await? .is_none()); @@ -179,10 +148,11 @@ impl Core { async fn block_ip(&self, ip: IpAddr) -> trc::Result<()> { // Add IP to blocked list - self.network.blocked_ips.ip_addresses.write().insert(ip); + self.inner.data.blocked_ips.write().insert(ip); // Write blocked IP to config - self.storage + self.core + .storage .config .set([ConfigKey { key: format!("{}.{}", BLOCKED_IP_KEY, ip), @@ -191,104 +161,96 @@ impl Core { .await?; // Increment version - self.network.blocked_ips.increment_version(); + self.increment_blocked_version(); Ok(()) } pub fn has_auth_fail2ban(&self) -> bool { - self.network.blocked_ips.auth_fail_rate.is_some() + self.core.network.security.auth_fail_rate.is_some() } pub fn is_ip_blocked(&self, ip: &IpAddr) -> bool { - self.network.blocked_ips.ip_addresses.read().contains(ip) - || (self.network.blocked_ips.has_networks + self.inner.data.blocked_ips.read().contains(ip) + || (self.core.network.security.has_blocked_networks && self + .core .network - .blocked_ips - .ip_networks + .security + .blocked_ip_networks .iter() .any(|network| network.matches(ip))) } pub fn is_ip_allowed(&self, ip: &IpAddr) -> bool { - self.network.allowed_ips.ip_addresses.contains(ip) - || (self.network.allowed_ips.has_networks + self.core.network.security.allowed_ip_addresses.contains(ip) + || (self.core.network.security.has_allowed_networks && self + .core .network - .allowed_ips - .ip_networks + .security + .allowed_ip_networks .iter() .any(|network| network.matches(ip))) } -} -impl BlockedIps { - pub fn increment_version(&self) { - self.version + pub fn increment_blocked_version(&self) { + self.inner + .data + .blocked_ips_version .fetch_add(1, std::sync::atomic::Ordering::Relaxed); } } -impl Default for BlockedIps { - fn default() -> Self { +impl BlockedIps { + pub fn parse(config: &mut Config) -> Self { + let mut blocked_ip_addresses = AHashSet::new(); + let mut blocked_ip_networks = Vec::new(); + + for ip in config + .set_values(BLOCKED_IP_KEY) + .map(IpAddrOrMask::parse_value) + .collect::<Vec<_>>() + { + match ip { + Ok(IpAddrOrMask::Ip(ip)) => { + blocked_ip_addresses.insert(ip); + } + Ok(IpAddrOrMask::Mask(ip)) => { + blocked_ip_networks.push(ip); + } + Err(err) => { + config.new_parse_error(BLOCKED_IP_KEY, err); + } + } + } + Self { - ip_addresses: RwLock::new(AHashSet::new()), - ip_networks: Default::default(), - has_networks: Default::default(), - version: Default::default(), - auth_fail_rate: Default::default(), - rcpt_fail_rate: Default::default(), - loiter_fail_rate: Default::default(), + blocked_ip_addresses, + blocked_ip_networks, } } } #[allow(clippy::derivable_impls)] -impl Default for AllowedIps { +impl Default for Security { fn default() -> Self { // Add IPv4 and IPv6 loopback addresses Self { #[cfg(not(feature = "test_mode"))] - ip_addresses: AHashSet::from_iter([ + allowed_ip_addresses: AHashSet::from_iter([ IpAddr::V4(std::net::Ipv4Addr::LOCALHOST), IpAddr::V6(std::net::Ipv6Addr::LOCALHOST), ]), #[cfg(feature = "test_mode")] - ip_addresses: Default::default(), - ip_networks: Default::default(), - has_networks: Default::default(), - } - } -} - -impl Clone for BlockedIps { - fn clone(&self) -> Self { - Self { - ip_addresses: RwLock::new(self.ip_addresses.read().clone()), - ip_networks: self.ip_networks.clone(), - has_networks: self.has_networks, - version: self - .version - .load(std::sync::atomic::Ordering::Relaxed) - .into(), - auth_fail_rate: self.auth_fail_rate.clone(), - rcpt_fail_rate: self.rcpt_fail_rate.clone(), - loiter_fail_rate: self.loiter_fail_rate.clone(), + allowed_ip_addresses: Default::default(), + allowed_ip_networks: Default::default(), + has_allowed_networks: Default::default(), + blocked_ip_networks: Default::default(), + has_blocked_networks: Default::default(), + auth_fail_rate: Default::default(), + rcpt_fail_rate: Default::default(), + loiter_fail_rate: Default::default(), } } } - -impl Debug for BlockedIps { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("BlockedIps") - .field("ip_addresses", &self.ip_addresses) - .field("ip_networks", &self.ip_networks) - .field("has_networks", &self.has_networks) - .field("version", &self.version) - .field("auth_fail_rate", &self.auth_fail_rate) - .field("rcpt_fail_rate", &self.rcpt_fail_rate) - .field("loiter_fail_rate", &self.loiter_fail_rate) - .finish() - } -} diff --git a/crates/common/src/listener/listen.rs b/crates/common/src/listener/listen.rs index a2877ce9..fc1cd38f 100644 --- a/crates/common/src/listener/listen.rs +++ b/crates/common/src/listener/listen.rs @@ -10,20 +10,17 @@ use std::{ time::Duration, }; -use arc_swap::ArcSwap; use proxy_header::io::ProxiedStream; use rustls::crypto::ring::cipher_suite::TLS13_AES_128_GCM_SHA256; -use tokio::{ - net::{TcpListener, TcpStream}, - sync::watch, -}; +use tokio::{net::TcpStream, sync::watch}; use tokio_rustls::server::TlsStream; use trc::{EventType, HttpEvent, ImapEvent, ManageSieveEvent, Pop3Event, SmtpEvent}; use utils::{config::Config, UnwrapFailure}; use crate::{ - config::server::{Listener, Server, ServerProtocol, Servers}, - Core, + config::server::{Listener, Listeners, ServerProtocol, TcpListener}, + core::BuildServer, + Inner, Server, }; use super::{ @@ -31,11 +28,11 @@ use super::{ TcpAcceptor, }; -impl Server { +impl Listener { pub fn spawn( self, manager: impl SessionManager, - core: Arc<ArcSwap<Core>>, + inner: Arc<Inner>, acceptor: TcpAcceptor, shutdown_rx: watch::Receiver<bool>, ) { @@ -95,7 +92,7 @@ impl Server { let mut shutdown_rx = instance.shutdown_rx.clone(); let manager = manager.clone(); let instance = instance.clone(); - let core = core.clone(); + let inner = inner.clone(); tokio::spawn(async move { let (span_start, span_end) = match self.protocol { ServerProtocol::Smtp | ServerProtocol::Lmtp => ( @@ -125,8 +122,8 @@ impl Server { stream = listener.accept() => { match stream { Ok((stream, remote_addr)) => { - let core = core.as_ref().load_full(); - let enable_acme = (is_https && core.has_acme_tls_providers()).then_some(core.clone()); + let server = inner.build_server(); + let enable_acme = (is_https && server.has_acme_tls_providers()).then(|| server.clone()); if has_proxies && instance.proxy_networks.iter().any(|network| network.matches(&remote_addr.ip())) { let instance = instance.clone(); @@ -142,7 +139,7 @@ impl Server { .proxied_address() .map(|addr| addr.source) .unwrap_or(remote_addr); - if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &core) { + if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &server) { // Spawn session manager.spawn(session, is_tls, enable_acme, span_start, span_end); } @@ -159,7 +156,7 @@ impl Server { } } }); - } else if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &core) { + } else if let Some(session) = instance.build_session(stream, local_addr, remote_addr, &server) { // Set socket options opts.apply(&session.stream); @@ -205,7 +202,7 @@ trait BuildSession { stream: T, local_addr: SocketAddr, remote_addr: SocketAddr, - core: &Core, + server: &Server, ) -> Option<SessionData<T>>; } @@ -215,7 +212,7 @@ impl BuildSession for Arc<ServerInstance> { stream: T, local_addr: SocketAddr, remote_addr: SocketAddr, - core: &Core, + server: &Server, ) -> Option<SessionData<T>> { // Convert mapped IPv6 addresses to IPv4 let remote_ip = match remote_addr.ip() { @@ -228,7 +225,7 @@ impl BuildSession for Arc<ServerInstance> { let remote_port = remote_addr.port(); // Check if blocked - if core.is_ip_blocked(&remote_ip) { + if server.is_ip_blocked(&remote_ip) { trc::event!( Security(trc::SecurityEvent::IpBlocked), ListenerId = self.id.clone(), @@ -303,7 +300,7 @@ impl SocketOpts { } } -impl Servers { +impl Listeners { pub fn bind_and_drop_priv(&self, config: &mut Config) { // Bind as root for server in &self.servers { @@ -332,7 +329,7 @@ impl Servers { pub fn spawn( mut self, - spawn: impl Fn(Server, TcpAcceptor, watch::Receiver<bool>), + spawn: impl Fn(Listener, TcpAcceptor, watch::Receiver<bool>), ) -> (watch::Sender<bool>, watch::Receiver<bool>) { // Spawn listeners let (shutdown_tx, shutdown_rx) = watch::channel(false); @@ -348,8 +345,8 @@ impl Servers { } } -impl Listener { - pub fn listen(self) -> Result<TcpListener, String> { +impl TcpListener { + pub fn listen(self) -> Result<tokio::net::TcpListener, String> { self.socket .listen(self.backlog.unwrap_or(1024)) .map_err(|err| format!("Failed to listen on {}: {}", self.addr, err)) diff --git a/crates/common/src/listener/mod.rs b/crates/common/src/listener/mod.rs index 790adaf6..6704a228 100644 --- a/crates/common/src/listener/mod.rs +++ b/crates/common/src/listener/mod.rs @@ -19,7 +19,7 @@ use utils::{config::ipmask::IpAddrMask, snowflake::SnowflakeIdGenerator}; use crate::{ config::server::ServerProtocol, expr::{functions::ResolveVariable, *}, - Core, + Server, }; use self::limiter::{ConcurrencyLimiter, InFlight}; @@ -91,7 +91,7 @@ pub trait SessionManager: Sync + Send + 'static + Clone { &self, mut session: SessionData<T>, is_tls: bool, - acme_core: Option<Arc<Core>>, + acme_core: Option<Server>, span_start: EventType, span_end: EventType, ) { diff --git a/crates/common/src/listener/tls.rs b/crates/common/src/listener/tls.rs index fc67521c..15ec20f1 100644 --- a/crates/common/src/listener/tls.rs +++ b/crates/common/src/listener/tls.rs @@ -11,7 +11,6 @@ use std::{ }; use ahash::AHashMap; -use arc_swap::ArcSwap; use rustls::{ server::{ClientHello, ResolvesServerCert}, sign::CertifiedKey, @@ -21,7 +20,7 @@ use rustls::{ use tokio::io::{AsyncRead, AsyncWrite, AsyncWriteExt}; use tokio_rustls::{Accept, LazyConfigAcceptor}; -use crate::{Core, SharedCore}; +use crate::{Inner, Server}; use super::{ acme::{ @@ -34,36 +33,31 @@ use super::{ pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13]; pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12]; -#[derive(Default)] -pub struct TlsManager { - pub certificates: ArcSwap<AHashMap<String, Arc<CertifiedKey>>>, - pub acme_providers: AHashMap<String, AcmeProvider>, - pub self_signed_cert: Option<Arc<CertifiedKey>>, +#[derive(Default, Clone)] +pub struct AcmeProviders { + pub providers: AHashMap<String, AcmeProvider>, } #[derive(Clone)] pub struct CertificateResolver { - pub core: SharedCore, + pub inner: Arc<Inner>, } impl CertificateResolver { - pub fn new(core: SharedCore) -> Self { - Self { core } + pub fn new(inner: Arc<Inner>) -> Self { + Self { inner } } } impl ResolvesServerCert for CertificateResolver { fn resolve(&self, hello: ClientHello<'_>) -> Option<Arc<CertifiedKey>> { - self.core - .as_ref() - .load() - .resolve_certificate(hello.server_name()) + self.resolve_certificate(hello.server_name()) } } -impl Core { +impl CertificateResolver { pub(crate) fn resolve_certificate(&self, name: Option<&str>) -> Option<Arc<CertifiedKey>> { - let certs = self.tls.certificates.load(); + let certs = self.inner.data.tls_certificates.load(); name.map_or_else( || certs.get("*"), @@ -98,7 +92,7 @@ impl Core { Tls(trc::TlsEvent::NoCertificatesAvailable), Total = certs.len(), ); - self.tls.self_signed_cert.as_ref() + self.inner.data.tls_self_signed_cert.as_ref() } }) .cloned() @@ -109,7 +103,7 @@ impl TcpAcceptor { pub async fn accept<IO>( &self, stream: IO, - enable_acme: Option<Arc<Core>>, + enable_acme: Option<Server>, instance: &ServerInstance, ) -> TcpAcceptorResult<IO> where @@ -215,13 +209,3 @@ impl std::fmt::Debug for CertificateResolver { f.debug_struct("CertificateResolver").finish() } } - -impl Clone for TlsManager { - fn clone(&self) -> Self { - Self { - certificates: ArcSwap::from_pointee(self.certificates.load().as_ref().clone()), - acme_providers: self.acme_providers.clone(), - self_signed_cert: self.self_signed_cert.clone(), - } - } -} diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index b11b5988..bf3dd8b5 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::path::PathBuf; +use std::{path::PathBuf, sync::Arc}; use arc_swap::ArcSwap; use pwhash::sha512_crypt; @@ -12,14 +12,16 @@ use store::{ rand::{distributions::Alphanumeric, thread_rng, Rng}, Stores, }; +use tokio::sync::{mpsc, Notify}; use utils::{ config::{Config, ConfigKey}, failed, UnwrapFailure, }; use crate::{ - config::{server::Servers, telemetry::Telemetry}, - Core, SharedCore, + config::{server::Listeners, telemetry::Telemetry}, + ipc::{DeliveryEvent, HousekeeperEvent, QueueEvent, ReportingEvent, StateEvent}, + Core, Data, Inner, Ipc, IPC_CHANNEL_BUFFER, }; use super::{ @@ -30,8 +32,17 @@ use super::{ pub struct BootManager { pub config: Config, - pub core: SharedCore, - pub servers: Servers, + pub inner: Arc<Inner>, + pub servers: Listeners, + pub ipc_rxs: IpcReceivers, +} + +pub struct IpcReceivers { + pub state_rx: Option<mpsc::Receiver<StateEvent>>, + pub housekeeper_rx: Option<mpsc::Receiver<HousekeeperEvent>>, + pub delivery_rx: Option<mpsc::Receiver<DeliveryEvent>>, + pub queue_rx: Option<mpsc::Receiver<QueueEvent>>, + pub report_rx: Option<mpsc::Receiver<ReportingEvent>>, } const HELP: &str = concat!( @@ -135,7 +146,7 @@ impl BootManager { config.resolve_macros(&["env"]).await; // Parser servers - let mut servers = Servers::parse(&mut config); + let mut servers = Listeners::parse(&mut config); // Bind ports and drop privileges servers.bind_and_drop_priv(&mut config); @@ -314,6 +325,9 @@ impl BootManager { // Parse settings let core = Core::parse(&mut config, stores, manager).await; + // Parse data + let data = Data::parse(&mut config); + // Enable telemetry #[cfg(feature = "enterprise")] telemetry.enable(core.is_enterprise_edition()); @@ -325,16 +339,22 @@ impl BootManager { Version = env!("CARGO_PKG_VERSION"), ); - // Build shared core - let core = core.into_shared(); + // Build shared inner + let (ipc, ipc_rxs) = build_ipc(); + let inner = Arc::new(Inner { + shared_core: ArcSwap::from_pointee(core), + data, + ipc, + }); // Parse TCP acceptors - servers.parse_tcp_acceptors(&mut config, core.clone()); + servers.parse_tcp_acceptors(&mut config, inner.clone()); BootManager { - core, + inner, config, servers, + ipc_rxs, } } ImportExport::Export(path) => { @@ -363,6 +383,32 @@ impl BootManager { } } +pub fn build_ipc() -> (Ipc, IpcReceivers) { + // Build ipc receivers + let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); + let (state_tx, state_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); + let (housekeeper_tx, housekeeper_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); + let (queue_tx, queue_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); + let (report_tx, report_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); + ( + Ipc { + state_tx, + housekeeper_tx, + delivery_tx, + queue_tx, + report_tx, + index_tx: Arc::new(Notify::new()), + }, + IpcReceivers { + state_rx: Some(state_rx), + housekeeper_rx: Some(housekeeper_rx), + delivery_rx: Some(delivery_rx), + queue_rx: Some(queue_rx), + report_rx: Some(report_rx), + }, + ) +} + fn quickstart(path: impl Into<PathBuf>) { let path = path.into(); diff --git a/crates/common/src/manager/reload.rs b/crates/common/src/manager/reload.rs index f7532a36..143d4008 100644 --- a/crates/common/src/manager/reload.rs +++ b/crates/common/src/manager/reload.rs @@ -4,18 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use ahash::AHashSet; +use ahash::AHashMap; use arc_swap::ArcSwap; use store::Stores; -use utils::config::{ipmask::IpAddrOrMask, utils::ParseValue, Config}; +use utils::config::Config; use crate::{ config::{ - server::{tls::parse_certificates, Servers}, + server::{tls::parse_certificates, Listeners}, telemetry::Telemetry, }, - listener::blocked::BLOCKED_IP_KEY, - Core, + listener::blocked::{BlockedIps, BLOCKED_IP_KEY}, + Core, Server, }; use super::config::{ConfigManager, Patterns}; @@ -26,49 +26,36 @@ pub struct ReloadResult { pub tracers: Option<Telemetry>, } -impl Core { +impl Server { pub async fn reload_blocked_ips(&self) -> trc::Result<ReloadResult> { - let mut ip_addresses = AHashSet::new(); - let mut config = self.storage.config.build_config(BLOCKED_IP_KEY).await?; - - for ip in config - .set_values(BLOCKED_IP_KEY) - .map(IpAddrOrMask::parse_value) - .collect::<Vec<_>>() - { - match ip { - Ok(IpAddrOrMask::Ip(ip)) => { - ip_addresses.insert(ip); - } - Ok(IpAddrOrMask::Mask(_)) => {} - Err(err) => { - config.new_parse_error(BLOCKED_IP_KEY, err); - } - } - } - - *self.network.blocked_ips.ip_addresses.write() = ip_addresses; + let mut config = self + .core + .storage + .config + .build_config(BLOCKED_IP_KEY) + .await?; + *self.inner.data.blocked_ips.write() = BlockedIps::parse(&mut config).blocked_ip_addresses; Ok(config.into()) } pub async fn reload_certificates(&self) -> trc::Result<ReloadResult> { - let mut config = self.storage.config.build_config("certificate").await?; - let mut certificates = self.tls.certificates.load().as_ref().clone(); + let mut config = self.core.storage.config.build_config("certificate").await?; + let mut certificates = self.inner.data.tls_certificates.load().as_ref().clone(); parse_certificates(&mut config, &mut certificates, &mut Default::default()); - self.tls.certificates.store(certificates.into()); + self.inner.data.tls_certificates.store(certificates.into()); Ok(config.into()) } pub async fn reload_lookups(&self) -> trc::Result<ReloadResult> { - let mut config = self.storage.config.build_config("lookup").await?; + let mut config = self.core.storage.config.build_config("lookup").await?; let mut stores = Stores::default(); stores.parse_memory_stores(&mut config); - let mut core = self.clone(); + let mut core = self.core.as_ref().clone(); for (id, store) in stores.lookup_stores { core.storage.lookups.insert(id, store); } @@ -81,14 +68,14 @@ impl Core { } pub async fn reload(&self) -> trc::Result<ReloadResult> { - let mut config = self.storage.config.build_config("").await?; + let mut config = self.core.storage.config.build_config("").await?; // Load stores let mut stores = Stores { - stores: self.storage.stores.clone(), - blob_stores: self.storage.blobs.clone(), - fts_stores: self.storage.ftss.clone(), - lookup_stores: self.storage.lookups.clone(), + stores: self.core.storage.stores.clone(), + blob_stores: self.core.storage.blobs.clone(), + fts_stores: self.core.storage.ftss.clone(), + lookup_stores: self.core.storage.lookups.clone(), purge_schedules: Default::default(), }; stores.parse_stores(&mut config).await; @@ -103,8 +90,10 @@ impl Core { // Build manager let manager = ConfigManager { - cfg_local: ArcSwap::from_pointee(self.storage.config.cfg_local.load().as_ref().clone()), - cfg_local_path: self.storage.config.cfg_local_path.clone(), + cfg_local: ArcSwap::from_pointee( + self.core.storage.config.cfg_local.load().as_ref().clone(), + ), + cfg_local_path: self.core.storage.config.cfg_local_path.clone(), cfg_local_patterns: Patterns::parse(&mut config).into(), cfg_store: config .value("storage.data") @@ -114,26 +103,29 @@ impl Core { }; // Parse settings and build shared core - let mut core = Core::parse(&mut config, stores, manager).await; + let core = Core::parse(&mut config, stores, manager).await; if !config.errors.is_empty() { return Ok(config.into()); } - // Copy ACME certificates - let mut certificates = core.tls.certificates.load().as_ref().clone(); - for (cert_id, cert) in self.tls.certificates.load().iter() { - certificates - .entry(cert_id.to_string()) - .or_insert(cert.clone()); + // Update TLS certificates + let mut new_certificates = AHashMap::new(); + parse_certificates(&mut config, &mut new_certificates, &mut Default::default()); + let mut current_certificates = self.inner.data.tls_certificates.load().as_ref().clone(); + for (cert_id, cert) in new_certificates { + current_certificates.insert(cert_id, cert); } - core.tls.certificates.store(certificates.into()); - core.tls - .self_signed_cert - .clone_from(&self.tls.self_signed_cert); + self.inner + .data + .tls_certificates + .store(current_certificates.into()); + + // Update blocked IPs + *self.inner.data.blocked_ips.write() = BlockedIps::parse(&mut config).blocked_ip_addresses; // Parser servers - let mut servers = Servers::parse(&mut config); - servers.parse_tcp_acceptors(&mut config, core.clone().into_shared()); + let mut servers = Listeners::parse(&mut config); + servers.parse_tcp_acceptors(&mut config, self.inner.clone()); Ok(if config.errors.is_empty() { ReloadResult { diff --git a/crates/common/src/scripts/plugins/bayes.rs b/crates/common/src/scripts/plugins/bayes.rs index 5250cac9..814f5a97 100644 --- a/crates/common/src/scripts/plugins/bayes.rs +++ b/crates/common/src/scripts/plugins/bayes.rs @@ -43,8 +43,8 @@ pub async fn exec_untrain(ctx: PluginContext<'_>) -> trc::Result<Variable> { async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable> { let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()), - _ => Some(&ctx.core.storage.lookup), + Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()), + _ => Some(&ctx.server.core.storage.lookup), } .ok_or_else(|| { trc::SieveEvent::RuntimeError @@ -80,7 +80,7 @@ async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable> ); // Update weight and invalidate cache - let bayes_cache = &ctx.cache.bayes_cache; + let bayes_cache = &ctx.server.inner.data.bayes_cache; if is_train { for (hash, weights) in model.weights { store @@ -129,8 +129,8 @@ async fn train(ctx: PluginContext<'_>, is_train: bool) -> trc::Result<Variable> pub async fn exec_classify(ctx: PluginContext<'_>) -> trc::Result<Variable> { let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()), - _ => Some(&ctx.core.storage.lookup), + Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()), + _ => Some(&ctx.server.core.storage.lookup), } .ok_or_else(|| { trc::SieveEvent::RuntimeError @@ -162,7 +162,7 @@ pub async fn exec_classify(ctx: PluginContext<'_>) -> trc::Result<Variable> { } // Obtain training counts - let bayes_cache = &ctx.cache.bayes_cache; + let bayes_cache = &ctx.server.inner.data.bayes_cache; let (spam_learns, ham_learns) = bayes_cache .get_or_update(TokenHash::default(), store) .await @@ -219,8 +219,8 @@ pub async fn exec_is_balanced(ctx: PluginContext<'_>) -> trc::Result<Variable> { } let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()), - _ => Some(&ctx.core.storage.lookup), + Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()), + _ => Some(&ctx.server.core.storage.lookup), } .ok_or_else(|| { trc::SieveEvent::RuntimeError @@ -231,7 +231,7 @@ pub async fn exec_is_balanced(ctx: PluginContext<'_>) -> trc::Result<Variable> { let learn_spam = ctx.arguments[1].to_bool(); // Obtain training counts - let bayes_cache = &ctx.cache.bayes_cache; + let bayes_cache = &ctx.server.inner.data.bayes_cache; let (spam_learns, ham_learns) = bayes_cache .get_or_update(TokenHash::default(), store) .await diff --git a/crates/common/src/scripts/plugins/dns.rs b/crates/common/src/scripts/plugins/dns.rs index cdbacb17..6e145ddd 100644 --- a/crates/common/src/scripts/plugins/dns.rs +++ b/crates/common/src/scripts/plugins/dns.rs @@ -25,6 +25,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { Ok(if record_type.eq_ignore_ascii_case("ip") { match ctx + .server .core .smtp .resolvers @@ -40,7 +41,15 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { Err(err) => err.short_error().into(), } } else if record_type.eq_ignore_ascii_case("mx") { - match ctx.core.smtp.resolvers.dns.mx_lookup(entry.as_ref()).await { + match ctx + .server + .core + .smtp + .resolvers + .dns + .mx_lookup(entry.as_ref()) + .await + { Ok(result) => result .iter() .flat_map(|mx| { @@ -61,6 +70,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { } match ctx + .server .core .smtp .resolvers @@ -73,7 +83,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { } } else if record_type.eq_ignore_ascii_case("ptr") { if let Ok(addr) = entry.parse::<IpAddr>() { - match ctx.core.smtp.resolvers.dns.ptr_lookup(addr).await { + match ctx.server.core.smtp.resolvers.dns.ptr_lookup(addr).await { Ok(result) => result .iter() .map(|host| Variable::from(host.to_string())) @@ -94,6 +104,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { } match ctx + .server .core .smtp .resolvers @@ -110,6 +121,7 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { } } else if record_type.eq_ignore_ascii_case("ipv6") { match ctx + .server .core .smtp .resolvers @@ -135,6 +147,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> { Ok(if record_type.eq_ignore_ascii_case("ip") { match ctx + .server .core .smtp .resolvers @@ -147,14 +160,22 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> { Err(_) => -1, } } else if record_type.eq_ignore_ascii_case("mx") { - match ctx.core.smtp.resolvers.dns.mx_lookup(entry.as_ref()).await { + match ctx + .server + .core + .smtp + .resolvers + .dns + .mx_lookup(entry.as_ref()) + .await + { Ok(result) => i64::from(result.iter().any(|mx| !mx.exchanges.is_empty())), Err(Error::DnsRecordNotFound(_)) => 0, Err(_) => -1, } } else if record_type.eq_ignore_ascii_case("ptr") { if let Ok(addr) = entry.parse::<IpAddr>() { - match ctx.core.smtp.resolvers.dns.ptr_lookup(addr).await { + match ctx.server.core.smtp.resolvers.dns.ptr_lookup(addr).await { Ok(result) => i64::from(!result.is_empty()), Err(Error::DnsRecordNotFound(_)) => 0, Err(_) => -1, @@ -171,6 +192,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> { } match ctx + .server .core .smtp .resolvers @@ -184,6 +206,7 @@ pub async fn exec_exists(ctx: PluginContext<'_>) -> trc::Result<Variable> { } } else if record_type.eq_ignore_ascii_case("ipv6") { match ctx + .server .core .smtp .resolvers diff --git a/crates/common/src/scripts/plugins/lookup.rs b/crates/common/src/scripts/plugins/lookup.rs index f19721c5..1d82ba93 100644 --- a/crates/common/src/scripts/plugins/lookup.rs +++ b/crates/common/src/scripts/plugins/lookup.rs @@ -42,8 +42,8 @@ pub fn register_local_domain(plugin_id: u32, fnc_map: &mut FunctionMap) { pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()), - _ => Some(&ctx.core.storage.lookup), + Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()), + _ => Some(&ctx.server.core.storage.lookup), } .ok_or_else(|| { trc::SieveEvent::RuntimeError @@ -76,8 +76,8 @@ pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { pub async fn exec_get(ctx: PluginContext<'_>) -> trc::Result<Variable> { match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()), - _ => Some(&ctx.core.storage.lookup), + Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()), + _ => Some(&ctx.server.core.storage.lookup), } .ok_or_else(|| { trc::SieveEvent::RuntimeError @@ -97,8 +97,8 @@ pub async fn exec_set(ctx: PluginContext<'_>) -> trc::Result<Variable> { }; match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()), - _ => Some(&ctx.core.storage.lookup), + Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()), + _ => Some(&ctx.server.core.storage.lookup), } .ok_or_else(|| { trc::SieveEvent::RuntimeError @@ -125,7 +125,7 @@ pub async fn exec_remote(ctx: PluginContext<'_>) -> trc::Result<Variable> { // Something went wrong, try again in one hour const RETRY: Duration = Duration::from_secs(3600); - let mut _lock = ctx.cache.remote_lists.write(); + let mut _lock = ctx.server.inner.data.remote_lists.write(); let list = _lock .entry(ctx.arguments[0].to_string().to_string()) .or_insert_with(|| RemoteList { @@ -169,7 +169,14 @@ async fn exec_remote_(ctx: &PluginContext<'_>) -> trc::Result<Variable> { const MAX_ENTRY_SIZE: usize = 256; const MAX_ENTRIES: usize = 100000; - match ctx.cache.remote_lists.read().get(resource.as_ref()) { + match ctx + .server + .inner + .data + .remote_lists + .read() + .get(resource.as_ref()) + { Some(remote_list) if remote_list.expires < Instant::now() => { return Ok(remote_list.entries.contains(item.as_ref()).into()) } @@ -256,7 +263,7 @@ async fn exec_remote_(ctx: &PluginContext<'_>) -> trc::Result<Variable> { }; // Lock remote list for writing - let mut _lock = ctx.cache.remote_lists.write(); + let mut _lock = ctx.server.inner.data.remote_lists.write(); let list = _lock .entry(resource.to_string()) .or_insert_with(|| RemoteList { @@ -352,8 +359,10 @@ pub async fn exec_local_domain(ctx: PluginContext<'_>) -> trc::Result<Variable> if !domain.is_empty() { return match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.storage.directories.get(v.as_ref()), - _ => Some(&ctx.core.storage.directory), + Variable::String(v) if !v.is_empty() => { + ctx.server.core.storage.directories.get(v.as_ref()) + } + _ => Some(&ctx.server.core.storage.directory), } .ok_or_else(|| { trc::SieveEvent::RuntimeError diff --git a/crates/common/src/scripts/plugins/mod.rs b/crates/common/src/scripts/plugins/mod.rs index f2802138..ba681eee 100644 --- a/crates/common/src/scripts/plugins/mod.rs +++ b/crates/common/src/scripts/plugins/mod.rs @@ -17,7 +17,7 @@ pub mod text; use mail_parser::Message; use sieve::{runtime::Variable, FunctionMap, Input}; -use crate::{config::scripts::ScriptCache, Core}; +use crate::{Core, Server}; use super::ScriptModification; @@ -25,8 +25,7 @@ type RegisterPluginFnc = fn(u32, &mut FunctionMap) -> (); pub struct PluginContext<'x> { pub session_id: u64, - pub core: &'x Core, - pub cache: &'x ScriptCache, + pub server: &'x Server, pub message: &'x Message<'x>, pub modifications: &'x mut Vec<ScriptModification>, pub arguments: Vec<Variable>, diff --git a/crates/common/src/scripts/plugins/query.rs b/crates/common/src/scripts/plugins/query.rs index 356808ae..15092caf 100644 --- a/crates/common/src/scripts/plugins/query.rs +++ b/crates/common/src/scripts/plugins/query.rs @@ -19,8 +19,8 @@ pub fn register(plugin_id: u32, fnc_map: &mut FunctionMap) { pub async fn exec(ctx: PluginContext<'_>) -> trc::Result<Variable> { // Obtain store name let store = match &ctx.arguments[0] { - Variable::String(v) if !v.is_empty() => ctx.core.storage.lookups.get(v.as_ref()), - _ => Some(&ctx.core.storage.lookup), + Variable::String(v) if !v.is_empty() => ctx.server.core.storage.lookups.get(v.as_ref()), + _ => Some(&ctx.server.core.storage.lookup), } .ok_or_else(|| { trc::SieveEvent::RuntimeError diff --git a/crates/common/src/telemetry/metrics/otel.rs b/crates/common/src/telemetry/metrics/otel.rs index a4e10c9b..7ce117f8 100644 --- a/crates/common/src/telemetry/metrics/otel.rs +++ b/crates/common/src/telemetry/metrics/otel.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{sync::Arc, time::SystemTime}; +use std::time::SystemTime; use opentelemetry::global::set_error_handler; use opentelemetry_sdk::metrics::data::{ @@ -13,19 +13,13 @@ use opentelemetry_sdk::metrics::data::{ }; use trc::{Collector, TelemetryEvent}; -use crate::{config::telemetry::OtelMetrics, Core}; +use crate::config::telemetry::OtelMetrics; impl OtelMetrics { - pub async fn push_metrics(&self, core: Arc<Core>, start_time: SystemTime) { + pub async fn push_metrics(&self, is_enterprise: bool, start_time: SystemTime) { let mut metrics = Vec::with_capacity(256); let now = SystemTime::now(); - #[cfg(feature = "enterprise")] - let is_enterprise = core.is_enterprise_edition(); - - #[cfg(not(feature = "enterprise"))] - let is_enterprise = false; - // Add counters for counter in Collector::collect_counters(is_enterprise) { metrics.push(Metric { diff --git a/crates/common/src/telemetry/metrics/prometheus.rs b/crates/common/src/telemetry/metrics/prometheus.rs index 35ff3c01..46bcf9c7 100644 --- a/crates/common/src/telemetry/metrics/prometheus.rs +++ b/crates/common/src/telemetry/metrics/prometheus.rs @@ -10,9 +10,9 @@ use prometheus::{ }; use trc::{atomics::histogram::AtomicHistogram, Collector}; -use crate::Core; +use crate::Server; -impl Core { +impl Server { pub async fn export_prometheus_metrics(&self) -> trc::Result<String> { let mut metrics = Vec::new(); diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs index ee676080..9806d184 100644 --- a/crates/directory/src/backend/internal/mod.rs +++ b/crates/directory/src/backend/internal/mod.rs @@ -274,22 +274,26 @@ impl MigrateDirectory for Store { }, ), |key, value| { - if key[0] == 2 && value[0] == 1 { - principals.push(( - key.get(1..) - .and_then(|b| b.read_leb128::<u32>().map(|(v, _)| v)) - .ok_or_else(|| { - trc::StoreEvent::DataCorruption - .caused_by(trc::location!()) - .ctx(trc::Key::Value, key) - })?, - Principal::deserialize(value)?, - )); - } else if key[0] == 3 { - let domain = std::str::from_utf8(&key[1..]).unwrap_or_default(); - if !domain.is_empty() { - domains.push(domain.to_string()); + match (key.first(), value.first()) { + (Some(2), Some(1)) => { + principals.push(( + key.get(1..) + .and_then(|b| b.read_leb128::<u32>().map(|(v, _)| v)) + .ok_or_else(|| { + trc::StoreEvent::DataCorruption + .caused_by(trc::location!()) + .ctx(trc::Key::Value, key) + })?, + Principal::deserialize(value)?, + )); } + (Some(3), _) => { + let domain = std::str::from_utf8(&key[1..]).unwrap_or_default(); + if !domain.is_empty() { + domains.push(domain.to_string()); + } + } + _ => {} } Ok(true) diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index 1952da3c..a6bea4e1 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -6,12 +6,14 @@ use std::{iter::Peekable, sync::Arc, vec::IntoIter}; -use common::listener::{limiter::ConcurrencyLimiter, SessionResult, SessionStream}; +use common::{ + listener::{limiter::ConcurrencyLimiter, SessionResult, SessionStream}, + ConcurrencyLimiters, +}; use imap_proto::{ receiver::{self, Request}, Command, ResponseType, StatusResponse, }; -use jmap::auth::rate_limit::ConcurrencyLimiters; use super::{SelectedMailbox, Session, SessionData, State}; @@ -255,9 +257,9 @@ impl<T: SessionStream> Session<T> { let state = &self.state; // Rate limit request if let State::Authenticated { data } | State::Selected { data, .. } = state { - if let Some(rate) = &self.jmap.core.imap.rate_requests { + if let Some(rate) = &self.server.core.imap.rate_requests { if data - .jmap + .server .core .storage .lookup @@ -301,7 +303,7 @@ impl<T: SessionStream> Session<T> { } Command::Login => { if let State::NotAuthenticated { .. } = state { - if self.is_tls || self.jmap.core.imap.allow_plain_auth { + if self.is_tls || self.server.core.imap.allow_plain_auth { Ok(request) } else { Err(trc::ImapEvent::Error @@ -385,9 +387,11 @@ impl<T: SessionStream> Session<T> { } pub fn get_concurrency_limiter(&self, account_id: u32) -> Option<Arc<ConcurrencyLimiters>> { - let rate = self.jmap.core.imap.rate_concurrent?; - self.imap - .rate_limiter + let rate = self.server.core.imap.rate_concurrent?; + self.server + .inner + .data + .imap_limiter .get(&account_id) .map(|limiter| limiter.clone()) .unwrap_or_else(|| { @@ -395,7 +399,11 @@ impl<T: SessionStream> Session<T> { concurrent_requests: ConcurrencyLimiter::new(rate), concurrent_uploads: ConcurrencyLimiter::new(rate), }); - self.imap.rate_limiter.insert(account_id, limiter.clone()); + self.server + .inner + .data + .imap_limiter + .insert(account_id, limiter.clone()); limiter }) .into() diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index 4a780ee6..5f872df6 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -8,10 +8,16 @@ use common::{ auth::AccessToken, config::jmap::settings::SpecialUse, listener::{limiter::InFlight, SessionStream}, + AccountId, Mailbox, }; use directory::{backend::internal::PrincipalField, QueryBy}; use imap_proto::protocol::list::Attribute; -use jmap::{auth::acl::EffectiveAcl, mailbox::INBOX_ID}; +use jmap::{ + auth::acl::{AclMethods, EffectiveAcl}, + changes::get::ChangesLookup, + mailbox::{get::MailboxGet, set::MailboxSet, INBOX_ID}, + JmapMethods, +}; use jmap_proto::{ object::Object, types::{acl::Acl, collection::Collection, id::Id, property::Property, value::Value}, @@ -21,7 +27,7 @@ use store::query::log::{Change, Query}; use trc::AddContext; use utils::lru_cache::LruCached; -use super::{Account, AccountId, Mailbox, MailboxId, MailboxSync, Session, SessionData}; +use super::{Account, MailboxId, MailboxSync, Session, SessionData}; impl<T: SessionStream> SessionData<T> { pub async fn new( @@ -31,8 +37,7 @@ impl<T: SessionStream> SessionData<T> { ) -> trc::Result<Self> { let mut session = SessionData { stream_tx: session.stream_tx.clone(), - jmap: session.jmap.clone(), - imap: session.imap.clone(), + server: session.server.clone(), account_id: access_token.primary_id(), session_id: session.session_id, mailboxes: Mutex::new(vec![]), @@ -56,9 +61,9 @@ impl<T: SessionStream> SessionData<T> { account_id, format!( "{}/{}", - session.jmap.core.jmap.shared_folder, + session.server.core.jmap.shared_folder, session - .jmap + .server .core .storage .directory @@ -88,7 +93,7 @@ impl<T: SessionStream> SessionData<T> { access_token: &AccessToken, ) -> trc::Result<Account> { let state_mailbox = self - .jmap + .server .core .storage .data @@ -96,7 +101,7 @@ impl<T: SessionStream> SessionData<T> { .await .caused_by(trc::location!())?; let state_email = self - .jmap + .server .core .storage .data @@ -107,19 +112,21 @@ impl<T: SessionStream> SessionData<T> { account_id, primary_id: access_token.primary_id(), }; - if let Some(cached_account) = - self.imap - .cache_account - .get(&cached_account_id) - .and_then(|cached_account| { - if cached_account.state_mailbox == state_mailbox - && cached_account.state_email == state_email - { - Some(cached_account) - } else { - None - } - }) + if let Some(cached_account) = self + .server + .inner + .data + .account_cache + .get(&cached_account_id) + .and_then(|cached_account| { + if cached_account.state_mailbox == state_mailbox + && cached_account.state_email == state_email + { + Some(cached_account) + } else { + None + } + }) { return Ok(cached_account.as_ref().clone()); } @@ -127,12 +134,12 @@ impl<T: SessionStream> SessionData<T> { let mailbox_ids = if access_token.is_primary_id(account_id) || access_token.member_of.contains(&account_id) { - self.jmap + self.server .mailbox_get_or_create(account_id) .await .caused_by(trc::location!())? } else { - self.jmap + self.server .shared_documents(access_token, account_id, Collection::Mailbox, Acl::Read) .await .caused_by(trc::location!())? @@ -142,7 +149,7 @@ impl<T: SessionStream> SessionData<T> { let mut mailboxes = Vec::with_capacity(10); let mut special_uses = AHashMap::new(); for (mailbox_id, values) in self - .jmap + .server .get_properties::<Object<Value>, _, _>( account_id, Collection::Mailbox, @@ -189,7 +196,7 @@ impl<T: SessionStream> SessionData<T> { let mut path = Vec::new(); let mut iter_stack = Vec::new(); let message_ids = self - .jmap + .server .get_document_ids(account_id, Collection::Email) .await .caused_by(trc::location!())?; @@ -246,7 +253,7 @@ impl<T: SessionStream> SessionData<T> { }, ), total_messages: self - .jmap + .server .get_tag( account_id, Collection::Email, @@ -259,7 +266,7 @@ impl<T: SessionStream> SessionData<T> { .unwrap_or(0) .into(), total_unseen: self - .jmap + .server .mailbox_unread_tags(account_id, *mailbox_id, &message_ids) .await .caused_by(trc::location!())? @@ -278,7 +285,7 @@ impl<T: SessionStream> SessionData<T> { // Map special use folder aliases to their internal ids let effective_mailbox_id = self - .jmap + .server .core .jmap .default_folders @@ -313,8 +320,10 @@ impl<T: SessionStream> SessionData<T> { } // Update cache - self.imap - .cache_account + self.server + .inner + .data + .account_cache .insert(cached_account_id, Arc::new(account.clone())); Ok(account) @@ -332,8 +341,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain access token let access_token = self - .jmap - .core + .server .get_cached_access_token(self.account_id) .await .caused_by(trc::location!())?; @@ -383,8 +391,8 @@ impl<T: SessionStream> SessionData<T> { for account_id in added_account_ids { let prefix = format!( "{}/{}", - self.jmap.core.jmap.shared_folder, - self.jmap + self.server.core.jmap.shared_folder, + self.server .core .storage .directory @@ -414,7 +422,7 @@ impl<T: SessionStream> SessionData<T> { .collect::<Vec<_>>(); for (account_id, last_state) in account_states { let changelog = self - .jmap + .server .changes_( account_id, Collection::Mailbox, @@ -437,7 +445,7 @@ impl<T: SessionStream> SessionData<T> { if has_child_changes && !has_changes && changes.is_none() { // Only child changes, no need to re-fetch mailboxes let state_email = self - .jmap + .server .core .storage .data @@ -461,8 +469,13 @@ impl<T: SessionStream> SessionData<T> { } // Update cache - if let Some(cached_account_) = - self.imap.cache_account.lock().get_mut(&AccountId { + if let Some(cached_account_) = self + .server + .inner + .data + .account_cache + .lock() + .get_mut(&AccountId { account_id, primary_id: access_token.primary_id(), }) @@ -488,8 +501,8 @@ impl<T: SessionStream> SessionData<T> { let mailbox_prefix = if !access_token.is_primary_id(account_id) { format!( "{}/{}", - self.jmap.core.jmap.shared_folder, - self.jmap + self.server.core.jmap.shared_folder, + self.server .core .storage .directory @@ -613,7 +626,7 @@ impl<T: SessionStream> SessionData<T> { let access_token = self.get_access_token().await?; Ok(access_token.is_member(account_id) || self - .jmap + .server .get_property::<Object<Value>>( account_id, Collection::Mailbox, diff --git a/crates/imap/src/core/message.rs b/crates/imap/src/core/message.rs index 2a3df644..0706eb89 100644 --- a/crates/imap/src/core/message.rs +++ b/crates/imap/src/core/message.rs @@ -7,9 +7,9 @@ use std::{collections::BTreeMap, sync::Arc}; use ahash::AHashMap; -use common::listener::SessionStream; +use common::{listener::SessionStream, NextMailboxState}; use imap_proto::protocol::{expunge, select::Exists, Sequence}; -use jmap::mailbox::UidMailbox; +use jmap::{mailbox::UidMailbox, JmapMethods}; use jmap_proto::{ object::Object, types::{collection::Collection, property::Property, value::Value}, @@ -20,7 +20,7 @@ use utils::lru_cache::LruCached; use crate::core::ImapId; -use super::{ImapUidToId, MailboxId, MailboxState, NextMailboxState, SelectedMailbox, SessionData}; +use super::{ImapUidToId, MailboxId, MailboxState, SelectedMailbox, SessionData}; pub(crate) const MAX_RETRIES: usize = 10; @@ -28,7 +28,7 @@ impl<T: SessionStream> SessionData<T> { pub async fn fetch_messages(&self, mailbox: &MailboxId) -> trc::Result<MailboxState> { // Obtain message ids let message_ids = self - .jmap + .server .get_tag( mailbox.account_id, Collection::Email, @@ -43,7 +43,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain current state let modseq = self - .jmap + .server .core .storage .data @@ -54,7 +54,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain all message ids let mut uid_map = BTreeMap::new(); for (message_id, uid_mailbox) in self - .jmap + .server .get_properties::<HashedValue<Vec<UidMailbox>>, _, _>( mailbox.account_id, Collection::Email, @@ -148,8 +148,10 @@ impl<T: SessionStream> SessionData<T> { current_state.id_to_imap = id_to_imap; // Update cache - self.imap - .cache_mailbox + self.server + .inner + .data + .mailbox_cache .insert(mailbox.id, Arc::new(new_state.clone())); // Update state @@ -207,7 +209,7 @@ impl<T: SessionStream> SessionData<T> { pub async fn get_modseq(&self, account_id: u32) -> trc::Result<Option<u64>> { // Obtain current modseq - self.jmap + self.server .core .storage .data @@ -221,7 +223,7 @@ impl<T: SessionStream> SessionData<T> { } pub async fn get_uid_validity(&self, mailbox: &MailboxId) -> trc::Result<u32> { - self.jmap + self.server .get_property::<Object<Value>>( mailbox.account_id, Collection::Mailbox, diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs index b8175d67..db92118c 100644 --- a/crates/imap/src/core/mod.rs +++ b/crates/imap/src/core/mod.rs @@ -5,29 +5,21 @@ */ use std::{ - collections::BTreeMap, net::IpAddr, sync::{atomic::AtomicU32, Arc}, }; -use ahash::AHashMap; use common::{ auth::AccessToken, listener::{limiter::InFlight, ServerInstance, SessionStream}, + Account, ImapId, Inner, MailboxId, MailboxState, Server, }; -use dashmap::DashMap; -use imap_proto::{ - protocol::{list::Attribute, ProtocolVersion}, - receiver::Receiver, - Command, -}; -use jmap::{auth::rate_limit::ConcurrencyLimiters, JmapInstance, JMAP}; +use imap_proto::{protocol::ProtocolVersion, receiver::Receiver, Command}; use tokio::{ io::{ReadHalf, WriteHalf}, sync::watch, }; use trc::AddContext; -use utils::lru_cache::LruCache; pub mod client; pub mod mailbox; @@ -36,32 +28,17 @@ pub mod session; #[derive(Clone)] pub struct ImapSessionManager { - pub imap: ImapInstance, + pub inner: Arc<Inner>, } impl ImapSessionManager { - pub fn new(imap: ImapInstance) -> Self { - Self { imap } + pub fn new(inner: Arc<Inner>) -> Self { + Self { inner } } } -#[derive(Clone)] -pub struct ImapInstance { - pub jmap_instance: JmapInstance, - pub imap_inner: Arc<Inner>, -} - -pub struct Inner { - pub rate_limiter: DashMap<u32, Arc<ConcurrencyLimiters>>, - pub cache_account: LruCache<AccountId, Arc<Account>>, - pub cache_mailbox: LruCache<MailboxId, Arc<MailboxState>>, -} - -pub struct IMAP {} - pub struct Session<T: SessionStream> { - pub jmap: JMAP, - pub imap: Arc<Inner>, + pub server: Server, pub instance: Arc<ServerInstance>, pub receiver: Receiver<Command>, pub version: ProtocolVersion, @@ -79,8 +56,7 @@ pub struct Session<T: SessionStream> { pub struct SessionData<T: SessionStream> { pub account_id: u32, pub access_token: Arc<AccessToken>, - pub jmap: JMAP, - pub imap: Arc<Inner>, + pub server: Server, pub session_id: u64, pub mailboxes: parking_lot::Mutex<Vec<Account>>, pub stream_tx: Arc<tokio::sync::Mutex<WriteHalf<T>>>, @@ -88,29 +64,6 @@ pub struct SessionData<T: SessionStream> { pub in_flight: Option<InFlight>, } -#[derive(Debug, Default, Clone)] -pub struct Mailbox { - pub has_children: bool, - pub is_subscribed: bool, - pub special_use: Option<Attribute>, - pub total_messages: Option<u32>, - pub total_unseen: Option<u32>, - pub total_deleted: Option<u32>, - pub uid_validity: Option<u32>, - pub uid_next: Option<u32>, - pub size: Option<u32>, -} - -#[derive(Debug, Clone, Default)] -pub struct Account { - pub account_id: u32, - pub prefix: Option<String>, - pub mailbox_names: BTreeMap<String, u32>, - pub mailbox_state: AHashMap<u32, Mailbox>, - pub state_email: Option<u64>, - pub state_mailbox: Option<u64>, -} - pub struct SelectedMailbox { pub id: MailboxId, pub state: parking_lot::Mutex<MailboxState>, @@ -119,36 +72,6 @@ pub struct SelectedMailbox { pub is_condstore: bool, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub struct MailboxId { - pub account_id: u32, - pub mailbox_id: u32, -} - -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub struct AccountId { - pub account_id: u32, - pub primary_id: u32, -} - -#[derive(Debug, Clone, Default)] -pub struct MailboxState { - pub uid_next: u32, - pub uid_validity: u32, - pub uid_max: u32, - pub id_to_imap: AHashMap<u32, ImapId>, - pub uid_to_id: AHashMap<u32, u32>, - pub total_messages: usize, - pub modseq: Option<u64>, - pub next_state: Option<Box<NextMailboxState>>, -} - -#[derive(Debug, Clone)] -pub struct NextMailboxState { - pub next_state: MailboxState, - pub deletions: Vec<ImapId>, -} - #[derive(Debug, Default)] pub struct MailboxSync { pub added: Vec<String>, @@ -167,12 +90,6 @@ pub enum SavedSearch { } #[derive(Debug, Clone, Copy, Default)] -pub struct ImapId { - pub uid: u32, - pub seqnum: u32, -} - -#[derive(Debug, Clone, Copy, Default)] pub struct ImapUidToId { pub uid: u32, pub id: u32, @@ -217,8 +134,7 @@ impl<T: SessionStream> State<T> { impl<T: SessionStream> SessionData<T> { pub async fn get_access_token(&self) -> trc::Result<Arc<AccessToken>> { - self.jmap - .core + self.server .get_cached_access_token(self.account_id) .await .caused_by(trc::location!()) @@ -230,8 +146,7 @@ impl<T: SessionStream> SessionData<T> { ) -> SessionData<U> { SessionData { account_id: self.account_id, - jmap: self.jmap, - imap: self.imap, + server: self.server, session_id: self.session_id, mailboxes: self.mailboxes, stream_tx: new_stream, diff --git a/crates/imap/src/core/session.rs b/crates/imap/src/core/session.rs index 8284ed67..9f2bc75e 100644 --- a/crates/imap/src/core/session.rs +++ b/crates/imap/src/core/session.rs @@ -6,12 +6,14 @@ use std::sync::Arc; -use common::listener::{stream::NullIo, SessionData, SessionManager, SessionResult, SessionStream}; +use common::{ + core::BuildServer, + listener::{stream::NullIo, SessionData, SessionManager, SessionResult, SessionStream}, +}; use imap_proto::{ protocol::{ProtocolVersion, SerializeResponse}, receiver::Receiver, }; -use jmap::JMAP; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio_rustls::server::TlsStream; @@ -51,9 +53,9 @@ impl<T: SessionStream> Session<T> { tokio::select! { result = tokio::time::timeout( if !matches!(self.state, State::NotAuthenticated {..}) { - self.jmap.core.imap.timeout_auth + self.server.core.imap.timeout_auth } else { - self.jmap.core.imap.timeout_unauth + self.server.core.imap.timeout_unauth }, self.stream_rx.read(&mut buf)) => { match result { @@ -138,17 +140,16 @@ impl<T: SessionStream> Session<T> { // Split stream into read and write halves let (stream_rx, stream_tx) = tokio::io::split(session.stream); - let jmap = JMAP::from(manager.imap.jmap_instance); + let server = manager.inner.build_server(); Ok(Session { - receiver: Receiver::with_max_request_size(jmap.core.imap.max_request_size), + receiver: Receiver::with_max_request_size(server.core.imap.max_request_size), version: ProtocolVersion::Rev1, state: State::NotAuthenticated { auth_failures: 0 }, is_tls, is_condstore: false, is_qresync: false, - jmap, - imap: manager.imap.imap_inner, + server, instance: session.instance, session_id: session.session_id, in_flight: session.in_flight, @@ -196,8 +197,7 @@ impl<T: SessionStream> Session<T> { let stream_tx = Arc::new(tokio::sync::Mutex::new(stream_tx)); Ok(Session { - jmap: self.jmap, - imap: self.imap, + server: self.server, instance: self.instance, receiver: self.receiver, version: self.version, diff --git a/crates/imap/src/lib.rs b/crates/imap/src/lib.rs index e4a0eccc..f6df61e0 100644 --- a/crates/imap/src/lib.rs +++ b/crates/imap/src/lib.rs @@ -4,19 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use core::{ImapInstance, Inner, IMAP}; -use std::{ - collections::hash_map::RandomState, - sync::{Arc, LazyLock}, -}; +use std::sync::LazyLock; -use dashmap::DashMap; use imap_proto::{protocol::capability::Capability, ResponseCode, StatusResponse}; -use jmap::JmapInstance; -use utils::{ - config::Config, - lru_cache::{LruCache, LruCached}, -}; pub mod core; pub mod op; @@ -39,33 +29,4 @@ pub(crate) static GREETING_WITHOUT_TLS: LazyLock<Vec<u8>> = LazyLock::new(|| { .into_bytes() }); -impl IMAP { - pub async fn init(config: &mut Config, jmap_instance: JmapInstance) -> ImapInstance { - let shard_amount = config - .property::<u64>("cache.shard") - .unwrap_or(32) - .next_power_of_two() as usize; - let capacity = config.property("cache.capacity").unwrap_or(100); - - let inner = Inner { - rate_limiter: DashMap::with_capacity_and_hasher_and_shard_amount( - capacity, - RandomState::default(), - shard_amount, - ), - cache_account: LruCache::with_capacity( - config.property("cache.account.size").unwrap_or(2048), - ), - cache_mailbox: LruCache::with_capacity( - config.property("cache.mailbox.size").unwrap_or(2048), - ), - }; - - ImapInstance { - jmap_instance, - imap_inner: Arc::new(inner), - } - } -} - pub struct ImapError; diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index b2bc1585..f52026d3 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -6,7 +6,7 @@ use std::{sync::Arc, time::Instant}; -use common::{auth::AccessToken, listener::SessionStream}; +use common::{auth::AccessToken, listener::SessionStream, MailboxId}; use directory::{backend::internal::PrincipalField, Permission, QueryBy}; use imap_proto::{ protocol::acl::{ @@ -16,7 +16,10 @@ use imap_proto::{ Command, ResponseCode, StatusResponse, }; -use jmap::{auth::acl::EffectiveAcl, mailbox::set::SCHEMA}; +use jmap::{ + auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA, + services::state::StateManager, JmapMethods, +}; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{ @@ -33,7 +36,7 @@ use trc::AddContext; use utils::map::bitmap::Bitmap; use crate::{ - core::{MailboxId, Session, SessionData, State}, + core::{Session, SessionData, State}, op::ImapContext, spawn_op, }; @@ -62,7 +65,7 @@ impl<T: SessionStream> Session<T> { { for item in acls { if let Some(account_name) = data - .jmap + .server .core .storage .directory @@ -244,7 +247,7 @@ impl<T: SessionStream> Session<T> { // Obtain principal id let acl_account_id = data - .jmap + .server .core .storage .directory @@ -345,18 +348,18 @@ impl<T: SessionStream> Session<T> { .with_current(values), ); if !batch.is_empty() { - data.jmap + data.server .write_batch(batch) .await .imap_ctx(&arguments.tag, trc::location!())?; let mut changes = ChangeLogBuilder::new(); changes.log_update(Collection::Mailbox, mailbox_id); let change_id = data - .jmap + .server .commit_changes(mailbox.account_id, changes) .await .imap_ctx(&arguments.tag, trc::location!())?; - data.jmap + data.server .broadcast_state_change( StateChange::new(mailbox.account_id) .with_change(DataType::Mailbox, change_id), @@ -365,11 +368,7 @@ impl<T: SessionStream> Session<T> { } // Invalidate ACLs - data.jmap - .core - .security - .access_tokens - .remove(&acl_account_id); + data.server.inner.data.access_tokens.remove(&acl_account_id); trc::event!( Imap(trc::ImapEvent::SetAcl), @@ -447,7 +446,7 @@ impl<T: SessionStream> SessionData<T> { ) -> trc::Result<(MailboxId, HashedValue<Object<Value>>, Arc<AccessToken>)> { if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) { if let Some(values) = self - .jmap + .server .get_property::<HashedValue<Object<Value>>>( mailbox.account_id, Collection::Mailbox, diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index 02308ef9..284b5c7d 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -14,11 +14,14 @@ use imap_proto::{ }; use crate::{ - core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData}, + core::{ImapUidToId, SelectedMailbox, Session, SessionData}, spawn_op, }; -use common::listener::SessionStream; -use jmap::email::ingest::{IngestEmail, IngestSource}; +use common::{listener::SessionStream, MailboxId}; +use jmap::{ + email::ingest::{EmailIngest, IngestEmail, IngestSource}, + services::state::StateManager, +}; use jmap_proto::types::{acl::Acl, keyword::Keyword, state::StateChange, type_state::DataType}; use mail_parser::MessageParser; @@ -89,8 +92,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain quota let resource_token = self - .jmap - .core + .server .get_cached_access_token(mailbox.account_id) .await .imap_ctx(&arguments.tag, trc::location!())? @@ -102,7 +104,7 @@ impl<T: SessionStream> SessionData<T> { let mut last_change_id = None; for message in arguments.messages { match self - .jmap + .server .email_ingest(IngestEmail { raw_message: &message.message, message: MessageParser::new().parse(&message.message), @@ -111,7 +113,7 @@ impl<T: SessionStream> SessionData<T> { keywords: message.flags.into_iter().map(Keyword::from).collect(), received_at: message.received_at.map(|d| d as u64), source: IngestSource::Imap, - encrypt: self.jmap.core.jmap.encrypt && self.jmap.core.jmap.encrypt_append, + encrypt: self.server.core.jmap.encrypt && self.server.core.jmap.encrypt_append, session_id: self.session_id, }) .await @@ -142,7 +144,7 @@ impl<T: SessionStream> SessionData<T> { // Broadcast changes if let Some(change_id) = last_change_id { - self.jmap + self.server .broadcast_state_change( StateChange::new(account_id) .with_change(DataType::Email, change_id) diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index 5724c6af..83327535 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -11,6 +11,9 @@ use imap_proto::{ receiver::{self, Request}, Command, ResponseCode, StatusResponse, }; +use jmap::auth::{ + authenticate::Authenticator, oauth::token::TokenHandler, rate_limit::RateLimiter, +}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use std::sync::Arc; @@ -70,7 +73,7 @@ impl<T: SessionStream> Session<T> { tag: String, ) -> trc::Result<()> { // Throttle authentication requests - self.jmap + self.server .is_auth_allowed_soft(&self.remote_addr) .await .map_err(|err| err.id(tag.clone()))?; @@ -78,17 +81,17 @@ impl<T: SessionStream> Session<T> { // Authenticate let access_token = match credentials { Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => { - self.jmap + self.server .authenticate_plain(&username, &secret, self.remote_addr, self.session_id) .await } Credentials::OAuthBearer { token } => { match self - .jmap + .server .validate_access_token("access_token", &token) .await { - Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await, + Ok((account_id, _, _)) => self.server.get_access_token(account_id).await, Err(err) => Err(err), } } @@ -96,7 +99,7 @@ impl<T: SessionStream> Session<T> { .map_err(|err| { if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) { let auth_failures = self.state.auth_failures(); - if auth_failures < self.jmap.core.imap.max_auth_failures { + if auth_failures < self.server.core.imap.max_auth_failures { self.state = State::NotAuthenticated { auth_failures: auth_failures + 1, }; @@ -127,7 +130,7 @@ impl<T: SessionStream> Session<T> { // Cache access token let access_token = Arc::new(access_token); - self.jmap.core.cache_access_token(access_token.clone()); + self.server.cache_access_token(access_token.clone()); // Create session self.state = State::Authenticated { diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs index a8f3d7f5..deebbb76 100644 --- a/crates/imap/src/op/capability.rs +++ b/crates/imap/src/op/capability.rs @@ -28,7 +28,7 @@ impl<T: SessionStream> Session<T> { Imap(trc::ImapEvent::Capabilities), SpanId = self.session_id, Tls = self.is_tls, - Strict = !self.jmap.core.imap.allow_plain_auth, + Strict = !self.server.core.imap.allow_plain_auth, Elapsed = op_start.elapsed() ); diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs index 43fd6795..697748fa 100644 --- a/crates/imap/src/op/copy_move.rs +++ b/crates/imap/src/op/copy_move.rs @@ -13,11 +13,17 @@ use imap_proto::{ }; use crate::{ - core::{MailboxId, SelectedMailbox, Session, SessionData}, + core::{SelectedMailbox, Session, SessionData}, spawn_op, }; -use common::listener::SessionStream; -use jmap::{email::set::TagManager, mailbox::UidMailbox}; +use common::{listener::SessionStream, MailboxId}; +use jmap::{ + changes::write::ChangeLog, + email::{copy::EmailCopy, ingest::EmailIngest, set::TagManager}, + mailbox::UidMailbox, + services::state::StateManager, + JmapMethods, +}; use jmap_proto::{ error::set::SetErrorType, types::{ @@ -200,7 +206,7 @@ impl<T: SessionStream> SessionData<T> { for uid_mailbox in mailboxes.inner_tags_mut() { if uid_mailbox.uid == 0 { let assigned_uid = self - .jmap + .server .assign_imap_uid(account_id, uid_mailbox.mailbox_id) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -219,13 +225,13 @@ impl<T: SessionStream> SessionData<T> { mailboxes.update_batch(&mut batch, Property::MailboxIds); if changelog.change_id == u64::MAX { changelog.change_id = self - .jmap + .server .assign_change_id(account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; } batch.value(Property::Cid, changelog.change_id, F_VALUE); - self.jmap + self.server .write_batch(batch) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -242,8 +248,7 @@ impl<T: SessionStream> SessionData<T> { let mut dest_change_id = None; let dest_account_id = dest_mailbox.account_id; let resource_token = self - .jmap - .core + .server .get_cached_access_token(dest_account_id) .await .imap_ctx(&arguments.tag, trc::location!())? @@ -251,7 +256,7 @@ impl<T: SessionStream> SessionData<T> { let mut destroy_ids = RoaringBitmap::new(); for (id, imap_id) in ids { match self - .jmap + .server .copy_message( src_account_id, id, @@ -303,7 +308,7 @@ impl<T: SessionStream> SessionData<T> { // Broadcast changes on destination account if let Some(change_id) = dest_change_id { - self.jmap + self.server .broadcast_state_change( StateChange::new(dest_account_id) .with_change(DataType::Email, change_id) @@ -317,11 +322,11 @@ impl<T: SessionStream> SessionData<T> { // Write changes on source account if !changelog.is_empty() { let change_id = self - .jmap + .server .commit_changes(src_mailbox.id.account_id, changelog) .await .imap_ctx(&arguments.tag, trc::location!())?; - self.jmap + self.server .broadcast_state_change( StateChange::new(src_mailbox.id.account_id) .with_change(DataType::Email, change_id) @@ -423,7 +428,7 @@ impl<T: SessionStream> SessionData<T> { ) -> trc::Result<Option<(TagManager<UidMailbox>, u32)>> { // Obtain mailbox tags if let (Some(mailboxes), Some(thread_id)) = ( - self.jmap + self.server .get_property::<HashedValue<Vec<UidMailbox>>>( account_id, Collection::Email, @@ -431,7 +436,7 @@ impl<T: SessionStream> SessionData<T> { Property::MailboxIds, ) .await?, - self.jmap + self.server .get_property::<u32>(account_id, Collection::Email, id, Property::ThreadId) .await?, ) { diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs index edd10ea0..2889cb5f 100644 --- a/crates/imap/src/op/create.rs +++ b/crates/imap/src/op/create.rs @@ -7,18 +7,20 @@ use std::time::Instant; use crate::{ - core::{Account, Mailbox, Session, SessionData}, + core::{Session, SessionData}, op::ImapContext, spawn_op, }; -use common::listener::SessionStream; +use common::{listener::SessionStream, Account, Mailbox}; use directory::Permission; use imap_proto::{ protocol::{create::Arguments, list::Attribute}, receiver::Request, Command, ResponseCode, StatusResponse, }; -use jmap::mailbox::set::SCHEMA; +use jmap::{ + changes::write::ChangeLog, mailbox::set::SCHEMA, services::state::StateManager, JmapMethods, +}; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{ @@ -75,7 +77,7 @@ impl<T: SessionStream> SessionData<T> { // Build batch let mut changes = self - .jmap + .server .begin_changes(params.account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -102,7 +104,7 @@ impl<T: SessionStream> SessionData<T> { .create_document() .custom(ObjectIndexBuilder::new(SCHEMA).with_changes(mailbox)); let mailbox_id = self - .jmap + .server .write_batch_expect_id(batch) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -118,13 +120,13 @@ impl<T: SessionStream> SessionData<T> { .with_account_id(params.account_id) .with_collection(Collection::Mailbox) .custom(changes); - self.jmap + self.server .write_batch(batch) .await .imap_ctx(&arguments.tag, trc::location!())?; // Broadcast changes - self.jmap + self.server .broadcast_state_change( StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id), ) @@ -205,7 +207,7 @@ impl<T: SessionStream> SessionData<T> { }; let effective_id = self - .jmap + .server .core .jmap .default_folders @@ -271,7 +273,7 @@ impl<T: SessionStream> SessionData<T> { return Err(trc::ImapEvent::Error .into_err() .details("Invalid empty path item.")); - } else if path_item.len() > self.jmap.core.jmap.mailbox_name_max_len { + } else if path_item.len() > self.server.core.jmap.mailbox_name_max_len { return Err(trc::ImapEvent::Error .into_err() .details("Mailbox name is too long.")); @@ -279,7 +281,7 @@ impl<T: SessionStream> SessionData<T> { path.push(path_item); } - if path.len() > self.jmap.core.jmap.mailbox_max_depth { + if path.len() > self.server.core.jmap.mailbox_max_depth { return Err(trc::ImapEvent::Error .into_err() .details("Mailbox path is too deep.")); @@ -295,7 +297,7 @@ impl<T: SessionStream> SessionData<T> { let (account_id, path) = { let mailboxes = self.mailboxes.lock(); let first_path_item = path.first().unwrap(); - let account = if first_path_item == &self.jmap.core.jmap.shared_folder { + let account = if first_path_item == &self.server.core.jmap.shared_folder { // Shared Folders/<username>/<folder> if path.len() < 3 { return Err(trc::ImapEvent::Error @@ -391,7 +393,7 @@ impl<T: SessionStream> SessionData<T> { special_use: if let Some(mailbox_role) = mailbox_role { // Make sure role is unique if !self - .jmap + .server .filter( account_id, Collection::Mailbox, diff --git a/crates/imap/src/op/delete.rs b/crates/imap/src/op/delete.rs index d8d028a8..0d9ecec8 100644 --- a/crates/imap/src/op/delete.rs +++ b/crates/imap/src/op/delete.rs @@ -15,6 +15,7 @@ use directory::Permission; use imap_proto::{ protocol::delete::Arguments, receiver::Request, Command, ResponseCode, StatusResponse, }; +use jmap::{changes::write::ChangeLog, mailbox::set::MailboxSet, services::state::StateManager}; use jmap_proto::types::{state::StateChange, type_state::DataType}; use store::write::log::ChangeLogBuilder; @@ -76,7 +77,7 @@ impl<T: SessionStream> SessionData<T> { .imap_ctx(&arguments.tag, trc::location!())?; let mut changelog = ChangeLogBuilder::new(); let did_remove_emails = match self - .jmap + .server .mailbox_destroy(account_id, mailbox_id, &mut changelog, &access_token, true) .await .imap_ctx(&arguments.tag, trc::location!())? @@ -93,13 +94,13 @@ impl<T: SessionStream> SessionData<T> { // Write changes let change_id = self - .jmap + .server .commit_changes(account_id, changelog) .await .imap_ctx(&arguments.tag, trc::location!())?; // Broadcast changes - self.jmap + self.server .broadcast_state_change(if did_remove_emails { StateChange::new(account_id) .with_change(DataType::Mailbox, change_id) diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs index 36c99003..519c2808 100644 --- a/crates/imap/src/op/expunge.rs +++ b/crates/imap/src/op/expunge.rs @@ -15,9 +15,15 @@ use imap_proto::{ }; use trc::AddContext; -use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData}; -use common::listener::SessionStream; -use jmap::{email::set::TagManager, mailbox::UidMailbox}; +use crate::core::{SavedSearch, SelectedMailbox, Session, SessionData}; +use common::{listener::SessionStream, ImapId}; +use jmap::{ + changes::write::ChangeLog, + email::{delete::EmailDeletion, set::TagManager}, + mailbox::UidMailbox, + services::state::StateManager, + JmapMethods, +}; use jmap_proto::types::{ acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property, state::StateChange, type_state::DataType, @@ -118,7 +124,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain message ids let account_id = mailbox.id.account_id; let mut deleted_ids = self - .jmap + .server .get_tag( account_id, Collection::Email, @@ -129,7 +135,7 @@ impl<T: SessionStream> SessionData<T> { .caused_by(trc::location!())? .unwrap_or_default() & self - .jmap + .server .get_tag( account_id, Collection::Email, @@ -167,8 +173,8 @@ impl<T: SessionStream> SessionData<T> { // Write changes on source account if !changelog.is_empty() { - let change_id = self.jmap.commit_changes(account_id, changelog).await?; - self.jmap + let change_id = self.server.commit_changes(account_id, changelog).await?; + self.server .broadcast_state_change( StateChange::new(account_id) .with_change(DataType::Email, change_id) @@ -192,7 +198,7 @@ impl<T: SessionStream> SessionData<T> { let mut destroy_ids = RoaringBitmap::new(); for (id, mailbox_ids) in self - .jmap + .server .get_properties::<HashedValue<Vec<UidMailbox>>, _, _>( account_id, Collection::Email, @@ -208,7 +214,7 @@ impl<T: SessionStream> SessionData<T> { if mailboxes.current().len() > 1 { // Remove deleted flag let (mut keywords, thread_id) = if let (Some(keywords), Some(thread_id)) = ( - self.jmap + self.server .get_property::<HashedValue<Vec<Keyword>>>( account_id, Collection::Email, @@ -217,7 +223,7 @@ impl<T: SessionStream> SessionData<T> { ) .await .caused_by(trc::location!())?, - self.jmap + self.server .get_property::<u32>( account_id, Collection::Email, @@ -245,10 +251,10 @@ impl<T: SessionStream> SessionData<T> { mailboxes.update_batch(&mut batch, Property::MailboxIds); keywords.update_batch(&mut batch, Property::Keywords); if changelog.change_id == u64::MAX { - changelog.change_id = self.jmap.assign_change_id(account_id).await? + changelog.change_id = self.server.assign_change_id(account_id).await? } batch.value(Property::Cid, changelog.change_id, F_VALUE); - match self.jmap.write_batch(batch).await { + match self.server.write_batch(batch).await { Ok(_) => { changelog.log_update(Collection::Email, Id::from_parts(thread_id, id)); changelog.log_child_update(Collection::Mailbox, mailbox_id.mailbox_id); @@ -268,7 +274,7 @@ impl<T: SessionStream> SessionData<T> { if !destroy_ids.is_empty() { // Delete message from all mailboxes let (changes, _) = self - .jmap + .server .emails_tombstone(account_id, destroy_ids) .await .caused_by(trc::location!())?; diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs index 182b8303..be2b1fc8 100644 --- a/crates/imap/src/op/fetch.rs +++ b/crates/imap/src/op/fetch.rs @@ -26,7 +26,13 @@ use imap_proto::{ receiver::Request, Command, ResponseCode, ResponseType, StatusResponse, }; -use jmap::email::metadata::MessageMetadata; +use jmap::{ + blob::download::BlobDownload, + changes::{get::ChangesLookup, write::ChangeLog}, + email::metadata::MessageMetadata, + services::state::StateManager, + JmapMethods, +}; use jmap_proto::types::{ acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property, state::StateChange, type_state::DataType, @@ -127,7 +133,7 @@ impl<T: SessionStream> SessionData<T> { if let Some(changed_since) = arguments.changed_since { // Obtain changes since the modseq. let changelog = self - .jmap + .server .changes_( account_id, Collection::Email, @@ -280,7 +286,7 @@ impl<T: SessionStream> SessionData<T> { for (seqnum, uid, id) in ids { // Obtain attributes and keywords let (email, keywords) = if let (Some(email), Some(keywords)) = ( - self.jmap + self.server .get_property::<Bincode<MessageMetadata>>( account_id, Collection::Email, @@ -289,7 +295,7 @@ impl<T: SessionStream> SessionData<T> { ) .await .imap_ctx(&arguments.tag, trc::location!())?, - self.jmap + self.server .get_property::<HashedValue<Vec<Keyword>>>( account_id, Collection::Email, @@ -316,7 +322,7 @@ impl<T: SessionStream> SessionData<T> { let raw_message = if needs_blobs { // Retrieve raw message if needed match self - .jmap + .server .get_blob(&email.blob_hash, 0..usize::MAX) .await .imap_ctx(&arguments.tag, trc::location!())? @@ -347,7 +353,7 @@ impl<T: SessionStream> SessionData<T> { set_seen_flags && !keywords.inner.iter().any(|k| k == &Keyword::Seen); let thread_id = if needs_thread_id || set_seen_flag { if let Some(thread_id) = self - .jmap + .server .get_property::<u32>(account_id, Collection::Email, id, Property::ThreadId) .await .imap_ctx(&arguments.tag, trc::location!())? @@ -479,7 +485,7 @@ impl<T: SessionStream> SessionData<T> { } Attribute::ModSeq => { if let Ok(Some(modseq)) = self - .jmap + .server .get_property::<u64>(account_id, Collection::Email, id, Property::Cid) .await { @@ -524,7 +530,7 @@ impl<T: SessionStream> SessionData<T> { // Set Seen ids if !set_seen_ids.is_empty() { let mut changelog = self - .jmap + .server .begin_changes(account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -539,7 +545,7 @@ impl<T: SessionStream> SessionData<T> { .value(Property::Keywords, keywords.inner, F_VALUE) .value(Property::Keywords, Keyword::Seen, F_BITMAP) .value(Property::Cid, changelog.change_id, F_VALUE); - match self.jmap.write_batch(batch).await { + match self.server.write_batch(batch).await { Ok(_) => { changelog.log_update(Collection::Email, id); } @@ -553,12 +559,12 @@ impl<T: SessionStream> SessionData<T> { if !changelog.is_empty() { // Write changes let change_id = self - .jmap + .server .commit_changes(account_id, changelog) .await .imap_ctx(&arguments.tag, trc::location!())?; modseq = change_id.into(); - self.jmap + self.server .broadcast_state_change( StateChange::new(account_id).with_change(DataType::Email, change_id), ) diff --git a/crates/imap/src/op/idle.rs b/crates/imap/src/op/idle.rs index 819311b9..c395bf21 100644 --- a/crates/imap/src/op/idle.rs +++ b/crates/imap/src/op/idle.rs @@ -20,6 +20,7 @@ use imap_proto::{ }; use common::listener::SessionStream; +use jmap::{changes::get::ChangesLookup, services::state::StateManager}; use jmap_proto::types::{collection::Collection, type_state::DataType}; use store::query::log::Query; use tokio::io::AsyncReadExt; @@ -53,7 +54,7 @@ impl<T: SessionStream> Session<T> { // Register with state manager let mut change_rx = self - .jmap + .server .subscribe_state_manager(data.account_id, types) .await .imap_ctx(&request.tag, trc::location!())?; @@ -72,7 +73,7 @@ impl<T: SessionStream> Session<T> { let mut buf = vec![0; 4]; loop { tokio::select! { - result = tokio::time::timeout(self.jmap.core.imap.timeout_idle, self.stream_rx.read_exact(&mut buf)) => { + result = tokio::time::timeout(self.server.core.imap.timeout_idle, self.stream_rx.read_exact(&mut buf)) => { match result { Ok(Ok(bytes_read)) => { if bytes_read > 0 { @@ -202,7 +203,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain changed messages let changelog = self - .jmap + .server .changes_( mailbox.id.account_id, Collection::Email, diff --git a/crates/imap/src/op/list.rs b/crates/imap/src/op/list.rs index dce26b75..9b2cc3f3 100644 --- a/crates/imap/src/op/list.rs +++ b/crates/imap/src/op/list.rs @@ -173,10 +173,10 @@ impl<T: SessionStream> SessionData<T> { if let Some(prefix) = &account.prefix { if !added_shared_folder { if !filter_subscribed - && matches_pattern(&patterns, &self.jmap.core.jmap.shared_folder) + && matches_pattern(&patterns, &self.server.core.jmap.shared_folder) { list_items.push(ListItem { - mailbox_name: self.jmap.core.jmap.shared_folder.clone(), + mailbox_name: self.server.core.jmap.shared_folder.clone(), attributes: if include_children { vec![Attribute::HasChildren, Attribute::NoSelect] } else { diff --git a/crates/imap/src/op/namespace.rs b/crates/imap/src/op/namespace.rs index 0f9ed1ac..10f0ea00 100644 --- a/crates/imap/src/op/namespace.rs +++ b/crates/imap/src/op/namespace.rs @@ -30,7 +30,7 @@ impl<T: SessionStream> Session<T> { .serialize( Response { shared_prefix: if self.state.session_data().mailboxes.lock().len() > 1 { - self.jmap.core.jmap.shared_folder.clone().into() + self.server.core.jmap.shared_folder.clone().into() } else { None }, diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs index cc8b27fb..9daa7fd7 100644 --- a/crates/imap/src/op/rename.rs +++ b/crates/imap/src/op/rename.rs @@ -15,7 +15,10 @@ use directory::Permission; use imap_proto::{ protocol::rename::Arguments, receiver::Request, Command, ResponseCode, StatusResponse, }; -use jmap::{auth::acl::EffectiveAcl, mailbox::set::SCHEMA}; +use jmap::{ + auth::acl::EffectiveAcl, changes::write::ChangeLog, mailbox::set::SCHEMA, + services::state::StateManager, JmapMethods, +}; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{ @@ -92,7 +95,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain mailbox let mailbox = self - .jmap + .server .get_property::<HashedValue<Object<Value>>>( params.account_id, Collection::Mailbox, @@ -133,7 +136,7 @@ impl<T: SessionStream> SessionData<T> { // Build batch let mut changes = self - .jmap + .server .begin_changes(params.account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -159,7 +162,7 @@ impl<T: SessionStream> SessionData<T> { ); let mailbox_id = self - .jmap + .server .write_batch_expect_id(batch) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -191,13 +194,13 @@ impl<T: SessionStream> SessionData<T> { let change_id = changes.change_id; batch.custom(changes); - self.jmap + self.server .write_batch(batch) .await .imap_ctx(&arguments.tag, trc::location!())?; // Broadcast changes - self.jmap + self.server .broadcast_state_change( StateChange::new(params.account_id).with_change(DataType::Mailbox, change_id), ) diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs index 86329275..bf8670ac 100644 --- a/crates/imap/src/op/search.rs +++ b/crates/imap/src/op/search.rs @@ -6,7 +6,7 @@ use std::{sync::Arc, time::Instant}; -use common::listener::SessionStream; +use common::{listener::SessionStream, ImapId}; use directory::Permission; use imap_proto::{ protocol::{ @@ -16,6 +16,7 @@ use imap_proto::{ receiver::Request, Command, StatusResponse, }; +use jmap::{changes::get::ChangesLookup, JmapMethods}; use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; use mail_parser::HeaderName; use nlp::language::Language; @@ -29,7 +30,7 @@ use tokio::sync::watch; use trc::AddContext; use crate::{ - core::{ImapId, MailboxState, SavedSearch, SelectedMailbox, Session, SessionData}, + core::{SavedSearch, SelectedMailbox, Session, SessionData}, spawn_op, }; @@ -142,7 +143,7 @@ impl<T: SessionStream> SessionData<T> { let mut imap_ids = Vec::with_capacity(results_len); let is_sort = if let Some(sort) = arguments.sort { mailbox.map_search_results( - self.jmap + self.server .core .storage .data @@ -260,7 +261,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain message ids let mut filters = Vec::with_capacity(imap_filter.len() + 1); let message_ids = self - .jmap + .server .get_tag( mailbox.id.account_id, Collection::Email, @@ -290,7 +291,7 @@ impl<T: SessionStream> SessionData<T> { fts_filters.push(FtsFilter::has_text_detect( Field::Body, text, - self.jmap.core.jmap.default_language, + self.server.core.jmap.default_language, )); } search::Filter::Cc(text) => { @@ -350,7 +351,7 @@ impl<T: SessionStream> SessionData<T> { fts_filters.push(FtsFilter::has_text_detect( Field::Header(HeaderName::Subject), text, - self.jmap.core.jmap.default_language, + self.server.core.jmap.default_language, )); } search::Filter::Text(text) => { @@ -378,17 +379,17 @@ impl<T: SessionStream> SessionData<T> { fts_filters.push(FtsFilter::has_text_detect( Field::Header(HeaderName::Subject), &text, - self.jmap.core.jmap.default_language, + self.server.core.jmap.default_language, )); fts_filters.push(FtsFilter::has_text_detect( Field::Body, &text, - self.jmap.core.jmap.default_language, + self.server.core.jmap.default_language, )); fts_filters.push(FtsFilter::has_text_detect( Field::Attachment, text, - self.jmap.core.jmap.default_language, + self.server.core.jmap.default_language, )); fts_filters.push(FtsFilter::End); } @@ -416,7 +417,7 @@ impl<T: SessionStream> SessionData<T> { } filters.push(query::Filter::is_in_set( - self.jmap + self.server .fts_filter(mailbox.id.account_id, Collection::Email, fts_filters) .await?, )); @@ -612,7 +613,7 @@ impl<T: SessionStream> SessionData<T> { search::Filter::ModSeq((modseq, _)) => { let mut set = RoaringBitmap::new(); for change in self - .jmap + .server .changes_( mailbox.id.account_id, Collection::Email, @@ -658,7 +659,7 @@ impl<T: SessionStream> SessionData<T> { } // Run query - self.jmap + self.server .filter(mailbox.id.account_id, Collection::Email, filters) .await .map(|res| (res, include_highest_modseq)) @@ -738,23 +739,6 @@ impl SelectedMailbox { } } -impl MailboxState { - pub fn map_result_id(&self, document_id: u32, is_uid: bool) -> Option<(u32, ImapId)> { - if let Some(imap_id) = self.id_to_imap.get(&document_id) { - Some((if is_uid { imap_id.uid } else { imap_id.seqnum }, *imap_id)) - } else if is_uid { - self.next_state.as_ref().and_then(|s| { - s.next_state - .id_to_imap - .get(&document_id) - .map(|imap_id| (imap_id.uid, *imap_id)) - }) - } else { - None - } - } -} - impl SavedSearch { pub async fn unwrap(&self) -> Option<Arc<Vec<ImapId>>> { match self { diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs index a36125ea..5d6c1853 100644 --- a/crates/imap/src/op/select.rs +++ b/crates/imap/src/op/select.rs @@ -47,35 +47,39 @@ impl<T: SessionStream> Session<T> { if let Some(mailbox) = data.get_mailbox_by_name(&arguments.mailbox_name) { // Try obtaining the mailbox from the cache - let state = { - let modseq = data - .get_modseq(mailbox.account_id) - .await - .imap_ctx(&arguments.tag, trc::location!())?; - - if let Some(cached_state) = - self.imap - .cache_mailbox - .get(&mailbox) - .and_then(|cached_state| { - if cached_state.modseq.unwrap_or(0) >= modseq.unwrap_or(0) { - Some(cached_state) - } else { - None - } - }) + let state = { - cached_state.as_ref().clone() - } else { - let new_state = Arc::new( - data.fetch_messages(&mailbox) - .await - .imap_ctx(&arguments.tag, trc::location!())?, - ); - self.imap.cache_mailbox.insert(mailbox, new_state.clone()); - new_state.as_ref().clone() - } - }; + let modseq = data + .get_modseq(mailbox.account_id) + .await + .imap_ctx(&arguments.tag, trc::location!())?; + + if let Some(cached_state) = + self.server.inner.data.mailbox_cache.get(&mailbox).and_then( + |cached_state| { + if cached_state.modseq.unwrap_or(0) >= modseq.unwrap_or(0) { + Some(cached_state) + } else { + None + } + }, + ) + { + cached_state.as_ref().clone() + } else { + let new_state = Arc::new( + data.fetch_messages(&mailbox) + .await + .imap_ctx(&arguments.tag, trc::location!())?, + ); + self.server + .inner + .data + .mailbox_cache + .insert(mailbox, new_state.clone()); + new_state.as_ref().clone() + } + }; // Synchronize messages let closed_previous = self.state.close_mailbox(); diff --git a/crates/imap/src/op/status.rs b/crates/imap/src/op/status.rs index ce8c6029..d06ae582 100644 --- a/crates/imap/src/op/status.rs +++ b/crates/imap/src/op/status.rs @@ -7,11 +7,11 @@ use std::{sync::Arc, time::Instant}; use crate::{ - core::{Mailbox, Session, SessionData}, + core::{Session, SessionData}, op::ImapContext, spawn_op, }; -use common::listener::SessionStream; +use common::{listener::SessionStream, Mailbox}; use directory::Permission; use imap_proto::{ parser::PushUnique, @@ -19,6 +19,7 @@ use imap_proto::{ receiver::Request, Command, ResponseCode, StatusResponse, }; +use jmap::JmapMethods; use jmap_proto::{ object::Object, types::{collection::Collection, id::Id, keyword::Keyword, property::Property, value::Value}, @@ -86,11 +87,11 @@ impl<T: SessionStream> SessionData<T> { mailbox } else { // Some IMAP clients will try to get the status of a mailbox with the NoSelect flag - return if mailbox_name == self.jmap.core.jmap.shared_folder + return if mailbox_name == self.server.core.jmap.shared_folder || mailbox_name .split_once('/') .map_or(false, |(base_name, path)| { - base_name == self.jmap.core.jmap.shared_folder && !path.contains('/') + base_name == self.server.core.jmap.shared_folder && !path.contains('/') }) { Ok(StatusItem { @@ -211,7 +212,7 @@ impl<T: SessionStream> SessionData<T> { // Retrieve latest values let mut values_update = Vec::with_capacity(items_update.len()); let mailbox_message_ids = self - .jmap + .server .get_tag( mailbox.account_id, Collection::Email, @@ -222,7 +223,7 @@ impl<T: SessionStream> SessionData<T> { .caused_by(trc::location!())? .map(Arc::new); let message_ids = self - .jmap + .server .get_document_ids(mailbox.account_id, Collection::Email) .await .caused_by(trc::location!())?; @@ -232,7 +233,7 @@ impl<T: SessionStream> SessionData<T> { Status::Messages => mailbox_message_ids.as_ref().map(|v| v.len()).unwrap_or(0), Status::UidNext => { (self - .jmap + .server .core .storage .data @@ -247,7 +248,7 @@ impl<T: SessionStream> SessionData<T> { + 1) as u64 } Status::UidValidity => self - .jmap + .server .get_property::<Object<Value>>( mailbox.account_id, Collection::Mailbox, @@ -270,7 +271,7 @@ impl<T: SessionStream> SessionData<T> { (&message_ids, &mailbox_message_ids) { if let Some(mut seen) = self - .jmap + .server .get_tag( mailbox.account_id, Collection::Email, @@ -293,7 +294,7 @@ impl<T: SessionStream> SessionData<T> { Status::Deleted => { if let (Some(mailbox_message_ids), Some(mut deleted)) = ( &mailbox_message_ids, - self.jmap + self.server .get_tag( mailbox.account_id, Collection::Email, @@ -378,7 +379,7 @@ impl<T: SessionStream> SessionData<T> { message_ids: &Arc<RoaringBitmap>, ) -> trc::Result<u32> { let mut total_size = 0u32; - self.jmap + self.server .core .storage .data diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs index 3a799841..14a5626a 100644 --- a/crates/imap/src/op/store.rs +++ b/crates/imap/src/op/store.rs @@ -22,7 +22,13 @@ use imap_proto::{ receiver::Request, Command, ResponseCode, ResponseType, StatusResponse, }; -use jmap::{email::set::TagManager, mailbox::UidMailbox}; +use jmap::{ + changes::{get::ChangesLookup, write::ChangeLog}, + email::set::TagManager, + mailbox::UidMailbox, + services::state::StateManager, + JmapMethods, +}; use jmap_proto::types::{ acl::Acl, collection::Collection, id::Id, keyword::Keyword, property::Property, state::StateChange, type_state::DataType, @@ -110,7 +116,7 @@ impl<T: SessionStream> SessionData<T> { if let Some(unchanged_since) = arguments.unchanged_since { // Obtain changes since the modseq. let changelog = self - .jmap + .server .changes_( account_id, Collection::Email, @@ -192,7 +198,7 @@ impl<T: SessionStream> SessionData<T> { loop { // Obtain current keywords let (mut keywords, thread_id) = if let (Some(keywords), Some(thread_id)) = ( - self.jmap + self.server .get_property::<HashedValue<Vec<Keyword>>>( account_id, Collection::Email, @@ -201,7 +207,7 @@ impl<T: SessionStream> SessionData<T> { ) .await .imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?, - self.jmap + self.server .get_property::<u32>(account_id, Collection::Email, *id, Property::ThreadId) .await .imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?, @@ -253,18 +259,18 @@ impl<T: SessionStream> SessionData<T> { keywords.update_batch(&mut batch, Property::Keywords); if changelog.change_id == u64::MAX { changelog.change_id = self - .jmap + .server .assign_change_id(account_id) .await .imap_ctx(response.tag.as_ref().unwrap(), trc::location!())? } batch.value(Property::Cid, changelog.change_id, F_VALUE); - match self.jmap.write_batch(batch).await { + match self.server.write_batch(batch).await { Ok(_) => { // Set all current mailboxes as changed if the Seen tag changed if seen_changed { if let Some(mailboxes) = self - .jmap + .server .get_property::<Vec<UidMailbox>>( account_id, Collection::Email, @@ -335,11 +341,11 @@ impl<T: SessionStream> SessionData<T> { // Write changes if !changelog.is_empty() { let change_id = self - .jmap + .server .commit_changes(account_id, changelog) .await .imap_ctx(response.tag.as_ref().unwrap(), trc::location!())?; - self.jmap + self.server .broadcast_state_change(if !changed_mailboxes.is_empty() { StateChange::new(account_id) .with_change(DataType::Email, change_id) diff --git a/crates/imap/src/op/subscribe.rs b/crates/imap/src/op/subscribe.rs index 995ed430..79b426d1 100644 --- a/crates/imap/src/op/subscribe.rs +++ b/crates/imap/src/op/subscribe.rs @@ -13,7 +13,12 @@ use crate::{ use common::listener::SessionStream; use directory::Permission; use imap_proto::{receiver::Request, Command, ResponseCode, StatusResponse}; -use jmap::mailbox::set::{MailboxSubscribe, SCHEMA}; +use jmap::{ + changes::write::ChangeLog, + mailbox::set::{MailboxSubscribe, SCHEMA}, + services::state::StateManager, + JmapMethods, +}; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{ @@ -100,7 +105,7 @@ impl<T: SessionStream> SessionData<T> { // Obtain mailbox let mailbox = self - .jmap + .server .get_property::<HashedValue<Object<Value>>>( account_id, Collection::Mailbox, @@ -122,7 +127,7 @@ impl<T: SessionStream> SessionData<T> { if let Some(value) = mailbox.inner.mailbox_subscribe(self.account_id, subscribe) { // Build batch let mut changes = self - .jmap + .server .begin_changes(account_id) .await .imap_ctx(&tag, trc::location!())?; @@ -142,13 +147,13 @@ impl<T: SessionStream> SessionData<T> { let change_id = changes.change_id; batch.custom(changes); - self.jmap + self.server .write_batch(batch) .await .imap_ctx(&tag, trc::location!())?; // Broadcast changes - self.jmap + self.server .broadcast_state_change( StateChange::new(account_id).with_change(DataType::Mailbox, change_id), ) diff --git a/crates/imap/src/op/thread.rs b/crates/imap/src/op/thread.rs index a8425652..94d8da12 100644 --- a/crates/imap/src/op/thread.rs +++ b/crates/imap/src/op/thread.rs @@ -21,6 +21,7 @@ use imap_proto::{ receiver::Request, Command, StatusResponse, }; +use jmap::email::cache::ThreadCache; use trc::AddContext; impl<T: SessionStream> Session<T> { @@ -80,7 +81,7 @@ impl<T: SessionStream> SessionData<T> { // Lock the cache let thread_ids = self - .jmap + .server .get_cached_thread_ids(mailbox.id.account_id, result_set.results.iter()) .await .caused_by(trc::location!())?; diff --git a/crates/jmap/src/api/autoconfig.rs b/crates/jmap/src/api/autoconfig.rs index 296dd5aa..27d61ac4 100644 --- a/crates/jmap/src/api/autoconfig.rs +++ b/crates/jmap/src/api/autoconfig.rs @@ -6,18 +6,34 @@ use std::fmt::Write; -use common::manager::webadmin::Resource; +use common::{manager::webadmin::Resource, Server}; use directory::{backend::internal::PrincipalField, QueryBy}; use quick_xml::events::Event; use quick_xml::Reader; use utils::url_params::UrlParams; -use crate::{api::http::ToHttpResponse, JMAP}; +use crate::api::http::ToHttpResponse; use super::{HttpRequest, HttpResponse}; +use std::future::Future; -impl JMAP { - pub async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> { +pub trait Autoconfig: Sync + Send { + fn handle_autoconfig_request( + &self, + req: &HttpRequest, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + fn handle_autodiscover_request( + &self, + body: Option<Vec<u8>>, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + fn autoconfig_parameters<'x>( + &self, + emailaddress: &'x str, + ) -> impl Future<Output = trc::Result<(String, String, &'x str)>> + Send; +} + +impl Autoconfig for Server { + async fn handle_autoconfig_request(&self, req: &HttpRequest) -> trc::Result<HttpResponse> { // Obtain parameters let params = UrlParams::new(req.uri().query()); let emailaddress = params @@ -73,7 +89,7 @@ impl JMAP { ) } - pub async fn handle_autodiscover_request( + async fn handle_autodiscover_request( &self, body: Option<Vec<u8>>, ) -> trc::Result<HttpResponse> { diff --git a/crates/jmap/src/api/event_source.rs b/crates/jmap/src/api/event_source.rs index 362eba1f..0a37544c 100644 --- a/crates/jmap/src/api/event_source.rs +++ b/crates/jmap/src/api/event_source.rs @@ -9,7 +9,7 @@ use std::{ time::{Duration, Instant}, }; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use http_body_util::{combinators::BoxBody, StreamBody}; use hyper::{ body::{Bytes, Frame}, @@ -18,9 +18,10 @@ use hyper::{ use jmap_proto::types::type_state::DataType; use utils::map::bitmap::Bitmap; -use crate::{JMAP, LONG_SLUMBER}; +use crate::{services::state::StateManager, LONG_SLUMBER}; use super::{HttpRequest, HttpResponse, HttpResponseBody, StateChangeResponse}; +use std::future::Future; struct Ping { interval: Duration, @@ -28,8 +29,16 @@ struct Ping { payload: Bytes, } -impl JMAP { - pub async fn handle_event_source( +pub trait EventSourceHandler: Sync + Send { + fn handle_event_source( + &self, + req: HttpRequest, + access_token: Arc<AccessToken>, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl EventSourceHandler for Server { + async fn handle_event_source( &self, req: HttpRequest, access_token: Arc<AccessToken>, diff --git a/crates/jmap/src/api/http.rs b/crates/jmap/src/api/http.rs index 86943ffe..4252edcb 100644 --- a/crates/jmap/src/api/http.rs +++ b/crates/jmap/src/api/http.rs @@ -8,10 +8,12 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc}; use common::{ auth::AccessToken, + core::BuildServer, expr::{functions::ResolveVariable, *}, + ipc::StateEvent, listener::{ServerInstance, SessionData, SessionManager, SessionStream}, manager::webadmin::Resource, - Core, + Inner, Server, }; use directory::Permission; use http_body_util::{BodyExt, Full}; @@ -29,17 +31,26 @@ use jmap_proto::{ response::Response, types::{blob::BlobId, id::Id}, }; +use std::future::Future; use crate::{ - auth::{authenticate::HttpHeaders, oauth::OAuthMetadata}, - blob::{DownloadResponse, UploadResponse}, - services::state, - JmapInstance, JMAP, + api::management::enterprise::telemetry::TelemetryApi, + auth::{ + authenticate::{Authenticator, HttpHeaders}, + oauth::{auth::OAuthApiHandler, token::TokenHandler, OAuthMetadata}, + rate_limit::RateLimiter, + }, + blob::{download::BlobDownload, upload::BlobUpload, DownloadResponse, UploadResponse}, + websocket::upgrade::WebSocketUpgrade, }; use super::{ - management::ManagementApiError, HtmlResponse, HttpRequest, HttpResponse, HttpResponseBody, - JmapSessionManager, JsonResponse, + autoconfig::Autoconfig, + event_source::EventSourceHandler, + management::{ManagementApi, ManagementApiError}, + request::RequestHandler, + session::SessionHandler, + HtmlResponse, HttpRequest, HttpResponse, HttpResponseBody, JmapSessionManager, JsonResponse, }; pub struct HttpSessionData { @@ -52,8 +63,16 @@ pub struct HttpSessionData { pub session_id: u64, } -impl JMAP { - pub async fn parse_http_request( +pub trait ParseHttp: Sync + Send { + fn parse_http_request( + &self, + req: HttpRequest, + session: HttpSessionData, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl ParseHttp for Server { + async fn parse_http_request( &self, mut req: HttpRequest, session: HttpSessionData, @@ -63,7 +82,7 @@ impl JMAP { // Validate endpoint access let ctx = HttpContext::new(&session, &req); - match ctx.has_endpoint_access(&self.core).await { + match ctx.has_endpoint_access(self).await { StatusCode::OK => (), status => { // Allow loopback address to avoid lockouts @@ -198,10 +217,7 @@ impl JMAP { self.authenticate_headers(&req, &session).await?; return Ok(self - .handle_session_resource( - ctx.resolve_response_url(&self.core).await, - access_token, - ) + .handle_session_resource(ctx.resolve_response_url(self).await, access_token) .await? .into_http_response()); } @@ -210,11 +226,11 @@ impl JMAP { self.is_anonymous_allowed(&session.remote_ip).await?; return Ok(JsonResponse::new(OAuthMetadata::new( - ctx.resolve_response_url(&self.core).await, + ctx.resolve_response_url(self).await, )) .into_http_response()); } - ("acme-challenge", &Method::GET) if self.core.has_acme_http_providers() => { + ("acme-challenge", &Method::GET) if self.has_acme_http_providers() => { if let Some(token) = path.next() { return match self .core @@ -230,7 +246,7 @@ impl JMAP { } } ("mta-sts.txt", &Method::GET) => { - if let Some(policy) = self.core.build_mta_sts_policy() { + if let Some(policy) = self.build_mta_sts_policy() { return Ok(Resource::new("text/plain", policy.to_string().into_bytes()) .into_http_response()); } else { @@ -256,7 +272,7 @@ impl JMAP { ("device", &Method::POST) => { self.is_anonymous_allowed(&session.remote_ip).await?; - let url = ctx.resolve_response_url(&self.core).await; + let url = ctx.resolve_response_url(self).await; return self .handle_device_auth(&mut req, url, session.session_id) .await; @@ -389,7 +405,7 @@ impl JMAP { return Ok(Resource::new( "text/plain; version=0.0.4", - self.core.export_prometheus_metrics().await?.into_bytes(), + self.export_prometheus_metrics().await?.into_bytes(), ) .into_http_response()); } @@ -400,13 +416,12 @@ impl JMAP { _ => (), }, #[cfg(feature = "enterprise")] - "logo.svg" if self.core.is_enterprise_edition() => { + "logo.svg" if self.is_enterprise_edition() => { // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> // SPDX-License-Identifier: LicenseRef-SEL match self - .core .logo_resource( req.headers() .get(header::HOST) @@ -425,7 +440,7 @@ impl JMAP { } } - let resource = self.inner.webadmin.get("logo.svg").await?; + let resource = self.inner.data.webadmin.get("logo.svg").await?; return if !resource.is_empty() { Ok(resource.into_http_response()) @@ -439,6 +454,7 @@ impl JMAP { let path = req.uri().path(); let resource = self .inner + .data .webadmin .get(path.strip_prefix('/').unwrap_or(path)) .await?; @@ -455,166 +471,156 @@ impl JMAP { } } -impl JmapInstance { - async fn handle_session<T: SessionStream>(self, session: SessionData<T>) { - let _in_flight = session.in_flight; - let is_tls = session.stream.is_tls(); - - if let Err(http_err) = http1::Builder::new() - .keep_alive(true) - .serve_connection( - TokioIo::new(session.stream), - service_fn(|req: hyper::Request<body::Incoming>| { - let jmap_instance = self.clone(); - let instance = session.instance.clone(); - - async move { - let jmap = JMAP::from(jmap_instance); - - // Obtain remote IP - let remote_ip = if !jmap.core.jmap.http_use_forwarded { - trc::event!( - Http(trc::HttpEvent::RequestUrl), - SpanId = session.session_id, - Url = req.uri().to_string(), - ); - - session.remote_ip - } else if let Some(forwarded_for) = req - .headers() - .get(header::FORWARDED) - .and_then(|h| h.to_str().ok()) - .and_then(|h| { - let h = h.to_ascii_lowercase(); - h.split_once("for=").and_then(|(_, rest)| { - let mut start_ip = usize::MAX; - let mut end_ip = usize::MAX; - - for (pos, ch) in rest.char_indices() { - match ch { - '0'..='9' | 'a'..='f' | ':' | '.' => { - if start_ip == usize::MAX { - start_ip = pos; - } - end_ip = pos; - } - '"' | '[' | ' ' if start_ip == usize::MAX => {} - _ => { - break; +async fn handle_session<T: SessionStream>(inner: Arc<Inner>, session: SessionData<T>) { + let _in_flight = session.in_flight; + let is_tls = session.stream.is_tls(); + + if let Err(http_err) = http1::Builder::new() + .keep_alive(true) + .serve_connection( + TokioIo::new(session.stream), + service_fn(|req: hyper::Request<body::Incoming>| { + let instance = session.instance.clone(); + let inner = inner.clone(); + + async move { + let server = inner.build_server(); + + // Obtain remote IP + let remote_ip = if !server.core.jmap.http_use_forwarded { + trc::event!( + Http(trc::HttpEvent::RequestUrl), + SpanId = session.session_id, + Url = req.uri().to_string(), + ); + + session.remote_ip + } else if let Some(forwarded_for) = req + .headers() + .get(header::FORWARDED) + .and_then(|h| h.to_str().ok()) + .and_then(|h| { + let h = h.to_ascii_lowercase(); + h.split_once("for=").and_then(|(_, rest)| { + let mut start_ip = usize::MAX; + let mut end_ip = usize::MAX; + + for (pos, ch) in rest.char_indices() { + match ch { + '0'..='9' | 'a'..='f' | ':' | '.' => { + if start_ip == usize::MAX { + start_ip = pos; } + end_ip = pos; + } + '"' | '[' | ' ' if start_ip == usize::MAX => {} + _ => { + break; } } + } - rest.get(start_ip..=end_ip) - .and_then(|h| h.parse::<IpAddr>().ok()) - }) - }) - .or_else(|| { - req.headers() - .get("X-Forwarded-For") - .and_then(|h| h.to_str().ok()) - .map(|h| h.split_once(',').map_or(h, |(ip, _)| ip).trim()) + rest.get(start_ip..=end_ip) .and_then(|h| h.parse::<IpAddr>().ok()) }) - { - trc::event!( - Http(trc::HttpEvent::RequestUrl), - SpanId = session.session_id, - RemoteIp = forwarded_for, - Url = req.uri().to_string(), - ); - - forwarded_for - } else { - trc::event!( - Http(trc::HttpEvent::XForwardedMissing), - SpanId = session.session_id, - ); - session.remote_ip - }; - - // Parse HTTP request - let response = match jmap - .parse_http_request( - req, - HttpSessionData { - instance, - local_ip: session.local_ip, - local_port: session.local_port, - remote_ip, - remote_port: session.remote_port, - is_tls, - session_id: session.session_id, - }, - ) - .await - { - Ok(response) => response, - Err(err) => { - let response = err.into_http_response(); - trc::error!(err.span_id(session.session_id)); - response - } - }; + }) + .or_else(|| { + req.headers() + .get("X-Forwarded-For") + .and_then(|h| h.to_str().ok()) + .map(|h| h.split_once(',').map_or(h, |(ip, _)| ip).trim()) + .and_then(|h| h.parse::<IpAddr>().ok()) + }) + { + trc::event!( + Http(trc::HttpEvent::RequestUrl), + SpanId = session.session_id, + RemoteIp = forwarded_for, + Url = req.uri().to_string(), + ); + forwarded_for + } else { trc::event!( - Http(trc::HttpEvent::ResponseBody), + Http(trc::HttpEvent::XForwardedMissing), SpanId = session.session_id, - Contents = match &response.body { - HttpResponseBody::Text(value) => trc::Value::String(value.clone()), - HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"), - HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"), - _ => trc::Value::None, - }, - Code = response.status.as_u16(), - Size = response.size(), ); + session.remote_ip + }; + + // Parse HTTP request + let response = match server + .parse_http_request( + req, + HttpSessionData { + instance, + local_ip: session.local_ip, + local_port: session.local_port, + remote_ip, + remote_port: session.remote_port, + is_tls, + session_id: session.session_id, + }, + ) + .await + { + Ok(response) => response, + Err(err) => { + let response = err.into_http_response(); + trc::error!(err.span_id(session.session_id)); + response + } + }; + + trc::event!( + Http(trc::HttpEvent::ResponseBody), + SpanId = session.session_id, + Contents = match &response.body { + HttpResponseBody::Text(value) => trc::Value::String(value.clone()), + HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"), + HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"), + _ => trc::Value::None, + }, + Code = response.status.as_u16(), + Size = response.size(), + ); - // Build response - let mut response = response.build(); + // Build response + let mut response = response.build(); - // Add custom headers - if !jmap.core.jmap.http_headers.is_empty() { - let headers = response.headers_mut(); + // Add custom headers + if !server.core.jmap.http_headers.is_empty() { + let headers = response.headers_mut(); - for (header, value) in &jmap.core.jmap.http_headers { - headers.insert(header.clone(), value.clone()); - } + for (header, value) in &server.core.jmap.http_headers { + headers.insert(header.clone(), value.clone()); } - - Ok::<_, hyper::Error>(response) } - }), - ) - .with_upgrades() - .await - { - trc::event!( - Http(trc::HttpEvent::Error), - SpanId = session.session_id, - Reason = http_err.to_string(), - ); - } + + Ok::<_, hyper::Error>(response) + } + }), + ) + .with_upgrades() + .await + { + trc::event!( + Http(trc::HttpEvent::Error), + SpanId = session.session_id, + Reason = http_err.to_string(), + ); } } impl SessionManager for JmapSessionManager { - fn handle<T: SessionStream>( - self, - session: SessionData<T>, - ) -> impl std::future::Future<Output = ()> + Send { - self.inner.handle_session(session) + fn handle<T: SessionStream>(self, session: SessionData<T>) -> impl Future<Output = ()> + Send { + handle_session(self.inner, session) } #[allow(clippy::manual_async_fn)] fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send { async { - let _ = self - .inner - .jmap_inner - .state_tx - .send(state::Event::Stop) - .await; + let _ = self.inner.ipc.state_tx.send(StateEvent::Stop).await; } } } @@ -629,31 +635,33 @@ impl<'x> HttpContext<'x> { Self { session, req } } - pub async fn resolve_response_url(&self, core: &Core) -> String { - core.eval_if( - &core.network.http_response_url, - self, - self.session.session_id, - ) - .await - .unwrap_or_else(|| { - format!( - "http{}://{}:{}", - if self.session.is_tls { "s" } else { "" }, - self.session.local_ip, - self.session.local_port + async fn resolve_response_url(&self, server: &Server) -> String { + server + .eval_if( + &server.core.network.http_response_url, + self, + self.session.session_id, ) - }) + .await + .unwrap_or_else(|| { + format!( + "http{}://{}:{}", + if self.session.is_tls { "s" } else { "" }, + self.session.local_ip, + self.session.local_port + ) + }) } - pub async fn has_endpoint_access(&self, core: &Core) -> StatusCode { - core.eval_if( - &core.network.http_allowed_endpoint, - self, - self.session.session_id, - ) - .await - .unwrap_or(StatusCode::OK) + async fn has_endpoint_access(&self, server: &Server) -> StatusCode { + server + .eval_if( + &server.core.network.http_allowed_endpoint, + self, + self.session.session_id, + ) + .await + .unwrap_or(StatusCode::OK) } } diff --git a/crates/jmap/src/api/management/dkim.rs b/crates/jmap/src/api/management/dkim.rs index 09e0c383..5fcbe4eb 100644 --- a/crates/jmap/src/api/management/dkim.rs +++ b/crates/jmap/src/api/management/dkim.rs @@ -6,7 +6,7 @@ use std::str::FromStr; -use common::{auth::AccessToken, config::smtp::auth::simple_pem_parse}; +use common::{auth::AccessToken, config::smtp::auth::simple_pem_parse, Server}; use directory::{backend::internal::manage, Permission}; use hyper::Method; use mail_auth::{ @@ -21,12 +21,10 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use store::write::now; -use crate::{ - api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - JMAP, -}; +use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}; use super::decode_path_element; +use std::future::Future; #[derive(Debug, Serialize, Deserialize, Copy, Clone, PartialEq, Eq)] pub enum Algorithm { @@ -42,8 +40,36 @@ struct DkimSignature { selector: Option<String>, } -impl JMAP { - pub async fn handle_manage_dkim( +pub trait DkimManagement: Sync + Send { + fn handle_manage_dkim( + &self, + req: &HttpRequest, + path: Vec<&str>, + body: Option<Vec<u8>>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn handle_get_public_key( + &self, + path: Vec<&str>, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn handle_create_signature( + &self, + body: Option<Vec<u8>>, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn create_dkim_key( + &self, + algo: Algorithm, + id: impl AsRef<str> + Send, + domain: impl Into<String> + Send, + selector: impl Into<String> + Send, + ) -> impl Future<Output = trc::Result<()>> + Send; +} + +impl DkimManagement for Server { + async fn handle_manage_dkim( &self, req: &HttpRequest, path: Vec<&str>, diff --git a/crates/jmap/src/api/management/dns.rs b/crates/jmap/src/api/management/dns.rs index 65e56bb1..72a7a54e 100644 --- a/crates/jmap/src/api/management/dns.rs +++ b/crates/jmap/src/api/management/dns.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use directory::{ backend::internal::manage::{self}, Permission, @@ -17,27 +17,39 @@ use sha1::Digest; use utils::config::Config; use x509_parser::parse_x509_certificate; -use crate::{ - api::{ - http::ToHttpResponse, - management::dkim::{obtain_dkim_public_key, Algorithm}, - HttpRequest, HttpResponse, JsonResponse, - }, - JMAP, +use crate::api::{ + http::ToHttpResponse, + management::dkim::{obtain_dkim_public_key, Algorithm}, + HttpRequest, HttpResponse, JsonResponse, }; use super::decode_path_element; +use std::future::Future; #[derive(Debug, Serialize, Deserialize)] -struct DnsRecord { +pub struct DnsRecord { #[serde(rename = "type")] typ: String, name: String, content: String, } -impl JMAP { - pub async fn handle_manage_dns( +pub trait DnsManagement: Sync + Send { + fn handle_manage_dns( + &self, + req: &HttpRequest, + path: Vec<&str>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn build_dns_records( + &self, + domain_name: &str, + ) -> impl Future<Output = trc::Result<Vec<DnsRecord>>> + Send; +} + +impl DnsManagement for Server { + async fn handle_manage_dns( &self, req: &HttpRequest, path: Vec<&str>, @@ -210,7 +222,7 @@ impl JMAP { }); // Add MTA-STS records - if let Some(policy) = self.core.build_mta_sts_policy() { + if let Some(policy) = self.build_mta_sts_policy() { records.push(DnsRecord { typ: "CNAME".to_string(), name: format!("mta-sts.{domain_name}."), @@ -239,7 +251,7 @@ impl JMAP { }); // Add TLSA records - for (name, key) in self.core.tls.certificates.load().iter() { + for (name, key) in self.inner.data.tls_certificates.load().iter() { if !name.ends_with(domain_name) || name.starts_with("mta-sts.") || name.starts_with("autoconfig.") diff --git a/crates/jmap/src/api/management/enterprise/telemetry.rs b/crates/jmap/src/api/management/enterprise/telemetry.rs index b5b06e8f..6a1ff7b2 100644 --- a/crates/jmap/src/api/management/enterprise/telemetry.rs +++ b/crates/jmap/src/api/management/enterprise/telemetry.rs @@ -19,6 +19,7 @@ use common::{ metrics::store::{Metric, MetricsStore}, tracers::store::{TracingQuery, TracingStore}, }, + Server, }; use directory::{backend::internal::manage, Permission}; use http_body_util::{combinators::BoxBody, StreamBody}; @@ -28,6 +29,7 @@ use hyper::{ }; use mail_parser::DateTime; use serde_json::json; +use std::future::Future; use store::ahash::{AHashMap, AHashSet}; use trc::{ ipc::{bitset::Bitset, subscriber::SubscriberBuilder}, @@ -41,11 +43,20 @@ use crate::{ http::ToHttpResponse, management::Timestamp, HttpRequest, HttpResponse, HttpResponseBody, JsonResponse, }, - JMAP, + auth::oauth::token::TokenHandler, }; -impl JMAP { - pub async fn handle_telemetry_api_request( +pub trait TelemetryApi: Sync + Send { + fn handle_telemetry_api_request( + &self, + req: &HttpRequest, + path: Vec<&str>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl TelemetryApi for Server { + async fn handle_telemetry_api_request( &self, req: &HttpRequest, path: Vec<&str>, @@ -455,9 +466,9 @@ impl JMAP { ] { if metric_types.contains(&metric_type) { let value = match metric_type { - MetricType::QueueCount => self.core.total_queued_messages().await?, - MetricType::UserCount => self.core.total_accounts().await?, - MetricType::DomainCount => self.core.total_domains().await?, + MetricType::QueueCount => self.total_queued_messages().await?, + MetricType::UserCount => self.total_accounts().await?, + MetricType::DomainCount => self.total_domains().await?, _ => unreachable!(), }; Collector::update_gauge(metric_type, value); diff --git a/crates/jmap/src/api/management/enterprise/undelete.rs b/crates/jmap/src/api/management/enterprise/undelete.rs index e14de9ed..e3a40cef 100644 --- a/crates/jmap/src/api/management/enterprise/undelete.rs +++ b/crates/jmap/src/api/management/enterprise/undelete.rs @@ -11,12 +11,13 @@ use std::str::FromStr; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use common::{auth::AccessToken, enterprise::undelete::DeletedBlob}; +use common::{auth::AccessToken, enterprise::undelete::DeletedBlob, Server}; use directory::backend::internal::manage::ManageDirectory; use hyper::Method; use jmap_proto::types::collection::Collection; use mail_parser::{DateTime, MessageParser}; use serde_json::json; +use std::future::Future; use store::write::{BatchBuilder, BlobOp, ValueClass}; use trc::AddContext; use utils::{url_params::UrlParams, BlobHash}; @@ -27,9 +28,10 @@ use crate::{ management::decode_path_element, HttpRequest, HttpResponse, JsonResponse, }, - email::ingest::{IngestEmail, IngestSource}, + blob::download::BlobDownload, + email::ingest::{EmailIngest, IngestEmail, IngestSource}, mailbox::INBOX_ID, - JMAP, + JmapMethods, }; #[derive(serde::Deserialize, serde::Serialize)] @@ -52,8 +54,18 @@ pub enum UndeleteResponse { Error { reason: String }, } -impl JMAP { - pub async fn handle_undelete_api_request( +pub trait UndeleteApi: Sync + Send { + fn handle_undelete_api_request( + &self, + req: &HttpRequest, + path: Vec<&str>, + body: Option<Vec<u8>>, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl UndeleteApi for Server { + async fn handle_undelete_api_request( &self, req: &HttpRequest, path: Vec<&str>, diff --git a/crates/jmap/src/api/management/log.rs b/crates/jmap/src/api/management/log.rs index 94a87af2..ef4191e9 100644 --- a/crates/jmap/src/api/management/log.rs +++ b/crates/jmap/src/api/management/log.rs @@ -5,18 +5,16 @@ use std::{ }; use chrono::DateTime; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use directory::{backend::internal::manage, Permission}; use rev_lines::RevLines; use serde::Serialize; use serde_json::json; +use std::future::Future; use tokio::sync::oneshot; use utils::url_params::UrlParams; -use crate::{ - api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - JMAP, -}; +use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}; #[derive(Serialize)] struct LogEntry { @@ -27,8 +25,16 @@ struct LogEntry { details: String, } -impl JMAP { - pub async fn handle_view_logs( +pub trait LogManagement: Sync + Send { + fn handle_view_logs( + &self, + req: &HttpRequest, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl LogManagement for Server { + async fn handle_view_logs( &self, req: &HttpRequest, access_token: &AccessToken, diff --git a/crates/jmap/src/api/management/mod.rs b/crates/jmap/src/api/management/mod.rs index 0a3dc0c2..d7a0b704 100644 --- a/crates/jmap/src/api/management/mod.rs +++ b/crates/jmap/src/api/management/mod.rs @@ -19,15 +19,28 @@ pub mod stores; use std::{borrow::Cow, str::FromStr, sync::Arc}; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use directory::{backend::internal::manage, Permission}; +use dkim::DkimManagement; +use dns::DnsManagement; +use enterprise::telemetry::TelemetryApi; use hyper::Method; +use log::LogManagement; use mail_parser::DateTime; +use principal::PrincipalManager; +use queue::QueueManagement; +use reload::ManageReload; +use report::ManageReports; use serde::Serialize; +use settings::ManageSettings; +use sieve::SieveHandler; use store::write::now; +use stores::ManageStore; + +use crate::{auth::oauth::auth::OAuthApiHandler, email::crypto::CryptoHandler}; use super::{http::HttpSessionData, HttpRequest, HttpResponse}; -use crate::JMAP; +use std::future::Future; #[derive(Serialize)] #[serde(tag = "error")] @@ -53,9 +66,19 @@ pub enum ManagementApiError<'x> { }, } -impl JMAP { +pub trait ManagementApi: Sync + Send { + fn handle_api_manage_request( + &self, + req: &HttpRequest, + body: Option<Vec<u8>>, + access_token: Arc<AccessToken>, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl ManagementApi for Server { #[allow(unused_variables)] - pub async fn handle_api_manage_request( + async fn handle_api_manage_request( &self, req: &HttpRequest, body: Option<Vec<u8>>, diff --git a/crates/jmap/src/api/management/principal.rs b/crates/jmap/src/api/management/principal.rs index 51994756..764414de 100644 --- a/crates/jmap/src/api/management/principal.rs +++ b/crates/jmap/src/api/management/principal.rs @@ -6,7 +6,7 @@ use std::sync::{atomic::Ordering, Arc}; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use directory::{ backend::internal::{ lookup::DirectoryStore, @@ -21,12 +21,10 @@ use serde_json::json; use trc::AddContext; use utils::url_params::UrlParams; -use crate::{ - api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - JMAP, -}; +use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}; use super::decode_path_element; +use std::future::Future; #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(tag = "type")] @@ -47,8 +45,32 @@ pub struct AccountAuthResponse { pub app_passwords: Vec<String>, } -impl JMAP { - pub async fn handle_manage_principal( +pub trait PrincipalManager: Sync + Send { + fn handle_manage_principal( + &self, + req: &HttpRequest, + path: Vec<&str>, + body: Option<Vec<u8>>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn handle_account_auth_get( + &self, + access_token: Arc<AccessToken>, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn handle_account_auth_post( + &self, + req: &HttpRequest, + access_token: Arc<AccessToken>, + body: Option<Vec<u8>>, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn assert_supported_directory(&self) -> trc::Result<()>; +} + +impl PrincipalManager for Server { + async fn handle_manage_principal( &self, req: &HttpRequest, path: Vec<&str>, @@ -297,13 +319,16 @@ impl JMAP { } // Remove entries from cache - self.inner.sessions.retain(|_, id| id.item != account_id); + self.inner + .data + .http_auth_cache + .retain(|_, id| id.item != account_id); if matches!(typ, Type::Role | Type::Tenant) { // Update permissions cache - self.core.security.permissions.clear(); - self.core - .security + self.inner.data.permissions.clear(); + self.inner + .data .permissions_version .fetch_add(1, Ordering::Relaxed); } @@ -399,20 +424,23 @@ impl JMAP { if expire_session { // Remove entries from cache - self.inner.sessions.retain(|_, id| id.item != account_id); + self.inner + .data + .http_auth_cache + .retain(|_, id| id.item != account_id); } if is_role_change { // Update permissions cache - self.core.security.permissions.clear(); - self.core - .security + self.inner.data.permissions.clear(); + self.inner + .data .permissions_version .fetch_add(1, Ordering::Relaxed); } if expire_token { - self.core.security.access_tokens.remove(&account_id); + self.inner.data.access_tokens.remove(&account_id); } Ok(JsonResponse::new(json!({ @@ -428,7 +456,7 @@ impl JMAP { } } - pub async fn handle_account_auth_get( + async fn handle_account_auth_get( &self, access_token: Arc<AccessToken>, ) -> trc::Result<HttpResponse> { @@ -463,7 +491,7 @@ impl JMAP { .into_http_response()) } - pub async fn handle_account_auth_post( + async fn handle_account_auth_post( &self, req: &HttpRequest, access_token: Arc<AccessToken>, @@ -513,7 +541,10 @@ impl JMAP { .await?; // Remove entries from cache - self.inner.sessions.retain(|_, id| id.item != u32::MAX); + self.inner + .data + .http_auth_cache + .retain(|_, id| id.item != u32::MAX); return Ok(JsonResponse::new(json!({ "data": (), @@ -578,7 +609,8 @@ impl JMAP { // Remove entries from cache self.inner - .sessions + .data + .http_auth_cache .retain(|_, id| id.item != access_token.primary_id()); Ok(JsonResponse::new(json!({ @@ -587,7 +619,7 @@ impl JMAP { .into_http_response()) } - pub fn assert_supported_directory(&self) -> trc::Result<()> { + fn assert_supported_directory(&self) -> trc::Result<()> { let class = match &self.core.storage.directory.store { DirectoryInner::Internal(_) => return Ok(()), DirectoryInner::Ldap(_) => "LDAP", diff --git a/crates/jmap/src/api/management/queue.rs b/crates/jmap/src/api/management/queue.rs index 596abf65..c19bc707 100644 --- a/crates/jmap/src/api/management/queue.rs +++ b/crates/jmap/src/api/management/queue.rs @@ -4,8 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use std::future::Future; + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use common::auth::AccessToken; +use common::{auth::AccessToken, ipc::QueueEvent, Server}; use directory::{ backend::internal::{manage::ManageDirectory, PrincipalField}, Permission, Type, @@ -19,7 +21,10 @@ use mail_auth::{ use mail_parser::DateTime; use serde::{Deserializer, Serializer}; use serde_json::json; -use smtp::queue::{self, ErrorDetails, HostResponse, QueueId, Status}; +use smtp::{ + queue::{self, spool::SmtpSpool, ErrorDetails, HostResponse, QueueId, Status}, + reporting::{dmarc::DmarcReporting, tls::TlsReporting}, +}; use store::{ write::{key::DeserializeBigEndian, now, Bincode, QueueClass, ReportEvent, ValueClass}, Deserialize, IterateParams, ValueKey, @@ -27,10 +32,7 @@ use store::{ use trc::AddContext; use utils::url_params::UrlParams; -use crate::{ - api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - JMAP, -}; +use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}; use super::{decode_path_element, FutureTimestamp}; @@ -106,8 +108,17 @@ pub enum Report { }, } -impl JMAP { - pub async fn handle_manage_queue( +pub trait QueueManagement: Sync + Send { + fn handle_manage_queue( + &self, + req: &HttpRequest, + path: Vec<&str>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl QueueManagement for Server { + async fn handle_manage_queue( &self, req: &HttpRequest, path: Vec<&str>, @@ -270,7 +281,6 @@ impl JMAP { access_token.assert_has_permission(Permission::MessageQueueGet)?; if let Some(message) = self - .smtp .read_message(queue_id.parse().unwrap_or_default()) .await .filter(|message| { @@ -298,7 +308,6 @@ impl JMAP { let item = params.get("filter"); if let Some(mut message) = self - .smtp .read_message(queue_id.parse().unwrap_or_default()) .await .filter(|message| { @@ -329,9 +338,9 @@ impl JMAP { if found { let next_event = message.next_event().unwrap_or_default(); message - .save_changes(&self.smtp, prev_event.into(), next_event.into()) + .save_changes(self, prev_event.into(), next_event.into()) .await; - let _ = self.smtp.inner.queue_tx.send(queue::Event::Reload).await; + let _ = self.inner.ipc.queue_tx.send(QueueEvent::Reload).await; } Ok(JsonResponse::new(json!({ @@ -347,7 +356,6 @@ impl JMAP { access_token.assert_has_permission(Permission::MessageQueueDelete)?; if let Some(mut message) = self - .smtp .read_message(queue_id.parse().unwrap_or_default()) .await .filter(|message| { @@ -411,14 +419,14 @@ impl JMAP { }) { let next_event = message.next_event().unwrap_or_default(); message - .save_changes(&self.smtp, next_event.into(), prev_event.into()) + .save_changes(self, next_event.into(), prev_event.into()) .await; } else { - message.remove(&self.smtp, prev_event).await; + message.remove(self, prev_event).await; } } } else { - message.remove(&self.smtp, prev_event).await; + message.remove(self, prev_event).await; found = true; } @@ -528,7 +536,6 @@ impl JMAP { { let mut rua = Vec::new(); if let Some(report) = self - .smtp .generate_dmarc_aggregate_report(&event, &mut rua, None, 0) .await? { @@ -542,7 +549,6 @@ impl JMAP { { let mut rua = Vec::new(); if let Some(report) = self - .smtp .generate_tls_aggregate_report(&[event.clone()], &mut rua, None, 0) .await? { @@ -573,7 +579,7 @@ impl JMAP { .as_ref() .map_or(true, |domains| domains.contains(&event.domain)) => { - self.smtp.delete_dmarc_report(event).await; + self.delete_dmarc_report(event).await; true } QueueClass::TlsReportHeader(event) @@ -581,7 +587,7 @@ impl JMAP { .as_ref() .map_or(true, |domains| domains.contains(&event.domain)) => { - self.smtp.delete_tls_report(vec![event]).await; + self.delete_tls_report(vec![event]).await; true } _ => false, diff --git a/crates/jmap/src/api/management/reload.rs b/crates/jmap/src/api/management/reload.rs index f396e1b6..4c4fe110 100644 --- a/crates/jmap/src/api/management/reload.rs +++ b/crates/jmap/src/api/management/reload.rs @@ -4,20 +4,36 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, ipc::HousekeeperEvent, Server}; use directory::Permission; use hyper::Method; use serde_json::json; +use std::future::Future; use utils::url_params::UrlParams; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - services::housekeeper::Event, - JMAP, + JmapMethods, }; -impl JMAP { - pub async fn handle_manage_reload( +pub trait ManageReload: Sync + Send { + fn handle_manage_reload( + &self, + req: &HttpRequest, + path: Vec<&str>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn handle_manage_update( + &self, + req: &HttpRequest, + path: Vec<&str>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl ManageReload for Server { + async fn handle_manage_reload( &self, req: &HttpRequest, path: Vec<&str>, @@ -28,10 +44,10 @@ impl JMAP { match (path.get(1).copied(), req.method()) { (Some("lookup"), &Method::GET) => { - let result = self.core.reload_lookups().await?; + let result = self.reload_lookups().await?; // Update core if let Some(core) = result.new_core { - self.shared_core.store(core.into()); + self.inner.shared_core.store(core.into()); } Ok(JsonResponse::new(json!({ @@ -40,13 +56,14 @@ impl JMAP { .into_http_response()) } (Some("certificate"), &Method::GET) => Ok(JsonResponse::new(json!({ - "data": self.core.reload_certificates().await?.config, + "data": self.reload_certificates().await?.config, })) .into_http_response()), (Some("server.blocked-ip"), &Method::GET) => { - let result = self.core.reload_blocked_ips().await?; + let result = self.reload_blocked_ips().await?; + // Increment version counter - self.core.network.blocked_ips.increment_version(); + self.increment_blocked_version(); Ok(JsonResponse::new(json!({ "data": result.config, @@ -54,28 +71,29 @@ impl JMAP { .into_http_response()) } (_, &Method::GET) => { - let result = self.core.reload().await?; + let result = self.reload().await?; if !UrlParams::new(req.uri().query()).has_key("dry-run") { if let Some(core) = result.new_core { // Update core - self.shared_core.store(core.into()); + self.inner.shared_core.store(core.into()); // Increment version counter - self.inner.increment_config_version(); + self.increment_config_version(); } if let Some(tracers) = result.tracers { // Update tracers #[cfg(feature = "enterprise")] - tracers.update(self.shared_core.load().is_enterprise_edition()); + tracers.update(self.inner.shared_core.load().is_enterprise_edition()); #[cfg(not(feature = "enterprise"))] tracers.update(false); } // Reload settings self.inner + .ipc .housekeeper_tx - .send(Event::ReloadSettings) + .send(HousekeeperEvent::ReloadSettings) .await .map_err(|err| { trc::EventType::Server(trc::ServerEvent::ThreadError) @@ -94,7 +112,7 @@ impl JMAP { } } - pub async fn handle_manage_update( + async fn handle_manage_update( &self, req: &HttpRequest, path: Vec<&str>, @@ -119,7 +137,11 @@ impl JMAP { // Validate the access token access_token.assert_has_permission(Permission::UpdateWebadmin)?; - self.inner.webadmin.update_and_unpack(&self.core).await?; + self.inner + .data + .webadmin + .update_and_unpack(&self.core) + .await?; Ok(JsonResponse::new(json!({ "data": (), diff --git a/crates/jmap/src/api/management/report.rs b/crates/jmap/src/api/management/report.rs index daa91451..5540d41b 100644 --- a/crates/jmap/src/api/management/report.rs +++ b/crates/jmap/src/api/management/report.rs @@ -4,7 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use std::future::Future; + +use common::{auth::AccessToken, Server}; use directory::{ backend::internal::{manage::ManageDirectory, PrincipalField}, Permission, Type, @@ -23,10 +25,7 @@ use store::{ use trc::AddContext; use utils::url_params::UrlParams; -use crate::{ - api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - JMAP, -}; +use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}; use super::decode_path_element; @@ -36,8 +35,17 @@ enum ReportType { Arf, } -impl JMAP { - pub async fn handle_manage_reports( +pub trait ManageReports: Sync + Send { + fn handle_manage_reports( + &self, + req: &HttpRequest, + path: Vec<&str>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl ManageReports for Server { + async fn handle_manage_reports( &self, req: &HttpRequest, path: Vec<&str>, diff --git a/crates/jmap/src/api/management/settings.rs b/crates/jmap/src/api/management/settings.rs index 6844c503..ccabef51 100644 --- a/crates/jmap/src/api/management/settings.rs +++ b/crates/jmap/src/api/management/settings.rs @@ -4,19 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use directory::Permission; use hyper::Method; use serde_json::json; use store::ahash::AHashMap; use utils::{config::ConfigKey, map::vec_map::VecMap, url_params::UrlParams}; -use crate::{ - api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - JMAP, -}; +use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}; use super::decode_path_element; +use std::future::Future; #[derive(Debug, serde::Serialize, serde::Deserialize)] #[serde(tag = "type")] @@ -34,8 +32,18 @@ pub enum UpdateSettings { }, } -impl JMAP { - pub async fn handle_manage_settings( +pub trait ManageSettings: Sync + Send { + fn handle_manage_settings( + &self, + req: &HttpRequest, + path: Vec<&str>, + body: Option<Vec<u8>>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl ManageSettings for Server { + async fn handle_manage_settings( &self, req: &HttpRequest, path: Vec<&str>, diff --git a/crates/jmap/src/api/management/sieve.rs b/crates/jmap/src/api/management/sieve.rs index eb918185..85159378 100644 --- a/crates/jmap/src/api/management/sieve.rs +++ b/crates/jmap/src/api/management/sieve.rs @@ -6,18 +6,16 @@ use std::time::SystemTime; -use common::{auth::AccessToken, scripts::ScriptModification, IntoString}; +use common::{auth::AccessToken, scripts::ScriptModification, IntoString, Server}; use directory::Permission; use hyper::Method; use serde_json::json; use sieve::{runtime::Variable, Envelope}; -use smtp::scripts::{ScriptParameters, ScriptResult}; +use smtp::scripts::{event_loop::RunScript, ScriptParameters, ScriptResult}; +use std::future::Future; use utils::url_params::UrlParams; -use crate::{ - api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, - JMAP, -}; +use crate::api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}; #[derive(Debug, serde::Serialize)] #[serde(tag = "action")] @@ -36,8 +34,18 @@ pub enum Response { Discard, } -impl JMAP { - pub async fn handle_run_sieve( +pub trait SieveHandler: Sync + Send { + fn handle_run_sieve( + &self, + req: &HttpRequest, + path: Vec<&str>, + body: Option<Vec<u8>>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl SieveHandler for Server { + async fn handle_run_sieve( &self, req: &HttpRequest, path: Vec<&str>, @@ -103,7 +111,7 @@ impl JMAP { } // Run script - let result = match self.smtp.run_script(script_id, script, params, 0).await { + let result = match self.run_script(script_id, script, params, 0).await { ScriptResult::Accept { modifications } => Response::Accept { modifications }, ScriptResult::Replace { message, diff --git a/crates/jmap/src/api/management/stores.rs b/crates/jmap/src/api/management/stores.rs index b86c88be..0b9dea48 100644 --- a/crates/jmap/src/api/management/stores.rs +++ b/crates/jmap/src/api/management/stores.rs @@ -5,7 +5,12 @@ */ use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; -use common::{auth::AccessToken, manager::webadmin::Resource}; +use common::{ + auth::AccessToken, + ipc::{HousekeeperEvent, PurgeType}, + manager::webadmin::Resource, + Server, +}; use directory::{ backend::internal::manage::{self, ManageDirectory}, Permission, @@ -19,14 +24,30 @@ use crate::{ http::{HttpSessionData, ToHttpResponse}, HttpRequest, HttpResponse, JsonResponse, }, - services::housekeeper::{Event, PurgeType}, - JMAP, + services::index::Indexer, }; -use super::decode_path_element; +use super::{decode_path_element, enterprise::undelete::UndeleteApi}; +use std::future::Future; + +pub trait ManageStore: Sync + Send { + fn handle_manage_store( + &self, + req: &HttpRequest, + path: Vec<&str>, + body: Option<Vec<u8>>, + session: &HttpSessionData, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn housekeeper_request( + &self, + event: HousekeeperEvent, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} -impl JMAP { - pub async fn handle_manage_store( +impl ManageStore for Server { + async fn handle_manage_store( &self, req: &HttpRequest, path: Vec<&str>, @@ -75,7 +96,7 @@ impl JMAP { // Validate the access token access_token.assert_has_permission(Permission::PurgeBlobStore)?; - self.housekeeper_request(Event::Purge(PurgeType::Blobs { + self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Blobs { store: self.core.storage.data.clone(), blob_store: self.core.storage.blob.clone(), })) @@ -95,7 +116,7 @@ impl JMAP { self.core.storage.data.clone() }; - self.housekeeper_request(Event::Purge(PurgeType::Data(store))) + self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Data(store))) .await } (Some("purge"), Some("lookup"), id, &Method::GET) => { @@ -112,7 +133,7 @@ impl JMAP { self.core.storage.lookup.clone() }; - self.housekeeper_request(Event::Purge(PurgeType::Lookup(store))) + self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Lookup(store))) .await } (Some("purge"), Some("account"), id, &Method::GET) => { @@ -131,7 +152,7 @@ impl JMAP { None }; - self.housekeeper_request(Event::Purge(PurgeType::Account(account_id))) + self.housekeeper_request(HousekeeperEvent::Purge(PurgeType::Account(account_id))) .await } (Some("reindex"), id, None, &Method::GET) => { @@ -192,12 +213,17 @@ impl JMAP { } } - async fn housekeeper_request(&self, event: Event) -> trc::Result<HttpResponse> { - self.inner.housekeeper_tx.send(event).await.map_err(|err| { - trc::EventType::Server(trc::ServerEvent::ThreadError) - .reason(err) - .details("Failed to send housekeeper event") - })?; + async fn housekeeper_request(&self, event: HousekeeperEvent) -> trc::Result<HttpResponse> { + self.inner + .ipc + .housekeeper_tx + .send(event) + .await + .map_err(|err| { + trc::EventType::Server(trc::ServerEvent::ThreadError) + .reason(err) + .details("Failed to send housekeeper event") + })?; Ok(JsonResponse::new(json!({ "data": (), diff --git a/crates/jmap/src/api/mod.rs b/crates/jmap/src/api/mod.rs index 2d9fee0e..0ee7ef52 100644 --- a/crates/jmap/src/api/mod.rs +++ b/crates/jmap/src/api/mod.rs @@ -4,15 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; +use common::Inner; use hyper::StatusCode; use jmap_proto::types::{id::Id, state::State, type_state::DataType}; use serde::Serialize; use utils::map::vec_map::VecMap; -use crate::JmapInstance; - pub mod autoconfig; pub mod event_source; pub mod http; @@ -22,11 +21,11 @@ pub mod session; #[derive(Clone)] pub struct JmapSessionManager { - pub inner: JmapInstance, + pub inner: Arc<Inner>, } impl JmapSessionManager { - pub fn new(inner: JmapInstance) -> Self { + pub fn new(inner: Arc<Inner>) -> Self { Self { inner } } } diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 6d81b480..4b2a184a 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -6,7 +6,7 @@ use std::{sync::Arc, time::Instant}; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::{ get, query, @@ -18,12 +18,51 @@ use jmap_proto::{ }; use trc::JmapEvent; -use crate::JMAP; +use crate::{ + blob::{copy::BlobCopy, get::BlobOperations, upload::BlobUpload}, + changes::{get::ChangesLookup, query::QueryChanges}, + email::{ + copy::EmailCopy, get::EmailGet, import::EmailImport, parse::EmailParse, query::EmailQuery, + set::EmailSet, snippet::EmailSearchSnippet, + }, + identity::{get::IdentityGet, set::IdentitySet}, + mailbox::{get::MailboxGet, query::MailboxQuery, set::MailboxSet}, + principal::{get::PrincipalGet, query::PrincipalQuery}, + push::{get::PushSubscriptionFetch, set::PushSubscriptionSet}, + quota::{get::QuotaGet, query::QuotaQuery}, + services::state::StateManager, + sieve::{ + get::SieveScriptGet, query::SieveScriptQuery, set::SieveScriptSet, + validate::SieveScriptValidate, + }, + submission::{get::EmailSubmissionGet, query::EmailSubmissionQuery, set::EmailSubmissionSet}, + thread::get::ThreadGet, + vacation::{get::VacationResponseGet, set::VacationResponseSet}, +}; use super::http::HttpSessionData; +use std::future::Future; + +pub trait RequestHandler: Sync + Send { + fn handle_request( + &self, + request: Request, + access_token: Arc<AccessToken>, + session: &HttpSessionData, + ) -> impl Future<Output = Response> + Send; + + fn handle_method_call( + &self, + method: RequestMethod, + method_name: &'static str, + access_token: &AccessToken, + next_call: &mut Option<Call<RequestMethod>>, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<ResponseMethod>> + Send; +} -impl JMAP { - pub async fn handle_request( +impl RequestHandler for Server { + async fn handle_request( &self, request: Request, access_token: Arc<AccessToken>, diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index 1480487f..3c94a334 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -6,18 +6,27 @@ use std::sync::Arc; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ request::capability::{Capability, Session}, types::{acl::Acl, collection::Collection, id::Id}, }; +use std::future::Future; use trc::AddContext; -use crate::JMAP; +use crate::auth::acl::AclMethods; -impl JMAP { - pub async fn handle_session_resource( +pub trait SessionHandler: Sync + Send { + fn handle_session_resource( + &self, + base_url: String, + access_token: Arc<AccessToken>, + ) -> impl Future<Output = trc::Result<Session>> + Send; +} + +impl SessionHandler for Server { + async fn handle_session_resource( &self, base_url: String, access_token: Arc<AccessToken>, diff --git a/crates/jmap/src/auth/acl.rs b/crates/jmap/src/auth/acl.rs index c574da61..e7a40117 100644 --- a/crates/jmap/src/auth/acl.rs +++ b/crates/jmap/src/auth/acl.rs @@ -4,7 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use std::future::Future; + +use common::{auth::AccessToken, Server}; use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ error::set::SetError, @@ -25,10 +27,77 @@ use store::{ use trc::AddContext; use utils::map::bitmap::Bitmap; -use crate::JMAP; +use crate::JmapMethods; + +pub trait AclMethods: Sync + Send { + fn shared_documents( + &self, + access_token: &AccessToken, + to_account_id: u32, + to_collection: Collection, + check_acls: impl Into<Bitmap<Acl>> + Send, + ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send; + + fn shared_messages( + &self, + access_token: &AccessToken, + to_account_id: u32, + check_acls: impl Into<Bitmap<Acl>> + Send, + ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send; + + fn owned_or_shared_documents( + &self, + access_token: &AccessToken, + account_id: u32, + collection: Collection, + check_acls: impl Into<Bitmap<Acl>> + Send, + ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send; + + fn owned_or_shared_messages( + &self, + access_token: &AccessToken, + account_id: u32, + check_acls: impl Into<Bitmap<Acl>> + Send, + ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send; + + fn has_access_to_document( + &self, + access_token: &AccessToken, + to_account_id: u32, + to_collection: impl Into<u8> + Send, + to_document_id: u32, + check_acls: impl Into<Bitmap<Acl>> + Send, + ) -> impl Future<Output = trc::Result<bool>> + Send; + + fn acl_set( + &self, + changes: &mut Object<Value>, + current: Option<&HashedValue<Object<Value>>>, + acl_changes: MaybePatchValue, + ) -> impl Future<Output = Result<(), SetError>> + Send; -impl JMAP { - pub async fn shared_documents( + fn acl_get( + &self, + value: &[AclGrant], + access_token: &AccessToken, + account_id: u32, + ) -> impl Future<Output = Value> + Send; + + fn refresh_acls(&self, changes: &Object<Value>, current: &Option<HashedValue<Object<Value>>>); + + fn map_acl_set( + &self, + acl_set: Vec<Value>, + ) -> impl Future<Output = Result<Vec<AclGrant>, SetError>> + Send; + + fn map_acl_patch( + &self, + acl_patch: Vec<Value>, + ) -> impl Future<Output = Result<(AclGrant, Option<bool>), SetError>> + Send; +} + +impl AclMethods for Server { + async fn shared_documents( &self, access_token: &AccessToken, to_account_id: u32, @@ -66,7 +135,7 @@ impl JMAP { Ok(document_ids) } - pub async fn shared_messages( + async fn shared_messages( &self, access_token: &AccessToken, to_account_id: u32, @@ -97,7 +166,7 @@ impl JMAP { Ok(shared_messages) } - pub async fn owned_or_shared_documents( + async fn owned_or_shared_documents( &self, access_token: &AccessToken, account_id: u32, @@ -117,7 +186,7 @@ impl JMAP { Ok(document_ids) } - pub async fn owned_or_shared_messages( + async fn owned_or_shared_messages( &self, access_token: &AccessToken, account_id: u32, @@ -136,7 +205,7 @@ impl JMAP { Ok(document_ids) } - pub async fn has_access_to_document( + async fn has_access_to_document( &self, access_token: &AccessToken, to_account_id: u32, @@ -179,7 +248,7 @@ impl JMAP { Ok(false) } - pub async fn acl_set( + async fn acl_set( &self, changes: &mut Object<Value>, current: Option<&HashedValue<Object<Value>>>, @@ -251,7 +320,7 @@ impl JMAP { Ok(()) } - pub async fn acl_get( + async fn acl_get( &self, value: &[AclGrant], access_token: &AccessToken, @@ -287,13 +356,9 @@ impl JMAP { } } - pub fn refresh_acls( - &self, - changes: &Object<Value>, - current: &Option<HashedValue<Object<Value>>>, - ) { + fn refresh_acls(&self, changes: &Object<Value>, current: &Option<HashedValue<Object<Value>>>) { if let Value::Acl(acl_changes) = changes.get(&Property::Acl) { - let access_tokens = &self.core.security.access_tokens; + let access_tokens = &self.inner.data.access_tokens; if let Some(Value::Acl(acl_current)) = current .as_ref() .and_then(|current| current.inner.properties.get(&Property::Acl)) diff --git a/crates/jmap/src/auth/authenticate.rs b/crates/jmap/src/auth/authenticate.rs index 9bb5cc2d..8009ab6d 100644 --- a/crates/jmap/src/auth/authenticate.rs +++ b/crates/jmap/src/auth/authenticate.rs @@ -6,82 +6,101 @@ use std::{net::IpAddr, sync::Arc, time::Instant}; -use common::listener::limiter::InFlight; +use common::{listener::limiter::InFlight, Server}; use directory::Permission; use hyper::header; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use utils::map::ttl_dashmap::TtlMap; -use crate::{ - api::{http::HttpSessionData, HttpRequest}, - JMAP, -}; +use crate::api::{http::HttpSessionData, HttpRequest}; use common::auth::AccessToken; +use std::future::Future; -impl JMAP { - pub async fn authenticate_headers( +use super::{oauth::token::TokenHandler, rate_limit::RateLimiter}; + +pub trait Authenticator: Sync + Send { + fn authenticate_headers( + &self, + req: &HttpRequest, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<(InFlight, Arc<AccessToken>)>> + Send; + + fn cache_session(&self, session_id: String, access_token: &AccessToken); + + fn authenticate_plain( + &self, + username: &str, + secret: &str, + remote_ip: IpAddr, + session_id: u64, + ) -> impl Future<Output = trc::Result<AccessToken>> + Send; +} + +impl Authenticator for Server { + async fn authenticate_headers( &self, req: &HttpRequest, session: &HttpSessionData, ) -> trc::Result<(InFlight, Arc<AccessToken>)> { if let Some((mechanism, token)) = req.authorization() { - let access_token = if let Some(account_id) = self.inner.sessions.get_with_ttl(token) { - self.core.get_cached_access_token(account_id).await? - } else { - let access_token = if mechanism.eq_ignore_ascii_case("basic") { - // Enforce rate limit for authentication requests - self.is_auth_allowed_soft(&session.remote_ip).await?; - - // Decode the base64 encoded credentials - if let Some((account, secret)) = base64_decode(token.as_bytes()) - .and_then(|token| String::from_utf8(token).ok()) - .and_then(|token| { - token.split_once(':').map(|(login, secret)| { - (login.trim().to_lowercase(), secret.to_string()) + let access_token = + if let Some(account_id) = self.inner.data.http_auth_cache.get_with_ttl(token) { + self.get_cached_access_token(account_id).await? + } else { + let access_token = if mechanism.eq_ignore_ascii_case("basic") { + // Enforce rate limit for authentication requests + self.is_auth_allowed_soft(&session.remote_ip).await?; + + // Decode the base64 encoded credentials + if let Some((account, secret)) = base64_decode(token.as_bytes()) + .and_then(|token| String::from_utf8(token).ok()) + .and_then(|token| { + token.split_once(':').map(|(login, secret)| { + (login.trim().to_lowercase(), secret.to_string()) + }) }) - }) - { - self.authenticate_plain( - &account, - &secret, - session.remote_ip, - session.session_id, - ) - .await? + { + self.authenticate_plain( + &account, + &secret, + session.remote_ip, + session.session_id, + ) + .await? + } else { + return Err(trc::AuthEvent::Error + .into_err() + .details("Failed to decode Basic auth request.") + .id(token.to_string()) + .caused_by(trc::location!())); + } + } else if mechanism.eq_ignore_ascii_case("bearer") { + // Enforce anonymous rate limit for bearer auth requests + self.is_anonymous_allowed(&session.remote_ip).await?; + + let (account_id, _, _) = + self.validate_access_token("access_token", token).await?; + + self.get_access_token(account_id).await? } else { + // Enforce anonymous rate limit + self.is_anonymous_allowed(&session.remote_ip).await?; return Err(trc::AuthEvent::Error .into_err() - .details("Failed to decode Basic auth request.") - .id(token.to_string()) + .reason("Unsupported authentication mechanism.") + .details(token.to_string()) .caused_by(trc::location!())); - } - } else if mechanism.eq_ignore_ascii_case("bearer") { - // Enforce anonymous rate limit for bearer auth requests - self.is_anonymous_allowed(&session.remote_ip).await?; + }; - let (account_id, _, _) = - self.validate_access_token("access_token", token).await?; - - self.core.get_access_token(account_id).await? - } else { - // Enforce anonymous rate limit - self.is_anonymous_allowed(&session.remote_ip).await?; - return Err(trc::AuthEvent::Error - .into_err() - .reason("Unsupported authentication mechanism.") - .details(token.to_string()) - .caused_by(trc::location!())); + // Cache session + let access_token = Arc::new(access_token); + self.cache_session(token.to_string(), &access_token); + self.cache_access_token(access_token.clone()); + access_token }; - // Cache session - let access_token = Arc::new(access_token); - self.cache_session(token.to_string(), &access_token); - self.core.cache_access_token(access_token.clone()); - access_token - }; - // Enforce authenticated rate limit self.is_account_allowed(&access_token) .await @@ -97,15 +116,15 @@ impl JMAP { } } - pub fn cache_session(&self, session_id: String, access_token: &AccessToken) { - self.inner.sessions.insert_with_ttl( + fn cache_session(&self, session_id: String, access_token: &AccessToken) { + self.inner.data.http_auth_cache.insert_with_ttl( session_id, access_token.primary_id(), Instant::now() + self.core.jmap.session_cache_ttl, ); } - pub async fn authenticate_plain( + async fn authenticate_plain( &self, username: &str, secret: &str, @@ -113,7 +132,6 @@ impl JMAP { session_id: u64, ) -> trc::Result<AccessToken> { match self - .core .authenticate( &self.core.storage.directory, session_id, @@ -126,15 +144,11 @@ impl JMAP { ) .await { - Ok(principal) => self - .core - .build_access_token(principal) - .await - .and_then(|token| { - token - .assert_has_permission(Permission::Authenticate) - .map(|_| token) - }), + Ok(principal) => self.build_access_token(principal).await.and_then(|token| { + token + .assert_has_permission(Permission::Authenticate) + .map(|_| token) + }), Err(err) => { if !err.matches(trc::EventType::Auth(trc::AuthEvent::MissingTotp)) { let _ = self.is_auth_allowed_hard(&remote_ip).await; diff --git a/crates/jmap/src/auth/oauth/auth.rs b/crates/jmap/src/auth/oauth/auth.rs index 78f53454..4274dc17 100644 --- a/crates/jmap/src/auth/oauth/auth.rs +++ b/crates/jmap/src/auth/oauth/auth.rs @@ -6,9 +6,10 @@ use std::sync::Arc; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use rand::distributions::Standard; use serde_json::json; +use std::future::Future; use store::{ rand::{distributions::Alphanumeric, thread_rng, Rng}, write::Bincode, @@ -18,7 +19,6 @@ use store::{ use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, auth::oauth::OAuthStatus, - JMAP, }; use super::{ @@ -26,8 +26,23 @@ use super::{ MAX_POST_LEN, USER_CODE_ALPHABET, USER_CODE_LEN, }; -impl JMAP { - pub async fn handle_oauth_api_request( +pub trait OAuthApiHandler: Sync + Send { + fn handle_oauth_api_request( + &self, + access_token: Arc<AccessToken>, + body: Option<Vec<u8>>, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn handle_device_auth( + &self, + req: &mut HttpRequest, + base_url: impl AsRef<str> + Send, + session_id: u64, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl OAuthApiHandler for Server { + async fn handle_oauth_api_request( &self, access_token: Arc<AccessToken>, body: Option<Vec<u8>>, @@ -143,7 +158,7 @@ impl JMAP { Ok(JsonResponse::new(response).into_http_response()) } - pub async fn handle_device_auth( + async fn handle_device_auth( &self, req: &mut HttpRequest, base_url: impl AsRef<str>, diff --git a/crates/jmap/src/auth/oauth/mod.rs b/crates/jmap/src/auth/oauth/mod.rs index 0a83063a..7c9e7337 100644 --- a/crates/jmap/src/auth/oauth/mod.rs +++ b/crates/jmap/src/auth/oauth/mod.rs @@ -202,7 +202,7 @@ pub struct FormData { } impl FormData { - pub async fn from_request( + async fn from_request( req: &mut HttpRequest, max_len: usize, session_id: u64, diff --git a/crates/jmap/src/auth/oauth/token.rs b/crates/jmap/src/auth/oauth/token.rs index 590a9c4d..ec23b3c4 100644 --- a/crates/jmap/src/auth/oauth/token.rs +++ b/crates/jmap/src/auth/oauth/token.rs @@ -6,10 +6,12 @@ use std::time::SystemTime; +use common::Server; use directory::{backend::internal::PrincipalField, QueryBy}; use hyper::StatusCode; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; +use std::future::Future; use store::{ blake3, rand::{thread_rng, Rng}, @@ -20,7 +22,6 @@ use utils::codec::leb128::{Leb128Iterator, Leb128Vec}; use crate::{ api::{http::ToHttpResponse, HttpRequest, HttpResponse, JsonResponse}, auth::SymmetricEncrypt, - JMAP, }; use super::{ @@ -28,9 +29,52 @@ use super::{ MAX_POST_LEN, RANDOM_CODE_LEN, }; -impl JMAP { +pub trait TokenHandler: Sync + Send { + fn handle_token_request( + &self, + req: &mut HttpRequest, + session_id: u64, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn password_hash( + &self, + account_id: u32, + ) -> impl Future<Output = Result<String, &'static str>> + Send; + + fn issue_token( + &self, + account_id: u32, + client_id: &str, + with_refresh_token: bool, + ) -> impl Future<Output = Result<OAuthResponse, &'static str>> + Send; + + fn issue_custom_token( + &self, + account_id: u32, + grant_type: &str, + client_id: &str, + expiry_in: u64, + ) -> impl Future<Output = trc::Result<String>> + Send; + + fn encode_access_token( + &self, + grant_type: &str, + account_id: u32, + password_hash: &str, + client_id: &str, + expiry_in: u64, + ) -> Result<String, &'static str>; + + fn validate_access_token( + &self, + grant_type: &str, + token_: &str, + ) -> impl Future<Output = trc::Result<(u32, String, u64)>> + Send; +} + +impl TokenHandler for Server { // Token endpoint - pub async fn handle_token_request( + async fn handle_token_request( &self, req: &mut HttpRequest, session_id: u64, @@ -199,7 +243,7 @@ impl JMAP { } } - pub async fn issue_token( + async fn issue_token( &self, account_id: u32, client_id: &str, @@ -233,7 +277,7 @@ impl JMAP { }) } - pub async fn issue_custom_token( + async fn issue_custom_token( &self, account_id: u32, grant_type: &str, @@ -303,7 +347,7 @@ impl JMAP { Ok(String::from_utf8(base64_encode(&token).unwrap_or_default()).unwrap()) } - pub async fn validate_access_token( + async fn validate_access_token( &self, grant_type: &str, token_: &str, diff --git a/crates/jmap/src/auth/rate_limit.rs b/crates/jmap/src/auth/rate_limit.rs index 25b1ec9b..6d7be622 100644 --- a/crates/jmap/src/auth/rate_limit.rs +++ b/crates/jmap/src/auth/rate_limit.rs @@ -6,23 +6,33 @@ use std::{net::IpAddr, sync::Arc}; -use common::listener::limiter::{ConcurrencyLimiter, InFlight}; +use common::{ + listener::limiter::{ConcurrencyLimiter, InFlight}, + ConcurrencyLimiters, Server, +}; use directory::Permission; use trc::AddContext; -use crate::JMAP; - use common::auth::AccessToken; +use std::future::Future; -pub struct ConcurrencyLimiters { - pub concurrent_requests: ConcurrencyLimiter, - pub concurrent_uploads: ConcurrencyLimiter, +pub trait RateLimiter: Sync + Send { + fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters>; + fn is_account_allowed( + &self, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<InFlight>> + Send; + fn is_anonymous_allowed(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send; + fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight>; + fn is_auth_allowed_soft(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send; + fn is_auth_allowed_hard(&self, addr: &IpAddr) -> impl Future<Output = trc::Result<()>> + Send; } -impl JMAP { - pub fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters> { +impl RateLimiter for Server { + fn get_concurrency_limiter(&self, account_id: u32) -> Arc<ConcurrencyLimiters> { self.inner - .concurrency_limiter + .data + .jmap_limiter .get(&account_id) .map(|limiter| limiter.clone()) .unwrap_or_else(|| { @@ -35,13 +45,14 @@ impl JMAP { ), }); self.inner - .concurrency_limiter + .data + .jmap_limiter .insert(account_id, limiter.clone()); limiter }) } - pub async fn is_account_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> { + async fn is_account_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> { let limiter = self.get_concurrency_limiter(access_token.primary_id()); let is_rate_allowed = if let Some(rate) = &self.core.jmap.rate_authenticated { self.core @@ -74,7 +85,7 @@ impl JMAP { } } - pub async fn is_anonymous_allowed(&self, addr: &IpAddr) -> trc::Result<()> { + async fn is_anonymous_allowed(&self, addr: &IpAddr) -> trc::Result<()> { if let Some(rate) = &self.core.jmap.rate_anonymous { if self .core @@ -91,7 +102,7 @@ impl JMAP { Ok(()) } - pub fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> { + fn is_upload_allowed(&self, access_token: &AccessToken) -> trc::Result<InFlight> { if let Some(in_flight_request) = self .get_concurrency_limiter(access_token.primary_id()) .concurrent_uploads @@ -105,7 +116,7 @@ impl JMAP { } } - pub async fn is_auth_allowed_soft(&self, addr: &IpAddr) -> trc::Result<()> { + async fn is_auth_allowed_soft(&self, addr: &IpAddr) -> trc::Result<()> { if let Some(rate) = &self.core.jmap.rate_authenticate_req { if self .core @@ -122,7 +133,7 @@ impl JMAP { Ok(()) } - pub async fn is_auth_allowed_hard(&self, addr: &IpAddr) -> trc::Result<()> { + async fn is_auth_allowed_hard(&self, addr: &IpAddr) -> trc::Result<()> { if let Some(rate) = &self.core.jmap.rate_authenticate_req { if self .core @@ -139,9 +150,3 @@ impl JMAP { Ok(()) } } - -impl ConcurrencyLimiters { - pub fn is_active(&self) -> bool { - self.concurrent_requests.is_active() || self.concurrent_uploads.is_active() - } -} diff --git a/crates/jmap/src/blob/copy.rs b/crates/jmap/src/blob/copy.rs index 635b6bc0..19aa6df0 100644 --- a/crates/jmap/src/blob/copy.rs +++ b/crates/jmap/src/blob/copy.rs @@ -4,23 +4,34 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::copy::{CopyBlobRequest, CopyBlobResponse}, types::blob::BlobId, }; +use std::future::Future; use store::{ write::{now, BatchBuilder, BlobOp}, BlobClass, Serialize, }; use utils::map::vec_map::VecMap; -use crate::JMAP; +use crate::JmapMethods; -impl JMAP { - pub async fn blob_copy( +use super::download::BlobDownload; + +pub trait BlobCopy: Sync + Send { + fn blob_copy( + &self, + request: CopyBlobRequest, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<CopyBlobResponse>> + Send; +} + +impl BlobCopy for Server { + async fn blob_copy( &self, request: CopyBlobRequest, access_token: &AccessToken, diff --git a/crates/jmap/src/blob/download.rs b/crates/jmap/src/blob/download.rs index 8b289e59..19534b07 100644 --- a/crates/jmap/src/blob/download.rs +++ b/crates/jmap/src/blob/download.rs @@ -6,7 +6,7 @@ use std::ops::Range; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::types::{ acl::Acl, blob::{BlobId, BlobSection}, @@ -16,15 +16,42 @@ use mail_parser::{ decoders::{base64::base64_decode, quoted_printable::quoted_printable_decode}, Encoding, }; +use std::future::Future; use store::BlobClass; use trc::AddContext; use utils::BlobHash; -use crate::JMAP; +use crate::auth::acl::AclMethods; -impl JMAP { +pub trait BlobDownload: Sync + Send { + fn blob_download( + &self, + blob_id: &BlobId, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send; + + fn get_blob_section( + &self, + hash: &BlobHash, + section: &BlobSection, + ) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send; + + fn get_blob( + &self, + hash: &BlobHash, + range: Range<usize>, + ) -> impl Future<Output = trc::Result<Option<Vec<u8>>>> + Send; + + fn has_access_blob( + &self, + blob_id: &BlobId, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<bool>> + Send; +} + +impl BlobDownload for Server { #[allow(clippy::blocks_in_conditions)] - pub async fn blob_download( + async fn blob_download( &self, blob_id: &BlobId, access_token: &AccessToken, @@ -84,7 +111,7 @@ impl JMAP { } } - pub async fn get_blob_section( + async fn get_blob_section( &self, hash: &BlobHash, section: &BlobSection, @@ -102,11 +129,7 @@ impl JMAP { })) } - pub async fn get_blob( - &self, - hash: &BlobHash, - range: Range<usize>, - ) -> trc::Result<Option<Vec<u8>>> { + async fn get_blob(&self, hash: &BlobHash, range: Range<usize>) -> trc::Result<Option<Vec<u8>>> { self.core .storage .blob @@ -115,7 +138,7 @@ impl JMAP { .caused_by(trc::location!()) } - pub async fn has_access_blob( + async fn has_access_blob( &self, blob_id: &BlobId, access_token: &AccessToken, diff --git a/crates/jmap/src/blob/get.rs b/crates/jmap/src/blob/get.rs index 3add516d..b73e05c5 100644 --- a/crates/jmap/src/blob/get.rs +++ b/crates/jmap/src/blob/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::{ get::{GetRequest, GetResponse}, @@ -26,10 +26,26 @@ use sha2::{Sha256, Sha512}; use store::BlobClass; use utils::map::vec_map::VecMap; -use crate::{mailbox::UidMailbox, JMAP}; +use crate::{mailbox::UidMailbox, JmapMethods}; +use std::future::Future; -impl JMAP { - pub async fn blob_get( +use super::download::BlobDownload; + +pub trait BlobOperations: Sync + Send { + fn blob_get( + &self, + request: GetRequest<GetArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; + + fn blob_lookup( + &self, + request: BlobLookupRequest, + ) -> impl Future<Output = trc::Result<BlobLookupResponse>> + Send; +} + +impl BlobOperations for Server { + async fn blob_get( &self, mut request: GetRequest<GetArguments>, access_token: &AccessToken, @@ -148,7 +164,7 @@ impl JMAP { Ok(response) } - pub async fn blob_lookup(&self, request: BlobLookupRequest) -> trc::Result<BlobLookupResponse> { + async fn blob_lookup(&self, request: BlobLookupRequest) -> trc::Result<BlobLookupResponse> { let mut include_email = false; let mut include_mailbox = false; let mut include_thread = false; diff --git a/crates/jmap/src/blob/upload.rs b/crates/jmap/src/blob/upload.rs index dfa902dc..49255196 100644 --- a/crates/jmap/src/blob/upload.rs +++ b/crates/jmap/src/blob/upload.rs @@ -6,7 +6,7 @@ use std::sync::Arc; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use directory::Permission; use jmap_proto::{ error::set::SetError, @@ -23,16 +23,40 @@ use store::{ use trc::AddContext; use utils::BlobHash; -use crate::JMAP; +use crate::{auth::rate_limit::RateLimiter, JmapMethods}; -use super::UploadResponse; +use super::{download::BlobDownload, UploadResponse}; +use std::future::Future; #[cfg(feature = "test_mode")] pub static DISABLE_UPLOAD_QUOTA: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true); -impl JMAP { - pub async fn blob_upload_many( +pub trait BlobUpload: Sync + Send { + fn blob_upload_many( + &self, + request: BlobUploadRequest, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<BlobUploadResponse>> + Send; + + fn blob_upload( + &self, + account_id: Id, + content_type: &str, + data: &[u8], + access_token: Arc<AccessToken>, + ) -> impl Future<Output = trc::Result<UploadResponse>> + Send; + + fn put_blob( + &self, + account_id: u32, + data: &[u8], + set_quota: bool, + ) -> impl Future<Output = trc::Result<BlobId>> + Send; +} + +impl BlobUpload for Server { + async fn blob_upload_many( &self, request: BlobUploadRequest, access_token: &AccessToken, @@ -178,7 +202,7 @@ impl JMAP { Ok(response) } - pub async fn blob_upload( + async fn blob_upload( &self, account_id: Id, content_type: &str, @@ -239,12 +263,7 @@ impl JMAP { } #[allow(clippy::blocks_in_conditions)] - pub async fn put_blob( - &self, - account_id: u32, - data: &[u8], - set_quota: bool, - ) -> trc::Result<BlobId> { + async fn put_blob(&self, account_id: u32, data: &[u8], set_quota: bool) -> trc::Result<BlobId> { // First reserve the hash let hash = BlobHash::from(data); let mut batch = BatchBuilder::new(); diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs index cd68d95a..3da6ba15 100644 --- a/crates/jmap/src/changes/get.rs +++ b/crates/jmap/src/changes/get.rs @@ -4,18 +4,32 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::changes::{ChangesRequest, ChangesResponse, RequestArguments}, types::{collection::Collection, property::Property, state::State}, }; +use std::future::Future; use store::query::log::{Change, Changes, Query}; use trc::AddContext; -use crate::JMAP; +pub trait ChangesLookup: Sync + Send { + fn changes( + &self, + request: ChangesRequest, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<ChangesResponse>> + Send; + + fn changes_( + &self, + account_id: u32, + collection: Collection, + query: Query, + ) -> impl Future<Output = trc::Result<Changes>> + Send; +} -impl JMAP { - pub async fn changes( +impl ChangesLookup for Server { + async fn changes( &self, request: ChangesRequest, access_token: &AccessToken, @@ -160,7 +174,7 @@ impl JMAP { Ok(response) } - pub async fn changes_( + async fn changes_( &self, account_id: u32, collection: Collection, diff --git a/crates/jmap/src/changes/query.rs b/crates/jmap/src/changes/query.rs index a6d31831..bfcb1517 100644 --- a/crates/jmap/src/changes/query.rs +++ b/crates/jmap/src/changes/query.rs @@ -4,17 +4,31 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::method::{ changes::{self, ChangesRequest}, query::{self, QueryRequest}, query_changes::{AddedItem, QueryChangesRequest, QueryChangesResponse}, }; +use std::future::Future; -use crate::JMAP; +use crate::{ + email::query::EmailQuery, mailbox::query::MailboxQuery, quota::query::QuotaQuery, + submission::query::EmailSubmissionQuery, +}; + +use super::get::ChangesLookup; + +pub trait QueryChanges: Sync + Send { + fn query_changes( + &self, + request: QueryChangesRequest, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<QueryChangesResponse>> + Send; +} -impl JMAP { - pub async fn query_changes( +impl QueryChanges for Server { + async fn query_changes( &self, request: QueryChangesRequest, access_token: &AccessToken, diff --git a/crates/jmap/src/changes/state.rs b/crates/jmap/src/changes/state.rs index 96f75c0d..266ef2bc 100644 --- a/crates/jmap/src/changes/state.rs +++ b/crates/jmap/src/changes/state.rs @@ -4,16 +4,31 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use jmap_proto::types::{collection::Collection, state::State}; +use std::future::Future; use trc::AddContext; -use crate::JMAP; +pub trait StateManager: Sync + Send { + fn get_state( + &self, + account_id: u32, + collection: impl Into<u8> + Send, + ) -> impl Future<Output = trc::Result<State>> + Send; + + fn assert_state( + &self, + account_id: u32, + collection: Collection, + if_in_state: &Option<State>, + ) -> impl Future<Output = trc::Result<State>> + Send; +} -impl JMAP { - pub async fn get_state( +impl StateManager for Server { + async fn get_state( &self, account_id: u32, - collection: impl Into<u8>, + collection: impl Into<u8> + Send, ) -> trc::Result<State> { let collection = collection.into(); self.core @@ -25,7 +40,7 @@ impl JMAP { .map(State::from) } - pub async fn assert_state( + async fn assert_state( &self, account_id: u32, collection: Collection, diff --git a/crates/jmap/src/changes/write.rs b/crates/jmap/src/changes/write.rs index a765a480..7074cc21 100644 --- a/crates/jmap/src/changes/write.rs +++ b/crates/jmap/src/changes/write.rs @@ -6,28 +6,47 @@ use std::time::Duration; +use common::Server; use jmap_proto::types::collection::Collection; +use std::future::Future; use store::{ write::{log::ChangeLogBuilder, BatchBuilder}, LogKey, }; use trc::AddContext; -use crate::JMAP; +pub trait ChangeLog: Sync + Send { + fn begin_changes( + &self, + account_id: u32, + ) -> impl Future<Output = trc::Result<ChangeLogBuilder>> + Send; + fn assign_change_id(&self, account_id: u32) -> impl Future<Output = trc::Result<u64>> + Send; + fn generate_snowflake_id(&self) -> trc::Result<u64>; + fn commit_changes( + &self, + account_id: u32, + changes: ChangeLogBuilder, + ) -> impl Future<Output = trc::Result<u64>> + Send; + fn delete_changes( + &self, + account_id: u32, + before: Duration, + ) -> impl Future<Output = trc::Result<()>> + Send; +} -impl JMAP { - pub async fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> { +impl ChangeLog for Server { + async fn begin_changes(&self, account_id: u32) -> trc::Result<ChangeLogBuilder> { self.assign_change_id(account_id) .await .map(ChangeLogBuilder::with_change_id) } - pub async fn assign_change_id(&self, _: u32) -> trc::Result<u64> { + async fn assign_change_id(&self, _: u32) -> trc::Result<u64> { self.generate_snowflake_id() } - pub fn generate_snowflake_id(&self) -> trc::Result<u64> { - self.inner.snowflake_id.generate().ok_or_else(|| { + fn generate_snowflake_id(&self) -> trc::Result<u64> { + self.inner.data.jmap_id_gen.generate().ok_or_else(|| { trc::StoreEvent::UnexpectedError .into_err() .caused_by(trc::location!()) @@ -35,7 +54,7 @@ impl JMAP { }) } - pub async fn commit_changes( + async fn commit_changes( &self, account_id: u32, mut changes: ChangeLogBuilder, @@ -56,8 +75,8 @@ impl JMAP { .map(|_| state) } - pub async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> { - let reference_cid = self.inner.snowflake_id.past_id(before).ok_or_else(|| { + async fn delete_changes(&self, account_id: u32, before: Duration) -> trc::Result<()> { + let reference_cid = self.inner.data.jmap_id_gen.past_id(before).ok_or_else(|| { trc::StoreEvent::UnexpectedError .caused_by(trc::location!()) .ctx(trc::Key::Reason, "Failed to generate reference change id.") diff --git a/crates/jmap/src/email/cache.rs b/crates/jmap/src/email/cache.rs index 2d73c6a8..830ee312 100644 --- a/crates/jmap/src/email/cache.rs +++ b/crates/jmap/src/email/cache.rs @@ -4,25 +4,29 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{collections::HashMap, sync::Arc}; +use std::sync::Arc; +use common::{Server, Threads}; use jmap_proto::types::{collection::Collection, property::Property}; +use std::future::Future; use trc::AddContext; use utils::lru_cache::LruCached; -use crate::JMAP; +use crate::JmapMethods; -#[derive(Debug, Default)] -pub struct Threads { - pub threads: HashMap<u32, u32>, - pub modseq: Option<u64>, +pub trait ThreadCache: Sync + Send { + fn get_cached_thread_ids( + &self, + account_id: u32, + message_ids: impl Iterator<Item = u32> + Send, + ) -> impl Future<Output = trc::Result<Vec<(u32, u32)>>> + Send; } -impl JMAP { - pub async fn get_cached_thread_ids( +impl ThreadCache for Server { + async fn get_cached_thread_ids( &self, account_id: u32, - message_ids: impl Iterator<Item = u32>, + message_ids: impl Iterator<Item = u32> + Send, ) -> trc::Result<Vec<(u32, u32)>> { // Obtain current state let modseq = self @@ -34,8 +38,12 @@ impl JMAP { .caused_by(trc::location!())?; // Lock the cache - let thread_cache = if let Some(thread_cache) = - self.inner.cache_threads.get(&account_id).and_then(|t| { + let thread_cache = if let Some(thread_cache) = self + .inner + .data + .threads_cache + .get(&account_id) + .and_then(|t| { if t.modseq.unwrap_or(0) >= modseq.unwrap_or(0) { Some(t) } else { @@ -58,7 +66,8 @@ impl JMAP { modseq, }); self.inner - .cache_threads + .data + .threads_cache .insert(account_id, thread_cache.clone()); thread_cache }; diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs index 472df5d0..bc7aa75b 100644 --- a/crates/jmap/src/email/copy.rs +++ b/crates/jmap/src/email/copy.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::{AccessToken, ResourceToken}; +use common::{ + auth::{AccessToken, ResourceToken}, + Server, +}; use jmap_proto::{ error::set::SetError, method::{ @@ -42,16 +45,46 @@ use store::{ use trc::AddContext; use utils::map::vec_map::VecMap; -use crate::{api::http::HttpSessionData, mailbox::UidMailbox, JMAP}; +use crate::{ + api::http::HttpSessionData, + auth::acl::AclMethods, + changes::{state::StateManager, write::ChangeLog}, + mailbox::{set::MailboxSet, UidMailbox}, + services::index::Indexer, + JmapMethods, +}; +use std::future::Future; use super::{ index::{EmailIndexBuilder, TrimTextValue, VisitValues, MAX_ID_LENGTH, MAX_SORT_FIELD_LENGTH}, - ingest::{IngestedEmail, LogEmailInsert}, + ingest::{EmailIngest, IngestedEmail, LogEmailInsert}, metadata::MessageMetadata, }; -impl JMAP { - pub async fn email_copy( +pub trait EmailCopy: Sync + Send { + fn email_copy( + &self, + request: CopyRequest<RequestArguments>, + access_token: &AccessToken, + next_call: &mut Option<Call<RequestMethod>>, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<CopyResponse>> + Send; + + #[allow(clippy::too_many_arguments)] + fn copy_message( + &self, + from_account_id: u32, + from_message_id: u32, + resource_token: &ResourceToken, + mailboxes: Vec<u32>, + keywords: Vec<Keyword>, + received_at: Option<UTCDate>, + session_id: u64, + ) -> impl Future<Output = trc::Result<Result<IngestedEmail, SetError>>> + Send; +} + +impl EmailCopy for Server { + async fn email_copy( &self, request: CopyRequest<RequestArguments>, access_token: &AccessToken, @@ -271,7 +304,7 @@ impl JMAP { } #[allow(clippy::too_many_arguments)] - pub async fn copy_message( + async fn copy_message( &self, from_account_id: u32, from_message_id: u32, @@ -435,7 +468,7 @@ impl JMAP { let document_id = ids.last_document_id().caused_by(trc::location!())?; // Request FTS index - self.inner.request_fts_index(); + self.request_fts_index(); // Update response email.id = Id::from_parts(thread_id, document_id); diff --git a/crates/jmap/src/email/crypto.rs b/crates/jmap/src/email/crypto.rs index 815de86d..758177a5 100644 --- a/crates/jmap/src/email/crypto.rs +++ b/crates/jmap/src/email/crypto.rs @@ -4,14 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Cow, collections::BTreeSet, fmt::Display, io::Cursor, sync::Arc}; +use std::{ + borrow::Cow, collections::BTreeSet, fmt::Display, future::Future, io::Cursor, sync::Arc, +}; use crate::{ api::{http::ToHttpResponse, HttpResponse, JsonResponse}, - JMAP, + JmapMethods, }; use aes::cipher::{block_padding::Pkcs7, BlockEncryptMut, KeyIvInit}; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use directory::backend::internal::manage; use jmap_proto::types::{collection::Collection, property::Property}; use mail_builder::{encoders::base64::base64_encode_mime, mime::make_boundary}; @@ -628,11 +630,21 @@ impl ToBitmaps for &EncryptionParams { } } -impl JMAP { - pub async fn handle_crypto_get( +pub trait CryptoHandler: Sync + Send { + fn handle_crypto_get( &self, access_token: Arc<AccessToken>, - ) -> trc::Result<HttpResponse> { + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; + + fn handle_crypto_post( + &self, + access_token: Arc<AccessToken>, + body: Option<Vec<u8>>, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl CryptoHandler for Server { + async fn handle_crypto_get(&self, access_token: Arc<AccessToken>) -> trc::Result<HttpResponse> { let params = self .get_property::<EncryptionParams>( access_token.primary_id(), @@ -664,7 +676,7 @@ impl JMAP { .into_http_response()) } - pub async fn handle_crypto_post( + async fn handle_crypto_post( &self, access_token: Arc<AccessToken>, body: Option<Vec<u8>>, diff --git a/crates/jmap/src/email/delete.rs b/crates/jmap/src/email/delete.rs index 5268925e..5d587aa3 100644 --- a/crates/jmap/src/email/delete.rs +++ b/crates/jmap/src/email/delete.rs @@ -6,6 +6,7 @@ use std::time::Duration; +use common::Server; use jmap_proto::types::{ collection::Collection, id::Id, keyword::Keyword, property::Property, state::StateChange, type_state::DataType, @@ -23,15 +24,41 @@ use trc::{AddContext, StoreEvent}; use utils::codec::leb128::Leb128Reader; use crate::{ + changes::write::ChangeLog, mailbox::{UidMailbox, JUNK_ID, TOMBSTONE_ID, TRASH_ID}, - JMAP, + services::state::StateManager, + JmapMethods, }; use super::{index::EmailIndexBuilder, metadata::MessageMetadata}; use rand::prelude::SliceRandom; +use std::future::Future; -impl JMAP { - pub async fn emails_tombstone( +pub trait EmailDeletion: Sync + Send { + fn emails_tombstone( + &self, + account_id: u32, + document_ids: RoaringBitmap, + ) -> impl Future<Output = trc::Result<(ChangeLogBuilder, RoaringBitmap)>> + Send; + + fn purge_accounts(&self) -> impl Future<Output = ()> + Send; + + fn purge_account(&self, account_id: u32) -> impl Future<Output = ()> + Send; + + fn emails_auto_expunge( + &self, + account_id: u32, + period: Duration, + ) -> impl Future<Output = trc::Result<()>> + Send; + + fn emails_purge_tombstoned( + &self, + account_id: u32, + ) -> impl Future<Output = trc::Result<()>> + Send; +} + +impl EmailDeletion for Server { + async fn emails_tombstone( &self, account_id: u32, mut document_ids: RoaringBitmap, @@ -208,7 +235,7 @@ impl JMAP { Ok((changes, document_ids)) } - pub async fn purge_accounts(&self) { + async fn purge_accounts(&self) { if let Ok(Some(account_ids)) = self.get_document_ids(u32::MAX, Collection::Principal).await { let mut account_ids: Vec<u32> = account_ids.into_iter().collect(); @@ -222,7 +249,7 @@ impl JMAP { } } - pub async fn purge_account(&self, account_id: u32) { + async fn purge_account(&self, account_id: u32) { // Lock account match self .core @@ -290,7 +317,7 @@ impl JMAP { } } - pub async fn emails_auto_expunge(&self, account_id: u32, period: Duration) -> trc::Result<()> { + async fn emails_auto_expunge(&self, account_id: u32, period: Duration) -> trc::Result<()> { let deletion_candidates = self .get_tag( account_id, @@ -313,7 +340,7 @@ impl JMAP { if deletion_candidates.is_empty() { return Ok(()); } - let reference_cid = self.inner.snowflake_id.past_id(period).ok_or_else(|| { + let reference_cid = self.inner.data.jmap_id_gen.past_id(period).ok_or_else(|| { trc::StoreEvent::UnexpectedError .into_err() .caused_by(trc::location!()) @@ -364,7 +391,7 @@ impl JMAP { Ok(()) } - pub async fn emails_purge_tombstoned(&self, account_id: u32) -> trc::Result<()> { + async fn emails_purge_tombstoned(&self, account_id: u32) -> trc::Result<()> { // Obtain tombstoned messages let tombstoned_ids = self .core @@ -401,7 +428,6 @@ impl JMAP { // Obtain tenant id let tenant_id = self - .core .get_cached_access_token(account_id) .await .caused_by(trc::location!())? diff --git a/crates/jmap/src/email/get.rs b/crates/jmap/src/email/get.rs index cc891d37..921ecd5f 100644 --- a/crates/jmap/src/email/get.rs +++ b/crates/jmap/src/email/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::{email::GetArguments, Object}, @@ -23,16 +23,29 @@ use mail_parser::HeaderName; use store::{write::Bincode, BlobClass}; use trc::{AddContext, StoreEvent}; -use crate::{email::headers::HeaderToValue, mailbox::UidMailbox, JMAP}; +use crate::{ + auth::acl::AclMethods, blob::download::BlobDownload, changes::state::StateManager, + email::headers::HeaderToValue, mailbox::UidMailbox, JmapMethods, +}; +use std::future::Future; use super::{ body::{ToBodyPart, TruncateBody}, + cache::ThreadCache, headers::IntoForm, metadata::{MessageMetadata, MetadataPartType}, }; -impl JMAP { - pub async fn email_get( +pub trait EmailGet: Sync + Send { + fn email_get( + &self, + request: GetRequest<GetArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; +} + +impl EmailGet for Server { + async fn email_get( &self, mut request: GetRequest<GetArguments>, access_token: &AccessToken, diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index 1abe06cc..1d9b441d 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::import::{ImportEmailRequest, ImportEmailResponse}, @@ -20,12 +20,25 @@ use jmap_proto::{ use mail_parser::MessageParser; use utils::map::vec_map::VecMap; -use crate::{api::http::HttpSessionData, JMAP}; +use crate::{ + api::http::HttpSessionData, auth::acl::AclMethods, blob::download::BlobDownload, + changes::state::StateManager, mailbox::set::MailboxSet, JmapMethods, +}; + +use super::ingest::{EmailIngest, IngestEmail, IngestSource}; +use std::future::Future; -use super::ingest::{IngestEmail, IngestSource}; +pub trait EmailImport: Sync + Send { + fn email_import( + &self, + request: ImportEmailRequest, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<ImportEmailResponse>> + Send; +} -impl JMAP { - pub async fn email_import( +impl EmailImport for Server { + async fn email_import( &self, request: ImportEmailRequest, access_token: &AccessToken, diff --git a/crates/jmap/src/email/ingest.rs b/crates/jmap/src/email/ingest.rs index 66a4cced..3a608113 100644 --- a/crates/jmap/src/email/ingest.rs +++ b/crates/jmap/src/email/ingest.rs @@ -9,7 +9,7 @@ use std::{ time::{Duration, Instant}, }; -use common::auth::ResourceToken; +use common::{auth::ResourceToken, Server}; use jmap_proto::{ object::Object, types::{ @@ -22,6 +22,7 @@ use mail_parser::{ }; use rand::Rng; +use std::future::Future; use store::{ ahash::AHashSet, query::Filter, @@ -36,12 +37,16 @@ use trc::{AddContext, MessageIngestEvent}; use utils::map::vec_map::VecMap; use crate::{ + blob::upload::BlobUpload, + changes::write::ChangeLog, email::index::{IndexMessage, VisitValues, MAX_ID_LENGTH}, mailbox::{UidMailbox, INBOX_ID, JUNK_ID}, - JMAP, + services::index::Indexer, + JmapMethods, }; use super::{ + cache::ThreadCache, crypto::{EncryptMessage, EncryptMessageError, EncryptionParams}, index::{TrimTextValue, MAX_SORT_FIELD_LENGTH}, }; @@ -76,9 +81,27 @@ pub enum IngestSource { const MAX_RETRIES: u32 = 10; -impl JMAP { +pub trait EmailIngest: Sync + Send { + fn email_ingest( + &self, + params: IngestEmail, + ) -> impl Future<Output = trc::Result<IngestedEmail>> + Send; + fn find_or_merge_thread( + &self, + account_id: u32, + thread_name: &str, + references: &[&str], + ) -> impl Future<Output = trc::Result<Option<u32>>> + Send; + fn assign_imap_uid( + &self, + account_id: u32, + mailbox_id: u32, + ) -> impl Future<Output = trc::Result<u32>> + Send; +} + +impl EmailIngest for Server { #[allow(clippy::blocks_in_conditions)] - pub async fn email_ingest(&self, mut params: IngestEmail<'_>) -> trc::Result<IngestedEmail> { + async fn email_ingest(&self, mut params: IngestEmail<'_>) -> trc::Result<IngestedEmail> { // Check quota let start_time = Instant::now(); let account_id = params.resource.account_id; @@ -338,7 +361,7 @@ impl JMAP { let id = Id::from_parts(thread_id, document_id); // Request FTS index - self.inner.request_fts_index(); + self.request_fts_index(); trc::event!( MessageIngest(match params.source { @@ -379,7 +402,7 @@ impl JMAP { }) } - pub async fn find_or_merge_thread( + async fn find_or_merge_thread( &self, account_id: u32, thread_name: &str, @@ -519,7 +542,7 @@ impl JMAP { } } - pub async fn assign_imap_uid(&self, account_id: u32, mailbox_id: u32) -> trc::Result<u32> { + async fn assign_imap_uid(&self, account_id: u32, mailbox_id: u32) -> trc::Result<u32> { // Increment UID next let mut batch = BatchBuilder::new(); batch diff --git a/crates/jmap/src/email/parse.rs b/crates/jmap/src/email/parse.rs index 83ec8162..d0b33751 100644 --- a/crates/jmap/src/email/parse.rs +++ b/crates/jmap/src/email/parse.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::parse::{ParseEmailRequest, ParseEmailResponse}, object::Object, @@ -13,9 +13,10 @@ use jmap_proto::{ use mail_parser::{ decoders::html::html_to_text, parsers::preview::preview_text, MessageParser, PartType, }; +use std::future::Future; use utils::map::vec_map::VecMap; -use crate::JMAP; +use crate::blob::download::BlobDownload; use super::{ body::{ToBodyPart, TruncateBody}, @@ -23,8 +24,16 @@ use super::{ index::PREVIEW_LENGTH, }; -impl JMAP { - pub async fn email_parse( +pub trait EmailParse: Sync + Send { + fn email_parse( + &self, + request: ParseEmailRequest, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<ParseEmailResponse>> + Send; +} + +impl EmailParse for Server { + async fn email_parse( &self, request: ParseEmailRequest, access_token: &AccessToken, diff --git a/crates/jmap/src/email/query.rs b/crates/jmap/src/email/query.rs index 56942279..1a56c33f 100644 --- a/crates/jmap/src/email/query.rs +++ b/crates/jmap/src/email/query.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty}, object::email::QueryArguments, @@ -12,6 +12,7 @@ use jmap_proto::{ }; use mail_parser::HeaderName; use nlp::language::Language; +use std::future::Future; use store::{ fts::{Field, FilterGroup, FtsFilter, IntoFilterGroup}, query::{self}, @@ -20,10 +21,27 @@ use store::{ ValueKey, }; -use crate::JMAP; +use crate::{auth::acl::AclMethods, JmapMethods}; -impl JMAP { - pub async fn email_query( +use super::cache::ThreadCache; + +pub trait EmailQuery: Sync + Send { + fn email_query( + &self, + request: QueryRequest<QueryArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<QueryResponse>> + Send; + + fn thread_keywords( + &self, + account_id: u32, + keyword: Keyword, + match_all: bool, + ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send; +} + +impl EmailQuery for Server { + async fn email_query( &self, mut request: QueryRequest<QueryArguments>, access_token: &AccessToken, diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 62a7562f..a1a0f719 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -6,7 +6,7 @@ use std::{borrow::Cow, collections::HashMap, slice::IterMut}; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{RequestArguments, SetRequest, SetResponse}, @@ -41,15 +41,33 @@ use store::{ }; use trc::AddContext; -use crate::{api::http::HttpSessionData, mailbox::UidMailbox, JMAP}; +use crate::{ + api::http::HttpSessionData, + auth::acl::AclMethods, + blob::download::BlobDownload, + changes::{state::StateManager, write::ChangeLog}, + mailbox::{set::MailboxSet, UidMailbox}, + JmapMethods, +}; +use std::future::Future; use super::{ + delete::EmailDeletion, headers::{BuildHeader, ValueToHeader}, - ingest::{IngestEmail, IngestSource}, + ingest::{EmailIngest, IngestEmail, IngestSource}, }; -impl JMAP { - pub async fn email_set( +pub trait EmailSet: Sync + Send { + fn email_set( + &self, + request: SetRequest<RequestArguments>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; +} + +impl EmailSet for Server { + async fn email_set( &self, mut request: SetRequest<RequestArguments>, access_token: &AccessToken, diff --git a/crates/jmap/src/email/snippet.rs b/crates/jmap/src/email/snippet.rs index dbb6e67c..bace9f17 100644 --- a/crates/jmap/src/email/snippet.rs +++ b/crates/jmap/src/email/snippet.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::{ query::Filter, @@ -16,12 +16,21 @@ use mail_parser::{decoders::html::html_to_text, GetHeader, HeaderName, PartType} use nlp::language::{search_snippet::generate_snippet, stemmer::Stemmer, Language}; use store::{backend::MAX_TOKEN_LENGTH, write::Bincode}; -use crate::JMAP; +use crate::{auth::acl::AclMethods, blob::download::BlobDownload, JmapMethods}; use super::metadata::{MessageMetadata, MetadataPartType}; +use std::future::Future; -impl JMAP { - pub async fn email_search_snippet( +pub trait EmailSearchSnippet: Sync + Send { + fn email_search_snippet( + &self, + request: GetSearchSnippetRequest, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<GetSearchSnippetResponse>> + Send; +} + +impl EmailSearchSnippet for Server { + async fn email_search_snippet( &self, request: GetSearchSnippetRequest, access_token: &AccessToken, diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index f02d8e24..6b934472 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, @@ -16,12 +17,25 @@ use store::{ }; use trc::AddContext; -use crate::JMAP; +use crate::{changes::state::StateManager, JmapMethods}; use super::set::sanitize_email; +use std::future::Future; -impl JMAP { - pub async fn identity_get( +pub trait IdentityGet: Sync + Send { + fn identity_get( + &self, + request: GetRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; + + fn identity_get_or_create( + &self, + account_id: u32, + ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send; +} + +impl IdentityGet for Server { + async fn identity_get( &self, mut request: GetRequest<RequestArguments>, ) -> trc::Result<GetResponse> { @@ -107,7 +121,7 @@ impl JMAP { Ok(response) } - pub async fn identity_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> { + async fn identity_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> { let mut identity_ids = self .get_document_ids(account_id, Collection::Identity) .await? diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 3ab94e51..9e82dcb8 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ error::set::SetError, @@ -16,12 +17,20 @@ use jmap_proto::{ value::{MaybePatchValue, Value}, }, }; +use std::future::Future; use store::write::{log::ChangeLogBuilder, BatchBuilder, F_CLEAR, F_VALUE}; -use crate::JMAP; +use crate::{changes::write::ChangeLog, JmapMethods}; -impl JMAP { - pub async fn identity_set( +pub trait IdentitySet: Sync + Send { + fn identity_set( + &self, + request: SetRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; +} + +impl IdentitySet for Server { + async fn identity_set( &self, mut request: SetRequest<RequestArguments>, ) -> trc::Result<SetResponse> { diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index ad83a0bd..bd75a9fe 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -4,22 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{ - collections::hash_map::RandomState, - fmt::Display, - sync::{atomic::AtomicU8, Arc}, - time::Duration, -}; +use std::{fmt::Display, future::Future, sync::Arc, time::Duration}; -use auth::rate_limit::ConcurrencyLimiters; +use changes::state::StateManager; use common::{ auth::{AccessToken, ResourceToken, TenantInfo}, - manager::webadmin::WebAdminManager, - Core, DeliveryEvent, SharedCore, + manager::boot::{BootManager, IpcReceivers}, + Inner, Server, }; -use dashmap::DashMap; use directory::QueryBy; -use email::cache::Threads; use jmap_proto::{ method::{ query::{QueryRequest, QueryResponse}, @@ -28,13 +21,10 @@ use jmap_proto::{ types::{collection::Collection, property::Property}, }; use services::{ - delivery::spawn_delivery_manager, - housekeeper::{self, init_housekeeper, spawn_housekeeper}, - index::spawn_index_task, - state::{self, init_state_manager, spawn_state_manager}, + delivery::spawn_delivery_manager, housekeeper::spawn_housekeeper, index::spawn_index_task, + state::spawn_state_manager, }; -use smtp::core::SMTP; use store::{ dispatch::DocumentSet, fts::FtsFilter, @@ -46,14 +36,7 @@ use store::{ }, BitmapKey, Deserialize, IterateParams, ValueKey, U32_LEN, }; -use tokio::sync::{mpsc, Notify}; use trc::AddContext; -use utils::{ - config::Config, - lru_cache::{LruCache, LruCached}, - map::ttl_dashmap::{TtlDashMap, TtlMap}, - snowflake::SnowflakeIdGenerator, -}; pub mod api; pub mod auth; @@ -74,76 +57,24 @@ pub mod websocket; pub const LONG_SLUMBER: Duration = Duration::from_secs(60 * 60 * 24); -#[derive(Clone)] -pub struct JMAP { - pub core: Arc<Core>, - pub shared_core: SharedCore, - pub inner: Arc<Inner>, - pub smtp: SMTP, +pub trait StartServices: Sync + Send { + fn start_services(&mut self) -> impl Future<Output = ()> + Send; } -#[derive(Clone)] -pub struct JmapInstance { - pub core: SharedCore, - pub jmap_inner: Arc<Inner>, - pub smtp_inner: Arc<smtp::core::Inner>, +pub trait SpawnServices { + fn spawn_services(&mut self, inner: Arc<Inner>); } -pub struct Inner { - pub sessions: TtlDashMap<String, u32>, - pub snowflake_id: SnowflakeIdGenerator, - pub webadmin: WebAdminManager, - pub config_version: AtomicU8, - - pub concurrency_limiter: DashMap<u32, Arc<ConcurrencyLimiters>>, - - pub state_tx: mpsc::Sender<state::Event>, - pub housekeeper_tx: mpsc::Sender<housekeeper::Event>, - pub index_tx: Arc<Notify>, - - pub cache_threads: LruCache<u32, Arc<Threads>>, -} - -impl JMAP { - pub async fn init( - config: &mut Config, - delivery_rx: mpsc::Receiver<DeliveryEvent>, - core: SharedCore, - smtp_inner: Arc<smtp::core::Inner>, - ) -> JmapInstance { - // Init state manager and housekeeper - let (state_tx, state_rx) = init_state_manager(); - let (housekeeper_tx, housekeeper_rx) = init_housekeeper(); - let index_tx = Arc::new(Notify::new()); - let shard_amount = config - .property::<u64>("cache.shard") - .unwrap_or(32) - .next_power_of_two() as usize; - let capacity = config.property("cache.capacity").unwrap_or(100); - - let inner = Inner { - webadmin: WebAdminManager::new(), - sessions: TtlDashMap::with_capacity(capacity, shard_amount), - snowflake_id: config - .property::<u64>("cluster.node-id") - .map(SnowflakeIdGenerator::with_node_id) - .unwrap_or_default(), - concurrency_limiter: DashMap::with_capacity_and_hasher_and_shard_amount( - capacity, - RandomState::default(), - shard_amount, - ), - state_tx, - housekeeper_tx, - index_tx: index_tx.clone(), - cache_threads: LruCache::with_capacity( - config.property("cache.thread.size").unwrap_or(2048), - ), - config_version: 0.into(), - }; - +impl StartServices for BootManager { + async fn start_services(&mut self) { // Unpack webadmin - if let Err(err) = inner.webadmin.unpack(&core.load().storage.blob).await { + if let Err(err) = self + .inner + .data + .webadmin + .unpack(&self.inner.shared_core.load().storage.blob) + .await + { trc::event!( Resource(trc::ResourceEvent::Error), Reason = err, @@ -151,33 +82,33 @@ impl JMAP { ); } - let jmap_instance = JmapInstance { - core, - jmap_inner: Arc::new(inner), - smtp_inner, - }; + self.ipc_rxs.spawn_services(self.inner.clone()); + } +} +impl SpawnServices for IpcReceivers { + fn spawn_services(&mut self, inner: Arc<Inner>) { // Spawn delivery manager - spawn_delivery_manager(jmap_instance.clone(), delivery_rx); + spawn_delivery_manager(inner.clone(), self.delivery_rx.take().unwrap()); // Spawn state manager - spawn_state_manager(jmap_instance.clone(), state_rx); + spawn_state_manager(inner.clone(), self.state_rx.take().unwrap()); // Spawn housekeeper - spawn_housekeeper(jmap_instance.clone(), housekeeper_rx); + spawn_housekeeper(inner.clone(), self.housekeeper_rx.take().unwrap()); // Spawn index task - spawn_index_task(jmap_instance.clone(), index_tx); - - jmap_instance + spawn_index_task(inner); } +} - pub async fn get_property<U>( +impl JmapMethods for Server { + async fn get_property<U>( &self, account_id: u32, collection: Collection, document_id: u32, - property: impl AsRef<Property>, + property: impl AsRef<Property> + Sync + Send, ) -> trc::Result<Option<U>> where U: Deserialize + 'static, @@ -203,7 +134,7 @@ impl JMAP { }) } - pub async fn get_properties<U, I, P>( + async fn get_properties<U, I, P>( &self, account_id: u32, collection: Collection, @@ -212,7 +143,7 @@ impl JMAP { ) -> trc::Result<Vec<(u32, U)>> where I: DocumentSet + Send + Sync, - P: AsRef<Property>, + P: AsRef<Property> + Sync + Send, U: Deserialize + 'static, { let property: u8 = property.as_ref().into(); @@ -258,7 +189,7 @@ impl JMAP { .map(|_| results) } - pub async fn get_document_ids( + async fn get_document_ids( &self, account_id: u32, collection: Collection, @@ -275,12 +206,12 @@ impl JMAP { }) } - pub async fn get_tag( + async fn get_tag( &self, account_id: u32, collection: Collection, - property: impl AsRef<Property>, - value: impl Into<TagValue<u32>>, + property: impl AsRef<Property> + Sync + Send, + value: impl Into<TagValue<u32>> + Sync + Send, ) -> trc::Result<Option<RoaringBitmap>> { let property = property.as_ref(); self.core @@ -304,7 +235,7 @@ impl JMAP { }) } - pub async fn prepare_set_response<T>( + async fn prepare_set_response<T: Sync + Send>( &self, request: &SetRequest<T>, collection: Collection, @@ -321,7 +252,7 @@ impl JMAP { ) } - pub async fn get_resource_token( + async fn get_resource_token( &self, access_token: &AccessToken, account_id: u32, @@ -380,7 +311,7 @@ impl JMAP { }) } - pub async fn get_used_quota(&self, account_id: u32) -> trc::Result<i64> { + async fn get_used_quota(&self, account_id: u32) -> trc::Result<i64> { self.core .storage .data @@ -389,11 +320,7 @@ impl JMAP { .add_context(|err| err.caused_by(trc::location!()).account_id(account_id)) } - pub async fn has_available_quota( - &self, - quotas: &ResourceToken, - item_size: u64, - ) -> trc::Result<()> { + async fn has_available_quota(&self, quotas: &ResourceToken, item_size: u64) -> trc::Result<()> { if quotas.quota != 0 { let used_quota = self.get_used_quota(quotas.account_id).await? as u64; @@ -428,7 +355,7 @@ impl JMAP { Ok(()) } - pub async fn filter( + async fn filter( &self, account_id: u32, collection: Collection, @@ -446,7 +373,7 @@ impl JMAP { }) } - pub async fn fts_filter<T: Into<u8> + Display + Clone + std::fmt::Debug>( + async fn fts_filter<T: Into<u8> + Display + Clone + std::fmt::Debug + Sync + Send>( &self, account_id: u32, collection: Collection, @@ -464,7 +391,7 @@ impl JMAP { }) } - pub async fn build_query_response<T>( + async fn build_query_response<T: Sync + Send>( &self, result_set: &ResultSet, request: &QueryRequest<T>, @@ -513,7 +440,7 @@ impl JMAP { )) } - pub async fn sort( + async fn sort( &self, result_set: ResultSet, comparators: Vec<Comparator>, @@ -539,7 +466,7 @@ impl JMAP { Ok(response) } - pub async fn write_batch(&self, batch: BatchBuilder) -> trc::Result<AssignedIds> { + async fn write_batch(&self, batch: BatchBuilder) -> trc::Result<AssignedIds> { self.core .storage .data @@ -548,34 +475,116 @@ impl JMAP { .caused_by(trc::location!()) } - pub async fn write_batch_expect_id(&self, batch: BatchBuilder) -> trc::Result<u32> { + async fn write_batch_expect_id(&self, batch: BatchBuilder) -> trc::Result<u32> { self.write_batch(batch) .await .and_then(|ids| ids.last_document_id().caused_by(trc::location!())) } -} -impl Inner { - pub fn increment_config_version(&self) { - self.config_version + fn increment_config_version(&self) { + self.inner + .data + .config_version .fetch_add(1, std::sync::atomic::Ordering::Relaxed); } } -impl From<JmapInstance> for JMAP { - fn from(value: JmapInstance) -> Self { - let shared_core = value.core.clone(); - let core = value.core.load_full(); - JMAP { - smtp: SMTP { - core: core.clone(), - inner: value.smtp_inner, - }, - core, - shared_core, - inner: value.jmap_inner, - } - } +pub trait JmapMethods: Sync + Send { + fn get_property<U>( + &self, + account_id: u32, + collection: Collection, + document_id: u32, + property: impl AsRef<Property> + Sync + Send, + ) -> impl Future<Output = trc::Result<Option<U>>> + Send + where + U: Deserialize + 'static; + + fn get_properties<U, I, P>( + &self, + account_id: u32, + collection: Collection, + iterate: &I, + property: P, + ) -> impl Future<Output = trc::Result<Vec<(u32, U)>>> + Send + where + I: DocumentSet + Send + Sync, + P: AsRef<Property> + Sync + Send, + U: Deserialize + 'static; + + fn get_document_ids( + &self, + account_id: u32, + collection: Collection, + ) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send; + + fn get_tag( + &self, + account_id: u32, + collection: Collection, + property: impl AsRef<Property> + Sync + Send, + value: impl Into<TagValue<u32>> + Sync + Send, + ) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send; + + fn prepare_set_response<T: Sync + Send>( + &self, + request: &SetRequest<T>, + collection: Collection, + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; + + fn get_resource_token( + &self, + access_token: &AccessToken, + account_id: u32, + ) -> impl Future<Output = trc::Result<ResourceToken>> + Send; + + fn get_used_quota(&self, account_id: u32) -> impl Future<Output = trc::Result<i64>> + Send; + + fn has_available_quota( + &self, + quotas: &ResourceToken, + item_size: u64, + ) -> impl Future<Output = trc::Result<()>> + Send; + + fn filter( + &self, + account_id: u32, + collection: Collection, + filters: Vec<Filter>, + ) -> impl Future<Output = trc::Result<ResultSet>> + Send; + + fn fts_filter<T: Into<u8> + Display + Clone + std::fmt::Debug + Sync + Send>( + &self, + account_id: u32, + collection: Collection, + filters: Vec<FtsFilter<T>>, + ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send; + + fn build_query_response<T: Sync + Send>( + &self, + result_set: &ResultSet, + request: &QueryRequest<T>, + ) -> impl Future<Output = trc::Result<(QueryResponse, Option<Pagination>)>> + Send; + + fn sort( + &self, + result_set: ResultSet, + comparators: Vec<Comparator>, + paginate: Pagination, + response: QueryResponse, + ) -> impl Future<Output = trc::Result<QueryResponse>> + Send; + + fn write_batch( + &self, + batch: BatchBuilder, + ) -> impl Future<Output = trc::Result<AssignedIds>> + Send; + + fn write_batch_expect_id( + &self, + batch: BatchBuilder, + ) -> impl Future<Output = trc::Result<u32>> + Send; + + fn increment_config_version(&self); } trait UpdateResults: Sized { diff --git a/crates/jmap/src/mailbox/get.rs b/crates/jmap/src/mailbox/get.rs index 069b678d..94f93304 100644 --- a/crates/jmap/src/mailbox/get.rs +++ b/crates/jmap/src/mailbox/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -13,12 +13,58 @@ use jmap_proto::{ use store::{ahash::AHashSet, query::Filter, roaring::RoaringBitmap}; use trc::AddContext; -use crate::{auth::acl::EffectiveAcl, JMAP}; +use crate::{ + auth::acl::{AclMethods, EffectiveAcl}, + changes::state::StateManager, + email::cache::ThreadCache, + JmapMethods, +}; -use super::INBOX_ID; +use super::{set::MailboxSet, INBOX_ID}; +use std::future::Future; -impl JMAP { - pub async fn mailbox_get( +pub trait MailboxGet: Sync + Send { + fn mailbox_get( + &self, + request: GetRequest<RequestArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; + + fn mailbox_count_threads( + &self, + account_id: u32, + document_ids: Option<RoaringBitmap>, + ) -> impl Future<Output = trc::Result<usize>> + Send; + + fn mailbox_unread_tags( + &self, + account_id: u32, + document_id: u32, + message_ids: &Option<RoaringBitmap>, + ) -> impl Future<Output = trc::Result<Option<RoaringBitmap>>> + Send; + + fn mailbox_expand_path<'x>( + &self, + account_id: u32, + path: &'x str, + exact_match: bool, + ) -> impl Future<Output = trc::Result<Option<ExpandPath<'x>>>> + Send; + + fn mailbox_get_by_name( + &self, + account_id: u32, + path: &str, + ) -> impl Future<Output = trc::Result<Option<u32>>> + Send; + + fn mailbox_get_by_role( + &self, + account_id: u32, + role: &str, + ) -> impl Future<Output = trc::Result<Option<u32>>> + Send; +} + +impl MailboxGet for Server { + async fn mailbox_get( &self, mut request: GetRequest<RequestArguments>, access_token: &AccessToken, @@ -257,7 +303,7 @@ impl JMAP { } } - pub async fn mailbox_unread_tags( + async fn mailbox_unread_tags( &self, account_id: u32, document_id: u32, @@ -297,7 +343,7 @@ impl JMAP { } } - pub async fn mailbox_expand_path<'x>( + async fn mailbox_expand_path<'x>( &self, account_id: u32, path: &'x str, @@ -376,11 +422,7 @@ impl JMAP { Ok(Some(ExpandPath { path, found_names })) } - pub async fn mailbox_get_by_name( - &self, - account_id: u32, - path: &str, - ) -> trc::Result<Option<u32>> { + async fn mailbox_get_by_name(&self, account_id: u32, path: &str) -> trc::Result<Option<u32>> { Ok(self .mailbox_expand_path(account_id, path, true) .await? @@ -403,11 +445,7 @@ impl JMAP { })) } - pub async fn mailbox_get_by_role( - &self, - account_id: u32, - role: &str, - ) -> trc::Result<Option<u32>> { + async fn mailbox_get_by_role(&self, account_id: u32, role: &str) -> trc::Result<Option<u32>> { self.filter( account_id, Collection::Mailbox, diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index 16295772..33263db9 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::query::{Comparator, Filter, QueryRequest, QueryResponse, SortProperty}, object::{mailbox::QueryArguments, Object}, @@ -16,10 +16,21 @@ use store::{ roaring::RoaringBitmap, }; -use crate::{UpdateResults, JMAP}; +use crate::{auth::acl::AclMethods, JmapMethods, UpdateResults}; +use std::future::Future; -impl JMAP { - pub async fn mailbox_query( +use super::set::MailboxSet; + +pub trait MailboxQuery: Sync + Send { + fn mailbox_query( + &self, + request: QueryRequest<QueryArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<QueryResponse>> + Send; +} + +impl MailboxQuery for Server { + async fn mailbox_query( &self, mut request: QueryRequest<QueryArguments>, access_token: &AccessToken, diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 91ee0216..beaca13d 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{auth::AccessToken, config::jmap::settings::SpecialUse}; +use common::{auth::AccessToken, config::jmap::settings::SpecialUse, Server}; use directory::Permission; use jmap_proto::{ error::set::{SetError, SetErrorType}, @@ -36,13 +36,19 @@ use store::{ }; use trc::AddContext; -use crate::{auth::acl::EffectiveAcl, JMAP}; +use crate::{ + auth::acl::{AclMethods, EffectiveAcl}, + changes::write::ChangeLog, + email::delete::EmailDeletion, + JmapMethods, +}; +use super::{get::MailboxGet, ARCHIVE_ID, DRAFTS_ID, SENT_ID}; #[allow(unused_imports)] use super::{UidMailbox, INBOX_ID, JUNK_ID, TRASH_ID}; -use super::{ARCHIVE_ID, DRAFTS_ID, SENT_ID}; +use std::future::Future; -struct SetContext<'x> { +pub struct SetContext<'x> { account_id: u32, access_token: &'x AccessToken, is_shared: bool, @@ -69,9 +75,44 @@ pub static SCHEMA: &[IndexProperty] = &[ IndexProperty::new(Property::Acl).index_as(IndexAs::Acl), ]; -impl JMAP { +pub trait MailboxSet: Sync + Send { + fn mailbox_set( + &self, + request: SetRequest<SetArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; + + fn mailbox_destroy( + &self, + account_id: u32, + document_id: u32, + changes: &mut ChangeLogBuilder, + access_token: &AccessToken, + remove_emails: bool, + ) -> impl Future<Output = trc::Result<Result<bool, SetError>>> + Send; + + fn mailbox_set_item( + &self, + changes_: Object<SetValue>, + update: Option<(u32, HashedValue<Object<Value>>)>, + ctx: &SetContext, + ) -> impl Future<Output = trc::Result<Result<ObjectIndexBuilder, SetError>>> + Send; + + fn mailbox_get_or_create( + &self, + account_id: u32, + ) -> impl Future<Output = trc::Result<RoaringBitmap>> + Send; + + fn mailbox_create_path( + &self, + account_id: u32, + path: &str, + ) -> impl Future<Output = trc::Result<Option<(u32, Option<u64>)>>> + Send; +} + +impl MailboxSet for Server { #[allow(clippy::blocks_in_conditions)] - pub async fn mailbox_set( + async fn mailbox_set( &self, mut request: SetRequest<SetArguments>, access_token: &AccessToken, @@ -283,7 +324,7 @@ impl JMAP { Ok(ctx.response) } - pub async fn mailbox_destroy( + async fn mailbox_destroy( &self, account_id: u32, document_id: u32, @@ -761,7 +802,7 @@ impl JMAP { .validate()) } - pub async fn mailbox_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> { + async fn mailbox_get_or_create(&self, account_id: u32) -> trc::Result<RoaringBitmap> { let mut mailbox_ids = self .get_document_ids(account_id, Collection::Mailbox) .await? @@ -828,7 +869,7 @@ impl JMAP { .map(|_| mailbox_ids) } - pub async fn mailbox_create_path( + async fn mailbox_create_path( &self, account_id: u32, path: &str, diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index b59abd77..f17d1198 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -4,17 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, types::{collection::Collection, property::Property, state::State, value::Value}, }; +use std::future::Future; -use crate::JMAP; +use crate::JmapMethods; -impl JMAP { - pub async fn principal_get( +pub trait PrincipalGet: Sync + Send { + fn principal_get( + &self, + request: GetRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; +} + +impl PrincipalGet for Server { + async fn principal_get( &self, mut request: GetRequest<RequestArguments>, ) -> trc::Result<GetResponse> { diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index 0ed616ea..fa9793ee 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use directory::QueryBy; use jmap_proto::{ method::query::{Filter, QueryRequest, QueryResponse, RequestArguments}, @@ -11,10 +12,19 @@ use jmap_proto::{ }; use store::{query::ResultSet, roaring::RoaringBitmap}; -use crate::{api::http::HttpSessionData, JMAP}; +use crate::{api::http::HttpSessionData, JmapMethods}; +use std::future::Future; -impl JMAP { - pub async fn principal_query( +pub trait PrincipalQuery: Sync + Send { + fn principal_query( + &self, + request: QueryRequest<RequestArguments>, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<QueryResponse>> + Send; +} + +impl PrincipalQuery for Server { + async fn principal_query( &self, mut request: QueryRequest<RequestArguments>, session: &HttpSessionData, @@ -51,7 +61,6 @@ impl JMAP { Filter::Email(email) => { let mut ids = RoaringBitmap::new(); for id in self - .core .email_to_ids(&self.core.storage.directory, &email, session.session_id) .await? { diff --git a/crates/jmap/src/push/get.rs b/crates/jmap/src/push/get.rs index 175b8be6..41692f9b 100644 --- a/crates/jmap/src/push/get.rs +++ b/crates/jmap/src/push/get.rs @@ -5,7 +5,11 @@ */ use base64::{engine::general_purpose, Engine}; -use common::auth::AccessToken; +use common::{ + auth::AccessToken, + ipc::{StateEvent, UpdateSubscription}, + Server, +}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -17,12 +21,26 @@ use store::{ }; use utils::map::bitmap::Bitmap; -use crate::{services::state, JMAP}; +use crate::JmapMethods; + +use super::{EncryptionKeys, PushSubscription}; +use std::future::Future; -use super::{EncryptionKeys, PushSubscription, UpdateSubscription}; +pub trait PushSubscriptionFetch: Sync + Send { + fn push_subscription_get( + &self, + request: GetRequest<RequestArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; + + fn fetch_push_subscriptions( + &self, + account_id: u32, + ) -> impl Future<Output = trc::Result<StateEvent>> + Send; +} -impl JMAP { - pub async fn push_subscription_get( +impl PushSubscriptionFetch for Server { + async fn push_subscription_get( &self, mut request: GetRequest<RequestArguments>, access_token: &AccessToken, @@ -99,7 +117,7 @@ impl JMAP { Ok(response) } - pub async fn fetch_push_subscriptions(&self, account_id: u32) -> trc::Result<state::Event> { + async fn fetch_push_subscriptions(&self, account_id: u32) -> trc::Result<StateEvent> { let mut subscriptions = Vec::new(); let document_ids = self .core @@ -235,7 +253,7 @@ impl JMAP { } } - Ok(state::Event::UpdateSubscriptions { + Ok(StateEvent::UpdateSubscriptions { account_id, subscriptions, }) diff --git a/crates/jmap/src/push/manager.rs b/crates/jmap/src/push/manager.rs index ba7acbfd..c543f400 100644 --- a/crates/jmap/src/push/manager.rs +++ b/crates/jmap/src/push/manager.rs @@ -5,23 +5,24 @@ */ use base64::{engine::general_purpose, Engine}; -use common::IPC_CHANNEL_BUFFER; +use common::{core::BuildServer, Inner, IPC_CHANNEL_BUFFER}; use jmap_proto::types::id::Id; use store::ahash::{AHashMap, AHashSet}; use tokio::sync::mpsc; use trc::PushSubscriptionEvent; -use crate::{api::StateChangeResponse, JmapInstance, LONG_SLUMBER}; +use crate::{api::StateChangeResponse, LONG_SLUMBER}; use super::{ece::ece_encrypt, EncryptionKeys, Event, PushServer, PushUpdate}; use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE}; use std::{ collections::hash_map::Entry, + sync::Arc, time::{Duration, Instant}, }; -pub fn spawn_push_manager(core: JmapInstance) -> mpsc::Sender<Event> { +pub fn spawn_push_manager(inner: Arc<Inner>) -> mpsc::Sender<Event> { let (push_tx_, mut push_rx) = mpsc::channel::<Event>(IPC_CHANNEL_BUFFER); let push_tx = push_tx_.clone(); @@ -37,13 +38,13 @@ pub fn spawn_push_manager(core: JmapInstance) -> mpsc::Sender<Event> { let event_or_timeout = tokio::time::timeout(retry_timeout, push_rx.recv()).await; // Load settings - let core_ = core.core.load_full(); - let push_attempt_interval = core_.jmap.push_attempt_interval; - let push_attempts_max = core_.jmap.push_attempts_max; - let push_retry_interval = core_.jmap.push_retry_interval; - let push_timeout = core_.jmap.push_timeout; - let push_verify_timeout = core_.jmap.push_verify_timeout; - let push_throttle = core_.jmap.push_throttle; + let server = inner.build_server(); + let push_attempt_interval = server.core.jmap.push_attempt_interval; + let push_attempts_max = server.core.jmap.push_attempts_max; + let push_retry_interval = server.core.jmap.push_retry_interval; + let push_timeout = server.core.jmap.push_timeout; + let push_verify_timeout = server.core.jmap.push_verify_timeout; + let push_throttle = server.core.jmap.push_throttle; match event_or_timeout { Ok(Some(event)) => match event { diff --git a/crates/jmap/src/push/mod.rs b/crates/jmap/src/push/mod.rs index 9e6dae69..65669632 100644 --- a/crates/jmap/src/push/mod.rs +++ b/crates/jmap/src/push/mod.rs @@ -11,34 +11,8 @@ pub mod set; use std::time::Instant; -use jmap_proto::types::{id::Id, state::StateChange, type_state::DataType}; -use utils::map::bitmap::Bitmap; - -#[derive(Debug)] -pub enum UpdateSubscription { - Unverified { - id: u32, - url: String, - code: String, - keys: Option<EncryptionKeys>, - }, - Verified(PushSubscription), -} - -#[derive(Debug)] -pub struct PushSubscription { - pub id: u32, - pub url: String, - pub expires: u64, - pub types: Bitmap<DataType>, - pub keys: Option<EncryptionKeys>, -} - -#[derive(Debug, Clone)] -pub struct EncryptionKeys { - pub p256dh: Vec<u8>, - pub auth: Vec<u8>, -} +use common::ipc::{EncryptionKeys, PushSubscription}; +use jmap_proto::types::{id::Id, state::StateChange}; #[derive(Debug)] pub enum Event { diff --git a/crates/jmap/src/push/set.rs b/crates/jmap/src/push/set.rs index 22f56f3e..d23b9436 100644 --- a/crates/jmap/src/push/set.rs +++ b/crates/jmap/src/push/set.rs @@ -5,7 +5,7 @@ */ use base64::{engine::general_purpose, Engine}; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ error::set::SetError, method::set::{RequestArguments, SetRequest, SetResponse}, @@ -19,18 +19,27 @@ use jmap_proto::{ value::{MaybePatchValue, Value}, }, }; +use std::future::Future; use store::{ rand::{distributions::Alphanumeric, thread_rng, Rng}, write::{now, BatchBuilder, F_CLEAR, F_VALUE}, }; -use crate::JMAP; +use crate::{services::state::StateManager, JmapMethods}; const EXPIRES_MAX: i64 = 7 * 24 * 3600; // 7 days const VERIFICATION_CODE_LEN: usize = 32; -impl JMAP { - pub async fn push_subscription_set( +pub trait PushSubscriptionSet: Sync + Send { + fn push_subscription_set( + &self, + request: SetRequest<RequestArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; +} + +impl PushSubscriptionSet for Server { + async fn push_subscription_set( &self, mut request: SetRequest<RequestArguments>, access_token: &AccessToken, diff --git a/crates/jmap/src/quota/get.rs b/crates/jmap/src/quota/get.rs index 193c77fb..a4edeb52 100644 --- a/crates/jmap/src/quota/get.rs +++ b/crates/jmap/src/quota/get.rs @@ -4,17 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, types::{id::Id, property::Property, state::State, type_state::DataType, value::Value}, }; +use std::future::Future; -use crate::JMAP; +use crate::JmapMethods; -impl JMAP { - pub async fn quota_get( +pub trait QuotaGet: Sync + Send { + fn quota_get( + &self, + request: GetRequest<RequestArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; +} + +impl QuotaGet for Server { + async fn quota_get( &self, mut request: GetRequest<RequestArguments>, access_token: &AccessToken, diff --git a/crates/jmap/src/quota/query.rs b/crates/jmap/src/quota/query.rs index 40d65a52..dae4ce1e 100644 --- a/crates/jmap/src/quota/query.rs +++ b/crates/jmap/src/quota/query.rs @@ -4,16 +4,23 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ method::query::{QueryRequest, QueryResponse, RequestArguments}, types::{id::Id, state::State}, }; +use std::future::Future; -use crate::JMAP; +pub trait QuotaQuery: Sync + Send { + fn quota_query( + &self, + request: QueryRequest<RequestArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<QueryResponse>> + Send; +} -impl JMAP { - pub async fn quota_query( +impl QuotaQuery for Server { + async fn quota_query( &self, request: QueryRequest<RequestArguments>, access_token: &AccessToken, diff --git a/crates/jmap/src/quota/set.rs b/crates/jmap/src/quota/set.rs index 6aec092a..4a08f11c 100644 --- a/crates/jmap/src/quota/set.rs +++ b/crates/jmap/src/quota/set.rs @@ -8,14 +8,16 @@ use jmap_proto::{ object::index::{IndexAs, IndexProperty}, types::property::Property, }; +use std::future::Future; -use crate::JMAP; - -impl JMAP { - pub async fn quota_set( +pub trait QuotaSet: Sync + Send { + fn quota_set( &self, account_id: u32, quota: &AccessToken, - ) -> trc::Result<SetResponse> { - } + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; +} + +impl QuotaSet for Server { + async fn quota_set(&self, account_id: u32, quota: &AccessToken) -> trc::Result<SetResponse> {} } diff --git a/crates/jmap/src/services/delivery.rs b/crates/jmap/src/services/delivery.rs index fc732125..b3f8b674 100644 --- a/crates/jmap/src/services/delivery.rs +++ b/crates/jmap/src/services/delivery.rs @@ -4,18 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::DeliveryEvent; +use std::sync::Arc; + +use common::{core::BuildServer, ipc::DeliveryEvent, Inner}; use tokio::sync::mpsc; -use crate::{JmapInstance, JMAP}; +use super::ingest::MailDelivery; -pub fn spawn_delivery_manager(core: JmapInstance, mut delivery_rx: mpsc::Receiver<DeliveryEvent>) { +pub fn spawn_delivery_manager(inner: Arc<Inner>, mut delivery_rx: mpsc::Receiver<DeliveryEvent>) { tokio::spawn(async move { while let Some(event) = delivery_rx.recv().await { match event { DeliveryEvent::Ingest { message, result_tx } => { result_tx - .send(JMAP::from(core.clone()).deliver_message(message).await) + .send(inner.build_server().deliver_message(message).await) .ok(); } DeliveryEvent::Stop => break, diff --git a/crates/jmap/src/services/gossip/mod.rs b/crates/jmap/src/services/gossip/mod.rs index def13548..959e1fe2 100644 --- a/crates/jmap/src/services/gossip/mod.rs +++ b/crates/jmap/src/services/gossip/mod.rs @@ -11,17 +11,16 @@ pub mod ping; pub mod request; pub mod spawn; +use common::Inner; use serde::{Deserialize, Serialize}; use std::{ net::{IpAddr, SocketAddr}, - sync::atomic::Ordering, + sync::{atomic::Ordering, Arc}, time::Instant, }; use tokio::sync::mpsc; use trc::ClusterEvent; -use crate::JmapInstance; - use self::request::Request; const UDP_MAX_PAYLOAD: usize = 65500; @@ -44,7 +43,7 @@ pub struct Gossiper { pub last_peer_pinged: usize, // IPC - pub core: JmapInstance, + pub inner: Arc<Inner>, pub gossip_tx: mpsc::Sender<(SocketAddr, Request)>, } @@ -101,23 +100,26 @@ impl From<&Peer> for PeerStatus { impl From<&Gossiper> for PeerStatus { fn from(cluster: &Gossiper) -> Self { - let core = cluster.core.core.load(); PeerStatus { addr: cluster.addr, epoch: cluster.epoch, - gen_config: cluster - .core - .jmap_inner - .config_version + gen_config: cluster.inner.data.config_version.load(Ordering::Relaxed), + gen_lists: cluster + .inner + .data + .blocked_ips_version + .load(Ordering::Relaxed), + gen_permissions: cluster + .inner + .data + .permissions_version .load(Ordering::Relaxed), - gen_lists: core.network.blocked_ips.version.load(Ordering::Relaxed), - gen_permissions: core.security.permissions_version.load(Ordering::Relaxed), } } } impl Gossiper { - pub async fn send_gossip(&self, dest: IpAddr, request: Request) { + async fn send_gossip(&self, dest: IpAddr, request: Request) { if let Err(err) = self .gossip_tx .send((SocketAddr::new(dest, self.port), request)) diff --git a/crates/jmap/src/services/gossip/ping.rs b/crates/jmap/src/services/gossip/ping.rs index ab30f88c..dc05a7e3 100644 --- a/crates/jmap/src/services/gossip/ping.rs +++ b/crates/jmap/src/services/gossip/ping.rs @@ -4,10 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use smtp::queue; +use common::{ + core::BuildServer, + ipc::{HousekeeperEvent, QueueEvent}, +}; use trc::ClusterEvent; -use crate::services::housekeeper; +use crate::services::index::Indexer; use super::{request::Request, Gossiper, PeerStatus}; @@ -66,13 +69,13 @@ impl Gossiper { } pub fn request_reload(&self) { - let core = self.core.clone(); + let server = self.inner.build_server(); tokio::spawn(async move { trc::event!(Cluster(ClusterEvent::OneOrMorePeersOffline)); - core.jmap_inner.request_fts_index(); - let _ = core.smtp_inner.queue_tx.send(queue::Event::Reload).await; + server.request_fts_index(); + let _ = server.inner.ipc.queue_tx.send(QueueEvent::Reload).await; }); } @@ -174,29 +177,30 @@ impl Gossiper { // Reload settings if update_permissions { - self.core.core.load().security.permissions.clear(); + self.inner.data.permissions.clear(); } if update_config || update_lists { - let core = self.core.core.clone(); - let inner = self.core.jmap_inner.clone(); + let server = self.inner.build_server(); tokio::spawn(async move { let result = if update_config { - core.load().reload().await + server.reload().await } else { - core.load().reload_blocked_ips().await + server.reload_blocked_ips().await }; match result { Ok(result) => { if let Some(new_core) = result.new_core { // Update core - core.store(new_core.into()); + server.inner.shared_core.store(new_core.into()); // Reload ACME - if inner + if server + .inner + .ipc .housekeeper_tx - .send(housekeeper::Event::ReloadSettings) + .send(HousekeeperEvent::ReloadSettings) .await .is_err() { diff --git a/crates/jmap/src/services/gossip/spawn.rs b/crates/jmap/src/services/gossip/spawn.rs index 579cf40a..f5d41056 100644 --- a/crates/jmap/src/services/gossip/spawn.rs +++ b/crates/jmap/src/services/gossip/spawn.rs @@ -5,11 +5,10 @@ */ use crate::auth::SymmetricEncrypt; -use crate::JmapInstance; use super::request::Request; use super::{Gossiper, Peer, UDP_MAX_PAYLOAD}; -use common::IPC_CHANNEL_BUFFER; +use common::{Inner, IPC_CHANNEL_BUFFER}; use std::net::IpAddr; use std::time::{Duration, Instant}; use std::{net::SocketAddr, sync::Arc}; @@ -64,7 +63,7 @@ impl GossiperBuilder { builder.into() } - pub async fn spawn(self, core: JmapInstance, mut shutdown_rx: watch::Receiver<bool>) { + pub async fn spawn(self, inner: Arc<Inner>, mut shutdown_rx: watch::Receiver<bool>) { // Bind port let quidnunc = Arc::new(Quidnunc { socket: match UdpSocket::bind(SocketAddr::new(self.bind_addr, self.port)).await { @@ -100,7 +99,7 @@ impl GossiperBuilder { epoch: 0, peers: self.peers, last_peer_pinged: u32::MAX as usize, - core, + inner, gossip_tx, }; let quidnunc_ = quidnunc.clone(); diff --git a/crates/jmap/src/services/housekeeper.rs b/crates/jmap/src/services/housekeeper.rs index ed05f8c6..4387146a 100644 --- a/crates/jmap/src/services/housekeeper.rs +++ b/crates/jmap/src/services/housekeeper.rs @@ -6,10 +6,16 @@ use std::{ collections::BinaryHeap, + sync::{atomic::Ordering, Arc}, time::{Duration, Instant, SystemTime}, }; -use common::{config::telemetry::OtelMetrics, IPC_CHANNEL_BUFFER}; +use common::{ + config::telemetry::OtelMetrics, + core::BuildServer, + ipc::{HousekeeperEvent, PurgeType}, + Inner, +}; #[cfg(feature = "enterprise")] use common::telemetry::{ @@ -17,33 +23,13 @@ use common::telemetry::{ tracers::store::TracingStore, }; -use smtp::core::SMTP; -use store::{ - write::{now, purge::PurgeStore}, - BlobStore, LookupStore, Store, -}; +use smtp::reporting::SmtpReporting; +use store::write::{now, purge::PurgeStore}; use tokio::sync::mpsc; -use trc::{Collector, HousekeeperEvent, MetricType}; +use trc::{Collector, MetricType}; use utils::map::ttl_dashmap::TtlMap; -use crate::{Inner, JmapInstance, JMAP, LONG_SLUMBER}; - -pub enum Event { - AcmeReschedule { - provider_id: String, - renew_at: Instant, - }, - Purge(PurgeType), - ReloadSettings, - Exit, -} - -pub enum PurgeType { - Data(Store), - Blobs { store: Store, blob_store: BlobStore }, - Lookup(LookupStore), - Account(Option<u32>), -} +use crate::{email::delete::EmailDeletion, JmapMethods, LONG_SLUMBER}; #[derive(PartialEq, Eq)] struct Action { @@ -75,30 +61,30 @@ struct Queue { #[cfg(feature = "enterprise")] const METRIC_ALERTS_INTERVAL: Duration = Duration::from_secs(5 * 60); -pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { +pub fn spawn_housekeeper(inner: Arc<Inner>, mut rx: mpsc::Receiver<HousekeeperEvent>) { tokio::spawn(async move { - trc::event!(Housekeeper(HousekeeperEvent::Start)); + trc::event!(Housekeeper(trc::HousekeeperEvent::Start)); let start_time = SystemTime::now(); // Add all events to queue let mut queue = Queue::default(); { - let core_ = core.core.load_full(); + let server = inner.build_server(); // Session purge queue.schedule( - Instant::now() + core_.jmap.session_purge_frequency.time_to_next(), + Instant::now() + server.core.jmap.session_purge_frequency.time_to_next(), ActionClass::Session, ); // Account purge queue.schedule( - Instant::now() + core_.jmap.account_purge_frequency.time_to_next(), + Instant::now() + server.core.jmap.account_purge_frequency.time_to_next(), ActionClass::Account, ); // Store purges - for (idx, schedule) in core_.storage.purge_schedules.iter().enumerate() { + for (idx, schedule) in server.core.storage.purge_schedules.iter().enumerate() { queue.schedule( Instant::now() + schedule.cron.time_to_next(), ActionClass::Store(idx), @@ -106,7 +92,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { } // OTEL Push Metrics - if let Some(otel) = &core_.metrics.otel { + if let Some(otel) = &server.core.metrics.otel { OtelMetrics::enable_errors(); queue.schedule(Instant::now() + otel.interval, ActionClass::OtelMetrics); } @@ -115,8 +101,8 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { queue.schedule(Instant::now(), ActionClass::CalculateMetrics); // Add all ACME renewals to heap - for provider in core_.tls.acme_providers.values() { - match core_.init_acme(provider).await { + for provider in server.core.acme.providers.values() { + match server.init_acme(provider).await { Ok(renew_at) => { queue.schedule( Instant::now() + renew_at, @@ -135,7 +121,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { // Enterprise Edition license management #[cfg(feature = "enterprise")] - if let Some(enterprise) = &core_.enterprise { + if let Some(enterprise) = &server.core.enterprise { queue.schedule( Instant::now() + enterprise.license.expires_in(), ActionClass::ValidateLicense, @@ -166,12 +152,11 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { loop { match tokio::time::timeout(queue.wake_up_time(), rx.recv()).await { Ok(Some(event)) => match event { - Event::ReloadSettings => { - let core_ = core.core.load_full(); - let inner = core.jmap_inner.clone(); + HousekeeperEvent::ReloadSettings => { + let server = inner.build_server(); // Reload OTEL push metrics - match &core_.metrics.otel { + match &server.core.metrics.otel { Some(otel) if !queue.has_action(&ActionClass::OtelMetrics) => { OtelMetrics::enable_errors(); @@ -187,7 +172,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - if let Some(enterprise) = &core_.enterprise { + if let Some(enterprise) = &server.core.enterprise { if !queue.has_action(&ActionClass::ValidateLicense) { queue.schedule( Instant::now() + enterprise.license.expires_in(), @@ -214,12 +199,14 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { // Reload ACME certificates tokio::spawn(async move { - for provider in core_.tls.acme_providers.values() { - match core_.init_acme(provider).await { + for provider in server.core.acme.providers.values() { + match server.init_acme(provider).await { Ok(renew_at) => { - inner + server + .inner + .ipc .housekeeper_tx - .send(Event::AcmeReschedule { + .send(HousekeeperEvent::AcmeReschedule { provider_id: provider.id.clone(), renew_at: Instant::now() + renew_at, }) @@ -234,7 +221,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { } }); } - Event::AcmeReschedule { + HousekeeperEvent::AcmeReschedule { provider_id, renew_at, } => { @@ -242,22 +229,22 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { queue.remove_action(&action); queue.schedule(renew_at, action); } - Event::Purge(purge) => match purge { + HousekeeperEvent::Purge(purge) => match purge { PurgeType::Data(store) => { // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd <hello@stalw.art> // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - let trace_retention = core - .core + let trace_retention = inner + .shared_core .load() .enterprise .as_ref() .and_then(|e| e.trace_store.as_ref()) .and_then(|t| t.retention); #[cfg(feature = "enterprise")] - let metrics_retention = core - .core + let metrics_retention = inner + .shared_core .load() .enterprise .as_ref() @@ -267,7 +254,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { tokio::spawn(async move { trc::event!( - Housekeeper(HousekeeperEvent::PurgeStore), + Housekeeper(trc::HousekeeperEvent::PurgeStore), Type = "data" ); if let Err(err) = store.purge_store().await { @@ -294,7 +281,10 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { }); } PurgeType::Blobs { store, blob_store } => { - trc::event!(Housekeeper(HousekeeperEvent::PurgeStore), Type = "blob"); + trc::event!( + Housekeeper(trc::HousekeeperEvent::PurgeStore), + Type = "blob" + ); tokio::spawn(async move { if let Err(err) = store.purge_blobs(blob_store).await { @@ -303,7 +293,10 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { }); } PurgeType::Lookup(store) => { - trc::event!(Housekeeper(HousekeeperEvent::PurgeStore), Type = "lookup"); + trc::event!( + Housekeeper(trc::HousekeeperEvent::PurgeStore), + Type = "lookup" + ); tokio::spawn(async move { if let Err(err) = store.purge_lookup_store().await { @@ -312,45 +305,44 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { }); } PurgeType::Account(account_id) => { - let jmap = JMAP::from(core.clone()); + let server = inner.build_server(); tokio::spawn(async move { - trc::event!(Housekeeper(HousekeeperEvent::PurgeAccounts)); + trc::event!(Housekeeper(trc::HousekeeperEvent::PurgeAccounts)); if let Some(account_id) = account_id { - jmap.purge_account(account_id).await; + server.purge_account(account_id).await; } else { - jmap.purge_accounts().await; + server.purge_accounts().await; } }); } }, - Event::Exit => { - trc::event!(Housekeeper(HousekeeperEvent::Stop)); + HousekeeperEvent::Exit => { + trc::event!(Housekeeper(trc::HousekeeperEvent::Stop)); return; } }, Ok(None) => { - trc::event!(Housekeeper(HousekeeperEvent::Stop)); + trc::event!(Housekeeper(trc::HousekeeperEvent::Stop)); return; } Err(_) => { - let core_ = core.core.load_full(); + let server = inner.build_server(); while let Some(event) = queue.pop() { match event.event { ActionClass::Acme(provider_id) => { - let inner = core.jmap_inner.clone(); - let core = core_.clone(); + let server = server.clone(); tokio::spawn(async move { if let Some(provider) = - core.tls.acme_providers.get(&provider_id) + server.core.acme.providers.get(&provider_id) { trc::event!( Acme(trc::AcmeEvent::OrderStart), Hostname = provider.domains.as_slice() ); - let renew_at = match core.renew(provider).await { + let renew_at = match server.renew(provider).await { Ok(renew_at) => { trc::event!( Acme(trc::AcmeEvent::OrderCompleted), @@ -371,11 +363,13 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { } }; - inner.increment_config_version(); + server.increment_config_version(); - inner + server + .inner + .ipc .housekeeper_tx - .send(Event::AcmeReschedule { + .send(HousekeeperEvent::AcmeReschedule { provider_id: provider_id.clone(), renew_at: Instant::now() + renew_at, }) @@ -385,35 +379,48 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { }); } ActionClass::Account => { - let jmap = JMAP::from(core.clone()); - tokio::spawn(async move { - trc::event!(Housekeeper(HousekeeperEvent::PurgeAccounts)); - jmap.purge_accounts().await; - }); + let server = server.clone(); queue.schedule( Instant::now() - + core_.jmap.account_purge_frequency.time_to_next(), + + server.core.jmap.account_purge_frequency.time_to_next(), ActionClass::Account, ); - } - ActionClass::Session => { - let inner = core.jmap_inner.clone(); - let core = core_.clone(); - tokio::spawn(async move { - trc::event!(Housekeeper(HousekeeperEvent::PurgeSessions)); - inner.purge(); - core.security.access_tokens.cleanup(); + trc::event!(Housekeeper(trc::HousekeeperEvent::PurgeAccounts)); + server.purge_accounts().await; }); + } + ActionClass::Session => { + let server = server.clone(); queue.schedule( Instant::now() - + core_.jmap.session_purge_frequency.time_to_next(), + + server.core.jmap.session_purge_frequency.time_to_next(), ActionClass::Session, ); + + tokio::spawn(async move { + trc::event!(Housekeeper(trc::HousekeeperEvent::PurgeSessions)); + server.inner.data.http_auth_cache.cleanup(); + server + .inner + .data + .jmap_limiter + .retain(|_, limiter| limiter.is_active()); + server.inner.data.access_tokens.cleanup(); + + for throttle in [ + &server.inner.data.smtp_session_throttle, + &server.inner.data.smtp_queue_throttle, + ] { + throttle.retain(|_, v| { + v.concurrent.load(Ordering::Relaxed) > 0 + }); + } + }); } ActionClass::Store(idx) => { if let Some(schedule) = - core_.storage.purge_schedules.get(idx).cloned() + server.core.storage.purge_schedules.get(idx).cloned() { queue.schedule( Instant::now() + schedule.cron.time_to_next(), @@ -435,7 +442,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { match result { Ok(_) => { trc::event!( - Housekeeper(HousekeeperEvent::PurgeStore), + Housekeeper(trc::HousekeeperEvent::PurgeStore), Id = schedule.store_id ); } @@ -451,16 +458,22 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { } } ActionClass::OtelMetrics => { - if let Some(otel) = &core_.metrics.otel { + if let Some(otel) = &server.core.metrics.otel { queue.schedule( Instant::now() + otel.interval, ActionClass::OtelMetrics, ); let otel = otel.clone(); - let core = core_.clone(); + + #[cfg(feature = "enterprise")] + let is_enterprise = server.is_enterprise_edition(); + + #[cfg(not(feature = "enterprise"))] + let is_enterprise = false; + tokio::spawn(async move { - otel.push_metrics(core, start_time).await; + otel.push_metrics(is_enterprise, start_time).await; }); } } @@ -479,12 +492,12 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { false }; - let core = core_.clone(); + let server = server.clone(); tokio::spawn(async move { #[cfg(feature = "enterprise")] - if core.is_enterprise_edition() { + if server.is_enterprise_edition() { // Obtain queue size - match core.total_queued_messages().await { + match server.total_queued_messages().await { Ok(total) => { Collector::update_gauge( MetricType::QueueCount, @@ -500,7 +513,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { } if update_other_metrics { - match core.total_accounts().await { + match server.total_accounts().await { Ok(total) => { Collector::update_gauge( MetricType::UserCount, @@ -514,7 +527,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { } } - match core.total_domains().await { + match server.total_domains().await { Ok(total) => { Collector::update_gauge( MetricType::DomainCount, @@ -556,7 +569,8 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] ActionClass::InternalMetrics => { - if let Some(metrics_store) = &core_ + if let Some(metrics_store) = &server + .core .enterprise .as_ref() .and_then(|e| e.metrics_store.as_ref()) @@ -568,7 +582,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { let metrics_store = metrics_store.store.clone(); let metrics_history = metrics_history.clone(); - let core = core_.clone(); + let core = server.core.clone(); tokio::spawn(async move { if let Err(err) = metrics_store .write_metrics(core, now(), metrics_history) @@ -582,22 +596,20 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { #[cfg(feature = "enterprise")] ActionClass::AlertMetrics => { - let smtp = SMTP { - core: core_.clone(), - inner: core.smtp_inner.clone(), - }; + let server = server.clone(); tokio::spawn(async move { - if let Some(messages) = smtp.core.process_alerts().await { + if let Some(messages) = server.process_alerts().await { for message in messages { - smtp.send_autogenerated( - message.from, - message.to.into_iter(), - message.body, - None, - 0, - ) - .await; + server + .send_autogenerated( + message.from, + message.to.into_iter(), + message.body, + None, + 0, + ) + .await; } } }); @@ -605,7 +617,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { #[cfg(feature = "enterprise")] ActionClass::ValidateLicense => { - match core_.reload().await { + match server.reload().await { Ok(result) => { if let Some(new_core) = result.new_core { if let Some(enterprise) = &new_core.enterprise { @@ -617,10 +629,10 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { } // Update core - core.core.store(new_core.into()); + server.inner.shared_core.store(new_core.into()); // Increment version counter - core.jmap_inner.increment_config_version(); + server.increment_config_version(); } } Err(err) => { @@ -639,7 +651,7 @@ pub fn spawn_housekeeper(core: JmapInstance, mut rx: mpsc::Receiver<Event>) { impl Queue { pub fn schedule(&mut self, due: Instant, event: ActionClass) { trc::event!( - Housekeeper(HousekeeperEvent::Schedule), + Housekeeper(trc::HousekeeperEvent::Schedule), Due = trc::Value::Timestamp( now() + due.saturating_duration_since(Instant::now()).as_secs() ), @@ -684,15 +696,3 @@ impl PartialOrd for Action { Some(self.cmp(other)) } } - -impl Inner { - pub fn purge(&self) { - self.sessions.cleanup(); - self.concurrency_limiter - .retain(|_, limiter| limiter.is_active()); - } -} - -pub fn init_housekeeper() -> (mpsc::Sender<Event>, mpsc::Receiver<Event>) { - mpsc::channel::<Event>(IPC_CHANNEL_BUFFER) -} diff --git a/crates/jmap/src/services/index.rs b/crates/jmap/src/services/index.rs index 5a17fa3e..7481cbee 100644 --- a/crates/jmap/src/services/index.rs +++ b/crates/jmap/src/services/index.rs @@ -6,6 +6,7 @@ use std::{sync::Arc, time::Instant}; +use common::{core::BuildServer, Inner, Server}; use directory::{ backend::internal::{manage::ManageDirectory, PrincipalField}, Type, @@ -22,17 +23,19 @@ use store::{ Deserialize, IterateParams, Serialize, ValueKey, U32_LEN, U64_LEN, }; -use tokio::sync::Notify; +use std::future::Future; use trc::{AddContext, FtsIndexEvent}; use utils::{BlobHash, BLOB_HASH_LEN}; use crate::{ + blob::download::BlobDownload, + changes::write::ChangeLog, email::{index::IndexMessageText, metadata::MessageMetadata}, - Inner, JmapInstance, JMAP, + JmapMethods, }; #[derive(Debug)] -struct IndexEmail { +pub struct IndexEmail { account_id: u32, document_id: u32, seq: u64, @@ -42,11 +45,12 @@ struct IndexEmail { const INDEX_LOCK_EXPIRY: u64 = 60 * 5; -pub fn spawn_index_task(core: JmapInstance, rx: Arc<Notify>) { +pub fn spawn_index_task(inner: Arc<Inner>) { tokio::spawn(async move { + let rx = inner.ipc.index_tx.clone(); loop { // Index any queued messages - JMAP::from(core.clone()).fts_index_queued().await; + inner.build_server().fts_index_queued().await; // Wait for a signal to index more messages rx.notified().await; @@ -54,8 +58,19 @@ pub fn spawn_index_task(core: JmapInstance, rx: Arc<Notify>) { }); } -impl JMAP { - pub async fn fts_index_queued(&self) { +pub trait Indexer: Sync + Send { + fn fts_index_queued(&self) -> impl Future<Output = ()> + Send; + fn try_lock_index(&self, event: &IndexEmail) -> impl Future<Output = bool> + Send; + fn reindex( + &self, + account_id: Option<u32>, + tenant_id: Option<u32>, + ) -> impl Future<Output = trc::Result<()>> + Send; + fn request_fts_index(&self); +} + +impl Indexer for Server { + async fn fts_index_queued(&self) { let from_key = ValueKey::<ValueClass<u32>> { account_id: 0, collection: 0, @@ -243,15 +258,11 @@ impl JMAP { } } - pub fn request_fts_index(&self) { - self.inner.request_fts_index(); + fn request_fts_index(&self) { + self.inner.ipc.index_tx.notify_one(); } - pub async fn reindex( - &self, - account_id: Option<u32>, - tenant_id: Option<u32>, - ) -> trc::Result<()> { + async fn reindex(&self, account_id: Option<u32>, tenant_id: Option<u32>) -> trc::Result<()> { let accounts = if let Some(account_id) = account_id { RoaringBitmap::from_sorted_iter([account_id]).unwrap() } else { @@ -362,12 +373,6 @@ impl JMAP { } } -impl Inner { - pub fn request_fts_index(&self) { - self.index_tx.notify_one(); - } -} - impl IndexEmail { fn value_class(&self) -> ValueClass<MaybeDynamicId> { ValueClass::FtsQueue(FtsQueueClass { diff --git a/crates/jmap/src/services/ingest.rs b/crates/jmap/src/services/ingest.rs index 07967628..aadeb15b 100644 --- a/crates/jmap/src/services/ingest.rs +++ b/crates/jmap/src/services/ingest.rs @@ -4,20 +4,33 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{DeliveryResult, IngestMessage}; +use common::{ + ipc::{DeliveryResult, IngestMessage}, + Server, +}; use directory::Permission; use jmap_proto::types::{state::StateChange, type_state::DataType}; use mail_parser::MessageParser; +use std::future::Future; use store::ahash::AHashMap; use crate::{ - email::ingest::{IngestEmail, IngestSource}, + email::ingest::{EmailIngest, IngestEmail, IngestSource}, mailbox::INBOX_ID, - JMAP, + sieve::{get::SieveScriptGet, ingest::SieveScriptIngest}, }; -impl JMAP { - pub async fn deliver_message(&self, message: IngestMessage) -> Vec<DeliveryResult> { +use super::state::StateManager; + +pub trait MailDelivery: Sync + Send { + fn deliver_message( + &self, + message: IngestMessage, + ) -> impl Future<Output = Vec<DeliveryResult>> + Send; +} + +impl MailDelivery for Server { + async fn deliver_message(&self, message: IngestMessage) -> Vec<DeliveryResult> { // Read message let raw_message = match self .core @@ -60,7 +73,6 @@ impl JMAP { let mut deliver_names = AHashMap::with_capacity(message.recipients.len()); for rcpt in &message.recipients { match self - .core .email_to_ids(&self.core.storage.directory, rcpt, message.session_id) .await { @@ -84,15 +96,11 @@ impl JMAP { // Deliver to each recipient for (uid, (status, rcpt)) in &mut deliver_names { // Obtain access token - let result = match self - .core - .get_cached_access_token(*uid) - .await - .and_then(|token| { - token - .assert_has_permission(Permission::EmailReceive) - .map(|_| token) - }) { + let result = match self.get_cached_access_token(*uid).await.and_then(|token| { + token + .assert_has_permission(Permission::EmailReceive) + .map(|_| token) + }) { Ok(access_token) => { // Check if there is an active sieve script match self.sieve_script_get_active(*uid).await { diff --git a/crates/jmap/src/services/state.rs b/crates/jmap/src/services/state.rs index 6f448d50..670f5ff9 100644 --- a/crates/jmap/src/services/state.rs +++ b/crates/jmap/src/services/state.rs @@ -4,39 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::{Duration, Instant, SystemTime}; +use std::{ + sync::Arc, + time::{Duration, Instant, SystemTime}, +}; -use common::IPC_CHANNEL_BUFFER; +use common::{ + core::BuildServer, + ipc::{PushSubscription, StateEvent, UpdateSubscription}, + Inner, Server, IPC_CHANNEL_BUFFER, +}; use jmap_proto::types::{id::Id, state::StateChange, type_state::DataType}; +use std::future::Future; use store::ahash::AHashMap; use tokio::sync::mpsc; use trc::ServerEvent; use utils::map::bitmap::Bitmap; -use crate::{ - push::{manager::spawn_push_manager, UpdateSubscription}, - JmapInstance, JMAP, -}; - -#[derive(Debug)] -pub enum Event { - Subscribe { - account_id: u32, - types: Bitmap<DataType>, - tx: mpsc::Sender<StateChange>, - }, - Publish { - state_change: StateChange, - }, - UpdateSharedAccounts { - account_id: u32, - }, - UpdateSubscriptions { - account_id: u32, - subscriptions: Vec<UpdateSubscription>, - }, - Stop, -} +use crate::push::{get::PushSubscriptionFetch, manager::spawn_push_manager}; #[derive(Debug)] struct Subscriber { @@ -62,10 +47,6 @@ impl Subscriber { const PURGE_EVERY: Duration = Duration::from_secs(3600); const SEND_TIMEOUT: Duration = Duration::from_millis(500); -pub fn init_state_manager() -> (mpsc::Sender<Event>, mpsc::Receiver<Event>) { - mpsc::channel::<Event>(IPC_CHANNEL_BUFFER) -} - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] enum SubscriberId { Ipc(u32), @@ -73,8 +54,8 @@ enum SubscriberId { } #[allow(clippy::unwrap_or_default)] -pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Event>) { - let push_tx = spawn_push_manager(core.clone()); +pub fn spawn_state_manager(inner: Arc<Inner>, mut change_rx: mpsc::Receiver<StateEvent>) { + let push_tx = spawn_push_manager(inner.clone()); tokio::spawn(async move { let mut subscribers: AHashMap<u32, AHashMap<SubscriberId, Subscriber>> = @@ -89,7 +70,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve let mut purge_needed = last_purge.elapsed() >= PURGE_EVERY; match event { - Event::Stop => { + StateEvent::Stop => { if push_tx.send(crate::push::Event::Reset).await.is_err() { trc::event!( Server(ServerEvent::ThreadError), @@ -99,13 +80,9 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve } break; } - Event::UpdateSharedAccounts { account_id } => { + StateEvent::UpdateSharedAccounts { account_id } => { // Obtain account membership and shared mailboxes - let acl = match JMAP::from(core.clone()) - .core - .get_access_token(account_id) - .await - { + let acl = match inner.build_server().get_access_token(account_id).await { Ok(result) => result, Err(err) => { trc::error!(err @@ -169,7 +146,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve } shared_accounts.insert(account_id, shared_account_ids); } - Event::Subscribe { + StateEvent::Subscribe { account_id, types, tx, @@ -185,7 +162,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve }, ); } - Event::Publish { state_change } => { + StateEvent::Publish { state_change } => { if let Some(shared_accounts) = shared_accounts_map.get(&state_change.account_id) { let current_time = SystemTime::now() @@ -266,7 +243,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve } } } - Event::UpdateSubscriptions { + StateEvent::UpdateSubscriptions { account_id, subscriptions, } => { @@ -280,7 +257,7 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve if let SubscriberId::Push(push_id) = subscriber_id { if !subscriptions.iter().any(|s| { matches!(s, UpdateSubscription::Verified( - crate::push::PushSubscription { id, .. } + PushSubscription { id, .. } ) if id == push_id) }) { remove_ids.push(*subscriber_id); @@ -388,18 +365,33 @@ pub fn spawn_state_manager(core: JmapInstance, mut change_rx: mpsc::Receiver<Eve }); } -impl JMAP { - pub async fn subscribe_state_manager( +pub trait StateManager: Sync + Send { + fn subscribe_state_manager( + &self, + account_id: u32, + types: Bitmap<DataType>, + ) -> impl Future<Output = trc::Result<mpsc::Receiver<StateChange>>> + Send; + + fn broadcast_state_change( + &self, + state_change: StateChange, + ) -> impl Future<Output = bool> + Send; + + fn update_push_subscriptions(&self, account_id: u32) -> impl Future<Output = bool> + Send; +} + +impl StateManager for Server { + async fn subscribe_state_manager( &self, account_id: u32, types: Bitmap<DataType>, ) -> trc::Result<mpsc::Receiver<StateChange>> { let (change_tx, change_rx) = mpsc::channel::<StateChange>(IPC_CHANNEL_BUFFER); - let state_tx = self.inner.state_tx.clone(); + let state_tx = self.inner.ipc.state_tx.clone(); for event in [ - Event::UpdateSharedAccounts { account_id }, - Event::Subscribe { + StateEvent::UpdateSharedAccounts { account_id }, + StateEvent::Subscribe { account_id, types, tx: change_tx, @@ -415,12 +407,13 @@ impl JMAP { Ok(change_rx) } - pub async fn broadcast_state_change(&self, state_change: StateChange) -> bool { + async fn broadcast_state_change(&self, state_change: StateChange) -> bool { match self .inner + .ipc .state_tx .clone() - .send(Event::Publish { state_change }) + .send(StateEvent::Publish { state_change }) .await { Ok(_) => true, @@ -436,7 +429,7 @@ impl JMAP { } } - pub async fn update_push_subscriptions(&self, account_id: u32) -> bool { + async fn update_push_subscriptions(&self, account_id: u32) -> bool { let push_subs = match self.fetch_push_subscriptions(account_id).await { Ok(push_subs) => push_subs, Err(err) => { @@ -447,8 +440,8 @@ impl JMAP { } }; - let state_tx = self.inner.state_tx.clone(); - for event in [Event::UpdateSharedAccounts { account_id }, push_subs] { + let state_tx = self.inner.ipc.state_tx.clone(); + for event in [StateEvent::UpdateSharedAccounts { account_id }, push_subs] { if state_tx.send(event).await.is_err() { trc::event!( Server(ServerEvent::ThreadError), diff --git a/crates/jmap/src/sieve/get.rs b/crates/jmap/src/sieve/get.rs index 7bee50a3..d40bea1d 100644 --- a/crates/jmap/src/sieve/get.rs +++ b/crates/jmap/src/sieve/get.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use common::Server; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, @@ -18,12 +19,42 @@ use store::{ BlobClass, Deserialize, Serialize, }; -use crate::{sieve::SeenIds, JMAP}; +use crate::{ + blob::{download::BlobDownload, upload::BlobUpload}, + changes::state::StateManager, + sieve::SeenIds, + JmapMethods, +}; use super::ActiveScript; +use std::future::Future; + +pub trait SieveScriptGet: Sync + Send { + fn sieve_script_get( + &self, + request: GetRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; -impl JMAP { - pub async fn sieve_script_get( + fn sieve_script_get_active( + &self, + account_id: u32, + ) -> impl Future<Output = trc::Result<Option<ActiveScript>>> + Send; + + fn sieve_script_get_by_name( + &self, + account_id: u32, + name: &str, + ) -> impl Future<Output = trc::Result<Option<Sieve>>> + Send; + + fn sieve_script_compile( + &self, + account_id: u32, + document_id: u32, + ) -> impl Future<Output = trc::Result<(Sieve, Object<Value>)>> + Send; +} + +impl SieveScriptGet for Server { + async fn sieve_script_get( &self, mut request: GetRequest<RequestArguments>, ) -> trc::Result<GetResponse> { @@ -115,10 +146,7 @@ impl JMAP { Ok(response) } - pub async fn sieve_script_get_active( - &self, - account_id: u32, - ) -> trc::Result<Option<ActiveScript>> { + async fn sieve_script_get_active(&self, account_id: u32) -> trc::Result<Option<ActiveScript>> { // Find the currently active script if let Some(document_id) = self .filter( @@ -156,7 +184,7 @@ impl JMAP { } } - pub async fn sieve_script_get_by_name( + async fn sieve_script_get_by_name( &self, account_id: u32, name: &str, diff --git a/crates/jmap/src/sieve/ingest.rs b/crates/jmap/src/sieve/ingest.rs index 16ecbe1f..08ba6051 100644 --- a/crates/jmap/src/sieve/ingest.rs +++ b/crates/jmap/src/sieve/ingest.rs @@ -6,7 +6,7 @@ use std::borrow::Cow; -use common::{auth::AccessToken, listener::stream::NullIo}; +use common::{auth::AccessToken, listener::stream::NullIo, Server}; use directory::{backend::internal::PrincipalField, QueryBy}; use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; use mail_parser::MessageParser; @@ -19,13 +19,14 @@ use store::{ use trc::{AddContext, SieveEvent}; use crate::{ - email::ingest::{IngestEmail, IngestSource, IngestedEmail}, - mailbox::{INBOX_ID, TRASH_ID}, + email::ingest::{EmailIngest, IngestEmail, IngestSource, IngestedEmail}, + mailbox::{get::MailboxGet, set::MailboxSet, INBOX_ID, TRASH_ID}, sieve::SeenIdHash, - JMAP, + JmapMethods, }; -use super::ActiveScript; +use super::{get::SieveScriptGet, ActiveScript}; +use std::future::Future; struct SieveMessage<'x> { pub raw_message: Cow<'x, [u8]>, @@ -33,9 +34,21 @@ struct SieveMessage<'x> { pub flags: Vec<Keyword>, } -impl JMAP { +pub trait SieveScriptIngest: Sync + Send { + fn sieve_script_ingest( + &self, + access_token: &AccessToken, + raw_message: &[u8], + envelope_from: &str, + envelope_to: &str, + session_id: u64, + active_script: ActiveScript, + ) -> impl Future<Output = trc::Result<IngestedEmail>> + Send; +} + +impl SieveScriptIngest for Server { #[allow(clippy::blocks_in_conditions)] - pub async fn sieve_script_ingest( + async fn sieve_script_ingest( &self, access_token: &AccessToken, raw_message: &[u8], @@ -124,8 +137,7 @@ impl JMAP { } } sieve::Script::Global(name_) => { - if let Some(script) = - self.core.get_untrusted_sieve_script(name_, session_id) + if let Some(script) = self.get_untrusted_sieve_script(name_, session_id) { input = Input::script(name, script.clone()); } else { @@ -353,7 +365,7 @@ impl JMAP { ); Session::<NullIo>::sieve( - self.smtp.clone(), + self.clone(), SessionAddress::new(mail_from.clone()), recipients, message.raw_message.to_vec(), diff --git a/crates/jmap/src/sieve/query.rs b/crates/jmap/src/sieve/query.rs index 144ccd65..0e1c8cc9 100644 --- a/crates/jmap/src/sieve/query.rs +++ b/crates/jmap/src/sieve/query.rs @@ -4,18 +4,27 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use jmap_proto::{ method::query::{ Comparator, Filter, QueryRequest, QueryResponse, RequestArguments, SortProperty, }, types::{collection::Collection, property::Property}, }; +use std::future::Future; use store::query::{self}; -use crate::JMAP; +use crate::JmapMethods; -impl JMAP { - pub async fn sieve_script_query( +pub trait SieveScriptQuery: Sync + Send { + fn sieve_script_query( + &self, + request: QueryRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<QueryResponse>> + Send; +} + +impl SieveScriptQuery for Server { + async fn sieve_script_query( &self, mut request: QueryRequest<RequestArguments>, ) -> trc::Result<QueryResponse> { diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 12f38da9..7ba9eec0 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::{AccessToken, ResourceToken}; +use common::{ + auth::{AccessToken, ResourceToken}, + Server, +}; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{SetRequest, SetResponse}, @@ -34,9 +37,15 @@ use store::{ BlobClass, }; -use crate::{api::http::HttpSessionData, JMAP}; +use crate::{ + api::http::HttpSessionData, + blob::{download::BlobDownload, upload::BlobUpload}, + changes::write::ChangeLog, + JmapMethods, +}; +use std::future::Future; -struct SetContext<'x> { +pub struct SetContext<'x> { resource_token: ResourceToken, access_token: &'x AccessToken, response: SetResponse, @@ -53,8 +62,38 @@ pub static SCHEMA: &[IndexProperty] = &[ IndexProperty::new(Property::IsActive).index_as(IndexAs::Integer), ]; -impl JMAP { - pub async fn sieve_script_set( +pub trait SieveScriptSet: Sync + Send { + fn sieve_script_set( + &self, + request: SetRequest<SetArguments>, + access_token: &AccessToken, + session: &HttpSessionData, + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; + + fn sieve_script_delete( + &self, + resource_token: &ResourceToken, + document_id: u32, + fail_if_active: bool, + ) -> impl Future<Output = trc::Result<bool>> + Send; + + fn sieve_set_item( + &self, + changes_: Object<SetValue>, + update: Option<(u32, HashedValue<Object<Value>>)>, + ctx: &SetContext, + session_id: u64, + ) -> impl Future<Output = trc::Result<Result<(ObjectIndexBuilder, Option<Vec<u8>>), SetError>>> + Send; + + fn sieve_activate_script( + &self, + account_id: u32, + activate_id: Option<u32>, + ) -> impl Future<Output = trc::Result<Vec<(u32, bool)>>> + Send; +} + +impl SieveScriptSet for Server { + async fn sieve_script_set( &self, mut request: SetRequest<SetArguments>, access_token: &AccessToken, @@ -343,7 +382,7 @@ impl JMAP { Ok(ctx.response) } - pub async fn sieve_script_delete( + async fn sieve_script_delete( &self, resource_token: &ResourceToken, document_id: u32, @@ -581,7 +620,7 @@ impl JMAP { .map(|obj| (obj, blob_update))) } - pub async fn sieve_activate_script( + async fn sieve_activate_script( &self, account_id: u32, mut activate_id: Option<u32>, diff --git a/crates/jmap/src/sieve/validate.rs b/crates/jmap/src/sieve/validate.rs index 2413a572..81b19fbb 100644 --- a/crates/jmap/src/sieve/validate.rs +++ b/crates/jmap/src/sieve/validate.rs @@ -4,16 +4,25 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::validate::{ValidateSieveScriptRequest, ValidateSieveScriptResponse}, }; +use std::future::Future; -use crate::JMAP; +use crate::blob::download::BlobDownload; -impl JMAP { - pub async fn sieve_script_validate( +pub trait SieveScriptValidate: Sync + Send { + fn sieve_script_validate( + &self, + request: ValidateSieveScriptRequest, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<ValidateSieveScriptResponse>> + Send; +} + +impl SieveScriptValidate for Server { + async fn sieve_script_validate( &self, request: ValidateSieveScriptRequest, access_token: &AccessToken, diff --git a/crates/jmap/src/submission/get.rs b/crates/jmap/src/submission/get.rs index f08cb166..ef9ca794 100644 --- a/crates/jmap/src/submission/get.rs +++ b/crates/jmap/src/submission/get.rs @@ -4,17 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, types::{collection::Collection, property::Property, value::Value}, }; -use smtp::queue; +use smtp::queue::{self, spool::SmtpSpool}; +use std::future::Future; -use crate::JMAP; +use crate::{changes::state::StateManager, JmapMethods}; -impl JMAP { - pub async fn email_submission_get( +pub trait EmailSubmissionGet: Sync + Send { + fn email_submission_get( + &self, + request: GetRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; +} + +impl EmailSubmissionGet for Server { + async fn email_submission_get( &self, mut request: GetRequest<RequestArguments>, ) -> trc::Result<GetResponse> { @@ -79,7 +88,6 @@ impl JMAP { // Obtain queueId let queued_message = self - .smtp .read_message(push.get(&Property::MessageId).as_uint().unwrap_or(u64::MAX)) .await; diff --git a/crates/jmap/src/submission/query.rs b/crates/jmap/src/submission/query.rs index 80ce88bc..fb1592e3 100644 --- a/crates/jmap/src/submission/query.rs +++ b/crates/jmap/src/submission/query.rs @@ -4,18 +4,27 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use jmap_proto::{ method::query::{ Comparator, Filter, QueryRequest, QueryResponse, RequestArguments, SortProperty, }, types::{collection::Collection, property::Property}, }; +use std::future::Future; use store::query::{self}; -use crate::JMAP; +use crate::JmapMethods; -impl JMAP { - pub async fn email_submission_query( +pub trait EmailSubmissionQuery: Sync + Send { + fn email_submission_query( + &self, + request: QueryRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<QueryResponse>> + Send; +} + +impl EmailSubmissionQuery for Server { + async fn email_submission_query( &self, mut request: QueryRequest<RequestArguments>, ) -> trc::Result<QueryResponse> { diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs index 54e64a16..c92ae0da 100644 --- a/crates/jmap/src/submission/set.rs +++ b/crates/jmap/src/submission/set.rs @@ -6,7 +6,10 @@ use std::{collections::HashMap, sync::Arc}; -use common::listener::{stream::NullIo, ServerInstance}; +use common::{ + listener::{stream::NullIo, ServerInstance}, + Server, +}; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{self, SetRequest, SetResponse}, @@ -29,12 +32,19 @@ use jmap_proto::{ }, }; use mail_parser::{HeaderName, HeaderValue}; -use smtp::core::{Session, SessionData, State}; +use smtp::{ + core::{Session, SessionData, State}, + queue::spool::SmtpSpool, +}; use smtp_proto::{request::parser::Rfc5321Parser, MailFrom, RcptTo}; use store::write::{assert::HashedValue, log::ChangeLogBuilder, now, BatchBuilder, Bincode}; use utils::map::vec_map::VecMap; -use crate::{email::metadata::MessageMetadata, identity::set::sanitize_email, JMAP}; +use crate::{ + blob::download::BlobDownload, changes::write::ChangeLog, email::metadata::MessageMetadata, + identity::set::sanitize_email, JmapMethods, +}; +use std::future::Future; pub static SCHEMA: &[IndexProperty] = &[ IndexProperty::new(Property::UndoStatus).index_as(IndexAs::Text { @@ -47,8 +57,25 @@ pub static SCHEMA: &[IndexProperty] = &[ IndexProperty::new(Property::SendAt).index_as(IndexAs::LongInteger), ]; -impl JMAP { - pub async fn email_submission_set( +pub trait EmailSubmissionSet: Sync + Send { + fn email_submission_set( + &self, + request: SetRequest<SetArguments>, + instance: &Arc<ServerInstance>, + next_call: &mut Option<Call<RequestMethod>>, + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; + + fn send_message( + &self, + account_id: u32, + response: &SetResponse, + instance: &Arc<ServerInstance>, + object: Object<SetValue>, + ) -> impl Future<Output = trc::Result<Result<Object<Value>, SetError>>> + Send; +} + +impl EmailSubmissionSet for Server { + async fn email_submission_set( &self, mut request: SetRequest<SetArguments>, instance: &Arc<ServerInstance>, @@ -147,10 +174,10 @@ impl JMAP { match undo_status { Some(undo_status) if undo_status == "canceled" => { - if let Some(queue_message) = self.smtp.read_message(queue_id).await { + if let Some(queue_message) = self.read_message(queue_id).await { // Delete message from queue let message_due = queue_message.next_event().unwrap_or_default(); - queue_message.remove(&self.smtp, message_due).await; + queue_message.remove(self, message_due).await; // Update record let mut batch = BatchBuilder::new(); @@ -553,7 +580,7 @@ impl JMAP { // Begin local SMTP session let mut session = - Session::<NullIo>::local(self.smtp.clone(), instance.clone(), SessionData::default()); + Session::<NullIo>::local(self.clone(), instance.clone(), SessionData::default()); // MAIL FROM let _ = session.handle_mail_from(mail_from).await; diff --git a/crates/jmap/src/thread/get.rs b/crates/jmap/src/thread/get.rs index 39403806..c328da38 100644 --- a/crates/jmap/src/thread/get.rs +++ b/crates/jmap/src/thread/get.rs @@ -4,18 +4,27 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, types::{collection::Collection, id::Id, property::Property}, }; +use std::future::Future; use store::query::{sort::Pagination, Comparator, ResultSet}; use trc::AddContext; -use crate::JMAP; +use crate::{changes::state::StateManager, JmapMethods}; -impl JMAP { - pub async fn thread_get( +pub trait ThreadGet: Sync + Send { + fn thread_get( + &self, + request: GetRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; +} + +impl ThreadGet for Server { + async fn thread_get( &self, mut request: GetRequest<RequestArguments>, ) -> trc::Result<GetResponse> { diff --git a/crates/jmap/src/vacation/get.rs b/crates/jmap/src/vacation/get.rs index 087d5561..de491cba 100644 --- a/crates/jmap/src/vacation/get.rs +++ b/crates/jmap/src/vacation/get.rs @@ -4,18 +4,32 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, object::Object, request::reference::MaybeReference, types::{any_id::AnyId, collection::Collection, id::Id, property::Property, value::Value}, }; +use std::future::Future; use store::query::Filter; -use crate::JMAP; +use crate::{changes::state::StateManager, JmapMethods}; -impl JMAP { - pub async fn vacation_response_get( +pub trait VacationResponseGet: Sync + Send { + fn vacation_response_get( + &self, + request: GetRequest<RequestArguments>, + ) -> impl Future<Output = trc::Result<GetResponse>> + Send; + + fn get_vacation_sieve_script_id( + &self, + account_id: u32, + ) -> impl Future<Output = trc::Result<Option<u32>>> + Send; +} + +impl VacationResponseGet for Server { + async fn vacation_response_get( &self, mut request: GetRequest<RequestArguments>, ) -> trc::Result<GetResponse> { @@ -100,7 +114,7 @@ impl JMAP { Ok(response) } - pub async fn get_vacation_sieve_script_id(&self, account_id: u32) -> trc::Result<Option<u32>> { + async fn get_vacation_sieve_script_id(&self, account_id: u32) -> trc::Result<Option<u32>> { self.filter( account_id, Collection::SieveScript, diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index 604e24f0..f924a782 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -6,7 +6,7 @@ use std::borrow::Cow; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use jmap_proto::{ error::set::{SetError, SetErrorType}, method::set::{RequestArguments, SetRequest, SetResponse}, @@ -22,6 +22,7 @@ use jmap_proto::{ }; use mail_builder::MessageBuilder; use mail_parser::decoders::html::html_to_text; +use std::future::Future; use store::write::{ assert::HashedValue, log::{Changes, LogInsert}, @@ -29,12 +30,26 @@ use store::write::{ }; use crate::{ - sieve::set::{ObjectBlobId, SCHEMA}, - JMAP, + blob::upload::BlobUpload, + changes::write::ChangeLog, + sieve::set::{ObjectBlobId, SieveScriptSet, SCHEMA}, + JmapMethods, }; -impl JMAP { - pub async fn vacation_response_set( +use super::get::VacationResponseGet; + +pub trait VacationResponseSet: Sync + Send { + fn vacation_response_set( + &self, + request: SetRequest<RequestArguments>, + access_token: &AccessToken, + ) -> impl Future<Output = trc::Result<SetResponse>> + Send; + + fn build_script(&self, obj: &mut ObjectIndexBuilder) -> trc::Result<Vec<u8>>; +} + +impl VacationResponseSet for Server { + async fn vacation_response_set( &self, mut request: SetRequest<RequestArguments>, access_token: &AccessToken, diff --git a/crates/jmap/src/websocket/stream.rs b/crates/jmap/src/websocket/stream.rs index 20fbac55..85fa8482 100644 --- a/crates/jmap/src/websocket/stream.rs +++ b/crates/jmap/src/websocket/stream.rs @@ -6,7 +6,7 @@ use std::{sync::Arc, time::Instant}; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use futures_util::{SinkExt, StreamExt}; use hyper::upgrade::Upgraded; use hyper_util::rt::TokioIo; @@ -23,12 +23,25 @@ use tungstenite::Message; use utils::map::bitmap::Bitmap; use crate::{ - api::http::{HttpSessionData, ToRequestError}, - JMAP, + api::{ + http::{HttpSessionData, ToRequestError}, + request::RequestHandler, + }, + services::state::StateManager, }; +use std::future::Future; + +pub trait WebSocketHandler: Sync + Send { + fn handle_websocket_stream( + &self, + stream: WebSocketStream<TokioIo<Upgraded>>, + access_token: Arc<AccessToken>, + session: HttpSessionData, + ) -> impl Future<Output = ()> + Send; +} -impl JMAP { - pub async fn handle_websocket_stream( +impl WebSocketHandler for Server { + async fn handle_websocket_stream( &self, mut stream: WebSocketStream<TokioIo<Upgraded>>, access_token: Arc<AccessToken>, diff --git a/crates/jmap/src/websocket/upgrade.rs b/crates/jmap/src/websocket/upgrade.rs index ee9aef1a..cdafc5ad 100644 --- a/crates/jmap/src/websocket/upgrade.rs +++ b/crates/jmap/src/websocket/upgrade.rs @@ -6,20 +6,29 @@ use std::sync::Arc; -use common::auth::AccessToken; +use common::{auth::AccessToken, Server}; use hyper::StatusCode; use hyper_util::rt::TokioIo; use tokio_tungstenite::WebSocketStream; use trc::JmapEvent; use tungstenite::{handshake::derive_accept_key, protocol::Role}; -use crate::{ - api::{http::HttpSessionData, HttpRequest, HttpResponse, HttpResponseBody}, - JMAP, -}; +use crate::api::{http::HttpSessionData, HttpRequest, HttpResponse, HttpResponseBody}; +use std::future::Future; -impl JMAP { - pub async fn upgrade_websocket_connection( +use super::stream::WebSocketHandler; + +pub trait WebSocketUpgrade: Sync + Send { + fn upgrade_websocket_connection( + &self, + req: HttpRequest, + access_token: Arc<AccessToken>, + session: HttpSessionData, + ) -> impl Future<Output = trc::Result<HttpResponse>> + Send; +} + +impl WebSocketUpgrade for Server { + async fn upgrade_websocket_connection( &self, req: HttpRequest, access_token: Arc<AccessToken>, diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 68335de2..c35c24d1 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -6,14 +6,13 @@ use std::time::Duration; -use common::{config::server::ServerProtocol, manager::boot::BootManager, Ipc, IPC_CHANNEL_BUFFER}; +use common::{config::server::ServerProtocol, core::BuildServer, manager::boot::BootManager}; use directory::backend::internal::MigrateDirectory; -use imap::core::{ImapSessionManager, IMAP}; -use jmap::{api::JmapSessionManager, services::gossip::spawn::GossiperBuilder, JMAP}; +use imap::core::ImapSessionManager; +use jmap::{api::JmapSessionManager, services::gossip::spawn::GossiperBuilder, StartServices}; use managesieve::core::ManageSieveSessionManager; use pop3::Pop3SessionManager; -use smtp::core::{SmtpSessionManager, SMTP}; -use tokio::sync::mpsc; +use smtp::{core::SmtpSessionManager, StartQueueManager}; use trc::Collector; use utils::wait_for_shutdown; @@ -27,72 +26,61 @@ static GLOBAL: Jemalloc = Jemalloc; #[tokio::main] async fn main() -> std::io::Result<()> { // Load config and apply macros - let init = BootManager::init().await; - - // Parse core - let mut config = init.config; - let core = init.core; - - // Setup IPC channels - let (delivery_tx, delivery_rx) = mpsc::channel(IPC_CHANNEL_BUFFER); - let ipc = Ipc { delivery_tx }; - - // Init servers - let smtp = SMTP::init( - &mut config, - core.clone(), - ipc, - init.servers.span_id_gen.clone(), - ) - .await; - let jmap = JMAP::init(&mut config, delivery_rx, core.clone(), smtp.inner.clone()).await; - let imap = IMAP::init(&mut config, jmap.clone()).await; - let gossiper = GossiperBuilder::try_parse(&mut config); + let mut init = BootManager::init().await; + + // Init services + init.start_services().await; + init.start_queue_manager(); + let gossiper = GossiperBuilder::try_parse(&mut init.config); // Log configuration errors - config.log_errors(); - config.log_warnings(); + init.config.log_errors(); + init.config.log_warnings(); + + { + let server = init.inner.build_server(); - // Log licensing information - #[cfg(feature = "enterprise")] - core.load().as_ref().log_license_details(); + // Log licensing information + #[cfg(feature = "enterprise")] + server.log_license_details(); - // Migrate directory - if let Err(err) = core.load().storage.data.migrate_directory().await { - trc::error!(err.details("Directory migration failed")); - std::process::exit(1); + // Migrate directory + if let Err(err) = server.store().migrate_directory().await { + trc::error!(err.details("Directory migration failed")); + std::process::exit(1); + } } // Spawn servers let (shutdown_tx, shutdown_rx) = init.servers.spawn(|server, acceptor, shutdown_rx| { match &server.protocol { ServerProtocol::Smtp | ServerProtocol::Lmtp => server.spawn( - SmtpSessionManager::new(smtp.clone()), - core.clone(), + SmtpSessionManager::new(init.inner.clone()), + init.inner.clone(), acceptor, shutdown_rx, ), ServerProtocol::Http => server.spawn( - JmapSessionManager::new(jmap.clone()), - core.clone(), + JmapSessionManager::new(init.inner.clone()), + init.inner.clone(), acceptor, shutdown_rx, ), ServerProtocol::Imap => server.spawn( - ImapSessionManager::new(imap.clone()), - core.clone(), + ImapSessionManager::new(init.inner.clone()), + init.inner.clone(), acceptor, shutdown_rx, ), ServerProtocol::Pop3 => server.spawn( - Pop3SessionManager::new(imap.clone()), - core.clone(), + Pop3SessionManager::new(init.inner.clone()), + init.inner.clone(), acceptor, shutdown_rx, ), ServerProtocol::ManageSieve => server.spawn( - ManageSieveSessionManager::new(imap.clone()), - core.clone(), + ManageSieveSessionManager::new(init.inner.clone()), + init.inner.clone(), acceptor, shutdown_rx, ), @@ -101,7 +89,7 @@ async fn main() -> std::io::Result<()> { // Spawn gossip if let Some(gossiper) = gossiper { - gossiper.spawn(jmap, shutdown_rx.clone()).await; + gossiper.spawn(init.inner, shutdown_rx.clone()).await; } // Wait for shutdown signal diff --git a/crates/managesieve/src/core/client.rs b/crates/managesieve/src/core/client.rs index 2380e8ef..87603b7e 100644 --- a/crates/managesieve/src/core/client.rs +++ b/crates/managesieve/src/core/client.rs @@ -118,7 +118,7 @@ impl<T: SessionStream> Session<T> { Command::Capability | Command::Logout | Command::Noop => Ok(command), Command::Authenticate => { if let State::NotAuthenticated { .. } = &self.state { - if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth { + if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { Ok(command) } else { Err(trc::ManageSieveEvent::Error @@ -151,9 +151,9 @@ impl<T: SessionStream> Session<T> { | Command::CheckScript | Command::Unauthenticate => { if let State::Authenticated { access_token, .. } = &self.state { - if let Some(rate) = &self.jmap.core.imap.rate_requests { + if let Some(rate) = &self.server.core.imap.rate_requests { if self - .jmap + .server .core .storage .lookup @@ -239,7 +239,7 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> { impl<T: AsyncWrite + AsyncRead> Session<T> { pub async fn get_script_id(&self, account_id: u32, name: &str) -> trc::Result<u32> { - self.jmap + self.server .core .storage .data diff --git a/crates/managesieve/src/core/mod.rs b/crates/managesieve/src/core/mod.rs index 1f89b895..20f41ab4 100644 --- a/crates/managesieve/src/core/mod.rs +++ b/crates/managesieve/src/core/mod.rs @@ -12,15 +12,13 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc}; use common::{ auth::AccessToken, listener::{limiter::InFlight, ServerInstance}, + Inner, Server, }; -use imap::core::{ImapInstance, Inner}; use imap_proto::receiver::{CommandParser, Receiver}; -use jmap::JMAP; use tokio::io::{AsyncRead, AsyncWrite}; pub struct Session<T: AsyncRead + AsyncWrite> { - pub jmap: JMAP, - pub imap: Arc<Inner>, + pub server: Server, pub instance: Arc<ServerInstance>, pub receiver: Receiver<Command>, pub state: State, @@ -51,12 +49,12 @@ impl State { #[derive(Clone)] pub struct ManageSieveSessionManager { - pub imap: ImapInstance, + pub inner: Arc<Inner>, } impl ManageSieveSessionManager { - pub fn new(imap: ImapInstance) -> Self { - Self { imap } + pub fn new(inner: Arc<Inner>) -> Self { + Self { inner } } } diff --git a/crates/managesieve/src/core/session.rs b/crates/managesieve/src/core/session.rs index 910fe6a3..e56ed5cc 100644 --- a/crates/managesieve/src/core/session.rs +++ b/crates/managesieve/src/core/session.rs @@ -4,9 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::{SessionData, SessionManager, SessionResult, SessionStream}; +use common::{ + core::BuildServer, + listener::{SessionData, SessionManager, SessionResult, SessionStream}, +}; use imap_proto::receiver::{self, Receiver}; -use jmap::JMAP; use tokio_rustls::server::TlsStream; use crate::SERVER_GREETING; @@ -21,12 +23,11 @@ impl SessionManager for ManageSieveSessionManager { ) -> impl std::future::Future<Output = ()> + Send { async move { // Create session - let jmap = JMAP::from(self.imap.jmap_instance); + let server = self.inner.build_server(); let mut session = Session { - receiver: Receiver::with_max_request_size(jmap.core.imap.max_request_size) + receiver: Receiver::with_max_request_size(server.core.imap.max_request_size) .with_start_state(receiver::State::Command { is_uid: false }), - jmap, - imap: self.imap.imap_inner, + server, instance: session.instance, state: State::NotAuthenticated { auth_failures: 0 }, session_id: session.session_id, @@ -67,9 +68,9 @@ impl<T: SessionStream> Session<T> { tokio::select! { result = tokio::time::timeout( if !matches!(self.state, State::NotAuthenticated {..}) { - self.jmap.core.imap.timeout_auth + self.server.core.imap.timeout_auth } else { - self.jmap.core.imap.timeout_unauth + self.server.core.imap.timeout_unauth }, self.read(&mut buf)) => { match result { @@ -142,8 +143,7 @@ impl<T: SessionStream> Session<T> { instance: self.instance, in_flight: self.in_flight, session_id: self.session_id, - jmap: self.jmap, - imap: self.imap, + server: self.server, receiver: self.receiver, remote_addr: self.remote_addr, }) diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs index da67f2e5..5157c04a 100644 --- a/crates/managesieve/src/op/authenticate.rs +++ b/crates/managesieve/src/op/authenticate.rs @@ -4,14 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::{limiter::ConcurrencyLimiter, SessionStream}; +use common::{ + listener::{limiter::ConcurrencyLimiter, SessionStream}, + ConcurrencyLimiters, +}; use directory::Permission; use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain}; use imap_proto::{ protocol::authenticate::Mechanism, receiver::{self, Request}, }; -use jmap::auth::rate_limit::ConcurrencyLimiters; +use jmap::auth::{ + authenticate::Authenticator, oauth::token::TokenHandler, rate_limit::RateLimiter, +}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use std::sync::Arc; @@ -66,22 +71,22 @@ impl<T: SessionStream> Session<T> { }; // Throttle authentication requests - self.jmap.is_auth_allowed_soft(&self.remote_addr).await?; + self.server.is_auth_allowed_soft(&self.remote_addr).await?; // Authenticate let access_token = match credentials { Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => { - self.jmap + self.server .authenticate_plain(&username, &secret, self.remote_addr, self.session_id) .await } Credentials::OAuthBearer { token } => { match self - .jmap + .server .validate_access_token("access_token", &token) .await { - Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await, + Ok((account_id, _, _)) => self.server.get_access_token(account_id).await, Err(err) => Err(err), } } @@ -90,7 +95,7 @@ impl<T: SessionStream> Session<T> { if err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) { match &self.state { State::NotAuthenticated { auth_failures } - if *auth_failures < self.jmap.core.imap.max_auth_failures => + if *auth_failures < self.server.core.imap.max_auth_failures => { self.state = State::NotAuthenticated { auth_failures: auth_failures + 1, @@ -122,7 +127,7 @@ impl<T: SessionStream> Session<T> { // Cache access token let access_token = Arc::new(access_token); - self.jmap.core.cache_access_token(access_token.clone()); + self.server.cache_access_token(access_token.clone()); // Create session self.state = State::Authenticated { @@ -146,9 +151,11 @@ impl<T: SessionStream> Session<T> { } pub fn get_concurrency_limiter(&self, account_id: u32) -> Option<Arc<ConcurrencyLimiters>> { - let rate = self.jmap.core.imap.rate_concurrent?; - self.imap - .rate_limiter + let rate = self.server.core.imap.rate_concurrent?; + self.server + .inner + .data + .imap_limiter .get(&account_id) .map(|limiter| limiter.clone()) .unwrap_or_else(|| { @@ -156,7 +163,11 @@ impl<T: SessionStream> Session<T> { concurrent_requests: ConcurrencyLimiter::new(rate), concurrent_uploads: ConcurrencyLimiter::new(rate), }); - self.imap.rate_limiter.insert(account_id, limiter.clone()); + self.server + .inner + .data + .imap_limiter + .insert(account_id, limiter.clone()); limiter }) .into() diff --git a/crates/managesieve/src/op/capability.rs b/crates/managesieve/src/op/capability.rs index 13f825db..33c1fb47 100644 --- a/crates/managesieve/src/op/capability.rs +++ b/crates/managesieve/src/op/capability.rs @@ -21,13 +21,13 @@ impl<T: SessionStream> Session<T> { if !self.stream.is_tls() { response.extend_from_slice(b"\"STARTTLS\"\r\n"); } - if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth { + if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { response.extend_from_slice(b"\"SASL\" \"PLAIN OAUTHBEARER\"\r\n"); } else { response.extend_from_slice(b"\"SASL\" \"OAUTHBEARER\"\r\n"); }; if let Some(sieve) = - self.jmap + self.server .core .jmap .capabilities @@ -62,7 +62,7 @@ impl<T: SessionStream> Session<T> { ManageSieve(trc::ManageSieveEvent::Capabilities), SpanId = self.session_id, Tls = self.stream.is_tls(), - Strict = !self.jmap.core.imap.allow_plain_auth, + Strict = !self.server.core.imap.allow_plain_auth, Elapsed = op_start.elapsed() ); diff --git a/crates/managesieve/src/op/checkscript.rs b/crates/managesieve/src/op/checkscript.rs index 58f79217..8e70b921 100644 --- a/crates/managesieve/src/op/checkscript.rs +++ b/crates/managesieve/src/op/checkscript.rs @@ -26,7 +26,7 @@ impl<T: SessionStream> Session<T> { } let script = request.tokens.into_iter().next().unwrap().unwrap_bytes(); - self.jmap + self.server .core .sieve .untrusted_compiler diff --git a/crates/managesieve/src/op/deletescript.rs b/crates/managesieve/src/op/deletescript.rs index f620769e..1a2b218c 100644 --- a/crates/managesieve/src/op/deletescript.rs +++ b/crates/managesieve/src/op/deletescript.rs @@ -9,6 +9,7 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; use imap_proto::receiver::Request; +use jmap::{changes::write::ChangeLog, sieve::set::SieveScriptSet}; use jmap_proto::types::collection::Collection; use store::write::log::ChangeLogBuilder; use trc::AddContext; @@ -37,7 +38,7 @@ impl<T: SessionStream> Session<T> { let account_id = access_token.primary_id(); let document_id = self.get_script_id(account_id, &name).await?; if self - .jmap + .server .sieve_script_delete(&access_token.as_resource_token(), document_id, true) .await .caused_by(trc::location!())? @@ -45,7 +46,7 @@ impl<T: SessionStream> Session<T> { // Write changes let mut changelog = ChangeLogBuilder::new(); changelog.log_delete(Collection::SieveScript, document_id); - self.jmap + self.server .commit_changes(account_id, changelog) .await .caused_by(trc::location!())?; diff --git a/crates/managesieve/src/op/getscript.rs b/crates/managesieve/src/op/getscript.rs index e21f368d..8837521a 100644 --- a/crates/managesieve/src/op/getscript.rs +++ b/crates/managesieve/src/op/getscript.rs @@ -9,7 +9,7 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; use imap_proto::receiver::Request; -use jmap::sieve::set::ObjectBlobId; +use jmap::{blob::download::BlobDownload, sieve::set::ObjectBlobId, JmapMethods}; use jmap_proto::{ object::Object, types::{collection::Collection, property::Property, value::Value}, @@ -37,7 +37,7 @@ impl<T: SessionStream> Session<T> { let account_id = self.state.access_token().primary_id(); let document_id = self.get_script_id(account_id, &name).await?; let (blob_section, blob_hash) = self - .jmap + .server .get_property::<Object<Value>>( account_id, Collection::SieveScript, @@ -61,7 +61,7 @@ impl<T: SessionStream> Session<T> { .code(ResponseCode::TryLater) })?; let script = self - .jmap + .server .get_blob_section(&blob_hash, &blob_section) .await .caused_by(trc::location!())? diff --git a/crates/managesieve/src/op/havespace.rs b/crates/managesieve/src/op/havespace.rs index 056c6208..a2b1a212 100644 --- a/crates/managesieve/src/op/havespace.rs +++ b/crates/managesieve/src/op/havespace.rs @@ -9,6 +9,7 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; use imap_proto::receiver::Request; +use jmap::JmapMethods; use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; @@ -52,7 +53,7 @@ impl<T: SessionStream> Session<T> { if access_token.quota == 0 || size as i64 + self - .jmap + .server .get_used_quota(account_id) .await .caused_by(trc::location!())? diff --git a/crates/managesieve/src/op/listscripts.rs b/crates/managesieve/src/op/listscripts.rs index 5659d293..35fd38ca 100644 --- a/crates/managesieve/src/op/listscripts.rs +++ b/crates/managesieve/src/op/listscripts.rs @@ -8,6 +8,7 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; +use jmap::JmapMethods; use jmap_proto::{ object::Object, types::{collection::Collection, property::Property, value::Value}, @@ -24,7 +25,7 @@ impl<T: SessionStream> Session<T> { let op_start = Instant::now(); let account_id = self.state.access_token().primary_id(); let document_ids = self - .jmap + .server .get_document_ids(account_id, Collection::SieveScript) .await .caused_by(trc::location!())? @@ -39,7 +40,7 @@ impl<T: SessionStream> Session<T> { for document_id in document_ids { if let Some(script) = self - .jmap + .server .get_property::<Object<Value>>( account_id, Collection::SieveScript, diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs index d980a768..d7a36279 100644 --- a/crates/managesieve/src/op/putscript.rs +++ b/crates/managesieve/src/op/putscript.rs @@ -9,7 +9,11 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; use imap_proto::receiver::Request; -use jmap::sieve::set::{ObjectBlobId, SCHEMA}; +use jmap::{ + blob::upload::BlobUpload, + sieve::set::{ObjectBlobId, SCHEMA}, + JmapMethods, +}; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{blob::BlobId, collection::Collection, property::Property, value::Value}, @@ -54,19 +58,19 @@ impl<T: SessionStream> Session<T> { // Check quota let resource_token = self.state.access_token().as_resource_token(); let account_id = resource_token.account_id; - self.jmap + self.server .has_available_quota(&resource_token, script_bytes.len() as u64) .await .caused_by(trc::location!())?; if self - .jmap + .server .get_document_ids(account_id, Collection::SieveScript) .await .caused_by(trc::location!())? .map(|ids| ids.len() as usize) .unwrap_or(0) - > self.jmap.core.jmap.sieve_max_scripts + > self.server.core.jmap.sieve_max_scripts { return Err(trc::ManageSieveEvent::Error .into_err() @@ -76,7 +80,7 @@ impl<T: SessionStream> Session<T> { // Compile script match self - .jmap + .server .core .sieve .untrusted_compiler @@ -103,7 +107,7 @@ impl<T: SessionStream> Session<T> { if let Some(document_id) = self.validate_name(account_id, &name).await? { // Obtain script values let script = self - .jmap + .server .get_property::<HashedValue<Object<Value>>>( account_id, Collection::SieveScript, @@ -127,7 +131,7 @@ impl<T: SessionStream> Session<T> { // Write script blob let blob_id = BlobId::new( - self.jmap + self.server .put_blob(account_id, &script_bytes, false) .await .caused_by(trc::location!())? @@ -168,7 +172,7 @@ impl<T: SessionStream> Session<T> { // Update tenant quota #[cfg(feature = "enterprise")] - if self.jmap.core.is_enterprise_edition() { + if self.server.core.is_enterprise_edition() { if let Some(tenant) = resource_token.tenant { batch.add(DirectoryClass::UsedQuota(tenant.id), update_quota); } @@ -183,7 +187,7 @@ impl<T: SessionStream> Session<T> { .with_property(Property::BlobId, Value::BlobId(blob_id)), ), ); - self.jmap + self.server .write_batch(batch) .await .caused_by(trc::location!())?; @@ -199,7 +203,7 @@ impl<T: SessionStream> Session<T> { } else { // Write script blob let blob_id = BlobId::new( - self.jmap + self.server .put_blob(account_id, &script_bytes, false) .await? .hash, @@ -236,14 +240,14 @@ impl<T: SessionStream> Session<T> { // Update tenant quota #[cfg(feature = "enterprise")] - if self.jmap.core.is_enterprise_edition() { + if self.server.core.is_enterprise_edition() { if let Some(tenant) = resource_token.tenant { batch.add(DirectoryClass::UsedQuota(tenant.id), script_size); } } let assigned_ids = self - .jmap + .server .write_batch(batch) .await .caused_by(trc::location!())?; @@ -265,7 +269,7 @@ impl<T: SessionStream> Session<T> { Err(trc::ManageSieveEvent::Error .into_err() .details("Script name cannot be empty.")) - } else if name.len() > self.jmap.core.jmap.sieve_max_script_name { + } else if name.len() > self.server.core.jmap.sieve_max_script_name { Err(trc::ManageSieveEvent::Error .into_err() .details("Script name is too long.")) @@ -275,7 +279,7 @@ impl<T: SessionStream> Session<T> { .details("The 'vacation' name is reserved, please use a different name.")) } else { Ok(self - .jmap + .server .filter( account_id, Collection::SieveScript, diff --git a/crates/managesieve/src/op/renamescript.rs b/crates/managesieve/src/op/renamescript.rs index 6bd84345..8164768c 100644 --- a/crates/managesieve/src/op/renamescript.rs +++ b/crates/managesieve/src/op/renamescript.rs @@ -9,7 +9,7 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; use imap_proto::receiver::Request; -use jmap::sieve::set::SCHEMA; +use jmap::{changes::write::ChangeLog, sieve::set::SCHEMA, JmapMethods}; use jmap_proto::{ object::{index::ObjectIndexBuilder, Object}, types::{collection::Collection, property::Property, value::Value}, @@ -62,7 +62,7 @@ impl<T: SessionStream> Session<T> { // Obtain script values let script = self - .jmap + .server .get_property::<HashedValue<Object<Value>>>( account_id, Collection::SieveScript, @@ -92,13 +92,13 @@ impl<T: SessionStream> Session<T> { ), ); if !batch.is_empty() { - self.jmap + self.server .write_batch(batch) .await .caused_by(trc::location!())?; let mut changelog = ChangeLogBuilder::new(); changelog.log_update(Collection::SieveScript, document_id); - self.jmap + self.server .commit_changes(account_id, changelog) .await .caused_by(trc::location!())?; diff --git a/crates/managesieve/src/op/setactive.rs b/crates/managesieve/src/op/setactive.rs index fa88b725..4ea6283c 100644 --- a/crates/managesieve/src/op/setactive.rs +++ b/crates/managesieve/src/op/setactive.rs @@ -9,6 +9,7 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; use imap_proto::receiver::Request; +use jmap::{changes::write::ChangeLog, sieve::set::SieveScriptSet}; use jmap_proto::types::collection::Collection; use store::write::log::ChangeLogBuilder; use trc::AddContext; @@ -35,7 +36,7 @@ impl<T: SessionStream> Session<T> { // De/activate script let account_id = self.state.access_token().primary_id(); let changes = self - .jmap + .server .sieve_activate_script( account_id, if !name.is_empty() { @@ -53,7 +54,7 @@ impl<T: SessionStream> Session<T> { for (document_id, _) in changes { changelog.log_update(Collection::SieveScript, document_id); } - self.jmap + self.server .commit_changes(account_id, changelog) .await .caused_by(trc::location!())?; diff --git a/crates/pop3/src/client.rs b/crates/pop3/src/client.rs index 5cea1276..ccc6f88c 100644 --- a/crates/pop3/src/client.rs +++ b/crates/pop3/src/client.rs @@ -163,7 +163,7 @@ impl<T: SessionStream> Session<T> { | Command::Pass { .. } | Command::Apop { .. } => { if let State::NotAuthenticated { username, .. } = &self.state { - if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth { + if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { if !matches!(command, Command::Pass { .. }) || username.is_some() { Ok(command) } else { @@ -211,9 +211,9 @@ impl<T: SessionStream> Session<T> { | Command::Stat | Command::Rset => { if let State::Authenticated { mailbox, .. } = &self.state { - if let Some(rate) = &self.jmap.core.imap.rate_requests { + if let Some(rate) = &self.server.core.imap.rate_requests { if self - .jmap + .server .core .storage .lookup diff --git a/crates/pop3/src/lib.rs b/crates/pop3/src/lib.rs index 5212dafe..a7429338 100644 --- a/crates/pop3/src/lib.rs +++ b/crates/pop3/src/lib.rs @@ -9,9 +9,8 @@ use std::{net::IpAddr, sync::Arc}; use common::{ auth::AccessToken, listener::{limiter::InFlight, ServerInstance, SessionStream}, + Inner, Server, }; -use imap::core::{ImapInstance, Inner}; -use jmap::JMAP; use mailbox::Mailbox; use protocol::request::Parser; @@ -25,18 +24,17 @@ static SERVER_GREETING: &str = "+OK Stalwart POP3 at your service.\r\n"; #[derive(Clone)] pub struct Pop3SessionManager { - pub pop3: ImapInstance, + pub inner: Arc<Inner>, } impl Pop3SessionManager { - pub fn new(pop3: ImapInstance) -> Self { - Self { pop3 } + pub fn new(inner: Arc<Inner>) -> Self { + Self { inner } } } pub struct Session<T: SessionStream> { - pub jmap: JMAP, - pub imap: Arc<Inner>, + pub server: Server, pub instance: Arc<ServerInstance>, pub receiver: Parser, pub state: State, diff --git a/crates/pop3/src/mailbox.rs b/crates/pop3/src/mailbox.rs index 10accd12..708bd86d 100644 --- a/crates/pop3/src/mailbox.rs +++ b/crates/pop3/src/mailbox.rs @@ -7,7 +7,10 @@ use std::collections::BTreeMap; use common::listener::SessionStream; -use jmap::mailbox::{UidMailbox, INBOX_ID}; +use jmap::{ + mailbox::{set::MailboxSet, UidMailbox, INBOX_ID}, + JmapMethods, +}; use jmap_proto::{ object::Object, types::{collection::Collection, property::Property, value::Value}, @@ -39,7 +42,7 @@ impl<T: SessionStream> Session<T> { pub async fn fetch_mailbox(&self, account_id: u32) -> trc::Result<Mailbox> { // Obtain message ids let message_ids = self - .jmap + .server .get_tag( account_id, Collection::Email, @@ -58,12 +61,12 @@ impl<T: SessionStream> Session<T> { let mut message_sizes = AHashMap::new(); // Obtain UID validity - self.jmap + self.server .mailbox_get_or_create(account_id) .await .caused_by(trc::location!())?; let uid_validity = self - .jmap + .server .get_property::<Object<Value>>( account_id, Collection::Mailbox, @@ -83,7 +86,7 @@ impl<T: SessionStream> Session<T> { .map(|v| v as u32)?; // Obtain message sizes - self.jmap + self.server .core .storage .data @@ -122,7 +125,7 @@ impl<T: SessionStream> Session<T> { // Sort by UID for (message_id, uid_mailbox) in self - .jmap + .server .get_properties::<Vec<UidMailbox>, _, _>( account_id, Collection::Email, diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs index fe7ca09d..1924d4f2 100644 --- a/crates/pop3/src/op/authenticate.rs +++ b/crates/pop3/src/op/authenticate.rs @@ -4,10 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::{limiter::ConcurrencyLimiter, SessionStream}; +use common::{ + listener::{limiter::ConcurrencyLimiter, SessionStream}, + ConcurrencyLimiters, +}; use directory::Permission; use imap::op::authenticate::{decode_challenge_oauth, decode_challenge_plain}; -use jmap::auth::rate_limit::ConcurrencyLimiters; +use jmap::auth::{ + authenticate::Authenticator, oauth::token::TokenHandler, rate_limit::RateLimiter, +}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use std::sync::Arc; @@ -60,22 +65,22 @@ impl<T: SessionStream> Session<T> { pub async fn handle_auth(&mut self, credentials: Credentials<String>) -> trc::Result<()> { // Throttle authentication requests - self.jmap.is_auth_allowed_soft(&self.remote_addr).await?; + self.server.is_auth_allowed_soft(&self.remote_addr).await?; // Authenticate let access_token = match credentials { Credentials::Plain { username, secret } | Credentials::XOauth2 { username, secret } => { - self.jmap + self.server .authenticate_plain(&username, &secret, self.remote_addr, self.session_id) .await } Credentials::OAuthBearer { token } => { match self - .jmap + .server .validate_access_token("access_token", &token) .await { - Ok((account_id, _, _)) => self.jmap.core.get_access_token(account_id).await, + Ok((account_id, _, _)) => self.server.get_access_token(account_id).await, Err(err) => Err(err), } } @@ -86,7 +91,7 @@ impl<T: SessionStream> Session<T> { State::NotAuthenticated { auth_failures, username, - } if *auth_failures < self.jmap.core.imap.max_auth_failures => { + } if *auth_failures < self.server.core.imap.max_auth_failures => { self.state = State::NotAuthenticated { auth_failures: auth_failures + 1, username: username.clone(), @@ -118,7 +123,7 @@ impl<T: SessionStream> Session<T> { // Cache access token let access_token = Arc::new(access_token); - self.jmap.core.cache_access_token(access_token.clone()); + self.server.cache_access_token(access_token.clone()); // Fetch mailbox let mailbox = self.fetch_mailbox(access_token.primary_id()).await?; @@ -133,9 +138,11 @@ impl<T: SessionStream> Session<T> { } pub fn get_concurrency_limiter(&self, account_id: u32) -> Option<Arc<ConcurrencyLimiters>> { - let rate = self.jmap.core.imap.rate_concurrent?; - self.imap - .rate_limiter + let rate = self.server.core.imap.rate_concurrent?; + self.server + .inner + .data + .imap_limiter .get(&account_id) .map(|limiter| limiter.clone()) .unwrap_or_else(|| { @@ -143,7 +150,11 @@ impl<T: SessionStream> Session<T> { concurrent_requests: ConcurrencyLimiter::new(rate), concurrent_uploads: ConcurrencyLimiter::new(rate), }); - self.imap.rate_limiter.insert(account_id, limiter.clone()); + self.server + .inner + .data + .imap_limiter + .insert(account_id, limiter.clone()); limiter }) .into() diff --git a/crates/pop3/src/op/delete.rs b/crates/pop3/src/op/delete.rs index 4e7799b2..e8b53c49 100644 --- a/crates/pop3/src/op/delete.rs +++ b/crates/pop3/src/op/delete.rs @@ -8,6 +8,9 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; +use jmap::{ + changes::write::ChangeLog, email::delete::EmailDeletion, services::state::StateManager, +}; use jmap_proto::types::{state::StateChange, type_state::DataType}; use store::roaring::RoaringBitmap; use trc::AddContext; @@ -87,16 +90,18 @@ impl<T: SessionStream> Session<T> { if !deleted.is_empty() { let num_deleted = deleted.len(); let (changes, not_deleted) = self - .jmap + .server .emails_tombstone(mailbox.account_id, deleted) .await .caused_by(trc::location!())?; if !changes.is_empty() { - if let Ok(change_id) = - self.jmap.commit_changes(mailbox.account_id, changes).await + if let Ok(change_id) = self + .server + .commit_changes(mailbox.account_id, changes) + .await { - self.jmap + self.server .broadcast_state_change( StateChange::new(mailbox.account_id) .with_change(DataType::Email, change_id) diff --git a/crates/pop3/src/op/fetch.rs b/crates/pop3/src/op/fetch.rs index be322723..9d58cf45 100644 --- a/crates/pop3/src/op/fetch.rs +++ b/crates/pop3/src/op/fetch.rs @@ -8,7 +8,7 @@ use std::time::Instant; use common::listener::SessionStream; use directory::Permission; -use jmap::email::metadata::MessageMetadata; +use jmap::{blob::download::BlobDownload, email::metadata::MessageMetadata, JmapMethods}; use jmap_proto::types::{collection::Collection, property::Property}; use store::write::Bincode; use trc::AddContext; @@ -26,7 +26,7 @@ impl<T: SessionStream> Session<T> { let mailbox = self.state.mailbox(); if let Some(message) = mailbox.messages.get(msg.saturating_sub(1) as usize) { if let Some(metadata) = self - .jmap + .server .get_property::<Bincode<MessageMetadata>>( mailbox.account_id, Collection::Email, @@ -37,7 +37,7 @@ impl<T: SessionStream> Session<T> { .caused_by(trc::location!())? { if let Some(bytes) = self - .jmap + .server .get_blob(&metadata.inner.blob_hash, 0..usize::MAX) .await .caused_by(trc::location!())? diff --git a/crates/pop3/src/op/mod.rs b/crates/pop3/src/op/mod.rs index 0488caa8..bfc1db7d 100644 --- a/crates/pop3/src/op/mod.rs +++ b/crates/pop3/src/op/mod.rs @@ -18,7 +18,7 @@ pub mod list; impl<T: SessionStream> Session<T> { pub async fn handle_capa(&mut self) -> trc::Result<()> { - let mechanisms = if self.stream.is_tls() || self.jmap.core.imap.allow_plain_auth { + let mechanisms = if self.stream.is_tls() || self.server.core.imap.allow_plain_auth { vec![Mechanism::Plain, Mechanism::OAuthBearer] } else { vec![Mechanism::OAuthBearer] @@ -28,7 +28,7 @@ impl<T: SessionStream> Session<T> { Pop3(trc::Pop3Event::Capabilities), SpanId = self.session_id, Tls = self.stream.is_tls(), - Strict = !self.jmap.core.imap.allow_plain_auth, + Strict = !self.server.core.imap.allow_plain_auth, Elapsed = trc::Value::Duration(0) ); diff --git a/crates/pop3/src/session.rs b/crates/pop3/src/session.rs index ca0ae1d9..d016170a 100644 --- a/crates/pop3/src/session.rs +++ b/crates/pop3/src/session.rs @@ -6,8 +6,10 @@ use std::borrow::Cow; -use common::listener::{SessionData, SessionManager, SessionResult, SessionStream}; -use jmap::JMAP; +use common::{ + core::BuildServer, + listener::{SessionData, SessionManager, SessionResult, SessionStream}, +}; use tokio_rustls::server::TlsStream; use crate::{ @@ -28,8 +30,7 @@ impl SessionManager for Pop3SessionManager { ) -> impl std::future::Future<Output = ()> + Send { async move { let mut session = Session { - jmap: JMAP::from(self.pop3.jmap_instance), - imap: self.pop3.imap_inner, + server: self.inner.build_server(), instance: session.instance, receiver: Parser::default(), state: State::NotAuthenticated { @@ -71,9 +72,9 @@ impl<T: SessionStream> Session<T> { tokio::select! { result = tokio::time::timeout( if !matches!(self.state, State::NotAuthenticated {..}) { - self.jmap.core.imap.timeout_auth + self.server.core.imap.timeout_auth } else { - self.jmap.core.imap.timeout_unauth + self.server.core.imap.timeout_unauth }, self.stream.read(&mut buf)) => { match result { @@ -141,8 +142,7 @@ impl<T: SessionStream> Session<T> { .instance .tls_accept(self.stream, self.session_id) .await?, - jmap: self.jmap, - imap: self.imap, + server: self.server, instance: self.instance, receiver: self.receiver, state: self.state, diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index d6832d7a..810521a1 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -12,86 +12,40 @@ use std::{ }; use common::{ - config::{scripts::ScriptCache, smtp::auth::VerifyStrategy}, + config::smtp::auth::VerifyStrategy, listener::{ limiter::{ConcurrencyLimiter, InFlight}, ServerInstance, }, - Core, Ipc, SharedCore, + Inner, Server, }; -use dashmap::DashMap; use directory::Directory; use mail_auth::{IprevOutput, SpfOutput}; use smtp_proto::request::receiver::{ BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver, }; -use tokio::{ - io::{AsyncRead, AsyncWrite}, - sync::mpsc, -}; -use tokio_rustls::TlsConnector; +use tokio::io::{AsyncRead, AsyncWrite}; use utils::snowflake::SnowflakeIdGenerator; use crate::{ inbound::auth::SaslToken, - queue::{self, DomainPart, QueueId}, - reporting, + queue::{DomainPart, QueueId}, }; -use self::throttle::{ThrottleKey, ThrottleKeyHasherBuilder}; - pub mod params; pub mod throttle; #[derive(Clone)] -pub struct SmtpInstance { - pub inner: Arc<Inner>, - pub core: SharedCore, -} - -impl SmtpInstance { - pub fn new(core: SharedCore, inner: impl Into<Arc<Inner>>) -> Self { - Self { - core, - inner: inner.into(), - } - } -} - -#[derive(Clone)] pub struct SmtpSessionManager { - pub inner: SmtpInstance, + pub inner: Arc<Inner>, } impl SmtpSessionManager { - pub fn new(inner: SmtpInstance) -> Self { + pub fn new(inner: Arc<Inner>) -> Self { Self { inner } } } -#[derive(Clone)] -pub struct SMTP { - pub core: Arc<Core>, - pub inner: Arc<Inner>, -} - -pub struct Inner { - pub session_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>, - pub queue_throttle: DashMap<ThrottleKey, ConcurrencyLimiter, ThrottleKeyHasherBuilder>, - pub queue_tx: mpsc::Sender<queue::Event>, - pub report_tx: mpsc::Sender<reporting::Event>, - pub queue_id_gen: SnowflakeIdGenerator, - pub span_id_gen: Arc<SnowflakeIdGenerator>, - pub connectors: TlsConnectors, - pub ipc: Ipc, - pub script_cache: ScriptCache, -} - -pub struct TlsConnectors { - pub pki_verify: TlsConnector, - pub dummy_verify: TlsConnector, -} - pub enum State { Request(RequestReceiver), Bdat(BdatReceiver), @@ -107,7 +61,7 @@ pub struct Session<T: AsyncWrite + AsyncRead> { pub hostname: String, pub state: State, pub instance: Arc<ServerInstance>, - pub core: SMTP, + pub server: Server, pub stream: T, pub data: SessionData, pub params: SessionParameters, @@ -260,15 +214,6 @@ impl PartialOrd for SessionAddress { } } -impl From<SmtpInstance> for SMTP { - fn from(value: SmtpInstance) -> Self { - SMTP { - core: value.core.load_full(), - inner: value.inner, - } - } -} - static SIEVE: LazyLock<Arc<ServerInstance>> = LazyLock::new(|| { Arc::new(ServerInstance { id: "sieve".to_string(), @@ -282,12 +227,16 @@ static SIEVE: LazyLock<Arc<ServerInstance>> = LazyLock::new(|| { }); impl Session<common::listener::stream::NullIo> { - pub fn local(core: SMTP, instance: std::sync::Arc<ServerInstance>, data: SessionData) -> Self { + pub fn local( + server: Server, + instance: std::sync::Arc<ServerInstance>, + data: SessionData, + ) -> Self { Session { hostname: "localhost".to_string(), state: State::None, instance, - core, + server, stream: common::listener::stream::NullIo::default(), data, params: SessionParameters { @@ -315,14 +264,14 @@ impl Session<common::listener::stream::NullIo> { } pub fn sieve( - core: SMTP, + server: Server, mail_from: SessionAddress, rcpt_to: Vec<SessionAddress>, message: Vec<u8>, session_id: u64, ) -> Self { Self::local( - core, + server, SIEVE.clone(), SessionData::local(mail_from.into(), rcpt_to, message, session_id), ) @@ -398,25 +347,3 @@ impl SessionAddress { } } } - -#[cfg(feature = "test_mode")] -impl Default for Inner { - fn default() -> Self { - Self { - session_throttle: Default::default(), - queue_throttle: Default::default(), - queue_tx: mpsc::channel(1).0, - report_tx: mpsc::channel(1).0, - queue_id_gen: Default::default(), - span_id_gen: Arc::new(SnowflakeIdGenerator::new()), - connectors: TlsConnectors { - pki_verify: mail_send::smtp::tls::build_tls_connector(false), - dummy_verify: mail_send::smtp::tls::build_tls_connector(true), - }, - ipc: Ipc { - delivery_tx: mpsc::channel(1).0, - }, - script_cache: Default::default(), - } - } -} diff --git a/crates/smtp/src/core/params.rs b/crates/smtp/src/core/params.rs index 4e555a24..5a4e873e 100644 --- a/crates/smtp/src/core/params.rs +++ b/crates/smtp/src/core/params.rs @@ -12,51 +12,45 @@ use super::Session; impl<T: SessionStream> Session<T> { pub async fn eval_session_params(&mut self) { - let c = &self.core.core.smtp.session; + let c = &self.server.core.smtp.session; self.data.bytes_left = self - .core - .core + .server .eval_if(&c.transfer_limit, self, self.data.session_id) .await .unwrap_or(250 * 1024 * 1024); self.data.valid_until += self - .core - .core + .server .eval_if(&c.duration, self, self.data.session_id) .await .unwrap_or_else(|| Duration::from_secs(15 * 60)); self.params.timeout = self - .core - .core + .server .eval_if(&c.timeout, self, self.data.session_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 60)); self.params.spf_ehlo = self - .core - .core + .server .eval_if( - &self.core.core.smtp.mail_auth.spf.verify_ehlo, + &self.server.core.smtp.mail_auth.spf.verify_ehlo, self, self.data.session_id, ) .await .unwrap_or(VerifyStrategy::Relaxed); self.params.spf_mail_from = self - .core - .core + .server .eval_if( - &self.core.core.smtp.mail_auth.spf.verify_mail_from, + &self.server.core.smtp.mail_auth.spf.verify_mail_from, self, self.data.session_id, ) .await .unwrap_or(VerifyStrategy::Relaxed); self.params.iprev = self - .core - .core + .server .eval_if( - &self.core.core.smtp.mail_auth.iprev.verify, + &self.server.core.smtp.mail_auth.iprev.verify, self, self.data.session_id, ) @@ -64,65 +58,56 @@ impl<T: SessionStream> Session<T> { .unwrap_or(VerifyStrategy::Relaxed); // Ehlo parameters - let ec = &self.core.core.smtp.session.ehlo; + let ec = &self.server.core.smtp.session.ehlo; self.params.ehlo_require = self - .core - .core + .server .eval_if(&ec.require, self, self.data.session_id) .await .unwrap_or(true); self.params.ehlo_reject_non_fqdn = self - .core - .core + .server .eval_if(&ec.reject_non_fqdn, self, self.data.session_id) .await .unwrap_or(true); // Auth parameters - let ac = &self.core.core.smtp.session.auth; + let ac = &self.server.core.smtp.session.auth; self.params.auth_directory = self - .core - .core + .server .eval_if::<String, _>(&ac.directory, self, self.data.session_id) .await - .and_then(|name| self.core.core.get_directory(&name)) + .and_then(|name| self.server.get_directory(&name)) .cloned(); self.params.auth_require = self - .core - .core + .server .eval_if(&ac.require, self, self.data.session_id) .await .unwrap_or(false); self.params.auth_errors_max = self - .core - .core + .server .eval_if(&ac.errors_max, self, self.data.session_id) .await .unwrap_or(3); self.params.auth_errors_wait = self - .core - .core + .server .eval_if(&ac.errors_wait, self, self.data.session_id) .await .unwrap_or_else(|| Duration::from_secs(30)); self.params.auth_match_sender = self - .core - .core + .server .eval_if(&ac.must_match_sender, self, self.data.session_id) .await .unwrap_or(true); // VRFY/EXPN parameters - let ec = &self.core.core.smtp.session.extensions; + let ec = &self.server.core.smtp.session.extensions; self.params.can_expn = self - .core - .core + .server .eval_if(&ec.expn, self, self.data.session_id) .await .unwrap_or(false); self.params.can_vrfy = self - .core - .core + .server .eval_if(&ec.vrfy, self, self.data.session_id) .await .unwrap_or(false); @@ -130,24 +115,21 @@ impl<T: SessionStream> Session<T> { pub async fn eval_post_auth_params(&mut self) { // Refresh VRFY/EXPN parameters - let ec = &self.core.core.smtp.session.extensions; + let ec = &self.server.core.smtp.session.extensions; self.params.can_expn = self - .core - .core + .server .eval_if(&ec.expn, self, self.data.session_id) .await .unwrap_or(false); self.params.can_vrfy = self - .core - .core + .server .eval_if(&ec.vrfy, self, self.data.session_id) .await .unwrap_or(false); self.params.auth_match_sender = self - .core - .core + .server .eval_if( - &self.core.core.smtp.session.auth.must_match_sender, + &self.server.core.smtp.session.auth.must_match_sender, self, self.data.session_id, ) @@ -156,30 +138,26 @@ impl<T: SessionStream> Session<T> { } pub async fn eval_rcpt_params(&mut self) { - let rc = &self.core.core.smtp.session.rcpt; + let rc = &self.server.core.smtp.session.rcpt; self.params.rcpt_errors_max = self - .core - .core + .server .eval_if(&rc.errors_max, self, self.data.session_id) .await .unwrap_or(10); self.params.rcpt_errors_wait = self - .core - .core + .server .eval_if(&rc.errors_wait, self, self.data.session_id) .await .unwrap_or_else(|| Duration::from_secs(30)); self.params.rcpt_max = self - .core - .core + .server .eval_if(&rc.max_recipients, self, self.data.session_id) .await .unwrap_or(100); self.params.rcpt_dsn = self - .core - .core + .server .eval_if( - &self.core.core.smtp.session.extensions.dsn, + &self.server.core.smtp.session.extensions.dsn, self, self.data.session_id, ) @@ -187,10 +165,9 @@ impl<T: SessionStream> Session<T> { .unwrap_or(true); self.params.max_message_size = self - .core - .core + .server .eval_if( - &self.core.core.smtp.session.data.max_message_size, + &self.server.core.smtp.session.data.max_message_size, self, self.data.session_id, ) diff --git a/crates/smtp/src/core/throttle.rs b/crates/smtp/src/core/throttle.rs index dc9fadf7..9abed15c 100644 --- a/crates/smtp/src/core/throttle.rs +++ b/crates/smtp/src/core/throttle.rs @@ -8,66 +8,13 @@ use common::{ config::smtp::{queue::QueueQuota, *}, expr::{functions::ResolveVariable, *}, listener::{limiter::ConcurrencyLimiter, SessionStream}, + ThrottleKey, }; use dashmap::mapref::entry::Entry; use trc::SmtpEvent; use utils::config::Rate; -use std::{ - hash::{BuildHasher, Hash, Hasher}, - sync::atomic::Ordering, -}; - -use super::{Session, SMTP}; - -#[derive(Debug, Clone, Eq)] -pub struct ThrottleKey { - hash: [u8; 32], -} - -impl PartialEq for ThrottleKey { - fn eq(&self, other: &Self) -> bool { - self.hash == other.hash - } -} - -impl Hash for ThrottleKey { - fn hash<H: Hasher>(&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]) { - self.hash = u64::from_ne_bytes((&bytes[..std::mem::size_of::<u64>()]).try_into().unwrap()); - } -} - -#[derive(Clone, Default)] -pub struct ThrottleKeyHasherBuilder {} - -impl BuildHasher for ThrottleKeyHasherBuilder { - type Hasher = ThrottleKeyHasher; - - fn build_hasher(&self) -> Self::Hasher { - ThrottleKeyHasher::default() - } -} +use super::Session; pub trait NewKey: Sized { fn new_key(&self, e: &impl ResolveVariable) -> ThrottleKey; @@ -199,18 +146,17 @@ impl NewKey for Throttle { impl<T: SessionStream> Session<T> { pub async fn is_allowed(&mut self) -> bool { let throttles = if !self.data.rcpt_to.is_empty() { - &self.core.core.smtp.session.throttle.rcpt_to + &self.server.core.smtp.session.throttle.rcpt_to } else if self.data.mail_from.is_some() { - &self.core.core.smtp.session.throttle.mail_from + &self.server.core.smtp.session.throttle.mail_from } else { - &self.core.core.smtp.session.throttle.connect + &self.server.core.smtp.session.throttle.connect }; for t in throttles { if t.expr.is_empty() || self - .core - .core + .server .eval_expr(&t.expr, self, "throttle", self.data.session_id) .await .unwrap_or(false) @@ -233,7 +179,13 @@ impl<T: SessionStream> Session<T> { // Check concurrency if let Some(concurrency) = &t.concurrency { - match self.core.inner.session_throttle.entry(key.clone()) { + match self + .server + .inner + .data + .smtp_session_throttle + .entry(key.clone()) + { Entry::Occupied(mut e) => { let limiter = e.get_mut(); if let Some(inflight) = limiter.is_allowed() { @@ -261,7 +213,7 @@ impl<T: SessionStream> Session<T> { // Check rate if let Some(rate) = &t.rate { if self - .core + .server .core .storage .lookup @@ -296,7 +248,7 @@ impl<T: SessionStream> Session<T> { hasher.update(&rate.period.as_secs().to_ne_bytes()[..]); hasher.update(&rate.requests.to_ne_bytes()[..]); - self.core + self.server .core .storage .lookup @@ -306,11 +258,3 @@ impl<T: SessionStream> Session<T> { .is_none() } } - -impl SMTP { - pub fn cleanup(&self) { - for throttle in [&self.inner.session_throttle, &self.inner.queue_throttle] { - throttle.retain(|_, v| v.concurrent.load(Ordering::Relaxed) > 0); - } - } -} diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index 419b4dab..d1ab3470 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -168,8 +168,7 @@ impl<T: SessionStream> Session<T> { // Authenticate let mut result = self - .core - .core + .server .authenticate( directory, self.data.session_id, @@ -182,8 +181,7 @@ impl<T: SessionStream> Session<T> { // Validate permissions if let Ok(principal) = &result { match self - .core - .core + .server .get_cached_access_token(principal.id()) .await .caused_by(trc::location!()) diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index 822fc734..80477eac 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -34,7 +34,8 @@ use utils::config::Rate; use crate::{ core::{Session, SessionAddress, State}, inbound::milter::Modification, - queue::{self, Message, MessageSource, QueueEnvelope, Schedule}, + queue::{self, quota::HasQueueQuota, Message, MessageSource, QueueEnvelope, Schedule}, + reporting::analysis::AnalyzeReport, scripts::ScriptResult, }; @@ -46,7 +47,7 @@ impl<T: SessionStream> Session<T> { let raw_message = Arc::new(std::mem::take(&mut self.data.message)); let auth_message = if let Some(auth_message) = AuthenticatedMessage::parse_with_opts( &raw_message, - self.core.core.smtp.mail_auth.dkim.strict, + self.server.core.smtp.mail_auth.dkim.strict, ) { auth_message } else { @@ -59,13 +60,12 @@ impl<T: SessionStream> Session<T> { }; // Loop detection - let dc = &self.core.core.smtp.session.data; - let ac = &self.core.core.smtp.mail_auth; - let rc = &self.core.core.smtp.report; + let dc = &self.server.core.smtp.session.data; + let ac = &self.server.core.smtp.mail_auth; + let rc = &self.server.core.smtp.report; if auth_message.received_headers_count() > self - .core - .core + .server .eval_if(&dc.max_received_headers, self, self.data.session_id) .await .unwrap_or(50) @@ -82,21 +82,19 @@ impl<T: SessionStream> Session<T> { // Verify DKIM let dkim = self - .core - .core + .server .eval_if(&ac.dkim.verify, self, self.data.session_id) .await .unwrap_or(VerifyStrategy::Relaxed); let dmarc = self - .core - .core + .server .eval_if(&ac.dmarc.verify, self, self.data.session_id) .await .unwrap_or(VerifyStrategy::Relaxed); let dkim_output = if dkim.verify() || dmarc.verify() { let time = Instant::now(); let dkim_output = self - .core + .server .core .smtp .resolvers @@ -111,8 +109,7 @@ impl<T: SessionStream> Session<T> { // Send reports for failed signatures if let Some(rate) = self - .core - .core + .server .eval_if::<Rate, _>(&rc.dkim.send, self, self.data.session_id) .await { @@ -155,21 +152,19 @@ impl<T: SessionStream> Session<T> { // Verify ARC let arc = self - .core - .core + .server .eval_if(&ac.arc.verify, self, self.data.session_id) .await .unwrap_or(VerifyStrategy::Relaxed); let arc_sealer = self - .core - .core + .server .eval_if::<String, _>(&ac.arc.seal, self, self.data.session_id) .await - .and_then(|name| self.core.core.get_arc_sealer(&name, self.data.session_id)); + .and_then(|name| self.server.get_arc_sealer(&name, self.data.session_id)); let arc_output = if arc.verify() || arc_sealer.is_some() { let time = Instant::now(); let arc_output = self - .core + .server .core .smtp .resolvers @@ -236,7 +231,7 @@ impl<T: SessionStream> Session<T> { Some(spf_output) if dmarc.verify() => { let time = Instant::now(); let dmarc_output = self - .core + .server .core .smtp .resolvers @@ -317,7 +312,7 @@ impl<T: SessionStream> Session<T> { // Analyze reports if is_report { - self.core + self.server .analyze_report(raw_message.clone(), self.data.session_id); if !rc.analysis.forward { self.data.messages_sent += 1; @@ -326,11 +321,16 @@ impl<T: SessionStream> Session<T> { } // Add Received header - let message_id = self.core.inner.queue_id_gen.generate().unwrap_or_else(now); + let message_id = self + .server + .inner + .data + .queue_id_gen + .generate() + .unwrap_or_else(now); let mut headers = Vec::with_capacity(64); if self - .core - .core + .server .eval_if(&dc.add_received, self, self.data.session_id) .await .unwrap_or(true) @@ -340,8 +340,7 @@ impl<T: SessionStream> Session<T> { // Add authentication results header if self - .core - .core + .server .eval_if(&dc.add_auth_results, self, self.data.session_id) .await .unwrap_or(true) @@ -352,8 +351,7 @@ impl<T: SessionStream> Session<T> { // Add Received-SPF header if let Some(spf_output) = &self.data.spf_mail_from { if self - .core - .core + .server .eval_if(&dc.add_received_spf, self, self.data.session_id) .await .unwrap_or(true) @@ -425,23 +423,20 @@ impl<T: SessionStream> Session<T> { // Pipe message for pipe in &dc.pipe_commands { if let Some(command_) = self - .core - .core + .server .eval_if::<String, _>(&pipe.command, self, self.data.session_id) .await { let piped_message = edited_message.as_ref().unwrap_or(&raw_message).clone(); let timeout = self - .core - .core + .server .eval_if(&pipe.timeout, self, self.data.session_id) .await .unwrap_or_else(|| Duration::from_secs(30)); let mut command = Command::new(&command_); for argument in self - .core - .core + .server .eval_if::<Vec<String>, _>(&pipe.arguments, self, self.data.session_id) .await .unwrap_or_default() @@ -538,13 +533,11 @@ impl<T: SessionStream> Session<T> { // Sieve filtering if let Some((script, script_id)) = self - .core - .core + .server .eval_if::<String, _>(&dc.script, self, self.data.session_id) .await .and_then(|name| { - self.core - .core + self.server .get_trusted_sieve_script(&name, self.data.session_id) .map(|s| (s, name)) }) @@ -642,8 +635,7 @@ impl<T: SessionStream> Session<T> { // Add Return-Path if self - .core - .core + .server .eval_if(&dc.add_return_path, self, self.data.session_id) .await .unwrap_or(true) @@ -656,8 +648,7 @@ impl<T: SessionStream> Session<T> { // Add any missing headers if !auth_message.has_date_header() && self - .core - .core + .server .eval_if(&dc.add_date, self, self.data.session_id) .await .unwrap_or(true) @@ -668,8 +659,7 @@ impl<T: SessionStream> Session<T> { } if !auth_message.has_message_id_header() && self - .core - .core + .server .eval_if(&dc.add_message_id, self, self.data.session_id) .await .unwrap_or(true) @@ -684,17 +674,12 @@ impl<T: SessionStream> Session<T> { .as_deref() .unwrap_or_else(|| raw_message.as_slice()); for signer in self - .core - .core + .server .eval_if::<Vec<String>, _>(&ac.dkim.sign, self, self.data.session_id) .await .unwrap_or_default() { - if let Some(signer) = self - .core - .core - .get_dkim_signer(&signer, self.data.session_id) - { + if let Some(signer) = self.server.get_dkim_signer(&signer, self.data.session_id) { match signer.sign_chained(&[headers.as_ref(), raw_message]) { Ok(signature) => { signature.write_header(&mut headers); @@ -712,7 +697,7 @@ impl<T: SessionStream> Session<T> { message.size = raw_message.len() + headers.len(); // Verify queue quota - if self.core.has_quota(&mut message).await { + if self.server.has_quota(&mut message).await { // Prepare webhook event let queue_id = message.queue_id; @@ -727,7 +712,7 @@ impl<T: SessionStream> Session<T> { Some(&headers), raw_message, self.data.session_id, - &self.core, + &self.server, source, ) .await @@ -799,10 +784,9 @@ impl<T: SessionStream> Session<T> { }; // Set expiration and notification times - let config = &self.core.core.smtp.queue; + let config = &self.server.core.smtp.queue; let (num_intervals, next_notify) = self - .core - .core + .server .eval_if::<Vec<Duration>, _>(&config.notify, &envelope, self.data.session_id) .await .and_then(|v| (v.len(), v.into_iter().next()?).into()) @@ -813,8 +797,7 @@ impl<T: SessionStream> Session<T> { now() + future_release.as_secs() + self - .core - .core + .server .eval_if(&config.expire, &envelope, self.data.session_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 86400)) @@ -827,8 +810,7 @@ impl<T: SessionStream> Session<T> { ) } else { let expire = self - .core - .core + .server .eval_if(&config.expire, &envelope, self.data.session_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 86400)); @@ -887,10 +869,9 @@ impl<T: SessionStream> Session<T> { if !self.data.rcpt_to.is_empty() { if self.data.messages_sent < self - .core - .core + .server .eval_if( - &self.core.core.smtp.session.data.max_messages, + &self.server.core.smtp.session.data.max_messages, self, self.data.session_id, ) diff --git a/crates/smtp/src/inbound/ehlo.rs b/crates/smtp/src/inbound/ehlo.rs index 49580945..deeae3d1 100644 --- a/crates/smtp/src/inbound/ehlo.rs +++ b/crates/smtp/src/inbound/ehlo.rs @@ -42,7 +42,7 @@ impl<T: SessionStream> Session<T> { if self.params.spf_ehlo.verify() { let time = Instant::now(); let spf_output = self - .core + .server .core .smtp .resolvers @@ -76,17 +76,15 @@ impl<T: SessionStream> Session<T> { // Sieve filtering if let Some((script, script_id)) = self - .core - .core + .server .eval_if::<String, _>( - &self.core.core.smtp.session.ehlo.script, + &self.server.core.smtp.session.ehlo.script, self, self.data.session_id, ) .await .and_then(|name| { - self.core - .core + self.server .get_trusted_sieve_script(&name, self.data.session_id) .map(|s| (s, name)) }) @@ -140,14 +138,13 @@ impl<T: SessionStream> Session<T> { if !self.stream.is_tls() && self.instance.acceptor.is_tls() { response.capabilities |= EXT_START_TLS; } - let ec = &self.core.core.smtp.session.extensions; - let ac = &self.core.core.smtp.session.auth; - let dc = &self.core.core.smtp.session.data; + let ec = &self.server.core.smtp.session.extensions; + let ac = &self.server.core.smtp.session.auth; + let dc = &self.server.core.smtp.session.data; // Pipelining if self - .core - .core + .server .eval_if(&ec.pipelining, self, self.data.session_id) .await .unwrap_or(true) @@ -157,8 +154,7 @@ impl<T: SessionStream> Session<T> { // Chunking if self - .core - .core + .server .eval_if(&ec.chunking, self, self.data.session_id) .await .unwrap_or(true) @@ -168,8 +164,7 @@ impl<T: SessionStream> Session<T> { // Address Expansion if self - .core - .core + .server .eval_if(&ec.expn, self, self.data.session_id) .await .unwrap_or(false) @@ -179,8 +174,7 @@ impl<T: SessionStream> Session<T> { // Recipient Verification if self - .core - .core + .server .eval_if(&ec.vrfy, self, self.data.session_id) .await .unwrap_or(false) @@ -190,8 +184,7 @@ impl<T: SessionStream> Session<T> { // Require TLS if self - .core - .core + .server .eval_if(&ec.requiretls, self, self.data.session_id) .await .unwrap_or(true) @@ -201,8 +194,7 @@ impl<T: SessionStream> Session<T> { // DSN if self - .core - .core + .server .eval_if(&ec.dsn, self, self.data.session_id) .await .unwrap_or(false) @@ -213,8 +205,7 @@ impl<T: SessionStream> Session<T> { // Authentication if self.data.authenticated_as.is_empty() { response.auth_mechanisms = self - .core - .core + .server .eval_if::<Mechanism, _>(&ac.mechanisms, self, self.data.session_id) .await .unwrap_or_default() @@ -226,8 +217,7 @@ impl<T: SessionStream> Session<T> { // Future release if let Some(value) = self - .core - .core + .server .eval_if::<Duration, _>(&ec.future_release, self, self.data.session_id) .await { @@ -242,8 +232,7 @@ impl<T: SessionStream> Session<T> { // Deliver By if let Some(value) = self - .core - .core + .server .eval_if::<Duration, _>(&ec.deliver_by, self, self.data.session_id) .await { @@ -253,8 +242,7 @@ impl<T: SessionStream> Session<T> { // Priority if let Some(value) = self - .core - .core + .server .eval_if::<MtPriority, _>(&ec.mt_priority, self, self.data.session_id) .await { @@ -264,8 +252,7 @@ impl<T: SessionStream> Session<T> { // Size response.size = self - .core - .core + .server .eval_if(&dc.max_message_size, self, self.data.session_id) .await .unwrap_or(25 * 1024 * 1024); @@ -275,8 +262,7 @@ impl<T: SessionStream> Session<T> { // No soliciting if let Some(value) = self - .core - .core + .server .eval_if::<String, _>(&ec.no_soliciting, self, self.data.session_id) .await { diff --git a/crates/smtp/src/inbound/hooks/message.rs b/crates/smtp/src/inbound/hooks/message.rs index 6ad3b44f..37720509 100644 --- a/crates/smtp/src/inbound/hooks/message.rs +++ b/crates/smtp/src/inbound/hooks/message.rs @@ -36,7 +36,7 @@ impl<T: SessionStream> Session<T> { message: Option<&AuthenticatedMessage<'_>>, queue_id: Option<QueueId>, ) -> Result<Vec<Modification>, FilterResponse> { - let mta_hooks = &self.core.core.smtp.session.hooks; + let mta_hooks = &self.server.core.smtp.session.hooks; if mta_hooks.is_empty() { return Ok(Vec::new()); } @@ -45,8 +45,7 @@ impl<T: SessionStream> Session<T> { for mta_hook in mta_hooks { if !mta_hook.run_on_stage.contains(&stage) || !self - .core - .core + .server .eval_if(&mta_hook.enable, self, self.data.session_id) .await .unwrap_or(false) diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs index 45428f9e..033b7d3d 100644 --- a/crates/smtp/src/inbound/mail.rs +++ b/crates/smtp/src/inbound/mail.rs @@ -54,7 +54,7 @@ impl<T: SessionStream> Session<T> { } else if self.data.iprev.is_none() && self.params.iprev.verify() { let time = Instant::now(); let iprev = self - .core + .server .core .smtp .resolvers @@ -122,10 +122,9 @@ impl<T: SessionStream> Session<T> { // Check whether the address is allowed if !self - .core - .core + .server .eval_if::<bool, _>( - &self.core.core.smtp.session.mail.is_allowed, + &self.server.core.smtp.session.mail.is_allowed, self, self.data.session_id, ) @@ -145,17 +144,15 @@ impl<T: SessionStream> Session<T> { // Sieve filtering if let Some((script, script_id)) = self - .core - .core + .server .eval_if::<String, _>( - &self.core.core.smtp.session.mail.script, + &self.server.core.smtp.session.mail.script, self, self.data.session_id, ) .await .and_then(|name| { - self.core - .core + self.server .get_trusted_sieve_script(&name, self.data.session_id) .map(|s| (s, name)) }) @@ -199,10 +196,9 @@ impl<T: SessionStream> Session<T> { // Address rewriting if let Some(new_address) = self - .core - .core + .server .eval_if::<String, _>( - &self.core.core.smtp.session.mail.rewrite, + &self.server.core.smtp.session.mail.rewrite, self, self.data.session_id, ) @@ -258,12 +254,11 @@ impl<T: SessionStream> Session<T> { } // Validate parameters - let config = &self.core.core.smtp.session.extensions; - let config_data = &self.core.core.smtp.session.data; + let config = &self.server.core.smtp.session.extensions; + let config_data = &self.server.core.smtp.session.data; if (from.flags & MAIL_REQUIRETLS) != 0 && !self - .core - .core + .server .eval_if(&config.requiretls, self, self.data.session_id) .await .unwrap_or(false) @@ -279,8 +274,7 @@ impl<T: SessionStream> Session<T> { } if (from.flags & (MAIL_BY_NOTIFY | MAIL_BY_RETURN)) != 0 { if let Some(duration) = self - .core - .core + .server .eval_if::<Duration, _>(&config.deliver_by, self, self.data.session_id) .await { @@ -320,8 +314,7 @@ impl<T: SessionStream> Session<T> { } if from.mt_priority != 0 { if self - .core - .core + .server .eval_if::<MtPriority, _>(&config.mt_priority, self, self.data.session_id) .await .is_some() @@ -351,8 +344,7 @@ impl<T: SessionStream> Session<T> { if from.size > 0 && from.size > self - .core - .core + .server .eval_if(&config_data.max_message_size, self, self.data.session_id) .await .unwrap_or(25 * 1024 * 1024) @@ -370,8 +362,7 @@ impl<T: SessionStream> Session<T> { } if from.hold_for != 0 || from.hold_until != 0 { if let Some(max_hold) = self - .core - .core + .server .eval_if::<Duration, _>(&config.future_release, self, self.data.session_id) .await { @@ -419,8 +410,7 @@ impl<T: SessionStream> Session<T> { } if has_dsn && !self - .core - .core + .server .eval_if(&config.dsn, self, self.data.session_id) .await .unwrap_or(false) @@ -438,7 +428,7 @@ impl<T: SessionStream> Session<T> { let time = Instant::now(); let mail_from = self.data.mail_from.as_ref().unwrap(); let spf_output = if !mail_from.address.is_empty() { - self.core + self.server .core .smtp .resolvers @@ -452,7 +442,7 @@ impl<T: SessionStream> Session<T> { ) .await } else { - self.core + self.server .core .smtp .resolvers @@ -542,10 +532,9 @@ impl<T: SessionStream> Session<T> { // Send report if let (Some(recipient), Some(rate)) = ( spf_output.report_address(), - self.core - .core + self.server .eval_if::<Rate, _>( - &self.core.core.smtp.report.spf.send, + &self.server.core.smtp.report.spf.send, self, self.data.session_id, ) diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs index 1bcbde1b..0b369161 100644 --- a/crates/smtp/src/inbound/milter/message.rs +++ b/crates/smtp/src/inbound/milter/message.rs @@ -35,7 +35,7 @@ impl<T: SessionStream> Session<T> { stage: Stage, message: Option<&AuthenticatedMessage<'_>>, ) -> Result<Vec<Modification>, FilterResponse> { - let milters = &self.core.core.smtp.session.milters; + let milters = &self.server.core.smtp.session.milters; if milters.is_empty() { return Ok(Vec::new()); } @@ -44,8 +44,7 @@ impl<T: SessionStream> Session<T> { for milter in milters { if !milter.run_on_stage.contains(&stage) || !self - .core - .core + .server .eval_if(&milter.enable, self, self.data.session_id) .await .unwrap_or(false) @@ -170,9 +169,9 @@ impl<T: SessionStream> Session<T> { client .into_tls( if !milter.tls_allow_invalid_certs { - &self.core.inner.connectors.pki_verify + &self.server.inner.data.smtp_connectors.pki_verify } else { - &self.core.inner.connectors.dummy_verify + &self.server.inner.data.smtp_connectors.dummy_verify }, &milter.hostname, ) diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index 89c7b8dc..aab0be1e 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -77,25 +77,23 @@ impl<T: SessionStream> Session<T> { // Address rewriting and Sieve filtering let rcpt_script = self - .core - .core + .server .eval_if::<String, _>( - &self.core.core.smtp.session.rcpt.script, + &self.server.core.smtp.session.rcpt.script, self, self.data.session_id, ) .await .and_then(|name| { - self.core - .core + self.server .get_trusted_sieve_script(&name, self.data.session_id) .map(|s| (s.clone(), name)) }); if rcpt_script.is_some() - || !self.core.core.smtp.session.rcpt.rewrite.is_empty() + || !self.server.core.smtp.session.rcpt.rewrite.is_empty() || self - .core + .server .core .smtp .session @@ -146,10 +144,9 @@ impl<T: SessionStream> Session<T> { // Address rewriting if let Some(new_address) = self - .core - .core + .server .eval_if::<String, _>( - &self.core.core.smtp.session.rcpt.rewrite, + &self.server.core.smtp.session.rcpt.rewrite, self, self.data.session_id, ) @@ -187,22 +184,20 @@ impl<T: SessionStream> Session<T> { // Verify address let rcpt = self.data.rcpt_to.last().unwrap(); if let Some(directory) = self - .core - .core + .server .eval_if::<String, _>( - &self.core.core.smtp.session.rcpt.directory, + &self.server.core.smtp.session.rcpt.directory, self, self.data.session_id, ) .await - .and_then(|name| self.core.core.get_directory(&name)) + .and_then(|name| self.server.get_directory(&name)) { match directory.is_local_domain(&rcpt.domain).await { Ok(is_local_domain) => { if is_local_domain { match self - .core - .core + .server .rcpt(directory, &rcpt.address_lcase, self.data.session_id) .await { @@ -233,10 +228,9 @@ impl<T: SessionStream> Session<T> { } } } else if !self - .core - .core + .server .eval_if( - &self.core.core.smtp.session.rcpt.relay, + &self.server.core.smtp.session.rcpt.relay, self, self.data.session_id, ) @@ -266,10 +260,9 @@ impl<T: SessionStream> Session<T> { } } } else if !self - .core - .core + .server .eval_if( - &self.core.core.smtp.session.rcpt.relay, + &self.server.core.smtp.session.rcpt.relay, self, self.data.session_id, ) @@ -315,12 +308,7 @@ impl<T: SessionStream> Session<T> { if self.data.rcpt_errors < self.params.rcpt_errors_max { Ok(()) } else { - match self - .core - .core - .is_rcpt_fail2banned(self.data.remote_ip) - .await - { + match self.server.is_rcpt_fail2banned(self.data.remote_ip).await { Ok(true) => { trc::event!( Security(SecurityEvent::BruteForceBan), diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index b0c91672..02648b5f 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -84,10 +84,9 @@ impl<T: SessionStream> Session<T> { initial_response, } => { let auth: u64 = self - .core - .core + .server .eval_if::<Mechanism, _>( - &self.core.core.smtp.session.auth.mechanisms, + &self.server.core.smtp.session.auth.mechanisms, self, self.data.session_id, ) diff --git a/crates/smtp/src/inbound/spawn.rs b/crates/smtp/src/inbound/spawn.rs index e6892fc1..d7e08f8b 100644 --- a/crates/smtp/src/inbound/spawn.rs +++ b/crates/smtp/src/inbound/spawn.rs @@ -8,6 +8,7 @@ use std::time::Instant; use common::{ config::smtp::session::Stage, + core::BuildServer, listener::{self, SessionManager, SessionStream}, }; use tokio_rustls::server::TlsStream; @@ -15,7 +16,6 @@ use trc::{SecurityEvent, SmtpEvent}; use crate::{ core::{Session, SessionData, SessionParameters, SmtpSessionManager, State}, - queue, reporting, scripts::ScriptResult, }; @@ -27,7 +27,7 @@ impl SessionManager for SmtpSessionManager { // Create session let mut session = Session { hostname: String::new(), - core: self.inner.into(), + server: self.inner.build_server(), instance: session.instance, state: State::default(), stream: session.stream, @@ -59,19 +59,23 @@ impl SessionManager for SmtpSessionManager { #[allow(clippy::manual_async_fn)] fn shutdown(&self) -> impl std::future::Future<Output = ()> + Send { async { - let _ = self.inner.inner.queue_tx.send(queue::Event::Stop).await; let _ = self .inner + .ipc + .queue_tx + .send(common::ipc::QueueEvent::Stop) + .await; + let _ = self .inner + .ipc .report_tx - .send(reporting::Event::Stop) + .send(common::ipc::ReportingEvent::Stop) .await; let _ = self .inner - .inner .ipc .delivery_tx - .send(common::DeliveryEvent::Stop) + .send(common::ipc::DeliveryEvent::Stop) .await; } } @@ -81,17 +85,15 @@ impl<T: SessionStream> Session<T> { pub async fn init_conn(&mut self) -> bool { self.eval_session_params().await; - let config = &self.core.core.smtp.session.connect; + let config = &self.server.core.smtp.session.connect; // Sieve filtering if let Some((script, script_id)) = self - .core - .core + .server .eval_if::<String, _>(&config.script, self, self.data.session_id) .await .and_then(|name| { - self.core - .core + self.server .get_trusted_sieve_script(&name, self.data.session_id) .map(|s| (s, name)) }) @@ -123,8 +125,7 @@ impl<T: SessionStream> Session<T> { // Obtain hostname self.hostname = self - .core - .core + .server .eval_if::<String, _>(&config.hostname, self, self.data.session_id) .await .unwrap_or_default(); @@ -138,8 +139,7 @@ impl<T: SessionStream> Session<T> { // Obtain greeting let greeting = self - .core - .core + .server .eval_if::<String, _>(&config.greeting, self, self.data.session_id) .await .filter(|g| !g.is_empty()) @@ -194,10 +194,7 @@ impl<T: SessionStream> Session<T> { .await .ok(); - match self - .core - .core - .is_loiter_fail2banned(self.data.remote_ip) + match self.server.is_loiter_fail2banned(self.data.remote_ip) .await { Ok(true) => { @@ -277,7 +274,7 @@ impl<T: SessionStream> Session<T> { state: self.state, data: self.data, instance: self.instance, - core: self.core, + server: self.server, in_flight: self.in_flight, params: self.params, }) diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs index 0c23687d..a99823d0 100644 --- a/crates/smtp/src/inbound/vrfy.rs +++ b/crates/smtp/src/inbound/vrfy.rs @@ -13,20 +13,18 @@ use std::fmt::Write; impl<T: SessionStream> Session<T> { pub async fn handle_vrfy(&mut self, address: String) -> Result<(), ()> { match self - .core - .core + .server .eval_if::<String, _>( - &self.core.core.smtp.session.rcpt.directory, + &self.server.core.smtp.session.rcpt.directory, self, self.data.session_id, ) .await - .and_then(|name| self.core.core.get_directory(&name)) + .and_then(|name| self.server.get_directory(&name)) { Some(directory) if self.params.can_vrfy => { match self - .core - .core + .server .vrfy(directory, &address.to_lowercase(), self.data.session_id) .await { @@ -88,20 +86,18 @@ impl<T: SessionStream> Session<T> { pub async fn handle_expn(&mut self, address: String) -> Result<(), ()> { match self - .core - .core + .server .eval_if::<String, _>( - &self.core.core.smtp.session.rcpt.directory, + &self.server.core.smtp.session.rcpt.directory, self, self.data.session_id, ) .await - .and_then(|name| self.core.core.get_directory(&name)) + .and_then(|name| self.server.get_directory(&name)) { Some(directory) if self.params.can_expn => { match self - .core - .core + .server .expn(directory, &address.to_lowercase(), self.data.session_id) .await { diff --git a/crates/smtp/src/lib.rs b/crates/smtp/src/lib.rs index eaef7d5d..0713f9bb 100644 --- a/crates/smtp/src/lib.rs +++ b/crates/smtp/src/lib.rs @@ -4,17 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::core::{throttle::ThrottleKeyHasherBuilder, TlsConnectors}; -use core::{Inner, SmtpInstance, SMTP}; use std::sync::Arc; -use common::{config::scripts::ScriptCache, Ipc, SharedCore}; -use dashmap::DashMap; -use mail_send::smtp::tls::build_tls_connector; +use common::{ + manager::boot::{BootManager, IpcReceivers}, + Inner, +}; use queue::manager::SpawnQueue; use reporting::scheduler::SpawnReport; -use tokio::sync::mpsc; -use utils::{config::Config, snowflake::SnowflakeIdGenerator}; pub mod core; pub mod inbound; @@ -23,54 +20,26 @@ pub mod queue; pub mod reporting; pub mod scripts; -impl SMTP { - pub async fn init( - config: &mut Config, - core: SharedCore, - ipc: Ipc, - span_id_gen: Arc<SnowflakeIdGenerator>, - ) -> SmtpInstance { - // Build inner - let capacity = config.property("cache.capacity").unwrap_or(2); - let shard = config - .property::<u64>("cache.shard") - .unwrap_or(32) - .next_power_of_two() as usize; - let (queue_tx, queue_rx) = mpsc::channel(1024); - let (report_tx, report_rx) = mpsc::channel(1024); - let inner = Inner { - session_throttle: DashMap::with_capacity_and_hasher_and_shard_amount( - capacity, - ThrottleKeyHasherBuilder::default(), - shard, - ), - queue_throttle: DashMap::with_capacity_and_hasher_and_shard_amount( - capacity, - ThrottleKeyHasherBuilder::default(), - shard, - ), - queue_tx, - report_tx, - queue_id_gen: config - .property::<u64>("cluster.node-id") - .map(SnowflakeIdGenerator::with_node_id) - .unwrap_or_default(), - span_id_gen, - connectors: TlsConnectors { - pki_verify: build_tls_connector(false), - dummy_verify: build_tls_connector(true), - }, - ipc, - script_cache: ScriptCache::parse(config), - }; - let inner = SmtpInstance::new(core, inner); +pub trait StartQueueManager { + fn start_queue_manager(&mut self); +} + +pub trait SpawnQueueManager { + fn spawn_queue_manager(&mut self, inner: Arc<Inner>); +} +impl StartQueueManager for BootManager { + fn start_queue_manager(&mut self) { + self.ipc_rxs.spawn_queue_manager(self.inner.clone()); + } +} + +impl SpawnQueueManager for IpcReceivers { + fn spawn_queue_manager(&mut self, inner: Arc<Inner>) { // Spawn queue manager - queue_rx.spawn(inner.clone()); + self.queue_rx.take().unwrap().spawn(inner.clone()); // Spawn report manager - report_rx.spawn(inner.clone()); - - inner + self.report_rx.take().unwrap().spawn(inner); } } diff --git a/crates/smtp/src/outbound/client.rs b/crates/smtp/src/outbound/client.rs index f064781a..5391cc52 100644 --- a/crates/smtp/src/outbound/client.rs +++ b/crates/smtp/src/outbound/client.rs @@ -177,10 +177,8 @@ impl<T: AsyncRead + AsyncWrite + Unpin> SmtpClient<T> { params: &SessionParams<'_>, ) -> Result<(), Status<(), Error>> { match params - .core - .core - .storage - .blob + .server + .blob_store() .get_blob(message.blob_hash.as_slice(), 0..usize::MAX) .await { diff --git a/crates/smtp/src/outbound/dane/dnssec.rs b/crates/smtp/src/outbound/dane/dnssec.rs index 1c2f11e3..de89c1f3 100644 --- a/crates/smtp/src/outbound/dane/dnssec.rs +++ b/crates/smtp/src/outbound/dane/dnssec.rs @@ -4,7 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::config::smtp::resolver::{Tlsa, TlsaEntry}; +use common::{ + config::smtp::resolver::{Tlsa, TlsaEntry}, + Server, +}; use mail_auth::{ common::{lru::DnsCache, resolver::IntoFqdn}, hickory_resolver::{ @@ -16,14 +19,27 @@ use mail_auth::{ Name, }, }; -use std::sync::Arc; +use std::{future::Future, sync::Arc}; -use crate::core::SMTP; +pub trait TlsaLookup: Sync + Send { + fn tlsa_lookup<'x>( + &self, + key: impl IntoFqdn<'x> + Sync + Send, + ) -> impl Future<Output = mail_auth::Result<Option<Arc<Tlsa>>>> + Send; -impl SMTP { - pub async fn tlsa_lookup<'x>( + #[cfg(feature = "test_mode")] + fn tlsa_add<'x>( &self, key: impl IntoFqdn<'x>, + value: impl Into<Arc<Tlsa>>, + valid_until: std::time::Instant, + ); +} + +impl TlsaLookup for Server { + async fn tlsa_lookup<'x>( + &self, + key: impl IntoFqdn<'x> + Sync + Send, ) -> mail_auth::Result<Option<Arc<Tlsa>>> { let key = key.into_fqdn(); if let Some(value) = self.core.smtp.resolvers.cache.tlsa.get(key.as_ref()) { @@ -102,7 +118,7 @@ impl SMTP { } #[cfg(feature = "test_mode")] - pub fn tlsa_add<'x>( + fn tlsa_add<'x>( &self, key: impl IntoFqdn<'x>, value: impl Into<Arc<Tlsa>>, diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index 8fe5c968..2a8694f2 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -5,12 +5,21 @@ */ use crate::outbound::client::{from_error_status, from_mail_send_error, SmtpClient}; +use crate::outbound::dane::dnssec::TlsaLookup; +use crate::outbound::lookup::DnsLookup; +use crate::outbound::mta_sts::lookup::MtaStsLookup; use crate::outbound::mta_sts::verify::VerifyPolicy; use crate::outbound::{client::StartTlsResult, dane::verify::TlsaVerify}; +use crate::queue::dsn::SendDsn; +use crate::queue::spool::SmtpSpool; +use crate::queue::throttle::IsAllowed; +use crate::reporting::SmtpReporting; use common::config::{ server::ServerProtocol, smtp::{queue::RequireOptional, report::AggregateFrequency}, }; +use common::ipc::{OnHold, PolicyType, QueueEvent, TlsEvent}; +use common::Server; use mail_auth::{ mta_sts::TlsRpt, report::tlsrpt::{FailureDetails, ResultType}, @@ -20,31 +29,28 @@ use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, time::{Duration, Instant}, }; -use store::write::{now, BatchBuilder, QueueClass, QueueEvent, ValueClass}; +use store::write::{now, BatchBuilder, QueueClass, ValueClass}; use trc::{DaneEvent, DeliveryEvent, MtaStsEvent, ServerEvent, TlsRptEvent}; use crate::{ - core::SMTP, queue::{ErrorDetails, Message}, - reporting::{tls::TlsRptOptions, PolicyType, TlsEvent}, + reporting::tls::TlsRptOptions, }; use super::{lookup::ToNextHop, mta_sts, session::SessionParams, NextHop, TlsStrategy}; -use crate::queue::{ - throttle, DeliveryAttempt, Domain, Error, Event, OnHold, QueueEnvelope, Status, -}; +use crate::queue::{throttle, DeliveryAttempt, Domain, Error, QueueEnvelope, Status}; impl DeliveryAttempt { - pub async fn try_deliver(mut self, core: SMTP) { + pub async fn try_deliver(mut self, server: Server) { tokio::spawn(async move { // Lock message - if let Some(event) = core.try_lock_event(self.event).await { + if let Some(event) = server.try_lock_event(self.event).await { self.event = event; // Fetch message - if let Some(mut message) = core.read_message(self.event.queue_id).await { + if let Some(mut message) = server.read_message(self.event.queue_id).await { // Generate span id - message.span_id = core.inner.span_id_gen.generate().unwrap_or_else(now); + message.span_id = server.inner.data.span_id_gen.generate().unwrap_or_else(now); let span_id = message.span_id; trc::event!( @@ -76,7 +82,7 @@ impl DeliveryAttempt { // Attempt delivery let start_time = Instant::now(); - self.deliver_task(core, message).await; + self.deliver_task(server, message).await; trc::event!( Delivery(DeliveryEvent::AttemptEnd), @@ -86,12 +92,14 @@ impl DeliveryAttempt { } else { // Message no longer exists, delete queue event. let mut batch = BatchBuilder::new(); - batch.clear(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { - due: self.event.due, - queue_id: self.event.queue_id, - }))); + batch.clear(ValueClass::Queue(QueueClass::MessageEvent( + store::write::QueueEvent { + due: self.event.due, + queue_id: self.event.queue_id, + }, + ))); - if let Err(err) = core.core.storage.data.write(batch.build()).await { + if let Err(err) = server.store().write(batch.build()).await { trc::error!(err .details("Failed to delete queue event.") .caused_by(trc::location!())); @@ -101,13 +109,13 @@ impl DeliveryAttempt { }); } - async fn deliver_task(mut self, core: SMTP, mut message: Message) { + async fn deliver_task(mut self, server: Server, mut message: Message) { // Check that the message still has recipients to be delivered let has_pending_delivery = message.has_pending_delivery(); let span_id = message.span_id; // Send any due Delivery Status Notifications - core.send_dsn(&mut message).await; + server.send_dsn(&mut message).await; if has_pending_delivery { // Re-queue the message if its not yet due for delivery @@ -115,9 +123,16 @@ impl DeliveryAttempt { if due > now() { // Save changes message - .save_changes(&core, self.event.due.into(), due.into()) + .save_changes(&server, self.event.due.into(), due.into()) .await; - if core.inner.queue_tx.send(Event::Reload).await.is_err() { + if server + .inner + .ipc + .queue_tx + .send(QueueEvent::Reload) + .await + .is_err() + { trc::event!( Server(ServerEvent::ThreadError), Reason = "Channel closed.", @@ -135,8 +150,15 @@ impl DeliveryAttempt { ); // All message recipients expired, do not re-queue. (DSN has been already sent) - message.remove(&core, self.event.due).await; - if core.inner.queue_tx.send(Event::Reload).await.is_err() { + message.remove(&server, self.event.due).await; + if server + .inner + .ipc + .queue_tx + .send(QueueEvent::Reload) + .await + .is_err() + { trc::event!( Server(ServerEvent::ThreadError), Reason = "Channel closed.", @@ -149,8 +171,8 @@ impl DeliveryAttempt { } // Throttle sender - for throttle in &core.core.smtp.queue.throttle.sender { - if let Err(err) = core + for throttle in &server.core.smtp.queue.throttle.sender { + if let Err(err) = server .is_allowed(throttle, &message, &mut self.in_flight, message.span_id) .await { @@ -158,7 +180,7 @@ impl DeliveryAttempt { throttle::Error::Concurrency { limiter } => { // Save changes to disk let next_due = message.next_event_after(now()); - message.save_changes(&core, None, None).await; + message.save_changes(&server, None, None).await; trc::event!( Delivery(DeliveryEvent::ConcurrencyLimitExceeded), @@ -166,7 +188,7 @@ impl DeliveryAttempt { SpanId = span_id, ); - Event::OnHold(OnHold { + QueueEvent::OnHold(OnHold { next_due, limiters: vec![limiter], message: self.event, @@ -187,14 +209,14 @@ impl DeliveryAttempt { ); message - .save_changes(&core, self.event.due.into(), next_event.into()) + .save_changes(&server, self.event.due.into(), next_event.into()) .await; - Event::Reload + QueueEvent::Reload } }; - if core.inner.queue_tx.send(event).await.is_err() { + if server.inner.ipc.queue_tx.send(event).await.is_err() { trc::event!( Server(ServerEvent::ThreadError), Reason = "Channel closed.", @@ -206,7 +228,7 @@ impl DeliveryAttempt { } } - let queue_config = &core.core.smtp.queue; + let queue_config = &server.core.smtp.queue; let mut on_hold = Vec::new(); let no_ip = IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)); let mut recipients = std::mem::take(&mut message.recipients); @@ -232,7 +254,7 @@ impl DeliveryAttempt { // Throttle recipient domain let mut in_flight = Vec::new(); for throttle in &queue_config.throttle.rcpt { - if let Err(err) = core + if let Err(err) = server .is_allowed(throttle, &envelope, &mut in_flight, message.span_id) .await { @@ -249,24 +271,22 @@ impl DeliveryAttempt { } // Obtain next hop - let (mut remote_hosts, is_smtp) = match core - .core + let (mut remote_hosts, is_smtp) = match server .eval_if::<String, _>(&queue_config.next_hop, &envelope, message.span_id) .await - .and_then(|name| core.core.get_relay_host(&name, message.span_id)) + .and_then(|name| server.get_relay_host(&name, message.span_id)) { Some(next_hop) if next_hop.protocol == ServerProtocol::Http => { // Deliver message locally let delivery_result = message .deliver_local( recipients.iter_mut().filter(|r| r.domain_idx == domain_idx), - &core.inner.ipc.delivery_tx, + &server.inner.ipc.delivery_tx, ) .await; // Update status for the current domain and continue with the next one - let schedule = core - .core + let schedule = server .eval_if::<Vec<Duration>, _>( &queue_config.retry, &envelope, @@ -286,23 +306,24 @@ impl DeliveryAttempt { // Prepare TLS strategy let mut tls_strategy = TlsStrategy { - mta_sts: core - .core + mta_sts: server .eval_if(&queue_config.tls.mta_sts, &envelope, message.span_id) .await .unwrap_or(RequireOptional::Optional), ..Default::default() }; - let allow_invalid_certs = core - .core + let allow_invalid_certs = server .eval_if(&queue_config.tls.invalid_certs, &envelope, message.span_id) .await .unwrap_or(false); // Obtain TLS reporting - let tls_report = match core - .core - .eval_if(&core.core.smtp.report.tls.send, &envelope, message.span_id) + let tls_report = match server + .eval_if( + &server.core.smtp.report.tls.send, + &envelope, + message.span_id, + ) .await .unwrap_or(AggregateFrequency::Never) { @@ -312,7 +333,7 @@ impl DeliveryAttempt { if is_smtp => { let time = Instant::now(); - match core + match server .core .smtp .resolvers @@ -357,10 +378,10 @@ impl DeliveryAttempt { // Obtain MTA-STS policy for domain let mta_sts_policy = if tls_strategy.try_mta_sts() && is_smtp { let time = Instant::now(); - match core + match server .lookup_mta_sts_policy( &domain.domain, - core.core + server .eval_if(&queue_config.timeout.mta_sts, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(10 * 60)), @@ -390,7 +411,7 @@ impl DeliveryAttempt { match &err { mta_sts::Error::Dns(mail_auth::Error::DnsRecordNotFound(_)) => { if strict { - core.schedule_report(TlsEvent { + server.schedule_report(TlsEvent { policy: PolicyType::Sts(None), domain: domain.domain.to_string(), failure: FailureDetails::new(ResultType::Other) @@ -406,16 +427,17 @@ impl DeliveryAttempt { } mta_sts::Error::Dns(mail_auth::Error::DnsError(_)) => (), _ => { - core.schedule_report(TlsEvent { - policy: PolicyType::Sts(None), - domain: domain.domain.to_string(), - failure: FailureDetails::new(&err) - .with_failure_reason_code(err.to_string()) - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + server + .schedule_report(TlsEvent { + policy: PolicyType::Sts(None), + domain: domain.domain.to_string(), + failure: FailureDetails::new(&err) + .with_failure_reason_code(err.to_string()) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } } } @@ -463,8 +485,7 @@ impl DeliveryAttempt { } if strict { - let schedule = core - .core + let schedule = server .eval_if::<Vec<Duration>, _>( &queue_config.retry, &envelope, @@ -488,7 +509,14 @@ impl DeliveryAttempt { if is_smtp && remote_hosts.is_empty() { // Lookup MX let time = Instant::now(); - mx_list = match core.core.smtp.resolvers.dns.mx_lookup(&domain.domain).await { + mx_list = match server + .core + .smtp + .resolvers + .dns + .mx_lookup(&domain.domain) + .await + { Ok(mx) => mx, Err(err) => { trc::event!( @@ -499,8 +527,7 @@ impl DeliveryAttempt { Elapsed = time.elapsed(), ); - let schedule = core - .core + let schedule = server .eval_if::<Vec<Duration>, _>( &queue_config.retry, &envelope, @@ -515,7 +542,7 @@ impl DeliveryAttempt { if let Some(remote_hosts_) = mx_list.to_remote_hosts( &domain.domain, - core.core + server .eval_if(&queue_config.max_mx, &envelope, message.span_id) .await .unwrap_or(5), @@ -539,8 +566,7 @@ impl DeliveryAttempt { Elapsed = time.elapsed(), ); - let schedule = core - .core + let schedule = server .eval_if::<Vec<Duration>, _>( &queue_config.retry, &envelope, @@ -559,8 +585,7 @@ impl DeliveryAttempt { } // Try delivering message - let max_multihomed = core - .core + let max_multihomed = server .eval_if(&queue_config.max_multihomed, &envelope, message.span_id) .await .unwrap_or(2); @@ -573,17 +598,18 @@ impl DeliveryAttempt { if !mta_sts_policy.verify(envelope.mx) { // Report MTA-STS failed verification if let Some(tls_report) = &tls_report { - core.schedule_report(TlsEvent { - policy: mta_sts_policy.into(), - domain: domain.domain.to_string(), - failure: FailureDetails::new(ResultType::ValidationFailure) - .with_receiving_mx_hostname(envelope.mx) - .with_failure_reason_code("MX not authorized by policy.") - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + server + .schedule_report(TlsEvent { + policy: mta_sts_policy.into(), + domain: domain.domain.to_string(), + failure: FailureDetails::new(ResultType::ValidationFailure) + .with_receiving_mx_hostname(envelope.mx) + .with_failure_reason_code("MX not authorized by policy.") + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } trc::event!( @@ -624,7 +650,7 @@ impl DeliveryAttempt { // Obtain source and remote IPs let time = Instant::now(); - let resolve_result = match core + let resolve_result = match server .resolve_host(remote_host, &envelope, max_multihomed, message.span_id) .await { @@ -661,13 +687,11 @@ impl DeliveryAttempt { }; // Update TLS strategy - tls_strategy.dane = core - .core + tls_strategy.dane = server .eval_if(&queue_config.tls.dane, &envelope, message.span_id) .await .unwrap_or(RequireOptional::Optional); - tls_strategy.tls = core - .core + tls_strategy.tls = server .eval_if(&queue_config.tls.start, &envelope, message.span_id) .await .unwrap_or(RequireOptional::Optional); @@ -676,7 +700,10 @@ impl DeliveryAttempt { let dane_policy = if tls_strategy.try_dane() && is_smtp { let time = Instant::now(); let strict = tls_strategy.is_dane_required(); - match core.tlsa_lookup(format!("_25._tcp.{}.", envelope.mx)).await { + match server + .tlsa_lookup(format!("_25._tcp.{}.", envelope.mx)) + .await + { Ok(Some(tlsa)) => { if tlsa.has_end_entities { trc::event!( @@ -703,17 +730,18 @@ impl DeliveryAttempt { // Report invalid TLSA record if let Some(tls_report) = &tls_report { - core.schedule_report(TlsEvent { - policy: tlsa.into(), - domain: domain.domain.to_string(), - failure: FailureDetails::new(ResultType::TlsaInvalid) - .with_receiving_mx_hostname(envelope.mx) - .with_failure_reason_code("Invalid TLSA record.") - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + server + .schedule_report(TlsEvent { + policy: tlsa.into(), + domain: domain.domain.to_string(), + failure: FailureDetails::new(ResultType::TlsaInvalid) + .with_receiving_mx_hostname(envelope.mx) + .with_failure_reason_code("Invalid TLSA record.") + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } if strict { @@ -740,19 +768,20 @@ impl DeliveryAttempt { if strict { // Report DANE required if let Some(tls_report) = &tls_report { - core.schedule_report(TlsEvent { - policy: PolicyType::Tlsa(None), - domain: domain.domain.to_string(), - failure: FailureDetails::new(ResultType::DaneRequired) - .with_receiving_mx_hostname(envelope.mx) - .with_failure_reason_code( - "No TLSA DNSSEC records found.", - ) - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + server + .schedule_report(TlsEvent { + policy: PolicyType::Tlsa(None), + domain: domain.domain.to_string(), + failure: FailureDetails::new(ResultType::DaneRequired) + .with_receiving_mx_hostname(envelope.mx) + .with_failure_reason_code( + "No TLSA DNSSEC records found.", + ) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } last_status = @@ -792,19 +821,22 @@ impl DeliveryAttempt { last_status = if not_found { // Report DANE required if let Some(tls_report) = &tls_report { - core.schedule_report(TlsEvent { - policy: PolicyType::Tlsa(None), - domain: domain.domain.to_string(), - failure: FailureDetails::new(ResultType::DaneRequired) + server + .schedule_report(TlsEvent { + policy: PolicyType::Tlsa(None), + domain: domain.domain.to_string(), + failure: FailureDetails::new( + ResultType::DaneRequired, + ) .with_receiving_mx_hostname(envelope.mx) .with_failure_reason_code( "No TLSA records found for MX.", ) .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } Status::PermanentFailure(Error::DaneError(ErrorDetails { @@ -837,7 +869,7 @@ impl DeliveryAttempt { let mut in_flight_host = Vec::new(); envelope.remote_ip = remote_ip; for throttle in &queue_config.throttle.host { - if let Err(err) = core + if let Err(err) = server .is_allowed(throttle, &envelope, &mut in_flight_host, message.span_id) .await { @@ -854,8 +886,7 @@ impl DeliveryAttempt { // Connect let time = Instant::now(); - let conn_timeout = core - .core + let conn_timeout = server .eval_if(&queue_config.timeout.connect, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 60)); @@ -908,8 +939,7 @@ impl DeliveryAttempt { }; // Obtain session parameters - let local_hostname = core - .core + let local_hostname = server .eval_if::<String, _>(&queue_config.hostname, &envelope, message.span_id) .await .filter(|s| !s.is_empty()) @@ -922,28 +952,24 @@ impl DeliveryAttempt { }); let params = SessionParams { session_id: message.span_id, - core: &core, + server: &server, credentials: remote_host.credentials(), is_smtp: remote_host.is_smtp(), hostname: envelope.mx, local_hostname: &local_hostname, - timeout_ehlo: core - .core + timeout_ehlo: server .eval_if(&queue_config.timeout.ehlo, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 60)), - timeout_mail: core - .core + timeout_mail: server .eval_if(&queue_config.timeout.mail, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 60)), - timeout_rcpt: core - .core + timeout_rcpt: server .eval_if(&queue_config.timeout.rcpt, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 60)), - timeout_data: core - .core + timeout_data: server .eval_if(&queue_config.timeout.data, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 60)), @@ -956,15 +982,14 @@ impl DeliveryAttempt { || dane_policy.is_some(); let tls_connector = if allow_invalid_certs || remote_host.allow_invalid_certs() { - &core.inner.connectors.dummy_verify + &server.inner.data.smtp_connectors.dummy_verify } else { - &core.inner.connectors.pki_verify + &server.inner.data.smtp_connectors.pki_verify }; let delivery_result = if !remote_host.implicit_tls() { // Read greeting - smtp_client.timeout = core - .core + smtp_client.timeout = server .eval_if(&queue_config.timeout.greeting, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 60)); @@ -1014,8 +1039,7 @@ impl DeliveryAttempt { // Try starting TLS if tls_strategy.try_start_tls() { let time = Instant::now(); - smtp_client.timeout = core - .core + smtp_client.timeout = server .eval_if(&queue_config.timeout.tls, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(3 * 60)); @@ -1055,22 +1079,23 @@ impl DeliveryAttempt { ) { // Report DANE verification failure if let Some(tls_report) = &tls_report { - core.schedule_report(TlsEvent { - policy: dane_policy.into(), - domain: domain.domain.to_string(), - failure: FailureDetails::new( - ResultType::ValidationFailure, - ) - .with_receiving_mx_hostname(envelope.mx) - .with_receiving_ip(remote_ip) - .with_failure_reason_code( - "No matching certificates found.", - ) - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + server + .schedule_report(TlsEvent { + policy: dane_policy.into(), + domain: domain.domain.to_string(), + failure: FailureDetails::new( + ResultType::ValidationFailure, + ) + .with_receiving_mx_hostname(envelope.mx) + .with_receiving_ip(remote_ip) + .with_failure_reason_code( + "No matching certificates found.", + ) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } last_status = status; @@ -1080,14 +1105,15 @@ impl DeliveryAttempt { // Report TLS success if let Some(tls_report) = &tls_report { - core.schedule_report(TlsEvent { - policy: (&mta_sts_policy, &dane_policy).into(), - domain: domain.domain.to_string(), - failure: None, - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + server + .schedule_report(TlsEvent { + policy: (&mta_sts_policy, &dane_policy).into(), + domain: domain.domain.to_string(), + failure: None, + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } // Deliver message over TLS @@ -1126,20 +1152,21 @@ impl DeliveryAttempt { ); if let Some(tls_report) = &tls_report { - core.schedule_report(TlsEvent { - policy: (&mta_sts_policy, &dane_policy).into(), - domain: domain.domain.to_string(), - failure: FailureDetails::new( - ResultType::StartTlsNotSupported, - ) - .with_receiving_mx_hostname(envelope.mx) - .with_receiving_ip(remote_ip) - .with_failure_reason_code(reason) - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + server + .schedule_report(TlsEvent { + policy: (&mta_sts_policy, &dane_policy).into(), + domain: domain.domain.to_string(), + failure: FailureDetails::new( + ResultType::StartTlsNotSupported, + ) + .with_receiving_mx_hostname(envelope.mx) + .with_receiving_ip(remote_ip) + .with_failure_reason_code(reason) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } if is_strict_tls { @@ -1173,20 +1200,21 @@ impl DeliveryAttempt { if let (Some(tls_report), mail_send::Error::Tls(error)) = (&tls_report, &error) { - core.schedule_report(TlsEvent { - policy: (&mta_sts_policy, &dane_policy).into(), - domain: domain.domain.to_string(), - failure: FailureDetails::new( - ResultType::CertificateNotTrusted, - ) - .with_receiving_mx_hostname(envelope.mx) - .with_receiving_ip(remote_ip) - .with_failure_reason_code(error.to_string()) - .into(), - tls_record: tls_report.record.clone(), - interval: tls_report.interval, - }) - .await; + server + .schedule_report(TlsEvent { + policy: (&mta_sts_policy, &dane_policy).into(), + domain: domain.domain.to_string(), + failure: FailureDetails::new( + ResultType::CertificateNotTrusted, + ) + .with_receiving_mx_hostname(envelope.mx) + .with_receiving_ip(remote_ip) + .with_failure_reason_code(error.to_string()) + .into(), + tls_record: tls_report.record.clone(), + interval: tls_report.interval, + }) + .await; } last_status = if is_strict_tls { @@ -1216,8 +1244,7 @@ impl DeliveryAttempt { } } else { // Start TLS - smtp_client.timeout = core - .core + smtp_client.timeout = server .eval_if(&queue_config.timeout.tls, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(3 * 60)); @@ -1239,8 +1266,7 @@ impl DeliveryAttempt { }; // Read greeting - smtp_client.timeout = core - .core + smtp_client.timeout = server .eval_if(&queue_config.timeout.greeting, &envelope, message.span_id) .await .unwrap_or_else(|| Duration::from_secs(5 * 60)); @@ -1268,8 +1294,7 @@ impl DeliveryAttempt { }; // Update status for the current domain and continue with the next one - let schedule = core - .core + let schedule = server .eval_if::<Vec<Duration>, _>( &queue_config.retry, &envelope, @@ -1283,8 +1308,7 @@ impl DeliveryAttempt { } // Update status - let schedule = core - .core + let schedule = server .eval_if::<Vec<Duration>, _>(&queue_config.retry, &envelope, message.span_id) .await .unwrap_or_else(|| vec![Duration::from_secs(60)]); @@ -1293,20 +1317,20 @@ impl DeliveryAttempt { message.recipients = recipients; // Send Delivery Status Notifications - core.send_dsn(&mut message).await; + server.send_dsn(&mut message).await; // Notify queue manager let result = if !on_hold.is_empty() { // Save changes to disk let next_due = message.next_event_after(now()); - message.save_changes(&core, None, None).await; + message.save_changes(&server, None, None).await; trc::event!( Delivery(DeliveryEvent::ConcurrencyLimitExceeded), SpanId = span_id, ); - Event::OnHold(OnHold { + QueueEvent::OnHold(OnHold { next_due, limiters: on_hold, message: self.event, @@ -1322,10 +1346,10 @@ impl DeliveryAttempt { // Save changes to disk message - .save_changes(&core, self.event.due.into(), due.into()) + .save_changes(&server, self.event.due.into(), due.into()) .await; - Event::Reload + QueueEvent::Reload } else { trc::event!( Delivery(DeliveryEvent::Completed), @@ -1334,11 +1358,11 @@ impl DeliveryAttempt { ); // Delete message from queue - message.remove(&core, self.event.due).await; + message.remove(&server, self.event.due).await; - Event::Reload + QueueEvent::Reload }; - if core.inner.queue_tx.send(result).await.is_err() { + if server.inner.ipc.queue_tx.send(result).await.is_err() { trc::event!( Server(ServerEvent::ThreadError), Reason = "Channel closed.", diff --git a/crates/smtp/src/outbound/local.rs b/crates/smtp/src/outbound/local.rs index c4f71501..4d900967 100644 --- a/crates/smtp/src/outbound/local.rs +++ b/crates/smtp/src/outbound/local.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{DeliveryEvent, DeliveryResult, IngestMessage}; +use common::ipc::{DeliveryEvent, DeliveryResult, IngestMessage}; use smtp_proto::Response; use tokio::sync::{mpsc, oneshot}; use trc::ServerEvent; diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index 8b8ba8f5..8b66cc86 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -5,18 +5,19 @@ */ use std::{ + future::Future, net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::Arc, }; -use common::expr::{functions::ResolveVariable, V_MX}; +use common::{ + expr::{functions::ResolveVariable, V_MX}, + Server, +}; use mail_auth::{IpLookupStrategy, MX}; use rand::{seq::SliceRandom, Rng}; -use crate::{ - core::SMTP, - queue::{Error, ErrorDetails, Status}, -}; +use crate::queue::{Error, ErrorDetails, Status}; use super::NextHop; @@ -26,8 +27,25 @@ pub struct IpLookupResult { pub remote_ips: Vec<IpAddr>, } -impl SMTP { - pub async fn ip_lookup( +pub trait DnsLookup: Sync + Send { + fn ip_lookup( + &self, + key: &str, + strategy: IpLookupStrategy, + max_results: usize, + ) -> impl Future<Output = mail_auth::Result<Vec<IpAddr>>> + Send; + + fn resolve_host<'x>( + &'x self, + remote_host: &NextHop<'_>, + envelope: &impl ResolveVariable, + max_multihomed: usize, + session_id: u64, + ) -> impl Future<Output = Result<IpLookupResult, Status<(), Error>>> + Send; +} + +impl DnsLookup for Server { + async fn ip_lookup( &self, key: &str, strategy: IpLookupStrategy, @@ -82,7 +100,7 @@ impl SMTP { } } - pub async fn resolve_host<'x>( + async fn resolve_host<'x>( &'x self, remote_host: &NextHop<'_>, envelope: &impl ResolveVariable, @@ -92,8 +110,7 @@ impl SMTP { let remote_ips = self .ip_lookup( remote_host.fqdn_hostname().as_ref(), - self.core - .eval_if(&self.core.smtp.queue.ip_strategy, envelope, session_id) + self.eval_if(&self.core.smtp.queue.ip_strategy, envelope, session_id) .await .unwrap_or(IpLookupStrategy::Ipv4thenIpv6), max_multihomed, @@ -122,7 +139,6 @@ impl SMTP { // Obtain source IPv4 address let source_ips = self - .core .eval_if::<Vec<Ipv4Addr>, _>( &self.core.smtp.queue.source_ip.ipv4, envelope, @@ -144,7 +160,6 @@ impl SMTP { // Obtain source IPv6 address let source_ips = self - .core .eval_if::<Vec<Ipv6Addr>, _>( &self.core.smtp.queue.source_ip.ipv6, envelope, diff --git a/crates/smtp/src/outbound/mod.rs b/crates/smtp/src/outbound/mod.rs index 5f963f65..7bd71e2f 100644 --- a/crates/smtp/src/outbound/mod.rs +++ b/crates/smtp/src/outbound/mod.rs @@ -6,16 +6,17 @@ use std::borrow::Cow; -use common::config::{ - server::ServerProtocol, - smtp::queue::{RelayHost, RequireOptional}, +use common::{ + config::{ + server::ServerProtocol, + smtp::queue::{RelayHost, RequireOptional}, + }, + ipc::QueueEventLock, }; use mail_send::Credentials; use smtp_proto::{Response, Severity}; -use crate::queue::{ - spool::QueueEventLock, DeliveryAttempt, Error, ErrorDetails, HostResponse, Status, -}; +use crate::queue::{DeliveryAttempt, Error, ErrorDetails, HostResponse, Status}; pub mod client; pub mod dane; diff --git a/crates/smtp/src/outbound/mta_sts/lookup.rs b/crates/smtp/src/outbound/mta_sts/lookup.rs index 38fe9164..e6ee8f6b 100644 --- a/crates/smtp/src/outbound/mta_sts/lookup.rs +++ b/crates/smtp/src/outbound/mta_sts/lookup.rs @@ -13,11 +13,9 @@ use std::{ #[cfg(feature = "test_mode")] pub static STS_TEST_POLICY: parking_lot::Mutex<Vec<u8>> = parking_lot::Mutex::new(Vec::new()); -use common::config::smtp::resolver::Policy; +use common::{config::smtp::resolver::Policy, Server}; use mail_auth::{common::lru::DnsCache, mta_sts::MtaSts, report::tlsrpt::ResultType}; -use crate::core::SMTP; - use super::{parse::ParsePolicy, Error}; #[cfg(not(feature = "test_mode"))] @@ -26,9 +24,25 @@ use common::HttpLimitResponse; #[cfg(not(feature = "test_mode"))] const MAX_POLICY_SIZE: usize = 1024 * 1024; +pub trait MtaStsLookup: Sync + Send { + fn lookup_mta_sts_policy<'x>( + &self, + domain: &str, + timeout: Duration, + ) -> impl std::future::Future<Output = Result<Arc<Policy>, Error>> + Send; + + #[cfg(feature = "test_mode")] + fn policy_add<'x>( + &self, + key: impl mail_auth::common::resolver::IntoFqdn<'x>, + value: Policy, + valid_until: std::time::Instant, + ); +} + #[allow(unused_variables)] -impl SMTP { - pub async fn lookup_mta_sts_policy<'x>( +impl MtaStsLookup for Server { + async fn lookup_mta_sts_policy<'x>( &self, domain: &str, timeout: Duration, @@ -96,7 +110,7 @@ impl SMTP { } #[cfg(feature = "test_mode")] - pub fn policy_add<'x>( + fn policy_add<'x>( &self, key: impl mail_auth::common::resolver::IntoFqdn<'x>, value: Policy, diff --git a/crates/smtp/src/outbound/session.rs b/crates/smtp/src/outbound/session.rs index b2e0dfef..41bf98b6 100644 --- a/crates/smtp/src/outbound/session.rs +++ b/crates/smtp/src/outbound/session.rs @@ -5,6 +5,7 @@ */ use common::config::smtp::queue::RequireOptional; +use common::Server; use mail_send::Credentials; use smtp_proto::{ EhloResponse, Severity, EXT_CHUNKING, EXT_DSN, EXT_REQUIRE_TLS, EXT_SIZE, EXT_SMTP_UTF8, @@ -17,17 +18,14 @@ use tokio::io::{AsyncRead, AsyncWrite}; use trc::DeliveryEvent; use crate::outbound::client::{from_error_status, from_mail_send_error}; -use crate::{ - core::SMTP, - queue::{ErrorDetails, HostResponse, RCPT_STATUS_CHANGED}, -}; +use crate::queue::{ErrorDetails, HostResponse, RCPT_STATUS_CHANGED}; use crate::queue::{Error, Message, Recipient, Status}; use super::{client::SmtpClient, TlsStrategy}; pub struct SessionParams<'x> { - pub core: &'x SMTP, + pub server: &'x Server, pub hostname: &'x str, pub credentials: Option<&'x Credentials<String>>, pub is_smtp: bool, diff --git a/crates/smtp/src/queue/dsn.rs b/crates/smtp/src/queue/dsn.rs index ce2e5f34..97fb572e 100644 --- a/crates/smtp/src/queue/dsn.rs +++ b/crates/smtp/src/queue/dsn.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use common::Server; use mail_builder::headers::content_type::ContentType; use mail_builder::headers::HeaderType; use mail_builder::mime::{make_boundary, BodyPart, MimePart}; @@ -13,19 +14,26 @@ use smtp_proto::{ Response, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, }; use std::fmt::Write; +use std::future::Future; use std::time::Duration; use store::write::now; -use crate::core::SMTP; use crate::outbound::client::from_error_status; +use crate::reporting::SmtpReporting; +use super::spool::SmtpSpool; use super::{ Domain, Error, ErrorDetails, HostResponse, Message, MessageSource, QueueEnvelope, Recipient, Status, RCPT_DSN_SENT, RCPT_STATUS_CHANGED, }; -impl SMTP { - pub async fn send_dsn(&self, message: &mut Message) { +pub trait SendDsn: Sync + Send { + fn send_dsn(&self, message: &mut Message) -> impl Future<Output = ()> + Send; + fn log_dsn(&self, message: &Message) -> impl Future<Output = ()> + Send; +} + +impl SendDsn for Server { + async fn send_dsn(&self, message: &mut Message) { // Send DSN events self.log_dsn(message).await; @@ -152,8 +160,8 @@ impl SMTP { } impl Message { - pub async fn build_dsn(&mut self, core: &SMTP) -> Option<Vec<u8>> { - let config = &core.core.smtp.queue; + pub async fn build_dsn(&mut self, server: &Server) -> Option<Vec<u8>> { + let config = &server.core.smtp.queue; let now = now(); let mut txt_success = String::new(); @@ -314,8 +322,7 @@ impl Message { { let envelope = QueueEnvelope::new(self, domain_idx); - if let Some(next_notify) = core - .core + if let Some(next_notify) = server .eval_if::<Vec<Duration>, _>(&config.notify, &envelope, self.span_id) .await .and_then(|notify| { @@ -337,19 +344,16 @@ impl Message { } // Obtain hostname and sender addresses - let from_name = core - .core + let from_name = server .eval_if(&config.dsn.name, self, self.span_id) .await .unwrap_or_else(|| String::from("Mail Delivery Subsystem")); - let from_addr = core - .core + let from_addr = server .eval_if(&config.dsn.address, self, self.span_id) .await .unwrap_or_else(|| String::from("MAILER-DAEMON@localhost")); - let reporting_mta = core - .core - .eval_if(&core.core.smtp.report.submitter, self, self.span_id) + let reporting_mta = server + .eval_if(&server.core.smtp.report.submitter, self, self.span_id) .await .unwrap_or_else(|| String::from("localhost")); @@ -359,10 +363,8 @@ impl Message { let dsn = dsn_header + dsn.as_str(); // Fetch up to 1024 bytes of message headers - let headers = match core - .core - .storage - .blob + let headers = match server + .blob_store() .get_blob(self.blob_hash.as_slice(), 0..1024) .await { diff --git a/crates/smtp/src/queue/manager.rs b/crates/smtp/src/queue/manager.rs index aa2ae6ad..6f16d7d9 100644 --- a/crates/smtp/src/queue/manager.rs +++ b/crates/smtp/src/queue/manager.rs @@ -4,33 +4,39 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{sync::atomic::Ordering, time::Duration}; - +use std::{ + sync::{atomic::Ordering, Arc}, + time::Duration, +}; + +use common::{ + core::BuildServer, + ipc::{OnHold, QueueEvent, QueueEventLock}, + Inner, +}; use store::write::now; use tokio::sync::mpsc; -use crate::core::{SmtpInstance, SMTP}; - -use super::{spool::QueueEventLock, DeliveryAttempt, Event, Message, OnHold, Status}; +use super::{spool::SmtpSpool, DeliveryAttempt, Message, Status}; pub(crate) const SHORT_WAIT: Duration = Duration::from_millis(1); pub(crate) const LONG_WAIT: Duration = Duration::from_secs(86400 * 365); pub struct Queue { - pub core: SmtpInstance, + pub core: Arc<Inner>, pub on_hold: Vec<OnHold<QueueEventLock>>, pub next_wake_up: Duration, } -impl SpawnQueue for mpsc::Receiver<Event> { - fn spawn(mut self, core: SmtpInstance) { +impl SpawnQueue for mpsc::Receiver<QueueEvent> { + fn spawn(mut self, core: Arc<Inner>) { tokio::spawn(async move { let mut queue = Queue::new(core); loop { let on_hold = match tokio::time::timeout(queue.next_wake_up, self.recv()).await { - Ok(Some(Event::OnHold(on_hold))) => on_hold.into(), - Ok(Some(Event::Stop)) | Ok(None) => { + Ok(Some(QueueEvent::OnHold(on_hold))) => on_hold.into(), + Ok(Some(QueueEvent::Stop)) | Ok(None) => { break; } _ => None, @@ -48,7 +54,7 @@ impl SpawnQueue for mpsc::Receiver<Event> { } impl Queue { - pub fn new(core: SmtpInstance) -> Self { + pub fn new(core: Arc<Inner>) -> Self { Queue { core, on_hold: Vec::with_capacity(128), @@ -58,20 +64,20 @@ impl Queue { pub async fn process_events(&mut self) { // Deliver any concurrency limited messages - let core = SMTP::from(self.core.clone()); + let server = self.core.build_server(); while let Some(queue_event) = self.next_on_hold() { DeliveryAttempt::new(queue_event) - .try_deliver(core.clone()) + .try_deliver(server.clone()) .await; } // Deliver scheduled messages let now = now(); self.next_wake_up = LONG_WAIT; - for queue_event in core.next_event().await { + for queue_event in server.next_event().await { if queue_event.due <= now { DeliveryAttempt::new(queue_event) - .try_deliver(core.clone()) + .try_deliver(server.clone()) .await; } else { self.next_wake_up = Duration::from_secs(queue_event.due - now); @@ -217,5 +223,5 @@ impl Message { } pub trait SpawnQueue { - fn spawn(self, core: SmtpInstance); + fn spawn(self, core: Arc<Inner>); } diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs index e1e98772..5849054c 100644 --- a/crates/smtp/src/queue/mod.rs +++ b/crates/smtp/src/queue/mod.rs @@ -12,15 +12,14 @@ use std::{ use common::{ expr::{self, functions::ResolveVariable, *}, - listener::limiter::{ConcurrencyLimiter, InFlight}, + ipc::QueueEventLock, + listener::limiter::InFlight, }; use serde::{Deserialize, Serialize}; use smtp_proto::Response; use store::write::now; use utils::BlobHash; -use self::spool::QueueEventLock; - pub mod dsn; pub mod manager; pub mod quota; @@ -29,20 +28,6 @@ pub mod throttle; pub type QueueId = u64; -#[derive(Debug)] -pub enum Event { - Reload, - OnHold(OnHold<QueueEventLock>), - Stop, -} - -#[derive(Debug)] -pub struct OnHold<T> { - pub next_due: Option<u64>, - pub limiters: Vec<ConcurrencyLimiter>, - pub message: T, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Schedule<T> { pub due: u64, diff --git a/crates/smtp/src/queue/quota.rs b/crates/smtp/src/queue/quota.rs index e7323b63..73f65380 100644 --- a/crates/smtp/src/queue/quota.rs +++ b/crates/smtp/src/queue/quota.rs @@ -4,19 +4,34 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{config::smtp::queue::QueueQuota, expr::functions::ResolveVariable}; +use std::future::Future; + +use common::{config::smtp::queue::QueueQuota, expr::functions::ResolveVariable, Server}; use store::{ write::{BatchBuilder, QueueClass, ValueClass}, ValueKey, }; use trc::QueueEvent; -use crate::core::{throttle::NewKey, SMTP}; +use crate::core::throttle::NewKey; use super::{Message, QueueEnvelope, QuotaKey, Status}; -impl SMTP { - pub async fn has_quota(&self, message: &mut Message) -> bool { +pub trait HasQueueQuota: Sync + Send { + fn has_quota(&self, message: &mut Message) -> impl Future<Output = bool> + Send; + fn check_quota<'x>( + &'x self, + quota: &'x QueueQuota, + envelope: &impl ResolveVariable, + size: usize, + id: u64, + refs: &mut Vec<QuotaKey>, + session_id: u64, + ) -> impl Future<Output = bool> + Send; +} + +impl HasQueueQuota for Server { + async fn has_quota(&self, message: &mut Message) -> bool { let mut quota_keys = Vec::new(); if !self.core.smtp.queue.quota.sender.is_empty() { @@ -110,7 +125,6 @@ impl SMTP { ) -> bool { if !quota.expr.is_empty() && self - .core .eval_expr("a.expr, envelope, "check_quota", session_id) .await .unwrap_or(false) diff --git a/crates/smtp/src/queue/spool.rs b/crates/smtp/src/queue/spool.rs index 54aecc7e..5c33cc12 100644 --- a/crates/smtp/src/queue/spool.rs +++ b/crates/smtp/src/queue/spool.rs @@ -5,32 +5,44 @@ */ use crate::queue::DomainPart; +use common::ipc::{QueueEvent, QueueEventLock}; +use common::Server; use std::borrow::Cow; +use std::future::Future; use std::time::{Duration, SystemTime}; use store::write::key::DeserializeBigEndian; -use store::write::{now, BatchBuilder, Bincode, BlobOp, QueueClass, QueueEvent, ValueClass}; +use store::write::{now, BatchBuilder, Bincode, BlobOp, QueueClass, ValueClass}; use store::{Deserialize, IterateParams, Serialize, ValueKey, U64_LEN}; use trc::ServerEvent; use utils::BlobHash; -use crate::core::SMTP; - use super::{ - Domain, Event, Message, MessageSource, QueueEnvelope, QueueId, QuotaKey, Recipient, Schedule, - Status, + Domain, Message, MessageSource, QueueEnvelope, QueueId, QuotaKey, Recipient, Schedule, Status, }; pub const LOCK_EXPIRY: u64 = 300; -#[derive(Debug)] -pub struct QueueEventLock { - pub due: u64, - pub queue_id: u64, - pub lock_expiry: u64, +pub trait SmtpSpool: Sync + Send { + fn new_message( + &self, + return_path: impl Into<String>, + return_path_lcase: impl Into<String>, + return_path_domain: impl Into<String>, + span_id: u64, + ) -> Message; + + fn next_event(&self) -> impl Future<Output = Vec<QueueEventLock>> + Send; + + fn try_lock_event( + &self, + event: QueueEventLock, + ) -> impl Future<Output = Option<QueueEventLock>> + Send; + + fn read_message(&self, id: QueueId) -> impl Future<Output = Option<Message>> + Send; } -impl SMTP { - pub fn new_message( +impl SmtpSpool for Server { + fn new_message( &self, return_path: impl Into<String>, return_path_lcase: impl Into<String>, @@ -41,7 +53,7 @@ impl SMTP { .duration_since(SystemTime::UNIX_EPOCH) .map_or(0, |d| d.as_secs()); Message { - queue_id: self.inner.queue_id_gen.generate().unwrap_or(created), + queue_id: self.inner.data.queue_id_gen.generate().unwrap_or(created), span_id, created, return_path: return_path.into(), @@ -58,22 +70,24 @@ impl SMTP { } } - pub async fn next_event(&self) -> Vec<QueueEventLock> { - let from_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { - due: 0, - queue_id: 0, - }))); - let to_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { - due: u64::MAX, - queue_id: u64::MAX, - }))); + async fn next_event(&self) -> Vec<QueueEventLock> { + let from_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent( + store::write::QueueEvent { + due: 0, + queue_id: 0, + }, + ))); + let to_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent( + store::write::QueueEvent { + due: u64::MAX, + queue_id: u64::MAX, + }, + ))); let mut events = Vec::new(); let now = now(); let result = self - .core - .storage - .data + .store() .iterate( IterateParams::new(from_key, to_key).ascending(), |key, value| { @@ -107,10 +121,10 @@ impl SMTP { events } - pub async fn try_lock_event(&self, mut event: QueueEventLock) -> Option<QueueEventLock> { + async fn try_lock_event(&self, mut event: QueueEventLock) -> Option<QueueEventLock> { let mut batch = BatchBuilder::new(); batch.assert_value( - ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { + ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent { due: event.due, queue_id: event.queue_id, })), @@ -118,13 +132,13 @@ impl SMTP { ); event.lock_expiry = now() + LOCK_EXPIRY; batch.set( - ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { + ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent { due: event.due, queue_id: event.queue_id, })), event.lock_expiry.serialize(), ); - match self.core.storage.data.write(batch.build()).await { + match self.store().write(batch.build()).await { Ok(_) => Some(event), Err(err) if err.is_assertion_failure() => { trc::event!( @@ -145,11 +159,9 @@ impl SMTP { } } - pub async fn read_message(&self, id: QueueId) -> Option<Message> { + async fn read_message(&self, id: QueueId) -> Option<Message> { match self - .core - .storage - .data + .store() .get_value::<Bincode<Message>>(ValueKey::from(ValueClass::Queue(QueueClass::Message( id, )))) @@ -174,7 +186,7 @@ impl Message { raw_headers: Option<&[u8]>, raw_message: &[u8], session_id: u64, - core: &SMTP, + server: &Server, source: MessageSource, ) -> bool { // Write blob @@ -203,7 +215,7 @@ impl Message { }, 0u32.serialize(), ); - if let Err(err) = core.core.storage.data.write(batch.build()).await { + if let Err(err) = server.store().write(batch.build()).await { trc::error!(err .details("Failed to write to store.") .span_id(session_id) @@ -211,10 +223,8 @@ impl Message { return false; } - if let Err(err) = core - .core - .storage - .blob + if let Err(err) = server + .blob_store() .put_blob(self.blob_hash.as_slice(), message.as_ref()) .await { @@ -271,7 +281,7 @@ impl Message { } batch .set( - ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { + ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent { due: self.next_event().unwrap_or_default(), queue_id: self.queue_id, })), @@ -299,7 +309,7 @@ impl Message { Bincode::new(self).serialize(), ); - if let Err(err) = core.core.storage.data.write(batch.build()).await { + if let Err(err) = server.store().write(batch.build()).await { trc::error!(err .details("Failed to write to store.") .span_id(session_id) @@ -309,7 +319,14 @@ impl Message { } // Queue the message - if core.inner.queue_tx.send(Event::Reload).await.is_err() { + if server + .inner + .ipc + .queue_tx + .send(QueueEvent::Reload) + .await + .is_err() + { trc::event!( Server(ServerEvent::ThreadError), Reason = "Channel closed.", @@ -326,7 +343,7 @@ impl Message { rcpt: impl Into<String>, rcpt_lcase: impl Into<String>, rcpt_domain: impl Into<String>, - core: &SMTP, + server: &Server, ) { let rcpt_domain = rcpt_domain.into(); let domain_idx = @@ -343,10 +360,9 @@ impl Message { status: Status::Scheduled, }); - let expires = core - .core + let expires = server .eval_if( - &core.core.smtp.queue.expire, + &server.core.smtp.queue.expire, &QueueEnvelope::new(self, idx), self.span_id, ) @@ -370,17 +386,17 @@ impl Message { }); } - pub async fn add_recipient(&mut self, rcpt: impl Into<String>, core: &SMTP) { + pub async fn add_recipient(&mut self, rcpt: impl Into<String>, server: &Server) { let rcpt = rcpt.into(); let rcpt_lcase = rcpt.to_lowercase(); let rcpt_domain = rcpt_lcase.domain_part().to_string(); - self.add_recipient_parts(rcpt, rcpt_lcase, rcpt_domain, core) + self.add_recipient_parts(rcpt, rcpt_lcase, rcpt_domain, server) .await; } pub async fn save_changes( mut self, - core: &SMTP, + server: &Server, prev_event: Option<u64>, next_event: Option<u64>, ) -> bool { @@ -395,12 +411,14 @@ impl Message { let mut batch = BatchBuilder::new(); if let (Some(prev_event), Some(next_event)) = (prev_event, next_event) { batch - .clear(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { - due: prev_event, - queue_id: self.queue_id, - }))) + .clear(ValueClass::Queue(QueueClass::MessageEvent( + store::write::QueueEvent { + due: prev_event, + queue_id: self.queue_id, + }, + ))) .set( - ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { + ValueClass::Queue(QueueClass::MessageEvent(store::write::QueueEvent { due: next_event, queue_id: self.queue_id, })), @@ -414,7 +432,7 @@ impl Message { Bincode::new(self).serialize(), ); - if let Err(err) = core.core.storage.data.write(batch.build()).await { + if let Err(err) = server.store().write(batch.build()).await { trc::error!(err .details("Failed to save changes.") .span_id(span_id) @@ -425,7 +443,7 @@ impl Message { } } - pub async fn remove(self, core: &SMTP, prev_event: u64) -> bool { + pub async fn remove(self, server: &Server, prev_event: u64) -> bool { let mut batch = BatchBuilder::new(); // Release all quotas @@ -448,13 +466,15 @@ impl Message { hash: self.blob_hash.clone(), id: self.queue_id, }) - .clear(ValueClass::Queue(QueueClass::MessageEvent(QueueEvent { - due: prev_event, - queue_id: self.queue_id, - }))) + .clear(ValueClass::Queue(QueueClass::MessageEvent( + store::write::QueueEvent { + due: prev_event, + queue_id: self.queue_id, + }, + ))) .clear(ValueClass::Queue(QueueClass::Message(self.queue_id))); - if let Err(err) = core.core.storage.data.write(batch.build()).await { + if let Err(err) = server.store().write(batch.build()).await { trc::error!(err .details("Failed to write to update queue.") .span_id(self.span_id) diff --git a/crates/smtp/src/queue/throttle.rs b/crates/smtp/src/queue/throttle.rs index a9de94bc..e80c3a44 100644 --- a/crates/smtp/src/queue/throttle.rs +++ b/crates/smtp/src/queue/throttle.rs @@ -4,15 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use std::future::Future; + use common::{ config::smtp::Throttle, expr::functions::ResolveVariable, listener::limiter::{ConcurrencyLimiter, InFlight}, + Server, }; use dashmap::mapref::entry::Entry; use store::write::now; -use crate::core::{throttle::NewKey, SMTP}; +use crate::core::throttle::NewKey; use super::{Domain, Status}; @@ -22,8 +25,18 @@ pub enum Error { Rate { retry_at: u64 }, } -impl SMTP { - pub async fn is_allowed<'x>( +pub trait IsAllowed: Sync + Send { + fn is_allowed<'x>( + &'x self, + throttle: &'x Throttle, + envelope: &impl ResolveVariable, + in_flight: &mut Vec<InFlight>, + session_id: u64, + ) -> impl Future<Output = Result<(), Error>> + Send; +} + +impl IsAllowed for Server { + async fn is_allowed<'x>( &'x self, throttle: &'x Throttle, envelope: &impl ResolveVariable, @@ -32,7 +45,6 @@ impl SMTP { ) -> Result<(), Error> { if throttle.expr.is_empty() || self - .core .eval_expr(&throttle.expr, envelope, "throttle", session_id) .await .unwrap_or(false) @@ -64,7 +76,7 @@ impl SMTP { } if let Some(concurrency) = &throttle.concurrency { - match self.inner.queue_throttle.entry(key) { + match self.inner.data.smtp_queue_throttle.entry(key) { Entry::Occupied(mut e) => { let limiter = e.get_mut(); if let Some(inflight) = limiter.is_allowed() { diff --git a/crates/smtp/src/reporting/analysis.rs b/crates/smtp/src/reporting/analysis.rs index 99ded690..ee6b4230 100644 --- a/crates/smtp/src/reporting/analysis.rs +++ b/crates/smtp/src/reporting/analysis.rs @@ -12,6 +12,7 @@ use std::{ }; use ahash::AHashMap; +use common::Server; use mail_auth::{ flate2::read::GzDecoder, report::{tlsrpt::TlsReport, ActionDisposition, DmarcResult, Feedback, Report}, @@ -25,8 +26,6 @@ use store::{ }; use trc::IncomingReportEvent; -use crate::core::SMTP; - enum Compression { None, Gzip, @@ -53,8 +52,12 @@ pub struct IncomingReport<T> { pub report: T, } -impl SMTP { - pub fn analyze_report(&self, message: Arc<Vec<u8>>, session_id: u64) { +pub trait AnalyzeReport: Sync + Send { + fn analyze_report(&self, message: Arc<Vec<u8>>, session_id: u64); +} + +impl AnalyzeReport for Server { + fn analyze_report(&self, message: Arc<Vec<u8>>, session_id: u64) { let core = self.clone(); tokio::spawn(async move { let message = if let Some(message) = MessageParser::default().parse(message.as_ref()) { @@ -282,7 +285,7 @@ impl SMTP { // Store report if let Some(expires_in) = &core.core.smtp.report.analysis.store { let expires = now() + expires_in.as_secs(); - let id = core.inner.queue_id_gen.generate().unwrap_or(expires); + let id = core.inner.data.queue_id_gen.generate().unwrap_or(expires); let mut batch = BatchBuilder::new(); match report { diff --git a/crates/smtp/src/reporting/dkim.rs b/crates/smtp/src/reporting/dkim.rs index 9c3e4b4e..f0b4d682 100644 --- a/crates/smtp/src/reporting/dkim.rs +++ b/crates/smtp/src/reporting/dkim.rs @@ -11,7 +11,7 @@ use mail_auth::{ use trc::OutgoingReportEvent; use utils::config::Rate; -use crate::core::Session; +use crate::{core::Session, reporting::SmtpReporting}; impl<T: SessionStream> Session<T> { pub async fn send_dkim_report( @@ -44,10 +44,9 @@ impl<T: SessionStream> Session<T> { return; } - let config = &self.core.core.smtp.report.dkim; + let config = &self.server.core.smtp.report.dkim; let from_addr = self - .core - .core + .server .eval_if(&config.address, self, self.data.session_id) .await .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); @@ -64,8 +63,7 @@ impl<T: SessionStream> Session<T> { .with_headers(std::str::from_utf8(message.raw_headers()).unwrap_or_default()) .write_rfc5322( ( - self.core - .core + self.server .eval_if(&config.name, self, self.data.session_id) .await .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) @@ -74,8 +72,7 @@ impl<T: SessionStream> Session<T> { ), rcpt, &self - .core - .core + .server .eval_if(&config.subject, self, self.data.session_id) .await .unwrap_or_else(|| "DKIM Report".to_string()), @@ -91,7 +88,7 @@ impl<T: SessionStream> Session<T> { ); // Send report - self.core + self.server .send_report( &from_addr, [rcpt].into_iter(), diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs index 42ce6b38..5b106fea 100644 --- a/crates/smtp/src/reporting/dmarc.rs +++ b/crates/smtp/src/reporting/dmarc.rs @@ -4,10 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::collections::hash_map::Entry; +use std::{collections::hash_map::Entry, future::Future}; use ahash::AHashMap; -use common::{config::smtp::report::AggregateFrequency, listener::SessionStream}; +use common::{ + config::smtp::report::AggregateFrequency, + ipc::{DmarcEvent, ToHash}, + listener::SessionStream, + Server, +}; use mail_auth::{ common::verify::VerifySignature, dmarc::{self, URI}, @@ -23,11 +28,12 @@ use trc::OutgoingReportEvent; use utils::config::Rate; use crate::{ - core::{Session, SMTP}, + core::Session, queue::{DomainPart, RecipientDomain}, + reporting::SmtpReporting, }; -use super::{scheduler::ToHash, AggregateTimestamp, DmarcEvent, ReportLock, SerializedSize}; +use super::{AggregateTimestamp, ReportLock, SerializedSize}; #[derive(Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct DmarcFormat { @@ -48,19 +54,18 @@ impl<T: SessionStream> Session<T> { arc_output: &Option<ArcOutput<'_>>, ) { let dmarc_record = dmarc_output.dmarc_record_cloned().unwrap(); - let config = &self.core.core.smtp.report.dmarc; + let config = &self.server.core.smtp.report.dmarc; // Send failure report if let (Some(failure_rate), Some(report_options)) = ( - self.core - .core + self.server .eval_if::<Rate, _>(&config.send, self, self.data.session_id) .await, dmarc_output.failure_report(), ) { // Verify that any external reporting addresses are authorized let rcpts = match self - .core + .server .core .smtp .resolvers @@ -113,8 +118,7 @@ impl<T: SessionStream> Session<T> { if !rcpts.is_empty() { let mut report = Vec::with_capacity(128); let from_addr = self - .core - .core + .server .eval_if(&config.address, self, self.data.session_id) .await .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); @@ -197,8 +201,7 @@ impl<T: SessionStream> Session<T> { }) .write_rfc5322( ( - self.core - .core + self.server .eval_if(&config.name, self, self.data.session_id) .await .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) @@ -207,8 +210,7 @@ impl<T: SessionStream> Session<T> { ), &rcpts.join(", "), &self - .core - .core + .server .eval_if(&config.subject, self, self.data.session_id) .await .unwrap_or_else(|| "DMARC Report".to_string()), @@ -227,7 +229,7 @@ impl<T: SessionStream> Session<T> { ); // Send report - self.core + self.server .send_report( &from_addr, rcpts.into_iter(), @@ -251,10 +253,9 @@ impl<T: SessionStream> Session<T> { // Send aggregate reports let interval = self - .core - .core + .server .eval_if( - &self.core.core.smtp.report.dmarc_aggregate.send, + &self.server.core.smtp.report.dmarc_aggregate.send, self, self.data.session_id, ) @@ -289,7 +290,7 @@ impl<T: SessionStream> Session<T> { } // Submit DMARC report event - self.core + self.server .schedule_report(DmarcEvent { domain: dmarc_output.into_domain(), report_record, @@ -300,9 +301,22 @@ impl<T: SessionStream> Session<T> { } } -impl SMTP { - pub async fn send_dmarc_aggregate_report(&self, event: ReportEvent) { - let span_id = self.inner.span_id_gen.generate().unwrap_or_else(now); +pub trait DmarcReporting: Sync + Send { + fn send_dmarc_aggregate_report(&self, event: ReportEvent) -> impl Future<Output = ()> + Send; + fn generate_dmarc_aggregate_report( + &self, + event: &ReportEvent, + rua: &mut Vec<URI>, + serialized_size: Option<&mut serde_json::Serializer<SerializedSize>>, + span_id: u64, + ) -> impl Future<Output = trc::Result<Option<Report>>> + Send; + fn delete_dmarc_report(&self, event: ReportEvent) -> impl Future<Output = ()> + Send; + fn schedule_dmarc(&self, event: Box<DmarcEvent>) -> impl Future<Output = ()> + Send; +} + +impl DmarcReporting for Server { + async fn send_dmarc_aggregate_report(&self, event: ReportEvent) { + let span_id = self.inner.data.span_id_gen.generate().unwrap_or_else(now); trc::event!( OutgoingReport(OutgoingReportEvent::DmarcAggregateReport), @@ -315,14 +329,13 @@ impl SMTP { // Generate report let mut serialized_size = serde_json::Serializer::new(SerializedSize::new( - self.core - .eval_if( - &self.core.smtp.report.dmarc_aggregate.max_size, - &RecipientDomain::new(event.domain.as_str()), - span_id, - ) - .await - .unwrap_or(25 * 1024 * 1024), + self.eval_if( + &self.core.smtp.report.dmarc_aggregate.max_size, + &RecipientDomain::new(event.domain.as_str()), + span_id, + ) + .await + .unwrap_or(25 * 1024 * 1024), )); let mut rua = Vec::new(); let report = match self @@ -392,7 +405,6 @@ impl SMTP { // Serialize report let config = &self.core.smtp.report.dmarc_aggregate; let from_addr = self - .core .eval_if( &config.address, &RecipientDomain::new(event.domain.as_str()), @@ -403,7 +415,6 @@ impl SMTP { let mut message = Vec::with_capacity(2048); let _ = report.write_rfc5322( &self - .core .eval_if( &self.core.smtp.report.submitter, &RecipientDomain::new(event.domain.as_str()), @@ -412,15 +423,14 @@ impl SMTP { .await .unwrap_or_else(|| "localhost".to_string()), ( - self.core - .eval_if( - &config.name, - &RecipientDomain::new(event.domain.as_str()), - span_id, - ) - .await - .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) - .as_str(), + self.eval_if( + &config.name, + &RecipientDomain::new(event.domain.as_str()), + span_id, + ) + .await + .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) + .as_str(), from_addr.as_str(), ), rua.iter().map(|a| a.as_str()), @@ -441,7 +451,7 @@ impl SMTP { self.delete_dmarc_report(event).await; } - pub async fn generate_dmarc_aggregate_report( + async fn generate_dmarc_aggregate_report( &self, event: &ReportEvent, rua: &mut Vec<URI>, @@ -473,17 +483,15 @@ impl SMTP { .with_date_range_end(event.due) .with_report_id(format!("{}_{}", event.policy_hash, event.seq_id)) .with_email( - self.core - .eval_if( - &config.address, - &RecipientDomain::new(event.domain.as_str()), - span_id, - ) - .await - .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()), + self.eval_if( + &config.address, + &RecipientDomain::new(event.domain.as_str()), + span_id, + ) + .await + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()), ); if let Some(org_name) = self - .core .eval_if::<String, _>( &config.org_name, &RecipientDomain::new(event.domain.as_str()), @@ -494,7 +502,6 @@ impl SMTP { report = report.with_org_name(org_name); } if let Some(contact_info) = self - .core .eval_if::<String, _>( &config.contact_info, &RecipientDomain::new(event.domain.as_str()), @@ -561,7 +568,7 @@ impl SMTP { Ok(Some(report)) } - pub async fn delete_dmarc_report(&self, event: ReportEvent) { + async fn delete_dmarc_report(&self, event: ReportEvent) { let from_key = ReportEvent { due: event.due, policy_hash: event.policy_hash, @@ -600,7 +607,7 @@ impl SMTP { } } - pub async fn schedule_dmarc(&self, event: Box<DmarcEvent>) { + async fn schedule_dmarc(&self, event: Box<DmarcEvent>) { let created = event.interval.to_timestamp(); let deliver_at = created + event.interval.as_secs(); let mut report_event = ReportEvent { @@ -647,7 +654,7 @@ impl SMTP { } // Write entry - report_event.seq_id = self.inner.queue_id_gen.generate().unwrap_or_else(now); + report_event.seq_id = self.inner.data.queue_id_gen.generate().unwrap_or_else(now); builder.set( ValueClass::Queue(QueueClass::DmarcReportEvent(report_event)), Bincode::new(event.report_record).serialize(), diff --git a/crates/smtp/src/reporting/mod.rs b/crates/smtp/src/reporting/mod.rs index fedd6689..92c81777 100644 --- a/crates/smtp/src/reporting/mod.rs +++ b/crates/smtp/src/reporting/mod.rs @@ -4,23 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{io, sync::Arc, time::SystemTime}; +use std::{future::Future, io, time::SystemTime}; use common::{ - config::smtp::{ - report::{AddressMatch, AggregateFrequency}, - resolver::{Policy, Tlsa}, - }, + config::smtp::report::{AddressMatch, AggregateFrequency}, expr::if_block::IfBlock, - USER_AGENT, + ipc::ReportingEvent, + Server, USER_AGENT, }; use mail_auth::{ common::headers::HeaderWriter, - dmarc::Dmarc, - mta_sts::TlsRpt, - report::{ - tlsrpt::FailureDetails, AuthFailureType, DeliveryResult, Feedback, FeedbackType, Record, - }, + report::{AuthFailureType, DeliveryResult, Feedback, FeedbackType}, }; use mail_parser::DateTime; @@ -28,9 +22,9 @@ use store::write::{QueueClass, ReportEvent}; use tokio::io::{AsyncRead, AsyncWrite}; use crate::{ - core::{Session, SMTP}, + core::Session, inbound::DkimSign, - queue::{DomainPart, Message, MessageSource}, + queue::{spool::SmtpSpool, DomainPart, Message, MessageSource}, }; pub mod analysis; @@ -40,37 +34,6 @@ pub mod scheduler; pub mod spf; pub mod tls; -#[derive(Debug)] -pub enum Event { - Dmarc(Box<DmarcEvent>), - Tls(Box<TlsEvent>), - Stop, -} - -#[derive(Debug)] -pub struct DmarcEvent { - pub domain: String, - pub report_record: Record, - pub dmarc_record: Arc<Dmarc>, - pub interval: AggregateFrequency, -} - -#[derive(Debug)] -pub struct TlsEvent { - pub domain: String, - pub policy: PolicyType, - pub failure: Option<FailureDetails>, - pub tls_record: Arc<TlsRpt>, - pub interval: AggregateFrequency, -} - -#[derive(Debug, Hash, PartialEq, Eq)] -pub enum PolicyType { - Tlsa(Option<Arc<Tlsa>>), - Sts(Option<Arc<Policy>>), - None, -} - impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> { pub fn new_auth_failure(&self, ft: AuthFailureType, rejected: bool) -> Feedback<'_> { Feedback::new(FeedbackType::AuthFailure) @@ -91,7 +54,7 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> { } pub fn is_report(&self) -> bool { - for addr_match in &self.core.core.smtp.report.analysis.addresses { + for addr_match in &self.server.core.smtp.report.analysis.addresses { for addr in &self.data.rcpt_to { match addr_match { AddressMatch::StartsWith(prefix) if addr.address_lcase.starts_with(prefix) => { @@ -110,11 +73,44 @@ impl<T: AsyncWrite + AsyncRead + Unpin> Session<T> { } } -impl SMTP { - pub async fn send_report( +pub trait SmtpReporting: Sync + Send { + fn send_report( + &self, + from_addr: &str, + rcpts: impl Iterator<Item = impl AsRef<str> + Sync + Send> + Sync + Send, + report: Vec<u8>, + sign_config: &IfBlock, + deliver_now: bool, + parent_session_id: u64, + ) -> impl Future<Output = ()> + Send; + + fn send_autogenerated( + &self, + from_addr: impl Into<String> + Sync + Send, + rcpts: impl Iterator<Item = impl Into<String> + Sync + Send> + Sync + Send, + raw_message: Vec<u8>, + sign_config: Option<&IfBlock>, + parent_session_id: u64, + ) -> impl Future<Output = ()> + Send; + + fn schedule_report( + &self, + report: impl Into<ReportingEvent> + Sync + Send, + ) -> impl Future<Output = ()> + Send; + + fn sign_message( + &self, + message: &mut Message, + config: &IfBlock, + bytes: &[u8], + ) -> impl Future<Output = Option<Vec<u8>>> + Send; +} + +impl SmtpReporting for Server { + async fn send_report( &self, from_addr: &str, - rcpts: impl Iterator<Item = impl AsRef<str>>, + rcpts: impl Iterator<Item = impl AsRef<str> + Sync + Send> + Sync + Send, report: Vec<u8>, sign_config: &IfBlock, deliver_now: bool, @@ -163,10 +159,10 @@ impl SMTP { .await; } - pub async fn send_autogenerated( + async fn send_autogenerated( &self, - from_addr: impl Into<String>, - rcpts: impl Iterator<Item = impl Into<String>>, + from_addr: impl Into<String> + Sync + Send, + rcpts: impl Iterator<Item = impl Into<String> + Sync + Send> + Sync + Send, raw_message: Vec<u8>, sign_config: Option<&IfBlock>, parent_session_id: u64, @@ -205,8 +201,8 @@ impl SMTP { .await; } - pub async fn schedule_report(&self, report: impl Into<Event>) { - if self.inner.report_tx.send(report.into()).await.is_err() { + async fn schedule_report(&self, report: impl Into<ReportingEvent> + Sync + Send) { + if self.inner.ipc.report_tx.send(report.into()).await.is_err() { trc::event!( Server(trc::ServerEvent::ThreadError), CausedBy = trc::location!(), @@ -215,21 +211,20 @@ impl SMTP { } } - pub async fn sign_message( + async fn sign_message( &self, message: &mut Message, config: &IfBlock, bytes: &[u8], ) -> Option<Vec<u8>> { let signers = self - .core .eval_if::<Vec<String>, _>(config, message, message.span_id) .await .unwrap_or_default(); if !signers.is_empty() { let mut headers = Vec::with_capacity(64); for signer in signers.iter() { - if let Some(signer) = self.core.get_dkim_signer(signer, message.span_id) { + if let Some(signer) = self.get_dkim_signer(signer, message.span_id) { match signer.sign(bytes) { Ok(signature) => { signature.write_header(&mut headers); @@ -300,52 +295,6 @@ impl AggregateTimestamp for AggregateFrequency { } } -impl From<DmarcEvent> for Event { - fn from(value: DmarcEvent) -> Self { - Event::Dmarc(Box::new(value)) - } -} - -impl From<TlsEvent> for Event { - fn from(value: TlsEvent) -> Self { - Event::Tls(Box::new(value)) - } -} - -impl From<Arc<Tlsa>> for PolicyType { - fn from(value: Arc<Tlsa>) -> Self { - PolicyType::Tlsa(Some(value)) - } -} - -impl From<Arc<Policy>> for PolicyType { - fn from(value: Arc<Policy>) -> Self { - PolicyType::Sts(Some(value)) - } -} - -impl From<&Arc<Tlsa>> for PolicyType { - fn from(value: &Arc<Tlsa>) -> Self { - PolicyType::Tlsa(Some(value.clone())) - } -} - -impl From<&Arc<Policy>> for PolicyType { - fn from(value: &Arc<Policy>) -> Self { - PolicyType::Sts(Some(value.clone())) - } -} - -impl From<(&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)> for PolicyType { - fn from(value: (&Option<Arc<Policy>>, &Option<Arc<Tlsa>>)) -> Self { - match value { - (Some(value), _) => PolicyType::Sts(Some(value.clone())), - (_, Some(value)) => PolicyType::Tlsa(Some(value.clone())), - _ => PolicyType::None, - } - } -} - pub struct SerializedSize { bytes_left: usize, } diff --git a/crates/smtp/src/reporting/scheduler.rs b/crates/smtp/src/reporting/scheduler.rs index 83a1c354..ba1c9fc3 100644 --- a/crates/smtp/src/reporting/scheduler.rs +++ b/crates/smtp/src/reporting/scheduler.rs @@ -4,34 +4,33 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use ahash::{AHashMap, RandomState}; -use common::Core; -use mail_auth::dmarc::Dmarc; +use ahash::AHashMap; +use common::{core::BuildServer, ipc::ReportingEvent, Inner, Server}; -use std::time::{Duration, Instant, SystemTime}; +use std::{ + future::Future, + sync::Arc, + time::{Duration, SystemTime}, +}; use store::{ write::{now, BatchBuilder, QueueClass, ReportEvent, ValueClass}, - Deserialize, IterateParams, Key, Serialize, ValueKey, + Deserialize, IterateParams, Key, Serialize, Store, ValueKey, }; use tokio::sync::mpsc; -use crate::{ - core::{SmtpInstance, SMTP}, - queue::{manager::LONG_WAIT, spool::LOCK_EXPIRY}, -}; +use crate::queue::{manager::LONG_WAIT, spool::LOCK_EXPIRY}; -use super::{Event, ReportLock}; +use super::{dmarc::DmarcReporting, tls::TlsReporting, ReportLock}; -impl SpawnReport for mpsc::Receiver<Event> { - fn spawn(mut self, core: SmtpInstance) { +impl SpawnReport for mpsc::Receiver<ReportingEvent> { + fn spawn(mut self, inner: Arc<Inner>) { tokio::spawn(async move { - let mut last_cleanup = Instant::now(); let mut next_wake_up; loop { // Read events let now = now(); - let events = next_report_event(&core.core.load_full()).await; + let events = next_report_event(inner.shared_core.load().storage.data.clone()).await; next_wake_up = events .last() .and_then(|e| match e { @@ -44,15 +43,15 @@ impl SpawnReport for mpsc::Receiver<Event> { }) .unwrap_or(LONG_WAIT); - let core = SMTP::from(core.clone()); - let core_ = core.clone(); + let server = inner.build_server(); + let server_ = server.clone(); tokio::spawn(async move { let mut tls_reports = AHashMap::new(); for report_event in events { match report_event { QueueClass::DmarcReportHeader(event) if event.due <= now => { - if core_.try_lock_report(QueueClass::dmarc_lock(&event)).await { - core_.send_dmarc_aggregate_report(event).await; + if server.try_lock_report(QueueClass::dmarc_lock(&event)).await { + server.send_dmarc_aggregate_report(event).await; } } QueueClass::TlsReportHeader(event) if event.due <= now => { @@ -66,40 +65,34 @@ impl SpawnReport for mpsc::Receiver<Event> { } for (_, tls_report) in tls_reports { - if core_ + if server .try_lock_report(QueueClass::tls_lock(tls_report.first().unwrap())) .await { - core_.send_tls_aggregate_report(tls_report).await; + server.send_tls_aggregate_report(tls_report).await; } } }); match tokio::time::timeout(next_wake_up, self.recv()).await { Ok(Some(event)) => match event { - Event::Dmarc(event) => { - core.schedule_dmarc(event).await; + ReportingEvent::Dmarc(event) => { + server_.schedule_dmarc(event).await; } - Event::Tls(event) => { - core.schedule_tls(event).await; + ReportingEvent::Tls(event) => { + server_.schedule_tls(event).await; } - Event::Stop => break, + ReportingEvent::Stop => break, }, Ok(None) => break, - Err(_) => { - // Cleanup expired throttles - if last_cleanup.elapsed().as_secs() >= 86400 { - last_cleanup = Instant::now(); - core.cleanup(); - } - } + Err(_) => {} } } }); } } -async fn next_report_event(core: &Core) -> Vec<QueueClass> { +async fn next_report_event(store: Store) -> Vec<QueueClass> { let from_key = ValueKey::from(ValueClass::Queue(QueueClass::DmarcReportHeader( ReportEvent { due: 0, @@ -119,9 +112,7 @@ async fn next_report_event(core: &Core) -> Vec<QueueClass> { let mut events = Vec::new(); let now = now(); - let result = core - .storage - .data + let result = store .iterate( IterateParams::new(from_key, to_key).ascending().no_values(), |key, _| { @@ -150,13 +141,15 @@ async fn next_report_event(core: &Core) -> Vec<QueueClass> { events } -impl SMTP { - pub async fn try_lock_report(&self, lock: QueueClass) -> bool { +pub trait LockReport: Sync + Send { + fn try_lock_report(&self, lock: QueueClass) -> impl Future<Output = bool> + Send; +} + +impl LockReport for Server { + async fn try_lock_report(&self, lock: QueueClass) -> bool { let now = now(); match self - .core - .storage - .data + .store() .get_value::<u64>(ValueKey::from(ValueClass::Queue(lock.clone()))) .await { @@ -216,22 +209,6 @@ impl SMTP { } } -pub trait ToHash { - fn to_hash(&self) -> u64; -} - -impl ToHash for Dmarc { - fn to_hash(&self) -> u64 { - RandomState::with_seeds(1, 9, 7, 9).hash_one(self) - } -} - -impl ToHash for super::PolicyType { - fn to_hash(&self) -> u64 { - RandomState::with_seeds(1, 9, 7, 9).hash_one(self) - } -} - pub trait ToTimestamp { fn to_timestamp(&self) -> u64; } @@ -246,5 +223,5 @@ impl ToTimestamp for Duration { } pub trait SpawnReport { - fn spawn(self, core: SmtpInstance); + fn spawn(self, core: Arc<Inner>); } diff --git a/crates/smtp/src/reporting/spf.rs b/crates/smtp/src/reporting/spf.rs index 830a2906..8b7c82b7 100644 --- a/crates/smtp/src/reporting/spf.rs +++ b/crates/smtp/src/reporting/spf.rs @@ -9,7 +9,7 @@ use mail_auth::{report::AuthFailureType, AuthenticationResults, SpfOutput}; use trc::OutgoingReportEvent; use utils::config::Rate; -use crate::core::Session; +use crate::{core::Session, reporting::SmtpReporting}; impl<T: SessionStream> Session<T> { pub async fn send_spf_report( @@ -35,10 +35,9 @@ impl<T: SessionStream> Session<T> { } // Generate report - let config = &self.core.core.smtp.report.spf; + let config = &self.server.core.smtp.report.spf; let from_addr = self - .core - .core + .server .eval_if(&config.address, self, self.data.session_id) .await .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); @@ -64,8 +63,7 @@ impl<T: SessionStream> Session<T> { .with_spf_dns(format!("txt : {} : v=SPF1", output.domain())) // TODO use DNS record .write_rfc5322( ( - self.core - .core + self.server .eval_if(&config.name, self, self.data.session_id) .await .unwrap_or_else(|| "Mailer Daemon".to_string()) @@ -74,8 +72,7 @@ impl<T: SessionStream> Session<T> { ), rcpt, &self - .core - .core + .server .eval_if(&config.subject, self, self.data.session_id) .await .unwrap_or_else(|| "SPF Report".to_string()), @@ -91,7 +88,7 @@ impl<T: SessionStream> Session<T> { ); // Send report - self.core + self.server .send_report( &from_addr, [rcpt].into_iter(), diff --git a/crates/smtp/src/reporting/tls.rs b/crates/smtp/src/reporting/tls.rs index 5fa82a75..0430badb 100644 --- a/crates/smtp/src/reporting/tls.rs +++ b/crates/smtp/src/reporting/tls.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{collections::hash_map::Entry, sync::Arc, time::Duration}; +use std::{collections::hash_map::Entry, future::Future, sync::Arc, time::Duration}; use ahash::AHashMap; use common::{ @@ -12,7 +12,8 @@ use common::{ report::AggregateFrequency, resolver::{Mode, MxPattern}, }, - USER_AGENT, + ipc::{TlsEvent, ToHash}, + Server, USER_AGENT, }; use mail_auth::{ flate2::{write::GzEncoder, Compression}, @@ -31,9 +32,9 @@ use store::{ }; use trc::OutgoingReportEvent; -use crate::{core::SMTP, queue::RecipientDomain}; +use crate::{queue::RecipientDomain, reporting::SmtpReporting}; -use super::{scheduler::ToHash, AggregateTimestamp, ReportLock, SerializedSize, TlsEvent}; +use super::{AggregateTimestamp, ReportLock, SerializedSize}; #[derive(Debug, Clone)] pub struct TlsRptOptions { @@ -51,14 +52,30 @@ pub struct TlsFormat { #[cfg(feature = "test_mode")] pub static TLS_HTTP_REPORT: parking_lot::Mutex<Vec<u8>> = parking_lot::Mutex::new(Vec::new()); -impl SMTP { - pub async fn send_tls_aggregate_report(&self, events: Vec<ReportEvent>) { +pub trait TlsReporting: Sync + Send { + fn send_tls_aggregate_report( + &self, + events: Vec<ReportEvent>, + ) -> impl Future<Output = ()> + Send; + fn generate_tls_aggregate_report( + &self, + events: &[ReportEvent], + rua: &mut Vec<ReportUri>, + serialized_size: Option<&mut serde_json::Serializer<SerializedSize>>, + span_id: u64, + ) -> impl Future<Output = trc::Result<Option<TlsReport>>> + Send; + fn schedule_tls(&self, event: Box<TlsEvent>) -> impl Future<Output = ()> + Send; + fn delete_tls_report(&self, events: Vec<ReportEvent>) -> impl Future<Output = ()> + Send; +} + +impl TlsReporting for Server { + async fn send_tls_aggregate_report(&self, events: Vec<ReportEvent>) { let (domain_name, event_from, event_to) = events .first() .map(|e| (e.domain.as_str(), e.seq_id, e.due)) .unwrap(); - let span_id = self.inner.span_id_gen.generate().unwrap_or_else(now); + let span_id = self.inner.data.span_id_gen.generate().unwrap_or_else(now); trc::event!( OutgoingReport(OutgoingReportEvent::TlsAggregate), @@ -72,14 +89,13 @@ impl SMTP { // Generate report let mut rua = Vec::new(); let mut serialized_size = serde_json::Serializer::new(SerializedSize::new( - self.core - .eval_if( - &self.core.smtp.report.tls.max_size, - &RecipientDomain::new(domain_name), - span_id, - ) - .await - .unwrap_or(25 * 1024 * 1024), + self.eval_if( + &self.core.smtp.report.tls.max_size, + &RecipientDomain::new(domain_name), + span_id, + ) + .await + .unwrap_or(25 * 1024 * 1024), )); let report = match self .generate_tls_aggregate_report(&events, &mut rua, Some(&mut serialized_size), span_id) @@ -191,7 +207,6 @@ impl SMTP { if !rcpts.is_empty() { let config = &self.core.smtp.report.tls; let from_addr = self - .core .eval_if(&config.address, &RecipientDomain::new(domain_name), span_id) .await .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); @@ -199,7 +214,6 @@ impl SMTP { let _ = report.write_rfc5322_from_bytes( domain_name, &self - .core .eval_if( &self.core.smtp.report.submitter, &RecipientDomain::new(domain_name), @@ -208,8 +222,7 @@ impl SMTP { .await .unwrap_or_else(|| "localhost".to_string()), ( - self.core - .eval_if(&config.name, &RecipientDomain::new(domain_name), span_id) + self.eval_if(&config.name, &RecipientDomain::new(domain_name), span_id) .await .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) .as_str(), @@ -239,7 +252,7 @@ impl SMTP { self.delete_tls_report(events).await; } - pub async fn generate_tls_aggregate_report( + async fn generate_tls_aggregate_report( &self, events: &[ReportEvent], rua: &mut Vec<ReportUri>, @@ -253,7 +266,6 @@ impl SMTP { let config = &self.core.smtp.report.tls; let mut report = TlsReport { organization_name: self - .core .eval_if( &config.org_name, &RecipientDomain::new(domain_name), @@ -266,7 +278,6 @@ impl SMTP { end_datetime: DateTime::from_timestamp(event_to as i64), }, contact_info: self - .core .eval_if( &config.contact_info, &RecipientDomain::new(domain_name), @@ -388,7 +399,7 @@ impl SMTP { }) } - pub async fn schedule_tls(&self, event: Box<TlsEvent>) { + async fn schedule_tls(&self, event: Box<TlsEvent>) { let created = event.interval.to_timestamp(); let deliver_at = created + event.interval.as_secs(); let mut report_event = ReportEvent { @@ -420,7 +431,7 @@ impl SMTP { }; match event.policy { - super::PolicyType::Tlsa(tlsa) => { + common::ipc::PolicyType::Tlsa(tlsa) => { policy.policy_type = PolicyType::Tlsa; if let Some(tlsa) = tlsa { for entry in &tlsa.entries { @@ -440,7 +451,7 @@ impl SMTP { } } } - super::PolicyType::Sts(sts) => { + common::ipc::PolicyType::Sts(sts) => { policy.policy_type = PolicyType::Sts; if let Some(sts) = sts { policy.policy_string.push("version: STSv1".to_string()); @@ -489,7 +500,7 @@ impl SMTP { } // Write entry - report_event.seq_id = self.inner.queue_id_gen.generate().unwrap_or_else(now); + report_event.seq_id = self.inner.data.queue_id_gen.generate().unwrap_or_else(now); builder.set( ValueClass::Queue(QueueClass::TlsReportEvent(report_event)), Bincode::new(event.failure).serialize(), @@ -502,7 +513,7 @@ impl SMTP { } } - pub async fn delete_tls_report(&self, events: Vec<ReportEvent>) { + async fn delete_tls_report(&self, events: Vec<ReportEvent>) { let mut batch = BatchBuilder::new(); for (pos, event) in events.into_iter().enumerate() { diff --git a/crates/smtp/src/scripts/event_loop.rs b/crates/smtp/src/scripts/event_loop.rs index 8dcc1d15..da6885cd 100644 --- a/crates/smtp/src/scripts/event_loop.rs +++ b/crates/smtp/src/scripts/event_loop.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Cow, sync::Arc, time::Instant}; +use std::{borrow::Cow, future::Future, sync::Arc, time::Instant}; -use common::scripts::plugins::PluginContext; +use common::{scripts::plugins::PluginContext, Server}; use mail_auth::common::headers::HeaderWriter; use sieve::{ compiler::grammar::actions::action_redirect::{ByMode, ByTime, Notify, NotifyItem, Ret}, @@ -19,15 +19,24 @@ use smtp_proto::{ use trc::SieveEvent; use crate::{ - core::SMTP, inbound::DkimSign, - queue::{DomainPart, MessageSource}, + queue::{quota::HasQueueQuota, spool::SmtpSpool, DomainPart, MessageSource}, }; use super::{ScriptModification, ScriptParameters, ScriptResult}; -impl SMTP { - pub async fn run_script( +pub trait RunScript: Sync + Send { + fn run_script( + &self, + script_id: String, + script: Arc<Sieve>, + params: ScriptParameters<'_>, + session_id: u64, + ) -> impl Future<Output = ScriptResult> + Send; +} + +impl RunScript for Server { + async fn run_script( &self, script_id: String, script: Arc<Sieve>, @@ -112,8 +121,7 @@ impl SMTP { id, PluginContext { session_id, - core: &self.core, - cache: &self.inner.script_cache, + server: self, message: instance.message(), modifications: &mut modifications, arguments, @@ -264,8 +272,7 @@ impl SMTP { let mut headers = Vec::new(); for dkim in ¶ms.sign { - if let Some(dkim) = self.core.get_dkim_signer(dkim, session_id) - { + if let Some(dkim) = self.get_dkim_signer(dkim, session_id) { match dkim.sign(raw_message) { Ok(signature) => { signature.write_header(&mut headers); diff --git a/crates/smtp/src/scripts/exec.rs b/crates/smtp/src/scripts/exec.rs index ec85c80b..7b92ccfa 100644 --- a/crates/smtp/src/scripts/exec.rs +++ b/crates/smtp/src/scripts/exec.rs @@ -13,7 +13,7 @@ use smtp_proto::*; use crate::{core::Session, inbound::AuthResult}; -use super::{ScriptParameters, ScriptResult}; +use super::{event_loop::RunScript, ScriptParameters, ScriptResult}; impl<T: SessionStream> Session<T> { pub fn build_script_parameters(&self, stage: &'static str) -> ScriptParameters<'_> { @@ -124,12 +124,12 @@ impl<T: SessionStream> Session<T> { script: Arc<Sieve>, params: ScriptParameters<'_>, ) -> ScriptResult { - self.core + self.server .run_script( script_id, script, params - .with_envelope(&self.core.core, self, self.data.session_id) + .with_envelope(&self.server, self, self.data.session_id) .await, self.data.session_id, ) diff --git a/crates/smtp/src/scripts/mod.rs b/crates/smtp/src/scripts/mod.rs index ee3c3ca7..cd162731 100644 --- a/crates/smtp/src/scripts/mod.rs +++ b/crates/smtp/src/scripts/mod.rs @@ -7,7 +7,7 @@ use std::borrow::Cow; use ahash::AHashMap; -use common::{expr::functions::ResolveVariable, scripts::ScriptModification, Core}; +use common::{expr::functions::ResolveVariable, scripts::ScriptModification, Server}; use sieve::{runtime::Variable, Envelope}; pub mod envelope; @@ -58,20 +58,23 @@ impl<'x> ScriptParameters<'x> { pub async fn with_envelope( mut self, - core: &Core, + server: &Server, vars: &impl ResolveVariable, session_id: u64, ) -> Self { for (variable, expr) in [ - (&mut self.from_addr, &core.sieve.from_addr), - (&mut self.from_name, &core.sieve.from_name), - (&mut self.return_path, &core.sieve.return_path), + (&mut self.from_addr, &server.core.sieve.from_addr), + (&mut self.from_name, &server.core.sieve.from_name), + (&mut self.return_path, &server.core.sieve.return_path), ] { - if let Some(value) = core.eval_if(expr, vars, session_id).await { + if let Some(value) = server.eval_if(expr, vars, session_id).await { *variable = value; } } - if let Some(value) = core.eval_if(&core.sieve.sign, vars, session_id).await { + if let Some(value) = server + .eval_if(&server.core.sieve.sign, vars, session_id) + .await + { self.sign = value; } self diff --git a/crates/utils/src/snowflake.rs b/crates/utils/src/snowflake.rs index a8619b89..99e8ecc7 100644 --- a/crates/utils/src/snowflake.rs +++ b/crates/utils/src/snowflake.rs @@ -87,3 +87,13 @@ impl Default for SnowflakeIdGenerator { Self::new() } } + +impl Clone for SnowflakeIdGenerator { + fn clone(&self) -> Self { + Self { + epoch: self.epoch, + node_id: self.node_id, + sequence: 0.into(), + } + } +} |