diff --git a/Cargo.lock b/Cargo.lock index 59ba803a..51206d12 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2939,6 +2939,7 @@ dependencies = [ "mime", "pkcs8", "quick-xml 0.38.4", + "registry", "rev_lines", "rkyv", "rsa", @@ -3335,6 +3336,7 @@ dependencies = [ "nlp", "parking_lot", "rand 0.9.2", + "registry", "rustls 0.23.36", "rustls-pemfile 2.2.0", "store", @@ -3609,6 +3611,7 @@ dependencies = [ "p256", "pkcs8", "rand 0.9.2", + "registry", "reqwest", "rkyv", "rsa", @@ -4149,6 +4152,7 @@ dependencies = [ "mail-send", "md5 0.8.0", "parking_lot", + "registry", "rkyv", "rustls 0.23.36", "rustls-pemfile 2.2.0", @@ -5231,6 +5235,7 @@ dependencies = [ "imap", "mail-parser", "mail-send", + "registry", "rustls 0.23.36", "store", "tokio", @@ -6966,6 +6971,7 @@ dependencies = [ "mail-parser", "memory-stats", "p256", + "registry", "reqwest", "rsa", "serde", @@ -7176,6 +7182,7 @@ dependencies = [ "directory", "email", "form_urlencoded", + "hashify", "http-body-util", "hyper 1.8.1", "hyper-util", @@ -7191,6 +7198,7 @@ dependencies = [ "rand 0.9.2", "rayon", "regex", + "registry", "reqwest", "rkyv", "rustls 0.23.36", diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index e38f277b..946791a5 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -404,8 +404,31 @@ impl AccessToken { } } - pub fn scoped(inner: Arc, scope_idx: usize) -> Self { - AccessToken { scope_idx, inner } + pub fn scoped(inner: Arc, credential_id: u32) -> trc::Result { + inner + .scopes + .iter() + .position(|scope| scope.credential_id == credential_id) + .ok_or_else(|| { + trc::SecurityEvent::Unauthorized + .into_err() + .ctx(trc::Key::AccountId, inner.account_id) + .ctx(trc::Key::Id, credential_id) + .reason("Credential expired or removed.") + }) + .map(|scope_idx| AccessToken { scope_idx, inner }) + .and_then(|token| token.assert_is_valid()) + } + + pub fn renew(inner: Arc, credential_id: Option) -> trc::Result { + if let Some(credential_id) = credential_id { + Self::scoped(inner, credential_id) + } else { + Ok(AccessToken { + scope_idx: 0, + inner, + }) + } } pub fn state(&self) -> u32 { @@ -477,12 +500,28 @@ impl AccessToken { .is_some_and(|scope| scope.permissions.get(permission as usize)) } - pub fn is_valid(&self) -> bool { + pub fn assert_is_valid(self) -> trc::Result { let todo = "use this function"; - self.inner + if self + .inner .scopes .get(self.scope_idx) .is_some_and(|scope| scope.expires_at > now()) + { + Ok(self) + } else { + Err(trc::SecurityEvent::Unauthorized + .into_err() + .ctx(trc::Key::AccountId, self.inner.account_id) + .reason("Access token expired.")) + } + } + + pub fn credential_id(&self) -> Option { + self.inner + .scopes + .get(self.scope_idx) + .map(|scope| scope.credential_id) } pub fn assert_has_permissions(self, permissions: &[Permission]) -> trc::Result { @@ -632,9 +671,15 @@ impl AccessToken { }), } } + + #[cfg(feature = "test_mode")] + pub fn from_id(account_id: u32) -> Self { + AccessToken::new(Arc::new(AccessTokenInner::from_id(account_id))) + } } impl AccessTokenInner { + #[cfg(feature = "test_mode")] pub fn from_id(account_id: u32) -> Self { Self { account_id, @@ -642,13 +687,6 @@ impl AccessTokenInner { } } - pub fn with_access_to(self, access_to: impl IntoIterator) -> Self { - Self { - access_to: access_to.into_iter().collect(), - ..self - } - } - pub fn with_tenant_id(mut self, tenant_id: Option) -> Self { self.tenant_id = tenant_id; self diff --git a/crates/common/src/auth/authentication.rs b/crates/common/src/auth/authentication.rs index bcef094a..4c4ef7a0 100644 --- a/crates/common/src/auth/authentication.rs +++ b/crates/common/src/auth/authentication.rs @@ -329,20 +329,9 @@ impl Server { let token = self .access_token_from_account(account_id, structs::Account::User(account)) .await?; - let scope_idx = token - .scopes - .iter() - .position(|scope| scope.credential_id == credential_id) - .ok_or_else(|| { - trc::AuthEvent::Error - .into_err() - .ctx(trc::Key::AccountId, account_id) - .ctx(trc::Key::Id, credential_id) - .ctx(trc::Key::SpanId, span_id) - .reason("Credential not found in access token scopes") - })?; - return Ok(AccessToken::scoped(token, scope_idx)); + return AccessToken::scoped(token, credential_id) + .add_context(|ctx| ctx.span_id(span_id)); } } diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 7d1a4f6d..dac27789 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -61,8 +61,7 @@ pub const DOMAIN_FLAG_ALIAS_LOGIN: u8 = 1 << 4; #[derive(Debug, Clone)] pub struct AccountCache { pub addresses: Box<[ArcStr]>, - pub addresses_temporary: Box<[TemporaryAddress]>, - pub id_tenant: u32, + pub id_tenant: Option, pub id_member_of: TinyVec<[u32; 3]>, pub quota_disk: u64, pub quota_objects: Option>, @@ -71,12 +70,6 @@ pub struct AccountCache { pub is_user: bool, } -#[derive(Debug, Clone)] -pub struct TemporaryAddress { - pub address: ArcStr, - pub expires_at: u64, -} - #[derive(Debug, Clone)] pub struct RoleCache { pub id_roles: TinyVec<[u32; 3]>, @@ -86,7 +79,6 @@ pub struct RoleCache { #[derive(Debug, Clone)] pub struct MailingListCache { pub addresses: Box<[ArcStr]>, - pub addresses_temporary: Box<[TemporaryAddress]>, pub recipients: Arc<[ArcStr]>, } diff --git a/crates/common/src/cache/directory.rs b/crates/common/src/cache/directory.rs index 5682aeb8..ced89656 100644 --- a/crates/common/src/cache/directory.rs +++ b/crates/common/src/cache/directory.rs @@ -4,15 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use registry::schema::{enums::Locale, prelude::Object}; use store::write::now; use crate::{ Server, auth::{ AccountCache, AccountInfo, AccountTenantIds, DomainCache, EmailCache, RoleCache, - TemporaryAddress, TenantCache, + TenantCache, }, config::smtp::auth::DkimSigner, + storage::ObjectQuota, }; use std::sync::Arc; @@ -64,6 +66,7 @@ impl Server { } impl AccountInfo { + #[inline(always)] pub fn account_id(&self) -> u32 { self.account_id } @@ -73,58 +76,94 @@ impl AccountInfo { .addresses .first() .map(|s| s.as_ref()) - .unwrap_or("") + .unwrap_or_default() } + #[inline(always)] pub fn description(&self) -> Option<&str> { self.account.description.as_deref() } + #[inline(always)] pub fn tenant_id(&self) -> Option { - if self.account.id_tenant != u32::MAX { - Some(self.account.id_tenant) - } else { - None - } + self.account.id_tenant } + #[inline(always)] pub fn account_tenant_ids(&self) -> AccountTenantIds { AccountTenantIds { account_id: self.account_id, - tenant_id: self.tenant_id(), + tenant_id: self.account.id_tenant, } } pub fn addresses(&self) -> impl Iterator { - let now = now(); - self.account.addresses(now).chain( - self.member_of - .iter() - .flat_map(move |member| member.addresses(now)), - ) + self.account + .addresses + .iter() + .chain( + self.member_of + .iter() + .flat_map(move |member| member.addresses.iter()), + ) + .map(|a| a.as_str()) } + #[inline(always)] pub fn is_user_account(&self) -> bool { self.account.is_user } + + #[inline(always)] + pub fn locale(&self) -> Locale { + self.account.locale + } + + #[inline(always)] + pub fn object_quotas(&self) -> Option<&ObjectQuota> { + self.account.quota_objects.as_deref() + } } impl AccountCache { - fn addresses(&self, now: u64) -> impl Iterator { - self.addresses.iter().map(|s| s.as_ref()).chain( - self.addresses_temporary - .iter() - .filter_map(move |a| a.validate(now)), - ) + #[inline(always)] + pub fn name(&self) -> &str { + self.addresses + .first() + .map(|s| s.as_ref()) + .unwrap_or_default() } -} -impl TemporaryAddress { - pub fn validate(&self, now: u64) -> Option<&str> { - if self.expires_at > now { - Some(self.address.as_ref()) - } else { - None + #[inline(always)] + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } + + #[inline(always)] + pub fn tenant_id(&self) -> Option { + self.id_tenant + } + + #[inline(always)] + pub fn is_user_account(&self) -> bool { + self.is_user + } + + #[inline(always)] + pub fn disk_quota(&self) -> u64 { + self.quota_disk + } + + #[inline(always)] + pub fn object_quotas(&self) -> Option<&ObjectQuota> { + self.quota_objects.as_deref() + } + + #[inline(always)] + pub fn account_tenant_ids(&self, account_id: u32) -> AccountTenantIds { + AccountTenantIds { + account_id, + tenant_id: self.id_tenant, } } } diff --git a/crates/common/src/cache/invalidate.rs b/crates/common/src/cache/invalidate.rs index d80492f5..5dc78510 100644 --- a/crates/common/src/cache/invalidate.rs +++ b/crates/common/src/cache/invalidate.rs @@ -42,7 +42,6 @@ impl Server { CacheInvalidation::List(id) => { cache.lists.remove(id); } - CacheInvalidation::PushServers(_) => {} } } diff --git a/crates/common/src/cache/reload.rs b/crates/common/src/cache/reload.rs index 92c4cb8a..9d3fc00b 100644 --- a/crates/common/src/cache/reload.rs +++ b/crates/common/src/cache/reload.rs @@ -7,6 +7,7 @@ use crate::{ Core, Server, config::{server::Listeners, telemetry::Telemetry}, + ipc::RegistryChange, }; use ahash::AHashMap; use arc_swap::ArcSwap; @@ -19,7 +20,7 @@ pub struct ReloadResult { } impl Server { - pub async fn reload_blocked_ips(&self) -> trc::Result { + async fn reload_blocked_ips(&self) -> trc::Result { todo!() /*let mut config = self .core @@ -32,7 +33,7 @@ impl Server { Ok(config.into())*/ } - pub async fn reload_certificates(&self) -> trc::Result { + async fn reload_certificates(&self) -> trc::Result { todo!() /*let mut config = self.core.storage.config.build_config("certificate").await?; let mut certificates = self.inner.data.tls_certificates.load().as_ref().clone(); @@ -44,7 +45,7 @@ impl Server { Ok(config.into())*/ } - pub async fn reload_lookups(&self) -> trc::Result { + async fn reload_lookups(&self) -> trc::Result { todo!() /*let mut config = self.core.storage.config.build_config("lookup").await?; let mut stores = Stores::default(); @@ -62,7 +63,8 @@ impl Server { })*/ } - pub async fn reload(&self) -> trc::Result { + pub async fn reload_registry(&self, change: RegistryChange) -> trc::Result { + // TODO: check the different events triggering this, spam filter reload, etc. todo!() /*let mut config = self.core.storage.config.build_config("").await?; diff --git a/crates/common/src/config/mailstore/email.rs b/crates/common/src/config/mailstore/email.rs index 65d72e46..ac1edf37 100644 --- a/crates/common/src/config/mailstore/email.rs +++ b/crates/common/src/config/mailstore/email.rs @@ -56,6 +56,8 @@ pub struct EmailConfig { pub max_objects: ObjectQuota, pub account_purge_frequency: SimpleCron, + pub data_purge_frequency: SimpleCron, + pub blob_purge_frequency: SimpleCron, } #[derive(Clone, Debug)] @@ -79,11 +81,12 @@ impl EmailConfig { let oidc = bp.setting_infallible::().await; // Parse default object quotas + let todo = "make sure all are configurable"; let mut max_objects = ObjectQuota::default(); for (item, max) in [ (StorageQuota::MaxMailboxes, email.max_mailboxes), (StorageQuota::MaxSieveScripts, sieve.max_scripts), - (StorageQuota::MaxIdentities, email.max_identities), + (StorageQuota::MaxEmailIdentities, email.max_identities), (StorageQuota::MaxEmailSubmissions, email.max_submissions), (StorageQuota::MaxMaskedAddresses, email.max_masked_addresses), (StorageQuota::MaxAppPasswords, oidc.max_app_passwords), @@ -242,9 +245,11 @@ impl EmailConfig { index_batch_size: search.index_batch_size as usize, index_fields, max_objects, - account_purge_frequency: dr.expunge_schedule.into(), default_folders, shared_folder, + account_purge_frequency: dr.expunge_schedule.into(), + data_purge_frequency: dr.data_cleanup_schedule.into(), + blob_purge_frequency: dr.blob_cleanup_schedule.into(), } } } diff --git a/crates/common/src/config/smtp/mod.rs b/crates/common/src/config/smtp/mod.rs index 9a510533..c75b720d 100644 --- a/crates/common/src/config/smtp/mod.rs +++ b/crates/common/src/config/smtp/mod.rs @@ -16,7 +16,7 @@ use self::{ }; use super::*; use crate::expr::Expression; -use registry::schema::structs::Rate; +use registry::{schema::structs::Rate, types::id::Id}; use store::registry::bootstrap::Bootstrap; #[derive(Clone)] @@ -31,7 +31,7 @@ pub struct SmtpConfig { #[derive(Debug, Default, Clone)] //#[cfg_attr(feature = "test_mode", derive(PartialEq, Eq))] pub struct QueueRateLimiter { - pub id: String, + pub id: Id, pub expr: Expression, pub keys: u16, pub rate: Rate, diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index 3f3445cd..0d448ee0 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -170,7 +170,7 @@ pub struct QueueQuotas { #[derive(Clone)] pub struct QueueQuota { - pub id: String, + pub id: Id, pub expr: Expression, pub keys: u16, pub size: Option, @@ -402,7 +402,7 @@ impl QueueRateLimiters { let limiter = QueueRateLimiter { expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()).default, - id: obj.object.name, + id: obj.id, keys: obj .object .key @@ -471,7 +471,7 @@ impl QueueRateLimiters { let limiter = QueueRateLimiter { expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()).default, - id: obj.object.name, + id: obj.id, keys: obj .object .key @@ -532,7 +532,7 @@ impl QueueQuotas { let quota = QueueQuota { expr: bp.compile_expr(obj.id, &obj.object.ctx_match_()).default, - id: obj.object.name, + id: obj.id, keys: obj .object .key diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index 565df7f2..d73dc54f 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -91,7 +91,6 @@ pub struct Mail { pub struct Rcpt { pub script: IfBlock, pub relay: IfBlock, - pub is_local: IfBlock, pub rewrite: IfBlock, pub errors_max: IfBlock, pub errors_wait: IfBlock, @@ -239,7 +238,6 @@ impl SessionConfig { script: bp.compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_script()), relay: bp .compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_allow_relaying()), - is_local: bp.compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_is_local()), rewrite: bp.compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_rewrite()), errors_max: bp .compile_expr(Object::MtaStageRcpt.singleton(), &rcpt.ctx_max_failures()), diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index 188bc364..3272c6ad 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -5,7 +5,7 @@ */ use super::*; -use crate::{Server, expr::StringCow, network::RcptExpansion}; +use crate::{Server, expr::StringCow, network::RcptResolution}; use compact_str::{CompactString, ToCompactString}; use mail_auth::IpLookupStrategy; use std::{cmp::Ordering, net::IpAddr, vec::IntoIter}; @@ -33,10 +33,18 @@ impl Server { F_IS_LOCAL_ADDRESS => { let address = params.next_as_string(); - self.rcpt_expand(address.as_ref()) + self.rcpt_resolve(address.as_ref()) .await .caused_by(trc::location!()) - .map(|v| (v != RcptExpansion::Invalid).into()) + .map(|v| { + (!matches!( + v, + RcptResolution::UnknownRecipient + | RcptResolution::UnknownDomain + | RcptResolution::Forward(_) + )) + .into() + }) } F_KEY_GET => { let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else { diff --git a/crates/common/src/ipc.rs b/crates/common/src/ipc.rs index 7b3bdc75..e41517cf 100644 --- a/crates/common/src/ipc.rs +++ b/crates/common/src/ipc.rs @@ -102,6 +102,7 @@ pub struct CalendarAlert { #[derive(Debug)] pub enum BroadcastEvent { PushNotification(PushNotification), + PushServerUpdate(u32), RegistryChange(RegistryChange), CacheInvalidation(Vec), } @@ -123,7 +124,6 @@ pub enum CacheInvalidation { Tenant(u32), Role(u32), List(u32), - PushServers(u32), } #[derive(Debug)] diff --git a/crates/common/src/network/mod.rs b/crates/common/src/network/mod.rs index 15fad2af..59700c8b 100644 --- a/crates/common/src/network/mod.rs +++ b/crates/common/src/network/mod.rs @@ -35,12 +35,14 @@ pub mod stream; pub mod tls; #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] -pub enum RcptExpansion { - Mailbox(u32), - List(Arc<[ArcStr]>), - External(ArcStr), +pub enum RcptResolution { + Accept, + Expand(Arc<[ArcStr]>), + Rewrite(String), + Forward(String), #[default] - Invalid, + UnknownRecipient, + UnknownDomain, } pub struct ServerInstance { diff --git a/crates/common/src/network/mta.rs b/crates/common/src/network/mta.rs index df69519f..61b729b5 100644 --- a/crates/common/src/network/mta.rs +++ b/crates/common/src/network/mta.rs @@ -17,7 +17,7 @@ use crate::{ }, }, manager::SPAM_CLASSIFIER_KEY, - network::RcptExpansion, + network::RcptResolution, }; use mail_auth::IpLookupStrategy; use sieve::Sieve; @@ -32,8 +32,8 @@ use store::{ use trc::{AddContext, SpamEvent}; impl Server { - pub async fn rcpt_expand(&self, address: &str) -> trc::Result { - let todo = "TODO: RcptExpansion implementation"; + pub async fn rcpt_resolve(&self, address: &str) -> trc::Result { + let todo = "TODO: RcptResolution implementation"; todo!() } diff --git a/crates/common/src/storage/quota.rs b/crates/common/src/storage/quota.rs index 86094872..7e116fa7 100644 --- a/crates/common/src/storage/quota.rs +++ b/crates/common/src/storage/quota.rs @@ -13,11 +13,12 @@ use trc::AddContext; use crate::{ Server, + auth::AccountCache, storage::{ObjectQuota, TenantQuota}, }; impl Server { - pub async fn get_used_quota(&self, account_id: u32) -> trc::Result { + pub async fn get_used_quota_account(&self, account_id: u32) -> trc::Result { self.core .storage .data @@ -26,10 +27,20 @@ impl Server { .add_context(|err| err.caused_by(trc::location!()).account_id(account_id)) } + pub async fn get_used_quota_tenant(&self, tenant_id: u32) -> trc::Result { + let todo = "use correct counter"; + self.core + .storage + .data + .get_counter(DirectoryClass::UsedQuota(tenant_id)) + .await + .add_context(|err| err.caused_by(trc::location!())) + } + pub async fn has_available_quota(&self, account_id: u32, item_size: u64) -> trc::Result<()> { let account = self.account(account_id).await.caused_by(trc::location!())?; if account.quota_disk != 0 { - let used_quota = self.get_used_quota(account_id).await? as u64; + let used_quota = self.get_used_quota_account(account_id).await? as u64; if used_quota + item_size > account.quota_disk { return Err(trc::LimitEvent::Quota @@ -44,14 +55,13 @@ impl Server { // SPDX-License-Identifier: LicenseRef-SEL #[cfg(feature = "enterprise")] - if self.core.is_enterprise_edition() && account.id_tenant != u32::MAX { - let tenant = self - .tenant(account.id_tenant) - .await - .caused_by(trc::location!())?; + if self.core.is_enterprise_edition() + && let Some(tenant_id) = account.id_tenant + { + let tenant = self.tenant(tenant_id).await.caused_by(trc::location!())?; if tenant.quota_disk != 0 { - let used_quota = self.get_used_quota(account.id_tenant).await? as u64; + let used_quota = self.get_used_quota_tenant(tenant_id).await? as u64; if used_quota + item_size > tenant.quota_disk { return Err(trc::LimitEvent::TenantQuota @@ -66,6 +76,11 @@ impl Server { Ok(()) } + + #[inline(always)] + pub fn object_quota(&self, user_quotas: Option<&ObjectQuota>, object: StorageQuota) -> u32 { + user_quotas.unwrap_or(&self.core.email.max_objects).0[object as usize] + } } impl ObjectQuota { diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index ba37581e..79a4d5b2 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -473,7 +473,7 @@ impl DavAclHandler for Server { }) } else { let grant_account = self - .account_info(grant_account_id) + .account(grant_account_id) .await .caused_by(trc::location!())?; diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index 7fd037a0..7ebfdcd3 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -1137,11 +1137,8 @@ impl PropFindRequestHandler for Server { let account = self.account(account_id).await.caused_by(trc::location!())?; let quota = if account.quota_disk > 0 { account.quota_disk - } else if account.id_tenant != u32::MAX { - let tenant = self - .tenant(account.id_tenant) - .await - .caused_by(trc::location!())?; + } else if let Some(tenant_id) = account.id_tenant { + let tenant = self.tenant(tenant_id).await.caused_by(trc::location!())?; if tenant.quota_disk > 0 { tenant.quota_disk } else { @@ -1151,7 +1148,7 @@ impl PropFindRequestHandler for Server { u32::MAX as u64 }; let used = self - .get_used_quota(account_id) + .get_used_quota_account(account_id) .await .caused_by(trc::location!())? as u64; diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index a79755e8..aba2953c 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -112,10 +112,7 @@ impl PrincipalPropFind for Server { let mut fields = Vec::with_capacity(properties.len()); let mut fields_not_found = Vec::new(); - let account = self - .account_info(account_id) - .await - .caused_by(trc::location!())?; + let account = self.account(account_id).await.caused_by(trc::location!())?; // Fetch quota let quota = if needs_quota { @@ -382,10 +379,7 @@ impl PrincipalPropFind for Server { if account_info.account_id() == account_id { Ok(account_info.current_user_principal()) } else { - let account_info = self - .account_info(account_id) - .await - .caused_by(trc::location!())?; + let account_info = self.account(account_id).await.caused_by(trc::location!())?; Ok(Href(format!( "{}/{}/", DavResourceName::Principal.base_path(), @@ -419,7 +413,7 @@ pub(crate) async fn build_home_set( for account_id in access_token.all_ids_by_collection(collection) { if account_id != access_token.account_id() { let other = server - .account_info(account_id) + .account(account_id) .await .caused_by(trc::location!())?; diff --git a/crates/email/src/message/copy.rs b/crates/email/src/message/copy.rs index f8d6ac31..0cb3f3c6 100644 --- a/crates/email/src/message/copy.rs +++ b/crates/email/src/message/copy.rs @@ -172,7 +172,7 @@ impl EmailCopy for Server { batch.with_account_id(to_account_id); // Determine thread id - let tenant_id = self.account_info(to_account_id).await?.tenant_id(); + let tenant_id = self.account(to_account_id).await?.tenant_id(); let thread_id = if let Some(thread_id) = thread_result.thread_id { thread_id } else { diff --git a/crates/email/src/message/delete.rs b/crates/email/src/message/delete.rs index c956c0b5..03cf010b 100644 --- a/crates/email/src/message/delete.rs +++ b/crates/email/src/message/delete.rs @@ -262,7 +262,7 @@ impl EmailDeletion for Server { // Delete messages let mut batch = BatchBuilder::new(); let tenant_id = self - .account_info(account_id) + .account(account_id) .await .caused_by(trc::location!())? .tenant_id(); diff --git a/crates/email/src/sieve/ingest.rs b/crates/email/src/sieve/ingest.rs index 3498eb8c..fcd285f9 100644 --- a/crates/email/src/sieve/ingest.rs +++ b/crates/email/src/sieve/ingest.rs @@ -117,10 +117,7 @@ impl SieveScriptIngest for Server { let mut instance = self.core.sieve.untrusted_runtime.filter_parsed(message); // Set account name and email - let account_info = self - .account_info(account_id) - .await - .caused_by(trc::location!())?; + let account_info = self.account(account_id).await.caused_by(trc::location!())?; let mail_from = account_info.name().to_string(); instance.set_user_full_name( account_info diff --git a/crates/groupware/src/cache/calcard.rs b/crates/groupware/src/cache/calcard.rs index 78ab5833..fa767b47 100644 --- a/crates/groupware/src/cache/calcard.rs +++ b/crates/groupware/src/cache/calcard.rs @@ -181,7 +181,7 @@ pub(super) async fn build_scheduling_resources( .caused_by(trc::location!())? .unwrap_or_default(); - let account_info = server.account_info(account_id).await?; + let account_info = server.account(account_id).await?; let item_ids = server .itip_ids(account_id) .await diff --git a/crates/groupware/src/cache/file.rs b/crates/groupware/src/cache/file.rs index 422981c1..ef3b36b8 100644 --- a/crates/groupware/src/cache/file.rs +++ b/crates/groupware/src/cache/file.rs @@ -31,7 +31,7 @@ pub(super) async fn build_file_resources( .await .caused_by(trc::location!())? .unwrap_or_default(); - let account_info = server.account_info(account_id).await?; + let account_info = server.account(account_id).await?; let mut resources = Vec::with_capacity(16); server diff --git a/crates/groupware/src/calendar/itip.rs b/crates/groupware/src/calendar/itip.rs index 3680c0ef..dec1f86f 100644 --- a/crates/groupware/src/calendar/itip.rs +++ b/crates/groupware/src/calendar/itip.rs @@ -496,13 +496,13 @@ impl ItipIngest for Server { if did_change { // Prepare write batch let account_info = self - .account_info(rsvp.account_id) + .account(rsvp.account_id) .await .caused_by(trc::location!())?; let mut batch = BatchBuilder::new(); new_event .update( - account_info.account_tenant_ids(), + account_info.account_tenant_ids(rsvp.account_id), event, rsvp.account_id, rsvp.document_id, diff --git a/crates/groupware/src/calendar/storage.rs b/crates/groupware/src/calendar/storage.rs index f3feb568..84726c69 100644 --- a/crates/groupware/src/calendar/storage.rs +++ b/crates/groupware/src/calendar/storage.rs @@ -134,10 +134,10 @@ impl ItipAutoExpunge for Server { // Tombstone messages let mut batch = BatchBuilder::new(); let changed_by = self - .account_info(account_id) + .account(account_id) .await .caused_by(trc::location!())? - .account_tenant_ids(); + .account_tenant_ids(account_id); for document_id in destroy_ids { // Fetch event diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index a517a8f6..215073f0 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -19,6 +19,7 @@ jmap_proto = { path = "../jmap-proto" } types = { path = "../types" } directory = { path = "../directory" } services = { path = "../services" } +registry = { path = "../registry" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = { version = "0.4" } diff --git a/crates/http/src/auth/authenticate.rs b/crates/http/src/auth/authenticate.rs index 13976360..56de5882 100644 --- a/crates/http/src/auth/authenticate.rs +++ b/crates/http/src/auth/authenticate.rs @@ -5,13 +5,12 @@ */ use common::auth::AccessToken; -use common::{HttpAuthCache, Server, auth::AuthRequest, listener::limiter::InFlight}; +use common::{HttpAuthCache, Server, auth::AuthRequest, network::limiter::InFlight}; use http_proto::{HttpRequest, HttpSessionData}; use hyper::header; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; use std::future::Future; -use std::sync::Arc; use std::time::{Duration, Instant}; pub trait Authenticator: Sync + Send { @@ -20,7 +19,7 @@ pub trait Authenticator: Sync + Send { req: &HttpRequest, session: &HttpSessionData, allow_api_access: bool, - ) -> impl Future, Arc)>> + Send; + ) -> impl Future, AccessToken)>> + Send; } impl Authenticator for Server { @@ -29,7 +28,7 @@ impl Authenticator for Server { req: &HttpRequest, session: &HttpSessionData, allow_api_access: bool, - ) -> trc::Result<(Option, Arc)> { + ) -> trc::Result<(Option, AccessToken)> { if let Some((mechanism, token)) = req.authorization() { // Check if the credentials are cached if let Some(http_cache) = self.inner.cache.http_auth.get(token) { diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index 349d8281..92ac5177 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -22,13 +22,11 @@ use crate::{ use common::{ Inner, KV_ACME, Server, auth::{AccessToken, oauth::GrantType}, - core::BuildServer, ipc::PushEvent, - listener::{SessionData, SessionManager, SessionStream}, manager::webadmin::Resource, + network::{SessionData, SessionManager, SessionStream}, }; use dav::{DavMethod, request::DavRequestHandler}; -use registry::schema::enums::Permission; use groupware::{DavResourceName, calendar::itip::ItipIngest}; use http_proto::{ DownloadResponse, HtmlResponse, HttpContext, HttpRequest, HttpResponse, HttpResponseBody, @@ -50,6 +48,7 @@ use jmap::{ websocket::upgrade::WebSocketUpgrade, }; use jmap_proto::request::{Request, capability::Session}; +use registry::schema::enums::Permission; use std::{net::IpAddr, str::FromStr, sync::Arc}; use store::dispatch::lookup::KeyValue; use trc::SecurityEvent; diff --git a/crates/imap/Cargo.toml b/crates/imap/Cargo.toml index 5f28d205..28506bf3 100644 --- a/crates/imap/Cargo.toml +++ b/crates/imap/Cargo.toml @@ -13,6 +13,7 @@ common = { path = "../common" } email = { path = "../email" } nlp = { path = "../nlp" } utils = { path = "../utils" } +registry = { path = "../registry" } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] } diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index 4815262b..00284f85 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -8,7 +8,7 @@ use std::{iter::Peekable, sync::Arc, vec::IntoIter}; use common::{ KV_RATE_LIMIT_IMAP, - listener::{SessionResult, SessionStream}, + network::{SessionResult, SessionStream}, }; use imap_proto::{ Command, ResponseType, StatusResponse, @@ -297,9 +297,7 @@ impl Session { && let Some(rate) = &self.server.core.imap.rate_requests && data .server - .core - .storage - .lookup + .in_memory_store() .is_rate_allowed( KV_RATE_LIMIT_IMAP, &data.account_id.to_be_bytes(), diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index 2ea56eca..3e6fa842 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -9,31 +9,27 @@ use crate::core::Mailbox; use ahash::AHashMap; use common::{ auth::AccessToken, - listener::{SessionStream, limiter::InFlight}, + network::{SessionStream, limiter::InFlight}, sharing::EffectiveAcl, }; -use directory::backend::internal::manage::ManageDirectory; use email::{ cache::{MessageCacheFetch, email::MessageCacheAccess, mailbox::MailboxCacheAccess}, mailbox::INBOX_ID, }; use imap_proto::protocol::list::Attribute; use parking_lot::Mutex; -use std::{ - collections::BTreeMap, - sync::{Arc, atomic::Ordering}, -}; +use std::{collections::BTreeMap, sync::atomic::Ordering}; use store::{ ValueKey, write::{AlignedBytes, Archive}, }; use trc::AddContext; -use types::{acl::Acl, collection::Collection, id::Id, keyword::Keyword, special_use::SpecialUse}; +use types::{acl::Acl, collection::Collection, keyword::Keyword, special_use::SpecialUse}; impl SessionData { pub async fn new( session: &Session, - access_token: Arc, + access_token: AccessToken, in_flight: Option, ) -> trc::Result { let mut session = SessionData { @@ -46,33 +42,31 @@ impl SessionData { access_token, in_flight, }; - let access_token = session.access_token.clone(); // Fetch mailboxes for the main account let mut mailboxes = vec![ session - .fetch_account_mailboxes(session.account_id, None, &access_token, None) + .fetch_account_mailboxes(session.account_id, None, &session.access_token, None) .await .caused_by(trc::location!())? .unwrap(), ]; // Fetch shared mailboxes - for &account_id in access_token.shared_accounts(Collection::Mailbox) { + for &account_id in session.access_token.shared_accounts(Collection::Mailbox) { let prefix: String = format!( "{}/{}", - session.server.core.jmap.shared_folder, + session.server.core.email.shared_folder, session .server - .store() - .get_principal_name(account_id) + .account(account_id) .await .caused_by(trc::location!())? - .unwrap_or_else(|| Id::from(account_id).to_string()) + .name() ); mailboxes.push( session - .fetch_account_mailboxes(account_id, prefix.into(), &access_token, None) + .fetch_account_mailboxes(account_id, prefix.into(), &session.access_token, None) .await .caused_by(trc::location!())? .unwrap(), @@ -100,9 +94,7 @@ impl SessionData { return Ok(None); } - let shared_mailbox_ids = if access_token.is_account_id(account_id) - || access_token.member_of.contains(&account_id) - { + let shared_mailbox_ids = if access_token.is_member(account_id) { None } else { cache.shared_mailboxes(access_token, Acl::Read).into() @@ -150,7 +142,7 @@ impl SessionData { let effective_mailbox_id = self .server .core - .jmap + .email .default_folders .iter() .find(|f| f.name == mailbox_name || f.aliases.iter().any(|a| a == &mailbox_name)) @@ -217,8 +209,7 @@ impl SessionData { // Obtain access token let access_token = self - .server - .get_access_token(self.account_id) + .refresh_access_token() .await .caused_by(trc::location!())?; let state = access_token.state(); @@ -267,13 +258,12 @@ impl SessionData { for account_id in added_account_ids { let prefix: String = format!( "{}/{}", - self.server.core.jmap.shared_folder, + self.server.core.email.shared_folder, self.server - .store() - .get_principal_name(account_id) + .account_info(account_id) .await .caused_by(trc::location!())? - .unwrap_or_else(|| Id::from(account_id).to_string()) + .name() ); added_accounts.push( self.fetch_account_mailboxes(account_id, prefix.into(), &access_token, None) @@ -399,7 +389,7 @@ impl SessionData { document_id: u32, item: Acl, ) -> trc::Result { - let access_token = self.get_access_token().await?; + let access_token = self.refresh_access_token().await?; Ok(access_token.is_member(account_id) || self .server diff --git a/crates/imap/src/core/message.rs b/crates/imap/src/core/message.rs index 4445558c..44d7fe66 100644 --- a/crates/imap/src/core/message.rs +++ b/crates/imap/src/core/message.rs @@ -9,7 +9,7 @@ use super::{ }; use crate::core::ImapId; use ahash::AHashMap; -use common::listener::SessionStream; +use common::network::SessionStream; use email::cache::MessageCacheFetch; use imap_proto::protocol::{Sequence, expunge, select::Exists}; use std::collections::BTreeMap; diff --git a/crates/imap/src/core/mod.rs b/crates/imap/src/core/mod.rs index 5c99790c..d49571b8 100644 --- a/crates/imap/src/core/mod.rs +++ b/crates/imap/src/core/mod.rs @@ -4,24 +4,22 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{ - collections::BTreeMap, - net::IpAddr, - sync::{Arc, atomic::AtomicU32}, -}; - use ahash::AHashMap; use common::{ Inner, Server, auth::AccessToken, - listener::{ServerInstance, SessionStream, limiter::InFlight}, + network::{ServerInstance, SessionStream, limiter::InFlight}, }; - use imap_proto::{ Command, protocol::{ProtocolVersion, list::Attribute}, receiver::Receiver, }; +use std::{ + collections::BTreeMap, + net::IpAddr, + sync::{Arc, atomic::AtomicU32}, +}; use tokio::{ io::{ReadHalf, WriteHalf}, sync::watch, @@ -63,7 +61,7 @@ pub struct Session { pub struct SessionData { pub account_id: u32, - pub access_token: Arc, + pub access_token: AccessToken, pub server: Server, pub session_id: u64, pub mailboxes: parking_lot::Mutex>, @@ -80,12 +78,6 @@ pub struct SelectedMailbox { pub is_condstore: bool, } -#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] -pub struct AccountId { - pub account_id: u32, - pub account_id: u32, -} - #[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)] pub struct MailboxId { pub account_id: u32, @@ -198,10 +190,11 @@ impl State { } impl SessionData { - pub async fn get_access_token(&self) -> trc::Result> { + pub async fn refresh_access_token(&self) -> trc::Result { self.server - .get_access_token(self.account_id) + .access_token(self.account_id) .await + .and_then(|inner| AccessToken::renew(inner, self.access_token.credential_id())) .caused_by(trc::location!()) } diff --git a/crates/imap/src/core/session.rs b/crates/imap/src/core/session.rs index c809ced6..108c288b 100644 --- a/crates/imap/src/core/session.rs +++ b/crates/imap/src/core/session.rs @@ -4,23 +4,20 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::sync::Arc; - +use super::{ImapSessionManager, Session, State}; +use crate::{GREETING_WITH_TLS, GREETING_WITHOUT_TLS}; use common::{ - core::BuildServer, - listener::{SessionData, SessionManager, SessionResult, SessionStream, stream::NullIo}, + BuildServer, + network::{SessionData, SessionManager, SessionResult, SessionStream, stream::NullIo}, }; use imap_proto::{ protocol::{ProtocolVersion, SerializeResponse}, receiver::Receiver, }; +use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncWriteExt}; use tokio_rustls::server::TlsStream; -use crate::{GREETING_WITH_TLS, GREETING_WITHOUT_TLS}; - -use super::{ImapSessionManager, Session, State}; - impl SessionManager for ImapSessionManager { #[allow(clippy::manual_async_fn)] fn handle( diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index 7f083444..ff8fbfe2 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -10,17 +10,10 @@ use crate::{ spawn_op, }; use common::{ - auth::AccessToken, listener::SessionStream, sharing::EffectiveAcl, + auth::AccessToken, ipc::CacheInvalidation, network::SessionStream, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder, }; use compact_str::ToCompactString; -use directory::{ - Permission, QueryParams, Type, - backend::internal::{ - PrincipalField, - manage::{ChangedPrincipals, ManageDirectory}, - }, -}; use imap_proto::{ Command, ResponseCode, StatusResponse, protocol::acl::{ @@ -28,7 +21,8 @@ use imap_proto::{ }, receiver::Request, }; -use std::{sync::Arc, time::Instant}; +use registry::schema::enums::Permission; +use std::time::Instant; use store::{ ValueKey, write::{AlignedBytes, Archive, BatchBuilder}, @@ -62,8 +56,16 @@ impl Session { // Add the current user if they are the owner or a group member if data.access_token.is_member(mailbox_id.account_id) { + let account_name = data + .server + .account(mailbox_id.account_id) + .await + .imap_ctx(&arguments.tag, trc::location!())? + .name() + .to_string(); + permissions.push(( - data.access_token.name.clone(), + account_name, vec![ Rights::Read, Rights::Lookup, @@ -86,55 +88,55 @@ impl Session { continue; } - if let Some(account_name) = data + let mut rights = Vec::new(); + + for acl in Bitmap::from(&item.grants) { + match acl { + Acl::Read => { + rights.push(Rights::Lookup); + } + Acl::Modify => { + rights.push(Rights::CreateMailbox); + } + Acl::Delete => { + rights.push(Rights::DeleteMailbox); + } + Acl::ReadItems => { + rights.push(Rights::Read); + } + Acl::AddItems => { + rights.push(Rights::Insert); + } + Acl::ModifyItems => { + rights.push(Rights::Write); + rights.push(Rights::Seen); + } + Acl::RemoveItems => { + rights.push(Rights::DeleteMessages); + rights.push(Rights::Expunge); + } + Acl::CreateChild => { + rights.push(Rights::CreateMailbox); + } + Acl::Share => { + rights.push(Rights::Administer); + } + Acl::Submit => { + rights.push(Rights::Post); + } + _ => (), + } + } + + let account_name = data .server - .store() - .get_principal_name(item.account_id.into()) + .account(item.account_id.into()) .await .imap_ctx(&arguments.tag, trc::location!())? - { - let mut rights = Vec::new(); + .name() + .to_string(); - for acl in Bitmap::from(&item.grants) { - match acl { - Acl::Read => { - rights.push(Rights::Lookup); - } - Acl::Modify => { - rights.push(Rights::CreateMailbox); - } - Acl::Delete => { - rights.push(Rights::DeleteMailbox); - } - Acl::ReadItems => { - rights.push(Rights::Read); - } - Acl::AddItems => { - rights.push(Rights::Insert); - } - Acl::ModifyItems => { - rights.push(Rights::Write); - rights.push(Rights::Seen); - } - Acl::RemoveItems => { - rights.push(Rights::DeleteMessages); - rights.push(Rights::Expunge); - } - Acl::CreateChild => { - rights.push(Rights::CreateMailbox); - } - Acl::Share => { - rights.push(Rights::Administer); - } - Acl::Submit => { - rights.push(Rights::Post); - } - _ => (), - } - } - - permissions.push((account_name, rights)); - } + permissions.push((account_name, rights)); } trc::event!( @@ -273,13 +275,7 @@ impl Session { // Obtain principal id let acl_account_id = data .server - .core - .storage - .directory - .query( - QueryParams::name(arguments.identifier.as_ref().unwrap()) - .with_return_member_of(false), - ) + .account_id(arguments.identifier.as_ref().unwrap()) .await .imap_ctx(&arguments.tag, trc::location!())? .ok_or_else(|| { @@ -288,8 +284,7 @@ impl Session { .details("Account does not exist") .id(arguments.tag.to_string()) .caused_by(trc::location!()) - })? - .id(); + })?; // Prepare changes let mut mailbox = current_mailbox.inner.clone(); @@ -379,11 +374,7 @@ impl Session { // Invalidate ACLs data.server - .invalidate_principal_caches(ChangedPrincipals::from_change( - acl_account_id, - Type::Individual, - PrincipalField::EnabledPermissions, - )) + .invalidate_caches(vec![CacheInvalidation::AccessToken(acl_account_id)], true) .await; trc::event!( @@ -446,9 +437,10 @@ impl Session { pub fn assert_has_permission(&self, permission: Permission) -> trc::Result { match &self.state { - State::Authenticated { data } | State::Selected { data, .. } => { - data.access_token.assert_has_permission(permission) - } + State::Authenticated { data } | State::Selected { data, .. } => data + .access_token + .enforce_permission(permission) + .map(|_| true), State::NotAuthenticated { .. } => Ok(false), } } @@ -459,7 +451,7 @@ impl SessionData { &self, arguments: &Arguments, validate: bool, - ) -> trc::Result<(MailboxId, Archive, Arc)> { + ) -> trc::Result<(MailboxId, Archive, AccessToken)> { if let Some(mailbox) = self.get_mailbox_by_name(&arguments.mailbox_name) { if let Some(values) = self .server @@ -472,7 +464,10 @@ impl SessionData { .await .caused_by(trc::location!())? { - let access_token = self.get_access_token().await.caused_by(trc::location!())?; + let access_token = self + .refresh_access_token() + .await + .caused_by(trc::location!())?; if !validate || access_token.is_member(mailbox.account_id) || values diff --git a/crates/imap/src/op/append.rs b/crates/imap/src/op/append.rs index 7a2a2e02..86911187 100644 --- a/crates/imap/src/op/append.rs +++ b/crates/imap/src/op/append.rs @@ -9,8 +9,7 @@ use crate::{ core::{ImapUidToId, MailboxId, SelectedMailbox, Session, SessionData}, spawn_op, }; -use common::{ipc::PushNotification, listener::SessionStream}; -use registry::schema::enums::Permission; +use common::{auth::BuildAccessToken, ipc::PushNotification, network::SessionStream}; use email::message::ingest::{EmailIngest, IngestEmail, IngestSource}; use imap_proto::{ Command, ResponseCode, StatusResponse, @@ -18,6 +17,7 @@ use imap_proto::{ receiver::Request, }; use mail_parser::MessageParser; +use registry::schema::enums::Permission; use std::{sync::Arc, time::Instant}; use types::{ acl::Acl, @@ -89,11 +89,17 @@ impl SessionData { } // Obtain access token - let access_token = self - .server - .get_access_token(mailbox.account_id) - .await - .imap_ctx(&arguments.tag, trc::location!())?; + let access_token = if mailbox.account_id == self.account_id { + self.refresh_access_token() + .await + .imap_ctx(&arguments.tag, trc::location!())? + } else { + self.server + .access_token(mailbox.account_id) + .await + .imap_ctx(&arguments.tag, trc::location!())? + .build() + }; // Append messages let mut response = StatusResponse::completed(Command::Append); diff --git a/crates/imap/src/op/authenticate.rs b/crates/imap/src/op/authenticate.rs index edaaac13..3289c55f 100644 --- a/crates/imap/src/op/authenticate.rs +++ b/crates/imap/src/op/authenticate.rs @@ -4,26 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::core::{Session, SessionData, State}; use common::{ - auth::{ - AuthRequest, - sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain}, - }, - listener::{SessionStream, limiter::LimiterResult}, + auth::AuthRequest, + network::{SessionStream, limiter::LimiterResult}, }; - -use registry::schema::enums::Permission; +use directory::Credentials; use imap_proto::{ Command, ResponseCode, StatusResponse, protocol::{authenticate::Mechanism, capability::Capability}, receiver::{self, Request}, }; use mail_parser::decoders::base64::base64_decode; -use mail_send::Credentials; +use registry::schema::enums::Permission; use std::sync::Arc; -use crate::core::{Session, SessionData, State}; - impl Session { pub async fn handle_authenticate(&mut self, request: Request) -> trc::Result<()> { let mut args = request.parse_authenticate()?; @@ -41,9 +36,9 @@ impl Session { })?; let credentials = if args.mechanism == Mechanism::Plain { - sasl_decode_challenge_plain(&challenge) + Credentials::decode_sasl_challenge_plain(&challenge) } else { - sasl_decode_challenge_oauth(&challenge) + Credentials::decode_sasl_challenge_oauth(&challenge) } .ok_or_else(|| { trc::AuthEvent::Error @@ -71,11 +66,7 @@ impl Session { } } - pub async fn authenticate( - &mut self, - credentials: Credentials, - tag: String, - ) -> trc::Result<()> { + pub async fn authenticate(&mut self, credentials: Credentials, tag: String) -> trc::Result<()> { // Authenticate let access_token = self .server @@ -99,11 +90,7 @@ impl Session { err.id(tag.clone()) }) - .and_then(|token| { - token - .assert_has_permission(Permission::ImapAuthenticate) - .map(|_| token) - })?; + .and_then(|token| token.assert_has_permission(Permission::ImapAuthenticate))?; // Enforce concurrency limits let in_flight = match access_token.is_imap_request_allowed() { diff --git a/crates/imap/src/op/capability.rs b/crates/imap/src/op/capability.rs index 450e82ed..82f8a4f4 100644 --- a/crates/imap/src/op/capability.rs +++ b/crates/imap/src/op/capability.rs @@ -7,8 +7,7 @@ use std::time::Instant; use crate::core::Session; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use imap_proto::{ Command, StatusResponse, protocol::{ @@ -17,6 +16,7 @@ use imap_proto::{ }, receiver::Request, }; +use registry::schema::enums::Permission; impl Session { pub async fn handle_capability(&mut self, request: Request) -> trc::Result<()> { diff --git a/crates/imap/src/op/close.rs b/crates/imap/src/op/close.rs index 1f8f4103..db77c32d 100644 --- a/crates/imap/src/op/close.rs +++ b/crates/imap/src/op/close.rs @@ -7,7 +7,7 @@ use std::time::Instant; use crate::core::{Session, State}; -use common::listener::SessionStream; +use common::network::SessionStream; use imap_proto::{Command, StatusResponse, receiver::Request}; use trc::AddContext; diff --git a/crates/imap/src/op/copy_move.rs b/crates/imap/src/op/copy_move.rs index d26a5656..cf5e56c8 100644 --- a/crates/imap/src/op/copy_move.rs +++ b/crates/imap/src/op/copy_move.rs @@ -9,8 +9,7 @@ use crate::{ core::{MailboxId, SelectedMailbox, Session, SessionData}, spawn_op, }; -use common::{ipc::PushNotification, listener::SessionStream, storage::index::ObjectIndexBuilder}; -use registry::schema::enums::Permission; +use common::{ipc::PushNotification, network::SessionStream, storage::index::ObjectIndexBuilder}; use email::{ cache::{MessageCacheFetch, email::MessageCacheAccess}, mailbox::{JUNK_ID, TRASH_ID, UidMailbox}, @@ -24,6 +23,7 @@ use imap_proto::{ Command, ResponseCode, ResponseType, StatusResponse, protocol::copy_move::Arguments, receiver::Request, }; +use registry::schema::enums::Permission; use std::{sync::Arc, time::Instant}; use store::{ ValueKey, @@ -195,11 +195,6 @@ impl SessionData { }); let mut did_move = false; let mut copied_ids = Vec::with_capacity(ids.len()); - let access_token = self - .server - .get_access_token(dest_mailbox.account_id) - .await - .imap_ctx(&arguments.tag, trc::location!())?; if src_mailbox.id.account_id == dest_mailbox.account_id { // Mailboxes are in the same account @@ -352,7 +347,6 @@ impl SessionData { let src_account_id = src_mailbox.id.account_id; let mut dest_change_id = None; let dest_account_id = dest_mailbox.account_id; - let resource_token = access_token.as_resource_token(); let mut destroy_ids = RoaringBitmap::new(); let cache = self .server @@ -365,7 +359,7 @@ impl SessionData { .copy_message( src_account_id, id, - &resource_token, + dest_account_id, vec![dest_mailbox_id], cache .email_by_id(&id) diff --git a/crates/imap/src/op/create.rs b/crates/imap/src/op/create.rs index 3c29edb4..4c6a4723 100644 --- a/crates/imap/src/op/create.rs +++ b/crates/imap/src/op/create.rs @@ -9,14 +9,14 @@ use crate::{ op::ImapContext, spawn_op, }; -use common::{listener::SessionStream, storage::index::ObjectIndexBuilder}; -use registry::schema::enums::Permission; +use common::{network::SessionStream, storage::index::ObjectIndexBuilder}; use email::cache::{MessageCacheFetch, mailbox::MailboxCacheAccess}; use imap_proto::{ Command, ResponseCode, StatusResponse, protocol::{create::Arguments, list::Attribute}, receiver::Request, }; +use registry::schema::enums::Permission; use std::time::Instant; use store::write::BatchBuilder; use trc::AddContext; @@ -155,7 +155,7 @@ impl SessionData { return Err(trc::ImapEvent::Error .into_err() .details("Invalid empty path item.")); - } else if path_item.len() > self.server.core.jmap.mailbox_name_max_len { + } else if path_item.len() > self.server.core.email.mailbox_name_max_len { return Err(trc::ImapEvent::Error .into_err() .details("Mailbox name is too long.")); @@ -163,7 +163,7 @@ impl SessionData { path.push(path_item); } - if path.len() > self.server.core.jmap.mailbox_max_depth { + if path.len() > self.server.core.email.mailbox_max_depth { return Err(trc::ImapEvent::Error .into_err() .details("Mailbox path is too deep.")); @@ -178,7 +178,7 @@ impl SessionData { let (account_id, path) = { let mailboxes = self.mailboxes.lock(); let (account, full_path, prefix) = - if path.first() == Some(&self.server.core.jmap.shared_folder.as_str()) { + if path.first() == Some(&self.server.core.email.shared_folder.as_str()) { // Shared Folders// if path.len() < 3 { return Err(trc::ImapEvent::Error @@ -272,7 +272,7 @@ impl SessionData { } } else if self.account_id != account_id && !self - .get_access_token() + .refresh_access_token() .await .caused_by(trc::location!())? .is_member(account_id) diff --git a/crates/imap/src/op/delete.rs b/crates/imap/src/op/delete.rs index ad06ca67..60eb3567 100644 --- a/crates/imap/src/op/delete.rs +++ b/crates/imap/src/op/delete.rs @@ -9,12 +9,12 @@ use crate::{ core::{Session, SessionData}, spawn_op, }; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use email::mailbox::destroy::{MailboxDestroy, MailboxDestroyError}; use imap_proto::{ Command, ResponseCode, StatusResponse, protocol::delete::Arguments, receiver::Request, }; +use registry::schema::enums::Permission; use std::time::Instant; impl Session { @@ -68,7 +68,7 @@ impl SessionData { // Delete message let access_token = self - .get_access_token() + .refresh_access_token() .await .imap_ctx(&arguments.tag, trc::location!())?; diff --git a/crates/imap/src/op/enable.rs b/crates/imap/src/op/enable.rs index 9b824597..435868f7 100644 --- a/crates/imap/src/op/enable.rs +++ b/crates/imap/src/op/enable.rs @@ -7,7 +7,7 @@ use std::time::Instant; use crate::core::Session; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; use imap_proto::{ Command, StatusResponse, diff --git a/crates/imap/src/op/expunge.rs b/crates/imap/src/op/expunge.rs index 6ade7a5f..bc5c7f5d 100644 --- a/crates/imap/src/op/expunge.rs +++ b/crates/imap/src/op/expunge.rs @@ -7,8 +7,7 @@ use super::{ImapContext, ToModSeq}; use crate::core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData}; use ahash::AHashMap; -use common::{listener::SessionStream, storage::index::ObjectIndexBuilder}; -use registry::schema::enums::Permission; +use common::{network::SessionStream, storage::index::ObjectIndexBuilder}; use email::{ cache::{MessageCacheFetch, email::MessageCacheAccess}, message::metadata::MessageData, @@ -18,6 +17,7 @@ use imap_proto::{ parser::parse_sequence_set, receiver::{Request, Token}, }; +use registry::schema::enums::Permission; use std::{sync::Arc, time::Instant}; use store::{ SerializeInfallible, @@ -194,7 +194,7 @@ impl SessionData { batch .custom( ObjectIndexBuilder::<_, ()>::new() - .with_access_token(&self.access_token) + .with_changed_by(self.access_token.account_tenant_ids()) .with_current(metadata), ) .caused_by(trc::location!())? diff --git a/crates/imap/src/op/fetch.rs b/crates/imap/src/op/fetch.rs index ffd9542b..bbae6631 100644 --- a/crates/imap/src/op/fetch.rs +++ b/crates/imap/src/op/fetch.rs @@ -10,7 +10,7 @@ use crate::{ spawn_op, }; use ahash::AHashMap; -use common::{listener::SessionStream, storage::index::ObjectIndexBuilder}; +use common::{network::SessionStream, storage::index::ObjectIndexBuilder}; use registry::schema::enums::Permission; use email::{ cache::{MessageCacheFetch, email::MessageCacheAccess}, diff --git a/crates/imap/src/op/idle.rs b/crates/imap/src/op/idle.rs index 3358d2b6..e1eaef7c 100644 --- a/crates/imap/src/op/idle.rs +++ b/crates/imap/src/op/idle.rs @@ -9,7 +9,7 @@ use crate::{ op::ImapContext, }; use ahash::AHashSet; -use common::{ipc::PushNotification, listener::SessionStream}; +use common::{ipc::PushNotification, network::SessionStream}; use registry::schema::enums::Permission; use imap_proto::{ Command, StatusResponse, diff --git a/crates/imap/src/op/list.rs b/crates/imap/src/op/list.rs index baf2f401..6ad81800 100644 --- a/crates/imap/src/op/list.rs +++ b/crates/imap/src/op/list.rs @@ -10,9 +10,8 @@ use crate::{ core::{Session, SessionData}, spawn_op, }; -use common::listener::SessionStream; +use common::network::SessionStream; -use registry::schema::enums::Permission; use imap_proto::{ Command, StatusResponse, protocol::{ @@ -23,6 +22,7 @@ use imap_proto::{ }, receiver::Request, }; +use registry::schema::enums::Permission; use trc::StoreEvent; use super::ImapContext; @@ -182,10 +182,10 @@ impl SessionData { if let Some(prefix) = &account.prefix { if !added_shared_folder { if !filter_subscribed - && matches_pattern(&patterns, &self.server.core.jmap.shared_folder) + && matches_pattern(&patterns, &self.server.core.email.shared_folder) { list_items.push(ListItem { - mailbox_name: self.server.core.jmap.shared_folder.as_str().into(), + mailbox_name: self.server.core.email.shared_folder.as_str().into(), attributes: if include_children { vec![Attribute::HasChildren, Attribute::NoSelect] } else { diff --git a/crates/imap/src/op/login.rs b/crates/imap/src/op/login.rs index 73c61f5a..417701e8 100644 --- a/crates/imap/src/op/login.rs +++ b/crates/imap/src/op/login.rs @@ -4,18 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use imap_proto::{Command, receiver::Request}; - use crate::core::Session; -use common::listener::SessionStream; -use mail_send::Credentials; +use common::network::SessionStream; +use directory::Credentials; +use imap_proto::{Command, receiver::Request}; impl Session { pub async fn handle_login(&mut self, request: Request) -> trc::Result<()> { let arguments = request.parse_login()?; self.authenticate( - Credentials::Plain { + Credentials::Basic { username: arguments.username.to_string(), secret: arguments.password.to_string(), }, diff --git a/crates/imap/src/op/logout.rs b/crates/imap/src/op/logout.rs index f5c214a3..44b9c7fd 100644 --- a/crates/imap/src/op/logout.rs +++ b/crates/imap/src/op/logout.rs @@ -7,7 +7,7 @@ use std::time::Instant; use crate::core::Session; -use common::listener::SessionStream; +use common::network::SessionStream; use imap_proto::{Command, StatusResponse, receiver::Request}; impl Session { diff --git a/crates/imap/src/op/namespace.rs b/crates/imap/src/op/namespace.rs index f8fb45f6..33ad5c56 100644 --- a/crates/imap/src/op/namespace.rs +++ b/crates/imap/src/op/namespace.rs @@ -5,13 +5,13 @@ */ use crate::core::Session; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use imap_proto::{ Command, StatusResponse, protocol::{ImapResponse, namespace::Response}, receiver::Request, }; +use registry::schema::enums::Permission; impl Session { pub async fn handle_namespace(&mut self, request: Request) -> trc::Result<()> { @@ -30,7 +30,7 @@ impl Session { .serialize( Response { shared_prefix: if self.state.session_data().mailboxes.lock().len() > 1 { - Some(self.server.core.jmap.shared_folder.as_str().into()) + Some(self.server.core.email.shared_folder.as_str().into()) } else { None }, diff --git a/crates/imap/src/op/noop.rs b/crates/imap/src/op/noop.rs index a9fb9bdc..fee287ce 100644 --- a/crates/imap/src/op/noop.rs +++ b/crates/imap/src/op/noop.rs @@ -7,7 +7,7 @@ use std::time::Instant; use crate::core::{Session, State}; -use common::listener::SessionStream; +use common::network::SessionStream; use imap_proto::{Command, StatusResponse, receiver::Request}; impl Session { diff --git a/crates/imap/src/op/quota.rs b/crates/imap/src/op/quota.rs index 6a4fa1e7..34ea9628 100644 --- a/crates/imap/src/op/quota.rs +++ b/crates/imap/src/op/quota.rs @@ -15,8 +15,7 @@ use crate::{ op::ImapContext, spawn_op, }; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use imap_proto::{ Command, ResponseCode, StatusResponse, protocol::{ @@ -26,6 +25,7 @@ use imap_proto::{ }, receiver::Request, }; +use registry::schema::enums::Permission; use std::time::Instant; impl Session { @@ -100,14 +100,14 @@ impl SessionData { })?; // Obtain access token for mailbox - let access_token = self + let account = self .server - .get_access_token(account_id) + .account(account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; let used_quota = self .server - .get_used_quota(account_id) + .get_used_quota_account(account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -117,7 +117,7 @@ impl SessionData { Id = arguments.name.clone(), Details = vec![ trc::Value::from(used_quota), - trc::Value::from(access_token.quota) + trc::Value::from(account.disk_quota()) ], Elapsed = op_start.elapsed() ); @@ -127,10 +127,10 @@ impl SessionData { quota_root_items: vec![], quota_items: vec![QuotaItem { name: arguments.name, - resources: if access_token.quota > 0 { + resources: if account.disk_quota() > 0 { vec![QuotaResource { resource: QuotaResourceName::Storage, - total: access_token.quota, + total: account.disk_quota(), used: used_quota as u64, }] } else { @@ -164,14 +164,14 @@ impl SessionData { }; // Obtain access token for mailbox - let access_token = self + let account = self .server - .get_access_token(account_id) + .account(account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; let used_quota = self .server - .get_used_quota(account_id) + .get_used_quota_account(account_id) .await .imap_ctx(&arguments.tag, trc::location!())?; @@ -181,7 +181,7 @@ impl SessionData { MailboxName = arguments.name.clone(), Details = vec![ trc::Value::from(used_quota), - trc::Value::from(access_token.quota) + trc::Value::from(account.disk_quota()) ], Elapsed = op_start.elapsed() ); @@ -191,10 +191,10 @@ impl SessionData { quota_root_items: vec![arguments.name, format!("#{account_id}")], quota_items: vec![QuotaItem { name: format!("#{account_id}"), - resources: if access_token.quota > 0 { + resources: if account.disk_quota() > 0 { vec![QuotaResource { resource: QuotaResourceName::Storage, - total: access_token.quota, + total: account.disk_quota(), used: used_quota as u64, }] } else { diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs index b73a6b18..dc821a36 100644 --- a/crates/imap/src/op/rename.rs +++ b/crates/imap/src/op/rename.rs @@ -8,11 +8,11 @@ use crate::{ core::{Session, SessionData}, spawn_op, }; -use common::{listener::SessionStream, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder}; -use registry::schema::enums::Permission; +use common::{network::SessionStream, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder}; use imap_proto::{ Command, ResponseCode, StatusResponse, protocol::rename::Arguments, receiver::Request, }; +use registry::schema::enums::Permission; use std::time::Instant; use store::{ ValueKey, @@ -110,7 +110,7 @@ impl SessionData { // Validate ACL let access_token = self - .get_access_token() + .refresh_access_token() .await .imap_ctx(&arguments.tag, trc::location!())?; if access_token.is_shared(params.account_id) diff --git a/crates/imap/src/op/search.rs b/crates/imap/src/op/search.rs index 9e3597c9..5b304d0f 100644 --- a/crates/imap/src/op/search.rs +++ b/crates/imap/src/op/search.rs @@ -9,8 +9,7 @@ use crate::{ core::{ImapId, SavedSearch, SelectedMailbox, Session, SessionData}, spawn_op, }; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use email::cache::{MessageCacheFetch, email::MessageCacheAccess}; use imap_proto::{ Command, StatusResponse, @@ -22,6 +21,7 @@ use imap_proto::{ }; use mail_parser::HeaderName; use nlp::language::Language; +use registry::schema::enums::Permission; use std::{str::FromStr, sync::Arc, time::Instant}; use store::{ query::log::Query, @@ -449,7 +449,7 @@ impl SessionData { filters.push(SearchFilter::has_text_detect( EmailSearchField::Body, text, - self.server.core.jmap.default_language, + self.server.core.email.default_language, )); } Filter::Cc(text) => { @@ -473,7 +473,7 @@ impl SessionData { filters.push(SearchFilter::has_text_detect( EmailSearchField::Subject, value, - self.server.core.jmap.default_language, + self.server.core.email.default_language, )); } header @ (HeaderName::From @@ -522,12 +522,12 @@ impl SessionData { filters.push(SearchFilter::has_text_detect( EmailSearchField::Subject, text, - self.server.core.jmap.default_language, + self.server.core.email.default_language, )); } Filter::Text(text) => { let (text, language) = - Language::detect(text, self.server.core.jmap.default_language); + Language::detect(text, self.server.core.email.default_language); filters.push(SearchFilter::Or); filters.push(SearchFilter::has_text( diff --git a/crates/imap/src/op/select.rs b/crates/imap/src/op/select.rs index ffbbe672..0d487c89 100644 --- a/crates/imap/src/op/select.rs +++ b/crates/imap/src/op/select.rs @@ -6,7 +6,7 @@ use super::{ImapContext, ToModSeq}; use crate::core::{SavedSearch, SelectedMailbox, Session, State}; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; use imap_proto::{ Command, ResponseCode, StatusResponse, diff --git a/crates/imap/src/op/status.rs b/crates/imap/src/op/status.rs index 749c818d..cb65403c 100644 --- a/crates/imap/src/op/status.rs +++ b/crates/imap/src/op/status.rs @@ -10,8 +10,7 @@ use crate::{ op::ImapContext, spawn_op, }; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use email::cache::{MessageCacheFetch, email::MessageCacheAccess}; use imap_proto::{ Command, ResponseCode, StatusResponse, @@ -19,6 +18,7 @@ use imap_proto::{ protocol::status::{Status, StatusItem, StatusItemType}, receiver::Request, }; +use registry::schema::enums::Permission; use std::time::Instant; use trc::AddContext; use types::{id::Id, keyword::Keyword}; @@ -89,11 +89,11 @@ impl SessionData { mailbox } else { // Some IMAP clients will try to get the status of a mailbox with the NoSelect flag - return if mailbox_name == self.server.core.jmap.shared_folder + return if mailbox_name == self.server.core.email.shared_folder || mailbox_name .split_once('/') .is_some_and(|(base_name, path)| { - base_name == self.server.core.jmap.shared_folder && !path.contains('/') + base_name == self.server.core.email.shared_folder && !path.contains('/') }) { Ok(StatusItem { diff --git a/crates/imap/src/op/store.rs b/crates/imap/src/op/store.rs index 48788a75..01d70148 100644 --- a/crates/imap/src/op/store.rs +++ b/crates/imap/src/op/store.rs @@ -10,7 +10,7 @@ use crate::{ spawn_op, }; use ahash::AHashSet; -use common::{listener::SessionStream, storage::index::ObjectIndexBuilder}; +use common::{network::SessionStream, storage::index::ObjectIndexBuilder}; use registry::schema::enums::Permission; use email::{ mailbox::TRASH_ID, diff --git a/crates/imap/src/op/subscribe.rs b/crates/imap/src/op/subscribe.rs index 84cb6929..eadd11b8 100644 --- a/crates/imap/src/op/subscribe.rs +++ b/crates/imap/src/op/subscribe.rs @@ -9,7 +9,7 @@ use crate::{ core::{Session, SessionData}, spawn_op, }; -use common::{listener::SessionStream, storage::index::ObjectIndexBuilder}; +use common::{network::SessionStream, storage::index::ObjectIndexBuilder}; use registry::schema::enums::Permission; use imap_proto::{Command, ResponseCode, StatusResponse, receiver::Request}; use std::time::Instant; diff --git a/crates/imap/src/op/thread.rs b/crates/imap/src/op/thread.rs index ec854c80..55ec0ffa 100644 --- a/crates/imap/src/op/thread.rs +++ b/crates/imap/src/op/thread.rs @@ -9,7 +9,7 @@ use crate::{ spawn_op, }; use ahash::AHashMap; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; use email::cache::{MessageCacheFetch, email::MessageCacheAccess}; use imap_proto::{ diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index 0d2dd9de..62747706 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -18,6 +18,7 @@ trc = { path = "../trc" } spam-filter = { path = "../spam-filter" } email = { path = "../email" } groupware = { path = "../groupware" } +registry = { path = "../registry" } calcard = { version = "0.3" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } diff --git a/crates/jmap/src/addressbook/set.rs b/crates/jmap/src/addressbook/set.rs index c22e3173..0693bb82 100644 --- a/crates/jmap/src/addressbook/set.rs +++ b/crates/jmap/src/addressbook/set.rs @@ -51,7 +51,11 @@ impl AddressBookSet for Server { ) -> trc::Result> { let account_id = request.account_id.document_id(); let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::AddressBook) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::AddressBook, + ) .await?; let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); @@ -107,7 +111,12 @@ impl AddressBookSet for Server { .await .caused_by(trc::location!())?; address_book - .insert(access_token, account_id, document_id, &mut batch) + .insert( + access_token.account_tenant_ids(), + account_id, + document_id, + &mut batch, + ) .caused_by(trc::location!())?; if let Some(MaybeIdReference::Reference(id_ref)) = @@ -188,7 +197,7 @@ impl AddressBookSet for Server { // Update record new_address_book .update( - access_token, + access_token.account_tenant_ids(), address_book, account_id, document_id, @@ -274,7 +283,13 @@ impl AddressBookSet for Server { // Delete record DestroyArchive(address_book) - .delete(access_token, account_id, document_id, None, &mut batch) + .delete( + access_token.account_tenant_ids(), + account_id, + document_id, + None, + &mut batch, + ) .caused_by(trc::location!())?; if default_address_book_id == Some(document_id) { @@ -308,7 +323,7 @@ impl AddressBookSet for Server { { // Card only belongs to address books being deleted, delete it DestroyArchive(card).delete_all( - access_token, + access_token.account_tenant_ids(), account_id, document_id, &mut batch, @@ -322,7 +337,7 @@ impl AddressBookSet for Server { .names .retain(|n| !destroy_parents.contains(&n.parent_id)); new_card.update( - access_token, + access_token.account_tenant_ids(), card, account_id, document_id, diff --git a/crates/jmap/src/api/acl.rs b/crates/jmap/src/api/acl.rs index 47088812..cbaa63e5 100644 --- a/crates/jmap/src/api/acl.rs +++ b/crates/jmap/src/api/acl.rs @@ -5,12 +5,13 @@ */ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; -use directory::backend::internal::manage::ManageDirectory; use jmap_proto::{ error::set::SetError, object::{JmapRight, JmapSharedObject}, }; use jmap_tools::{JsonPointerIter, Key, Map, Property, Value}; +use registry::schema::prelude::Object; +use store::{registry::RegistryQuery, roaring::RoaringBitmap}; use types::{ acl::{Acl, AclGrant}, id::Id, @@ -240,8 +241,8 @@ impl JmapAcl for Server { } let principal_ids = self - .store() - .principal_ids(None, None) + .registry() + .query::(RegistryQuery::new(Object::Account)) .await .unwrap_or_default(); diff --git a/crates/jmap/src/api/mod.rs b/crates/jmap/src/api/mod.rs index 42145972..71aa896e 100644 --- a/crates/jmap/src/api/mod.rs +++ b/crates/jmap/src/api/mod.rs @@ -98,7 +98,7 @@ impl ToRequestError for trc::Error { }, trc::EventType::Auth(cause) => match cause { trc::AuthEvent::MissingTotp => { - RequestError::blank(402, "TOTP code required", cause.message()) + RequestError::blank(402, "TOTP code required", self.as_ref().message()) } trc::AuthEvent::TooManyAttempts => RequestError::too_many_auth_attempts(), _ => RequestError::unauthorized(), @@ -110,6 +110,9 @@ impl ToRequestError for trc::Error { | trc::SecurityEvent::LoiterBan | trc::SecurityEvent::IpBlocked => RequestError::too_many_auth_attempts(), trc::SecurityEvent::Unauthorized => RequestError::forbidden(), + trc::SecurityEvent::IpBlockExpired | trc::SecurityEvent::IpAllowExpired => { + RequestError::internal_server_error() + } }, trc::EventType::Resource(cause) => match cause { trc::ResourceEvent::NotFound => RequestError::not_found(), diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index d7dd1fc1..27e50711 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -429,7 +429,7 @@ impl RequestHandler for Server { set_account_id_if_missing(&mut req.account_id, access_token); access_token.assert_is_member(req.account_id)?; - self.identity_set(req, access_token).await?.into() + self.identity_set(req).await?.into() } SetRequestMethod::EmailSubmission(mut req) => { set_account_id_if_missing(&mut req.account_id, access_token); @@ -511,9 +511,7 @@ impl RequestHandler for Server { set_account_id_if_missing(&mut req.account_id, access_token); access_token.assert_is_member(req.account_id)?; - self.participant_identity_set(req, access_token) - .await? - .into() + self.participant_identity_set(req).await?.into() } }, RequestMethod::Changes(mut req) => { diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index eea03b3d..a04ae27e 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -5,12 +5,12 @@ */ use common::{Server, auth::AccessToken}; -use registry::schema::enums::Permission; use jmap_proto::request::capability::{ Account, Capabilities, Capability, EmptyCapabilities, Session, }; +use registry::schema::enums::Permission; use std::future::Future; -use std::sync::Arc; +use trc::AddContext; use types::id::Id; use utils::map::vec_map::VecMap; @@ -18,7 +18,7 @@ pub trait SessionHandler: Sync + Send { fn handle_session_resource( &self, base_url: String, - access_token: Arc, + access_token: &AccessToken, ) -> impl Future> + Send; } @@ -26,17 +26,21 @@ impl SessionHandler for Server { async fn handle_session_resource( &self, base_url: String, - access_token: Arc, + access_token: &AccessToken, ) -> trc::Result { let mut session = Session::new(base_url, &self.core.jmap.capabilities); session.set_state(access_token.state()); let account_capabilities = &self.core.jmap.capabilities.account; // Set primary account - session.username = access_token.name.to_string(); + let account = self + .account(access_token.account_id()) + .await + .caused_by(trc::location!())?; + session.username = account.name().to_string(); let account_id = Id::from(access_token.account_id()); let mut account = Account { - name: access_token.name.to_string(), + name: account.name().to_string(), is_personal: true, is_read_only: false, account_capabilities: VecMap::with_capacity(account_capabilities.len()), @@ -56,20 +60,11 @@ impl SessionHandler for Server { // Add secondary accounts for &account_id in access_token.secondary_ids() { let is_owner = access_token.is_member(account_id); - let access_token = match self.get_access_token(account_id).await { - Ok(token) => token, - Err(err) => { - if err.matches(trc::EventType::Auth(trc::AuthEvent::Error)) { - continue; - } else { - return Err(err.caused_by(trc::location!())); - } - } - }; + let account = self.account(account_id).await.caused_by(trc::location!())?; let account_id = Id::from(account_id); let mut account = Account { - name: access_token.name.to_string(), + name: account.name().to_string(), is_personal: false, is_read_only: false, account_capabilities: VecMap::with_capacity(account_capabilities.len()), diff --git a/crates/jmap/src/blob/download.rs b/crates/jmap/src/blob/download.rs index 086d40b4..0cc22ab5 100644 --- a/crates/jmap/src/blob/download.rs +++ b/crates/jmap/src/blob/download.rs @@ -9,9 +9,9 @@ use email::cache::MessageCacheFetch; use email::cache::email::MessageCacheAccess; use email::message::metadata::MessageMetadata; use groupware::cache::GroupwareCache; +use std::future::Future; use store::ValueKey; use store::write::{AlignedBytes, Archive}; -use std::future::Future; use trc::AddContext; use types::acl::Acl; use types::blob::{BlobClass, BlobId}; @@ -125,7 +125,7 @@ impl BlobDownload for Server { | Collection::ContactCard | Collection::CalendarEvent) => self .fetch_dav_resources( - access_token, + access_token.account_id(), *account_id, SyncCollection::from(collection), ) diff --git a/crates/jmap/src/calendar/set.rs b/crates/jmap/src/calendar/set.rs index 7823fb01..ee537fcc 100644 --- a/crates/jmap/src/calendar/set.rs +++ b/crates/jmap/src/calendar/set.rs @@ -56,7 +56,11 @@ impl CalendarSet for Server { ) -> trc::Result> { let account_id = request.account_id.document_id(); let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::Calendar) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::Calendar, + ) .await?; let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); @@ -112,7 +116,12 @@ impl CalendarSet for Server { .await .caused_by(trc::location!())?; calendar - .insert(access_token, account_id, document_id, &mut batch) + .insert( + access_token.account_tenant_ids(), + account_id, + document_id, + &mut batch, + ) .caused_by(trc::location!())?; if let Some(MaybeIdReference::Reference(id_ref)) = @@ -188,7 +197,13 @@ impl CalendarSet for Server { // Update record new_calendar - .update(access_token, calendar, account_id, document_id, &mut batch) + .update( + access_token.account_tenant_ids(), + calendar, + account_id, + document_id, + &mut batch, + ) .caused_by(trc::location!())?; response.updated.append(id, None); } @@ -265,7 +280,13 @@ impl CalendarSet for Server { // Delete record DestroyArchive(calendar) - .delete(access_token, account_id, document_id, None, &mut batch) + .delete( + access_token.account_tenant_ids(), + account_id, + document_id, + None, + &mut batch, + ) .caused_by(trc::location!())?; if default_calendar_id == Some(document_id) { @@ -277,6 +298,10 @@ impl CalendarSet for Server { // Delete children if !destroy_children.is_empty() { + let account_info = self + .account_info(access_token.account_id()) + .await + .caused_by(trc::location!())?; for document_id in destroy_children { if let Some(event_) = self .store() @@ -299,7 +324,7 @@ impl CalendarSet for Server { { // Event only belongs to calendars being deleted, delete it DestroyArchive(event).delete_all( - access_token, + &account_info, account_id, document_id, false, @@ -314,7 +339,7 @@ impl CalendarSet for Server { .names .retain(|n| !destroy_parents.contains(&n.parent_id)); new_event.update( - access_token, + access_token.account_tenant_ids(), event, account_id, document_id, diff --git a/crates/jmap/src/calendar_event/copy.rs b/crates/jmap/src/calendar_event/copy.rs index 8d0a8c00..96ccb8e3 100644 --- a/crates/jmap/src/calendar_event/copy.rs +++ b/crates/jmap/src/calendar_event/copy.rs @@ -26,7 +26,11 @@ use jmap_proto::{ }, types::state::State, }; -use store::{ValueKey, roaring::RoaringBitmap, write::{AlignedBytes, Archive, BatchBuilder}}; +use store::{ + ValueKey, + roaring::RoaringBitmap, + write::{AlignedBytes, Archive, BatchBuilder}, +}; use trc::AddContext; use types::{ acl::Acl, @@ -61,7 +65,11 @@ impl JmapCalendarEventCopy for Server { .details("From accountId is equal to fromAccountId")); } let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::Calendar) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::Calendar, + ) .await .caused_by(trc::location!())?; let old_state = cache.assert_state(false, &request.if_in_state)?; @@ -75,7 +83,11 @@ impl JmapCalendarEventCopy for Server { }; let from_cache = self - .fetch_dav_resources(access_token.account_id(), from_account_id, SyncCollection::Calendar) + .fetch_dav_resources( + access_token.account_id(), + from_account_id, + SyncCollection::Calendar, + ) .await .caused_by(trc::location!())?; let from_calendar_event_ids = if access_token.is_member(from_account_id) { @@ -94,7 +106,14 @@ impl JmapCalendarEventCopy for Server { let on_success_delete = request.on_success_destroy_original.unwrap_or(false); let mut destroy_ids = Vec::new(); - // Obtain quota + // Obtain account info + let account_info = self + .account_info(access_token.account_id()) + .await + .caused_by(trc::location!())?; + let account_emails = account_info.addresses().collect::>(); + + // Prepare batch let mut batch = BatchBuilder::new(); 'create: for (id, create) in request.create.into_valid() { @@ -151,6 +170,7 @@ impl JmapCalendarEventCopy for Server { &mut batch, access_token, account_id, + &account_emails, false, &can_add_calendars, calendar_event.data.event.into_jscalendar(), diff --git a/crates/jmap/src/calendar_event/get.rs b/crates/jmap/src/calendar_event/get.rs index 1321ab56..326d3d32 100644 --- a/crates/jmap/src/calendar_event/get.rs +++ b/crates/jmap/src/calendar_event/get.rs @@ -31,7 +31,7 @@ use jmap_proto::{ request::{IntoValid, reference::MaybeResultReference}, }; use jmap_tools::{Key, Map, Value}; -use std::{str::FromStr, sync::Arc}; +use std::{borrow::Cow, str::FromStr}; use store::{ ValueKey, ahash::{AHashMap, AHashSet}, @@ -67,7 +67,11 @@ impl CalendarEventGet for Server { let properties = request.unwrap_properties(&[]); let account_id = request.account_id.document_id(); let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::Calendar) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::Calendar, + ) .await?; let calendar_event_ids = if access_token.is_member(account_id) { cache.document_ids(false).collect::() @@ -153,14 +157,23 @@ impl CalendarEventGet for Server { vec![], ) }; + let current_account_info = self + .account_info(access_token.account_id()) + .await + .caused_by(trc::location!())?; let return_is_origin = if return_is_origin { - if access_token.account_id() == account_id { - OriginAddresses::Ref(access_token) + if account_id == access_token.account_id() { + Some(Cow::Borrowed(¤t_account_info)) } else { - OriginAddresses::Owned(self.get_access_token(account_id).await?) + Some( + self.account_info(account_id) + .await + .map(Cow::Owned) + .caused_by(trc::location!())?, + ) } } else { - OriginAddresses::None + None }; // Sort by baseId @@ -255,9 +268,8 @@ impl CalendarEventGet for Server { ), ) }) || entry.calendar_address().is_some_and(|addr| { - access_token - .emails - .iter() + current_account_info + .addresses() .any(|a| a.eq_ignore_ascii_case(addr)) }) } @@ -464,13 +476,13 @@ impl CalendarEventGet for Server { } for (id, ical, expansion) in results { - let is_origin = return_is_origin.addresses().is_some_and(|addresses| { + let is_origin = return_is_origin.as_ref().is_some_and(|account| { ical.components .iter() .find(|c| c.component_type.is_scheduling_object()) .and_then(|c| c.property(&ICalendarProperty::Organizer)) .and_then(|v| v.calendar_address()) - .is_none_or(|v| addresses.iter().any(|a| a.eq_ignore_ascii_case(v))) + .is_none_or(|v| account.addresses().any(|a| a.eq_ignore_ascii_case(v))) }); let jscal = ical @@ -601,19 +613,3 @@ impl CalendarEventGet for Server { Ok(response) } } - -enum OriginAddresses<'x> { - Owned(Arc), - Ref(&'x AccessToken), - None, -} - -impl<'x> OriginAddresses<'x> { - fn addresses(&self) -> Option<&[String]> { - match self { - OriginAddresses::Owned(t) if !t.emails.is_empty() => Some(&t.emails), - OriginAddresses::Ref(t) if !t.emails.is_empty() => Some(&t.emails), - _ => None, - } - } -} diff --git a/crates/jmap/src/calendar_event/query.rs b/crates/jmap/src/calendar_event/query.rs index ca0cb4d1..9bbc2a7d 100644 --- a/crates/jmap/src/calendar_event/query.rs +++ b/crates/jmap/src/calendar_event/query.rs @@ -17,7 +17,10 @@ use jmap_proto::{ use nlp::language::Language; use std::{cmp::Ordering, sync::Arc}; use store::{ - ValueKey, roaring::RoaringBitmap, search::{CalendarSearchField, SearchComparator, SearchFilter, SearchQuery}, write::{AlignedBytes, Archive, SearchIndex} + ValueKey, + roaring::RoaringBitmap, + search::{CalendarSearchField, SearchComparator, SearchFilter, SearchQuery}, + write::{AlignedBytes, Archive, SearchIndex}, }; use trc::AddContext; use types::{ @@ -43,7 +46,11 @@ impl CalendarEventQuery for Server { let account_id = request.account_id.document_id(); let mut filters = Vec::with_capacity(request.filter.len()); let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::Calendar) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::Calendar, + ) .await?; let default_tz = request.arguments.time_zone.unwrap_or(Tz::UTC); let mut filter: Option = None; @@ -74,7 +81,7 @@ impl CalendarEventQuery for Server { } CalendarEventFilter::Text(value) => { let (text, language) = - Language::detect(value, self.core.jmap.default_language); + Language::detect(value, self.core.email.default_language); filters.push(SearchFilter::Or); filters.push(SearchFilter::has_text( CalendarSearchField::Title, @@ -107,21 +114,21 @@ impl CalendarEventQuery for Server { filters.push(SearchFilter::has_text_detect( CalendarSearchField::Title, title, - self.core.jmap.default_language, + self.core.email.default_language, )); } CalendarEventFilter::Description(description) => { filters.push(SearchFilter::has_text_detect( CalendarSearchField::Description, description, - self.core.jmap.default_language, + self.core.email.default_language, )); } CalendarEventFilter::Location(location) => { filters.push(SearchFilter::has_text_detect( CalendarSearchField::Location, location, - self.core.jmap.default_language, + self.core.email.default_language, )); } CalendarEventFilter::Owner(owner) => { diff --git a/crates/jmap/src/calendar_event/set.rs b/crates/jmap/src/calendar_event/set.rs index f160c4ea..0612ec23 100644 --- a/crates/jmap/src/calendar_event/set.rs +++ b/crates/jmap/src/calendar_event/set.rs @@ -16,7 +16,6 @@ use calcard::{ }; use chrono::DateTime; use common::{DavName, DavResources, Server, auth::AccessToken}; -use registry::schema::enums::Permission; use groupware::{ DestroyArchive, cache::GroupwareCache, @@ -36,6 +35,7 @@ use jmap_proto::{ types::state::State, }; use jmap_tools::{JsonPointerHandler, JsonPointerItem, Key, Map, Value}; +use registry::schema::enums::Permission; use std::{borrow::Cow, str::FromStr}; use store::{ ValueKey, @@ -66,6 +66,7 @@ pub trait CalendarEventSet: Sync + Send { batch: &mut BatchBuilder, access_token: &AccessToken, account_id: u32, + account_emails: &[&str], send_scheduling_messages: bool, can_add_calendars: &Option, js_calendar_event: JSCalendar<'_, Id, BlobId>, @@ -82,8 +83,17 @@ impl CalendarEventSet for Server { ) -> trc::Result> { let account_id = request.account_id.document_id(); let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::Calendar) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::Calendar, + ) .await?; + let account_info = self + .account_info(access_token.account_id()) + .await + .caused_by(trc::location!())?; + let account_emails = account_info.addresses().collect::>(); let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); @@ -115,6 +125,7 @@ impl CalendarEventSet for Server { &mut batch, access_token, account_id, + &account_emails, send_scheduling_messages, &can_add_calendars, JSCalendar::default(), @@ -303,7 +314,7 @@ impl CalendarEventSet for Server { let mut itip_messages = None; if send_scheduling_messages && self.core.groupware.itip_enabled - && !access_token.emails.is_empty() + && !account_emails.is_empty() && access_token.has_permission(Permission::CalendarSchedulingSend) && new_calendar_event.data.event_range_end() > now { @@ -314,12 +325,12 @@ impl CalendarEventSet for Server { itip_update( &mut new_calendar_event.data.event, &old_ical, - access_token.emails.as_slice(), + account_emails.as_slice(), ) } else { itip_create( &mut new_calendar_event.data.event, - access_token.emails.as_slice(), + account_emails.as_slice(), ) }; @@ -381,13 +392,7 @@ impl CalendarEventSet for Server { let extra_bytes = (new_calendar_event.size as u64) .saturating_sub(u32::from(calendar_event.inner.size) as u64); if extra_bytes > 0 { - match self - .has_available_quota( - account_id, - extra_bytes, - ) - .await - { + match self.has_available_quota(account_id, extra_bytes).await { Ok(_) => {} Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => { response.not_updated.append(id, SetError::over_quota()); @@ -400,7 +405,7 @@ impl CalendarEventSet for Server { // Update record new_calendar_event .update( - access_token, + access_token.account_tenant_ids(), calendar_event, account_id, document_id, @@ -479,7 +484,7 @@ impl CalendarEventSet for Server { // Delete event DestroyArchive(calendar_event) .delete_all( - access_token, + &account_info, account_id, document_id, send_scheduling_messages, @@ -511,6 +516,7 @@ impl CalendarEventSet for Server { batch: &mut BatchBuilder, access_token: &AccessToken, account_id: u32, + account_emails: &[&str], send_scheduling_messages: bool, can_add_calendars: &Option, mut js_calendar_group: JSCalendar<'_, Id, BlobId>, @@ -617,11 +623,11 @@ impl CalendarEventSet for Server { let mut itip_messages = None; if send_scheduling_messages && self.core.groupware.itip_enabled - && !access_token.emails.is_empty() + && !account_emails.is_empty() && access_token.has_permission(Permission::CalendarSchedulingSend) && event.data.event_range_end() > now() as i64 { - match itip_create(&mut event.data.event, access_token.emails.as_slice()) { + match itip_create(&mut event.data.event, account_emails) { Ok(messages) => { if messages.iter().map(|r| r.to.len()).sum::() < self.core.groupware.itip_outbound_max_recipients @@ -648,13 +654,7 @@ impl CalendarEventSet for Server { } // Validate quota - match self - .has_available_quota( - account_id, - size as u64, - ) - .await - { + match self.has_available_quota(account_id, size as u64).await { Ok(_) => {} Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => { return Ok(Err(SetError::over_quota())); @@ -670,7 +670,7 @@ impl CalendarEventSet for Server { .caused_by(trc::location!())?; event .insert( - access_token, + access_token.account_tenant_ids(), account_id, document_id, next_email_alarm, diff --git a/crates/jmap/src/calendar_event_notification/get.rs b/crates/jmap/src/calendar_event_notification/get.rs index d2dc6127..4edd15cb 100644 --- a/crates/jmap/src/calendar_event_notification/get.rs +++ b/crates/jmap/src/calendar_event_notification/get.rs @@ -25,7 +25,10 @@ use jmap_proto::{ }, types::date::UTCDate, }; -use store::{ValueKey, write::{AlignedBytes, Archive, serialize::rkyv_deserialize}}; +use store::{ + ValueKey, + write::{AlignedBytes, Archive, serialize::rkyv_deserialize}, +}; use trc::AddContext; use types::{ blob::BlobId, @@ -57,7 +60,7 @@ impl CalendarEventNotificationGet for Server { let account_id = request.account_id.document_id(); let cache = self .fetch_dav_resources( - access_token, + access_token.account_id(), account_id, SyncCollection::CalendarEventNotification, ) @@ -119,9 +122,10 @@ impl CalendarEventNotificationGet for Server { match &event.changed_by { ArchivedChangedBy::PrincipalId(id) => { - if let Ok(token) = self.get_access_token(id.to_native()).await { - changed_by.name = token.description.clone().unwrap_or_default(); - changed_by.email = token.emails.first().cloned(); + if let Ok(account) = self.account(id.to_native()).await { + changed_by.name = + account.description().unwrap_or(account.name()).to_string(); + changed_by.email = account.name().to_string().into(); } changed_by.principal_id = Some(id.to_native().into()); } diff --git a/crates/jmap/src/calendar_event_notification/query.rs b/crates/jmap/src/calendar_event_notification/query.rs index 6fdc4770..bc5ec2f3 100644 --- a/crates/jmap/src/calendar_event_notification/query.rs +++ b/crates/jmap/src/calendar_event_notification/query.rs @@ -52,7 +52,7 @@ impl CalendarEventNotificationQuery for Server { let mut filters = Vec::with_capacity(request.filter.len()); let cache = self .fetch_dav_resources( - access_token, + access_token.account_id(), account_id, SyncCollection::CalendarEventNotification, ) diff --git a/crates/jmap/src/calendar_event_notification/set.rs b/crates/jmap/src/calendar_event_notification/set.rs index 9330021d..e6bdddfe 100644 --- a/crates/jmap/src/calendar_event_notification/set.rs +++ b/crates/jmap/src/calendar_event_notification/set.rs @@ -14,7 +14,10 @@ use jmap_proto::{ request::IntoValid, types::state::State, }; -use store::{ValueKey, write::{AlignedBytes, Archive, BatchBuilder}}; +use store::{ + ValueKey, + write::{AlignedBytes, Archive, BatchBuilder}, +}; use trc::AddContext; use types::collection::{Collection, SyncCollection}; @@ -39,7 +42,7 @@ impl CalendarEventNotificationSet for Server { let account_id = request.account_id.document_id(); let cache = self .fetch_dav_resources( - access_token, + access_token.account_id(), account_id, SyncCollection::CalendarEventNotification, ) @@ -90,7 +93,12 @@ impl CalendarEventNotificationSet for Server { .caused_by(trc::location!())?; DestroyArchive(event) - .delete(access_token, account_id, document_id, &mut batch) + .delete( + access_token.account_tenant_ids(), + account_id, + document_id, + &mut batch, + ) .caused_by(trc::location!())?; response.destroyed.push(id); diff --git a/crates/jmap/src/changes/get.rs b/crates/jmap/src/changes/get.rs index 69c9300b..3cdf9617 100644 --- a/crates/jmap/src/changes/get.rs +++ b/crates/jmap/src/changes/get.rs @@ -112,7 +112,7 @@ impl ChangesLookup for Server { .max_changes .filter(|n| *n != 0) .unwrap_or(usize::MAX), - self.core.jmap.changes_max_results.unwrap_or(usize::MAX), + self.core.jmap.changes_max_results, ); let mut response: ChangesResponse = ChangesResponse { account_id: request.account_id, diff --git a/crates/jmap/src/contact/query.rs b/crates/jmap/src/contact/query.rs index be38c4d0..19cf5939 100644 --- a/crates/jmap/src/contact/query.rs +++ b/crates/jmap/src/contact/query.rs @@ -50,7 +50,11 @@ impl ContactCardQuery for Server { let account_id = request.account_id.document_id(); let mut filters = Vec::with_capacity(request.filter.len()); let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::AddressBook) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::AddressBook, + ) .await?; let mut created_to_updated = Vec::new(); @@ -154,7 +158,7 @@ impl ContactCardQuery for Server { filters.push(SearchFilter::has_text_detect( ContactSearchField::Note, value, - self.core.jmap.default_language, + self.core.email.default_language, )); } ContactCardFilter::HasMember(value) => { @@ -203,7 +207,7 @@ impl ContactCardQuery for Server { filters.push(SearchFilter::has_text_detect( ContactSearchField::Note, value, - self.core.jmap.default_language, + self.core.email.default_language, )); filters.push(SearchFilter::End); } diff --git a/crates/jmap/src/contact/set.rs b/crates/jmap/src/contact/set.rs index d7cf63c8..89587dcd 100644 --- a/crates/jmap/src/contact/set.rs +++ b/crates/jmap/src/contact/set.rs @@ -61,7 +61,11 @@ impl ContactCardSet for Server { ) -> trc::Result> { let account_id = request.account_id.document_id(); let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::AddressBook) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::AddressBook, + ) .await?; let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); @@ -252,13 +256,7 @@ impl ContactCardSet for Server { let extra_bytes = (new_contact_card.size as u64) .saturating_sub(u32::from(contact_card.inner.size) as u64); if extra_bytes > 0 { - match self - .has_available_quota( - account_id, - extra_bytes, - ) - .await - { + match self.has_available_quota(account_id, extra_bytes).await { Ok(_) => {} Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => { response.not_updated.append(id, SetError::over_quota()); @@ -271,7 +269,7 @@ impl ContactCardSet for Server { // Update record new_contact_card .update( - access_token, + access_token.account_tenant_ids(), contact_card, account_id, document_id, @@ -327,7 +325,12 @@ impl ContactCardSet for Server { // Delete record DestroyArchive(contact_card) - .delete_all(access_token, account_id, document_id, &mut batch) + .delete_all( + access_token.account_tenant_ids(), + account_id, + document_id, + &mut batch, + ) .caused_by(trc::location!())?; response.destroyed.push(id); @@ -406,13 +409,7 @@ impl ContactCardSet for Server { ), ))); } - match self - .has_available_quota( - account_id, - size as u64, - ) - .await - { + match self.has_available_quota(account_id, size as u64).await { Ok(_) => {} Err(err) if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) => { return Ok(Err(SetError::over_quota())); @@ -432,7 +429,12 @@ impl ContactCardSet for Server { card, ..Default::default() } - .insert(access_token, account_id, document_id, batch) + .insert( + access_token.account_tenant_ids(), + account_id, + document_id, + batch, + ) .caused_by(trc::location!()) .map(|_| Ok(document_id)) } diff --git a/crates/jmap/src/email/copy.rs b/crates/jmap/src/email/copy.rs index 502f2765..6b923c12 100644 --- a/crates/jmap/src/email/copy.rs +++ b/crates/jmap/src/email/copy.rs @@ -88,9 +88,6 @@ impl JmapEmailCopy for Server { let on_success_delete = request.on_success_destroy_original.unwrap_or(false); let mut destroy_ids = Vec::new(); - // Obtain quota - let resource_token = self.get_resource_token(access_token, account_id).await?; - 'create: for (id, create) in request.create.into_valid() { let from_message_id = id.document_id(); if !from_message_ids.contains(from_message_id) { @@ -203,7 +200,7 @@ impl JmapEmailCopy for Server { .copy_message( from_account_id, from_message_id, - &resource_token, + account_id, mailboxes, keywords, received_at.map(|dt| dt.timestamp() as u64), diff --git a/crates/jmap/src/email/import.rs b/crates/jmap/src/email/import.rs index e6332e89..be0e6790 100644 --- a/crates/jmap/src/email/import.rs +++ b/crates/jmap/src/email/import.rs @@ -56,15 +56,17 @@ impl EmailImport for Server { let import_access_token = if account_id != access_token.account_id() { #[cfg(feature = "test_mode")] { - std::sync::Arc::new(AccessToken::from_id(account_id)).into() + AccessToken::from_id(account_id).into() } #[cfg(not(feature = "test_mode"))] { + use common::auth::BuildAccessToken; use trc::AddContext; - self.get_access_token(account_id) + self.access_token(account_id) .await .caused_by(trc::location!())? + .build() .into() } } else { @@ -149,7 +151,7 @@ impl EmailImport for Server { raw_message: &raw_message, message: MessageParser::new().parse(&raw_message), blob_hash: Some(&blob_id.hash), - access_token: import_access_token.as_deref().unwrap_or(access_token), + access_token: import_access_token.as_ref().unwrap_or(access_token), source: IngestSource::Jmap { train_classifier: email .keywords diff --git a/crates/jmap/src/email/query.rs b/crates/jmap/src/email/query.rs index 03b19aeb..faf9a615 100644 --- a/crates/jmap/src/email/query.rs +++ b/crates/jmap/src/email/query.rs @@ -52,7 +52,7 @@ impl EmailQuery for Server { Filter::Property(cond) => match cond { EmailFilter::Text(text) => { let (text, language) = - Language::detect(text, self.core.jmap.default_language); + Language::detect(text, self.core.email.default_language); filters.push(SearchFilter::Or); filters.push(SearchFilter::has_text( @@ -115,12 +115,12 @@ impl EmailQuery for Server { EmailFilter::Subject(text) => filters.push(SearchFilter::has_text_detect( EmailSearchField::Subject, text, - self.core.jmap.default_language, + self.core.email.default_language, )), EmailFilter::Body(text) => filters.push(SearchFilter::has_text_detect( EmailSearchField::Body, text, - self.core.jmap.default_language, + self.core.email.default_language, )), EmailFilter::Header(header) => { let mut header = header.into_iter(); diff --git a/crates/jmap/src/email/set.rs b/crates/jmap/src/email/set.rs index 496a71b6..9e28b5c4 100644 --- a/crates/jmap/src/email/set.rs +++ b/crates/jmap/src/email/set.rs @@ -613,9 +613,9 @@ impl EmailSet for Server { // Check attachment sizes if !is_multipart { size_attachments += parts.last().unwrap().size(); - if self.core.jmap.mail_attachments_max_size > 0 + if self.core.email.mail_attachments_max_size > 0 && size_attachments - > self.core.jmap.mail_attachments_max_size + > self.core.email.mail_attachments_max_size { response.not_created.append( id, @@ -623,7 +623,7 @@ impl EmailSet for Server { .with_property(property) .with_description(format!( "Message exceeds maximum size of {} bytes.", - self.core.jmap.mail_attachments_max_size + self.core.email.mail_attachments_max_size )), ); continue 'create; diff --git a/crates/jmap/src/email/snippet.rs b/crates/jmap/src/email/snippet.rs index 0d564183..3628653a 100644 --- a/crates/jmap/src/email/snippet.rs +++ b/crates/jmap/src/email/snippet.rs @@ -49,7 +49,7 @@ impl EmailSearchSnippet for Server { let mut include_term = true; let mut terms = vec![]; let mut is_exact = false; - let mut language = self.core.jmap.default_language; + let mut language = self.core.email.default_language; for cond in request.filter { match cond { @@ -60,7 +60,7 @@ impl EmailSearchSnippet for Server { && include_term { let (text, language_) = - Language::detect(text, self.core.jmap.default_language); + Language::detect(text, self.core.email.default_language); language = language_; if (text.starts_with('"') && text.ends_with('"')) || (text.starts_with('\'') && text.ends_with('\'')) diff --git a/crates/jmap/src/file/set.rs b/crates/jmap/src/file/set.rs index e93957b5..d89cea45 100644 --- a/crates/jmap/src/file/set.rs +++ b/crates/jmap/src/file/set.rs @@ -51,7 +51,11 @@ impl FileNodeSet for Server { ) -> trc::Result> { let account_id = request.account_id.document_id(); let cache = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::FileNode) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::FileNode, + ) .await?; let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); @@ -172,7 +176,12 @@ impl FileNodeSet for Server { created_folders.insert(document_id, file_node.acls.clone()); } file_node - .insert(access_token, account_id, document_id, &mut batch) + .insert( + access_token.account_tenant_ids(), + account_id, + document_id, + &mut batch, + ) .caused_by(trc::location!())?; response.created(id, document_id); } @@ -295,7 +304,13 @@ impl FileNodeSet for Server { // Update record new_file_node - .update(access_token, file_node, account_id, document_id, &mut batch) + .update( + access_token.account_tenant_ids(), + file_node, + account_id, + document_id, + &mut batch, + ) .caused_by(trc::location!())?; response.updated.append(id, None); } @@ -370,7 +385,7 @@ impl FileNodeSet for Server { DestroyArchive(sorted_ids) .delete_batch( self, - access_token, + access_token.account_tenant_ids(), account_id, cache.format_resource(file_node).into(), &mut batch, diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index 2b3c0e0a..952f9164 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -6,7 +6,6 @@ use crate::changes::state::StateManager; use common::{Server, storage::index::ObjectIndexBuilder}; -use directory::{PrincipalData, QueryParams}; use email::identity::{ArchivedEmailAddress, Identity}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, @@ -15,7 +14,10 @@ use jmap_proto::{ use jmap_tools::{Map, Value}; use std::future::Future; use store::{ - ValueKey, rkyv::{option::ArchivedOption, vec::ArchivedVec}, roaring::RoaringBitmap, write::{AlignedBytes, Archive, BatchBuilder} + ValueKey, + rkyv::{option::ArchivedOption, vec::ArchivedVec}, + roaring::RoaringBitmap, + write::{AlignedBytes, Archive, BatchBuilder}, }; use trc::AddContext; use types::{ @@ -154,34 +156,11 @@ impl IdentityGet for Server { return Ok(identity_ids); } - // Obtain principal - let principal = if let Some(principal) = self - .core - .storage - .directory - .query(QueryParams::id(account_id).with_return_member_of(false)) + // Obtain account info + let account = self + .account_info(account_id) .await - .caused_by(trc::location!())? - { - principal - } else { - return Ok(identity_ids); - }; - - let mut emails = Vec::new(); - let mut description = None; - for data in principal.data { - match data { - PrincipalData::PrimaryEmail(v) | PrincipalData::EmailAlias(v) => emails.push(v), - PrincipalData::Description(v) => description = Some(v), - _ => {} - } - } - - let num_emails = emails.len(); - if num_emails == 0 { - return Ok(identity_ids); - } + .caused_by(trc::location!())?; let mut batch = BatchBuilder::new(); batch @@ -189,10 +168,11 @@ impl IdentityGet for Server { .with_collection(Collection::Identity); // Create identities - let name = description.unwrap_or(principal.name); + let name = account.description().unwrap_or(account.name()); + let emails = account.addresses().collect::>(); let mut next_document_id = self .store() - .assign_document_ids(account_id, Collection::Identity, num_emails as u64) + .assign_document_ids(account_id, Collection::Identity, emails.len() as u64) .await .caused_by(trc::location!())?; for email in &emails { @@ -201,9 +181,9 @@ impl IdentityGet for Server { continue; } let name = if name.is_empty() { - email.clone() + email.to_string() } else { - name.clone() + name.to_string() }; let document_id = next_document_id; next_document_id -= 1; diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 17e191bf..90b6c4e4 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -4,8 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; -use directory::QueryParams; +use common::{Server, storage::index::ObjectIndexBuilder}; use email::identity::{EmailAddress, Identity}; use jmap_proto::{ error::set::{SetError, SetErrorType}, @@ -16,8 +15,12 @@ use jmap_proto::{ types::state::State, }; use jmap_tools::{Key, Value}; +use registry::schema::enums::StorageQuota; use std::future::Future; -use store::{ValueKey, write::{AlignedBytes, Archive, BatchBuilder}}; +use store::{ + ValueKey, + write::{AlignedBytes, Archive, BatchBuilder}, +}; use trc::AddContext; use types::{ collection::{Collection, SyncCollection}, @@ -29,7 +32,6 @@ pub trait IdentitySet: Sync + Send { fn identity_set( &self, request: SetRequest<'_, identity::Identity>, - access_token: &AccessToken, ) -> impl Future>> + Send; } @@ -37,7 +39,6 @@ impl IdentitySet for Server { async fn identity_set( &self, mut request: SetRequest<'_, identity::Identity>, - access_token: &AccessToken, ) -> trc::Result> { let account_id = request.account_id.document_id(); let identity_ids = self @@ -45,6 +46,10 @@ impl IdentitySet for Server { .await?; let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); + let account_info = self + .account_info(account_id) + .await + .caused_by(trc::location!())?; // Process creates let mut batch = BatchBuilder::new(); @@ -63,12 +68,7 @@ impl IdentitySet for Server { // Validate email address if !identity.email.is_empty() { - if self - .directory() - .query(QueryParams::id(account_id).with_return_member_of(false)) - .await? - .is_none_or(|p| !p.email_addresses().any(|e| e == identity.email)) - { + if !account_info.addresses().any(|e| e == identity.email) { response.not_created.append( id, SetError::invalid_properties() @@ -90,7 +90,12 @@ impl IdentitySet for Server { } // Validate quota - if identity_ids.len() >= access_token.object_quota(Collection::Identity) as u64 { + if identity_ids.len() + >= self.object_quota( + account_info.object_quotas(), + StorageQuota::MaxEmailIdentities, + ) as u64 + { response.not_created.append( id, SetError::new(SetErrorType::OverQuota).with_description(concat!( diff --git a/crates/jmap/src/mailbox/query.rs b/crates/jmap/src/mailbox/query.rs index b0c28acc..0af6b77a 100644 --- a/crates/jmap/src/mailbox/query.rs +++ b/crates/jmap/src/mailbox/query.rs @@ -120,7 +120,7 @@ impl MailboxQuery for Server { .items .iter() .filter(|mailbox| { - mailbox.subscribers.contains(&access_token.account_id) + mailbox.subscribers.contains(&access_token.account_id()) == is_subscribed }) .map(|m| m.document_id) @@ -247,7 +247,7 @@ impl MailboxQuery for Server { for document_id in results.results() { let mut check_id = document_id; - for _ in 0..self.core.jmap.mailbox_max_depth { + for _ in 0..self.core.email.mailbox_max_depth { if let Some(mailbox) = mailboxes.mailbox_by_id(&check_id) { if let Some(parent_id) = mailbox.parent_id() { if results.results().contains(parent_id) { diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index 052a00b0..c480fcab 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -29,6 +29,7 @@ use jmap_proto::{ types::state::State, }; use jmap_tools::{JsonPointerItem, Key, Map, Value}; +use registry::schema::enums::StorageQuota; use std::future::Future; use store::{ ValueKey, @@ -89,6 +90,7 @@ impl MailboxSet for Server { will_destroy: request.unwrap_destroy().into_valid().collect(), }; let mut change_id = None; + let account_info = self.account_info(account_id).await?; // Process creates let mut batch = BatchBuilder::new(); @@ -98,7 +100,10 @@ impl MailboxSet for Server { }; // Validate quota - if ctx.mailbox_ids.len() >= access_token.object_quota(Collection::Mailbox) as u64 { + if ctx.mailbox_ids.len() + >= self.object_quota(account_info.object_quotas(), StorageQuota::MaxMailboxes) + as u64 + { ctx.response.not_created.append( id, SetError::new(SetErrorType::OverQuota).with_description(concat!( @@ -341,7 +346,7 @@ impl MailboxSet for Server { match (&property, value) { (Key::Property(MailboxProperty::Name), Value::Str(value)) => { let value = value.trim(); - if !value.is_empty() && value.len() < self.core.jmap.mailbox_name_max_len { + if !value.is_empty() && value.len() < self.core.email.mailbox_name_max_len { changes.name = value.into(); } else { return Ok(Err(SetError::invalid_properties() @@ -448,7 +453,7 @@ impl MailboxSet for Server { .as_ref() .map_or(u32::MAX, |(mailbox_id, _)| *mailbox_id + 1); let mut success = false; - for depth in 0..self.core.jmap.mailbox_max_depth { + for depth in 0..self.core.email.mailbox_max_depth { if mailbox_parent_id == current_mailbox_id { return Ok(Err(SetError::invalid_properties() .with_property(MailboxProperty::ParentId) diff --git a/crates/jmap/src/participant_identity/get.rs b/crates/jmap/src/participant_identity/get.rs index faba4b10..927618ee 100644 --- a/crates/jmap/src/participant_identity/get.rs +++ b/crates/jmap/src/participant_identity/get.rs @@ -5,7 +5,6 @@ */ use common::Server; -use directory::{PrincipalData, QueryParams}; use groupware::calendar::{ParticipantIdentities, ParticipantIdentity}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, @@ -128,32 +127,13 @@ impl ParticipantIdentityGet for Server { return Ok(Some(identities)); } - // Obtain principal - let principal = if let Some(principal) = self - .core - .storage - .directory - .query(QueryParams::id(account_id).with_return_member_of(false)) + // Obtain account info + let account = self + .account_info(account_id) .await - .caused_by(trc::location!())? - { - principal - } else { - return Ok(None); - }; - let mut emails = Vec::new(); - let mut description = None; - for data in principal.data { - match data { - PrincipalData::PrimaryEmail(v) | PrincipalData::EmailAlias(v) => emails.push(v), - PrincipalData::Description(v) => description = Some(v), - _ => {} - } - } - let num_emails = emails.len(); - if num_emails == 0 { - return Ok(None); - } + .caused_by(trc::location!())?; + let name = account.description().unwrap_or(account.name()); + let emails = account.addresses().collect::>(); // Build identities let identities = ParticipantIdentities { @@ -167,7 +147,7 @@ impl ParticipantIdentityGet for Server { }) .collect(), default: 0, - default_name: description.unwrap_or(principal.name), + default_name: name.to_string(), }; let mut batch = BatchBuilder::new(); diff --git a/crates/jmap/src/participant_identity/set.rs b/crates/jmap/src/participant_identity/set.rs index bfe6a530..7a8e32eb 100644 --- a/crates/jmap/src/participant_identity/set.rs +++ b/crates/jmap/src/participant_identity/set.rs @@ -5,8 +5,7 @@ */ use crate::participant_identity::get::ParticipantIdentityGet; -use common::{Server, auth::AccessToken}; -use directory::QueryParams; +use common::Server; use groupware::calendar::{ParticipantIdentities, ParticipantIdentity}; use jmap_proto::{ error::set::{SetError, SetErrorType}, @@ -15,6 +14,7 @@ use jmap_proto::{ request::{IntoValid, reference::MaybeIdReference}, }; use jmap_tools::{Key, Value}; +use registry::schema::prelude::StorageQuota; use store::{ Serialize, ahash::AHashSet, @@ -28,7 +28,6 @@ pub trait ParticipantIdentitySet: Sync + Send { fn participant_identity_set( &self, request: SetRequest<'_, participant_identity::ParticipantIdentity>, - access_token: &AccessToken, ) -> impl Future>> + Send; } @@ -36,7 +35,6 @@ impl ParticipantIdentitySet for Server { async fn participant_identity_set( &self, mut request: SetRequest<'_, participant_identity::ParticipantIdentity>, - access_token: &AccessToken, ) -> trc::Result> { let account_id = request.account_id.document_id(); let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; @@ -53,13 +51,13 @@ impl ParticipantIdentitySet for Server { None => (None, ParticipantIdentities::default()), }; + let account_info = self + .account_info(account_id) + .await + .caused_by(trc::location!())?; + // Obtain allowed emails - let allowed_emails = self - .directory() - .query(QueryParams::id(account_id).with_return_member_of(false)) - .await? - .map(|p| p.into_email_addresses().collect::>()) - .unwrap_or_default(); + let allowed_emails = account_info.addresses().collect::>(); // Process creates let mut has_changes = false; @@ -87,7 +85,10 @@ impl ParticipantIdentitySet for Server { // Validate quota if identities.identities.len() - >= access_token.object_quota(Collection::Identity) as usize + >= self.object_quota( + account_info.object_quotas(), + StorageQuota::MaxParticipantIdentities, + ) as usize { response.not_created.append( id, @@ -197,7 +198,7 @@ impl ParticipantIdentitySet for Server { fn validate_identity_value( update: Value<'_, ParticipantIdentityProperty, ParticipantIdentityValue>, identity: &mut ParticipantIdentity, - allowed_emails: &AHashSet, + allowed_emails: &AHashSet<&str>, ) -> Result<(), SetError> { for (property, value) in update.into_expanded_object() { let Key::Property(property) = property else { diff --git a/crates/jmap/src/principal/availability.rs b/crates/jmap/src/principal/availability.rs index 4fbc35ba..8c29f865 100644 --- a/crates/jmap/src/principal/availability.rs +++ b/crates/jmap/src/principal/availability.rs @@ -14,8 +14,10 @@ use calcard::{ }, jscalendar::{JSCalendar, JSCalendarProperty, JSCalendarValue}, }; -use common::{Server, TinyCalendarPreferences, auth::AccessToken}; -use registry::schema::enums::Permission; +use common::{ + Server, TinyCalendarPreferences, + auth::{AccessToken, BuildAccessToken}, +}; use groupware::{ cache::GroupwareCache, calendar::{CALENDAR_SUBSCRIBED, CalendarEvent}, @@ -29,8 +31,13 @@ use jmap_proto::{ types::date::UTCDate, }; use jmap_tools::{Key, Map, Value}; +use registry::schema::enums::Permission; use std::{collections::hash_map::Entry, future::Future}; -use store::{ValueKey, ahash::AHashMap, write::{AlignedBytes, Archive}}; +use store::{ + ValueKey, + ahash::AHashMap, + write::{AlignedBytes, Archive}, +}; use trc::AddContext; use types::{ TimeRange, @@ -88,14 +95,23 @@ impl PrincipalGetAvailability for Server { }; let principal_id = request.id.document_id(); let principal = self - .get_access_token(principal_id) + .access_token(principal_id) + .await + .caused_by(trc::location!())? + .build(); + let principal_account = self + .account_info(principal_id) .await .caused_by(trc::location!())?; let mut periods = Vec::new(); for account_id in principal.all_ids_by_collection(Collection::Calendar) { let resources = self - .fetch_dav_resources(access_token.account_id(), account_id, SyncCollection::Calendar) + .fetch_dav_resources( + access_token.account_id(), + account_id, + SyncCollection::Calendar, + ) .await .caused_by(trc::location!())?; @@ -247,7 +263,7 @@ impl PrincipalGetAvailability for Server { ) }) { // Condition: the Principal is a participant of the event, and has a "participationStatus" of "accepted" or "tentative". - if principal.emails.contains(&attendee) { + if principal_account.addresses().any(|e| e == attendee) { busy_status = Some( entry .parameters(&ICalendarParameterName::Partstat) diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index f54529fb..67a57eb5 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -5,7 +5,6 @@ */ use common::{Server, auth::AccessToken}; -use directory::{Permission, QueryParams, Type, backend::internal::manage::ManageDirectory}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::principal::{Principal, PrincipalProperty, PrincipalType, PrincipalValue}, @@ -13,8 +12,9 @@ use jmap_proto::{ types::state::State, }; use jmap_tools::{Key, Map, Value}; +use registry::schema::prelude::{Object, Permission, Property}; use std::future::Future; -use store::roaring::RoaringBitmap; +use store::{registry::RegistryQuery, roaring::RoaringBitmap}; use trc::AddContext; pub trait PrincipalGet: Sync + Send { @@ -50,26 +50,13 @@ impl PrincipalGet for Server { // Return all principals let principal_ids = self - .store() - .list_principals( - None, - access_token.tenant_id(), - &[ - Type::Individual, - Type::Group, - Type::Resource, - Type::Location, - ], - false, - 0, - 0, + .registry() + .query::( + RegistryQuery::new(Object::Account) + .equal_opt(Property::MemberTenantId, access_token.tenant_id()), ) .await - .caused_by(trc::location!())? - .items - .into_iter() - .map(|p| p.id()) - .collect::(); + .caused_by(trc::location!())?; let ids = if let Some(ids) = ids { ids @@ -90,31 +77,24 @@ impl PrincipalGet for Server { for id in ids { // Obtain the principal let document_id = id.document_id(); - let principal = if principal_ids.contains(document_id) - && let Some(principal) = self - .core - .storage - .directory - .query(QueryParams::id(document_id).with_return_member_of(false)) - .await? - { - principal - } else { + if !principal_ids.contains(document_id) { response.not_found.push(id); continue; }; + let principal = self + .account_info(document_id) + .await + .caused_by(trc::location!())?; let mut result = Map::with_capacity(properties.len()); for property in &properties { let value = match property { PrincipalProperty::Id => Value::Element(PrincipalValue::Id(id)), PrincipalProperty::Type => { - Value::Element(PrincipalValue::Type(match principal.typ() { - Type::Individual => PrincipalType::Individual, - Type::Group => PrincipalType::Group, - Type::Resource => PrincipalType::Resource, - Type::Location => PrincipalType::Location, - _ => PrincipalType::Other, + Value::Element(PrincipalValue::Type(if principal.is_user_account() { + PrincipalType::Individual + } else { + PrincipalType::Group })) } PrincipalProperty::Name => Value::Str(principal.name().to_string().into()), @@ -122,10 +102,7 @@ impl PrincipalGet for Server { .description() .map(|v| Value::Str(v.to_string().into())) .unwrap_or(Value::Null), - PrincipalProperty::Email => principal - .primary_email() - .map(|email| Value::Str(email.to_string().into())) - .unwrap_or(Value::Null), + PrincipalProperty::Email => Value::Str(principal.name().to_string().into()), PrincipalProperty::Accounts => Value::Object(Map::from(vec![( Key::Property(PrincipalProperty::IdValue(id)), Value::Object(Map::from_iter( @@ -173,11 +150,7 @@ impl PrincipalGet for Server { ( Key::Borrowed("calendarAddress"), Value::Str( - principal - .primary_email() - .map(|email| format!("mailto:{}", email)) - .unwrap_or_default() - .into(), + format!("mailto:{}", principal.name()).into(), ), ), ])), diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index 2e252602..df946e21 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -6,15 +6,16 @@ use crate::api::query::QueryResponseBuilder; use common::{Server, auth::AccessToken}; -use directory::{Permission, QueryParams, Type, backend::internal::manage::ManageDirectory}; use http_proto::HttpSessionData; use jmap_proto::{ method::query::{Filter, QueryRequest, QueryResponse}, object::principal::{Principal, PrincipalFilter, PrincipalType}, types::state::State, }; +use registry::schema::prelude::{Object, Permission, Property}; use std::future::Future; use store::{ + registry::RegistryQuery, roaring::RoaringBitmap, search::{SearchFilter, SearchQuery}, write::SearchIndex, @@ -46,118 +47,85 @@ impl PrincipalQuery for Server { } let principal_ids = self - .store() - .list_principals( - None, - access_token.tenant_id(), - &[ - Type::Individual, - Type::Group, - Type::Resource, - Type::Location, - ], - false, - 0, - 0, + .registry() + .query::( + RegistryQuery::new(Object::Account) + .equal_opt(Property::MemberTenantId, access_token.tenant_id()), ) .await - .caused_by(trc::location!())? - .items - .into_iter() - .map(|p| p.id()) - .collect::(); + .caused_by(trc::location!())?; let mut filters = Vec::with_capacity(request.filter.len()); for cond in std::mem::take(&mut request.filter) { match cond { - Filter::Property(cond) => match cond { - PrincipalFilter::Name(name) => { - if let Some(principal) = self - .core - .storage - .directory - .query(QueryParams::name(name.as_str()).with_return_member_of(false)) - .await? - { + Filter::Property(cond) => { + match cond { + PrincipalFilter::Name(name) | PrincipalFilter::Email(name) => { + if let Some(account_id) = self.account_id(&name).await? { + filters.push(SearchFilter::is_in_set( + RoaringBitmap::from_sorted_iter([account_id]).unwrap(), + )); + } + } + PrincipalFilter::AccountIds(ids) => { filters.push(SearchFilter::is_in_set( - RoaringBitmap::from_sorted_iter([principal.id()]).unwrap(), + ids.into_iter() + .filter_map(|id| { + let id = id.document_id(); + if principal_ids.contains(id) { + Some(id) + } else { + None + } + }) + .collect::(), )); } - } - PrincipalFilter::Email(email) => { - if let Some(id) = self - .email_to_id(self.directory(), &email, session.session_id) - .await? - { + PrincipalFilter::Text(text) => { filters.push(SearchFilter::is_in_set( - RoaringBitmap::from_sorted_iter([id]).unwrap(), + self.registry() + .query::( + RegistryQuery::new(Object::Account) + .equal_opt( + Property::MemberTenantId, + access_token.tenant_id(), + ) + .text(text), + ) + .await + .caused_by(trc::location!())?, )); } - } - PrincipalFilter::AccountIds(ids) => { - filters.push(SearchFilter::is_in_set( - ids.into_iter() - .filter_map(|id| { - let id = id.document_id(); - if principal_ids.contains(id) { - Some(id) - } else { - None - } - }) - .collect::(), - )); - } - PrincipalFilter::Text(text) => { - filters.push(SearchFilter::is_in_set( - self.store() - .list_principals( - Some(text.as_str()), - access_token.tenant.map(|t| t.id), - &[], - false, - 0, - 0, - ) - .await? - .items - .into_iter() - .map(|p| p.id()) - .collect::(), - )); - } - PrincipalFilter::Type(principal_type) => { - let typ = match principal_type { - PrincipalType::Individual => Type::Individual, - PrincipalType::Group => Type::Group, - PrincipalType::Resource => Type::Resource, - PrincipalType::Location => Type::Location, - PrincipalType::Other => Type::Other, - }; + PrincipalFilter::Type(principal_type) => { + let todo = "make sure this works"; + let typ = match principal_type { + PrincipalType::Individual => Object::UserAccount, + PrincipalType::Group => Object::GroupAccount, + PrincipalType::Resource + | PrincipalType::Location + | PrincipalType::Other => { + filters.push(SearchFilter::is_in_set(Default::default())); + continue; + } + }; - filters.push(SearchFilter::is_in_set( - self.store() - .list_principals( - None, - access_token.tenant.map(|t| t.id), - &[typ], - false, - 0, - 0, - ) - .await? - .items - .into_iter() - .map(|p| p.id()) - .collect::(), - )); + filters.push(SearchFilter::is_in_set( + self.registry() + .query::(RegistryQuery::new(typ).equal_opt( + Property::MemberTenantId, + access_token.tenant_id(), + )) + .await + .caused_by(trc::location!())?, + )); + } + other => { + return Err(trc::JmapEvent::UnsupportedFilter + .into_err() + .details(other.to_string())); + } } - other => { - return Err(trc::JmapEvent::UnsupportedFilter - .into_err() - .details(other.to_string())); - } - }, + } Filter::And => { filters.push(SearchFilter::And); } diff --git a/crates/jmap/src/push/set.rs b/crates/jmap/src/push/set.rs index 10a75738..4c953592 100644 --- a/crates/jmap/src/push/set.rs +++ b/crates/jmap/src/push/set.rs @@ -17,6 +17,7 @@ use jmap_proto::{ }; use jmap_tools::{Key, Map, Value}; use rand::distr::Alphanumeric; +use registry::schema::enums::StorageQuota; use std::future::Future; use store::{ Serialize, ValueKey, @@ -76,13 +77,15 @@ impl PushSubscriptionSet for Server { // Prepare response let mut response = SetResponse::from_request(&request, self.core.jmap.set_max_objects)?; let will_destroy = request.unwrap_destroy().into_valid().collect::>(); + let account = self.account(account_id).await.caused_by(trc::location!())?; // Process creates 'create: for (id, object) in request.unwrap_create() { let mut push = PushSubscription::default(); if subscriptions.subscriptions.len() - >= access_token.object_quota(Collection::PushSubscription) as usize + >= self.object_quota(account.object_quotas(), StorageQuota::MaxPushSubscriptions) + as usize { response.not_created.append(id, SetError::new(SetErrorType::OverQuota).with_description( "There are too many subscriptions, please delete some before adding a new one.", diff --git a/crates/jmap/src/quota/get.rs b/crates/jmap/src/quota/get.rs index e41c4936..475292f7 100644 --- a/crates/jmap/src/quota/get.rs +++ b/crates/jmap/src/quota/get.rs @@ -11,7 +11,7 @@ use jmap_proto::{ types::state::State, }; use jmap_tools::{Map, Value}; -use std::{future::Future, sync::Arc}; +use std::{borrow::Cow, future::Future}; use trc::AddContext; use types::{id::Id, type_state::DataType}; @@ -43,7 +43,8 @@ impl QuotaGet for Server { QuotaProperty::Types, ]); let account_id = request.account_id.document_id(); - let quota_ids = if access_token.quota > 0 { + let account = self.account(account_id).await.caused_by(trc::location!())?; + let quota_ids = if account.disk_quota() > 0 { vec![0u32] } else { vec![] @@ -60,14 +61,10 @@ impl QuotaGet for Server { not_found: vec![], }; - let access_token = if account_id == access_token.account_id() { - AccessTokenRef::Borrowed(access_token) + let account = if account_id == access_token.account_id() { + Cow::Borrowed(&account) } else { - AccessTokenRef::Owned( - self.get_access_token(account_id) - .await - .caused_by(trc::location!())?, - ) + Cow::Owned(self.account(account_id).await.caused_by(trc::location!())?) }; for id in ids { @@ -83,11 +80,13 @@ impl QuotaGet for Server { let value = match property { QuotaProperty::Id => Value::Element(id.into()), QuotaProperty::ResourceType => "octets".to_string().into(), - QuotaProperty::Used => (self.get_used_quota(account_id).await? as u64).into(), - QuotaProperty::HardLimit => access_token.as_ref().quota.into(), + QuotaProperty::Used => { + (self.get_used_quota_account(account_id).await? as u64).into() + } + QuotaProperty::HardLimit => account.as_ref().disk_quota().into(), QuotaProperty::Scope => "account".to_string().into(), - QuotaProperty::Name => access_token.as_ref().name.to_string().into(), - QuotaProperty::Description => access_token + QuotaProperty::Name => account.as_ref().name().to_string().into(), + QuotaProperty::Description => account .as_ref() .description .as_ref() @@ -112,17 +111,3 @@ impl QuotaGet for Server { Ok(response) } } - -enum AccessTokenRef<'x> { - Owned(Arc), - Borrowed(&'x AccessToken), -} - -impl AccessTokenRef<'_> { - fn as_ref(&self) -> &AccessToken { - match self { - AccessTokenRef::Owned(token) => token, - AccessTokenRef::Borrowed(token) => token, - } - } -} diff --git a/crates/jmap/src/quota/query.rs b/crates/jmap/src/quota/query.rs index bdf5f0bc..ab137c12 100644 --- a/crates/jmap/src/quota/query.rs +++ b/crates/jmap/src/quota/query.rs @@ -32,7 +32,7 @@ impl QuotaQuery for Server { query_state: State::Initial, can_calculate_changes: false, position: 0, - ids: if access_token.quota > 0 { + ids: if self.account(access_token.account_id()).await?.disk_quota() > 0 { vec![Id::new(0)] } else { vec![] diff --git a/crates/jmap/src/share_notification/get.rs b/crates/jmap/src/share_notification/get.rs index a8e10f98..aad7f98e 100644 --- a/crates/jmap/src/share_notification/get.rs +++ b/crates/jmap/src/share_notification/get.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken, sharing::notification::ShareNotification}; +use common::{Server, auth::AccountInfo, sharing::notification::ShareNotification}; use jmap_proto::{ method::get::{GetRequest, GetResponse}, object::{ @@ -19,7 +19,7 @@ use jmap_proto::{ types::{date::UTCDate, state::State}, }; use jmap_tools::{Key, Map, Value}; -use std::{sync::Arc, time::Duration}; +use std::time::Duration; use store::{ Deserialize, IterateParams, LogKey, U64_LEN, ahash::{AHashMap, AHashSet}, @@ -63,7 +63,7 @@ impl ShareNotificationGet for Server { let mut min_id = u64::MAX; let mut max_id = 0u64; - let mut token_cache: AHashMap> = AHashMap::new(); + let mut account_cache: AHashMap = AHashMap::new(); let mut ids = if let Some(ids) = request.ids.take() { let ids = ids.unwrap(); @@ -91,7 +91,7 @@ impl ShareNotificationGet for Server { if min_id == u64::MAX { min_id = SnowflakeIdGenerator::from_duration( self.core - .jmap + .email .share_notification_max_history .unwrap_or(Duration::from_secs(30 * 86400)), ) @@ -146,24 +146,25 @@ impl ShareNotificationGet for Server { .caused_by(trc::location!())?; for (change_id, notification) in notifications { - let changed_by_token = if let Some(token) = token_cache.get(¬ification.changed_by) { - token.clone() - } else { - let token = if let Ok(token) = self.get_access_token(notification.changed_by).await - { - token + let changed_by_account = + if let Some(account) = account_cache.get(¬ification.changed_by) { + account.clone() } else { - Arc::new(AccessToken::from_id(notification.changed_by)) - }; + let account = + if let Ok(account) = self.account_info(notification.changed_by).await { + account + } else { + continue; + }; - token_cache.insert(notification.changed_by, token.clone()); - token - }; + account_cache.insert(notification.changed_by, account.clone()); + account + }; response.list.push(build_share_notification( change_id, notification, - &changed_by_token, + &changed_by_account, &properties, )); } @@ -183,7 +184,7 @@ impl ShareNotificationGet for Server { fn build_share_notification( id: u64, mut notification: ShareNotification, - changed_by: &AccessToken, + changed_by: &AccountInfo, properties: &[ShareNotificationProperty], ) -> Value<'static, ShareNotificationProperty, ShareNotificationValue> { let mut result = Map::with_capacity(properties.len()); @@ -202,19 +203,16 @@ fn build_share_notification( Key::Property(ShareNotificationProperty::ChangedByName), Value::Str( changed_by - .description + .description() .as_deref() - .unwrap_or(changed_by.name.as_str()) + .unwrap_or(changed_by.name()) .to_string() .into(), ), ), ( Key::Property(ShareNotificationProperty::ChangedByEmail), - changed_by - .emails - .first() - .map_or(Value::Null, |email| Value::Str(email.to_string().into())), + Value::Str(changed_by.name().to_string().into()), ), ])), ShareNotificationProperty::ObjectType => DataType::try_from(notification.object_type) diff --git a/crates/jmap/src/share_notification/query.rs b/crates/jmap/src/share_notification/query.rs index 06429e65..556dceff 100644 --- a/crates/jmap/src/share_notification/query.rs +++ b/crates/jmap/src/share_notification/query.rs @@ -35,7 +35,7 @@ impl ShareNotificationQuery for Server { let account_id = request.account_id.document_id(); let mut from_change_id = SnowflakeIdGenerator::from_duration( self.core - .jmap + .email .share_notification_max_history .unwrap_or(Duration::from_secs(30 * 86400)), ) diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index bc8a928f..a0ae312a 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -5,11 +5,7 @@ */ use crate::{blob::download::BlobDownload, changes::state::StateManager}; -use common::{ - Server, - auth::{AccessToken, ResourceToken}, - storage::index::ObjectIndexBuilder, -}; +use common::{Server, auth::AccessToken, storage::index::ObjectIndexBuilder}; use email::sieve::{ ArchivedSieveScript, SieveScript, delete::SieveScriptDelete, ingest::SieveScriptIngest, }; @@ -24,10 +20,13 @@ use jmap_proto::{ }; use jmap_tools::{Key, Map, Value}; use rand::distr::Alphanumeric; +use registry::schema::enums::StorageQuota; use sieve::compiler::ErrorType; use std::future::Future; use store::{ - Serialize, SerializeInfallible, ValueKey, rand::{Rng, rng}, write::{AlignedBytes, Archive, Archiver, BatchBuilder} + Serialize, SerializeInfallible, ValueKey, + rand::{Rng, rng}, + write::{AlignedBytes, Archive, Archiver, BatchBuilder}, }; use trc::AddContext; use types::{ @@ -38,7 +37,7 @@ use types::{ }; pub struct SetContext<'x> { - resource_token: ResourceToken, + account_id: u32, access_token: &'x AccessToken, response: SetResponse, } @@ -83,7 +82,7 @@ impl SieveScriptSet for Server { .document_ids(account_id, Collection::SieveScript, SieveField::Name) .await?; let mut ctx = SetContext { - resource_token: self.get_resource_token(access_token, account_id).await?, + account_id, access_token, response: SetResponse::from_request(&request, self.core.jmap.set_max_objects)? .with_state( @@ -105,9 +104,12 @@ impl SieveScriptSet for Server { } // Process creates + let account = self.account(account_id).await.caused_by(trc::location!())?; let mut batch = BatchBuilder::new(); for (id, object) in request.unwrap_create() { - if sieve_ids.len() < access_token.object_quota(Collection::SieveScript) as u64 { + if sieve_ids.len() + < self.object_quota(account.object_quotas(), StorageQuota::MaxSieveScripts) as u64 + { match self .sieve_set_item(object, None, &ctx, session.session_id) .await? @@ -131,7 +133,7 @@ impl SieveScriptSet for Server { .with_account_id(account_id) .with_collection(Collection::SieveScript) .with_document(document_id) - .custom(builder.with_access_token(ctx.access_token)) + .custom(builder.with_changed_by(ctx.access_token.account_tenant_ids())) .caused_by(trc::location!())? .clear(blob_hold) .commit_point(); @@ -253,7 +255,7 @@ impl SieveScriptSet for Server { // Write record batch - .custom(builder.with_access_token(ctx.access_token)) + .custom(builder.with_changed_by(ctx.access_token.account_tenant_ids())) .caused_by(trc::location!())? .commit_point(); @@ -399,7 +401,7 @@ impl SieveScriptSet for Server { }; match (&property, value) { (Key::Property(SieveProperty::Name), Value::Str(value)) => { - if value.len() > self.core.jmap.sieve_max_script_name { + if value.len() > self.core.email.sieve_max_script_name { return Ok(Err(SetError::invalid_properties() .with_property(property.into_owned()) .with_description("Script name is too long."))); @@ -414,7 +416,7 @@ impl SieveScriptSet for Server { .is_none_or(|(_, obj)| obj.inner.name != value.as_ref()) && let Some(id) = self .document_ids_matching( - ctx.resource_token.account_id, + ctx.account_id, Collection::SieveScript, SieveField::Name, value.as_bytes(), @@ -463,13 +465,13 @@ impl SieveScriptSet for Server { let blob_update = if let Some(blob_id) = blob_id { if update.as_ref().is_none_or( |(document_id, _)| { - !matches!(blob_id.class, BlobClass::Linked { account_id, collection, document_id: d } if account_id == ctx.resource_token.account_id && collection == u8::from(Collection::SieveScript) && *document_id == d) + !matches!(blob_id.class, BlobClass::Linked { account_id, collection, document_id: d } if account_id == ctx.account_id && collection == u8::from(Collection::SieveScript) && *document_id == d) }) { // Check access if let Some(mut bytes) = self.blob_download(&blob_id, ctx.access_token).await? { // Check quota match self - .has_available_quota(&ctx.resource_token, bytes.len() as u64) + .has_available_quota(ctx.account_id, bytes.len() as u64) .await { Ok(_) => (), @@ -477,7 +479,7 @@ impl SieveScriptSet for Server { if err.matches(trc::EventType::Limit(trc::LimitEvent::Quota)) || err.matches(trc::EventType::Limit(trc::LimitEvent::TenantQuota)) { - trc::error!(err.account_id(ctx.resource_token.account_id).span_id(session_id)); + trc::error!(err.account_id(ctx.account_id).span_id(session_id)); return Ok(Err(SetError::over_quota())); } else { return Err(err); diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs index e28a1ef7..36ac3a52 100644 --- a/crates/jmap/src/submission/set.rs +++ b/crates/jmap/src/submission/set.rs @@ -7,7 +7,7 @@ use common::{ Server, config::smtp::queue::QueueName, - listener::{ServerInstance, stream::NullIo}, + network::{ServerInstance, stream::NullIo}, storage::index::ObjectIndexBuilder, }; use email::{ @@ -615,11 +615,11 @@ impl EmailSubmissionSet for Server { .get_blob(metadata.blob_hash.0.as_slice(), 0..usize::MAX) .await? { - if message.len() > self.core.jmap.mail_max_size { + if message.len() > self.core.email.mail_max_size { return Ok(Err(SetError::new(SetErrorType::InvalidEmail) .with_description(format!( "Message exceeds maximum size of {} bytes.", - self.core.jmap.mail_max_size + self.core.email.mail_max_size )))); } @@ -644,7 +644,7 @@ impl EmailSubmissionSet for Server { self.clone(), instance.clone(), SessionData::local( - self.get_access_token(account_id) + self.account_info(account_id) .await .caused_by(trc::location!())?, None, diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index c3a7d12d..24ca0f18 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -260,7 +260,7 @@ impl VacationResponseSet for Server { let mut obj = ObjectIndexBuilder::new() .with_current_opt(prev_sieve) .with_changes(sieve) - .with_account_info(&account_info); + .with_changed_by(access_token.account_tenant_ids()); // Update id let document_id = if let Some(document_id) = document_id { diff --git a/crates/managesieve/Cargo.toml b/crates/managesieve/Cargo.toml index 53ebef72..2f2dffc9 100644 --- a/crates/managesieve/Cargo.toml +++ b/crates/managesieve/Cargo.toml @@ -14,6 +14,7 @@ store = { path = "../store" } utils = { path = "../utils" } email = { path = "../email" } trc = { path = "../trc" } +registry = { path = "../registry" } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } sieve-rs = { version = "0.7", features = ["rkyv"] } diff --git a/crates/managesieve/src/core/client.rs b/crates/managesieve/src/core/client.rs index 4fd73d87..eb929726 100644 --- a/crates/managesieve/src/core/client.rs +++ b/crates/managesieve/src/core/client.rs @@ -7,7 +7,7 @@ use super::{Command, ResponseCode, SerializeResponse, Session, State}; use common::{ KV_RATE_LIMIT_IMAP, - listener::{SessionResult, SessionStream}, + network::{SessionResult, SessionStream}, }; use imap_proto::receiver::{self, Request}; use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; @@ -183,9 +183,7 @@ impl Session { if let Some(rate) = &self.server.core.imap.rate_requests { if self .server - .core - .storage - .lookup + .in_memory_store() .is_rate_allowed( KV_RATE_LIMIT_IMAP, &access_token.account_id().to_be_bytes(), diff --git a/crates/managesieve/src/core/mod.rs b/crates/managesieve/src/core/mod.rs index 22b2603c..57e0194f 100644 --- a/crates/managesieve/src/core/mod.rs +++ b/crates/managesieve/src/core/mod.rs @@ -12,7 +12,7 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc}; use common::{ Inner, Server, auth::AccessToken, - listener::{ServerInstance, limiter::InFlight}, + network::{ServerInstance, limiter::InFlight}, }; use compact_str::CompactString; @@ -35,7 +35,7 @@ pub enum State { auth_failures: u32, }, Authenticated { - access_token: Arc, + access_token: AccessToken, in_flight: Option, }, } diff --git a/crates/managesieve/src/core/session.rs b/crates/managesieve/src/core/session.rs index f316cc38..9c7ab366 100644 --- a/crates/managesieve/src/core/session.rs +++ b/crates/managesieve/src/core/session.rs @@ -4,17 +4,15 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{ManageSieveSessionManager, Session, State}; +use crate::SERVER_GREETING; use common::{ - core::BuildServer, - listener::{SessionData, SessionManager, SessionResult, SessionStream}, + BuildServer, + network::{SessionData, SessionManager, SessionResult, SessionStream}, }; use imap_proto::receiver::{self, Receiver}; use tokio_rustls::server::TlsStream; -use crate::SERVER_GREETING; - -use super::{ManageSieveSessionManager, Session, State}; - impl SessionManager for ManageSieveSessionManager { #[allow(clippy::manual_async_fn)] fn handle( diff --git a/crates/managesieve/src/op/authenticate.rs b/crates/managesieve/src/op/authenticate.rs index 6af63e4b..3f117cf9 100644 --- a/crates/managesieve/src/op/authenticate.rs +++ b/crates/managesieve/src/op/authenticate.rs @@ -4,22 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::core::{Command, Session, State, StatusResponse}; use common::{ - auth::{ - AuthRequest, - sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain}, - }, - listener::{SessionStream, limiter::LimiterResult}, + auth::AuthRequest, + network::{SessionStream, limiter::LimiterResult}, }; - -use registry::schema::enums::Permission; +use directory::Credentials; use imap_proto::{ protocol::authenticate::Mechanism, receiver::{self, Request}, }; use mail_parser::decoders::base64::base64_decode; - -use crate::core::{Command, Session, State, StatusResponse}; +use registry::schema::enums::Permission; impl Session { pub async fn handle_authenticate(&mut self, request: Request) -> trc::Result> { @@ -42,9 +38,9 @@ impl Session { base64_decode(params.pop().unwrap().as_bytes()) .and_then(|challenge| { if mechanism == Mechanism::Plain { - sasl_decode_challenge_plain(&challenge) + Credentials::decode_sasl_challenge_plain(&challenge) } else { - sasl_decode_challenge_oauth(&challenge) + Credentials::decode_sasl_challenge_oauth(&challenge) } }) .ok_or_else(|| { @@ -96,11 +92,7 @@ impl Session { err }) - .and_then(|token| { - token - .assert_has_permission(Permission::SieveAuthenticate) - .map(|_| token) - })?; + .and_then(|token| token.assert_has_permission(Permission::SieveAuthenticate))?; // Enforce concurrency limits let in_flight = match access_token.is_imap_request_allowed() { diff --git a/crates/managesieve/src/op/capability.rs b/crates/managesieve/src/op/capability.rs index e91463ed..c0ceff6a 100644 --- a/crates/managesieve/src/op/capability.rs +++ b/crates/managesieve/src/op/capability.rs @@ -5,7 +5,7 @@ */ use crate::core::{Session, StatusResponse}; -use common::listener::SessionStream; +use common::network::SessionStream; use jmap_proto::request::capability::Capabilities; use std::time::Instant; diff --git a/crates/managesieve/src/op/checkscript.rs b/crates/managesieve/src/op/checkscript.rs index 852f2e67..6c1a15aa 100644 --- a/crates/managesieve/src/op/checkscript.rs +++ b/crates/managesieve/src/op/checkscript.rs @@ -6,7 +6,7 @@ use std::time::Instant; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; use imap_proto::receiver::Request; diff --git a/crates/managesieve/src/op/deletescript.rs b/crates/managesieve/src/op/deletescript.rs index ecfbde8f..8976b713 100644 --- a/crates/managesieve/src/op/deletescript.rs +++ b/crates/managesieve/src/op/deletescript.rs @@ -5,7 +5,7 @@ */ use crate::core::{Command, ResponseCode, Session, StatusResponse}; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; use email::sieve::{delete::SieveScriptDelete, ingest::SieveScriptIngest}; use imap_proto::receiver::Request; diff --git a/crates/managesieve/src/op/getscript.rs b/crates/managesieve/src/op/getscript.rs index c8ba9605..64277432 100644 --- a/crates/managesieve/src/op/getscript.rs +++ b/crates/managesieve/src/op/getscript.rs @@ -5,7 +5,7 @@ */ use crate::core::{Command, ResponseCode, Session, StatusResponse}; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; use email::sieve::SieveScript; use imap_proto::receiver::Request; diff --git a/crates/managesieve/src/op/havespace.rs b/crates/managesieve/src/op/havespace.rs index a86f4c47..48bdf5b2 100644 --- a/crates/managesieve/src/op/havespace.rs +++ b/crates/managesieve/src/op/havespace.rs @@ -6,9 +6,9 @@ use std::time::Instant; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use imap_proto::receiver::Request; +use registry::schema::enums::Permission; use trc::AddContext; use crate::core::{Command, ResponseCode, Session, StatusResponse}; @@ -44,19 +44,19 @@ impl Session { })?; // Validate name - let access_token = self.state.access_token(); - let account_id = access_token.account_id(); + let account_id = self.state.access_token().account_id(); + let account = self.server.account(account_id).await?; self.validate_name(account_id, &name).await?; // Validate quota - if access_token.quota == 0 + if account.disk_quota() == 0 || size as i64 + self .server - .get_used_quota(account_id) + .get_used_quota_account(account_id) .await .caused_by(trc::location!())? - <= access_token.quota as i64 + <= account.disk_quota() as i64 { trc::event!( ManageSieve(trc::ManageSieveEvent::HaveSpace), diff --git a/crates/managesieve/src/op/listscripts.rs b/crates/managesieve/src/op/listscripts.rs index 6db99a95..58da4b8a 100644 --- a/crates/managesieve/src/op/listscripts.rs +++ b/crates/managesieve/src/op/listscripts.rs @@ -5,9 +5,9 @@ */ use crate::core::{Session, StatusResponse}; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use email::sieve::{SieveScript, ingest::SieveScriptIngest}; +use registry::schema::enums::Permission; use std::time::Instant; use store::{ ValueKey, diff --git a/crates/managesieve/src/op/mod.rs b/crates/managesieve/src/op/mod.rs index e01a0bae..f8a54dcf 100644 --- a/crates/managesieve/src/op/mod.rs +++ b/crates/managesieve/src/op/mod.rs @@ -5,7 +5,7 @@ */ use crate::core::{Session, State, StatusResponse}; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; pub mod authenticate; @@ -35,7 +35,7 @@ impl Session { pub fn assert_has_permission(&self, permission: Permission) -> trc::Result { match &self.state { State::Authenticated { access_token, .. } => { - access_token.assert_has_permission(permission) + access_token.enforce_permission(permission).map(|_| true) } State::NotAuthenticated { .. } => Ok(false), } diff --git a/crates/managesieve/src/op/putscript.rs b/crates/managesieve/src/op/putscript.rs index 315e00be..84f643c8 100644 --- a/crates/managesieve/src/op/putscript.rs +++ b/crates/managesieve/src/op/putscript.rs @@ -5,10 +5,10 @@ */ use crate::core::{Command, ResponseCode, Session, StatusResponse}; -use common::{listener::SessionStream, storage::index::ObjectIndexBuilder}; -use registry::schema::enums::Permission; +use common::{network::SessionStream, storage::index::ObjectIndexBuilder}; use email::sieve::SieveScript; use imap_proto::receiver::Request; +use registry::schema::enums::{Permission, StorageQuota}; use sieve::compiler::ErrorType; use std::time::Instant; use store::{ @@ -48,8 +48,9 @@ impl Session { // Check quota let access_token = self.state.access_token(); let account_id = access_token.account_id(); + let account = self.server.account(account_id).await?; self.server - .has_available_quota(&access_token.as_resource_token(), script_bytes.len() as u64) + .has_available_quota(account_id, script_bytes.len() as u64) .await .caused_by(trc::location!())?; @@ -59,7 +60,10 @@ impl Session { .await .caused_by(trc::location!())? .len() - > access_token.object_quota(Collection::SieveScript) as u64 + >= self + .server + .object_quota(account.object_quotas(), StorageQuota::MaxSieveScripts) + as u64 { return Err(trc::ManageSieveEvent::Error .into_err() @@ -142,7 +146,7 @@ impl Session { .with_blob_hash(blob_hash.clone()), ) .with_current(script) - .with_account_info(&account_info), + .with_changed_by(account.account_tenant_ids(account_id)), ) .caused_by(trc::location!())? .clear(blob_hold); @@ -185,7 +189,7 @@ impl Session { SieveScript::new(name.clone(), blob_hash.clone()) .with_size(script_size as u32), ) - .with_account_info(&account_info), + .with_changed_by(account.account_tenant_ids(account_id)), ) .caused_by(trc::location!())? .clear(blob_hold); @@ -212,7 +216,7 @@ impl Session { Err(trc::ManageSieveEvent::Error .into_err() .details("Script name cannot be empty.")) - } else if name.len() > self.server.core.jmap.sieve_max_script_name { + } else if name.len() > self.server.core.email.sieve_max_script_name { Err(trc::ManageSieveEvent::Error .into_err() .details("Script name is too long.")) diff --git a/crates/managesieve/src/op/renamescript.rs b/crates/managesieve/src/op/renamescript.rs index 5d828a7d..134ad430 100644 --- a/crates/managesieve/src/op/renamescript.rs +++ b/crates/managesieve/src/op/renamescript.rs @@ -5,7 +5,7 @@ */ use crate::core::{Command, ResponseCode, Session, StatusResponse}; -use common::{listener::SessionStream, storage::index::ObjectIndexBuilder}; +use common::{network::SessionStream, storage::index::ObjectIndexBuilder}; use registry::schema::enums::Permission; use email::sieve::SieveScript; use imap_proto::receiver::Request; diff --git a/crates/managesieve/src/op/setactive.rs b/crates/managesieve/src/op/setactive.rs index 07d9863a..5b5bbf96 100644 --- a/crates/managesieve/src/op/setactive.rs +++ b/crates/managesieve/src/op/setactive.rs @@ -6,7 +6,7 @@ use std::time::Instant; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; use imap_proto::receiver::Request; use store::{SerializeInfallible, write::BatchBuilder}; diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index 06c28007..47198d11 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{ +/*use crate::{ blob::migrate_blobs_v014, queue_v1::{migrate_queue_v011, migrate_queue_v012}, queue_v2::migrate_queue_v014, @@ -12,7 +12,7 @@ use crate::{ v012::migrate_v0_12, v013::migrate_v0_13, v014::{SUBSPACE_BITMAP_ID, migrate_principal_v0_14, migrate_v0_14}, -}; +};*/ use common::{DATABASE_SCHEMA_VERSION, Server, manager::boot::DEFAULT_SETTINGS}; use std::time::Duration; use store::{ @@ -28,7 +28,7 @@ use store::{ use trc::AddContext; use types::collection::Collection; -pub mod addressbook_v2; +/*pub mod addressbook_v2; pub mod blob; pub mod calendar_v2; pub mod changelog; @@ -58,171 +58,173 @@ pub mod threads; pub mod v011; pub mod v012; pub mod v013; -pub mod v014; +pub mod v014;*/ const LOCK_WAIT_TIME_ACCOUNT: u64 = 3 * 60; const LOCK_WAIT_TIME_CORE: u64 = 5 * 60; const LOCK_RETRY_TIME: Duration = Duration::from_secs(30); pub async fn try_migrate(server: &Server) -> trc::Result<()> { - for var in [ - "FORCE_MIGRATE_QUEUE", - "FORCE_MIGRATE_BLOBS", - "FORCE_MIGRATE_ACCOUNT", - "FORCE_MIGRATE", - ] { - let Some(version) = std::env::var(var).ok().and_then(|s| s.parse::().ok()) else { - continue; - }; - match var { - "FORCE_MIGRATE_QUEUE" => match version { - 1 => { - migrate_queue_v011(server) + /*for var in [ + "FORCE_MIGRATE_QUEUE", + "FORCE_MIGRATE_BLOBS", + "FORCE_MIGRATE_ACCOUNT", + "FORCE_MIGRATE", + ] { + let Some(version) = std::env::var(var).ok().and_then(|s| s.parse::().ok()) else { + continue; + }; + match var { + "FORCE_MIGRATE_QUEUE" => match version { + 1 => { + migrate_queue_v011(server) + .await + .caused_by(trc::location!())?; + } + 2 => { + migrate_queue_v012(server) + .await + .caused_by(trc::location!())?; + } + 4 => { + migrate_queue_v014(server) + .await + .caused_by(trc::location!())?; + } + _ => { + panic!("Unknown migration queue version: {version}"); + } + }, + "FORCE_MIGRATE_BLOBS" => { + migrate_blobs_v014(server) .await .caused_by(trc::location!())?; } - 2 => { - migrate_queue_v012(server) + "FORCE_MIGRATE" => match version { + 1 => { + migrate_v0_12(server, true) + .await + .caused_by(trc::location!())?; + migrate_v0_13(server).await.caused_by(trc::location!())?; + migrate_v0_14(server).await.caused_by(trc::location!())?; + } + 2 => { + migrate_v0_12(server, false) + .await + .caused_by(trc::location!())?; + migrate_v0_13(server).await.caused_by(trc::location!())?; + migrate_v0_14(server).await.caused_by(trc::location!())?; + } + 3 => { + migrate_v0_13(server).await.caused_by(trc::location!())?; + migrate_v0_14(server).await.caused_by(trc::location!())?; + } + 4 => { + migrate_v0_14(server).await.caused_by(trc::location!())?; + } + _ => { + panic!("Unknown migration version: {version}"); + } + }, + "FORCE_MIGRATE_ACCOUNT" => { + migrate_principal_v0_14(server, version) .await .caused_by(trc::location!())?; } - 4 => { - migrate_queue_v014(server) - .await - .caused_by(trc::location!())?; - } - _ => { - panic!("Unknown migration queue version: {version}"); - } - }, - "FORCE_MIGRATE_BLOBS" => { - migrate_blobs_v014(server) - .await - .caused_by(trc::location!())?; + _ => unreachable!(), } - "FORCE_MIGRATE" => match version { - 1 => { - migrate_v0_12(server, true) - .await - .caused_by(trc::location!())?; - migrate_v0_13(server).await.caused_by(trc::location!())?; - migrate_v0_14(server).await.caused_by(trc::location!())?; - } - 2 => { - migrate_v0_12(server, false) - .await - .caused_by(trc::location!())?; - migrate_v0_13(server).await.caused_by(trc::location!())?; - migrate_v0_14(server).await.caused_by(trc::location!())?; - } - 3 => { - migrate_v0_13(server).await.caused_by(trc::location!())?; - migrate_v0_14(server).await.caused_by(trc::location!())?; - } - 4 => { - migrate_v0_14(server).await.caused_by(trc::location!())?; - } - _ => { - panic!("Unknown migration version: {version}"); - } - }, - "FORCE_MIGRATE_ACCOUNT" => { - migrate_principal_v0_14(server, version) - .await - .caused_by(trc::location!())?; - } - _ => unreachable!(), - } - return Ok(()); - } - - let add_v013_config = match server - .store() - .get_value::(AnyKey { - subspace: SUBSPACE_PROPERTY, - key: vec![0u8], - }) - .await - .caused_by(trc::location!())? - { - Some(DATABASE_SCHEMA_VERSION) => { return Ok(()); } - Some(1) => { - migrate_v0_12(server, true) - .await - .caused_by(trc::location!())?; - migrate_v0_13(server).await.caused_by(trc::location!())?; - migrate_v0_14(server).await.caused_by(trc::location!())?; - true - } - Some(2) => { - migrate_v0_12(server, false) - .await - .caused_by(trc::location!())?; - migrate_v0_13(server).await.caused_by(trc::location!())?; - migrate_v0_14(server).await.caused_by(trc::location!())?; - true - } - Some(3) => { - migrate_v0_13(server).await.caused_by(trc::location!())?; - migrate_v0_14(server).await.caused_by(trc::location!())?; - false - } - Some(4) => { - migrate_v0_14(server).await.caused_by(trc::location!())?; - false - } - Some(version) => { - panic!( - "Unknown database schema version, expected {} or below, found {}", - DATABASE_SCHEMA_VERSION, version - ); - } - _ => { - if !is_new_install(server).await.caused_by(trc::location!())? { - migrate_v0_11(server).await.caused_by(trc::location!())?; + + let add_v013_config = match server + .store() + .get_value::(AnyKey { + subspace: SUBSPACE_PROPERTY, + key: vec![0u8], + }) + .await + .caused_by(trc::location!())? + { + Some(DATABASE_SCHEMA_VERSION) => { + return Ok(()); + } + Some(1) => { + migrate_v0_12(server, true) + .await + .caused_by(trc::location!())?; + migrate_v0_13(server).await.caused_by(trc::location!())?; + migrate_v0_14(server).await.caused_by(trc::location!())?; true - } else { + } + Some(2) => { + migrate_v0_12(server, false) + .await + .caused_by(trc::location!())?; + migrate_v0_13(server).await.caused_by(trc::location!())?; + migrate_v0_14(server).await.caused_by(trc::location!())?; + true + } + Some(3) => { + migrate_v0_13(server).await.caused_by(trc::location!())?; + migrate_v0_14(server).await.caused_by(trc::location!())?; false } - } - }; - - let mut batch = BatchBuilder::new(); - batch.set( - ValueClass::Any(AnyClass { - subspace: SUBSPACE_PROPERTY, - key: vec![0u8], - }), - DATABASE_SCHEMA_VERSION.serialize(), - ); - - if add_v013_config { - for (key, value) in DEFAULT_SETTINGS { - if key - .strip_prefix("queue.") - .is_some_and(|s| !s.starts_with("limiter.") && !s.starts_with("quota.")) - { - batch.set( - ValueClass::Any(AnyClass { - subspace: SUBSPACE_SETTINGS, - key: key.as_bytes().to_vec(), - }), - value.as_bytes().to_vec(), + Some(4) => { + migrate_v0_14(server).await.caused_by(trc::location!())?; + false + } + Some(version) => { + panic!( + "Unknown database schema version, expected {} or below, found {}", + DATABASE_SCHEMA_VERSION, version ); } + _ => { + if !is_new_install(server).await.caused_by(trc::location!())? { + migrate_v0_11(server).await.caused_by(trc::location!())?; + true + } else { + false + } + } + }; + + let mut batch = BatchBuilder::new(); + batch.set( + ValueClass::Any(AnyClass { + subspace: SUBSPACE_PROPERTY, + key: vec![0u8], + }), + DATABASE_SCHEMA_VERSION.serialize(), + ); + + if add_v013_config { + for (key, value) in DEFAULT_SETTINGS { + if key + .strip_prefix("queue.") + .is_some_and(|s| !s.starts_with("limiter.") && !s.starts_with("quota.")) + { + batch.set( + ValueClass::Any(AnyClass { + subspace: SUBSPACE_SETTINGS, + key: key.as_bytes().to_vec(), + }), + value.as_bytes().to_vec(), + ); + } + } } - } - - server - .store() - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; + server + .store() + .write(batch.build_all()) + .await + .caused_by(trc::location!())?; Ok(()) + */ + + todo!() } async fn is_new_install(server: &Server) -> trc::Result { @@ -319,7 +321,7 @@ where .map(|_| results) } -pub async fn get_document_ids( +/*pub async fn get_document_ids( server: &Server, account_id: u32, collection: Collection, @@ -345,7 +347,7 @@ pub async fn get_document_ids( }, ) .await -} +}*/ pub async fn get_bitmap( server: &Server, diff --git a/crates/migration/src/principal_v1.rs b/crates/migration/src/principal_v1.rs index 31d7aea9..8691d808 100644 --- a/crates/migration/src/principal_v1.rs +++ b/crates/migration/src/principal_v1.rs @@ -11,10 +11,6 @@ use crate::{ submission::migrate_email_submissions, threads::migrate_threads, }; use common::Server; -use directory::{ - Permission, Principal, PrincipalData, ROLE_ADMIN, ROLE_USER, Type, - backend::internal::{PrincipalField, PrincipalSet, SpecialSecrets}, -}; use nlp::tokenizers::word::WordTokenizer; use std::{slice::Iter, time::Instant}; use store::{ diff --git a/crates/migration/src/principal_v2.rs b/crates/migration/src/principal_v2.rs index 5f34a5fa..4996961c 100644 --- a/crates/migration/src/principal_v2.rs +++ b/crates/migration/src/principal_v2.rs @@ -14,7 +14,6 @@ use crate::{ sieve_v2::migrate_sieve_v013, }; use common::Server; -use directory::{Principal, PrincipalData, Type, backend::internal::SpecialSecrets}; use proc_macros::EnumMethods; use std::time::Instant; use store::{ diff --git a/crates/migration/src/v014.rs b/crates/migration/src/v014.rs index 00ed8145..6a97e315 100644 --- a/crates/migration/src/v014.rs +++ b/crates/migration/src/v014.rs @@ -10,7 +10,6 @@ use crate::{ tasks_v2::migrate_tasks_v014, }; use common::Server; -use directory::backend::internal::manage::ManageDirectory; use email::submission::EmailSubmission; use groupware::{calendar::CalendarEventNotification, contact::ContactCard}; use std::sync::Arc; diff --git a/crates/pop3/Cargo.toml b/crates/pop3/Cargo.toml index 089bfacc..a212c045 100644 --- a/crates/pop3/Cargo.toml +++ b/crates/pop3/Cargo.toml @@ -12,6 +12,7 @@ utils = { path = "../utils" } trc = { path = "../trc" } types = { path = "../types" } email = { path = "../email" } +registry = { path = "../registry" } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-send = { version = "0.5", default-features = false, features = ["cram-md5", "ring", "tls12"] } rustls = { version = "0.23.5", default-features = false, features = ["std", "ring", "tls12"] } diff --git a/crates/pop3/src/client.rs b/crates/pop3/src/client.rs index 199c42e9..e8687421 100644 --- a/crates/pop3/src/client.rs +++ b/crates/pop3/src/client.rs @@ -4,17 +4,16 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{ - KV_RATE_LIMIT_IMAP, - listener::{SessionResult, SessionStream}, -}; -use mail_send::Credentials; -use trc::{AddContext, SecurityEvent}; - use crate::{ Session, State, protocol::{Command, Mechanism, request::Error}, }; +use common::{ + KV_RATE_LIMIT_IMAP, + network::{SessionResult, SessionStream}, +}; +use directory::Credentials; +use trc::{AddContext, SecurityEvent}; impl Session { pub async fn ingest(&mut self, bytes: &[u8]) -> SessionResult { @@ -101,7 +100,7 @@ impl Session { } else { unreachable!() }; - self.handle_auth(Credentials::Plain { + self.handle_auth(Credentials::Basic { username, secret: string, }) @@ -239,9 +238,7 @@ impl Session { if let Some(rate) = &self.server.core.imap.rate_requests { if self .server - .core - .storage - .lookup + .in_memory_store() .is_rate_allowed( KV_RATE_LIMIT_IMAP, &mailbox.account_id.to_be_bytes(), diff --git a/crates/pop3/src/lib.rs b/crates/pop3/src/lib.rs index a8ff41f1..dda0ed92 100644 --- a/crates/pop3/src/lib.rs +++ b/crates/pop3/src/lib.rs @@ -9,7 +9,7 @@ use std::{net::IpAddr, sync::Arc}; use common::{ Inner, Server, auth::AccessToken, - listener::{ServerInstance, SessionStream, limiter::InFlight}, + network::{ServerInstance, SessionStream, limiter::InFlight}, }; use mailbox::Mailbox; use protocol::request::Parser; @@ -52,7 +52,7 @@ pub enum State { Authenticated { mailbox: Mailbox, in_flight: Option, - access_token: Arc, + access_token: AccessToken, }, } @@ -71,7 +71,7 @@ impl State { } } - pub fn access_token(&self) -> &Arc { + pub fn access_token(&self) -> &AccessToken { match self { State::Authenticated { access_token, .. } => access_token, _ => unreachable!(), diff --git a/crates/pop3/src/mailbox.rs b/crates/pop3/src/mailbox.rs index a26c7c31..17473557 100644 --- a/crates/pop3/src/mailbox.rs +++ b/crates/pop3/src/mailbox.rs @@ -5,7 +5,7 @@ */ use crate::Session; -use common::listener::SessionStream; +use common::network::SessionStream; use email::{ cache::{MessageCacheFetch, mailbox::MailboxCacheAccess}, mailbox::INBOX_ID, diff --git a/crates/pop3/src/op/authenticate.rs b/crates/pop3/src/op/authenticate.rs index fa69798a..4b0b6a8b 100644 --- a/crates/pop3/src/op/authenticate.rs +++ b/crates/pop3/src/op/authenticate.rs @@ -4,21 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{ - auth::{ - AuthRequest, - sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain}, - }, - listener::{SessionStream, limiter::LimiterResult}, -}; -use registry::schema::enums::Permission; -use mail_parser::decoders::base64::base64_decode; -use mail_send::Credentials; - use crate::{ Session, State, protocol::{Command, Mechanism, request}, }; +use common::{ + auth::AuthRequest, + network::{SessionStream, limiter::LimiterResult}, +}; +use directory::Credentials; +use mail_parser::decoders::base64::base64_decode; +use registry::schema::enums::Permission; impl Session { pub async fn handle_sasl( @@ -32,9 +28,9 @@ impl Session { let credentials = base64_decode(params.pop().unwrap().as_bytes()) .and_then(|challenge| { if mechanism == Mechanism::Plain { - sasl_decode_challenge_plain(&challenge) + Credentials::decode_sasl_challenge_plain(&challenge) } else { - sasl_decode_challenge_oauth(&challenge) + Credentials::decode_sasl_challenge_oauth(&challenge) } }) .ok_or_else(|| { @@ -64,7 +60,7 @@ impl Session { } } - pub async fn handle_auth(&mut self, credentials: Credentials) -> trc::Result<()> { + pub async fn handle_auth(&mut self, credentials: Credentials) -> trc::Result<()> { // Authenticate let access_token = self .server @@ -94,11 +90,7 @@ impl Session { err }) - .and_then(|token| { - token - .assert_has_permission(Permission::Pop3Authenticate) - .map(|_| token) - })?; + .and_then(|token| token.assert_has_permission(Permission::Pop3Authenticate))?; // Enforce concurrency limits let in_flight = match access_token.is_imap_request_allowed() { diff --git a/crates/pop3/src/op/delete.rs b/crates/pop3/src/op/delete.rs index 442468e4..20ddc83a 100644 --- a/crates/pop3/src/op/delete.rs +++ b/crates/pop3/src/op/delete.rs @@ -6,9 +6,9 @@ use std::time::Instant; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use email::message::delete::EmailDeletion; +use registry::schema::enums::Permission; use store::{roaring::RoaringBitmap, write::BatchBuilder}; use trc::AddContext; @@ -19,7 +19,7 @@ impl Session { // Validate access self.state .access_token() - .assert_has_permission(Permission::Pop3Dele)?; + .enforce_permission(Permission::Pop3Dele)?; let op_start = Instant::now(); let mailbox = self.state.mailbox_mut(); diff --git a/crates/pop3/src/op/fetch.rs b/crates/pop3/src/op/fetch.rs index 5101dd84..fedc41a2 100644 --- a/crates/pop3/src/op/fetch.rs +++ b/crates/pop3/src/op/fetch.rs @@ -5,9 +5,9 @@ */ use crate::{Session, protocol::response::Response}; -use common::listener::SessionStream; -use registry::schema::enums::Permission; +use common::network::SessionStream; use email::message::metadata::MessageMetadata; +use registry::schema::enums::Permission; use std::time::Instant; use store::{ ValueKey, @@ -22,7 +22,7 @@ impl Session { // Validate access self.state .access_token() - .assert_has_permission(Permission::Pop3Retr)?; + .enforce_permission(Permission::Pop3Retr)?; let op_start = Instant::now(); let mailbox = self.state.mailbox(); diff --git a/crates/pop3/src/op/list.rs b/crates/pop3/src/op/list.rs index ebddc2c3..07232172 100644 --- a/crates/pop3/src/op/list.rs +++ b/crates/pop3/src/op/list.rs @@ -6,7 +6,7 @@ use std::time::Instant; -use common::listener::SessionStream; +use common::network::SessionStream; use registry::schema::enums::Permission; use crate::{Session, protocol::response::Response}; @@ -16,7 +16,7 @@ impl Session { // Validate access self.state .access_token() - .assert_has_permission(Permission::Pop3List)?; + .enforce_permission(Permission::Pop3List)?; let op_start = Instant::now(); let mailbox = self.state.mailbox(); @@ -57,7 +57,7 @@ impl Session { // Validate access self.state .access_token() - .assert_has_permission(Permission::Pop3Uidl)?; + .enforce_permission(Permission::Pop3Uidl)?; let op_start = Instant::now(); let mailbox = self.state.mailbox(); @@ -106,7 +106,7 @@ impl Session { // Validate access self.state .access_token() - .assert_has_permission(Permission::Pop3Stat)?; + .enforce_permission(Permission::Pop3Stat)?; let op_start = Instant::now(); let mailbox = self.state.mailbox(); diff --git a/crates/pop3/src/op/mod.rs b/crates/pop3/src/op/mod.rs index 4ca6a275..6086f169 100644 --- a/crates/pop3/src/op/mod.rs +++ b/crates/pop3/src/op/mod.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::SessionStream; +use common::network::SessionStream; use crate::{ Session, diff --git a/crates/pop3/src/session.rs b/crates/pop3/src/session.rs index 1da1e4bf..88e7af9a 100644 --- a/crates/pop3/src/session.rs +++ b/crates/pop3/src/session.rs @@ -4,14 +4,6 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; - -use common::{ - core::BuildServer, - listener::{SessionData, SessionManager, SessionResult, SessionStream}, -}; -use tokio_rustls::server::TlsStream; - use crate::{ Pop3SessionManager, SERVER_GREETING, Session, State, protocol::{ @@ -19,8 +11,13 @@ use crate::{ response::{Response, SerializeResponse}, }, }; - +use common::{ + BuildServer, + network::{SessionData, SessionManager, SessionResult, SessionStream}, +}; +use std::borrow::Cow; use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio_rustls::server::TlsStream; impl SessionManager for Pop3SessionManager { #[allow(clippy::manual_async_fn)] diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index fa6aaebb..bd45d194 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -15,6 +15,7 @@ spam-filter = { path = "../spam-filter" } types = { path = "../types" } jmap_proto = { path = "../jmap-proto" } directory = { path = "../directory" } +registry = { path = "../registry" } smtp-proto = { version = "0.2", features = ["rkyv", "serde"] } tokio = { version = "1.47", features = ["rt"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } diff --git a/crates/services/src/broadcast/mod.rs b/crates/services/src/broadcast/mod.rs index 678dbdab..633566cd 100644 --- a/crates/services/src/broadcast/mod.rs +++ b/crates/services/src/broadcast/mod.rs @@ -4,7 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::ipc::{BroadcastEvent, CalendarAlert, EmailPush, PushNotification}; +use common::ipc::{ + BroadcastEvent, CacheInvalidation, CalendarAlert, PushNotification, RegistryChange, +}; +use registry::{ + schema::prelude::Object, + types::{EnumType, id::Id}, +}; use std::{borrow::Borrow, io::Write}; use types::type_state::StateChange; use utils::{ @@ -66,32 +72,44 @@ impl BroadcastBatch> { let _ = serialized.write_leb128(email_push.change_id); } }, - BroadcastEvent::InvalidateAccessTokens(items) => { + BroadcastEvent::PushServerUpdate(account_id) => { serialized.push(3u8); - let _ = serialized.write_leb128(items.len()); - for item in items { - let _ = serialized.write_leb128(*item); - } - } - BroadcastEvent::InvalidateGroupwareCache(items) => { - serialized.push(4u8); - let _ = serialized.write_leb128(items.len()); - for item in items { - let _ = serialized.write_leb128(*item); - } - } - BroadcastEvent::ReloadSettings => { - serialized.push(5u8); - } - BroadcastEvent::ReloadBlockedIps => { - serialized.push(6u8); - } - BroadcastEvent::ReloadPushServers(account_id) => { - serialized.push(7u8); let _ = serialized.write_leb128(*account_id); } - BroadcastEvent::ReloadSpamFilter => { - serialized.push(8u8); + BroadcastEvent::RegistryChange(items) => match items { + RegistryChange::Insert(id) => { + serialized.push(4u8); + let _ = serialized.write_leb128(id.object().to_id()); + let _ = serialized.write_leb128(id.id()); + } + RegistryChange::Delete(id) => { + serialized.push(5u8); + let _ = serialized.write_leb128(id.object().to_id()); + let _ = serialized.write_leb128(id.id()); + } + RegistryChange::Reload(object) => { + serialized.push(6u8); + let _ = serialized.write_leb128(object.to_id()); + } + }, + BroadcastEvent::CacheInvalidation(items) => { + serialized.push(7u8); + let _ = serialized.write_leb128(items.len()); + for item in items { + let (marker, id) = match item { + CacheInvalidation::AccessToken(id) => (0u8, *id), + CacheInvalidation::DavResources(id) => (1u8, *id), + CacheInvalidation::Domain(id) => (2u8, *id), + CacheInvalidation::Account(id) => (3u8, *id), + CacheInvalidation::Group(id) => (4u8, *id), + CacheInvalidation::Tenant(id) => (5u8, *id), + CacheInvalidation::Role(id) => (6u8, *id), + CacheInvalidation::List(id) => (7u8, *id), + }; + + serialized.push(marker); + let _ = serialized.write_leb128(id); + } } } } @@ -153,43 +171,50 @@ where }), ))) } - - 2 => Ok(Some(BroadcastEvent::PushNotification( - PushNotification::EmailPush(EmailPush { - account_id: self.messages.next_leb128().ok_or(())?, - email_id: self.messages.next_leb128().ok_or(())?, - change_id: self.messages.next_leb128().ok_or(())?, - }), - ))), - 3 => { - let count = self.messages.next_leb128::().ok_or(())?; - let mut items = Vec::with_capacity(count); - for _ in 0..count { - items.push(self.messages.next_leb128().ok_or(())?); - } - Ok(Some(BroadcastEvent::InvalidateAccessTokens(items))) - } - - 4 => { - let count = self.messages.next_leb128::().ok_or(())?; - let mut items = Vec::with_capacity(count); - for _ in 0..count { - items.push(self.messages.next_leb128().ok_or(())?); - } - Ok(Some(BroadcastEvent::InvalidateGroupwareCache(items))) - } - - 5 => Ok(Some(BroadcastEvent::ReloadSettings)), - - 6 => Ok(Some(BroadcastEvent::ReloadBlockedIps)), - - 7 => { let account_id = self.messages.next_leb128().ok_or(())?; - Ok(Some(BroadcastEvent::ReloadPushServers(account_id))) + Ok(Some(BroadcastEvent::PushServerUpdate(account_id))) + } + 4 => { + let object_id = self.messages.next_leb128().ok_or(())?; + let id = self.messages.next_leb128::().ok_or(())?; + Ok(Some(BroadcastEvent::RegistryChange( + RegistryChange::Insert(Id::new(Object::from_id(object_id).ok_or(())?, id)), + ))) + } + 5 => { + let object_id = self.messages.next_leb128().ok_or(())?; + let id = self.messages.next_leb128::().ok_or(())?; + Ok(Some(BroadcastEvent::RegistryChange( + RegistryChange::Delete(Id::new(Object::from_id(object_id).ok_or(())?, id)), + ))) + } + 6 => { + let object_id = self.messages.next_leb128().ok_or(())?; + Ok(Some(BroadcastEvent::RegistryChange( + RegistryChange::Reload(Object::from_id(object_id).ok_or(())?), + ))) + } + 7 => { + let count = self.messages.next_leb128::().ok_or(())?; + let mut items = Vec::with_capacity(count); + for _ in 0..count { + let marker = self.messages.next().ok_or(())?.borrow().to_owned(); + let id = self.messages.next_leb128::().ok_or(())?; + items.push(match marker { + 0 => CacheInvalidation::AccessToken(id), + 1 => CacheInvalidation::DavResources(id), + 2 => CacheInvalidation::Domain(id), + 3 => CacheInvalidation::Account(id), + 4 => CacheInvalidation::Group(id), + 5 => CacheInvalidation::Tenant(id), + 6 => CacheInvalidation::Role(id), + 7 => CacheInvalidation::List(id), + _ => return Err(()), + }); + } + Ok(Some(BroadcastEvent::CacheInvalidation(items))) } - - 8 => Ok(Some(BroadcastEvent::ReloadSpamFilter)), _ => Err(()), } diff --git a/crates/services/src/broadcast/publisher.rs b/crates/services/src/broadcast/publisher.rs index 1af5ea12..cf81f287 100644 --- a/crates/services/src/broadcast/publisher.rs +++ b/crates/services/src/broadcast/publisher.rs @@ -13,13 +13,13 @@ use trc::ClusterEvent; use super::{BROADCAST_TOPIC, BroadcastBatch}; pub fn spawn_broadcast_publisher(inner: Arc, mut event_rx: mpsc::Receiver) { - let (pubsub, this_node_id) = { + let (coordinator, this_node_id) = { let _core = inner.shared_core.load(); - let pubsub = inner.shared_core.load().storage.pubsub.clone(); - if pubsub.is_none() { + let coordinator = inner.shared_core.load().storage.coordinator.clone(); + if coordinator.is_none() { return; } - (pubsub, _core.network.node_id as u16) + (coordinator, _core.network.node_id as u16) }; tokio::spawn(async move { @@ -36,7 +36,7 @@ pub fn spawn_broadcast_publisher(inner: Arc, mut event_rx: mpsc::Receiver } } - match pubsub + match coordinator .publish(BROADCAST_TOPIC, batch.serialize(this_node_id)) .await { diff --git a/crates/services/src/broadcast/subscriber.rs b/crates/services/src/broadcast/subscriber.rs index 6dce0a7a..38aeb833 100644 --- a/crates/services/src/broadcast/subscriber.rs +++ b/crates/services/src/broadcast/subscriber.rs @@ -6,11 +6,10 @@ use crate::broadcast::{BROADCAST_TOPIC, BroadcastBatch}; use common::{ - Inner, - core::BuildServer, - ipc::{BroadcastEvent, HousekeeperEvent, PushEvent, PushNotification}, + BuildServer, Inner, + ipc::{BroadcastEvent, HousekeeperEvent, PushEvent, PushNotification, RegistryChange}, }; -use compact_str::CompactString; +use registry::types::EnumType; use std::{sync::Arc, time::Duration}; use tokio::sync::watch; use trc::{ClusterEvent, ServerEvent}; @@ -18,7 +17,7 @@ use trc::{ClusterEvent, ServerEvent}; pub fn spawn_broadcast_subscriber(inner: Arc, mut shutdown_rx: watch::Receiver) { let this_node_id = { let _core = inner.shared_core.load(); - if _core.storage.pubsub.is_none() { + if _core.storage.coordinator.is_none() { return; } _core.network.node_id as u16 @@ -30,16 +29,16 @@ pub fn spawn_broadcast_subscriber(inner: Arc, mut shutdown_rx: watch::Rec trc::event!(Cluster(ClusterEvent::SubscriberStart)); loop { - let pubsub = inner.shared_core.load().storage.pubsub.clone(); - if pubsub.is_none() { + let coordinator = inner.shared_core.load().storage.coordinator.clone(); + if coordinator.is_none() { trc::event!( Cluster(ClusterEvent::SubscriberError), - Details = "PubSub is no longer configured" + Details = "Coordinator is no longer configured" ); break; } - let mut stream = match pubsub.subscribe(BROADCAST_TOPIC).await { + let mut stream = match coordinator.subscribe(BROADCAST_TOPIC).await { Ok(stream) => { retry_count = 0; stream @@ -122,7 +121,7 @@ pub fn spawn_broadcast_subscriber(inner: Arc, mut shutdown_rx: watch::Rec ); } } - BroadcastEvent::ReloadPushServers(account_id) => { + BroadcastEvent::PushServerUpdate(account_id) => { if inner .ipc .push_tx @@ -137,22 +136,12 @@ pub fn spawn_broadcast_subscriber(inner: Arc, mut shutdown_rx: watch::Rec ); } } - BroadcastEvent::InvalidateAccessTokens(ids) => { - for id in &ids { - inner.cache.permissions.remove(id); - inner.cache.access_tokens.remove(id); - } + BroadcastEvent::CacheInvalidation(changes) => { + inner.build_server().invalidate_caches(changes, false).await; + } - BroadcastEvent::InvalidateGroupwareCache(ids) => { - for id in &ids { - inner.cache.files.remove(id); - inner.cache.contacts.remove(id); - inner.cache.events.remove(id); - inner.cache.scheduling.remove(id); - } - } - BroadcastEvent::ReloadSettings => { - match inner.build_server().reload().await { + BroadcastEvent::RegistryChange(change) => { + match inner.build_server().reload_registry(change).await { Ok(result) => { if let Some(new_core) = result.new_core { // Update core @@ -181,22 +170,6 @@ pub fn spawn_broadcast_subscriber(inner: Arc, mut shutdown_rx: watch::Rec } } } - BroadcastEvent::ReloadBlockedIps => { - if let Err(err) = inner.build_server().reload_blocked_ips().await { - trc::error!( - err.details("Failed to reload settings") - .caused_by(trc::location!()) - ); - } - } - BroadcastEvent::ReloadSpamFilter => { - if let Err(err) = inner.build_server().spam_model_reload().await { - trc::error!( - err.details("Failed to reload spam filter model") - .caused_by(trc::location!()) - ); - } - } } } Ok(None) => break, @@ -251,27 +224,31 @@ fn log_event(event: &BroadcastEvent) -> trc::Value { email_push.change_id.into(), ]), }, - BroadcastEvent::ReloadSettings => CompactString::const_new("ReloadSettings").into(), - BroadcastEvent::ReloadBlockedIps => CompactString::const_new("ReloadBlockedIps").into(), - BroadcastEvent::InvalidateAccessTokens(items) => { + BroadcastEvent::PushServerUpdate(account_id) => { + trc::Value::Array(vec!["PushServerUpdate".into(), (*account_id).into()]) + } + BroadcastEvent::RegistryChange(change) => match change { + RegistryChange::Insert(id) => trc::Value::Array(vec![ + "RegistryInsert".into(), + id.object().as_str().into(), + id.id().into(), + ]), + RegistryChange::Delete(id) => trc::Value::Array(vec![ + "RegistryDelete".into(), + id.object().as_str().into(), + id.id().into(), + ]), + RegistryChange::Reload(object) => { + trc::Value::Array(vec!["RegistryReload".into(), object.as_str().into()]) + } + }, + BroadcastEvent::CacheInvalidation(items) => { let mut array = Vec::with_capacity(items.len() + 1); - array.push("InvalidateAccessTokens".into()); + array.push("CacheInvalidation".into()); for item in items { - array.push((*item).into()); + array.push(format!("{:?}", item).into()); } trc::Value::Array(array) } - BroadcastEvent::InvalidateGroupwareCache(items) => { - let mut array = Vec::with_capacity(items.len() + 1); - array.push("InvalidateGroupwareCache".into()); - for item in items { - array.push((*item).into()); - } - trc::Value::Array(array) - } - BroadcastEvent::ReloadPushServers(account_id) => { - trc::Value::Array(vec!["ReloadPushServers".into(), (*account_id).into()]) - } - BroadcastEvent::ReloadSpamFilter => CompactString::const_new("ReloadSpamFilter").into(), } } diff --git a/crates/services/src/housekeeper/mod.rs b/crates/services/src/housekeeper/mod.rs index 7f027dc7..c9934088 100644 --- a/crates/services/src/housekeeper/mod.rs +++ b/crates/services/src/housekeeper/mod.rs @@ -5,9 +5,8 @@ */ use common::{ - Inner, KV_LOCK_HOUSEKEEPER, LONG_1D_SLUMBER, Server, - config::{spamfilter, telemetry::OtelMetrics}, - core::BuildServer, + BuildServer, Inner, KV_LOCK_HOUSEKEEPER, LONG_1D_SLUMBER, Server, + config::{mailstore::spamfilter, telemetry::OtelMetrics}, ipc::{BroadcastEvent, HousekeeperEvent, PurgeType}, }; use email::message::delete::EmailDeletion; @@ -19,7 +18,7 @@ use std::{ sync::Arc, time::{Duration, Instant, SystemTime}, }; -use store::{PurgeStore, write::now}; +use store::write::now; use tokio::sync::mpsc; use trc::{Collector, MetricType, PurgeEvent}; @@ -41,8 +40,9 @@ struct Action { #[derive(PartialEq, Eq, Debug)] enum ActionClass { - Account, - Store(usize), + PurgeAccount, + PurgeDataStore, + PurgeBlobStore, Acme(String), OtelMetrics, CalculateMetrics, @@ -85,19 +85,21 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver, mut rx: mpsc::Receiver { @@ -153,7 +156,7 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver @@ -254,7 +257,8 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver { @@ -276,7 +280,7 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver, mut rx: mpsc::Receiver { let server = inner.build_server(); tokio::spawn(async move { - server.purge(purge, 0).await; + server.purge(purge).await; }); } HousekeeperEvent::Exit => { @@ -316,7 +320,8 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver { trc::event!(Housekeeper(trc::HousekeeperEvent::Run), Type = "acme"); - let server = server.clone(); + let todo = "fix"; + /*let server = server.clone(); tokio::spawn(async move { if let Some(provider) = server.core.acme.providers.get(&provider_id) @@ -362,9 +367,9 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver { + ActionClass::PurgeAccount => { trc::event!( Housekeeper(trc::HousekeeperEvent::Run), Type = "purge_account" @@ -373,59 +378,63 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver { - if let Some(schedule) = - server.core.storage.purge_schedules.get(idx).cloned() - { - trc::event!( - Housekeeper(trc::HousekeeperEvent::Run), - Type = "purge_store", - Id = idx - ); + ActionClass::PurgeDataStore => { + trc::event!( + Housekeeper(trc::HousekeeperEvent::Run), + Type = "purge_data_store" + ); - queue.schedule( - Instant::now() + schedule.cron.time_to_next(), - ActionClass::Store(idx), - ); + queue.schedule( + Instant::now() + + server.core.email.data_purge_frequency.time_to_next(), + ActionClass::PurgeDataStore, + ); + let server_ = server.clone(); + let store = server.store().clone(); + tokio::spawn(async move { + server_.purge(PurgeType::Data(store)).await; + }); - let server = server.clone(); - tokio::spawn(async move { - server - .purge( - match schedule.store { - PurgeStore::Data(store) => { - PurgeType::Data(store) - } - PurgeStore::Blobs { store, blob_store } => { - PurgeType::Blobs { store, blob_store } - } - PurgeStore::Lookup(in_memory_store) => { - PurgeType::Lookup { - store: in_memory_store, - prefix: None, - } - } - }, - idx as u32, - ) - .await; - }); - } + let server = server.clone(); + let store = server.in_memory_store().clone(); + tokio::spawn(async move { + server + .purge(PurgeType::Lookup { + store, + prefix: None, + }) + .await; + }); + } + ActionClass::PurgeBlobStore => { + trc::event!( + Housekeeper(trc::HousekeeperEvent::Run), + Type = "purge_blob_store" + ); + + queue.schedule( + Instant::now() + + server.core.email.blob_purge_frequency.time_to_next(), + ActionClass::PurgeBlobStore, + ); + let server = server.clone(); + let store = server.store().clone(); + let blob_store = server.blob_store().clone(); + tokio::spawn(async move { + server.purge(PurgeType::Blobs { store, blob_store }).await; + }); } ActionClass::OtelMetrics => { if let Some(otel) = &server.core.metrics.otel { @@ -664,14 +673,22 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver { + use common::ipc::RegistryChange; + use registry::schema::prelude::Object; + trc::event!( Housekeeper(trc::HousekeeperEvent::Run), Type = "renew_license" ); - match server.reload().await { + match server + .reload_registry(RegistryChange::Reload(Object::Enterprise)) + .await + { Ok(result) => { if let Some(new_core) = result.new_core { + use registry::schema::prelude::Object; + if let Some(enterprise) = &new_core.enterprise { let renew_in = if enterprise.license.is_near_expiration() { @@ -698,7 +715,9 @@ pub fn spawn_housekeeper(inner: Arc, mut rx: mpsc::Receiver, mut rx: mpsc::Receiver impl Future + Send; + fn purge(&self, purge: PurgeType) -> impl Future + Send; } impl Purge for Server { - async fn purge(&self, purge: PurgeType, store_idx: u32) { + async fn purge(&self, purge: PurgeType) { // Lock task let (lock_type, lock_name) = match &purge { - PurgeType::Data(_) => ( - "data", - [0u8] - .into_iter() - .chain(store_idx.to_be_bytes().into_iter()) - .collect::>() - .into(), - ), - PurgeType::Blobs { .. } => ( - "blob", - [1u8] - .into_iter() - .chain(store_idx.to_be_bytes().into_iter()) - .collect::>() - .into(), - ), - PurgeType::Lookup { prefix: None, .. } => ( - "in-memory", - [2u8] - .into_iter() - .chain(store_idx.to_be_bytes().into_iter()) - .collect::>() - .into(), - ), + PurgeType::Data(_) => ("data", [0u8].into()), + PurgeType::Blobs { .. } => ("blob", [1u8].into()), + PurgeType::Lookup { prefix: None, .. } => ("in-memory", [2u8].into()), PurgeType::Lookup { .. } => ("in-memory-prefix", None), PurgeType::Account { .. } => ("account", None), }; if let Some(lock_name) = &lock_name { match self - .core - .storage - .lookup + .in_memory_store() .try_lock(KV_LOCK_HOUSEKEEPER, lock_name, 3600) .await { @@ -770,7 +766,7 @@ impl Purge for Server { } } - trc::event!(Purge(PurgeEvent::Started), Type = lock_type, Id = store_idx); + trc::event!(Purge(PurgeEvent::Started), Type = lock_type); let time = Instant::now(); match purge { @@ -861,7 +857,6 @@ impl Purge for Server { trc::event!( Purge(PurgeEvent::Finished), Type = lock_type, - Id = store_idx, Elapsed = time.elapsed() ); diff --git a/crates/services/src/state_manager/manager.rs b/crates/services/src/state_manager/manager.rs index 865be340..121d9939 100644 --- a/crates/services/src/state_manager/manager.rs +++ b/crates/services/src/state_manager/manager.rs @@ -146,7 +146,7 @@ pub fn spawn_push_router(inner: Arc, mut change_rx: mpsc::Receiver trc::Result<(PushSubscriptions, Vec)> { let member_of = server - .get_access_token(account_id) + .access_token(account_id) .await .caused_by(trc::location!())? + .build() .member_ids() .collect::>(); diff --git a/crates/services/src/task_manager/alarm.rs b/crates/services/src/task_manager/alarm.rs index ba0d6982..963808d4 100644 --- a/crates/services/src/task_manager/alarm.rs +++ b/crates/services/src/task_manager/alarm.rs @@ -11,13 +11,12 @@ use calcard::{ use chrono::{DateTime, Locale}; use common::{ DEFAULT_LOGO_BASE64, Server, - auth::AccessToken, + auth::{AccountInfo, BuildAccessToken}, config::groupware::CalendarTemplateVariable, i18n, ipc::{CalendarAlert, PushNotification}, - listener::{ServerInstance, stream::NullIo}, + network::{ServerInstance, stream::NullIo}, }; -use registry::schema::enums::Permission; use groupware::calendar::{ ArchivedCalendarEvent, CalendarEvent, alarm::{CalendarAlarm, CalendarAlarmType}, @@ -28,6 +27,7 @@ use mail_builder::{ mime::{BodyPart, MimePart}, }; use mail_parser::decoders::html::html_to_text; +use registry::{schema::enums::Permission, types::EnumType}; use smtp::core::{Session, SessionData}; use smtp_proto::{MailFrom, RcptTo}; use std::{str::FromStr, sync::Arc, time::Duration}; @@ -100,9 +100,10 @@ async fn send_email_alarm( ) -> trc::Result { // Obtain access token let access_token = server - .get_access_token(account_id) + .access_token(account_id) .await - .caused_by(trc::location!())?; + .caused_by(trc::location!())? + .build(); if !access_token.has_permission(Permission::CalendarAlarms) { trc::event!( @@ -112,7 +113,13 @@ async fn send_email_alarm( DocumentId = document_id, ); return Ok(true); - } else if access_token.emails.is_empty() { + } + let account_info = server + .account_info(account_id) + .await + .caused_by(trc::location!())?; + + if account_info.name().is_empty() { trc::event!( Calendar(trc::CalendarEvent::AlarmFailed), Reason = "Account does not have any email addresses", @@ -149,12 +156,12 @@ async fn send_email_alarm( .caused_by(trc::location!())?; // Build message body - let account_main_email = access_token.emails.first().unwrap(); + let account_main_email = account_info.name(); let account_main_domain = account_main_email.rsplit('@').next().unwrap_or("localhost"); let logo_cid = format!("logo.{}@{account_main_domain}", now()); let Some(tpl) = build_template( server, - &access_token, + &account_info, account_id, document_id, alarm, @@ -238,7 +245,7 @@ async fn send_email_alarm( let mut session = Session::::local( server_, server_instance, - SessionData::local(access_token, None, vec![], vec![], 0), + SessionData::local(account_info, None, vec![], vec![], 0), ); // MAIL FROM @@ -434,7 +441,7 @@ struct Details { async fn build_template( server: &Server, - access_token: &AccessToken, + account_info: &AccountInfo, account_id: u32, document_id: u32, alarm: &CalendarAlarm, @@ -455,7 +462,7 @@ async fn build_template( }; // Build webcal URI - let webcal_uri = match event.webcal_uri(server, access_token).await { + let webcal_uri = match event.webcal_uri(server, account_info).await { Ok(uri) => uri, Err(err) => { trc::error!( @@ -536,7 +543,7 @@ async fn build_template( // Validate recipient let rcpt_to = if let Some(rcpt_to) = rcpt_to { if server.core.groupware.alarms_allow_external_recipients - || access_token.emails.iter().any(|email| email == &rcpt_to) + || account_info.addresses().any(|email| email == &rcpt_to) { rcpt_to } else { @@ -548,10 +555,10 @@ async fn build_template( DocumentId = document_id, ); - access_token.emails.first().unwrap().to_string() + account_info.name().to_string() } } else { - access_token.emails.first().unwrap().to_string() + account_info.name().to_string() }; // SPDX-SnippetBegin @@ -568,12 +575,8 @@ async fn build_template( #[cfg(not(feature = "enterprise"))] let template = &server.core.groupware.alarms_template; - let locale = i18n::locale_or_default(access_token.locale.as_deref().unwrap_or("en")); - let chrono_locale = access_token - .locale - .as_deref() - .and_then(|locale| Locale::from_str(locale).ok()) - .unwrap_or(Locale::en_US); + let locale = i18n::locale_or_default(account_info.locale().as_str()); + let chrono_locale = Locale::from_str(account_info.locale().as_str()).unwrap_or(Locale::en_US); let (event_start, event_start_tz, event_end, event_end_tz) = match alarm.typ { CalendarAlarmType::Email { event_start, @@ -617,7 +620,7 @@ async fn build_template( (None, Some(name)) => name.to_string(), _ => unreachable!(), }) - .unwrap_or_else(|| access_token.name.clone()); + .unwrap_or_else(|| account_info.name().to_string()); let mut variables = Variables::new(); variables.insert_single(CalendarTemplateVariable::PageTitle, subject.as_str()); variables.insert_single( diff --git a/crates/services/src/task_manager/imip.rs b/crates/services/src/task_manager/imip.rs index dce407fc..ff2c93af 100644 --- a/crates/services/src/task_manager/imip.rs +++ b/crates/services/src/task_manager/imip.rs @@ -15,10 +15,10 @@ use calcard::{ use chrono::{DateTime, Locale}; use common::{ DEFAULT_LOGO_BASE64, Server, - auth::AccessToken, + auth::AccountInfo, config::groupware::CalendarTemplateVariable, i18n, - listener::{ServerInstance, stream::NullIo}, + network::{ServerInstance, stream::NullIo}, }; use groupware::{ calendar::itip::ItipIngest, @@ -30,6 +30,7 @@ use mail_builder::{ mime::{BodyPart, MimePart}, }; use mail_parser::decoders::html::html_to_text; +use registry::types::EnumType; use smtp::core::{Session, SessionData}; use smtp_proto::{MailFrom, RcptTo}; use std::{str::FromStr, sync::Arc, time::Duration}; @@ -82,12 +83,6 @@ async fn send_imip( due: TaskEpoch, server_instance: Arc, ) -> trc::Result { - // Obtain access token - let access_token = server - .get_access_token(account_id) - .await - .caused_by(trc::location!())?; - // Obtain iMIP payload let Some(archive) = server .store() @@ -149,12 +144,17 @@ async fn send_imip( .inline() .cid(&logo_cid); + let account_info = server + .account_info(account_id) + .await + .caused_by(trc::location!())?; + for itip_message in imip.messages.iter() { for recipient in itip_message.to.iter() { // Build template let tpl = build_itip_template( server, - &access_token, + &account_info, account_id, document_id, itip_message.from.as_str(), @@ -168,10 +168,10 @@ async fn send_imip( // Build message let message = MessageBuilder::new() .from(( - access_token - .description + account_info + .description() .as_deref() - .unwrap_or(access_token.name.as_str()), + .unwrap_or(account_info.name()), itip_message.from.as_str(), )) .to(recipient.as_str()) @@ -218,14 +218,14 @@ async fn send_imip( // Send message let server_ = server.clone(); let server_instance = server_instance.clone(); - let access_token = access_token.clone(); + let account_info = account_info.clone(); let from = itip_message.from.to_string(); let to = recipient.to_string(); tokio::spawn(async move { let mut session = Session::::local( server_, server_instance, - SessionData::local(access_token, None, vec![], vec![], 0), + SessionData::local(account_info, None, vec![], vec![], 0), ); // MAIL FROM @@ -312,7 +312,7 @@ pub struct Details { #[allow(clippy::too_many_arguments)] pub async fn build_itip_template( server: &Server, - access_token: &AccessToken, + account_info: &AccountInfo, account_id: u32, document_id: u32, from: &str, @@ -333,12 +333,8 @@ pub async fn build_itip_template( // SPDX-SnippetEnd #[cfg(not(feature = "enterprise"))] let template = &server.core.groupware.itip_template; - let locale = i18n::locale_or_default(access_token.locale.as_deref().unwrap_or("en")); - let chrono_locale = access_token - .locale - .as_deref() - .and_then(|locale| Locale::from_str(locale).ok()) - .unwrap_or(Locale::en_US); + let locale = i18n::locale_or_default(account_info.locale().as_str()); + let chrono_locale = Locale::from_str(account_info.locale().as_str()).unwrap_or(Locale::en_US); let mut variables = Variables::new(); let mut subject; diff --git a/crates/services/src/task_manager/index.rs b/crates/services/src/task_manager/index.rs index 9c77b76e..709ff4e2 100644 --- a/crates/services/src/task_manager/index.rs +++ b/crates/services/src/task_manager/index.rs @@ -5,14 +5,15 @@ */ use crate::task_manager::{IndexAction, Task}; -use common::{Server, auth::AccessToken}; -use directory::{Type, backend::internal::manage::ManageDirectory}; +use common::Server; use email::{cache::MessageCacheFetch, message::metadata::MessageMetadata}; use groupware::{cache::GroupwareCache, calendar::CalendarEvent, contact::ContactCard}; +use registry::schema::prelude::{Object, Property}; use std::cmp::Ordering; use store::{ IterateParams, SerializeInfallible, ValueKey, ahash::AHashMap, + registry::RegistryQuery, roaring::RoaringBitmap, search::{IndexDocument, SearchField, SearchFilter, SearchQuery}, write::{ @@ -277,26 +278,13 @@ impl ReindexIndexTask for Server { let accounts = if let Some(account_id) = account_id { RoaringBitmap::from_sorted_iter([account_id]).unwrap() } else { - let mut accounts = RoaringBitmap::new(); - for principal in self - .core - .storage - .data - .list_principals( - None, - tenant_id, - &[Type::Individual, Type::Group], - false, - 0, - 0, + self.registry() + .query( + RegistryQuery::new(Object::Account) + .equal_opt(Property::MemberTenantId, tenant_id), ) .await .caused_by(trc::location!())? - .items - { - accounts.insert(principal.id()); - } - accounts }; let due = TaskEpoch::now(); @@ -344,7 +332,7 @@ impl ReindexIndexTask for Server { for account_id in accounts { let cache = self .fetch_dav_resources( - &AccessToken::from_id(account_id).with_tenant_id(tenant_id), + account_id, account_id, if index == SearchIndex::Calendar { SyncCollection::Calendar @@ -453,7 +441,7 @@ async fn build_email_document( account_id: u32, document_id: u32, ) -> trc::Result> { - let Some(index_fields) = server.core.jmap.index_fields.get(&SearchIndex::Email) else { + let Some(index_fields) = server.core.email.index_fields.get(&SearchIndex::Email) else { return Ok(None); }; @@ -488,7 +476,7 @@ async fn build_email_document( document_id, &raw_message, index_fields, - server.core.jmap.default_language, + server.core.email.default_language, ))) } None => Ok(None), @@ -500,7 +488,7 @@ async fn build_calendar_document( account_id: u32, document_id: u32, ) -> trc::Result> { - let Some(index_fields) = server.core.jmap.index_fields.get(&SearchIndex::Calendar) else { + let Some(index_fields) = server.core.email.index_fields.get(&SearchIndex::Calendar) else { return Ok(None); }; @@ -521,7 +509,7 @@ async fn build_calendar_document( account_id, document_id, index_fields, - server.core.jmap.default_language, + server.core.email.default_language, ), )), None => Ok(None), @@ -533,7 +521,7 @@ async fn build_contact_document( account_id: u32, document_id: u32, ) -> trc::Result> { - let Some(index_fields) = server.core.jmap.index_fields.get(&SearchIndex::Contacts) else { + let Some(index_fields) = server.core.email.index_fields.get(&SearchIndex::Contacts) else { return Ok(None); }; @@ -554,7 +542,7 @@ async fn build_contact_document( account_id, document_id, index_fields, - server.core.jmap.default_language, + server.core.email.default_language, ), )), None => Ok(None), @@ -573,7 +561,7 @@ async fn build_tracing_span_document( ) -> trc::Result> { use common::telemetry::tracers::store::{TracingStore, build_span_document}; - let Some(index_fields) = server.core.jmap.index_fields.get(&SearchIndex::Tracing) else { + let Some(index_fields) = server.core.email.index_fields.get(&SearchIndex::Tracing) else { return Ok(None); }; let Some(store) = server diff --git a/crates/services/src/task_manager/mod.rs b/crates/services/src/task_manager/mod.rs index ca2eea26..c4ac779f 100644 --- a/crates/services/src/task_manager/mod.rs +++ b/crates/services/src/task_manager/mod.rs @@ -9,11 +9,11 @@ use crate::task_manager::index::SearchIndexTask; use crate::task_manager::lock::{TaskLock, TaskLockManager}; use crate::task_manager::merge_threads::MergeThreadsTask; use alarm::SendAlarmTask; -use common::IPC_CHANNEL_BUFFER; use common::config::server::ServerProtocol; -use common::listener::limiter::ConcurrencyLimiter; -use common::listener::{ServerInstance, TcpAcceptor}; -use common::{Inner, KV_LOCK_TASK, Server, core::BuildServer}; +use common::network::limiter::ConcurrencyLimiter; +use common::network::{ServerInstance, TcpAcceptor}; +use common::{BuildServer, IPC_CHANNEL_BUFFER}; +use common::{Inner, KV_LOCK_TASK, Server}; use email::message::ingest::MergeThreadIds; use groupware::calendar::alarm::{CalendarAlarm, CalendarAlarmType}; use std::collections::hash_map::Entry; @@ -111,7 +111,7 @@ pub fn spawn_task_manager(inner: Arc) { tokio::spawn(async move { while let Some(task) = rx_index_1.recv().await { let server = inner.build_server(); - let batch_size = server.core.jmap.index_batch_size; + let batch_size = server.core.email.index_batch_size; let mut batch = Vec::with_capacity(batch_size); batch.push(task); diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index c4fb4661..3d05d845 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -18,6 +18,7 @@ nlp = { path = "../nlp" } directory = { path = "../directory" } common = { path = "../common" } email = { path = "../email" } +registry = { path = "../registry" } spam-filter = { path = "../spam-filter" } trc = { path = "../trc" } mail-auth = { path = "/Users/me/code/mail-auth", features = ["rkyv"] } @@ -54,9 +55,10 @@ num_cpus = "1.15.0" chrono = "0.4" rkyv = { version = "0.8.10", features = ["little_endian"] } compact_str = "0.9.0" +hashify = { version = "0.2" } [features] -test_mode = [] +test_mode = ["mail-auth/test"] enterprise = [] #[[bench]] diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index 82202680..fdad2da8 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -7,11 +7,10 @@ use crate::{inbound::auth::SaslToken, queue::QueueId}; use common::{ Inner, Server, - auth::AccessToken, + auth::AccountInfo, config::smtp::auth::VerifyStrategy, - listener::{ServerInstance, asn::AsnGeoLookupResult}, + network::{ServerInstance, asn::AsnGeoLookupResult}, }; -use directory::Directory; use mail_auth::{IprevOutput, SpfOutput}; use smtp_proto::request::receiver::{ BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver, @@ -77,7 +76,7 @@ pub struct SessionData { pub rcpt_oks: usize, pub message: Vec, - pub authenticated_as: Option>, + pub authenticated_as: Option, pub auth_errors: usize, pub priority: i16, @@ -113,7 +112,6 @@ pub struct SessionParameters { pub ehlo_reject_non_fqdn: bool, // Auth parameters - pub auth_directory: Option>, pub auth_require: bool, pub auth_errors_max: usize, pub auth_errors_wait: Duration, @@ -208,7 +206,7 @@ impl PartialOrd for SessionAddress { } } -impl Session { +impl Session { pub fn local( server: Server, instance: std::sync::Arc, @@ -219,13 +217,12 @@ impl Session { state: State::None, instance, server, - stream: common::listener::stream::NullIo::default(), + stream: common::network::stream::NullIo::default(), data, params: SessionParameters { timeout: Default::default(), ehlo_require: Default::default(), ehlo_reject_non_fqdn: Default::default(), - auth_directory: Default::default(), auth_require: Default::default(), auth_errors_max: Default::default(), auth_errors_wait: Default::default(), @@ -260,7 +257,7 @@ impl Session { impl SessionData { pub fn local( - authenticated_as: Arc, + authenticated_as: AccountInfo, mail_from: Option, rcpt_to: Vec, message: Vec, @@ -297,11 +294,11 @@ impl SessionData { } } -impl Default for SessionData { +/*impl Default for SessionData { fn default() -> Self { - Self::local(Arc::new(AccessToken::from_id(0)), None, vec![], vec![], 0) + Self::local(AccessToken::from_id(0), None, vec![], vec![], 0) } -} +}*/ impl SessionAddress { pub fn new(address: String) -> Self { diff --git a/crates/smtp/src/core/params.rs b/crates/smtp/src/core/params.rs index 45589942..8cba1933 100644 --- a/crates/smtp/src/core/params.rs +++ b/crates/smtp/src/core/params.rs @@ -4,11 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Duration; - -use common::{config::smtp::auth::VerifyStrategy, listener::SessionStream}; - use super::Session; +use common::{config::smtp::auth::VerifyStrategy, network::SessionStream}; +use std::time::Duration; impl Session { pub async fn eval_session_params(&mut self) { @@ -72,12 +70,6 @@ impl Session { // Auth parameters let ac = &self.server.core.smtp.session.auth; - self.params.auth_directory = self - .server - .eval_if::(&ac.directory, self, self.data.session_id) - .await - .and_then(|name| self.server.get_directory(&name)) - .cloned(); self.params.auth_require = self .server .eval_if(&ac.require, self, self.data.session_id) diff --git a/crates/smtp/src/core/throttle.rs b/crates/smtp/src/core/throttle.rs index 81c0917c..f7b9e1d2 100644 --- a/crates/smtp/src/core/throttle.rs +++ b/crates/smtp/src/core/throttle.rs @@ -4,17 +4,14 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::Session; use common::{ - KV_RATE_LIMIT_SMTP, ThrottleKey, - config::smtp::*, - expr::{functions::ResolveVariable, *}, - listener::SessionStream, + KV_RATE_LIMIT_SMTP, ThrottleKey, config::smtp::*, expr::functions::ResolveVariable, + network::SessionStream, }; use queue::QueueQuota; +use registry::schema::{enums::ExpressionVariable, prelude::Property, structs::Rate}; use trc::SmtpEvent; -use utils::config::Rate; - -use super::Session; pub trait NewKey: Sized { fn new_key(&self, e: &impl ResolveVariable, context: &str) -> ThrottleKey; @@ -148,13 +145,21 @@ impl NewKey for QueueRateLimiter { ); } if (self.keys & THROTTLE_REMOTE_IP) != 0 { - hasher.update(e.resolve_variable(ExpressionVariable::RemoteIp).to_string().as_bytes()); + hasher.update( + e.resolve_variable(ExpressionVariable::RemoteIp) + .to_string() + .as_bytes(), + ); } if (self.keys & THROTTLE_LOCAL_IP) != 0 { - hasher.update(e.resolve_variable(ExpressionVariable::LocalIp).to_string().as_bytes()); + hasher.update( + e.resolve_variable(ExpressionVariable::LocalIp) + .to_string() + .as_bytes(), + ); } hasher.update(&self.rate.period.as_secs().to_be_bytes()[..]); - hasher.update(&self.rate.requests.to_be_bytes()[..]); + hasher.update(&self.rate.count.to_be_bytes()[..]); hasher.update(context.as_bytes()); ThrottleKey { @@ -177,7 +182,7 @@ impl Session { if t.expr.is_empty() || self .server - .eval_expr(&t.expr, self, "throttle", self.data.session_id) + .eval_expr(&t.expr, self, t.id, Property::Match, self.data.session_id) .await .unwrap_or(false) { @@ -200,9 +205,7 @@ impl Session { // Check rate match self .server - .core - .storage - .lookup + .in_memory_store() .is_rate_allowed(KV_RATE_LIMIT_SMTP, key.hash.as_slice(), &t.rate, false) .await { @@ -210,10 +213,10 @@ impl Session { trc::event!( Smtp(SmtpEvent::RateLimitExceeded), SpanId = self.data.session_id, - Id = t.id.clone(), + Id = t.id.to_string(), Limit = vec![ - trc::Value::from(t.rate.requests), - trc::Value::from(t.rate.period) + trc::Value::from(t.rate.count), + trc::Value::from(t.rate.period.into_inner()) ], ); @@ -238,13 +241,11 @@ impl Session { hasher.update(rcpt.as_bytes()); hasher.update(ctx.as_bytes()); hasher.update(&rate.period.as_secs().to_ne_bytes()[..]); - hasher.update(&rate.requests.to_ne_bytes()[..]); + hasher.update(&rate.count.to_ne_bytes()[..]); match self .server - .core - .storage - .lookup + .in_memory_store() .is_rate_allowed( KV_RATE_LIMIT_SMTP, hasher.finalize().as_bytes(), diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index 3e16ceba..51080d75 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -4,25 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{ - auth::{ - AuthRequest, - sasl::{sasl_decode_challenge_oauth, sasl_decode_challenge_plain}, - }, - listener::SessionStream, -}; - -use registry::schema::enums::Permission; +use common::{auth::AuthRequest, network::SessionStream}; +use directory::Credentials; use mail_parser::decoders::base64::base64_decode; -use mail_send::Credentials; +use registry::schema::enums::Permission; use smtp_proto::{AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2, IntoString}; -use trc::{AuthEvent, SmtpEvent}; +use trc::AuthEvent; use crate::core::Session; pub struct SaslToken { mechanism: u64, - credentials: Credentials, + credentials: Credentials, } impl SaslToken { @@ -30,7 +23,7 @@ impl SaslToken { match mechanism { AUTH_PLAIN | AUTH_LOGIN => SaslToken { mechanism, - credentials: Credentials::Plain { + credentials: Credentials::Basic { username: String::new(), secret: String::new(), }, @@ -38,7 +31,8 @@ impl SaslToken { .into(), AUTH_OAUTHBEARER | AUTH_XOAUTH2 => SaslToken { mechanism, - credentials: Credentials::OAuthBearer { + credentials: Credentials::Bearer { + username: None, token: String::new(), }, } @@ -60,7 +54,7 @@ impl Session { self.write(b"334 Go ahead.\r\n").await?; return Ok(true); } - (AUTH_LOGIN, Credentials::Plain { username, secret }) => { + (AUTH_LOGIN, Credentials::Basic { username, secret }) => { if username.is_empty() && secret.is_empty() { self.write(b"334 VXNlcm5hbWU6\r\n").await?; return Ok(true); @@ -71,23 +65,29 @@ impl Session { } else if let Some(response) = base64_decode(response) { match (token.mechanism, &mut token.credentials) { (AUTH_PLAIN, _) => { - if let Some(credentials) = sasl_decode_challenge_plain(&response) { + if let Some(credentials) = Credentials::decode_sasl_challenge_plain(&response) { return self.authenticate(credentials).await; } } - (AUTH_LOGIN, Credentials::Plain { username, secret }) => { + (AUTH_LOGIN, Credentials::Basic { username, secret }) => { return if username.is_empty() { *username = response.into_string(); self.write(b"334 UGFzc3dvcmQ6\r\n").await?; Ok(true) } else { *secret = response.into_string(); - self.authenticate(std::mem::take(&mut token.credentials)) - .await + self.authenticate(std::mem::replace( + &mut token.credentials, + Credentials::Basic { + username: String::new(), + secret: String::new(), + }, + )) + .await }; } (AUTH_OAUTHBEARER | AUTH_XOAUTH2, _) => { - if let Some(credentials) = sasl_decode_challenge_oauth(&response) { + if let Some(credentials) = Credentials::decode_sasl_challenge_oauth(&response) { return self.authenticate(credentials).await; } } @@ -98,79 +98,71 @@ impl Session { self.auth_error(b"500 5.5.6 Invalid challenge.\r\n").await } - pub async fn authenticate(&mut self, credentials: Credentials) -> Result { - if let Some(directory) = &self.params.auth_directory { - // Authenticate - let result = self - .server - .authenticate( - &AuthRequest::from_credentials( - credentials, - self.data.session_id, - self.data.remote_ip, - ) - .with_directory(directory), - ) - .await - .and_then(|access_token| { - access_token - .assert_has_permission(Permission::EmailSend) - .map(|_| access_token) - }); + pub async fn authenticate(&mut self, credentials: Credentials) -> Result { + // Authenticate + let result = self + .server + .authenticate(&AuthRequest::from_credentials( + credentials, + self.data.session_id, + self.data.remote_ip, + )) + .await + .and_then(|access_token| access_token.assert_has_permission(Permission::EmailSend)); - match result { - Ok(access_token) => { - self.data.authenticated_as = access_token.into(); - self.eval_post_auth_params().await; - self.write(b"235 2.7.0 Authentication succeeded.\r\n") - .await?; - return Ok(false); - } - Err(err) => { - let reason = *err.as_ref(); + let result = match result { + Ok(access_token) => self.server.account_info(access_token.account_id()).await, + Err(err) => Err(err), + }; - trc::error!(err.span_id(self.data.session_id)); + match result { + Ok(account_info) => { + self.data.authenticated_as = account_info.into(); + self.eval_post_auth_params().await; + self.write(b"235 2.7.0 Authentication succeeded.\r\n") + .await?; + return Ok(false); + } + Err(err) => { + let reason = *err.as_ref(); - match reason { - trc::EventType::Auth(trc::AuthEvent::Failed) => { - return self - .auth_error(b"535 5.7.8 Authentication credentials invalid.\r\n") - .await; - } - trc::EventType::Auth(trc::AuthEvent::TokenExpired) => { - return self.auth_error(b"535 5.7.8 OAuth token expired.\r\n").await; - } - trc::EventType::Auth(trc::AuthEvent::MissingTotp) => { - return self + trc::error!(err.span_id(self.data.session_id)); + + match reason { + trc::EventType::Auth(trc::AuthEvent::Failed) => { + return self + .auth_error(b"535 5.7.8 Authentication credentials invalid.\r\n") + .await; + } + trc::EventType::Auth(trc::AuthEvent::TokenExpired) => { + return self.auth_error(b"535 5.7.8 OAuth token expired.\r\n").await; + } + trc::EventType::Auth(trc::AuthEvent::MissingTotp) => { + return self .auth_error( b"334 5.7.8 Missing TOTP token, try with 'secret$totp_code'.\r\n", ) .await; - } - trc::EventType::Security(trc::SecurityEvent::Unauthorized) => { - self.write( - concat!( - "550 5.7.1 Your account is not authorized ", - "to use this service.\r\n" - ) - .as_bytes(), - ) - .await?; - return Ok(false); - } - trc::EventType::Security(_) => { - return Err(()); - } - _ => (), } + trc::EventType::Security(trc::SecurityEvent::Unauthorized) => { + self.write( + concat!( + "550 5.7.1 Your account is not authorized ", + "to use this service.\r\n" + ) + .as_bytes(), + ) + .await?; + return Ok(false); + } + trc::EventType::Security(_) => { + return Err(()); + } + _ => (), } } - } else { - trc::event!( - Smtp(SmtpEvent::MissingAuthDirectory), - SpanId = self.data.session_id, - ); } + self.write(b"454 4.7.0 Temporary authentication failure\r\n") .await?; @@ -196,24 +188,17 @@ impl Session { } pub fn authenticated_as(&self) -> Option<&str> { - self.data.authenticated_as.as_ref().map(|token| { - if !token.name.is_empty() { - token.name.as_str() - } else { - "unavailable" - } - }) + self.data + .authenticated_as + .as_ref() + .map(|authenticated_as| authenticated_as.name()) } pub fn is_authenticated(&self) -> bool { self.data.authenticated_as.is_some() } - pub fn authenticated_emails(&self) -> &[String] { - self.data - .authenticated_as - .as_ref() - .map(|token| token.emails.as_slice()) - .unwrap_or_default() + pub fn authenticated_emails(&self) -> impl Iterator { + self.data.authenticated_as.as_ref().unwrap().addresses() } } diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index 591a1fa3..feb3a7cf 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use super::{ArcSeal, AuthResult, DkimSign}; +use super::{AuthResult, DkimSign}; use crate::{ core::{Session, SessionAddress, State}, inbound::milter::Modification, @@ -17,14 +17,14 @@ use crate::{ }; use common::{ config::{ + mailstore::spamfilter::SpamFilterAction, smtp::{ auth::VerifyStrategy, queue::{QueueExpiry, QueueName}, session::Stage, }, - spamfilter::SpamFilterAction, }, - listener::SessionStream, + network::SessionStream, psl, scripts::ScriptModification, }; @@ -35,6 +35,7 @@ use mail_auth::{ }; use mail_builder::headers::{date::Date, message_id::generate_message_id_header}; use mail_parser::MessageParser; +use registry::schema::structs::Rate; use sieve::runtime::Variable; use smtp_proto::{ MAIL_BY_RETURN, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, @@ -44,7 +45,7 @@ use std::{ time::{Instant, SystemTime}, }; use trc::SmtpEvent; -use utils::{DomainPart, config::Rate}; +use utils::DomainPart; impl Session { pub async fn queue_message(&mut self) -> Cow<'static, [u8]> { @@ -170,12 +171,7 @@ impl Session { .eval_if(&ac.arc.verify, self, self.data.session_id) .await .unwrap_or(VerifyStrategy::Relaxed); - let arc_sealer = self - .server - .eval_if::(&ac.arc.seal, self, self.data.session_id) - .await - .and_then(|name| self.server.get_arc_sealer(&name, self.data.session_id)); - let arc_output = if arc.verify() || arc_sealer.is_some() { + let arc_output = if arc.verify() { let time = Instant::now(); let arc_output = self .server @@ -405,25 +401,6 @@ impl Session { .write_header(&mut headers); } - // ARC Seal - if let (Some(arc_sealer), Some(arc_output)) = (arc_sealer, &arc_output) - && !dkim_output.is_empty() - && arc_output.can_be_sealed() - { - match arc_sealer.seal(&auth_message, &auth_results, arc_output) { - Ok(set) => { - set.write_header(&mut headers); - } - Err(err) => { - trc::error!( - trc::Error::from(err) - .span_id(self.data.session_id) - .details("Failed to ARC seal message") - ); - } - } - } - // Run SPAM filter let mut train_spam = None; if self.server.core.spam.enabled @@ -647,25 +624,35 @@ impl Session { // DKIM sign let raw_message = edited_message.as_deref().unwrap_or(raw_message.as_slice()); - for signer in self + if let Some(sign_with_domain) = self .server - .eval_if::, _>(&ac.dkim.sign, self, self.data.session_id) + .eval_if::(&ac.dkim.sign, self, self.data.session_id) .await - .unwrap_or_default() { - 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); - } - Err(err) => { - trc::error!( - trc::Error::from(err) - .span_id(self.data.session_id) - .details("Failed to DKIM sign message") - ); + match self.server.dkim_signers(&sign_with_domain).await { + Ok(Some(signers)) => { + for signer in signers.as_ref() { + match signer.sign_chained(&[headers.as_ref(), raw_message]) { + Ok(signature) => { + signature.write_header(&mut headers); + } + Err(err) => { + trc::error!( + trc::Error::from(err) + .span_id(self.data.session_id) + .details("Failed to DKIM sign message") + ); + } + } } } + Ok(None) => {} + Err(err) => { + trc::error!( + err.span_id(self.data.session_id) + .details("Failed to retrieve DKIM signers") + ); + } } } diff --git a/crates/smtp/src/inbound/ehlo.rs b/crates/smtp/src/inbound/ehlo.rs index 89ffe213..57343e73 100644 --- a/crates/smtp/src/inbound/ehlo.rs +++ b/crates/smtp/src/inbound/ehlo.rs @@ -7,7 +7,7 @@ use crate::{core::Session, scripts::ScriptResult}; use common::{ config::smtp::session::{Mechanism, Stage}, - listener::SessionStream, + network::SessionStream, }; use mail_auth::{ SpfResult, diff --git a/crates/smtp/src/inbound/hooks/message.rs b/crates/smtp/src/inbound/hooks/message.rs index e2b634c1..695c39f7 100644 --- a/crates/smtp/src/inbound/hooks/message.rs +++ b/crates/smtp/src/inbound/hooks/message.rs @@ -10,7 +10,7 @@ use ahash::AHashMap; use common::{ DAEMON_NAME, config::smtp::session::{MTAHook, Stage}, - listener::SessionStream, + network::SessionStream, }; use mail_auth::AuthenticatedMessage; @@ -192,7 +192,7 @@ impl Session { .as_ref() .and_then(|ip_rev| ip_rev.ptr.as_ref()) .and_then(|ptrs| ptrs.first()) - .map(Into::into), + .map(|ip| ip.to_string()), helo: (!self.data.helo_domain.is_empty()) .then(|| self.data.helo_domain.clone()), active_connections: 1, diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs index 00c83824..116c597b 100644 --- a/crates/smtp/src/inbound/mail.rs +++ b/crates/smtp/src/inbound/mail.rs @@ -8,15 +8,16 @@ use crate::{ core::{Session, SessionAddress}, scripts::ScriptResult, }; -use common::{config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification}; +use common::{config::smtp::session::Stage, network::SessionStream, scripts::ScriptModification}; use mail_auth::{IprevOutput, IprevResult, SpfOutput, SpfResult, spf::verify::SpfParameters}; +use registry::schema::structs::Rate; use smtp_proto::{MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_REQUIRETLS, MailFrom, MtPriority}; use std::{ borrow::Cow, time::{Duration, Instant, SystemTime}, }; use trc::SmtpEvent; -use utils::{DomainPart, config::Rate}; +use utils::DomainPart; impl Session { pub async fn handle_mail_from(&mut self, from: MailFrom>) -> Result<(), ()> { @@ -244,10 +245,7 @@ impl Session { { let address_lcase = self.data.mail_from.as_ref().unwrap().address_lcase.as_str(); if authenticated_as != address_lcase - && !self.authenticated_emails().iter().any(|e| { - e == address_lcase - || (e.starts_with('@') && address_lcase.ends_with(e.as_str())) - }) + && !self.authenticated_emails().any(|e| e == address_lcase) { trc::event!( Smtp(SmtpEvent::MailFromUnauthorized), @@ -257,8 +255,7 @@ impl Session { .into_iter() .chain( self.authenticated_emails() - .iter() - .map(|e| trc::Value::String(e.as_str().into())) + .map(|e| trc::Value::String(e.into())) ) .collect::>() ); @@ -559,16 +556,9 @@ impl Session { .await, ) { // Do not send SPF auth failures to local domains, as they are likely relay attempts (which are blocked later on) - match self - .server - .core - .storage - .directory - .is_local_domain(recipient.domain_part()) - .await - { - Ok(true) => return Ok(result), - Ok(false) => (), + match self.server.domain(recipient.domain_part()).await { + Ok(Some(_)) => return Ok(result), + Ok(None) => (), Err(err) => { trc::error!( err.caused_by(trc::location!()) diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs index f087aca0..955a85f7 100644 --- a/crates/smtp/src/inbound/milter/message.rs +++ b/crates/smtp/src/inbound/milter/message.rs @@ -12,7 +12,7 @@ use crate::{ use common::{ DAEMON_NAME, config::smtp::session::{Milter, Stage}, - listener::SessionStream, + network::SessionStream, }; use mail_auth::AuthenticatedMessage; use smtp_proto::{IntoString, request::parser::Rfc5321Parser}; @@ -194,7 +194,7 @@ impl Session { .as_ref() .and_then(|ip_rev| ip_rev.ptr.as_ref()) .and_then(|ptrs| ptrs.first()) - .map(|s| s.as_str()); + .map(|s| s.as_ref()); client .connection( client_ptr.unwrap_or(self.data.helo_domain.as_str()), diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index 532f889f..b5fdfee6 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -9,9 +9,11 @@ use crate::{ scripts::ScriptResult, }; use common::{ - KV_GREYLIST, config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification, + KV_GREYLIST, + config::smtp::session::Stage, + network::{RcptResolution, SessionStream}, + scripts::ScriptModification, }; -use directory::backend::RcptType; use smtp_proto::{ RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, RcptTo, }; @@ -191,97 +193,73 @@ impl Session { // Verify address let rcpt = self.data.rcpt_to.last().unwrap(); let mut rcpt_members = None; - if let Some(directory) = self - .server - .eval_if::(&rcpt_config.directory, self, self.data.session_id) - .await - .and_then(|name| self.server.get_directory(&name)) - { - match directory.is_local_domain(&rcpt.domain).await { - Ok(true) => { - match self - .server - .rcpt(directory, &rcpt.address_lcase, self.data.session_id) - .await - { - Ok(RcptType::Mailbox) => {} - Ok(RcptType::List(members)) => { - rcpt_members = Some(members); - } - Ok(RcptType::Invalid) => { - trc::event!( - Smtp(SmtpEvent::MailboxDoesNotExist), - SpanId = self.data.session_id, - To = rcpt.address_lcase.clone(), - ); - let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; - return self - .rcpt_error(b"550 5.1.2 Mailbox does not exist.\r\n", rcpt_to) - .await; - } - Err(err) => { - trc::error!( - err.span_id(self.data.session_id) - .caused_by(trc::location!()) - .details("Failed to verify address.") - ); + match self.server.rcpt_resolve(&rcpt.address_lcase).await { + Ok(RcptResolution::Accept) => {} + Ok(RcptResolution::Forward(address) | RcptResolution::Rewrite(address)) => { + let orig_addr = self.data.rcpt_to.pop().unwrap(); + let mut new_addr = SessionAddress::new(address); - self.data.rcpt_to.pop(); - return self - .write(b"451 4.4.3 Unable to verify address at this time.\r\n") - .await; - } - } + if !self.data.rcpt_to.contains(&new_addr) { + new_addr.dsn_info = format!("rfc822;{}", orig_addr.address_lcase).into(); + new_addr.flags = orig_addr.flags; + self.data.rcpt_to.push(new_addr); + } else { + trc::event!( + Smtp(SmtpEvent::RcptToDuplicate), + SpanId = self.data.session_id, + To = new_addr.address_lcase.clone(), + ); + self.data.rcpt_oks += 1; + return self.write(b"250 2.1.5 OK\r\n").await; } - Ok(false) => { - if !self - .server - .eval_if(&rcpt_config.relay, self, self.data.session_id) - .await - .unwrap_or(false) - { - trc::event!( - Smtp(SmtpEvent::RelayNotAllowed), - SpanId = self.data.session_id, - To = rcpt.address_lcase.clone(), - ); + } + Ok(RcptResolution::Expand(members)) => { + rcpt_members = Some(members); + } + Ok(RcptResolution::UnknownRecipient) => { + trc::event!( + Smtp(SmtpEvent::MailboxDoesNotExist), + SpanId = self.data.session_id, + To = rcpt.address_lcase.clone(), + ); - let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; - return self - .rcpt_error(b"550 5.1.2 Relay not allowed.\r\n", rcpt_to) - .await; - } - } - Err(err) => { - trc::error!( - err.span_id(self.data.session_id) - .caused_by(trc::location!()) - .details("Failed to verify address.") + let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; + return self + .rcpt_error(b"550 5.1.2 Mailbox does not exist.\r\n", rcpt_to) + .await; + } + Ok(RcptResolution::UnknownDomain) => { + if !self + .server + .eval_if(&rcpt_config.relay, self, self.data.session_id) + .await + .unwrap_or(false) + { + trc::event!( + Smtp(SmtpEvent::RelayNotAllowed), + SpanId = self.data.session_id, + To = rcpt.address_lcase.clone(), ); - self.data.rcpt_to.pop(); + let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; return self - .write(b"451 4.4.3 Unable to verify address at this time.\r\n") + .rcpt_error(b"550 5.1.2 Relay not allowed.\r\n", rcpt_to) .await; } } - } else if !self - .server - .eval_if(&rcpt_config.relay, self, self.data.session_id) - .await - .unwrap_or(false) - { - trc::event!( - Smtp(SmtpEvent::RelayNotAllowed), - SpanId = self.data.session_id, - To = rcpt.address_lcase.clone(), - ); + Err(err) => { + trc::error!( + err.span_id(self.data.session_id) + .caused_by(trc::location!()) + .details("Failed to verify address.") + ); - let rcpt_to = self.data.rcpt_to.pop().unwrap().address_lcase; - return self - .rcpt_error(b"550 5.1.2 Relay not allowed.\r\n", rcpt_to) - .await; + self.data.rcpt_to.pop(); + return self + .write(b"451 4.4.3 Unable to verify address at this time.\r\n") + .await; + } } if self.is_allowed().await { @@ -375,8 +353,8 @@ impl Session { if let Some(members) = rcpt_members { let list_addr = self.data.rcpt_to.pop().unwrap(); let orcpt = format!("rfc822;{}", list_addr.address_lcase); - for member in members { - let mut member_addr = SessionAddress::new(member); + for member in members.as_ref() { + let mut member_addr = SessionAddress::new(member.to_string()); if !self.data.rcpt_to.contains(&member_addr) && member_addr.address_lcase != list_addr.address_lcase { diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index f479016c..d38bb5e6 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -7,10 +7,11 @@ use common::{ config::{server::ServerProtocol, smtp::session::Mechanism}, expr::{self, functions::ResolveVariable, *}, - listener::SessionStream, + network::SessionStream, }; use compact_str::ToCompactString; +use registry::schema::enums::ExpressionVariable; use smtp_proto::{ request::receiver::{ BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, @@ -95,7 +96,7 @@ impl Session { .await .unwrap_or_default() .into(); - if auth == 0 || self.params.auth_directory.is_none() { + if auth == 0 { trc::event!( Smtp(SmtpEvent::AuthNotAllowed), SpanId = self.data.session_id, diff --git a/crates/smtp/src/inbound/spam.rs b/crates/smtp/src/inbound/spam.rs index bb0eb45d..23bc73db 100644 --- a/crates/smtp/src/inbound/spam.rs +++ b/crates/smtp/src/inbound/spam.rs @@ -4,7 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{config::spamfilter::SpamFilterAction, listener::SessionStream}; +use crate::core::Session; +use common::{config::mailstore::spamfilter::SpamFilterAction, network::SessionStream}; use mail_auth::{ArcOutput, DkimOutput, DmarcResult, dmarc::Policy}; use mail_parser::Message; use spam_filter::{ @@ -15,8 +16,6 @@ use spam_filter::{ }, }; -use crate::core::Session; - impl Session { pub async fn spam_classify<'x>( &'x self, @@ -64,7 +63,7 @@ impl Session { iprev_result: self.data.iprev.as_ref(), remote_ip: self.data.remote_ip, ehlo_domain: self.data.helo_domain.as_str().into(), - authenticated_as: self.data.authenticated_as.as_ref().map(|a| a.name.as_str()), + authenticated_as: self.data.authenticated_as.as_ref().map(|a| a.name()), asn: self.data.asn_geo_data.asn.as_ref().map(|a| a.id), country: self.data.asn_geo_data.country.as_ref().map(|c| c.as_str()), is_tls: self.stream.is_tls(), diff --git a/crates/smtp/src/inbound/spawn.rs b/crates/smtp/src/inbound/spawn.rs index d861899e..b0e9df59 100644 --- a/crates/smtp/src/inbound/spawn.rs +++ b/crates/smtp/src/inbound/spawn.rs @@ -4,24 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::time::Instant; - -use common::{ - config::smtp::session::Stage, - core::BuildServer, - listener::{self, SessionManager, SessionStream}, -}; - -use tokio_rustls::server::TlsStream; -use trc::{SecurityEvent, SmtpEvent}; - use crate::{ core::{Session, SessionData, SessionParameters, SmtpSessionManager, State}, scripts::ScriptResult, }; +use common::{ + BuildServer, + config::smtp::session::Stage, + network::{self, SessionManager, SessionStream}, +}; +use std::time::Instant; +use tokio_rustls::server::TlsStream; +use trc::{SecurityEvent, SmtpEvent}; impl SessionManager for SmtpSessionManager { - async fn handle(self, session: listener::SessionData) { + async fn handle(self, session: network::SessionData) { // Build server and create session let server = self.inner.build_server(); let _in_flight = session.in_flight; diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs index d430196f..32e23eef 100644 --- a/crates/smtp/src/inbound/vrfy.rs +++ b/crates/smtp/src/inbound/vrfy.rs @@ -5,154 +5,110 @@ */ use crate::core::Session; -use common::listener::SessionStream; +use common::network::{RcptResolution, SessionStream}; use std::{borrow::Cow, fmt::Write}; use trc::SmtpEvent; impl Session { pub async fn handle_vrfy(&mut self, address: Cow<'_, str>) -> Result<(), ()> { - match self - .server - .eval_if::( - &self.server.core.smtp.session.rcpt.directory, - self, - self.data.session_id, - ) - .await - .and_then(|name| self.server.get_directory(&name)) - { - Some(directory) if self.params.can_vrfy => { - match self - .server - .vrfy(directory, &address.to_lowercase(), self.data.session_id) - .await - { - Ok(values) if !values.is_empty() => { - let mut result = String::with_capacity(32); - for (pos, value) in values.iter().enumerate() { - let _ = write!( - result, - "250{}{}\r\n", - if pos == values.len() - 1 { " " } else { "-" }, - value - ); - } + if self.params.can_vrfy { + match self.server.rcpt_resolve(&address.to_lowercase()).await { + Ok( + RcptResolution::Accept + | RcptResolution::Forward(_) + | RcptResolution::Rewrite(_) + | RcptResolution::Expand(_), + ) => { + trc::event!( + Smtp(SmtpEvent::Vrfy), + SpanId = self.data.session_id, + To = address.as_ref().to_string(), + ); - trc::event!( - Smtp(SmtpEvent::Vrfy), - SpanId = self.data.session_id, - To = address.as_ref().to_string(), - Result = values, - ); + self.write(format!("250 {}\r\n", address.as_ref()).as_bytes()) + .await + } + Ok(RcptResolution::UnknownRecipient) | Ok(RcptResolution::UnknownDomain) => { + trc::event!( + Smtp(SmtpEvent::VrfyNotFound), + SpanId = self.data.session_id, + To = address.as_ref().to_string(), + ); - self.write(result.as_bytes()).await - } - Ok(_) => { - trc::event!( - Smtp(SmtpEvent::VrfyNotFound), - SpanId = self.data.session_id, - To = address.as_ref().to_string(), - ); + self.write(b"550 5.1.2 Address not found.\r\n").await + } + Err(err) => { + trc::error!( + err.span_id(self.data.session_id) + .caused_by(trc::location!()) + .details("Failed to verify address.") + ); - self.write(b"550 5.1.2 Address not found.\r\n").await - } - Err(err) => { - let is_not_supported = - err.matches(trc::EventType::Store(trc::StoreEvent::NotSupported)); - - trc::error!(err.span_id(self.data.session_id).details("VRFY failed")); - - if !is_not_supported { - self.write(b"252 2.4.3 Unable to verify address at this time.\r\n") - .await - } else { - self.write(b"550 5.1.2 Address not found.\r\n").await - } - } + self.write(b"252 2.4.3 Unable to verify address at this time.\r\n") + .await } } - _ => { - trc::event!( - Smtp(SmtpEvent::VrfyDisabled), - SpanId = self.data.session_id, - To = address.as_ref().to_string(), - ); + } else { + trc::event!( + Smtp(SmtpEvent::VrfyDisabled), + SpanId = self.data.session_id, + To = address.as_ref().to_string(), + ); - self.write(b"252 2.5.1 VRFY is disabled.\r\n").await - } + self.write(b"252 2.5.1 VRFY is disabled.\r\n").await } } pub async fn handle_expn(&mut self, address: Cow<'_, str>) -> Result<(), ()> { - match self - .server - .eval_if::( - &self.server.core.smtp.session.rcpt.directory, - self, - self.data.session_id, - ) - .await - .and_then(|name| self.server.get_directory(&name)) - { - Some(directory) if self.params.can_expn => { - match self - .server - .expn(directory, &address.to_lowercase(), self.data.session_id) - .await - { - Ok(values) if !values.is_empty() => { - let mut result = String::with_capacity(32); - for (pos, value) in values.iter().enumerate() { - let _ = write!( - result, - "250{}{}\r\n", - if pos == values.len() - 1 { " " } else { "-" }, - value - ); - } - - trc::event!( - Smtp(SmtpEvent::Expn), - SpanId = self.data.session_id, - To = address.as_ref().to_string(), - Result = values, + if self.params.can_expn { + match self.server.rcpt_resolve(&address.to_lowercase()).await { + Ok(RcptResolution::Expand(addresses)) => { + let mut result = String::with_capacity(32); + for (pos, value) in addresses.iter().enumerate() { + let _ = write!( + result, + "250{}{}\r\n", + if pos == addresses.len() - 1 { " " } else { "-" }, + value ); - - self.write(result.as_bytes()).await } - Ok(_) => { - trc::event!( - Smtp(SmtpEvent::ExpnNotFound), - SpanId = self.data.session_id, - To = address.as_ref().to_string(), - ); - self.write(b"550 5.1.2 Mailing list not found.\r\n").await - } - Err(err) => { - let is_not_supported = - err.matches(trc::EventType::Store(trc::StoreEvent::NotSupported)); + trc::event!( + Smtp(SmtpEvent::Expn), + SpanId = self.data.session_id, + To = address.as_ref().to_string(), + ); - trc::error!(err.span_id(self.data.session_id).details("VRFY failed")); + self.write(result.as_bytes()).await + } + Ok(_) => { + trc::event!( + Smtp(SmtpEvent::ExpnNotFound), + SpanId = self.data.session_id, + To = address.as_ref().to_string(), + ); - if !is_not_supported { - self.write(b"252 2.4.3 Unable to expand mailing list at this time.\r\n") - .await - } else { - self.write(b"550 5.1.2 Mailing list not found.\r\n").await - } - } + self.write(b"550 5.1.2 Mailing list not found.\r\n").await + } + Err(err) => { + trc::error!( + err.span_id(self.data.session_id) + .caused_by(trc::location!()) + .details("Failed to verify address.") + ); + + self.write(b"252 2.4.3 Unable to expand mailing list at this time.\r\n") + .await } } - _ => { - trc::event!( - Smtp(SmtpEvent::ExpnDisabled), - SpanId = self.data.session_id, - To = address.as_ref().to_string(), - ); + } else { + trc::event!( + Smtp(SmtpEvent::ExpnDisabled), + SpanId = self.data.session_id, + To = address.as_ref().to_string(), + ); - self.write(b"252 2.5.1 EXPN is disabled.\r\n").await - } + self.write(b"252 2.5.1 EXPN is disabled.\r\n").await } } } diff --git a/crates/smtp/src/outbound/dane/dnssec.rs b/crates/smtp/src/outbound/dane/dnssec.rs index 1d83a1e6..b917ea1f 100644 --- a/crates/smtp/src/outbound/dane/dnssec.rs +++ b/crates/smtp/src/outbound/dane/dnssec.rs @@ -9,7 +9,7 @@ use common::{ config::smtp::resolver::{Tlsa, TlsaEntry}, }; use mail_auth::{ - common::resolver::IntoFqdn, + common::resolver::ToFqdn, hickory_resolver::{ Name, proto::rr::rdata::tlsa::{CertUsage, Matching, Selector}, @@ -18,16 +18,16 @@ use mail_auth::{ use std::{future::Future, sync::Arc}; pub trait TlsaLookup: Sync + Send { - fn tlsa_lookup<'x>( + fn tlsa_lookup( &self, - key: impl IntoFqdn<'x> + Sync + Send, + key: impl ToFqdn + Sync + Send, ) -> impl Future>>> + Send; } impl TlsaLookup for Server { - async fn tlsa_lookup<'x>( + async fn tlsa_lookup( &self, - key: impl IntoFqdn<'x> + Sync + Send, + key: impl ToFqdn + Sync + Send, ) -> mail_auth::Result>> { let key = key.to_fqdn(); if let Some(value) = self.inner.cache.dns_tlsa.get(key.as_ref()) { @@ -94,7 +94,7 @@ impl TlsaLookup for Server { }); self.inner.cache.dns_tlsa.insert_with_expiry( - key.into_owned(), + key, tlsa.clone(), tlsa_lookup.valid_until(), ); diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index 408f2ddc..e6c8e833 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -187,7 +187,7 @@ impl QueuedMessage { { trc::event!( Delivery(DeliveryEvent::RateLimitExceeded), - Id = throttle.id.clone(), + Id = throttle.id.to_string(), SpanId = span_id, NextRetry = trc::Value::Timestamp(retry_at) ); @@ -262,7 +262,7 @@ impl QueuedMessage { { trc::event!( Delivery(DeliveryEvent::RateLimitExceeded), - Id = throttle.id.clone(), + Id = throttle.id.to_string(), SpanId = span_id, Domain = domain.to_string(), ); @@ -512,7 +512,7 @@ impl QueuedMessage { Elapsed = time.elapsed(), ); - Arc::new(vec![]) + Arc::new([]) } Err(err) => { trc::event!( @@ -844,7 +844,7 @@ impl QueuedMessage { trc::event!( Delivery(DeliveryEvent::RateLimitExceeded), SpanId = message.span_id, - Id = throttle.id.clone(), + Id = throttle.id.to_string(), RemoteIp = remote_ip, ); delivery_results diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index dde0caa4..2cab02c2 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -9,10 +9,11 @@ use crate::queue::{Error, ErrorDetails, HostResponse, Status}; use common::{ Server, config::smtp::queue::{ConnectionStrategy, IpAndHost, MxConfig}, - expr::{ExpressionVariable::Mx, functions::ResolveVariable}, + expr::functions::ResolveVariable, }; use mail_auth::{IpLookupStrategy, MX}; use rand::{Rng, seq::SliceRandom}; +use registry::schema::enums::ExpressionVariable; use std::{future::Future, net::IpAddr, sync::Arc}; pub struct IpLookupResult { @@ -57,11 +58,11 @@ impl DnsLookup for Server { .await { Ok(addrs) => addrs, - Err(_) if has_ipv6 => Arc::new(Vec::new()), + Err(_) if has_ipv6 => Arc::new([]), Err(err) => return Err(err), } } else { - Arc::new(Vec::new()) + Arc::new([]) }; if has_ipv6 { @@ -74,7 +75,7 @@ impl DnsLookup for Server { .await { Ok(addrs) => addrs, - Err(_) if !ipv4_addrs.is_empty() => Arc::new(Vec::new()), + Err(_) if !ipv4_addrs.is_empty() => Arc::new([]), Err(err) => return Err(err), }; if v4_first { @@ -203,7 +204,7 @@ pub trait ToNextHop { ) -> Option>>; } -impl ToNextHop for Vec { +impl ToNextHop for Arc<[MX]> { fn to_remote_hosts<'x, 'y: 'x>( &'x self, domain: &'y str, @@ -219,7 +220,7 @@ impl ToNextHop for Vec { slice.shuffle(&mut rand::rng()); for remote_host in slice { remote_hosts.push(NextHop::MX { - host: remote_host.as_str(), + host: remote_host.as_ref(), is_implicit: false, config, }); @@ -229,11 +230,11 @@ impl ToNextHop for Vec { } } else if let Some(remote_host) = mx.exchanges.first() { // Check for Null MX - if mx.preference == 0 && remote_host == "." { + if mx.preference == 0 && remote_host.as_ref() == "." { return None; } remote_hosts.push(NextHop::MX { - host: remote_host.as_str(), + host: remote_host.as_ref(), is_implicit: false, config, }); diff --git a/crates/smtp/src/outbound/mta_sts/lookup.rs b/crates/smtp/src/outbound/mta_sts/lookup.rs index 00dddbee..d6ede094 100644 --- a/crates/smtp/src/outbound/mta_sts/lookup.rs +++ b/crates/smtp/src/outbound/mta_sts/lookup.rs @@ -88,7 +88,7 @@ impl MtaStsLookup for Server { )?); self.inner.cache.dns_mta_sts.insert( - domain.to_string(), + domain.into(), policy.clone(), Duration::from_secs(if (3600..31557600).contains(&policy.max_age) { policy.max_age diff --git a/crates/smtp/src/outbound/mta_sts/parse.rs b/crates/smtp/src/outbound/mta_sts/parse.rs index 02263f40..5477f2d8 100644 --- a/crates/smtp/src/outbound/mta_sts/parse.rs +++ b/crates/smtp/src/outbound/mta_sts/parse.rs @@ -27,8 +27,8 @@ impl ParsePolicy for Policy { data = ""; next_data.trim() }; - match key.trim() { - "mx" => { + hashify::fnc_map!(key.trim().as_bytes(), + b"mx" => { if let Some(suffix) = value.strip_prefix("*.") { if !suffix.is_empty() { mx.push(MxPattern::StartsWith(suffix.to_lowercase())); @@ -36,27 +36,27 @@ impl ParsePolicy for Policy { } else if !value.is_empty() { mx.push(MxPattern::Equals(value.to_lowercase())); } - } - "max_age" => { + }, + b"max_age" => { if let Ok(value) = value.parse() { max_age = value; } - } - "mode" => { + }, + b"mode" => { mode = match value { "enforce" => Mode::Enforce, "testing" => Mode::Testing, "none" => Mode::None, _ => return Err(format!("Unsupported mode {value:?}.")), }; - } - "version" => { + }, + b"version" => { if !value.eq_ignore_ascii_case("STSv1") { return Err(format!("Unsupported version {value:?}.")); } - } - _ => (), - } + }, + _ => {} + ); } else { break; } @@ -66,7 +66,7 @@ impl ParsePolicy for Policy { Ok(Policy { id, mode, - mx, + mx: mx.into_boxed_slice(), max_age, }) } else { diff --git a/crates/smtp/src/queue/manager.rs b/crates/smtp/src/queue/manager.rs index 7cc5e42f..26184055 100644 --- a/crates/smtp/src/queue/manager.rs +++ b/crates/smtp/src/queue/manager.rs @@ -8,9 +8,8 @@ use super::{Message, QueueId, Status, spool::SmtpSpool}; use crate::queue::{Recipient, spool::LOCK_EXPIRY}; use ahash::AHashMap; use common::{ - Inner, + BuildServer, Inner, config::smtp::queue::{QueueExpiry, QueueName}, - core::BuildServer, ipc::{QueueEvent, QueueEventStatus}, }; use rand::{Rng, seq::SliceRandom}; diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs index 31b62605..c444c0b0 100644 --- a/crates/smtp/src/queue/mod.rs +++ b/crates/smtp/src/queue/mod.rs @@ -9,6 +9,7 @@ use common::{ expr::{self, functions::ResolveVariable, *}, }; use compact_str::ToCompactString; +use registry::schema::enums::ExpressionVariable; use smtp_proto::Response; use std::{ fmt::Display, diff --git a/crates/smtp/src/queue/quota.rs b/crates/smtp/src/queue/quota.rs index a80be003..c4212bda 100644 --- a/crates/smtp/src/queue/quota.rs +++ b/crates/smtp/src/queue/quota.rs @@ -8,6 +8,7 @@ use super::{QueueEnvelope, QuotaKey, Status}; use crate::{core::throttle::NewKey, queue::MessageWrapper}; use ahash::AHashSet; use common::{Server, config::smtp::queue::QueueQuota, expr::functions::ResolveVariable}; +use registry::schema::prelude::Property; use std::future::Future; use store::{ ValueKey, @@ -49,7 +50,7 @@ impl HasQueueQuota for Server { trc::event!( Queue(QueueEvent::QuotaExceeded), SpanId = message.span_id, - Id = quota.id.clone(), + Id = quota.id.to_string(), Type = "Sender" ); @@ -77,7 +78,7 @@ impl HasQueueQuota for Server { trc::event!( Queue(QueueEvent::QuotaExceeded), SpanId = message.span_id, - Id = quota.id.clone(), + Id = quota.id.to_string(), Type = "Domain" ); @@ -103,7 +104,7 @@ impl HasQueueQuota for Server { trc::event!( Queue(QueueEvent::QuotaExceeded), SpanId = message.span_id, - Id = quota.id.clone(), + Id = quota.id.to_string(), Type = "Recipient" ); @@ -128,7 +129,7 @@ impl HasQueueQuota for Server { ) -> bool { if !quota.expr.is_empty() && self - .eval_expr("a.expr, envelope, "check_quota", session_id) + .eval_expr("a.expr, envelope, quota.id, Property::Match, session_id) .await .unwrap_or(false) { diff --git a/crates/smtp/src/queue/spool.rs b/crates/smtp/src/queue/spool.rs index 82fe9934..2423e379 100644 --- a/crates/smtp/src/queue/spool.rs +++ b/crates/smtp/src/queue/spool.rs @@ -374,7 +374,11 @@ impl MessageWrapper { } if let Err(err) = server .blob_store() - .put_blob(self.message.blob_hash.as_slice(), message.as_ref()) + .put_blob( + self.message.blob_hash.as_slice(), + message.as_ref(), + server.core.storage.compression, + ) .await { trc::error!( diff --git a/crates/smtp/src/queue/throttle.rs b/crates/smtp/src/queue/throttle.rs index 48772b39..d428c137 100644 --- a/crates/smtp/src/queue/throttle.rs +++ b/crates/smtp/src/queue/throttle.rs @@ -8,6 +8,7 @@ use crate::core::throttle::NewKey; use common::{ KV_RATE_LIMIT_SMTP, Server, config::smtp::QueueRateLimiter, expr::functions::ResolveVariable, }; +use registry::schema::prelude::Property; use std::future::Future; use store::write::now; @@ -29,16 +30,20 @@ impl IsAllowed for Server { ) -> Result<(), u64> { if throttle.expr.is_empty() || self - .eval_expr(&throttle.expr, envelope, "throttle", session_id) + .eval_expr( + &throttle.expr, + envelope, + throttle.id, + Property::Match, + session_id, + ) .await .unwrap_or(false) { let key = throttle.new_key(envelope, "outbound"); match self - .core - .storage - .lookup + .in_memory_store() .is_rate_allowed(KV_RATE_LIMIT_SMTP, key.as_ref(), &throttle.rate, false) .await { @@ -46,10 +51,10 @@ impl IsAllowed for Server { trc::event!( Queue(trc::QueueEvent::RateLimitExceeded), SpanId = session_id, - Id = throttle.id.clone(), + Id = throttle.id.to_string(), Limit = vec![ - trc::Value::from(throttle.rate.requests), - trc::Value::from(throttle.rate.period) + trc::Value::from(throttle.rate.count), + trc::Value::from(throttle.rate.period.into_inner()) ], ); diff --git a/crates/smtp/src/reporting/dkim.rs b/crates/smtp/src/reporting/dkim.rs index e1ad911d..620eb9ec 100644 --- a/crates/smtp/src/reporting/dkim.rs +++ b/crates/smtp/src/reporting/dkim.rs @@ -4,15 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::SessionStream; - +use crate::{core::Session, reporting::SmtpReporting}; +use common::network::SessionStream; use mail_auth::{ AuthenticatedMessage, AuthenticationResults, DkimOutput, common::verify::VerifySignature, }; +use registry::schema::structs::Rate; use trc::OutgoingReportEvent; -use utils::config::Rate; - -use crate::{core::Session, reporting::SmtpReporting}; impl Session { pub async fn send_dkim_report( @@ -37,8 +35,8 @@ impl Session { SpanId = self.data.session_id, To = rcpt.to_string(), Limit = vec![ - trc::Value::from(rate.requests), - trc::Value::from(rate.period) + trc::Value::from(rate.count), + trc::Value::from(rate.period.into_inner()) ], ); diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs index 154858b7..d51fd377 100644 --- a/crates/smtp/src/reporting/dmarc.rs +++ b/crates/smtp/src/reporting/dmarc.rs @@ -11,7 +11,7 @@ use common::{ Server, config::smtp::report::AggregateFrequency, ipc::{DmarcEvent, ToHash}, - listener::SessionStream, + network::SessionStream, }; use compact_str::ToCompactString; use mail_auth::{ @@ -21,13 +21,14 @@ use mail_auth::{ dmarc::{self, URI}, report::{AuthFailureType, IdentityAlignment, PolicyPublished, Record, Report, SPFDomainScope}, }; +use registry::schema::structs::Rate; use std::{collections::hash_map::Entry, future::Future}; use store::{ Deserialize, IterateParams, Serialize, ValueKey, write::{AlignedBytes, Archive, Archiver, BatchBuilder, QueueClass, ReportEvent, ValueClass}, }; use trc::{AddContext, OutgoingReportEvent}; -use utils::{DomainPart, config::Rate}; +use utils::DomainPart; #[derive( Debug, @@ -251,8 +252,8 @@ impl Session { OutgoingReport(OutgoingReportEvent::DmarcRateLimited), SpanId = self.data.session_id, Limit = vec![ - trc::Value::from(failure_rate.requests), - trc::Value::from(failure_rate.period) + trc::Value::from(failure_rate.count), + trc::Value::from(failure_rate.period.into_inner()) ], ); } diff --git a/crates/smtp/src/reporting/mod.rs b/crates/smtp/src/reporting/mod.rs index 6cd85bc8..3f2f789c 100644 --- a/crates/smtp/src/reporting/mod.rs +++ b/crates/smtp/src/reporting/mod.rs @@ -202,14 +202,15 @@ impl SmtpReporting for Server { config: &IfBlock, bytes: &[u8], ) -> Option> { - let signers = self - .eval_if::, _>(config, &message.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.get_dkim_signer(signer, message.span_id) { + let sign_with_domain = self + .eval_if::(config, &message.message, message.span_id) + .await?; + + match self.dkim_signers(&sign_with_domain).await { + Ok(Some(signers)) => { + let mut headers = Vec::with_capacity(64); + + for signer in signers.as_ref() { match signer.sign(bytes) { Ok(signature) => { signature.write_header(&mut headers); @@ -224,12 +225,18 @@ impl SmtpReporting for Server { } } } + + Some(headers) } - if !headers.is_empty() { - return Some(headers); + Ok(None) => None, + Err(err) => { + trc::error!( + err.span_id(message.span_id) + .details("Failed to retrieve DKIM signers") + ); + None } } - None } } diff --git a/crates/smtp/src/reporting/scheduler.rs b/crates/smtp/src/reporting/scheduler.rs index bdfa6b42..14d37a37 100644 --- a/crates/smtp/src/reporting/scheduler.rs +++ b/crates/smtp/src/reporting/scheduler.rs @@ -4,9 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{AggregateTimestamp, ReportLock, dmarc::DmarcReporting, tls::TlsReporting}; +use crate::queue::spool::LOCK_EXPIRY; use ahash::AHashMap; -use common::{Inner, KV_LOCK_QUEUE_REPORT, Server, core::BuildServer, ipc::ReportingEvent}; - +use common::{BuildServer, Inner, KV_LOCK_QUEUE_REPORT, Server, ipc::ReportingEvent}; use std::{ future::Future, sync::Arc, @@ -18,10 +19,6 @@ use store::{ }; use tokio::sync::mpsc; -use crate::queue::spool::LOCK_EXPIRY; - -use super::{AggregateTimestamp, ReportLock, dmarc::DmarcReporting, tls::TlsReporting}; - pub const REPORT_REFRESH: Duration = Duration::from_secs(86400); impl SpawnReport for mpsc::Receiver { diff --git a/crates/smtp/src/reporting/spf.rs b/crates/smtp/src/reporting/spf.rs index 0f6d6cac..e2ec9bf9 100644 --- a/crates/smtp/src/reporting/spf.rs +++ b/crates/smtp/src/reporting/spf.rs @@ -4,13 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::listener::SessionStream; - -use mail_auth::{AuthenticationResults, SpfOutput, report::AuthFailureType}; -use trc::OutgoingReportEvent; -use utils::config::Rate; - use crate::{core::Session, reporting::SmtpReporting}; +use common::network::SessionStream; +use mail_auth::{AuthenticationResults, SpfOutput, report::AuthFailureType}; +use registry::schema::structs::Rate; +use trc::OutgoingReportEvent; impl Session { pub async fn send_spf_report( @@ -27,8 +25,8 @@ impl Session { SpanId = self.data.session_id, To = rcpt.to_string(), Limit = vec![ - trc::Value::from(rate.requests), - trc::Value::from(rate.period) + trc::Value::from(rate.count), + trc::Value::from(rate.period.into_inner()) ], ); diff --git a/crates/smtp/src/scripts/event_loop.rs b/crates/smtp/src/scripts/event_loop.rs index bfa565d3..2933d531 100644 --- a/crates/smtp/src/scripts/event_loop.rs +++ b/crates/smtp/src/scripts/event_loop.rs @@ -274,32 +274,45 @@ impl RunScript for Server { instance.message().raw_message().into() }; if let Some(raw_message) = raw_message.filter(|m| !m.is_empty()) { - let headers = if !params.sign.is_empty() { - let mut headers = Vec::new(); + let headers = if let Some(sign_domain) = ¶ms.sign_domain { + match self.dkim_signers(sign_domain).await { + Ok(Some(signers)) => { + let mut headers = Vec::new(); - for dkim in ¶ms.sign { - if let Some(dkim) = self.get_dkim_signer(dkim, session_id) { - match dkim.sign(raw_message) { - Ok(signature) => { - signature.write_header(&mut headers); - } - Err(err) => { - trc::error!( - trc::Error::from(err) - .span_id(session_id) - .caused_by(trc::location!()) - .details("DKIM sign failed") - ); + for signer in signers.as_ref() { + match signer.sign(raw_message) { + Ok(signature) => { + signature.write_header(&mut headers); + } + Err(err) => { + trc::error!( + trc::Error::from(err) + .span_id(session_id) + .caused_by(trc::location!()) + .details("DKIM sign failed") + ); + } } } + + if is_forward { + headers.extend_from_slice( + params.headers.unwrap_or_default(), + ); + } + + Some(Cow::Owned(headers)) + } + Ok(None) => None, + Err(err) => { + trc::error!( + err.details("Failed to obtain DKIM signers") + .caused_by(trc::location!()) + ); + + None } } - - if is_forward { - headers.extend_from_slice(params.headers.unwrap_or_default()); - } - - Some(Cow::Owned(headers)) } else if is_forward { params.headers.map(Cow::Borrowed) } else { diff --git a/crates/smtp/src/scripts/exec.rs b/crates/smtp/src/scripts/exec.rs index 1b7784f8..cc5ac3f9 100644 --- a/crates/smtp/src/scripts/exec.rs +++ b/crates/smtp/src/scripts/exec.rs @@ -6,7 +6,7 @@ use std::{sync::Arc, time::SystemTime}; -use common::listener::SessionStream; +use common::network::SessionStream; use mail_auth::common::resolver::ToReverseName; use sieve::{Envelope, Sieve, runtime::Variable}; diff --git a/crates/smtp/src/scripts/mod.rs b/crates/smtp/src/scripts/mod.rs index d81549be..7ee7d8eb 100644 --- a/crates/smtp/src/scripts/mod.rs +++ b/crates/smtp/src/scripts/mod.rs @@ -39,7 +39,7 @@ pub struct ScriptParameters<'x> { from_addr: String, from_name: String, return_path: String, - sign: Vec, + sign_domain: Option, access_token: Option<&'x AccessToken>, session_id: u64, } @@ -54,7 +54,7 @@ impl<'x> ScriptParameters<'x> { from_addr: Default::default(), from_name: Default::default(), return_path: Default::default(), - sign: Default::default(), + sign_domain: Default::default(), access_token: None, session_id: Default::default(), } @@ -75,12 +75,9 @@ impl<'x> ScriptParameters<'x> { *variable = value; } } - if let Some(value) = server + self.sign_domain = server .eval_if(&server.core.sieve.sign, vars, session_id) - .await - { - self.sign = value; - } + .await; self }