diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index 4873b216..60e45325 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -7,79 +7,56 @@ use super::AccessToken; use crate::{ Server, - ipc::BroadcastEvent, - listener::limiter::{ConcurrencyLimiter, LimiterResult}, + auth::{ + AccessScope, AccessTo, AccessTokenInner, FALLBACK_ADMIN_ID, Permissions, PermissionsGroup, + }, + network::limiter::{ConcurrencyLimiter, LimiterResult}, +}; +use registry::{ + schema::{ + enums::Permission, + structs::{self, Account}, + }, + types::EnumType, }; -use ahash::AHashSet; -use registry::schema::enums::Permission; use std::{ hash::{DefaultHasher, Hash, Hasher}, sync::Arc, }; -use store::{query::acl::AclQuery, rand}; +use store::{query::acl::AclQuery, rand, write::now}; +use tinyvec::TinyVec; use trc::AddContext; use types::{acl::Acl, collection::Collection}; -use utils::map::{ - bitmap::{Bitmap, BitmapItem}, - vec_map::VecMap, -}; +use utils::map::bitmap::{Bitmap, BitmapItem}; impl Server { - async fn build_access_token_from_principal( + async fn build_account_access_token( &self, - principal: Principal, + account: Account, + account_id: u32, revision: u64, - ) -> trc::Result { - let mut role_permissions = PermissionsGroup::default(); - - // Extract data - let mut object_quota = self.core.email.max_objects; - let mut description = None; - let mut tenant_id = None; - let mut quota = None; - let mut locale = None; - let mut member_of = Vec::new(); - let mut emails = Vec::new(); - for data in principal.data { - match data { - PrincipalData::Tenant(v) => tenant_id = Some(v), - PrincipalData::MemberOf(v) => member_of.push(v), - PrincipalData::Role(v) => { - role_permissions.union(self.get_role_permissions(v).await?.as_ref()); - } - PrincipalData::Permission { - permission_id, - grant, - } => { - if grant { - role_permissions.enabled.set(permission_id as usize); - } else { - role_permissions.disabled.set(permission_id as usize); - } - } - PrincipalData::DiskQuota(v) => quota = Some(v), - PrincipalData::ObjectQuota { quota, typ } => { - object_quota[typ as usize] = quota; - } - PrincipalData::Description(v) => description = Some(v), - PrincipalData::PrimaryEmail(v) => { - if emails.is_empty() { - emails.push(v); - } else { - emails.insert(0, v); - } - } - PrincipalData::EmailAlias(v) => { - emails.push(v); - } - PrincipalData::Locale(v) => locale = Some(v), - _ => (), + ) -> trc::Result { + // Calculate effective permissions + let (mut permissions, roles) = match account.permissions { + structs::Permissions::Inherit => { + (PermissionsGroup::default(), account.role_ids.as_slice()) } + structs::Permissions::Merge(permissions) => ( + PermissionsGroup::from(permissions), + account.role_ids.as_slice(), + ), + structs::Permissions::Replace(permissions) => { + (PermissionsGroup::from(permissions), &[][..]) + } + }; + if !roles.is_empty() { + permissions = self + .add_role_permissions(permissions, roles.into_iter().map(|v| v.id() as u32)) + .await + .caused_by(trc::location!())? } - // Apply principal permissions - let mut permissions = role_permissions.finalize(); - let mut tenant = None; + let tenant_id = account.member_tenant_id.map(|t| t.id() as u32); // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC @@ -87,92 +64,56 @@ impl Server { #[cfg(feature = "enterprise")] { - use directory::{QueryParams, ROLE_USER}; - if let Some(tenant_id) = tenant_id { if self.is_enterprise_edition() { // Limit tenant permissions - permissions.intersection(&self.get_role_permissions(tenant_id).await?.enabled); - - // Obtain tenant quota - tenant = Some(TenantInfo { - id: tenant_id, - quota: self - .store() - .query(QueryParams::id(tenant_id).with_return_member_of(false)) + let tenant = self.tenant(tenant_id).await.caused_by(trc::location!())?; + let (mut tenant_permissions, tenant_roles) = + if let Some(permissions) = &tenant.permissions { + if permissions.merge { + ((**permissions).clone(), tenant.id_roles.as_slice()) + } else { + ((**permissions).clone(), &[][..]) + } + } else { + (PermissionsGroup::default(), tenant.id_roles.as_slice()) + }; + if !tenant_roles.is_empty() { + tenant_permissions = self + .add_role_permissions(tenant_permissions, tenant_roles.iter().copied()) .await .caused_by(trc::location!())? - .ok_or_else(|| { - trc::SecurityEvent::Unauthorized - .into_err() - .details("Tenant not found") - .id(tenant_id) - .caused_by(trc::location!()) - })? - .quota() - .unwrap_or_default(), - }); + } + + permissions.restrict(&tenant_permissions); } else { // Enterprise edition downgrade, remove any tenant administrator permissions - permissions.intersection(&self.get_role_permissions(ROLE_USER).await?.enabled); + permissions.restrict(&PermissionsGroup::user()); } } } // SPDX-SnippetEnd - // Build member of and e-mail addresses - for &group_id in &member_of { - if let Some(group) = self - .store() - .query(QueryParams::id(group_id).with_return_member_of(false)) - .await - .caused_by(trc::location!())? - && group.typ == Type::Group - { - emails.extend(group.into_email_addresses()); - } - } - - // Build access token - let mut access_token = AccessToken { - account_id: principal.id, - member_of, - access_to: VecMap::new(), - tenant, - name: principal.name, - description, - emails, - quota: quota.unwrap_or_default(), - locale, - permissions, - object_quota, - concurrent_imap_requests: self.core.imap.rate_concurrent.map(ConcurrencyLimiter::new), - concurrent_http_requests: self - .core - .jmap - .request_max_concurrent - .map(ConcurrencyLimiter::new), - concurrent_uploads: self - .core - .jmap - .upload_max_concurrent - .map(ConcurrencyLimiter::new), - obj_size: 0, - revision, - }; - - for grant_account_id in [access_token.account_id] - .into_iter() - .chain(access_token.member_of.iter().copied()) - { + let can_impersonate = permissions.enabled.get(Permission::Impersonate as usize) + && !permissions.disabled.get(Permission::Impersonate as usize); + let member_of = account + .role_ids + .iter() + .map(|m| m.id() as u32) + .collect::>(); + let mut access_to: Vec = Vec::new(); + for grant_account_id in [account_id].into_iter().chain(member_of.iter().copied()) { for acl_item in self .store() .acl_query(AclQuery::HasAccess { grant_account_id }) .await .caused_by(trc::location!())? { - if !access_token.is_member(acl_item.to_account_id) { + if acl_item.to_account_id != account_id + && !member_of.contains(&acl_item.to_account_id) + && !can_impersonate + { let acl = Bitmap::::from(acl_item.permissions); let collection = acl_item.to_collection; if !collection.is_valid() { @@ -194,248 +135,186 @@ impl Server { } if !collections.is_empty() { - access_token - .access_to - .get_mut_or_insert_with(acl_item.to_account_id, Bitmap::new) - .union(&collections); + if let Some(idx) = access_to + .iter() + .position(|a| a.account_id == acl_item.to_account_id) + { + access_to[idx].collections.union(&collections); + } else { + access_to.push(AccessTo { + account_id: acl_item.to_account_id, + collections, + }); + } } } } } - Ok(access_token.update_size()) - } + let now = now(); + let app_password_scopes = account + .app_passwords + .into_iter() + .filter_map(|pass| { + let expires_at = pass + .expires_at + .map(|v| v.timestamp() as u64) + .unwrap_or(u64::MAX); + if expires_at > now { + let permissions = match pass.permissions { + structs::Permissions::Inherit => permissions.clone().finalize(), + structs::Permissions::Merge(merge) => { + let mut permissions = permissions.clone(); + permissions.union(&PermissionsGroup::from(merge)); + permissions.finalize() + } + structs::Permissions::Replace(replace) => { + PermissionsGroup::from(replace).finalize() + } + }; + Some(AccessScope { + permissions, + expires_at, + }) + } else { + None + } + }) + .collect::>(); - async fn build_access_token(&self, account_id: u32, revision: u64) -> trc::Result { - let err = match self - .directory() - .query(QueryParams::id(account_id).with_return_member_of(true)) - .await - { - Ok(Some(principal)) => { - return self - .build_access_token_from_principal(principal, revision) - .await; - } - Ok(None) => Err(trc::AuthEvent::Error - .into_err() - .details("Account not found.") - .caused_by(trc::location!())), - Err(err) => Err(err), - }; - - match &self.core.network.security.fallback_admin { - Some((_, secret)) if account_id == u32::MAX => { - self.build_access_token_from_principal(Principal::fallback_admin(secret), revision) - .await - } - _ => err, + Ok(AccessTokenInner { + concurrent_imap_requests: self.core.imap.rate_concurrent.map(ConcurrencyLimiter::new), + concurrent_http_requests: self + .core + .jmap + .request_max_concurrent + .map(ConcurrencyLimiter::new), + concurrent_uploads: self + .core + .jmap + .upload_max_concurrent + .map(ConcurrencyLimiter::new), + obj_size: 0, + revision, + account_id, + tenant_id, + member_of, + access_to: access_to.into_boxed_slice(), + scopes: [AccessScope::new(permissions.finalize())] + .into_iter() + .chain(app_password_scopes) + .collect::>(), } + .update_size()) } - pub async fn get_access_token( + pub async fn account_access_token( &self, - principal: impl Into, - ) -> trc::Result> { - let principal = principal.into(); - - // Obtain current revision - let principal_id = principal.id(); - + account_id: u32, + ) -> trc::Result> { match self .inner .cache .access_tokens - .get_value_or_guard_async(&principal_id) + .get_value_or_guard_async(&account_id) .await { Ok(token) => Ok(token), Err(guard) => { let revision = rand::random::(); - let token: Arc = match principal { - PrincipalOrId::Principal(principal) => { - self.build_access_token_from_principal(principal, revision) - .await? - } - PrincipalOrId::Id(account_id) => { - self.build_access_token(account_id, revision).await? - } - } - .into(); + let account = self + .registry() + .object::(account_id) + .await? + .ok_or_else(|| { + trc::SecurityEvent::Unauthorized + .into_err() + .details("Account not found") + .account_id(account_id) + .caused_by(trc::location!()) + })?; + let token: Arc = self + .build_account_access_token(account, account_id, revision) + .await? + .into(); let _ = guard.insert(token.clone()); Ok(token) } } } - pub async fn invalidate_principal_caches(&self, changed_principals: ChangedPrincipals) { - let mut nested_principals = Vec::new(); - let mut changed_ids = AHashSet::new(); - let mut changed_names = Vec::new(); - - for (id, changed_principal) in changed_principals.iter() { - changed_ids.insert(*id); - - if changed_principal.name_change { - self.inner.cache.files.remove(id); - self.inner.cache.contacts.remove(id); - self.inner.cache.events.remove(id); - self.inner.cache.scheduling.remove(id); - changed_names.push(*id); + async fn access_token_from_account( + &self, + account_id: u32, + account: Account, + ) -> trc::Result> { + match self + .inner + .cache + .access_tokens + .get_value_or_guard_async(&account_id) + .await + { + Ok(token) => Ok(token), + Err(guard) => { + let revision = rand::random::(); + let token: Arc = self + .build_account_access_token(account, account_id, revision) + .await? + .into(); + let _ = guard.insert(token.clone()); + Ok(token) } - - if changed_principal.member_change { - if changed_principal.typ == Type::Tenant { - match self - .store() - .list_principals( - None, - (*id).into(), - &[Type::Individual, Type::Group, Type::Role, Type::ApiKey], - false, - 0, - 0, - ) - .await - { - Ok(principals) => { - for principal in principals.items { - changed_ids.insert(principal.id()); - } - } - Err(err) => { - trc::error!( - err.details("Failed to list principals") - .caused_by(trc::location!()) - .account_id(*id) - ); - } - } - } else { - nested_principals.push(*id); - } - } - } - - if !nested_principals.is_empty() { - let mut ids = nested_principals.into_iter(); - let mut ids_stack = vec![]; - - loop { - if let Some(id) = ids.next() { - // Skip if already fetched - if !changed_ids.insert(id) { - continue; - } - - // Obtain principal - match self.store().get_members(id).await { - Ok(members) => { - ids_stack.push(ids); - ids = members.into_iter(); - } - Err(err) => { - trc::error!( - err.details("Failed to obtain principal") - .caused_by(trc::location!()) - .account_id(id) - ); - } - } - } else if let Some(prev_ids) = ids_stack.pop() { - ids = prev_ids; - } else { - break; - } - } - } - - // Invalidate access tokens in cluster - if !changed_ids.is_empty() { - let mut ids = Vec::with_capacity(changed_ids.len()); - for id in changed_ids { - self.inner.cache.permissions.remove(&id); - self.inner.cache.access_tokens.remove(&id); - ids.push(id); - } - self.cluster_broadcast(BroadcastEvent::InvalidateAccessTokens(ids)) - .await; - } - - // Invalidate DAV caches - if !changed_names.is_empty() { - self.cluster_broadcast(BroadcastEvent::InvalidateGroupwareCache(changed_names)) - .await; } } } impl AccessToken { - pub fn from_id(account_id: u32) -> Self { - Self { - account_id, - ..Default::default() - } - } - - pub fn with_access_to(self, access_to: VecMap>) -> Self { - Self { access_to, ..self } - } - - pub fn with_permission(mut self, permission: Permission) -> Self { - self.permissions.set(permission.id() as usize); - self - } - - pub fn with_tenant_id(mut self, tenant_id: Option) -> Self { - self.tenant = tenant_id.map(|id| TenantInfo { id, quota: 0 }); - self - } - pub fn state(&self) -> u32 { // Hash state let mut s = DefaultHasher::new(); - self.member_of.hash(&mut s); - self.access_to.hash(&mut s); + self.inner.member_of.hash(&mut s); + self.inner.access_to.hash(&mut s); s.finish() as u32 } #[inline(always)] pub fn account_id(&self) -> u32 { - self.account_id + self.inner.account_id } #[inline(always)] pub fn tenant_id(&self) -> Option { - self.tenant.as_ref().map(|t| t.id) + self.inner.tenant_id } pub fn secondary_ids(&self) -> impl Iterator { - self.member_of + self.inner + .member_of .iter() - .chain(self.access_to.iter().map(|(id, _)| id)) + .chain(self.inner.access_to.iter().map(|a| &a.account_id)) } pub fn member_ids(&self) -> impl Iterator { - [self.account_id] + [self.inner.account_id] .into_iter() - .chain(self.member_of.iter().copied()) + .chain(self.inner.member_of.iter().copied()) } pub fn all_ids(&self) -> impl Iterator { - [self.account_id] + [self.inner.account_id] .into_iter() - .chain(self.member_of.iter().copied()) - .chain(self.access_to.iter().map(|(id, _)| *id)) + .chain(self.inner.member_of.iter().copied()) + .chain(self.inner.access_to.iter().map(|a| a.account_id)) } pub fn all_ids_by_collection(&self, collection: Collection) -> impl Iterator { - [self.account_id] + [self.inner.account_id] .into_iter() - .chain(self.member_of.iter().copied()) - .chain(self.access_to.iter().filter_map(move |(id, cols)| { - if cols.contains(collection) { - Some(*id) + .chain(self.inner.member_of.iter().copied()) + .chain(self.inner.access_to.iter().filter_map(move |a| { + if a.collections.contains(collection) { + Some(a.account_id) } else { None } @@ -443,18 +322,29 @@ impl AccessToken { } pub fn is_member(&self, account_id: u32) -> bool { - self.account_id == account_id - || self.member_of.contains(&account_id) + self.inner.account_id == account_id + || self.inner.member_of.contains(&account_id) || self.has_permission(Permission::Impersonate) } pub fn is_account_id(&self, account_id: u32) -> bool { - self.account_id == account_id + self.inner.account_id == account_id } #[inline(always)] pub fn has_permission(&self, permission: Permission) -> bool { - self.permissions.get(permission.id() as usize) + self.inner + .scopes + .get(self.scope_id as usize) + .map_or(false, |scope| scope.permissions.get(permission as usize)) + } + + pub fn is_valid(&self) -> bool { + let todo = "use this function"; + self.inner + .scopes + .get(self.scope_id as usize) + .map_or(false, |scope| scope.expires_at > now()) } pub fn assert_has_permission(&self, permission: Permission) -> trc::Result { @@ -463,7 +353,7 @@ impl AccessToken { } else { Err(trc::SecurityEvent::Unauthorized .into_err() - .details(permission.name())) + .details(permission.as_str())) } } @@ -472,14 +362,18 @@ impl AccessToken { const USIZE_MASK: u32 = USIZE_BITS as u32 - 1; let mut permissions = Vec::new(); - for (block_num, bytes) in self.permissions.inner().iter().enumerate() { + let Some(scope) = self.inner.scopes.get(self.scope_id as usize) else { + return permissions; + }; + + for (block_num, bytes) in scope.permissions.inner().iter().enumerate() { let mut bytes = *bytes; while bytes != 0 { let item = USIZE_MASK - bytes.leading_zeros(); bytes ^= 1 << item; if let Some(permission) = - Permission::from_id(((block_num * USIZE_BITS) + item as usize) as u32) + Permission::from_id(((block_num * USIZE_BITS) + item as usize) as u16) { permissions.push(permission); } @@ -488,21 +382,22 @@ impl AccessToken { permissions } - #[inline(always)] - pub fn object_quota(&self, collection: Collection) -> u32 { - self.object_quota[collection as usize] - } - pub fn is_shared(&self, account_id: u32) -> bool { - !self.is_member(account_id) && self.access_to.iter().any(|(id, _)| *id == account_id) + !self.is_member(account_id) + && self + .inner + .access_to + .iter() + .any(|a| a.account_id == account_id) } pub fn shared_accounts(&self, collection: Collection) -> impl Iterator { - self.member_of + self.inner + .member_of .iter() - .chain(self.access_to.iter().filter_map(move |(id, cols)| { - if cols.contains(collection) { - id.into() + .chain(self.inner.access_to.iter().filter_map(move |a| { + if a.collections.contains(collection) { + Some(&a.account_id) } else { None } @@ -512,49 +407,106 @@ impl AccessToken { pub fn has_access(&self, to_account_id: u32, to_collection: impl Into) -> bool { let to_collection = to_collection.into(); self.is_member(to_account_id) - || self.access_to.iter().any(|(id, collections)| { - *id == to_account_id && collections.contains(to_collection) - }) + || self + .inner + .access_to + .iter() + .any(|a| a.account_id == to_account_id && a.collections.contains(to_collection)) } pub fn has_account_access(&self, to_account_id: u32) -> bool { - self.is_member(to_account_id) || self.access_to.iter().any(|(id, _)| *id == to_account_id) - } - - pub fn as_resource_token(&self) -> ResourceToken { - ResourceToken { - account_id: self.account_id, - quota: self.quota, - tenant: self.tenant, - } + self.is_member(to_account_id) + || self + .inner + .access_to + .iter() + .any(|a| a.account_id == to_account_id) } pub fn is_http_request_allowed(&self) -> LimiterResult { - self.concurrent_http_requests + self.inner + .concurrent_http_requests .as_ref() .map_or(LimiterResult::Disabled, |limiter| limiter.is_allowed()) } pub fn is_imap_request_allowed(&self) -> LimiterResult { - self.concurrent_imap_requests + self.inner + .concurrent_imap_requests .as_ref() .map_or(LimiterResult::Disabled, |limiter| limiter.is_allowed()) } pub fn is_upload_allowed(&self) -> LimiterResult { - self.concurrent_uploads + self.inner + .concurrent_uploads .as_ref() .map_or(LimiterResult::Disabled, |limiter| limiter.is_allowed()) } +} + +impl AccessTokenInner { + pub fn from_id(account_id: u32) -> Self { + Self { + account_id, + ..Default::default() + } + } + + pub fn with_access_to(self, access_to: impl IntoIterator) -> Self { + Self { + access_to: access_to.into_iter().collect(), + ..self + } + } + + pub fn with_scopes(self, scopes: impl IntoIterator) -> Self { + Self { + scopes: scopes.into_iter().collect(), + ..self + } + } + + pub fn with_tenant_id(mut self, tenant_id: Option) -> Self { + self.tenant_id = tenant_id; + self + } + + pub fn new_admin() -> Self { + AccessTokenInner { + account_id: FALLBACK_ADMIN_ID, + tenant_id: Default::default(), + member_of: Default::default(), + access_to: Default::default(), + scopes: Box::new([AccessScope::new(Permissions::all())]), + concurrent_http_requests: Default::default(), + concurrent_imap_requests: Default::default(), + concurrent_uploads: Default::default(), + revision: Default::default(), + obj_size: Default::default(), + } + } pub fn update_size(mut self) -> Self { self.obj_size = (std::mem::size_of::() + (self.member_of.len() * std::mem::size_of::()) + (self.access_to.len() * (std::mem::size_of::() + std::mem::size_of::())) - + self.name.len() - + self.description.as_ref().map_or(0, |v| v.len()) - + self.locale.as_ref().map_or(0, |v| v.len()) - + self.emails.iter().map(|v| v.len()).sum::()) as u64; + + (self.scopes.len() * std::mem::size_of::())) + as u64; + self + } +} + +impl AccessScope { + pub fn new(permissions: Permissions) -> Self { + Self { + permissions, + expires_at: u64::MAX, + } + } + + pub fn expires_at(mut self, expires_at: u64) -> Self { + self.expires_at = expires_at; self } } diff --git a/crates/common/src/auth/authentication.rs b/crates/common/src/auth/authentication.rs index 5e956bfd..4ec05f08 100644 --- a/crates/common/src/auth/authentication.rs +++ b/crates/common/src/auth/authentication.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::Server; + impl Server { pub async fn authenticate(&self, req: &AuthRequest<'_>) -> trc::Result> { // Resolve directory @@ -239,12 +241,6 @@ impl<'x> AuthRequest<'x> { } } -impl CacheItemWeight for AccessToken { - fn weight(&self) -> u64 { - self.obj_size - } -} - pub(crate) trait CredentialsUsername { fn login(&self) -> Option<&str>; } diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 5de0e79b..278aa1a4 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -4,14 +4,18 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{expr::if_block::IfBlock, listener::limiter::ConcurrencyLimiter}; +use crate::{ + expr::if_block::IfBlock, + network::limiter::ConcurrencyLimiter, + storage::{ObjectQuota, TenantQuota}, +}; use arcstr::ArcStr; use directory::Credentials; use registry::{ - schema::enums::{Locale, Permission, StorageQuota, TenantStorageQuota}, + schema::enums::{Locale, Permission}, types::EnumType, }; -use std::net::IpAddr; +use std::{net::IpAddr, sync::Arc}; use tinyvec::TinyVec; use trc::ipc::bitset::Bitset; use types::collection::Collection; @@ -24,10 +28,10 @@ pub mod permissions; pub mod rate_limit; pub mod sasl; +pub const FALLBACK_ADMIN_ID: u32 = u32::MAX; const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::()); pub type Permissions = Bitset; -pub type ObjectQuota = [u32; StorageQuota::COUNT - 1]; -pub type TenantQuota = [u32; TenantStorageQuota::COUNT - 1]; + //pub type IdMap = HashMap, nohash_hasher::BuildNoHashHasher>; //pub type NameMap = AHashMap>; @@ -41,9 +45,10 @@ pub enum EmailCache { #[derive(Debug, Clone)] pub struct DomainCache { pub name: ArcStr, - pub id_tenant: u32, + pub id: u32, pub id_directory: u32, - pub catch_all: Option>, + pub id_tenant: u32, + pub catch_all: Option, pub sub_addressing_custom: Option>, pub flags: u8, } @@ -58,9 +63,8 @@ pub const DOMAIN_FLAG_ALIAS_LOGIN: u8 = 1 << 4; pub struct AccountCache { pub addresses: Box<[ArcStr]>, pub addresses_temporary: Box<[TemporaryAddress]>, - pub id_member_of: TinyVec<[u32; 3]>, pub id_tenant: u32, - pub id_roles: TinyVec<[u32; 3]>, + pub id_member_of: TinyVec<[u32; 3]>, pub quota_disk: u64, pub quota_objects: Option>, pub description: Option>, @@ -75,7 +79,6 @@ pub struct TemporaryAddress { #[derive(Debug, Clone)] pub struct RoleCache { - pub id_tenant: u32, pub id_roles: TinyVec<[u32; 3]>, pub permissions: PermissionsGroup, } @@ -84,8 +87,7 @@ pub struct RoleCache { pub struct MailingListCache { pub addresses: Box<[ArcStr]>, pub addresses_temporary: Box<[TemporaryAddress]>, - pub id_tenant: u32, - pub recipients: Box<[ArcStr]>, + pub recipients: Arc<[ArcStr]>, } #[derive(Debug, Clone)] @@ -96,15 +98,6 @@ pub struct TenantCache { pub permissions: Option>, } -#[derive(Debug, Clone)] -pub struct ApiKeyCache { - pub id: u32, - pub id_tenant: u32, - pub id_roles: TinyVec<[u32; 3]>, - pub permissions: Option>, - pub expires_at: u64, -} - #[derive(Debug, Clone, Default)] pub struct PermissionsGroup { pub enabled: Permissions, @@ -114,9 +107,17 @@ pub struct PermissionsGroup { #[derive(Debug, Default)] pub struct AccessToken { + scope_id: u32, + inner: Arc, +} + +#[derive(Debug, Default)] +pub struct AccessTokenInner { pub account_id: u32, + pub tenant_id: Option, + pub member_of: TinyVec<[u32; 3]>, pub access_to: Box<[AccessTo]>, - pub permissions: Permissions, + pub scopes: Box<[AccessScope]>, pub concurrent_http_requests: Option, pub concurrent_imap_requests: Option, pub concurrent_uploads: Option, @@ -125,6 +126,12 @@ pub struct AccessToken { } #[derive(Debug, Default)] +struct AccessScope { + pub permissions: Permissions, + pub expires_at: u64, +} + +#[derive(Debug, Default, Hash, PartialEq, Eq)] pub struct AccessTo { pub account_id: u32, pub collections: Bitmap, @@ -138,7 +145,7 @@ pub struct AuthRequest { allow_api_access: bool, } -impl CacheItemWeight for AccessToken { +impl CacheItemWeight for AccessTokenInner { fn weight(&self) -> u64 { self.obj_size } @@ -196,10 +203,3 @@ impl CacheItemWeight for PermissionsGroup { std::mem::size_of::() as u64 } } - -impl CacheItemWeight for ApiKeyCache { - fn weight(&self) -> u64 { - std::mem::size_of::() as u64 - + self.permissions.as_ref().map_or(0, |p| p.weight()) - } -} diff --git a/crates/common/src/auth/oauth/introspect.rs b/crates/common/src/auth/oauth/introspect.rs index 89cdbdb8..7d6572ee 100644 --- a/crates/common/src/auth/oauth/introspect.rs +++ b/crates/common/src/auth/oauth/introspect.rs @@ -57,16 +57,13 @@ impl Server { Ok(token_info) => Ok(OAuthIntrospect { active: true, client_id: Some(token_info.client_id), - username: if access_token.account_id() == token_info.account_id { - access_token.name.clone() - } else { - self.get_access_token(token_info.account_id) - .await - .caused_by(trc::location!())? - .name - .clone() - } - .into(), + username: self + .account(access_token.account_id()) + .await + .caused_by(trc::location!())? + .addresses + .first() + .map(|v| v.to_string()), token_type: Some("bearer".into()), exp: Some(token_info.expiry as i64), iat: Some(token_info.issued_at as i64), diff --git a/crates/common/src/auth/oauth/token.rs b/crates/common/src/auth/oauth/token.rs index ad062bde..5a583f47 100644 --- a/crates/common/src/auth/oauth/token.rs +++ b/crates/common/src/auth/oauth/token.rs @@ -8,6 +8,7 @@ use super::{CLIENT_ID_MAX_LEN, GrantType, RANDOM_CODE_LEN, crypto::SymmetricEncr use crate::Server; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; +use registry::schema::structs::Account; use std::time::SystemTime; use store::{ blake3, @@ -216,34 +217,18 @@ impl Server { pub async fn password_hash(&self, account_id: u32) -> trc::Result { if account_id != u32::MAX { - self.core - .storage - .directory - .query(QueryParams::id(account_id).with_return_member_of(false)) + self.registry() + .object::(account_id) .await .caused_by(trc::location!())? + .map(|account| account.secret) .ok_or_else(|| { trc::AuthEvent::Error .into_err() .details("Account no longer exists") - })? - .data - .into_iter() - .filter_map(|v| { - if let PrincipalData::Password(secret) = v { - Some(secret) - } else { - None - } }) - .next() - .ok_or( - trc::AuthEvent::Error - .into_err() - .details("Account does not contain secrets") - .caused_by(trc::location!()), - ) } else if let Some((_, secret)) = &self.core.network.security.fallback_admin { + let todo = "api keys?"; Ok(secret.into()) } else { Err(trc::AuthEvent::Error diff --git a/crates/common/src/auth/permissions.rs b/crates/common/src/auth/permissions.rs index e5e87640..6abfdd7a 100644 --- a/crates/common/src/auth/permissions.rs +++ b/crates/common/src/auth/permissions.rs @@ -9,15 +9,16 @@ use crate::{ auth::{Permissions, PermissionsGroup}, }; use ahash::AHashSet; +use registry::schema::{enums::Permission, structs::PermissionsList}; use trc::AddContext; impl Server { - pub async fn effective_permissions( + pub async fn add_role_permissions( &self, mut base_permissions: PermissionsGroup, - role_id: u32, - ) -> trc::Result { - let mut role_ids = vec![role_id]; + roles: impl IntoIterator, + ) -> trc::Result { + let mut role_ids = roles.into_iter().collect::>(); let mut fetched_role_ids = AHashSet::new(); while let Some(role_id) = role_ids.pop() { @@ -29,7 +30,7 @@ impl Server { } } - Ok(base_permissions.finalize()) + Ok(base_permissions) } } @@ -39,6 +40,11 @@ impl PermissionsGroup { self.disabled.union(&other.disabled); } + pub fn restrict(&mut self, other: &PermissionsGroup) { + self.enabled.intersection(&other.enabled); + self.disabled.union(&other.disabled); + } + pub fn finalize(mut self) -> Permissions { self.enabled.difference(&self.disabled); self.enabled @@ -49,4 +55,207 @@ impl PermissionsGroup { enabled.difference(&self.disabled); enabled } + + pub fn user() -> Self { + let mut permissions = PermissionsGroup::default(); + for permission in [ + Permission::Authenticate, + Permission::AuthenticateOauth, + Permission::EmailSend, + Permission::EmailReceive, + Permission::ManageEncryption, + Permission::ManagePasswords, + Permission::JmapEmailGet, + Permission::JmapMailboxGet, + Permission::JmapThreadGet, + Permission::JmapIdentityGet, + Permission::JmapEmailSubmissionGet, + Permission::JmapPushSubscriptionGet, + Permission::JmapSieveScriptGet, + Permission::JmapVacationResponseGet, + Permission::JmapQuotaGet, + Permission::JmapBlobGet, + Permission::JmapEmailSet, + Permission::JmapMailboxSet, + Permission::JmapIdentitySet, + Permission::JmapEmailSubmissionSet, + Permission::JmapPushSubscriptionSet, + Permission::JmapSieveScriptSet, + Permission::JmapVacationResponseSet, + Permission::JmapEmailChanges, + Permission::JmapMailboxChanges, + Permission::JmapThreadChanges, + Permission::JmapIdentityChanges, + Permission::JmapEmailSubmissionChanges, + Permission::JmapQuotaChanges, + Permission::JmapEmailCopy, + Permission::JmapBlobCopy, + Permission::JmapEmailImport, + Permission::JmapEmailParse, + Permission::JmapEmailQueryChanges, + Permission::JmapMailboxQueryChanges, + Permission::JmapEmailSubmissionQueryChanges, + Permission::JmapSieveScriptQueryChanges, + Permission::JmapQuotaQueryChanges, + Permission::JmapEmailQuery, + Permission::JmapMailboxQuery, + Permission::JmapEmailSubmissionQuery, + Permission::JmapSieveScriptQuery, + Permission::JmapQuotaQuery, + Permission::JmapSearchSnippet, + Permission::JmapSieveScriptValidate, + Permission::JmapBlobLookup, + Permission::JmapBlobUpload, + Permission::JmapEcho, + Permission::ImapAuthenticate, + Permission::ImapAclGet, + Permission::ImapAclSet, + Permission::ImapMyRights, + Permission::ImapListRights, + Permission::ImapAppend, + Permission::ImapCapability, + Permission::ImapId, + Permission::ImapCopy, + Permission::ImapMove, + Permission::ImapCreate, + Permission::ImapDelete, + Permission::ImapEnable, + Permission::ImapExpunge, + Permission::ImapFetch, + Permission::ImapIdle, + Permission::ImapList, + Permission::ImapLsub, + Permission::ImapNamespace, + Permission::ImapRename, + Permission::ImapSearch, + Permission::ImapSort, + Permission::ImapSelect, + Permission::ImapExamine, + Permission::ImapStatus, + Permission::ImapStore, + Permission::ImapSubscribe, + Permission::ImapThread, + Permission::Pop3Authenticate, + Permission::Pop3List, + Permission::Pop3Uidl, + Permission::Pop3Stat, + Permission::Pop3Retr, + Permission::Pop3Dele, + Permission::SieveAuthenticate, + Permission::SieveListScripts, + Permission::SieveSetActive, + Permission::SieveGetScript, + Permission::SievePutScript, + Permission::SieveDeleteScript, + Permission::SieveRenameScript, + Permission::SieveCheckScript, + Permission::SieveHaveSpace, + Permission::DavSyncCollection, + Permission::DavExpandProperty, + Permission::DavPrincipalAcl, + Permission::DavPrincipalList, + Permission::DavPrincipalSearch, + Permission::DavPrincipalMatch, + Permission::DavPrincipalSearchPropSet, + Permission::DavFilePropFind, + Permission::DavFilePropPatch, + Permission::DavFileGet, + Permission::DavFileMkCol, + Permission::DavFileDelete, + Permission::DavFilePut, + Permission::DavFileCopy, + Permission::DavFileMove, + Permission::DavFileLock, + Permission::DavFileAcl, + Permission::DavCardPropFind, + Permission::DavCardPropPatch, + Permission::DavCardGet, + Permission::DavCardMkCol, + Permission::DavCardDelete, + Permission::DavCardPut, + Permission::DavCardCopy, + Permission::DavCardMove, + Permission::DavCardLock, + Permission::DavCardAcl, + Permission::DavCardQuery, + Permission::DavCardMultiGet, + Permission::DavCalPropFind, + Permission::DavCalPropPatch, + Permission::DavCalGet, + Permission::DavCalMkCol, + Permission::DavCalDelete, + Permission::DavCalPut, + Permission::DavCalCopy, + Permission::DavCalMove, + Permission::DavCalLock, + Permission::DavCalAcl, + Permission::DavCalQuery, + Permission::DavCalMultiGet, + Permission::DavCalFreeBusyQuery, + Permission::CalendarAlarms, + Permission::CalendarSchedulingSend, + Permission::CalendarSchedulingReceive, + Permission::JmapAddressBookGet, + Permission::JmapAddressBookSet, + Permission::JmapAddressBookChanges, + Permission::JmapContactCardGet, + Permission::JmapContactCardChanges, + Permission::JmapContactCardQuery, + Permission::JmapContactCardQueryChanges, + Permission::JmapContactCardSet, + Permission::JmapContactCardCopy, + Permission::JmapContactCardParse, + Permission::JmapFileNodeGet, + Permission::JmapFileNodeSet, + Permission::JmapFileNodeChanges, + Permission::JmapFileNodeQuery, + Permission::JmapFileNodeQueryChanges, + Permission::JmapPrincipalGetAvailability, + Permission::JmapPrincipalChanges, + Permission::JmapPrincipalQuery, + Permission::JmapPrincipalGet, + Permission::JmapPrincipalQueryChanges, + Permission::JmapShareNotificationGet, + Permission::JmapShareNotificationSet, + Permission::JmapShareNotificationChanges, + Permission::JmapShareNotificationQuery, + Permission::JmapShareNotificationQueryChanges, + Permission::JmapCalendarGet, + Permission::JmapCalendarSet, + Permission::JmapCalendarChanges, + Permission::JmapCalendarEventGet, + Permission::JmapCalendarEventSet, + Permission::JmapCalendarEventChanges, + Permission::JmapCalendarEventQuery, + Permission::JmapCalendarEventQueryChanges, + Permission::JmapCalendarEventCopy, + Permission::JmapCalendarEventParse, + Permission::JmapCalendarEventNotificationGet, + Permission::JmapCalendarEventNotificationSet, + Permission::JmapCalendarEventNotificationChanges, + Permission::JmapCalendarEventNotificationQuery, + Permission::JmapCalendarEventNotificationQueryChanges, + Permission::JmapParticipantIdentityGet, + Permission::JmapParticipantIdentitySet, + Permission::JmapParticipantIdentityChanges, + ] { + permissions.enabled.set(permission as usize); + } + + permissions + } +} + +impl From for PermissionsGroup { + fn from(value: PermissionsList) -> Self { + let mut permissions = PermissionsGroup::default(); + for (permission, is_set) in value.permissions { + if is_set { + permissions.enabled.set(permission as usize); + } else { + permissions.disabled.set(permission as usize); + } + } + permissions + } } diff --git a/crates/common/src/auth/rate_limit.rs b/crates/common/src/auth/rate_limit.rs index 80d17249..32020fb9 100644 --- a/crates/common/src/auth/rate_limit.rs +++ b/crates/common/src/auth/rate_limit.rs @@ -5,10 +5,9 @@ */ use crate::auth::AccessToken; -use crate::{ - KV_RATE_LIMIT_HTTP_ANONYMOUS, KV_RATE_LIMIT_HTTP_AUTHENTICATED, Server, ip_to_bytes, - listener::limiter::{InFlight, LimiterResult}, -}; +use crate::network::ip_to_bytes; +use crate::network::limiter::{InFlight, LimiterResult}; +use crate::{KV_RATE_LIMIT_HTTP_ANONYMOUS, KV_RATE_LIMIT_HTTP_AUTHENTICATED, Server}; use registry::schema::enums::Permission; use std::net::IpAddr; use trc::AddContext; @@ -24,7 +23,7 @@ impl Server { .memory .is_rate_allowed( KV_RATE_LIMIT_HTTP_AUTHENTICATED, - &access_token.account_id.to_be_bytes(), + &access_token.account_id().to_be_bytes(), rate, false, ) diff --git a/crates/common/src/cache/directory.rs b/crates/common/src/cache/directory.rs index db8ae156..aa459ad7 100644 --- a/crates/common/src/cache/directory.rs +++ b/crates/common/src/cache/directory.rs @@ -6,7 +6,7 @@ use crate::{ Server, - auth::{AccountCache, ApiKeyCache, DomainCache, EmailCache, RoleCache, TenantCache}, + auth::{AccountCache, DomainCache, EmailCache, RoleCache, TenantCache}, config::smtp::auth::DkimSigner, }; use std::sync::Arc; @@ -21,10 +21,13 @@ impl Server { } pub async fn account(&self, id: u32) -> trc::Result> { - todo!() - } + /* - pub async fn group(&self, id: u32) -> trc::Result> { + Err(trc::AuthEvent::Error + .into_err() + .details("Account not found.") + .caused_by(trc::location!())) + */ todo!() } @@ -32,15 +35,11 @@ impl Server { todo!() } - pub async fn tenant(&self, id: u32) -> trc::Result>> { + pub async fn tenant(&self, id: u32) -> trc::Result> { todo!() } - pub async fn api_key(&self, id: u32) -> trc::Result>> { - todo!() - } - - pub async fn dkim_signers(&self, domain_id: u32) -> trc::Result>> { + pub async fn dkim_signers(&self, domain: &str) -> trc::Result>> { todo!() } } diff --git a/crates/common/src/cache/invalidate.rs b/crates/common/src/cache/invalidate.rs new file mode 100644 index 00000000..d80492f5 --- /dev/null +++ b/crates/common/src/cache/invalidate.rs @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use crate::{ + Server, + ipc::{BroadcastEvent, CacheInvalidation}, +}; + +impl Server { + pub async fn invalidate_caches(&self, changes: Vec, broadcast: bool) { + let cache = &self.inner.cache; + + for change in &changes { + match change { + CacheInvalidation::AccessToken(id) => { + cache.access_tokens.remove(id); + } + CacheInvalidation::DavResources(id) => { + cache.files.remove(id); + cache.contacts.remove(id); + cache.events.remove(id); + cache.scheduling.remove(id); + } + CacheInvalidation::Domain(id) => { + cache.domains.remove(id); + } + CacheInvalidation::Account(id) => { + cache.accounts.remove(id); + } + CacheInvalidation::Group(id) => { + cache.accounts.remove(id); + } + CacheInvalidation::Tenant(id) => { + cache.tenants.remove(id); + } + CacheInvalidation::Role(id) => { + cache.roles.remove(id); + } + CacheInvalidation::List(id) => { + cache.lists.remove(id); + } + CacheInvalidation::PushServers(_) => {} + } + } + + // Broadcast cache invalidation to other servers + if broadcast { + self.cluster_broadcast(BroadcastEvent::CacheInvalidation(changes)) + .await; + } + } +} diff --git a/crates/common/src/cache/mod.rs b/crates/common/src/cache/mod.rs index c9878942..f44912a4 100644 --- a/crates/common/src/cache/mod.rs +++ b/crates/common/src/cache/mod.rs @@ -4,5 +4,49 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use crate::{DavResources, HttpAuthCache, MailboxCache, MessageStoreCache}; +use utils::cache::CacheItemWeight; + pub mod directory; +pub mod invalidate; pub mod reload; + +impl MailboxCache { + pub fn parent_id(&self) -> Option { + if self.parent_id != u32::MAX { + Some(self.parent_id) + } else { + None + } + } + + pub fn sort_order(&self) -> Option { + if self.sort_order != u32::MAX { + Some(self.sort_order) + } else { + None + } + } + + pub fn is_root(&self) -> bool { + self.parent_id == u32::MAX + } +} + +impl CacheItemWeight for MessageStoreCache { + fn weight(&self) -> u64 { + self.size + } +} + +impl CacheItemWeight for HttpAuthCache { + fn weight(&self) -> u64 { + std::mem::size_of::() as u64 + } +} + +impl CacheItemWeight for DavResources { + fn weight(&self) -> u64 { + self.size + } +} diff --git a/crates/common/src/cache/reload.rs b/crates/common/src/cache/reload.rs index dbf1c56f..92c4cb8a 100644 --- a/crates/common/src/cache/reload.rs +++ b/crates/common/src/cache/reload.rs @@ -7,13 +7,13 @@ use crate::{ Core, Server, config::{server::Listeners, telemetry::Telemetry}, - listener::blocked::BlockedIps, }; use ahash::AHashMap; use arc_swap::ArcSwap; +use store::registry::bootstrap::Bootstrap; pub struct ReloadResult { - pub config: Config, + pub bp: Bootstrap, pub new_core: Option, pub tracers: Option, } diff --git a/crates/common/src/config/inner.rs b/crates/common/src/config/inner.rs index 4f381b90..e5384376 100644 --- a/crates/common/src/config/inner.rs +++ b/crates/common/src/config/inner.rs @@ -8,14 +8,16 @@ use super::server::tls::build_self_signed_cert; use crate::{ Caches, Data, DavResource, DavResources, MailboxCache, MessageStoreCache, MessageUidCache, TlsConnectors, - auth::AccessToken, + auth::{AccessTokenInner, AccountCache, DomainCache, MailingListCache, RoleCache, TenantCache}, config::{ mailstore::spamfilter::SpamClassifier, server::tls::parse_certificates, - smtp::resolver::{Policy, Tlsa}, + smtp::{ + auth::DkimSigner, + resolver::{Policy, Tlsa}, + }, }, - listener::blocked::BlockedIps, - manager::webadmin::WebAdminManager, + network::security::BlockedIps, }; use ahash::{AHashMap, AHashSet}; use arc_swap::ArcSwap; @@ -94,7 +96,7 @@ impl Caches { Caches { access_tokens: Cache::new( cache.access_tokens, - (std::mem::size_of::() + 255) as u64, + (std::mem::size_of::() + 255) as u64, ), http_auth: Cache::new(cache.http_auth, (50 + std::mem::size_of::()) as u64), messages: Cache::new( @@ -124,6 +126,40 @@ impl Caches { (std::mem::size_of::() + (500 * std::mem::size_of::())) as u64, ), + emails: Cache::new(cache.email_addresses, 255u64), + emails_negative: CacheWithTtl::new( + cache.email_addresses_negative, + (std::mem::size_of::() + 255) as u64, + ), + domain_names: Cache::new( + cache.domain_names, + (std::mem::size_of::() + 255) as u64, + ), + domain_names_negative: CacheWithTtl::new( + cache.domain_names_negative, + (std::mem::size_of::() + 255) as u64, + ), + domains: Cache::new( + cache.domains, + (std::mem::size_of::() + 255) as u64, + ), + accounts: Cache::new( + cache.accounts, + (std::mem::size_of::() + 255) as u64, + ), + roles: Cache::new(cache.roles, (std::mem::size_of::() + 255) as u64), + tenants: Cache::new( + cache.tenants, + (std::mem::size_of::() + 255) as u64, + ), + lists: Cache::new( + cache.mailing_lists, + (std::mem::size_of::() + 255) as u64, + ), + dkim_signers: Cache::new( + cache.dkim_signatures, + (std::mem::size_of::() + 255) as u64, + ), dns_txt: CacheWithTtl::new(cache.dns_txt, (std::mem::size_of::() + 255) as u64), dns_mx: CacheWithTtl::new(cache.dns_mx, ((std::mem::size_of::() + 255) * 2) as u64), dns_ptr: CacheWithTtl::new(cache.dns_ptr, (std::mem::size_of::() + 255) as u64), @@ -144,6 +180,7 @@ impl Caches { cache.dns_rbl, ((std::mem::size_of::() + 255) * 2) as u64, ), + negative_cache_ttl: cache.negative_ttl.into_inner(), } } diff --git a/crates/common/src/config/mailstore/email.rs b/crates/common/src/config/mailstore/email.rs index bf841003..65d72e46 100644 --- a/crates/common/src/config/mailstore/email.rs +++ b/crates/common/src/config/mailstore/email.rs @@ -8,9 +8,10 @@ use ahash::{AHashMap, AHashSet}; use nlp::language::Language; use registry::{ schema::{ - enums::{SearchCalendarField, SearchContactField, SearchEmailField}, + enums::{SearchCalendarField, SearchContactField, SearchEmailField, StorageQuota}, structs::{ - AddressBook, Calendar, DataRetention, Email, Jmap, Search, SieveUserInterpreter, + AddressBook, Calendar, DataRetention, Email, Jmap, OidcProvider, Search, + SieveUserInterpreter, }, }, types::EnumType, @@ -21,9 +22,11 @@ use store::{ search::{CalendarSearchField, ContactSearchField, EmailSearchField, SearchField}, write::SearchIndex, }; -use types::{collection::Collection, special_use::SpecialUse}; +use types::special_use::SpecialUse; use utils::cron::SimpleCron; +use crate::storage::ObjectQuota; + #[derive(Clone)] pub struct EmailConfig { pub default_language: Language, @@ -50,7 +53,7 @@ pub struct EmailConfig { pub index_batch_size: usize, pub index_fields: AHashMap>, - pub max_objects: [u32; Collection::MAX], + pub max_objects: ObjectQuota, pub account_purge_frequency: SimpleCron, } @@ -73,22 +76,28 @@ impl EmailConfig { let jmap = bp.setting_infallible::().await; let calendar = bp.setting_infallible::().await; let address_book = bp.setting_infallible::().await; + let oidc = bp.setting_infallible::().await; // Parse default object quotas - let mut max_objects = [u32::MAX; Collection::MAX]; - for (collection, max) in [ - (Collection::Mailbox, email.max_mailboxes), - (Collection::SieveScript, sieve.max_scripts), - (Collection::Identity, email.max_identities), - (Collection::EmailSubmission, email.max_submissions), - (Collection::PushSubscription, jmap.max_subscriptions), - (Collection::Calendar, calendar.max_calendars), - (Collection::CalendarEvent, calendar.max_events), - (Collection::AddressBook, address_book.max_address_books), - (Collection::ContactCard, address_book.max_contacts), + 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::MaxEmailSubmissions, email.max_submissions), + (StorageQuota::MaxMaskedAddresses, email.max_masked_addresses), + (StorageQuota::MaxAppPasswords, oidc.max_app_passwords), + (StorageQuota::MaxPushSubscriptions, jmap.max_subscriptions), + (StorageQuota::MaxCalendars, calendar.max_calendars), + (StorageQuota::MaxCalendarEvents, calendar.max_events), + ( + StorageQuota::MaxAddressBooks, + address_book.max_address_books, + ), + (StorageQuota::MaxContactCards, address_book.max_contacts), ] { if let Some(max) = max { - max_objects[collection as usize] = max as u32; + max_objects.set(item, max as u32); } } diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 41e4b89f..efd05edb 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -6,7 +6,7 @@ use self::{mailstore::jmap::JmapConfig, smtp::SmtpConfig, storage::Storage}; use crate::{ - Core, Network, Security, + Core, Network, auth::oauth::config::OAuthConfig, config::mailstore::{imap::ImapConfig, scripts::Scripting, spamfilter::SpamFilterConfig}, expr::*, diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index 84cdb343..864d2a92 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -5,7 +5,10 @@ */ use super::*; -use crate::expr::if_block::{BootstrapExprExt, IfBlock}; +use crate::{ + expr::if_block::{BootstrapExprExt, IfBlock}, + network::security::Security, +}; use ahash::AHashMap; use registry::{ schema::{ diff --git a/crates/common/src/config/server/listener.rs b/crates/common/src/config/server/listener.rs index 810fa09a..f9ff89e6 100644 --- a/crates/common/src/config/server/listener.rs +++ b/crates/common/src/config/server/listener.rs @@ -10,7 +10,7 @@ use super::{ }; use crate::{ Inner, - listener::{TcpAcceptor, tls::CertificateResolver}, + network::{TcpAcceptor, tls::CertificateResolver}, }; use registry::schema::{ enums::{NetworkListenerProtocol, TlsCipherSuite, TlsVersion}, diff --git a/crates/common/src/config/server/mod.rs b/crates/common/src/config/server/mod.rs index 45d716d2..90a994d4 100644 --- a/crates/common/src/config/server/mod.rs +++ b/crates/common/src/config/server/mod.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::listener::TcpAcceptor; +use crate::network::TcpAcceptor; use ahash::AHashMap; use registry::{ schema::structs::NetworkListener, diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index 6516ed47..33dc08a3 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use crate::{Server, listener::acme::AcmeProvider}; +use crate::{Server, network::acme::AcmeProvider}; use ahash::{AHashMap, AHashSet}; use dns_update::{ Algorithm, DnsUpdater, TsigAlgorithm, @@ -48,7 +48,7 @@ impl Server { pub async fn build_acme_provider(&self, id: Id) -> trc::Result { if let Some(server) = self .registry() - .get::(id) + .id::(id) .await .caused_by(trc::location!())? { @@ -66,7 +66,7 @@ impl Server { pub async fn build_dns_updater(&self, id: Id) -> trc::Result { let Some(server) = self .registry() - .get::(id) + .id::(id) .await .caused_by(trc::location!())? else { diff --git a/crates/common/src/config/smtp/resolver.rs b/crates/common/src/config/smtp/resolver.rs index 0587b75c..9264ac1d 100644 --- a/crates/common/src/config/smtp/resolver.rs +++ b/crates/common/src/config/smtp/resolver.rs @@ -251,18 +251,20 @@ impl Policy { T: AsRef, { if self.mx.is_empty() { + let mut mx = Vec::new(); for name in names { let name = name.as_ref(); if let Some(domain) = name.strip_prefix('.') { - self.mx.push(MxPattern::StartsWith(domain.to_string())); + mx.push(MxPattern::StartsWith(domain.to_string())); } else if name != "*" && !name.is_empty() { - self.mx.push(MxPattern::Equals(name.to_string())); + mx.push(MxPattern::Equals(name.to_string())); } } - if !self.mx.is_empty() { - self.mx.sort_unstable(); + if !mx.is_empty() { + mx.sort_unstable(); self.id = self.hash().to_string(); + self.mx = mx.into_boxed_slice(); Some(self) } else { None diff --git a/crates/common/src/config/storage.rs b/crates/common/src/config/storage.rs index 13a28f0f..9354060a 100644 --- a/crates/common/src/config/storage.rs +++ b/crates/common/src/config/storage.rs @@ -6,6 +6,7 @@ use coordinator::Coordinator; use directory::Directory; +use registry::schema::enums::CompressionAlgo; use std::{collections::HashMap, sync::Arc}; use store::{BlobStore, InMemoryStore, RegistryStore, SearchStore, Store}; @@ -21,4 +22,5 @@ pub struct Storage { pub coordinator: Coordinator, pub directory: Option>, pub directories: IdMap, + pub compression: CompressionAlgo, } diff --git a/crates/common/src/core.rs b/crates/common/src/core.rs deleted file mode 100644 index 02fc002b..00000000 --- a/crates/common/src/core.rs +++ /dev/null @@ -1,1020 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - * - * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL - */ - -use crate::{ - Inner, Server, - auth::AccessToken, - config::{ - mailstore::spamfilter::SpamClassifier, - smtp::{ - auth::DkimSigner, - queue::{ - ConnectionStrategy, DEFAULT_QUEUE_NAME, MxConfig, QueueExpiry, QueueName, - QueueStrategy, RequireOptional, RoutingStrategy, TlsStrategy, VirtualQueue, - }, - }, - }, - ipc::{BroadcastEvent, PushEvent, PushNotification}, - manager::SPAM_CLASSIFIER_KEY, -}; -use directory::Directory; -use mail_auth::IpLookupStrategy; -use sieve::Sieve; -use std::{ - sync::{Arc, LazyLock}, - time::Duration, -}; -use store::{ - BlobStore, Deserialize, InMemoryStore, IndexKey, IndexKeyPrefix, IterateParams, Key, LogKey, - RegistryStore, SUBSPACE_LOGS, SearchStore, SerializeInfallible, Store, U32_LEN, U64_LEN, - ValueKey, - dispatch::DocumentSet, - roaring::RoaringBitmap, - write::{ - AlignedBytes, AnyClass, Archive, AssignedIds, BatchBuilder, BlobLink, BlobOp, - DirectoryClass, QueueClass, ValueClass, key::DeserializeBigEndian, now, - }, -}; -use trc::{AddContext, SpamEvent}; -use types::{ - blob::{BlobClass, BlobId}, - blob_hash::BlobHash, - collection::{Collection, SyncCollection}, - field::Field, - type_state::{DataType, StateChange}, -}; -use utils::{map::bitmap::Bitmap, snowflake::SnowflakeIdGenerator}; - -impl Server { - #[inline(always)] - pub fn registry(&self) -> &RegistryStore { - &self.core.storage.registry - } - - #[inline(always)] - pub fn store(&self) -> &Store { - &self.core.storage.data - } - - #[inline(always)] - pub fn blob_store(&self) -> &BlobStore { - &self.core.storage.blob - } - - #[inline(always)] - pub fn search_store(&self) -> &SearchStore { - &self.core.storage.fts - } - - #[inline(always)] - pub fn in_memory_store(&self) -> &InMemoryStore { - &self.core.storage.lookup - } - - pub fn get_directory(&self, name: &str) -> Option<&Arc> { - self.core.storage.directories.get(name) - } - - pub fn get_default_directory(&self) -> Option<&Arc> { - self.core.storage.directory.as_ref() - } - - pub fn get_lookup_store(&self, name: &str) -> Option { - self.inner.data.lookup_stores.load().get(name).cloned() - } - - pub fn get_dkim_signer(&self, name: &str, session_id: u64) -> Option> { - todo!() - /*self.resolve_signature(name).map(|s| s.signer).or_else(|| { - trc::event!( - Dkim(trc::DkimEvent::SignerNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - - None - })*/ - } - - pub fn get_trusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc> { - self.core.sieve.trusted_scripts.get(name).or_else(|| { - trc::event!( - Sieve(trc::SieveEvent::ScriptNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - - None - }) - } - - pub fn get_untrusted_sieve_script(&self, name: &str, session_id: u64) -> Option<&Arc> { - self.core.sieve.untrusted_scripts.get(name).or_else(|| { - trc::event!( - Sieve(trc::SieveEvent::ScriptNotFound), - Id = name.to_string(), - SpanId = session_id, - ); - - None - }) - } - - pub fn get_route_or_default(&self, name: &str, session_id: u64) -> &RoutingStrategy { - static LOCAL_GATEWAY: RoutingStrategy = RoutingStrategy::Local; - static MX_GATEWAY: RoutingStrategy = RoutingStrategy::Mx(MxConfig { - max_mx: 5, - max_multi_homed: 2, - ip_lookup_strategy: IpLookupStrategy::Ipv4thenIpv6, - }); - self.core - .smtp - .queue - .routing_strategy - .get(name) - .unwrap_or_else(|| match name { - "local" => &LOCAL_GATEWAY, - "mx" => &MX_GATEWAY, - _ => { - trc::event!( - Smtp(trc::SmtpEvent::IdNotFound), - Id = name.to_string(), - Details = "Gateway not found", - SpanId = session_id, - ); - &MX_GATEWAY - } - }) - } - - pub fn get_virtual_queue_or_default(&self, name: &QueueName) -> &VirtualQueue { - static DEFAULT_QUEUE: VirtualQueue = VirtualQueue { threads: 25 }; - self.core - .smtp - .queue - .virtual_queues - .get(name) - .unwrap_or_else(|| { - if name != &DEFAULT_QUEUE_NAME { - trc::event!( - Smtp(trc::SmtpEvent::IdNotFound), - Id = name.to_string(), - Details = "Virtual queue not found", - ); - } - - &DEFAULT_QUEUE - }) - } - - pub fn get_queue_or_default(&self, name: &str, session_id: u64) -> &QueueStrategy { - static DEFAULT_SCHEDULE: LazyLock = LazyLock::new(|| QueueStrategy { - retry: vec![ - 120, // 2 minutes - 300, // 5 minutes - 600, // 10 minutes - 900, // 15 minutes - 1800, // 30 minutes - 3600, // 1 hour - 7200, // 2 hours - ], - notify: vec![ - 86400, // 1 day - 259200, // 3 days - ], - expiry: QueueExpiry::Ttl(432000), // 5 days - virtual_queue: QueueName::default(), - }); - self.core - .smtp - .queue - .queue_strategy - .get(name) - .unwrap_or_else(|| { - if name != "default" { - trc::event!( - Smtp(trc::SmtpEvent::IdNotFound), - Id = name.to_string(), - Details = "Queue strategy not found", - SpanId = session_id, - ); - } - - &DEFAULT_SCHEDULE - }) - } - - pub fn get_tls_or_default(&self, name: &str, session_id: u64) -> &TlsStrategy { - static DEFAULT_TLS: TlsStrategy = TlsStrategy { - dane: RequireOptional::Optional, - mta_sts: RequireOptional::Optional, - tls: RequireOptional::Optional, - allow_invalid_certs: false, - timeout_tls: Duration::from_secs(3 * 60), - timeout_mta_sts: Duration::from_secs(5 * 60), - }; - self.core - .smtp - .queue - .tls_strategy - .get(name) - .unwrap_or_else(|| { - if name != "default" { - trc::event!( - Smtp(trc::SmtpEvent::IdNotFound), - Id = name.to_string(), - Details = "TLS strategy not found", - SpanId = session_id, - ); - } - - &DEFAULT_TLS - }) - } - - pub fn get_connection_or_default(&self, name: &str, session_id: u64) -> &ConnectionStrategy { - static DEFAULT_CONNECTION: ConnectionStrategy = ConnectionStrategy { - source_ipv4: Vec::new(), - source_ipv6: Vec::new(), - ehlo_hostname: None, - timeout_connect: Duration::from_secs(5 * 60), - timeout_greeting: Duration::from_secs(5 * 60), - timeout_ehlo: Duration::from_secs(5 * 60), - timeout_mail: Duration::from_secs(5 * 60), - timeout_rcpt: Duration::from_secs(5 * 60), - timeout_data: Duration::from_secs(10 * 60), - }; - - self.core - .smtp - .queue - .connection_strategy - .get(name) - .unwrap_or_else(|| { - if name != "default" { - trc::event!( - Smtp(trc::SmtpEvent::IdNotFound), - Id = name.to_string(), - Details = "Connection strategy not found", - SpanId = session_id, - ); - } - - &DEFAULT_CONNECTION - }) - } - - pub async fn get_used_quota(&self, account_id: u32) -> trc::Result { - self.core - .storage - .data - .get_counter(DirectoryClass::UsedQuota(account_id)) - .await - .add_context(|err| err.caused_by(trc::location!()).account_id(account_id)) - } - - pub async fn has_available_quota( - &self, - quotas: &ResourceToken, - item_size: u64, - ) -> trc::Result<()> { - if quotas.quota != 0 { - let used_quota = self.get_used_quota(quotas.account_id).await? as u64; - - if used_quota + item_size > quotas.quota { - return Err(trc::LimitEvent::Quota - .into_err() - .ctx(trc::Key::Limit, quotas.quota) - .ctx(trc::Key::Size, used_quota)); - } - } - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - #[cfg(feature = "enterprise")] - if self.core.is_enterprise_edition() - && let Some(tenant) = quotas.tenant.filter(|tenant| tenant.quota != 0) - { - let used_quota = self.get_used_quota(tenant.id).await? as u64; - - if used_quota + item_size > tenant.quota { - return Err(trc::LimitEvent::TenantQuota - .into_err() - .ctx(trc::Key::Limit, tenant.quota) - .ctx(trc::Key::Size, used_quota)); - } - } - - // SPDX-SnippetEnd - - Ok(()) - } - - pub async fn get_resource_token( - &self, - access_token: &AccessToken, - account_id: u32, - ) -> trc::Result { - Ok(if access_token.account_id == account_id { - ResourceToken { - account_id, - quota: access_token.quota, - tenant: access_token.tenant, - } - } else { - let mut quotas = ResourceToken { - account_id, - ..Default::default() - }; - - if let Some(principal) = self - .core - .storage - .directory - .query(QueryParams::id(account_id).with_return_member_of(false)) - .await - .add_context(|err| err.caused_by(trc::location!()).account_id(account_id))? - { - quotas.quota = principal.quota().unwrap_or_default(); - - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - - #[cfg(feature = "enterprise")] - if self.core.is_enterprise_edition() - && let Some(tenant_id) = principal.tenant() - { - quotas.tenant = TenantInfo { - id: tenant_id, - quota: self - .core - .storage - .directory - .query(QueryParams::id(tenant_id).with_return_member_of(false)) - .await - .add_context(|err| { - err.caused_by(trc::location!()).account_id(tenant_id) - })? - .and_then(|tenant| tenant.quota()) - .unwrap_or_default(), - } - .into(); - } - - // SPDX-SnippetEnd - } - - quotas - }) - } - - pub async fn archives( - &self, - account_id: u32, - collection: Collection, - documents: &I, - mut cb: CB, - ) -> trc::Result<()> - where - I: DocumentSet + Send + Sync, - CB: FnMut(u32, Archive) -> trc::Result + Send + Sync, - { - let collection: u8 = collection.into(); - - self.core - .storage - .data - .iterate( - IterateParams::new( - ValueKey { - account_id, - collection, - document_id: documents.min(), - class: ValueClass::Property(Field::ARCHIVE.into()), - }, - ValueKey { - account_id, - collection, - document_id: documents.max(), - class: ValueClass::Property(Field::ARCHIVE.into()), - }, - ), - |key, value| { - let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?; - if documents.contains(document_id) { - as Deserialize>::deserialize(value) - .and_then(|archive| cb(document_id, archive)) - } else { - Ok(true) - } - }, - ) - .await - .add_context(|err| { - err.caused_by(trc::location!()) - .account_id(account_id) - .collection(collection) - }) - } - - pub async fn all_archives( - &self, - account_id: u32, - collection: Collection, - field: u8, - mut cb: CB, - ) -> trc::Result<()> - where - CB: FnMut(u32, Archive) -> trc::Result<()> + Send + Sync, - { - let collection: u8 = collection.into(); - - self.core - .storage - .data - .iterate( - IterateParams::new( - ValueKey { - account_id, - collection, - document_id: 0, - class: ValueClass::Property(field), - }, - ValueKey { - account_id, - collection, - document_id: u32::MAX, - class: ValueClass::Property(field), - }, - ), - |key, value| { - let document_id = key.deserialize_be_u32(key.len() - U32_LEN)?; - let archive = as Deserialize>::deserialize(value)?; - cb(document_id, archive)?; - - Ok(true) - }, - ) - .await - .add_context(|err| { - err.caused_by(trc::location!()) - .account_id(account_id) - .collection(collection) - }) - } - - pub async fn document_ids( - &self, - account_id: u32, - collection: Collection, - field: impl Into, - ) -> trc::Result { - let field = field.into(); - let mut results = RoaringBitmap::new(); - self.store() - .iterate( - IterateParams::new( - IndexKeyPrefix { - account_id, - collection: collection.into(), - field, - }, - IndexKeyPrefix { - account_id, - collection: collection.into(), - field: field + 1, - }, - ) - .no_values(), - |key, _| { - results.insert(key.deserialize_be_u32(key.len() - U32_LEN)?); - - Ok(true) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| results) - } - - pub async fn document_exists( - &self, - account_id: u32, - collection: Collection, - field: impl Into, - filter: impl AsRef<[u8]>, - ) -> trc::Result { - let field = field.into(); - let mut exists = false; - let filter = filter.as_ref(); - let key_len = IndexKeyPrefix::len() + filter.len() + U32_LEN; - - self.store() - .iterate( - IterateParams::new( - IndexKey { - account_id, - collection: collection.into(), - document_id: 0, - field, - key: filter, - }, - IndexKey { - account_id, - collection: collection.into(), - document_id: u32::MAX, - field, - key: filter, - }, - ) - .no_values(), - |key, _| { - exists = key.len() == key_len; - - Ok(!exists) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| exists) - } - - pub async fn document_ids_matching( - &self, - account_id: u32, - collection: Collection, - field: impl Into, - filter: impl AsRef<[u8]>, - ) -> trc::Result { - let field = field.into(); - let filter = filter.as_ref(); - let key_len = IndexKeyPrefix::len() + filter.len() + U32_LEN; - let mut results = RoaringBitmap::new(); - - self.store() - .iterate( - IterateParams::new( - IndexKey { - account_id, - collection: collection.into(), - document_id: 0, - field, - key: filter, - }, - IndexKey { - account_id, - collection: collection.into(), - document_id: u32::MAX, - field, - key: filter, - }, - ) - .no_values(), - |key, _| { - if key.len() == key_len { - results.insert(key.deserialize_be_u32(key.len() - U32_LEN)?); - } - - Ok(true) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| results) - } - - #[inline(always)] - pub fn notify_task_queue(&self) { - self.inner.ipc.task_tx.notify_one(); - } - - pub async fn total_queued_messages(&self) -> trc::Result { - let mut total = 0; - self.store() - .iterate( - IterateParams::new( - ValueKey::from(ValueClass::Queue(QueueClass::Message(0))), - ValueKey::from(ValueClass::Queue(QueueClass::Message(u64::MAX))), - ) - .no_values(), - |_, _| { - total += 1; - - Ok(true) - }, - ) - .await - .caused_by(trc::location!()) - .map(|_| total) - } - - #[inline(always)] - pub fn generate_snowflake_id(&self) -> u64 { - self.inner.data.jmap_id_gen.generate() - } - - pub async fn commit_batch(&self, mut builder: BatchBuilder) -> trc::Result { - let mut assigned_ids = AssignedIds::default(); - let mut commit_points = builder.commit_points(); - - for commit_point in commit_points.iter() { - let batch = builder.build_one(commit_point); - assigned_ids - .ids - .extend(self.store().write(batch).await?.ids); - } - - if let Some(changes) = builder.changes() { - for (account_id, changed_collections) in changes { - let mut state_change = StateChange::new(account_id); - for changed_collection in changed_collections.changed_containers { - if let Some(data_type) = DataType::try_from_sync(changed_collection, true) { - state_change.set_change(data_type); - } - } - for changed_collection in changed_collections.changed_items { - if let Some(data_type) = DataType::try_from_sync(changed_collection, false) { - state_change.set_change(data_type); - } - } - if state_change.has_changes() { - self.broadcast_push_notification(PushNotification::StateChange( - state_change.with_change_id(assigned_ids.last_change_id(account_id)?), - )) - .await; - } - if let Some(change_id) = changed_collections.share_notification_id { - self.broadcast_push_notification(PushNotification::StateChange(StateChange { - account_id, - change_id, - types: Bitmap::from_iter([DataType::ShareNotification]), - })) - .await; - } - } - } - - Ok(assigned_ids) - } - - pub async fn delete_changes( - &self, - account_id: u32, - max_entries: Option, - max_duration: Option, - ) -> trc::Result<()> { - if let Some(max_entries) = max_entries { - for sync_collection in [ - SyncCollection::Email, - SyncCollection::Thread, - SyncCollection::Identity, - SyncCollection::EmailSubmission, - SyncCollection::SieveScript, - SyncCollection::FileNode, - SyncCollection::AddressBook, - SyncCollection::Calendar, - SyncCollection::CalendarEventNotification, - ] { - let collection = sync_collection.into(); - let from_key = LogKey { - account_id, - collection, - change_id: 0, - }; - let to_key = LogKey { - account_id, - collection, - change_id: u64::MAX, - }; - - let mut first_change_id = 0; - let mut num_changes = 0; - - self.store() - .iterate( - IterateParams::new(from_key, to_key) - .descending() - .no_values(), - |key, _| { - first_change_id = key.deserialize_be_u64(key.len() - U64_LEN)?; - num_changes += 1; - - Ok(num_changes <= max_entries) - }, - ) - .await - .caused_by(trc::location!())?; - - if num_changes > max_entries { - self.store() - .delete_range( - LogKey { - account_id, - collection, - change_id: 0, - }, - LogKey { - account_id, - collection, - change_id: first_change_id, - }, - ) - .await - .caused_by(trc::location!())?; - - // Delete vanished items - if let Some(vanished_collection) = - sync_collection.vanished_collection().map(u8::from) - { - self.store() - .delete_range( - LogKey { - account_id, - collection: vanished_collection, - change_id: 0, - }, - LogKey { - account_id, - collection: vanished_collection, - change_id: first_change_id, - }, - ) - .await - .caused_by(trc::location!())?; - } - - // Write truncation entry for cache - let mut batch = BatchBuilder::new(); - batch.with_account_id(account_id).set( - ValueClass::Any(AnyClass { - subspace: SUBSPACE_LOGS, - key: LogKey { - account_id, - collection, - change_id: first_change_id, - } - .serialize(0), - }), - Vec::new(), - ); - self.store() - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - } - } - } - - if let Some(max_duration) = max_duration { - self.store() - .delete_range( - LogKey { - account_id, - collection: SyncCollection::ShareNotification.into(), - change_id: 0, - }, - LogKey { - account_id, - collection: SyncCollection::ShareNotification.into(), - change_id: SnowflakeIdGenerator::from_duration(max_duration) - .unwrap_or_default(), - }, - ) - .await - .caused_by(trc::location!())?; - } - Ok(()) - } - - pub async fn broadcast_push_notification(&self, notification: PushNotification) -> bool { - match self - .inner - .ipc - .push_tx - .clone() - .send(PushEvent::Publish { - notification, - broadcast: true, - }) - .await - { - Ok(_) => true, - Err(_) => { - trc::event!( - Server(trc::ServerEvent::ThreadError), - Details = "Error sending state change.", - CausedBy = trc::location!() - ); - - false - } - } - } - - pub async fn cluster_broadcast(&self, event: BroadcastEvent) { - let todo = "refactor event names"; - if let Some(broadcast_tx) = &self.inner.ipc.broadcast_tx.clone() - && broadcast_tx.send(event).await.is_err() - { - trc::event!( - Server(trc::ServerEvent::ThreadError), - Details = "Error sending broadcast event.", - CausedBy = trc::location!() - ); - } - } - - #[allow(clippy::blocks_in_conditions)] - pub async fn put_jmap_blob(&self, account_id: u32, data: &[u8]) -> trc::Result { - // First reserve the hash - let hash = BlobHash::generate(data); - let mut batch = BatchBuilder::new(); - let until = now() + self.core.jmap.upload_tmp_ttl; - - batch - .with_account_id(account_id) - .set( - BlobOp::Link { - hash: hash.clone(), - to: BlobLink::Temporary { until }, - }, - vec![BlobLink::QUOTA_LINK], - ) - .set( - BlobOp::Quota { - hash: hash.clone(), - until, - }, - (data.len() as u32).serialize(), - ); - - self.core - .storage - .data - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - - if !self - .core - .storage - .data - .blob_exists(&hash) - .await - .caused_by(trc::location!())? - { - // Upload blob to store - self.core - .storage - .blob - .put_blob(hash.as_ref(), data) - .await - .caused_by(trc::location!())?; - - // Commit blob - let mut batch = BatchBuilder::new(); - batch.set(BlobOp::Commit { hash: hash.clone() }, Vec::new()); - self.core - .storage - .data - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - } - - Ok(BlobId { - hash, - class: BlobClass::Reserved { - account_id, - expires: until, - }, - section: None, - }) - } - - pub async fn put_temporary_blob( - &self, - account_id: u32, - data: &[u8], - hold_for: u64, - ) -> trc::Result<(BlobHash, BlobOp)> { - // First reserve the hash - let hash = BlobHash::generate(data); - let mut batch = BatchBuilder::new(); - let until = now() + hold_for; - - batch.with_account_id(account_id).set( - BlobOp::Link { - hash: hash.clone(), - to: BlobLink::Temporary { until }, - }, - vec![], - ); - - self.core - .storage - .data - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - - if !self - .core - .storage - .data - .blob_exists(&hash) - .await - .caused_by(trc::location!())? - { - // Upload blob to store - self.core - .storage - .blob - .put_blob(hash.as_ref(), data) - .await - .caused_by(trc::location!())?; - - // Commit blob - let mut batch = BatchBuilder::new(); - batch.set(BlobOp::Commit { hash: hash.clone() }, Vec::new()); - self.core - .storage - .data - .write(batch.build_all()) - .await - .caused_by(trc::location!())?; - } - - Ok(( - hash.clone(), - BlobOp::Link { - hash, - to: BlobLink::Temporary { until }, - }, - )) - } - - pub async fn total_accounts(&self) -> trc::Result { - self.store() - .count_principals(None, Type::Individual.into(), None) - .await - .caused_by(trc::location!()) - } - - pub async fn total_domains(&self) -> trc::Result { - self.store() - .count_principals(None, Type::Domain.into(), None) - .await - .caused_by(trc::location!()) - } - - pub async fn spam_model_reload(&self) -> trc::Result<()> { - if self.core.spam.classifier.is_some() { - if let Some(model) = self - .blob_store() - .get_blob(SPAM_CLASSIFIER_KEY, 0..usize::MAX) - .await - .and_then(|archive| match archive { - Some(archive) => as Deserialize>::deserialize(&archive) - .and_then(|archive| archive.deserialize_untrusted::()) - .map(Some), - None => Ok(None), - }) - .caused_by(trc::location!())? - { - self.inner.data.spam_classifier.store(Arc::new(model)); - } else { - trc::event!(Spam(SpamEvent::ModelNotFound)); - } - } - - Ok(()) - } - - #[cfg(not(feature = "enterprise"))] - pub async fn logo_resource( - &self, - _: &str, - ) -> trc::Result>>> { - Ok(None) - } -} - -pub trait BuildServer { - fn build_server(&self) -> Server; -} - -impl BuildServer for Arc { - fn build_server(&self) -> Server { - Server { - inner: self.clone(), - core: self.shared_core.load_full(), - } - } -} diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index 1dca3705..e27105c0 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -22,7 +22,10 @@ use ahash::{AHashMap, AHashSet}; use license::LicenseKey; use llm::AiApiConfig; use mail_parser::DateTime; -use registry::types::id::Id; +use registry::{ + schema::structs::{Domain, Tenant}, + types::id::Id, +}; use std::{sync::Arc, time::Duration}; use store::Store; use trc::{AddContext, MetricType}; @@ -150,11 +153,7 @@ impl Server { pub async fn can_create_account(&self) -> trc::Result { if let Some(enterprise) = &self.core.enterprise { - let total_accounts = self - .store() - .count_principals(None, Type::Individual.into(), None) - .await - .caused_by(trc::location!())?; + let total_accounts = self.total_accounts().await.caused_by(trc::location!())?; if total_accounts + 1 > enterprise.license.accounts as u64 { trc::event!( @@ -175,88 +174,78 @@ impl Server { pub async fn logo_resource(&self, domain: &str) -> trc::Result>>> { const MAX_IMAGE_SIZE: usize = 1024 * 1024; - if self.is_enterprise_edition() { - let domain = psl::domain_str(domain).unwrap_or(domain); - let logo = { self.inner.data.logos.lock().get(domain).cloned() }; - - if let Some(logo) = logo { - Ok(logo) - } else { - // Try fetching the logo for the domain - let logo_url = if let Some(mut principal) = self - .store() - .query(QueryParams::name(domain).with_return_member_of(false)) - .await - .caused_by(trc::location!())? - .filter(|p| p.typ() == Type::Domain) - { - if let Some(logo) = principal.picture_mut().filter(|l| l.starts_with("http")) { - std::mem::take(logo).into() - } else if let Some(tenant_id) = principal.tenant() { - if let Some(logo) = self - .store() - .query(QueryParams::id(tenant_id).with_return_member_of(false)) - .await - .caused_by(trc::location!())? - .and_then(|mut p| p.picture_mut().map(std::mem::take)) - .filter(|l| l.starts_with("http")) - { - logo.clone().into() - } else { - self.default_logo_url() - } - } else { - self.default_logo_url() - } - } else { - self.default_logo_url() - }; - - let mut logo = None; - if let Some(logo_url) = logo_url { - let response = reqwest::get(logo_url.as_str()).await.map_err(|err| { - trc::ResourceEvent::DownloadExternal - .into_err() - .details("Failed to download logo") - .reason(err) - })?; - - let content_type = response - .headers() - .get(reqwest::header::CONTENT_TYPE) - .and_then(|ct| ct.to_str().ok()) - .unwrap_or("image/svg+xml") - .to_string(); - - let contents = response - .bytes_with_limit(MAX_IMAGE_SIZE) - .await - .map_err(|err| { - trc::ResourceEvent::DownloadExternal - .into_err() - .details("Failed to download logo") - .reason(err) - })? - .ok_or_else(|| { - trc::ResourceEvent::DownloadExternal - .into_err() - .details("Download exceeded maximum size") - })?; - - logo = Resource::new(content_type, contents).into(); - } - - self.inner - .data - .logos - .lock() - .insert(domain.to_string(), logo.clone()); - - Ok(logo) - } - } else { - Ok(None) + if !self.is_enterprise_edition() { + return Ok(None); } + + let domain = psl::domain_str(domain).unwrap_or(domain); + let logo = { self.inner.data.logos.lock().get(domain).cloned() }; + if let Some(logo) = logo { + return Ok(logo); + } + + let Some((domain_id, tenant_id)) = self.domain(domain).await?.map(|d| (d.id, d.id_tenant)) + else { + self.inner.data.logos.lock().insert(domain.into(), None); + return Ok(None); + }; + + let Some(domain_record) = self.registry().object::(domain_id).await? else { + return Ok(None); + }; + let mut logo = domain_record.logo; + + if logo.is_none() && tenant_id != u32::MAX { + logo = self + .registry() + .object::(tenant_id) + .await? + .and_then(|t| t.logo); + } + + let logo_url = logo.or_else(|| self.default_logo_url()); + + let mut logo = None; + if let Some(logo_url) = logo_url { + let response = reqwest::get(logo_url.as_str()).await.map_err(|err| { + trc::ResourceEvent::DownloadExternal + .into_err() + .details("Failed to download logo") + .reason(err) + })?; + + let content_type = response + .headers() + .get(reqwest::header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .unwrap_or("image/svg+xml") + .to_string(); + + let contents = response + .bytes_with_limit(MAX_IMAGE_SIZE) + .await + .map_err(|err| { + trc::ResourceEvent::DownloadExternal + .into_err() + .details("Failed to download logo") + .reason(err) + })? + .ok_or_else(|| { + trc::ResourceEvent::DownloadExternal + .into_err() + .details("Download exceeded maximum size") + })?; + + logo = Resource::new(content_type, contents).into(); + } + + self.inner + .data + .logos + .lock() + .insert(domain.into(), logo.clone()); + + Ok(logo) } fn default_logo_url(&self) -> Option { diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index c1e4fc3e..188bc364 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}; +use crate::{Server, expr::StringCow, network::RcptExpansion}; use compact_str::{CompactString, ToCompactString}; use mail_auth::IpLookupStrategy; use std::{cmp::Ordering, net::IpAddr, vec::IntoIter}; @@ -25,47 +25,51 @@ impl Server { F_IS_LOCAL_DOMAIN => { let domain = params.next_as_string(); - self.get_directory_or_default(directory.as_ref(), session_id) - .is_local_domain(domain.as_ref()) + self.domain(domain.as_str()) .await .caused_by(trc::location!()) - .map(|v| v.into()) + .map(|v| v.is_some().into()) } F_IS_LOCAL_ADDRESS => { let address = params.next_as_string(); - self.get_directory_or_default(directory.as_ref(), session_id) - .rcpt(address.as_ref()) + self.rcpt_expand(address.as_ref()) .await .caused_by(trc::location!()) - .map(|v| (v != RcptType::Invalid).into()) + .map(|v| (v != RcptExpansion::Invalid).into()) } F_KEY_GET => { - let store = params.next_as_string(); + let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else { + return Ok(Variable::default()); + }; let key = params.next_as_string(); - self.get_in_memory_store_or_default(store.as_str(), session_id) + store .key_get::(key.as_str()) .await .map(|value| value.map(|v| v.into_inner()).unwrap_or_default()) .caused_by(trc::location!()) } F_KEY_EXISTS => { - let store = params.next_as_string(); + let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else { + return Ok(Variable::default()); + }; let key = params.next_as_string(); - self.get_in_memory_store_or_default(store.as_str(), session_id) + store .key_exists(key.as_str()) .await .caused_by(trc::location!()) .map(|v| v.into()) } F_KEY_SET => { - let store = params.next_as_string(); + let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else { + return Ok(Variable::default()); + }; let key = params.next_as_string(); let value = params.next_as_string(); - self.get_in_memory_store_or_default(store.as_ref(), session_id) + store .key_set(KeyValue::new( key.as_bytes().to_vec(), value.as_bytes().to_vec(), @@ -76,21 +80,25 @@ impl Server { .map(|v| v.into()) } F_COUNTER_INCR => { - let store = params.next_as_string(); + let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else { + return Ok(Variable::default()); + }; let key = params.next_as_string(); let value = params.next_as_integer(); - self.get_in_memory_store_or_default(store.as_ref(), session_id) + store .counter_incr(KeyValue::new(key.into_owned(), value), true) .await .map(Variable::Integer) .caused_by(trc::location!()) } F_COUNTER_GET => { - let store = params.next_as_string(); + let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else { + return Ok(Variable::default()); + }; let key = params.next_as_string(); - self.get_in_memory_store_or_default(store.as_ref(), session_id) + store .counter_get(key.as_bytes().to_vec()) .await .map(Variable::Integer) @@ -107,13 +115,24 @@ impl Server { mut arguments: FncParams<'x>, session_id: u64, ) -> trc::Result> { - let store = self.get_data_store(arguments.next_as_string().as_ref(), session_id); + let store_name = arguments.next_as_string(); + let Some(store) = self + .get_lookup_store(store_name.as_ref()) + .and_then(|v| v.into_store()) + else { + return Err(trc::EventType::Eval(trc::EvalEvent::Error) + .into_err() + .id(store_name.into_owned()) + .span_id(session_id) + .details("Store not found or is not a SQL store")); + }; let query = arguments.next_as_string(); if query.is_empty() { return Err(trc::EventType::Eval(trc::EvalEvent::Error) .into_err() - .details("Empty query string")); + .details("Empty query string") + .span_id(session_id)); } // Obtain arguments @@ -203,9 +222,7 @@ impl Server { .flat_map(|mx| { mx.exchanges.iter().map(|host| { Variable::String(StringCow::Owned( - host.strip_suffix('.') - .unwrap_or(host.as_str()) - .to_compact_string(), + host.strip_suffix('.').unwrap_or(host).to_compact_string(), )) }) }) diff --git a/crates/common/src/expr/if_block.rs b/crates/common/src/expr/if_block.rs index 3e113563..bb0a3bbb 100644 --- a/crates/common/src/expr/if_block.rs +++ b/crates/common/src/expr/if_block.rs @@ -122,9 +122,7 @@ impl BootstrapExprExt for Bootstrap { ) -> Option { // Parse conditions let mut if_then = Vec::with_capacity(expr.match_.len()); - let mut default = Expression { - items: Default::default(), - }; + let default; if expr.else_.is_empty() { if !expr.match_.is_empty() { diff --git a/crates/common/src/expr/mod.rs b/crates/common/src/expr/mod.rs index ed22f972..aa6b2a04 100644 --- a/crates/common/src/expr/mod.rs +++ b/crates/common/src/expr/mod.rs @@ -18,6 +18,9 @@ use std::{ time::Duration, }; use trc::MetricType; +use utils::cache::CacheItemWeight; + +use crate::expr::if_block::IfBlock; pub mod eval; pub mod functions; @@ -498,3 +501,21 @@ where .collect() } } + +impl CacheItemWeight for Expression { + fn weight(&self) -> u64 { + self.items.len() as u64 * std::mem::size_of::() as u64 + } +} + +impl CacheItemWeight for IfBlock { + fn weight(&self) -> u64 { + std::mem::size_of::() as u64 + + self + .if_then + .iter() + .map(|if_then| if_then.expr.weight() + if_then.then.weight()) + .sum::() + + self.default.weight() + } +} diff --git a/crates/common/src/expr/parser.rs b/crates/common/src/expr/parser.rs index 4800f0e1..e74eb77c 100644 --- a/crates/common/src/expr/parser.rs +++ b/crates/common/src/expr/parser.rs @@ -222,7 +222,9 @@ impl<'x> ExpressionParser<'x> { } if self.operator_stack.is_empty() { - Ok(Expression { items: self.output }) + Ok(Expression { + items: self.output.into_boxed_slice(), + }) } else { Err("Invalid expression".to_string()) } diff --git a/crates/common/src/ipc.rs b/crates/common/src/ipc.rs index c2f4f9b3..0b2218ea 100644 --- a/crates/common/src/ipc.rs +++ b/crates/common/src/ipc.rs @@ -15,6 +15,7 @@ use mail_auth::{ mta_sts::TlsRpt, report::{Record, tlsrpt::FailureDetails}, }; +use registry::{schema::prelude::Object, types::id::Id}; use std::{ sync::{ Arc, @@ -101,12 +102,28 @@ pub struct CalendarAlert { #[derive(Debug)] pub enum BroadcastEvent { PushNotification(PushNotification), - InvalidateAccessTokens(Vec), - InvalidateGroupwareCache(Vec), - ReloadPushServers(u32), - ReloadSettings, - ReloadBlockedIps, - ReloadSpamFilter, + RegistryChange(RegistryChange), + CacheInvalidation(Vec), +} + +#[derive(Debug)] +pub enum RegistryChange { + Insert(Id), + Delete(Id), + Reload(Object), +} + +#[derive(Debug, Clone, Copy)] +pub enum CacheInvalidation { + AccessToken(u32), + DavResources(u32), + Domain(u32), + Account(u32), + Group(u32), + Tenant(u32), + Role(u32), + List(u32), + PushServers(u32), } #[derive(Debug)] diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index 46fbfe09..b029aa24 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -6,11 +6,10 @@ #![warn(clippy::large_futures)] +use crate::auth::AccessTokenInner; +use crate::network::asn::AsnGeoLookupData; use crate::{ - auth::{ - AccountCache, ApiKeyCache, DomainCache, EmailCache, MailingListCache, RoleCache, - TenantCache, - }, + auth::{AccountCache, DomainCache, EmailCache, MailingListCache, RoleCache, TenantCache}, config::{ mailstore::{ email::EmailConfig, @@ -21,12 +20,12 @@ use crate::{ smtp::auth::DkimSigner, }, ipc::TrainTaskController, - listener::blocked::BlockedIps, + network::security::BlockedIps, }; use ahash::{AHashMap, AHashSet}; use arc_swap::ArcSwap; use arcstr::ArcStr; -use auth::{AccessToken, oauth::config::OAuthConfig}; +use auth::oauth::config::OAuthConfig; use calcard::common::timezone::Tz; use config::{ groupware::GroupwareConfig, @@ -40,40 +39,33 @@ use config::{ telemetry::Metrics, }; use ipc::{BroadcastEvent, HousekeeperEvent, PushEvent, QueueEvent, ReportingEvent}; -use listener::{asn::AsnGeoLookupData, blocked::Security}; use mail_auth::{MX, Txt}; use manager::webadmin::{Resource, WebAdminManager}; use parking_lot::{Mutex, RwLock}; use rustls::sign::CertifiedKey; use std::{ - hash::{BuildHasher, Hash, Hasher}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, sync::{Arc, atomic::AtomicBool}, time::{Duration, Instant}, }; -use store::{ - InMemoryStore, - rand::{Rng, distr::Alphanumeric}, -}; +use store::InMemoryStore; use tinyvec::TinyVec; use tokio::sync::{Notify, Semaphore, mpsc}; use tokio_rustls::TlsConnector; use types::{acl::AclGrant, special_use::SpecialUse}; use utils::{ - cache::{Cache, CacheItemWeight, CacheWithTtl}, + cache::{Cache, CacheWithTtl}, snowflake::SnowflakeIdGenerator, }; pub mod auth; pub mod cache; pub mod config; -pub mod core; -pub mod dns; pub mod expr; pub mod i18n; pub mod ipc; -pub mod listener; pub mod manager; +pub mod network; pub mod scripts; pub mod sharing; pub mod storage; @@ -172,7 +164,7 @@ pub struct Data { } pub struct Caches { - pub access_tokens: Cache>, + pub access_tokens: Cache>, pub http_auth: Cache, HttpAuthCache>, pub messages: Cache>, @@ -182,19 +174,17 @@ pub struct Caches { pub scheduling: Cache>, pub emails: Cache, - pub emails_temporary: CacheWithTtl, pub emails_negative: CacheWithTtl, - pub domains: Cache>, - pub domains_negative: CacheWithTtl, + pub domain_names: Cache, + pub domain_names_negative: CacheWithTtl, + pub domains: Cache>, pub accounts: Cache>, - pub groups: Cache>, pub roles: Cache>, pub tenants: Cache>, pub lists: Cache>, - pub api_keys: Cache>, - pub dkim_signers: Cache, Arc<[DkimSigner]>>, + pub dkim_signers: Cache>, pub dns_txt: CacheWithTtl, Txt>, pub dns_mx: CacheWithTtl, Arc<[MX]>>, @@ -204,6 +194,8 @@ pub struct Caches { pub dns_tlsa: CacheWithTtl, Arc>, pub dns_mta_sts: CacheWithTtl, Arc>, pub dns_rbl: CacheWithTtl, Option>>, + + pub negative_cache_ttl: Duration, } #[derive(Debug, Clone)] @@ -385,21 +377,16 @@ pub struct Core { // SPDX-SnippetEnd } -impl CacheItemWeight for MessageStoreCache { - fn weight(&self) -> u64 { - self.size - } +pub trait BuildServer { + fn build_server(&self) -> Server; } -impl CacheItemWeight for HttpAuthCache { - fn weight(&self) -> u64 { - std::mem::size_of::() as u64 - } -} - -impl CacheItemWeight for DavResources { - fn weight(&self) -> u64 { - self.size +impl BuildServer for Arc { + fn build_server(&self) -> Server { + Server { + inner: self.clone(), + core: self.shared_core.load_full(), + } } } @@ -419,458 +406,14 @@ pub struct ThrottleKey { pub hash: [u8; 32], } -impl PartialEq for ThrottleKey { - fn eq(&self, other: &Self) -> bool { - self.hash == other.hash - } -} - -impl std::hash::Hash for ThrottleKey { - fn hash(&self, state: &mut H) { - self.hash.hash(state); - } -} - -impl AsRef<[u8]> for ThrottleKey { - fn as_ref(&self) -> &[u8] { - &self.hash - } -} - #[derive(Default)] pub struct ThrottleKeyHasher { hash: u64, } -impl Hasher for ThrottleKeyHasher { - fn finish(&self) -> u64 { - self.hash - } - - fn write(&mut self, bytes: &[u8]) { - debug_assert!( - bytes.len() >= std::mem::size_of::(), - "ThrottleKeyHasher: input too short {bytes:?}" - ); - self.hash = bytes - .get(0..std::mem::size_of::()) - .map_or(0, |b| u64::from_ne_bytes(b.try_into().unwrap())); - } -} - #[derive(Clone, Default)] pub struct ThrottleKeyHasherBuilder {} -impl BuildHasher for ThrottleKeyHasherBuilder { - type Hasher = ThrottleKeyHasher; - - fn build_hasher(&self) -> Self::Hasher { - ThrottleKeyHasher::default() - } -} - -pub fn ip_to_bytes(ip: &IpAddr) -> Vec { - match ip { - IpAddr::V4(ip) => ip.octets().to_vec(), - IpAddr::V6(ip) => ip.octets().to_vec(), - } -} - -pub fn ip_to_bytes_prefix(prefix: u8, ip: &IpAddr) -> Vec { - match ip { - IpAddr::V4(ip) => { - let mut buf = Vec::with_capacity(5); - buf.push(prefix); - buf.extend_from_slice(&ip.octets()); - buf - } - IpAddr::V6(ip) => { - let mut buf = Vec::with_capacity(17); - buf.push(prefix); - buf.extend_from_slice(&ip.octets()); - buf - } - } -} - -impl DavResourcePath<'_> { - #[inline(always)] - pub fn document_id(&self) -> u32 { - self.resource.document_id - } - - #[inline(always)] - pub fn parent_id(&self) -> Option { - self.path.parent_id - } - - #[inline(always)] - pub fn path(&self) -> &str { - self.path.path.as_str() - } - - #[inline(always)] - pub fn is_container(&self) -> bool { - self.resource.is_container() - } - - #[inline(always)] - pub fn hierarchy_seq(&self) -> u32 { - self.path.hierarchy_seq - } - - #[inline(always)] - pub fn size(&self) -> u32 { - self.resource.size().unwrap_or_default() - } -} - -impl DavResources { - pub fn by_path(&self, name: &str) -> Option> { - self.paths.get(name).map(|path| DavResourcePath { - path, - resource: &self.resources[path.resource_idx], - }) - } - - pub fn container_resource_by_id(&self, id: u32) -> Option<&DavResource> { - self.resources - .iter() - .find(|res| res.document_id == id && res.is_container()) - } - - pub fn container_resource_path_by_id(&self, id: u32) -> Option> { - self.resources - .iter() - .enumerate() - .find(|(_, resource)| resource.document_id == id && resource.is_container()) - .and_then(|(idx, resource)| { - self.paths - .iter() - .find(|path| path.resource_idx == idx) - .map(|path| DavResourcePath { path, resource }) - }) - } - - pub fn any_resource_path_by_id(&self, id: u32) -> Option> { - self.resources - .iter() - .enumerate() - .find(|(_, resource)| resource.document_id == id) - .and_then(|(idx, resource)| { - self.paths - .iter() - .find(|path| path.resource_idx == idx) - .map(|path| DavResourcePath { path, resource }) - }) - } - - pub fn subtree(&self, search_path: &str) -> impl Iterator> { - let prefix = format!("{search_path}/"); - self.paths.iter().filter_map(move |path| { - if path.path.starts_with(&prefix) || path.path == search_path { - Some(DavResourcePath { - path, - resource: &self.resources[path.resource_idx], - }) - } else { - None - } - }) - } - - pub fn subtree_with_depth( - &self, - search_path: &str, - depth: usize, - ) -> impl Iterator> { - let prefix = format!("{search_path}/"); - self.paths.iter().filter_map(move |path| { - if path - .path - .strip_prefix(&prefix) - .is_some_and(|name| name.as_bytes().iter().filter(|&&c| c == b'/').count() < depth) - || path.path.as_str() == search_path - { - Some(DavResourcePath { - path, - resource: &self.resources[path.resource_idx], - }) - } else { - None - } - }) - } - - pub fn tree_with_depth(&self, depth: usize) -> impl Iterator> { - self.paths.iter().filter_map(move |path| { - if path.path.as_bytes().iter().filter(|&&c| c == b'/').count() <= depth { - Some(DavResourcePath { - path, - resource: &self.resources[path.resource_idx], - }) - } else { - None - } - }) - } - - pub fn children(&self, parent_id: u32) -> impl Iterator> { - self.paths - .iter() - .filter(move |item| item.parent_id.is_some_and(|id| id == parent_id)) - .map(|path| DavResourcePath { - path, - resource: &self.resources[path.resource_idx], - }) - } - - pub fn children_ids(&self, parent_id: u32) -> impl Iterator { - self.paths - .iter() - .filter(move |item| item.parent_id.is_some_and(|id| id == parent_id)) - .map(|path| self.resources[path.resource_idx].document_id) - } - - pub fn format_resource(&self, resource: DavResourcePath<'_>) -> String { - if resource.resource.is_container() { - format!("{}{}/", self.base_path, resource.path.path) - } else { - format!("{}{}", self.base_path, resource.path.path) - } - } - - pub fn format_collection(&self, name: &str) -> String { - format!("{}{name}/", self.base_path) - } - - pub fn format_item(&self, name: &str) -> String { - format!("{}{}", self.base_path, name) - } -} - -const SCHEDULE_INBOX_ID: u32 = u32::MAX - 1; - -impl DavResource { - pub fn is_child_of(&self, parent_id: u32) -> bool { - match &self.data { - DavResourceMetadata::File { parent_id: id, .. } => id.is_some_and(|id| id == parent_id), - DavResourceMetadata::CalendarEvent { names, .. } => { - names.iter().any(|name| name.parent_id == parent_id) - } - DavResourceMetadata::ContactCard { names } => { - names.iter().any(|name| name.parent_id == parent_id) - } - DavResourceMetadata::CalendarEventNotification { names } => { - names.is_empty() && parent_id == SCHEDULE_INBOX_ID - } - _ => false, - } - } - - pub fn parent_id(&self) -> Option { - match &self.data { - DavResourceMetadata::File { parent_id, .. } => *parent_id, - DavResourceMetadata::CalendarEvent { names, .. } => { - names.first().map(|name| name.parent_id) - } - DavResourceMetadata::ContactCard { names } => names.first().map(|name| name.parent_id), - DavResourceMetadata::CalendarEventNotification { names } if names.is_empty() => { - Some(SCHEDULE_INBOX_ID) - } - _ => None, - } - } - - pub fn child_names(&self) -> Option<&[DavName]> { - match &self.data { - DavResourceMetadata::CalendarEvent { names, .. } => Some(names.as_slice()), - DavResourceMetadata::ContactCard { names } => Some(names.as_slice()), - DavResourceMetadata::CalendarEventNotification { names } if !names.is_empty() => { - Some(names.as_slice()) - } - _ => None, - } - } - - pub fn container_name(&self) -> Option<&str> { - match &self.data { - DavResourceMetadata::File { name, .. } => Some(name.as_str()), - DavResourceMetadata::Calendar { name, .. } => Some(name.as_str()), - DavResourceMetadata::AddressBook { name, .. } => Some(name.as_str()), - DavResourceMetadata::CalendarEventNotification { names } if names.is_empty() => { - Some(if self.document_id == SCHEDULE_INBOX_ID { - "inbox" - } else { - "outbox" - }) - } - _ => None, - } - } - - pub fn has_hierarchy_changes(&self, other: &DavResource) -> bool { - match (&self.data, &other.data) { - ( - DavResourceMetadata::File { - name: a, - parent_id: c, - .. - }, - DavResourceMetadata::File { - name: b, - parent_id: d, - .. - }, - ) => a != b || c != d, - ( - DavResourceMetadata::Calendar { name: a, .. }, - DavResourceMetadata::Calendar { name: b, .. }, - ) => a != b, - ( - DavResourceMetadata::AddressBook { name: a, .. }, - DavResourceMetadata::AddressBook { name: b, .. }, - ) => a != b, - ( - DavResourceMetadata::CalendarEvent { names: a, .. }, - DavResourceMetadata::CalendarEvent { names: b, .. }, - ) => a != b, - ( - DavResourceMetadata::ContactCard { names: a, .. }, - DavResourceMetadata::ContactCard { names: b, .. }, - ) => a != b, - ( - DavResourceMetadata::CalendarEventNotification { names: a, .. }, - DavResourceMetadata::CalendarEventNotification { names: b, .. }, - ) => a != b, - _ => unreachable!(), - } - } - - pub fn event_time_range(&self) -> Option<(i64, i64)> { - match &self.data { - DavResourceMetadata::CalendarEvent { - start, duration, .. - } => Some((*start, *start + *duration as i64)), - _ => None, - } - } - - pub fn calendar_preferences(&self, account_id: u32) -> Option<&TinyCalendarPreferences> { - match &self.data { - DavResourceMetadata::Calendar { preferences, .. } => preferences - .iter() - .find(|pref| pref.account_id == account_id) - .or_else(|| preferences.first()), - _ => None, - } - } - - pub fn is_container(&self) -> bool { - match &self.data { - DavResourceMetadata::File { size, .. } => size.is_none(), - DavResourceMetadata::Calendar { .. } | DavResourceMetadata::AddressBook { .. } => true, - DavResourceMetadata::CalendarEventNotification { names } => names.is_empty(), - _ => false, - } - } - - pub fn size(&self) -> Option { - match &self.data { - DavResourceMetadata::File { size, .. } => *size, - _ => None, - } - } - - pub fn acls(&self) -> Option<&[AclGrant]> { - match &self.data { - DavResourceMetadata::File { acls, .. } => Some(acls.as_slice()), - DavResourceMetadata::Calendar { acls, .. } => Some(acls.as_slice()), - DavResourceMetadata::AddressBook { acls, .. } => Some(acls.as_slice()), - _ => None, - } - } -} - -impl Hash for DavPath { - fn hash(&self, state: &mut H) { - self.path.hash(state); - } -} - -impl PartialEq for DavPath { - fn eq(&self, other: &Self) -> bool { - self.path == other.path - } -} - -impl Eq for DavPath {} - -impl std::borrow::Borrow for DavPath { - fn borrow(&self) -> &str { - &self.path - } -} - -impl std::hash::Hash for DavResource { - fn hash(&self, state: &mut H) { - self.document_id.hash(state); - } -} - -impl PartialEq for DavResource { - fn eq(&self, other: &Self) -> bool { - self.document_id == other.document_id - } -} - -impl Eq for DavResource {} - -impl std::borrow::Borrow for DavResource { - fn borrow(&self) -> &u32 { - &self.document_id - } -} - -impl DavName { - pub fn new(name: String, parent_id: u32) -> Self { - Self { name, parent_id } - } - - pub fn new_with_rand_name(parent_id: u32) -> Self { - Self { - name: store::rand::rng() - .sample_iter(Alphanumeric) - .take(10) - .map(char::from) - .collect::(), - parent_id, - } - } -} - -impl MailboxCache { - pub fn parent_id(&self) -> Option { - if self.parent_id != u32::MAX { - Some(self.parent_id) - } else { - None - } - } - - pub fn sort_order(&self) -> Option { - if self.sort_order != u32::MAX { - Some(self.sort_order) - } else { - None - } - } - - pub fn is_root(&self) -> bool { - self.parent_id == u32::MAX - } -} - pub const DEFAULT_LOGO_RAW: &str = r#"