From 726e8d9c794870db2c207cf7cb3dd0c2c1672468 Mon Sep 17 00:00:00 2001 From: mdecimus <11444311+mdecimus@users.noreply.github.com> Date: Fri, 13 Feb 2026 22:58:18 +0000 Subject: [PATCH] Update all modules to use registry - part 4 --- Cargo.lock | 1 + crates/common/Cargo.toml | 1 + crates/common/src/auth/authentication.rs | 137 +++++------ crates/common/src/auth/mod.rs | 67 +++++- crates/common/src/cache/directory.rs | 164 ++------------ crates/common/src/cache/mod.rs | 1 + crates/common/src/cache/principals.rs | 214 ++++++++++++++++++ crates/common/src/cache/reload.rs | 2 +- crates/common/src/config/mailstore/email.rs | 28 ++- crates/common/src/config/mod.rs | 1 + crates/common/src/config/server/tls.rs | 21 +- crates/common/src/expr/eval.rs | 3 +- crates/common/src/expr/functions/asynch.rs | 14 +- crates/common/src/lib.rs | 4 +- crates/common/src/manager/backup.rs | 12 +- crates/common/src/network/acme/mod.rs | 2 +- crates/common/src/network/masked.rs | 42 ++++ crates/common/src/network/mod.rs | 2 +- crates/common/src/network/mta.rs | 152 ++++++++++++- crates/common/src/storage/index.rs | 16 +- crates/common/src/storage/quota.rs | 12 +- crates/dav/src/calendar/scheduling.rs | 6 +- crates/dav/src/common/uri.rs | 2 +- crates/directory/src/core/config.rs | 29 +-- crates/email/src/message/delivery.rs | 2 +- crates/groupware/src/calendar/itip.rs | 2 +- crates/imap/src/op/acl.rs | 2 +- crates/jmap/src/principal/query.rs | 2 +- crates/migration/src/lib.rs | 6 +- crates/registry/src/types/index.rs | 36 ++- crates/smtp/src/inbound/data.rs | 2 +- crates/smtp/src/inbound/rcpt.rs | 8 +- crates/smtp/src/inbound/vrfy.rs | 17 +- crates/spam-filter/src/modules/classifier.rs | 12 +- .../store/src/backend/foundationdb/write.rs | 3 +- crates/store/src/backend/mysql/main.rs | 3 +- crates/store/src/backend/postgres/main.rs | 3 +- crates/store/src/backend/rocksdb/main.rs | 3 +- crates/store/src/backend/sqlite/main.rs | 3 +- crates/store/src/dispatch/registry.rs | 8 +- crates/store/src/lib.rs | 4 +- crates/store/src/registry/bootstrap.rs | 64 +++--- crates/store/src/write/key.rs | 111 ++++----- crates/store/src/write/mod.rs | 16 +- crates/types/src/field.rs | 18 +- tests/src/store/cleanup.rs | 8 +- tests/src/store/import_export.rs | 3 +- 47 files changed, 791 insertions(+), 478 deletions(-) create mode 100644 crates/common/src/cache/principals.rs create mode 100644 crates/common/src/network/masked.rs diff --git a/Cargo.lock b/Cargo.lock index 992420e0..52ea2675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1111,6 +1111,7 @@ dependencies = [ "proxy-header", "psl", "pwhash", + "quick_cache", "rcgen 0.12.1", "regex", "registry", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index d47cf2be..1f8907f6 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -79,6 +79,7 @@ lz4_flex = { version = "0.12", features = ["frame"], default-features = false } hickory-proto = "0.24" arcstr = "1.2.0" nohash-hasher = "0.2.0" +quick_cache = "0.6.9" [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/common/src/auth/authentication.rs b/crates/common/src/auth/authentication.rs index 4e287335..b1fc2a78 100644 --- a/crates/common/src/auth/authentication.rs +++ b/crates/common/src/auth/authentication.rs @@ -7,13 +7,13 @@ use crate::{ Server, auth::{ - AccessToken, AuthRequest, + AccessToken, AuthRequest, DomainCache, credential::{ApiKey, AppPassword}, oauth::GrantType, }, }; use directory::{ - Credentials, Directory, + Credentials, core::secret::{verify_mfa_secret_hash, verify_secret_hash}, }; use registry::schema::{ @@ -74,7 +74,9 @@ impl Server { return if verify_secret_hash(fallback_hash, secret.as_bytes()).await? { if username.is_master() { let address = username.account().address(); - if let Some(account_id) = self.account_id(address).await? { + if let Some(account_id) = + self.account_id_from_email(address, false).await? + { trc::event!( Auth(trc::AuthEvent::Success), AccountName = address.to_string(), @@ -108,12 +110,20 @@ impl Server { }; } + // Obtain domain let auth_as = username.auth_as(); + let auth_as_address = auth_as.address(); + let auth_as_local = auth_as.local(); + let auth_as_domain = auth_as.domain(); + let domain = self + .domain_or_default(auth_as_address, auth_as_domain) + .await?; // Authenticate app passwords if let Some(app_pass) = AppPassword::parse(secret) { - let account_name = auth_as.address(); - return if let Some(account_id) = self.account_id(account_name).await? { + return if let Some(account_id) = + self.account_id_from_parts(auth_as_local, domain.id).await? + { self.validate_credential( account_id, app_pass.credential_id, @@ -124,24 +134,26 @@ impl Server { } else { Err(trc::AuthEvent::Failed .into_err() - .ctx(trc::Key::AccountName, account_name.to_string()) + .ctx(trc::Key::AccountName, auth_as_address.to_string()) .reason("App password authentication failed: account not found")) }; } // Obtain external directory, if any - let address = auth_as.address(); - let directory = self - .directory_for_domain(address, auth_as.domain()) - .await - .caused_by(trc::location!())?; + let directory = domain + .id_directory + .and_then(|domain_id| self.core.storage.directories.get(&domain_id)) + .or_else(|| self.get_default_directory()); let mut is_alias_login = false; let token = if let Some(directory) = directory { let directory_account = directory.authenticate(&req.credentials).await?; - is_alias_login = directory_account.email != address; - self.synchronize_directory(directory_account).await - } else if let Some(account_id) = self.account_id(address).await? { + + is_alias_login = directory_account.email != auth_as_address; + self.build_directory_token(directory_account).await + } else if let Some(account_id) = + self.account_id_from_parts(auth_as_local, domain.id).await? + { if let Some(account) = self .registry() .object::(account_id) @@ -155,12 +167,12 @@ impl Server { ) .await? { - is_alias_login = account.name != address; + is_alias_login = account.name != auth_as_address; self.access_token(account_id).await.map(AccessToken::new) } else { Err(trc::AuthEvent::Failed .into_err() - .ctx(trc::Key::AccountName, address.to_string()) + .ctx(trc::Key::AccountName, auth_as_address.to_string()) .ctx(trc::Key::AccountId, account_id) .ctx(trc::Key::SpanId, req.session_id) .reason("Authentication failed")) @@ -168,14 +180,14 @@ impl Server { } else { Err(trc::AuthEvent::Error .into_err() - .ctx(trc::Key::AccountName, address.to_string()) + .ctx(trc::Key::AccountName, auth_as_address.to_string()) .ctx(trc::Key::AccountId, account_id) .reason("Account not found in registry")) } } else { Err(trc::AuthEvent::Failed .into_err() - .ctx(trc::Key::AccountName, address.to_string()) + .ctx(trc::Key::AccountName, auth_as_address.to_string()) .reason("Account not found")) }?; @@ -183,7 +195,7 @@ impl Server { if is_alias_login && !token.has_permission(Permission::AuthenticateAlias) { return Err(trc::AuthEvent::Failed .into_err() - .ctx(trc::Key::AccountName, address.to_string()) + .ctx(trc::Key::AccountName, auth_as_address.to_string()) .ctx(trc::Key::AccountId, token.account_id()) .ctx(trc::Key::SpanId, req.session_id) .reason("Authenticated using an email alias but account does not have AuthenticateAlias permission")); @@ -197,7 +209,7 @@ impl Server { ])?; let address = username.account().address(); let master_address = username.account().address(); - if let Some(account_id) = self.account_id(address).await? { + if let Some(account_id) = self.account_id_from_email(address, false).await? { trc::event!( Auth(trc::AuthEvent::Success), AccountName = address.to_string(), @@ -217,7 +229,7 @@ impl Server { } else { trc::event!( Auth(trc::AuthEvent::Success), - AccountName = address.to_string(), + AccountName = auth_as_address.to_string(), AccountId = token.account_id(), SpanId = req.session_id, ); @@ -241,10 +253,15 @@ impl Server { // Obtain external directory, if any let directory = if let Some(username) = username.as_deref().map(UsernameParts::new) { - let auth_as = username.auth_as(); - self.directory_for_domain(auth_as.address(), auth_as.domain()) - .await - .caused_by(trc::location!())? + if let Some(domain_name) = username.auth_as().domain() { + self.domain(domain_name) + .await + .caused_by(trc::location!())? + .and_then(|domain| self.core.storage.directories.get(&domain.id)) + .or_else(|| self.get_default_directory()) + } else { + self.get_default_directory() + } } else { self.get_default_directory() }; @@ -253,7 +270,7 @@ impl Server { { match directory.authenticate(&req.credentials).await { Ok(result) => { - return self.synchronize_directory(result).await; + return self.build_directory_token(result).await; } Err(err) => { if !err.matches(trc::EventType::Auth(trc::AuthEvent::Failed)) { @@ -350,55 +367,35 @@ impl Server { } } - async fn directory_for_domain( + async fn domain_or_default( &self, address: &str, domain_name: Option<&str>, - ) -> trc::Result>> { - // SPDX-SnippetBegin - // SPDX-FileCopyrightText: 2020 Stalwart Labs LLC - // SPDX-License-Identifier: LicenseRef-SEL - #[cfg(feature = "enterprise")] - if self.core.is_enterprise_edition() { - if let Some(domain_name) = domain_name { - if let Some(domain) = self.domain(domain_name).await? { - if domain.id_directory != u32::MAX { - if let Some(directory) = - self.core.storage.directories.get(&domain.id_directory) - { - return Ok(Some(directory)); - } else { - trc::event!( - Auth(trc::AuthEvent::Warning), - AccountName = address.to_string(), - Domain = domain_name.to_string(), - Id = domain.id_directory, - Reason = "Directory not found for domain", - ); - } - } - } else { - trc::event!( - Auth(trc::AuthEvent::Warning), - AccountName = address.to_string(), - Reason = "Domain not found", - ); - } + ) -> trc::Result> { + if let Some(domain_name) = domain_name { + if let Some(domain) = self.domain(domain_name).await? { + Ok(domain) } else { - trc::event!( - Auth(trc::AuthEvent::Warning), - AccountName = address.to_string(), - Reason = "No domain in username", - ); + Err(trc::AuthEvent::Failed + .into_err() + .ctx(trc::Key::AccountName, address.to_string()) + .reason("Domain not found")) } + } else { + trc::event!( + Auth(trc::AuthEvent::Warning), + AccountName = address.to_string(), + Reason = "No domain in username", + ); + self.domain_by_id(self.core.email.default_domain_id).await } - // SPDX-SnippetEnd - - Ok(self.get_default_directory()) } - async fn synchronize_directory(&self, account: directory::Account) -> trc::Result { - todo!() + async fn build_directory_token(&self, account: directory::Account) -> trc::Result { + let account = self.synchronize_account(account).await?; + self.access_token_from_account(account.id, account.account) + .await + .map(AccessToken::new) } } @@ -453,6 +450,12 @@ impl Username { self.name.as_str() } + pub fn local(&self) -> &str { + self.name + .get(..self.domain_start.saturating_sub(1)) + .unwrap_or_default() + } + pub fn domain(&self) -> Option<&str> { self.name.get(self.domain_start..) } diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 674c5d39..d508ab33 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -11,11 +11,16 @@ use crate::{ }; use arcstr::ArcStr; use directory::Credentials; +use quick_cache::Equivalent; use registry::{ schema::enums::{Locale, Permission}, types::EnumType, }; -use std::{net::IpAddr, sync::Arc}; +use std::{ + hash::{Hash, Hasher}, + net::IpAddr, + sync::Arc, +}; use tinyvec::TinyVec; use trc::ipc::bitset::Bitset; use types::collection::Collection; @@ -32,6 +37,18 @@ 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; +#[derive(Debug, PartialEq, Eq)] +pub struct EmailAddress { + local_part: Box, + id_domain: u32, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct EmailAddressRef<'x> { + local_part: &'x str, + id_domain: u32, +} + #[derive(Debug, Clone, Copy)] pub enum EmailCache { Account(u32), @@ -40,20 +57,17 @@ pub enum EmailCache { #[derive(Debug, Clone)] pub struct DomainCache { - pub name: ArcStr, + pub names: Box<[ArcStr]>, pub id: u32, - pub id_directory: u32, + pub id_directory: Option, pub id_tenant: u32, pub catch_all: Option, pub sub_addressing_custom: Option>, pub flags: u8, } -pub const DOMAIN_FLAG_REMOTE: u8 = 1; -pub const DOMAIN_FLAG_SYSTEM: u8 = 1 << 1; -pub const DOMAIN_FLAG_SUB_ADDRESSING: u8 = 1 << 2; -pub const DOMAIN_FLAG_WILDCARD: u8 = 1 << 3; -pub const DOMAIN_FLAG_ALIAS_LOGIN: u8 = 1 << 4; +pub const DOMAIN_FLAG_RELAY: u8 = 1; +pub const DOMAIN_FLAG_SUB_ADDRESSING: u8 = 1 << 1; #[derive(Debug, Clone)] pub struct AccountCache { @@ -153,6 +167,12 @@ impl CacheItemWeight for AccessTokenInner { } } +impl CacheItemWeight for EmailAddress { + fn weight(&self) -> u64 { + std::mem::size_of::() as u64 + self.local_part.len() as u64 + } +} + impl CacheItemWeight for EmailCache { fn weight(&self) -> u64 { std::mem::size_of::() as u64 @@ -162,7 +182,7 @@ impl CacheItemWeight for EmailCache { impl CacheItemWeight for DomainCache { fn weight(&self) -> u64 { std::mem::size_of::() as u64 - + self.name.len() as u64 + + self.names.iter().map(|s| s.len() as u64).sum::() + self.catch_all.as_ref().map_or(0, |s| s.len() as u64) + self .sub_addressing_custom @@ -171,6 +191,26 @@ impl CacheItemWeight for DomainCache { } } +impl Equivalent for EmailAddressRef<'_> { + fn equivalent(&self, key: &EmailAddress) -> bool { + self.local_part == &*key.local_part && self.id_domain == key.id_domain + } +} + +impl Hash for EmailAddress { + fn hash(&self, state: &mut H) { + self.local_part.as_ref().hash(state); + self.id_domain.hash(state); + } +} + +impl Hash for EmailAddressRef<'_> { + fn hash(&self, state: &mut H) { + self.local_part.hash(state); + self.id_domain.hash(state); + } +} + impl CacheItemWeight for AccountCache { fn weight(&self) -> u64 { std::mem::size_of::() as u64 @@ -218,3 +258,12 @@ impl BuildAccessToken for Arc { } } } + +impl<'x> EmailAddressRef<'x> { + pub fn new(local_part: &'x str, id_domain: u32) -> Self { + Self { + local_part, + id_domain, + } + } +} diff --git a/crates/common/src/cache/directory.rs b/crates/common/src/cache/directory.rs index 76e85e5f..4e9b7362 100644 --- a/crates/common/src/cache/directory.rs +++ b/crates/common/src/cache/directory.rs @@ -4,162 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use registry::schema::enums::Locale; +use crate::Server; +use registry::schema::structs::Account; -use crate::{ - Server, - auth::{AccountCache, AccountInfo, AccountTenantIds, DomainCache, RoleCache, TenantCache}, - config::smtp::auth::DkimSigner, - storage::ObjectQuota, -}; -use std::sync::Arc; +pub(crate) struct AccountWithId { + pub id: u32, + pub account: Account, +} impl Server { - pub async fn domain(&self, domain: &str) -> trc::Result>> { + pub(crate) async fn synchronize_account( + &self, + account: directory::Account, + ) -> trc::Result { todo!() } - pub async fn account(&self, id: u32) -> trc::Result> { - /* - - Err(trc::AuthEvent::Error - .into_err() - .details("Account not found.") - .caused_by(trc::location!())) - */ - todo!() - } - - pub async fn account_id(&self, address: &str) -> trc::Result> { - todo!() - } - - pub async fn account_info(&self, id: u32) -> trc::Result { - let account = self.account(id).await?; - let mut member_of = Vec::with_capacity(account.id_member_of.len()); - for &group_id in &account.id_member_of { - member_of.push(self.account(group_id).await?); - } - - Ok(AccountInfo { - account_id: id, - account, - member_of, - }) - } - - pub async fn role(&self, id: u32) -> trc::Result> { - todo!() - } - - pub async fn tenant(&self, id: u32) -> trc::Result> { - todo!() - } - - pub async fn dkim_signers(&self, domain: &str) -> trc::Result>> { + pub(crate) async fn synchronize_group( + &self, + group: directory::Group, + ) -> trc::Result { todo!() } } - -impl AccountInfo { - #[inline(always)] - pub fn account_id(&self) -> u32 { - self.account_id - } - - pub fn name(&self) -> &str { - self.account - .addresses - .first() - .map(|s| s.as_ref()) - .unwrap_or_default() - } - - #[inline(always)] - pub fn description(&self) -> Option<&str> { - self.account.description.as_deref() - } - - #[inline(always)] - pub fn tenant_id(&self) -> Option { - self.account.id_tenant - } - - #[inline(always)] - pub fn account_tenant_ids(&self) -> AccountTenantIds { - AccountTenantIds { - account_id: self.account_id, - tenant_id: self.account.id_tenant, - } - } - - pub fn addresses(&self) -> impl Iterator { - self.account - .addresses - .iter() - .chain( - self.member_of - .iter() - .flat_map(move |member| member.addresses.iter()), - ) - .map(|a| a.as_str()) - } - - #[inline(always)] - pub fn is_user_account(&self) -> bool { - self.account.is_user - } - - #[inline(always)] - pub fn locale(&self) -> Locale { - self.account.locale - } - - #[inline(always)] - pub fn object_quotas(&self) -> Option<&ObjectQuota> { - self.account.quota_objects.as_deref() - } -} - -impl AccountCache { - #[inline(always)] - pub fn name(&self) -> &str { - self.addresses - .first() - .map(|s| s.as_ref()) - .unwrap_or_default() - } - - #[inline(always)] - pub fn description(&self) -> Option<&str> { - self.description.as_deref() - } - - #[inline(always)] - pub fn tenant_id(&self) -> Option { - self.id_tenant - } - - #[inline(always)] - pub fn is_user_account(&self) -> bool { - self.is_user - } - - #[inline(always)] - pub fn disk_quota(&self) -> u64 { - self.quota_disk - } - - #[inline(always)] - pub fn object_quotas(&self) -> Option<&ObjectQuota> { - self.quota_objects.as_deref() - } - - #[inline(always)] - pub fn account_tenant_ids(&self, account_id: u32) -> AccountTenantIds { - AccountTenantIds { - account_id, - tenant_id: self.id_tenant, - } - } -} diff --git a/crates/common/src/cache/mod.rs b/crates/common/src/cache/mod.rs index c225a897..db38fdd4 100644 --- a/crates/common/src/cache/mod.rs +++ b/crates/common/src/cache/mod.rs @@ -11,6 +11,7 @@ use utils::cache::CacheItemWeight; pub mod directory; pub mod invalidate; +pub mod principals; pub mod reload; impl MailboxCache { diff --git a/crates/common/src/cache/principals.rs b/crates/common/src/cache/principals.rs new file mode 100644 index 00000000..448a2e7c --- /dev/null +++ b/crates/common/src/cache/principals.rs @@ -0,0 +1,214 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use p256::elliptic_curve::group; +use registry::schema::enums::Locale; + +use crate::{ + Server, + auth::{ + AccountCache, AccountInfo, AccountTenantIds, DomainCache, EmailCache, MailingListCache, + RoleCache, TenantCache, + }, + config::smtp::auth::DkimSigner, + storage::ObjectQuota, +}; +use std::sync::Arc; + +impl Server { + pub async fn domain(&self, domain: &str) -> trc::Result>> { + todo!() + } + + pub async fn domain_by_id(&self, domain_id: u32) -> trc::Result> { + todo!() + } + + pub async fn rcpt_id_from_parts( + &self, + local_part: &str, + domain_id: u32, + ) -> trc::Result> { + todo!() + } + + pub async fn rcpt_id_from_email(&self, address: &str) -> trc::Result> { + todo!() + } + + pub async fn account(&self, id: u32) -> trc::Result> { + /* + + Err(trc::AuthEvent::Error + .into_err() + .details("Account not found.") + .caused_by(trc::location!())) + */ + todo!() + } + + pub async fn try_account(&self, id: u32) -> trc::Result>> { + /* + + Err(trc::AuthEvent::Error + .into_err() + .details("Account not found.") + .caused_by(trc::location!())) + */ + todo!() + } + + pub async fn account_id_from_parts( + &self, + local: &str, + domain_id: u32, + ) -> trc::Result> { + todo!() + } + + pub async fn account_id_from_email( + &self, + address: &str, + resolve: bool, + ) -> trc::Result> { + let todo = "resolve subaddressing"; + todo!() + } + + pub async fn account_info(&self, id: u32) -> trc::Result { + let account = self.account(id).await?; + let mut member_of = Vec::with_capacity(account.id_member_of.len()); + for &group_id in &account.id_member_of { + if let Some(group) = self.try_account(group_id).await? { + member_of.push(group); + } + } + Ok(AccountInfo { + account_id: id, + account, + member_of, + }) + } + + pub async fn role(&self, id: u32) -> trc::Result> { + todo!() + } + + pub async fn tenant(&self, id: u32) -> trc::Result> { + todo!() + } + + pub async fn try_list(&self, id: u32) -> trc::Result>> { + todo!() + } + + pub async fn dkim_signers(&self, domain: &str) -> trc::Result>> { + todo!() + } +} + +impl AccountInfo { + #[inline(always)] + pub fn account_id(&self) -> u32 { + self.account_id + } + + pub fn name(&self) -> &str { + self.account + .addresses + .first() + .map(|s| s.as_ref()) + .unwrap_or_default() + } + + #[inline(always)] + pub fn description(&self) -> Option<&str> { + self.account.description.as_deref() + } + + #[inline(always)] + pub fn tenant_id(&self) -> Option { + self.account.id_tenant + } + + #[inline(always)] + pub fn account_tenant_ids(&self) -> AccountTenantIds { + AccountTenantIds { + account_id: self.account_id, + tenant_id: self.account.id_tenant, + } + } + + pub fn addresses(&self) -> impl Iterator { + self.account + .addresses + .iter() + .chain( + self.member_of + .iter() + .flat_map(move |member| member.addresses.iter()), + ) + .map(|a| a.as_str()) + } + + #[inline(always)] + pub fn is_user_account(&self) -> bool { + self.account.is_user + } + + #[inline(always)] + pub fn locale(&self) -> Locale { + self.account.locale + } + + #[inline(always)] + pub fn object_quotas(&self) -> Option<&ObjectQuota> { + self.account.quota_objects.as_deref() + } +} + +impl AccountCache { + #[inline(always)] + pub fn name(&self) -> &str { + self.addresses + .first() + .map(|s| s.as_ref()) + .unwrap_or_default() + } + + #[inline(always)] + pub fn description(&self) -> Option<&str> { + self.description.as_deref() + } + + #[inline(always)] + pub fn tenant_id(&self) -> Option { + self.id_tenant + } + + #[inline(always)] + pub fn is_user_account(&self) -> bool { + self.is_user + } + + #[inline(always)] + pub fn disk_quota(&self) -> u64 { + self.quota_disk + } + + #[inline(always)] + pub fn object_quotas(&self) -> Option<&ObjectQuota> { + self.quota_objects.as_deref() + } + + #[inline(always)] + pub fn account_tenant_ids(&self, account_id: u32) -> AccountTenantIds { + AccountTenantIds { + account_id, + tenant_id: self.id_tenant, + } + } +} diff --git a/crates/common/src/cache/reload.rs b/crates/common/src/cache/reload.rs index 90deb62e..8e5d0a27 100644 --- a/crates/common/src/cache/reload.rs +++ b/crates/common/src/cache/reload.rs @@ -32,7 +32,7 @@ impl Server { let object = match change { RegistryChange::Insert(id) => { if matches!(id.object(), Object::BlockedIp) { - if let Some(ip) = bootstrap.get_infallible::(id).await + if let Some(ip) = bootstrap.get_infallible::(id.id()).await && ip.expires_at.is_none_or(|ip| ip.timestamp() > now() as i64) { let mut ips = self.inner.data.blocked_ips.write(); diff --git a/crates/common/src/config/mailstore/email.rs b/crates/common/src/config/mailstore/email.rs index 08ab2ff7..dcaef144 100644 --- a/crates/common/src/config/mailstore/email.rs +++ b/crates/common/src/config/mailstore/email.rs @@ -12,8 +12,9 @@ use registry::{ CompressionAlgo, SearchCalendarField, SearchContactField, SearchEmailField, StorageQuota, }, + prelude::Object, structs::{ - AddressBook, Calendar, DataRetention, Email, Jmap, OidcProvider, Search, + AddressBook, Authentication, Calendar, DataRetention, Domain, Email, Jmap, Search, SieveUserInterpreter, }, }, @@ -33,6 +34,8 @@ use crate::storage::ObjectQuota; #[derive(Clone)] pub struct EmailConfig { pub default_language: Language, + pub default_domain_id: u32, + pub default_domain_name: String, pub mailbox_max_depth: usize, pub mailbox_name_max_len: usize, @@ -82,7 +85,24 @@ 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; + let auth = bp.setting_infallible::().await; + + // Obtain default domain name + let default_domain_name = if let Some(default_domain) = bp + .get_infallible::(auth.default_domain_id.id()) + .await + { + default_domain.name + } else { + bp.build_error( + Object::Authentication.singleton(), + format!( + "Default domain with ID {} not found", + auth.default_domain_id + ), + ); + "localhost.local".to_string() + }; // Parse default object quotas let todo = "make sure all are configurable"; @@ -93,7 +113,7 @@ impl EmailConfig { (StorageQuota::MaxEmailIdentities, email.max_identities), (StorageQuota::MaxEmailSubmissions, email.max_submissions), (StorageQuota::MaxMaskedAddresses, email.max_masked_addresses), - (StorageQuota::MaxAppPasswords, oidc.max_app_passwords), + (StorageQuota::MaxAppPasswords, auth.max_app_passwords), (StorageQuota::MaxPushSubscriptions, jmap.max_subscriptions), (StorageQuota::MaxCalendars, calendar.max_calendars), (StorageQuota::MaxCalendarEvents, calendar.max_events), @@ -255,6 +275,8 @@ impl EmailConfig { data_purge_frequency: dr.data_cleanup_schedule.into(), blob_purge_frequency: dr.blob_cleanup_schedule.into(), compression: email.compression_algorithm, + default_domain_id: auth.default_domain_id.id() as u32, + default_domain_name, } } } diff --git a/crates/common/src/config/mod.rs b/crates/common/src/config/mod.rs index 75f5b8b7..7fb4b7a8 100644 --- a/crates/common/src/config/mod.rs +++ b/crates/common/src/config/mod.rs @@ -63,6 +63,7 @@ impl Core { } storage.metrics = Store::None; storage.metrics = Store::None; + storage.directories.clear(); } enterprise }; diff --git a/crates/common/src/config/server/tls.rs b/crates/common/src/config/server/tls.rs index 33dc08a3..64eb94a3 100644 --- a/crates/common/src/config/server/tls.rs +++ b/crates/common/src/config/server/tls.rs @@ -12,12 +12,9 @@ use dns_update::{ }; use hickory_proto::rr::dnssec::KeyPair; use rcgen::generate_simple_self_signed; -use registry::{ - schema::{ - enums, - structs::{self, Certificate, DnsServer}, - }, - types::id::Id, +use registry::schema::{ + enums, + structs::{self, Certificate, DnsServer}, }; use ring::signature::{EcdsaKeyPair, Ed25519KeyPair}; use rustls::{ @@ -33,7 +30,7 @@ use std::{ net::{Ipv4Addr, Ipv6Addr, SocketAddr}, sync::Arc, }; -use store::registry::{RegistryObject, bootstrap::Bootstrap}; +use store::registry::bootstrap::Bootstrap; use trc::AddContext; use x509_parser::{ certificate::X509Certificate, @@ -45,14 +42,14 @@ pub static TLS13_VERSION: &[&SupportedProtocolVersion] = &[&TLS13]; pub static TLS12_VERSION: &[&SupportedProtocolVersion] = &[&TLS12]; impl Server { - pub async fn build_acme_provider(&self, id: Id) -> trc::Result { + pub async fn build_acme_provider(&self, id: u64) -> trc::Result { if let Some(server) = self .registry() - .id::(id) + .object::(id) .await .caused_by(trc::location!())? { - Ok(AcmeProvider::new(RegistryObject { id, object: server })) + Ok(AcmeProvider::new(server)) } else { trc::bail!( trc::AcmeEvent::Error @@ -63,10 +60,10 @@ impl Server { } } - pub async fn build_dns_updater(&self, id: Id) -> trc::Result { + pub async fn build_dns_updater(&self, id: u64) -> trc::Result { let Some(server) = self .registry() - .id::(id) + .object::(id) .await .caused_by(trc::location!())? else { diff --git a/crates/common/src/expr/eval.rs b/crates/common/src/expr/eval.rs index b83d8a29..f5f5969c 100644 --- a/crates/common/src/expr/eval.rs +++ b/crates/common/src/expr/eval.rs @@ -224,8 +224,7 @@ impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec { - let todo = "implement domain retrieval"; - //stack.push(self.core.core.network.report_domain.as_str().into()) + stack.push(self.core.core.email.default_domain_name.as_str().into()) } SystemVariable::NodeId => stack.push(self.core.core.network.node_id.into()), SystemVariable::Metric(variable) => { diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index 3272c6ad..aead9848 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -5,7 +5,7 @@ */ use super::*; -use crate::{Server, expr::StringCow, network::RcptResolution}; +use crate::{Server, expr::StringCow}; use compact_str::{CompactString, ToCompactString}; use mail_auth::IpLookupStrategy; use std::{cmp::Ordering, net::IpAddr, vec::IntoIter}; @@ -33,18 +33,10 @@ impl Server { F_IS_LOCAL_ADDRESS => { let address = params.next_as_string(); - self.rcpt_resolve(address.as_ref()) + self.rcpt_id_from_email(address.as_ref()) .await .caused_by(trc::location!()) - .map(|v| { - (!matches!( - v, - RcptResolution::UnknownRecipient - | RcptResolution::UnknownDomain - | RcptResolution::Forward(_) - )) - .into() - }) + .map(|v| v.is_some().into()) } F_KEY_GET => { let Some(store) = self.get_lookup_store(params.next_as_string().as_str()) else { diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index b1469a36..5045a1c9 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -6,7 +6,7 @@ #![warn(clippy::large_futures)] -use crate::auth::AccessTokenInner; +use crate::auth::{AccessTokenInner, EmailAddress}; use crate::network::asn::AsnGeoLookupData; use crate::{ auth::{AccountCache, DomainCache, EmailCache, MailingListCache, RoleCache, TenantCache}, @@ -175,7 +175,7 @@ pub struct Caches { pub events: Cache>, pub scheduling: Cache>, - pub emails: Cache, + pub emails: Cache, pub emails_negative: CacheWithTtl, pub domain_names: Cache, pub domain_names_negative: CacheWithTtl, diff --git a/crates/common/src/manager/backup.rs b/crates/common/src/manager/backup.rs index 5c4062d6..cbcbb656 100644 --- a/crates/common/src/manager/backup.rs +++ b/crates/common/src/manager/backup.rs @@ -24,9 +24,8 @@ pub(super) const MAGIC_MARKER: u8 = 123; #[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub(super) enum Family { Data = 0, - Directory = 1, Blob = 2, - Config = 3, + Registry = 3, Changelog = 4, Queue = 5, Report = 6, @@ -66,9 +65,8 @@ impl Core { if params.families.is_empty() { params.families = [ Family::Data, - Family::Directory, + Family::Registry, Family::Blob, - Family::Config, Family::Changelog, Family::Queue, Family::Report, @@ -314,9 +312,8 @@ impl Family { SUBSPACE_COUNTER, SUBSPACE_PROPERTY, ], - Family::Directory => &[SUBSPACE_DIRECTORY], Family::Blob => &[SUBSPACE_BLOBS, SUBSPACE_BLOB_EXTRA, SUBSPACE_BLOB_LINK], - Family::Config => &[SUBSPACE_SETTINGS], + Family::Registry => &[SUBSPACE_REGISTRY], Family::Changelog => &[SUBSPACE_LOGS], Family::Queue => &[SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUEUE_EVENT], Family::Report => &[SUBSPACE_REPORT_OUT, SUBSPACE_REPORT_IN], @@ -328,9 +325,8 @@ impl Family { pub fn parse(family: &str) -> Result { match family { "data" => Ok(Family::Data), - "directory" => Ok(Family::Directory), + "registry" => Ok(Family::Registry), "blob" => Ok(Family::Blob), - "config" => Ok(Family::Config), "changelog" => Ok(Family::Changelog), "queue" => Ok(Family::Queue), "report" => Ok(Family::Report), diff --git a/crates/common/src/network/acme/mod.rs b/crates/common/src/network/acme/mod.rs index 60efa14d..f60289f0 100644 --- a/crates/common/src/network/acme/mod.rs +++ b/crates/common/src/network/acme/mod.rs @@ -55,7 +55,7 @@ pub struct StaticResolver { } impl AcmeProvider { - pub fn new(obj: RegistryObject) -> Self { + pub fn new(obj: structs::AcmeProvider) -> Self { // TODO: Prefix contact with "mailto:" if not present todo!() } diff --git a/crates/common/src/network/masked.rs b/crates/common/src/network/masked.rs new file mode 100644 index 00000000..8fc007b4 --- /dev/null +++ b/crates/common/src/network/masked.rs @@ -0,0 +1,42 @@ +/* + * SPDX-FileCopyrightText: 2020 Stalwart Labs LLC + * + * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL + */ + +use store::write::now; + +pub struct MaskedAddress { + pub account_id: u32, + pub address_id: u32, + pub has_expired: bool, +} + +const DEFAULT_EPOCH: u64 = 1632280000; // 52 years after UNIX_EPOCH + +impl MaskedAddress { + pub fn parse(local_part: &str) -> Option { + let mut parts = local_part.split('.'); + let _prefix = parts.next().filter(|v| !v.is_empty())?; + let ids = parts.next().filter(|v| !v.is_empty())?; + if parts.next().is_some() { + return None; + } + // Format: ... encoded as base36 + let ids = u128::from_str_radix(ids, 36).ok()?; + let account_id = (ids >> 96) as u32; + let address_id = (ids >> 64) as u32; + let expires = (ids >> 32) as u32; + let checksum = ids as u32; + + if checksum == (account_id ^ address_id ^ expires) { + Some(Self { + account_id, + address_id, + has_expired: (now().saturating_sub(DEFAULT_EPOCH) / 60) > expires as u64, + }) + } else { + None + } + } +} diff --git a/crates/common/src/network/mod.rs b/crates/common/src/network/mod.rs index 59700c8b..bd609c9d 100644 --- a/crates/common/src/network/mod.rs +++ b/crates/common/src/network/mod.rs @@ -29,6 +29,7 @@ pub mod asn; pub mod dns; pub mod limiter; pub mod listen; +pub mod masked; pub mod mta; pub mod security; pub mod stream; @@ -39,7 +40,6 @@ pub enum RcptResolution { Accept, Expand(Arc<[ArcStr]>), Rewrite(String), - Forward(String), #[default] UnknownRecipient, UnknownDomain, diff --git a/crates/common/src/network/mta.rs b/crates/common/src/network/mta.rs index 61b729b5..26f84587 100644 --- a/crates/common/src/network/mta.rs +++ b/crates/common/src/network/mta.rs @@ -6,6 +6,7 @@ use crate::{ Server, + auth::{DOMAIN_FLAG_RELAY, DOMAIN_FLAG_SUB_ADDRESSING, EmailAddressRef, EmailCache}, config::{ mailstore::spamfilter::SpamClassifier, smtp::{ @@ -16,25 +17,154 @@ use crate::{ }, }, }, + expr::{Variable, functions::ResolveVariable}, manager::SPAM_CLASSIFIER_KEY, - network::RcptResolution, + network::{RcptResolution, masked::MaskedAddress}, }; +use directory::Recipient; use mail_auth::IpLookupStrategy; +use registry::schema::{enums::ExpressionVariable, structs::MaskedEmail}; use sieve::Sieve; use std::{ + borrow::Cow, sync::{Arc, LazyLock}, time::Duration, }; use store::{ Deserialize, IterateParams, ValueKey, - write::{AlignedBytes, Archive, QueueClass, ValueClass}, + write::{AlignedBytes, Archive, QueueClass, ValueClass, now}, }; use trc::{AddContext, SpamEvent}; +use types::id::Id; impl Server { - pub async fn rcpt_resolve(&self, address: &str) -> trc::Result { - let todo = "TODO: RcptResolution implementation"; - todo!() + pub async fn rcpt_resolve(&self, rcpt: &str, session_id: u64) -> trc::Result { + // Obtain domain settings + let Some((local_part, domain_part)) = rcpt.rsplit_once('@') else { + return Ok(RcptResolution::UnknownDomain); + }; + let Some(domain) = self.domain(domain_part).await? else { + return Ok(RcptResolution::UnknownDomain); + }; + + // Sub-addressing resolution + let local_part_orig = local_part; + let mut local_part = Cow::Borrowed(local_part); + if domain.flags & DOMAIN_FLAG_SUB_ADDRESSING != 0 { + if let Some(sub_addressing) = &domain.sub_addressing_custom { + // Custom sub-addressing resolution + if let Some(result) = self + .eval_if::(sub_addressing, &Address(local_part.as_ref()), session_id) + .await + { + local_part = Cow::Owned(result); + } + } else if let Some((new_local_part, _)) = rcpt.split_once('+') { + local_part = Cow::Borrowed(new_local_part); + } + } + + // Masked email resolution + if let Cow::Borrowed(addr) = &local_part + && let Some(masked) = MaskedAddress::parse(addr) + { + return if !masked.has_expired + && let Some(masked_entry) = self + .registry() + .object::( + Id::from_parts(masked.account_id, masked.account_id).id(), + ) + .await + .caused_by(trc::location!())? + && masked_entry.enabled + && masked_entry + .expires_at + .is_none_or(|at| at.timestamp() > now() as i64) + && let Some(account) = self + .try_account(masked.account_id) + .await + .caused_by(trc::location!())? + && account.addresses.iter().any(|addr| { + addr.strip_suffix(domain_part) + .is_some_and(|a| a.ends_with('@')) + }) { + Ok(RcptResolution::Rewrite(account.name().to_string())) + } else { + Ok(RcptResolution::UnknownRecipient) + }; + } + + // Try resolving address from registry + if let Some(address_type) = self + .rcpt_id_from_parts(local_part.as_ref(), domain.id) + .await? + { + match address_type { + EmailCache::Account(id) => { + if self.try_account(id).await?.is_some() { + return if local_part.as_ref() == local_part_orig { + Ok(RcptResolution::Accept) + } else { + Ok(RcptResolution::Rewrite(format!( + "{local_part}@{domain_part}" + ))) + }; + } else { + self.inner + .cache + .emails + .remove(&EmailAddressRef::new(local_part.as_ref(), domain.id)); + } + } + EmailCache::MailingList(id) => { + if let Some(list) = self.try_list(id).await? { + return Ok(RcptResolution::Expand(list.recipients.clone())); + } else { + self.inner + .cache + .emails + .remove(&EmailAddressRef::new(local_part.as_ref(), domain.id)); + } + } + } + } + + // Obtain external directory, if configured + if let Some(directory) = domain + .id_directory + .and_then(|id| self.core.storage.directories.get(&id)) + .or_else(|| self.get_default_directory()) + .filter(|directory| directory.can_lookup_recipients()) + { + let address = if local_part.as_ref() == local_part_orig { + Cow::Borrowed(rcpt) + } else { + Cow::Owned(format!("{local_part}@{domain_part}")) + }; + match directory.recipient(address.as_ref()).await? { + Recipient::Account(account) => { + self.synchronize_account(account).await?; + return Ok(RcptResolution::Accept); + } + Recipient::Group(group) => { + self.synchronize_group(group).await?; + return Ok(RcptResolution::Accept); + } + Recipient::Invalid => {} + } + } + + // Catch-all resolution + if let Some(catch_all) = &domain.catch_all { + return Ok(RcptResolution::Rewrite(catch_all.to_string())); + } + + // Verify whether domain relaying is enabled + if domain.flags & DOMAIN_FLAG_RELAY != 0 { + Ok(RcptResolution::Accept) + } else { + Ok(RcptResolution::UnknownRecipient) + } } pub async fn get_dkim_signers( @@ -266,3 +396,15 @@ impl Server { .map(|_| total) } } + +struct Address<'x>(&'x str); + +impl ResolveVariable for Address<'_> { + fn resolve_variable(&'_ self, _: ExpressionVariable) -> crate::expr::Variable<'_> { + Variable::from(self.0) + } + + fn resolve_global(&self, _: &str) -> Variable<'_> { + Variable::Integer(0) + } +} diff --git a/crates/common/src/storage/index.rs b/crates/common/src/storage/index.rs index 17512375..bd4e205c 100644 --- a/crates/common/src/storage/index.rs +++ b/crates/common/src/storage/index.rs @@ -14,8 +14,8 @@ use std::{borrow::Cow, fmt::Debug}; use store::{ Serialize, SerializeInfallible, write::{ - Archive, Archiver, BatchBuilder, BlobLink, BlobOp, DirectoryClass, IntoOperations, Params, - SearchIndex, TaskEpoch, TaskQueueClass, ValueClass, + Archive, Archiver, BatchBuilder, BlobLink, BlobOp, IntoOperations, Params, SearchIndex, + TaskEpoch, TaskQueueClass, ValueClass, }, }; use types::{ @@ -499,12 +499,10 @@ fn build_index( IndexValue::Quota { used } => { let value = if set { used as i64 } else { -(used as i64) }; - if let Some(account_id) = batch.last_account_id() { - batch.add(DirectoryClass::UsedQuota(account_id), value); - } + batch.add(ValueClass::Quota, value); if let Some(tenant_id) = tenant_id { - batch.add(DirectoryClass::UsedQuota(tenant_id), value); + batch.add(ValueClass::TenantQuota(tenant_id), value); } } IndexValue::LogItem { @@ -722,12 +720,10 @@ fn merge_index( } (IndexValue::Quota { used: old_used }, IndexValue::Quota { used: new_used }) => { let value = new_used as i64 - old_used as i64; - if let Some(account_id) = batch.last_account_id() { - batch.add(DirectoryClass::UsedQuota(account_id), value); - } + batch.add(ValueClass::Quota, value); if let Some(tenant_id) = tenant_id { - batch.add(DirectoryClass::UsedQuota(tenant_id), value); + batch.add(ValueClass::TenantQuota(tenant_id), value); } } ( diff --git a/crates/common/src/storage/quota.rs b/crates/common/src/storage/quota.rs index 0b9b40a8..b3fcfbc8 100644 --- a/crates/common/src/storage/quota.rs +++ b/crates/common/src/storage/quota.rs @@ -12,7 +12,7 @@ use registry::{ schema::enums::{StorageQuota, TenantStorageQuota}, types::EnumType, }; -use store::write::DirectoryClass; +use store::{ValueKey, write::ValueClass}; use trc::AddContext; impl Server { @@ -20,17 +20,21 @@ impl Server { self.core .storage .data - .get_counter(DirectoryClass::UsedQuota(account_id)) + .get_counter(ValueKey { + account_id, + collection: 0, + document_id: 0, + class: ValueClass::Quota, + }) .await .add_context(|err| err.caused_by(trc::location!()).account_id(account_id)) } pub async fn get_used_quota_tenant(&self, tenant_id: u32) -> trc::Result { - let todo = "use correct counter"; self.core .storage .data - .get_counter(DirectoryClass::UsedQuota(tenant_id)) + .get_counter(ValueKey::from(ValueClass::TenantQuota(tenant_id))) .await .add_context(|err| err.caused_by(trc::location!())) } diff --git a/crates/dav/src/calendar/scheduling.rs b/crates/dav/src/calendar/scheduling.rs index 0779ab73..769e0f35 100644 --- a/crates/dav/src/calendar/scheduling.rs +++ b/crates/dav/src/calendar/scheduling.rs @@ -367,7 +367,11 @@ impl CalendarEventNotificationHandler for Server { let mut response = ScheduleResponse::default(); for (email, attendee) in attendees { - if let Some(account_id) = self.account_id(&email).await.caused_by(trc::location!())? { + if let Some(account_id) = self + .account_id_from_email(&email, false) + .await + .caused_by(trc::location!())? + { let resources = self .fetch_dav_resources( access_token.account_id(), diff --git a/crates/dav/src/common/uri.rs b/crates/dav/src/common/uri.rs index 8f952528..d1c2f988 100644 --- a/crates/dav/src/common/uri.rs +++ b/crates/dav/src/common/uri.rs @@ -91,7 +91,7 @@ impl DavUriResource for Server { .map_err(|_| DavError::Code(error_status))? } else { let account = decode_path_element(account); - self.account_id(&account) + self.account_id_from_email(&account, false) .await .caused_by(trc::location!())? .ok_or(DavError::Code(error_status))? diff --git a/crates/directory/src/core/config.rs b/crates/directory/src/core/config.rs index 8b819a34..3a36fee6 100644 --- a/crates/directory/src/core/config.rs +++ b/crates/directory/src/core/config.rs @@ -39,23 +39,24 @@ impl Directories { } } - let default_directory = match bp.setting_infallible::().await { - Authentication::Internal => Ok(None), - Authentication::Ldap(directory) => LdapDirectory::open(directory).map(Some), - Authentication::Sql(directory) => SqlDirectory::open(directory, &bp.data_store) - .await - .map(Some), - Authentication::Oidc(directory) => OpenIdDirectory::open(directory).map(Some), + let auth = bp.setting_infallible::().await; + let default_directory = if let Some(directory_id) = auth.directory_id { + match directories.get(&(directory_id.id() as u32)) { + Some(default_directory) => default_directory.clone().into(), + None => { + bp.build_error( + Object::Authentication.singleton(), + format!("Default directory with ID {} not found", directory_id), + ); + None + } + } + } else { + None }; Directories { - default_directory: match default_directory { - Ok(default_directory) => default_directory.map(Arc::new), - Err(err) => { - bp.build_error(Object::Authentication.singleton(), err); - None - } - }, + default_directory, directories, } } diff --git a/crates/email/src/message/delivery.rs b/crates/email/src/message/delivery.rs index 2f2d5abd..99b177cf 100644 --- a/crates/email/src/message/delivery.rs +++ b/crates/email/src/message/delivery.rs @@ -118,7 +118,7 @@ impl MailDelivery for Server { }; for rcpt in message.recipients { - let account_id = match self.account_id(&rcpt.address).await { + let account_id = match self.account_id_from_email(&rcpt.address, false).await { Ok(Some(account_id)) => account_id, Ok(None) => { // Something went wrong diff --git a/crates/groupware/src/calendar/itip.rs b/crates/groupware/src/calendar/itip.rs index dec1f86f..ddb0635e 100644 --- a/crates/groupware/src/calendar/itip.rs +++ b/crates/groupware/src/calendar/itip.rs @@ -141,7 +141,7 @@ impl ItipIngest for Server { } // Obtain changedBy - let changed_by = if let Some(id) = self.account_id(sender).await? { + let changed_by = if let Some(id) = self.account_id_from_email(sender, false).await? { ChangedBy::PrincipalId(id) } else { ChangedBy::CalendarAddress(sender.into()) diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index ff8fbfe2..a8af4123 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -275,7 +275,7 @@ impl Session { // Obtain principal id let acl_account_id = data .server - .account_id(arguments.identifier.as_ref().unwrap()) + .account_id_from_email(arguments.identifier.as_ref().unwrap(), false) .await .imap_ctx(&arguments.tag, trc::location!())? .ok_or_else(|| { diff --git a/crates/jmap/src/principal/query.rs b/crates/jmap/src/principal/query.rs index f4fd1e26..360b24f2 100644 --- a/crates/jmap/src/principal/query.rs +++ b/crates/jmap/src/principal/query.rs @@ -63,7 +63,7 @@ impl PrincipalQuery for Server { match cond { Filter::Property(cond) => match cond { PrincipalFilter::Name(name) | PrincipalFilter::Email(name) => { - if let Some(account_id) = self.account_id(&name).await? { + if let Some(account_id) = self.account_id_from_email(&name, false).await? { filters.push(SearchFilter::is_in_set( RoaringBitmap::from_sorted_iter([account_id]).unwrap(), )); diff --git a/crates/migration/src/lib.rs b/crates/migration/src/lib.rs index 47198d11..63f7ceee 100644 --- a/crates/migration/src/lib.rs +++ b/crates/migration/src/lib.rs @@ -16,8 +16,8 @@ use common::{DATABASE_SCHEMA_VERSION, Server, manager::boot::DEFAULT_SETTINGS}; use std::time::Duration; use store::{ - Deserialize, IterateParams, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_REPORT_IN, - SUBSPACE_REPORT_OUT, SUBSPACE_SETTINGS, SerializeInfallible, U32_LEN, Value, ValueKey, + Deserialize, IterateParams, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_REGISTRY, + SUBSPACE_REPORT_IN, SUBSPACE_REPORT_OUT, SerializeInfallible, U32_LEN, Value, ValueKey, dispatch::DocumentSet, roaring::RoaringBitmap, write::{ @@ -207,7 +207,7 @@ pub async fn try_migrate(server: &Server) -> trc::Result<()> { { batch.set( ValueClass::Any(AnyClass { - subspace: SUBSPACE_SETTINGS, + subspace: SUBSPACE_REGISTRY, key: key.as_bytes().to_vec(), }), value.as_bytes().to_vec(), diff --git a/crates/registry/src/types/index.rs b/crates/registry/src/types/index.rs index 7e8e9fff..add1c5ac 100644 --- a/crates/registry/src/types/index.rs +++ b/crates/registry/src/types/index.rs @@ -6,7 +6,7 @@ use crate::{ schema::prelude::{Object, Property}, - types::ipmask::IpAddrOrMask, + types::{id::Id, ipmask::IpAddrOrMask}, }; use ahash::AHashSet; use std::borrow::Cow; @@ -16,7 +16,7 @@ pub enum IndexType { Unique, Search, TextSearch, - GlobalUnique, + Global, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -26,6 +26,7 @@ pub enum IndexValue<'x> { U64(u64), I64(i64), U16(u16), + None, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] @@ -33,6 +34,7 @@ pub struct IndexKey<'x> { pub property: Property, pub typ: IndexType, pub value: IndexValue<'x>, + pub value_composite: IndexValue<'x>, } #[derive(Debug, Default)] @@ -54,6 +56,7 @@ impl<'x> IndexBuilder<'x> { property: Property::Type, typ: IndexType::Search, value: IndexValue::U16(typ), + value_composite: IndexValue::None, }); } @@ -62,6 +65,7 @@ impl<'x> IndexBuilder<'x> { property, typ: IndexType::Unique, value: value.into(), + value_composite: IndexValue::None, }); } @@ -70,6 +74,7 @@ impl<'x> IndexBuilder<'x> { property, typ: IndexType::Search, value: value.into(), + value_composite: IndexValue::None, }); } @@ -86,22 +91,39 @@ impl<'x> IndexBuilder<'x> { property, typ: IndexType::TextSearch, value: IndexValue::Text(Cow::Borrowed(word)), + value_composite: IndexValue::None, }); } else { self.keys.insert(IndexKey { property, typ: IndexType::TextSearch, value: IndexValue::Text(Cow::Owned(word.to_lowercase())), + value_composite: IndexValue::Text(Cow::Borrowed(word)), }); } } } - pub fn global_unique(&mut self, property: Property, value: impl Into>) { + pub fn global(&mut self, property: Property, value: impl Into>) { self.keys.insert(IndexKey { property, - typ: IndexType::GlobalUnique, + typ: IndexType::Global, value: value.into(), + value_composite: IndexValue::None, + }); + } + + pub fn composite( + &mut self, + property: Property, + value: impl Into>, + composite: impl Into>, + ) { + self.keys.insert(IndexKey { + property, + typ: IndexType::Global, + value: value.into(), + value_composite: composite.into(), }); } } @@ -166,3 +188,9 @@ impl<'x> From<&'x String> for IndexValue<'x> { IndexValue::Text(Cow::Borrowed(value.as_str())) } } + +impl<'x> From<&'x Id> for IndexValue<'x> { + fn from(value: &'x Id) -> Self { + IndexValue::U64(value.id()) + } +} diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index feb3a7cf..ba49e2ee 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -236,7 +236,7 @@ impl Session { } // Verify DMARC - let is_report = self.is_report(); + let is_report = !self.is_authenticated() && self.is_report(); let (dmarc_result, dmarc_policy) = match &self.data.spf_mail_from { Some(spf_output) if dmarc.verify() => { let time = Instant::now(); diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index b5fdfee6..a39a0975 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -194,9 +194,13 @@ impl Session { let rcpt = self.data.rcpt_to.last().unwrap(); let mut rcpt_members = None; - match self.server.rcpt_resolve(&rcpt.address_lcase).await { + match self + .server + .rcpt_resolve(&rcpt.address_lcase, self.data.session_id) + .await + { Ok(RcptResolution::Accept) => {} - Ok(RcptResolution::Forward(address) | RcptResolution::Rewrite(address)) => { + Ok(RcptResolution::Rewrite(address)) => { let orig_addr = self.data.rcpt_to.pop().unwrap(); let mut new_addr = SessionAddress::new(address); diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs index 32e23eef..e6442500 100644 --- a/crates/smtp/src/inbound/vrfy.rs +++ b/crates/smtp/src/inbound/vrfy.rs @@ -12,12 +12,13 @@ use trc::SmtpEvent; impl Session { pub async fn handle_vrfy(&mut self, address: Cow<'_, str>) -> Result<(), ()> { if self.params.can_vrfy { - match self.server.rcpt_resolve(&address.to_lowercase()).await { + match self + .server + .rcpt_resolve(&address.to_lowercase(), self.data.session_id) + .await + { Ok( - RcptResolution::Accept - | RcptResolution::Forward(_) - | RcptResolution::Rewrite(_) - | RcptResolution::Expand(_), + RcptResolution::Accept | RcptResolution::Rewrite(_) | RcptResolution::Expand(_), ) => { trc::event!( Smtp(SmtpEvent::Vrfy), @@ -61,7 +62,11 @@ impl Session { pub async fn handle_expn(&mut self, address: Cow<'_, str>) -> Result<(), ()> { if self.params.can_expn { - match self.server.rcpt_resolve(&address.to_lowercase()).await { + match self + .server + .rcpt_resolve(&address.to_lowercase(), self.data.session_id) + .await + { Ok(RcptResolution::Expand(addresses)) => { let mut result = String::with_capacity(32); for (pos, value) in addresses.iter().enumerate() { diff --git a/crates/spam-filter/src/modules/classifier.rs b/crates/spam-filter/src/modules/classifier.rs index e0e09934..2f63d2f3 100644 --- a/crates/spam-filter/src/modules/classifier.rs +++ b/crates/spam-filter/src/modules/classifier.rs @@ -586,8 +586,10 @@ impl SpamClassifier for Server { } for rcpt in &ctx.input.env_rcpt_to { - let prediction = if let Some(account_id) = - self.account_id(rcpt).await.caused_by(trc::location!())? + let prediction = if let Some(account_id) = self + .account_id_from_email(rcpt, true) + .await + .caused_by(trc::location!())? { has_prediction = true; classifier @@ -626,8 +628,10 @@ impl SpamClassifier for Server { } for rcpt in &ctx.input.env_rcpt_to { - let prediction = if let Some(account_id) = - self.account_id(rcpt).await.caused_by(trc::location!())? + let prediction = if let Some(account_id) = self + .account_id_from_email(rcpt, true) + .await + .caused_by(trc::location!())? { has_prediction = true; classifier diff --git a/crates/store/src/backend/foundationdb/write.rs b/crates/store/src/backend/foundationdb/write.rs index 1590e301..18683b2d 100644 --- a/crates/store/src/backend/foundationdb/write.rs +++ b/crates/store/src/backend/foundationdb/write.rs @@ -168,8 +168,7 @@ impl FdbStore { ValueOp::Clear => { if matches!( key[0], - SUBSPACE_DIRECTORY - | SUBSPACE_TASK_QUEUE + SUBSPACE_TASK_QUEUE | SUBSPACE_IN_MEMORY_VALUE | SUBSPACE_PROPERTY | SUBSPACE_QUEUE_MESSAGE diff --git a/crates/store/src/backend/mysql/main.rs b/crates/store/src/backend/mysql/main.rs index b5c63eea..91afa848 100644 --- a/crates/store/src/backend/mysql/main.rs +++ b/crates/store/src/backend/mysql/main.rs @@ -86,13 +86,12 @@ impl MysqlStore { for table in [ SUBSPACE_ACL, - SUBSPACE_DIRECTORY, SUBSPACE_TASK_QUEUE, SUBSPACE_BLOB_EXTRA, SUBSPACE_BLOB_LINK, SUBSPACE_IN_MEMORY_VALUE, SUBSPACE_PROPERTY, - SUBSPACE_SETTINGS, + SUBSPACE_REGISTRY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUEUE_EVENT, SUBSPACE_REPORT_OUT, diff --git a/crates/store/src/backend/postgres/main.rs b/crates/store/src/backend/postgres/main.rs index be055408..5f8292a6 100644 --- a/crates/store/src/backend/postgres/main.rs +++ b/crates/store/src/backend/postgres/main.rs @@ -88,13 +88,12 @@ impl PostgresStore { for table in [ SUBSPACE_ACL, - SUBSPACE_DIRECTORY, SUBSPACE_TASK_QUEUE, SUBSPACE_BLOB_EXTRA, SUBSPACE_BLOB_LINK, SUBSPACE_IN_MEMORY_VALUE, SUBSPACE_PROPERTY, - SUBSPACE_SETTINGS, + SUBSPACE_REGISTRY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUEUE_EVENT, SUBSPACE_REPORT_OUT, diff --git a/crates/store/src/backend/rocksdb/main.rs b/crates/store/src/backend/rocksdb/main.rs index d2b246f7..78d7feae 100644 --- a/crates/store/src/backend/rocksdb/main.rs +++ b/crates/store/src/backend/rocksdb/main.rs @@ -45,13 +45,12 @@ impl RocksDbStore { for subspace in [ SUBSPACE_INDEXES, SUBSPACE_ACL, - SUBSPACE_DIRECTORY, SUBSPACE_TASK_QUEUE, SUBSPACE_BLOB_EXTRA, SUBSPACE_BLOB_LINK, SUBSPACE_IN_MEMORY_VALUE, SUBSPACE_PROPERTY, - SUBSPACE_SETTINGS, + SUBSPACE_REGISTRY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUEUE_EVENT, SUBSPACE_REPORT_OUT, diff --git a/crates/store/src/backend/sqlite/main.rs b/crates/store/src/backend/sqlite/main.rs index 58e41619..5f6fe4de 100644 --- a/crates/store/src/backend/sqlite/main.rs +++ b/crates/store/src/backend/sqlite/main.rs @@ -63,13 +63,12 @@ impl SqliteStore { for table in [ SUBSPACE_ACL, - SUBSPACE_DIRECTORY, SUBSPACE_TASK_QUEUE, SUBSPACE_BLOB_EXTRA, SUBSPACE_BLOB_LINK, SUBSPACE_IN_MEMORY_VALUE, SUBSPACE_PROPERTY, - SUBSPACE_SETTINGS, + SUBSPACE_REGISTRY, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUEUE_EVENT, SUBSPACE_REPORT_OUT, diff --git a/crates/store/src/dispatch/registry.rs b/crates/store/src/dispatch/registry.rs index 1aa8d901..882e085b 100644 --- a/crates/store/src/dispatch/registry.rs +++ b/crates/store/src/dispatch/registry.rs @@ -11,16 +11,12 @@ use registry::{ }; impl RegistryStore { - pub async fn id(&self, id: Id) -> trc::Result> { + pub async fn object(&self, id: impl Into) -> trc::Result> { todo!() } - pub async fn object(&self, id: impl Into) -> trc::Result> { - self.id(Id::new(T::object(), id.into())).await - } - pub async fn singleton(&self) -> trc::Result> { - self.id(T::object().singleton()).await + todo!() } pub async fn insert(&self, object: &T) -> trc::Result { diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index ed1c0d2f..5e07bcc4 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -87,7 +87,6 @@ pub const U32_LEN: usize = std::mem::size_of::(); pub const U16_LEN: usize = std::mem::size_of::(); pub const SUBSPACE_ACL: u8 = b'a'; -pub const SUBSPACE_DIRECTORY: u8 = b'd'; pub const SUBSPACE_TASK_QUEUE: u8 = b'f'; pub const SUBSPACE_INDEXES: u8 = b'i'; pub const SUBSPACE_BLOB_EXTRA: u8 = b'j'; @@ -98,7 +97,7 @@ pub const SUBSPACE_COUNTER: u8 = b'n'; pub const SUBSPACE_IN_MEMORY_VALUE: u8 = b'm'; pub const SUBSPACE_IN_MEMORY_COUNTER: u8 = b'y'; pub const SUBSPACE_PROPERTY: u8 = b'p'; -pub const SUBSPACE_SETTINGS: u8 = b's'; +pub const SUBSPACE_REGISTRY: u8 = b's'; pub const SUBSPACE_QUEUE_MESSAGE: u8 = b'e'; pub const SUBSPACE_QUEUE_EVENT: u8 = b'q'; pub const SUBSPACE_QUOTA: u8 = b'u'; @@ -114,6 +113,7 @@ pub const LEGACY_SUBSPACE_BITMAP_TAG: u8 = b'c'; pub const LEGACY_SUBSPACE_BITMAP_TEXT: u8 = b'v'; pub const LEGACY_SUBSPACE_FTS_INDEX: u8 = b'g'; pub const LEGACY_SUBSPACE_TELEMETRY_INDEX: u8 = b'w'; +pub const LEGACY_SUBSPACE_DIRECTORY: u8 = b'd'; #[derive(Clone)] pub struct IterateParams { diff --git a/crates/store/src/registry/bootstrap.rs b/crates/store/src/registry/bootstrap.rs index 958be310..10111a7b 100644 --- a/crates/store/src/registry/bootstrap.rs +++ b/crates/store/src/registry/bootstrap.rs @@ -43,7 +43,7 @@ impl Bootstrap { pub async fn setting(&mut self) -> trc::Result { let object_id = T::object().singleton(); - if let Some(setting) = self.registry.id::(object_id).await? { + if let Some(setting) = self.registry.object::(object_id.id()).await? { let mut errors = Vec::new(); if setting.validate(&mut errors) { return Ok(setting); @@ -70,43 +70,37 @@ impl Bootstrap { } } - pub async fn get_infallible(&mut self, id: Id) -> Option { - if id.object() != T::object() { - match self.registry.id::(id).await { - Ok(Some(setting)) => { - let mut errors = Vec::new(); - if setting.validate(&mut errors) { - Some(setting) - } else { - self.errors.push(Error::Validation { - object_id: id, - errors, - }); - None - } - } - Ok(None) => { - self.errors.push(Error::NotFound { object_id: id }); - None - } - Err(err) => { - if !self.has_fatal_errors { - self.errors.push(Error::Internal { - object_id: Some(id), - error: err, - }); - self.has_fatal_errors = true; - } + pub async fn get_infallible(&mut self, id: impl Into) -> Option { + let id = id.into(); + match self.registry.object::(id).await { + Ok(Some(setting)) => { + let mut errors = Vec::new(); + if setting.validate(&mut errors) { + Some(setting) + } else { + self.errors.push(Error::Validation { + object_id: Id::new(T::object(), id), + errors, + }); None } } - } else { - self.errors.push(Error::TypeMismatch { - object_id: id, - object_type: id.object(), - expected_type: T::object(), - }); - None + Ok(None) => { + self.errors.push(Error::NotFound { + object_id: Id::new(T::object(), id), + }); + None + } + Err(err) => { + if !self.has_fatal_errors { + self.errors.push(Error::Internal { + object_id: Some(Id::new(T::object(), id)), + error: err, + }); + self.has_fatal_errors = true; + } + None + } } } diff --git a/crates/store/src/write/key.rs b/crates/store/src/write/key.rs index de53fd6a..1c380318 100644 --- a/crates/store/src/write/key.rs +++ b/crates/store/src/write/key.rs @@ -5,21 +5,26 @@ */ use super::{ - AnyKey, BlobOp, DirectoryClass, InMemoryClass, QueueClass, ReportClass, ReportEvent, - TaskQueueClass, TelemetryClass, ValueClass, + AnyKey, BlobOp, InMemoryClass, QueueClass, ReportClass, ReportEvent, TaskQueueClass, + TelemetryClass, ValueClass, }; use crate::{ Deserialize, IndexKey, IndexKeyPrefix, Key, LogKey, SUBSPACE_ACL, SUBSPACE_BLOB_EXTRA, - SUBSPACE_BLOB_LINK, SUBSPACE_COUNTER, SUBSPACE_DIRECTORY, SUBSPACE_IN_MEMORY_COUNTER, - SUBSPACE_IN_MEMORY_VALUE, SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_PROPERTY, - SUBSPACE_QUEUE_EVENT, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUOTA, SUBSPACE_REPORT_IN, - SUBSPACE_REPORT_OUT, SUBSPACE_SEARCH_INDEX, SUBSPACE_SETTINGS, SUBSPACE_TASK_QUEUE, - SUBSPACE_TELEMETRY_METRIC, SUBSPACE_TELEMETRY_SPAN, U16_LEN, U32_LEN, U64_LEN, ValueKey, - WITH_SUBSPACE, - write::{BlobLink, IndexPropertyClass, SearchIndex, SearchIndexId, SearchIndexType}, + SUBSPACE_BLOB_LINK, SUBSPACE_COUNTER, SUBSPACE_IN_MEMORY_COUNTER, SUBSPACE_IN_MEMORY_VALUE, + SUBSPACE_INDEXES, SUBSPACE_LOGS, SUBSPACE_PROPERTY, SUBSPACE_QUEUE_EVENT, + SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUOTA, SUBSPACE_REGISTRY, SUBSPACE_REPORT_IN, + SUBSPACE_REPORT_OUT, SUBSPACE_SEARCH_INDEX, SUBSPACE_TASK_QUEUE, SUBSPACE_TELEMETRY_METRIC, + SUBSPACE_TELEMETRY_SPAN, U16_LEN, U32_LEN, U64_LEN, ValueKey, WITH_SUBSPACE, + write::{ + BlobLink, IndexPropertyClass, RegistryClass, SearchIndex, SearchIndexId, SearchIndexType, + }, }; use std::convert::TryInto; -use types::{blob_hash::BLOB_HASH_LEN, collection::SyncCollection, field::Field}; +use types::{ + blob_hash::BLOB_HASH_LEN, + collection::{Collection, SyncCollection}, + field::{Field, MailboxField}, +}; use utils::codec::leb128::Leb128_; pub struct KeySerializer { @@ -160,10 +165,6 @@ impl> ValueKey { ..self } } - - pub fn is_counter(&self) -> bool { - self.class.as_ref().is_counter(self.collection) - } } impl ValueKey { @@ -368,32 +369,13 @@ impl ValueClass { .write(account_id) .write::<&[u8]>(hash.as_ref()), }, - ValueClass::Config(key) => serializer.write(key.as_slice()), ValueClass::InMemory(lookup) => match lookup { InMemoryClass::Key(key) => serializer.write(key.as_slice()), InMemoryClass::Counter(key) => serializer.write(key.as_slice()), }, - ValueClass::Directory(directory) => match directory { - DirectoryClass::NameToId(name) => serializer.write(0u8).write(name.as_slice()), - DirectoryClass::EmailToId(email) => serializer.write(1u8).write(email.as_slice()), - DirectoryClass::Principal(uid) => serializer.write(2u8).write_leb128(*uid), - DirectoryClass::UsedQuota(uid) => serializer.write(4u8).write_leb128(*uid), - DirectoryClass::MemberOf { - principal_id, - member_of, - } => serializer.write(5u8).write(*principal_id).write(*member_of), - DirectoryClass::Members { - principal_id, - has_member, - } => serializer - .write(6u8) - .write(*principal_id) - .write(*has_member), - DirectoryClass::Index { word, principal_id } => serializer - .write(7u8) - .write(word.as_slice()) - .write(*principal_id), - }, + ValueClass::Registry(registry) => { + todo!() + } ValueClass::Queue(queue) => match queue { QueueClass::Message(queue_id) => serializer.write(*queue_id), QueueClass::MessageEvent(event) => serializer @@ -453,6 +435,8 @@ impl ValueClass { }, ValueClass::DocumentId => serializer.write(account_id).write(collection), ValueClass::ChangeId => serializer.write(account_id), + ValueClass::Quota => serializer.write(account_id).write(u8::MAX), + ValueClass::TenantQuota(tenant_id) => serializer.write(*tenant_id).write(u8::MAX - 1), ValueClass::ShareNotification { notification_id, notify_account_id, @@ -577,14 +561,11 @@ impl ValueClass { IndexPropertyClass::Integer { .. } => U32_LEN * 2 + 3 + U64_LEN, }, ValueClass::Acl(_) => U32_LEN * 3 + 2, - ValueClass::InMemory(InMemoryClass::Counter(v) | InMemoryClass::Key(v)) - | ValueClass::Config(v) => v.len(), - ValueClass::Directory(d) => match d { - DirectoryClass::NameToId(v) | DirectoryClass::EmailToId(v) => v.len(), - DirectoryClass::Principal(_) | DirectoryClass::UsedQuota(_) => U32_LEN, - DirectoryClass::Members { .. } | DirectoryClass::MemberOf { .. } => U32_LEN * 2, - DirectoryClass::Index { word, .. } => word.len() + U32_LEN, - }, + ValueClass::InMemory(InMemoryClass::Counter(v) | InMemoryClass::Key(v)) => v.len(), + ValueClass::Registry(registry) => { + let todo = "implement"; + todo!() + } ValueClass::Blob(op) => match op { BlobOp::Commit { .. } => BLOB_HASH_LEN, BlobOp::Link { to, .. } => { @@ -629,7 +610,7 @@ impl ValueClass { TelemetryClass::Span { .. } => U64_LEN + 1, TelemetryClass::Metric { .. } => U64_LEN * 2 + 1, }, - ValueClass::DocumentId => U32_LEN + 1, + ValueClass::DocumentId | ValueClass::Quota | ValueClass::TenantQuota(_) => U32_LEN + 1, ValueClass::ChangeId => U32_LEN, ValueClass::ShareNotification { .. } => U32_LEN + U64_LEN + 1, ValueClass::SearchIndex(v) => match &v.typ { @@ -645,9 +626,12 @@ impl ValueClass { } pub fn subspace(&self, collection: u8) -> u8 { + const MAILBOX_COLLECTION: u8 = Collection::Mailbox as u8; + const MAILBOX_COUNTER_FIELD: u8 = MailboxField::UidCounter as u8; + match self { ValueClass::Property(field) => { - if *field == 84 && collection == 1 { + if (collection == MAILBOX_COLLECTION && *field == MAILBOX_COUNTER_FIELD) { SUBSPACE_COUNTER } else { SUBSPACE_PROPERTY @@ -662,15 +646,11 @@ impl ValueClass { SUBSPACE_BLOB_EXTRA } }, - ValueClass::Config(_) => SUBSPACE_SETTINGS, + ValueClass::Registry(_) => SUBSPACE_REGISTRY, ValueClass::InMemory(lookup) => match lookup { InMemoryClass::Key(_) => SUBSPACE_IN_MEMORY_VALUE, InMemoryClass::Counter(_) => SUBSPACE_IN_MEMORY_COUNTER, }, - ValueClass::Directory(directory) => match directory { - DirectoryClass::UsedQuota(_) => SUBSPACE_QUOTA, - _ => SUBSPACE_DIRECTORY, - }, ValueClass::Queue(queue) => match queue { QueueClass::Message(_) => SUBSPACE_QUEUE_MESSAGE, QueueClass::MessageEvent(_) => SUBSPACE_QUEUE_EVENT, @@ -685,24 +665,15 @@ impl ValueClass { TelemetryClass::Span { .. } => SUBSPACE_TELEMETRY_SPAN, TelemetryClass::Metric { .. } => SUBSPACE_TELEMETRY_METRIC, }, - ValueClass::DocumentId | ValueClass::ChangeId => SUBSPACE_COUNTER, + ValueClass::DocumentId + | ValueClass::ChangeId + | ValueClass::Quota + | ValueClass::TenantQuota(_) => SUBSPACE_COUNTER, ValueClass::ShareNotification { .. } => SUBSPACE_LOGS, ValueClass::SearchIndex(_) => SUBSPACE_SEARCH_INDEX, ValueClass::Any(any) => any.subspace, } } - - pub fn is_counter(&self, collection: u8) -> bool { - match self { - ValueClass::Directory(DirectoryClass::UsedQuota(_)) - | ValueClass::InMemory(InMemoryClass::Counter(_)) - | ValueClass::Queue(QueueClass::QuotaCount(_) | QueueClass::QuotaSize(_)) - | ValueClass::DocumentId - | ValueClass::ChangeId => true, - ValueClass::Property(84) if collection == 1 => true, // TODO: Find a more elegant way to do this - _ => false, - } - } } impl From for ValueKey { @@ -716,20 +687,20 @@ impl From for ValueKey { } } -impl From for ValueKey { - fn from(value: DirectoryClass) -> Self { +impl From for ValueKey { + fn from(value: RegistryClass) -> Self { ValueKey { account_id: 0, collection: 0, document_id: 0, - class: ValueClass::Directory(value), + class: ValueClass::Registry(value), } } } -impl From for ValueClass { - fn from(value: DirectoryClass) -> Self { - ValueClass::Directory(value) +impl From for ValueClass { + fn from(value: RegistryClass) -> Self { + ValueClass::Registry(value) } } diff --git a/crates/store/src/write/mod.rs b/crates/store/src/write/mod.rs index 24144def..c4915843 100644 --- a/crates/store/src/write/mod.rs +++ b/crates/store/src/write/mod.rs @@ -169,9 +169,8 @@ pub enum ValueClass { Acl(u32), InMemory(InMemoryClass), TaskQueue(TaskQueueClass), - Directory(DirectoryClass), Blob(BlobOp), - Config(Vec), + Registry(RegistryClass), Queue(QueueClass), Report(ReportClass), Telemetry(TelemetryClass), @@ -183,6 +182,8 @@ pub enum ValueClass { }, DocumentId, ChangeId, + Quota, + TenantQuota(u32), } #[derive(Debug, PartialEq, Clone, Eq, Hash)] @@ -267,17 +268,6 @@ pub enum InMemoryClass { Counter(Vec), } -#[derive(Debug, PartialEq, Clone, Eq, Hash)] -pub enum DirectoryClass { - NameToId(Vec), - EmailToId(Vec), - Index { word: Vec, principal_id: u32 }, - MemberOf { principal_id: u32, member_of: u32 }, - Members { principal_id: u32, has_member: u32 }, - Principal(u32), - UsedQuota(u32), -} - #[derive(Debug, PartialEq, Clone, Eq, Hash)] pub enum RegistryClass { Item(u64), diff --git a/crates/types/src/field.rs b/crates/types/src/field.rs index 3d22eb11..4b475ae0 100644 --- a/crates/types/src/field.rs +++ b/crates/types/src/field.rs @@ -47,8 +47,8 @@ pub enum EmailField { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum MailboxField { - UidCounter, - Archive, + UidCounter = 84, + Archive = ARCHIVE_FIELD, } #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -76,13 +76,13 @@ pub enum IdentityField { #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(u8)] pub enum PrincipalField { - Archive, - EncryptionKeys, - ParticipantIdentities, - DefaultCalendarId, - DefaultAddressBookId, - ActiveScriptId, - PushSubscriptions, + Archive = ARCHIVE_FIELD, + EncryptionKeys = 46, + ParticipantIdentities = 45, + DefaultCalendarId = 47, + DefaultAddressBookId = 48, + ActiveScriptId = 49, + PushSubscriptions = 44, } impl From for u8 { diff --git a/tests/src/store/cleanup.rs b/tests/src/store/cleanup.rs index 13cd221e..323b23b9 100644 --- a/tests/src/store/cleanup.rs +++ b/tests/src/store/cleanup.rs @@ -17,7 +17,6 @@ pub async fn store_destroy(store: &Store) { for subspace in [ SUBSPACE_ACL, - SUBSPACE_DIRECTORY, SUBSPACE_TASK_QUEUE, SUBSPACE_INDEXES, SUBSPACE_BLOB_EXTRA, @@ -27,7 +26,7 @@ pub async fn store_destroy(store: &Store) { SUBSPACE_IN_MEMORY_VALUE, SUBSPACE_COUNTER, SUBSPACE_PROPERTY, - SUBSPACE_SETTINGS, + SUBSPACE_REGISTRY, SUBSPACE_BLOBS, SUBSPACE_QUEUE_MESSAGE, SUBSPACE_QUEUE_EVENT, @@ -250,12 +249,11 @@ pub async fn store_assert_is_empty(store: &Store, blob_store: BlobStore, include for (subspace, with_values) in [ (SUBSPACE_ACL, true), - (SUBSPACE_DIRECTORY, true), (SUBSPACE_TASK_QUEUE, true), (SUBSPACE_IN_MEMORY_VALUE, true), (SUBSPACE_IN_MEMORY_COUNTER, false), (SUBSPACE_PROPERTY, true), - (SUBSPACE_SETTINGS, true), + (SUBSPACE_REGISTRY, true), (SUBSPACE_QUEUE_MESSAGE, true), (SUBSPACE_QUEUE_EVENT, true), (SUBSPACE_REPORT_OUT, true), @@ -271,7 +269,7 @@ pub async fn store_assert_is_empty(store: &Store, blob_store: BlobStore, include (SUBSPACE_SEARCH_INDEX, true), ] { if (subspace == SUBSPACE_SEARCH_INDEX && store.is_pg_or_mysql()) - || (subspace == SUBSPACE_DIRECTORY && !include_directory) + //|| (subspace == directory && !include_directory) { continue; } diff --git a/tests/src/store/import_export.rs b/tests/src/store/import_export.rs index ed0a696d..b9c5e876 100644 --- a/tests/src/store/import_export.rs +++ b/tests/src/store/import_export.rs @@ -254,7 +254,6 @@ impl Snapshot { for (subspace, with_values) in [ (SUBSPACE_ACL, true), - (SUBSPACE_DIRECTORY, true), (SUBSPACE_TASK_QUEUE, true), (SUBSPACE_INDEXES, false), (SUBSPACE_BLOB_EXTRA, true), @@ -265,7 +264,7 @@ impl Snapshot { (SUBSPACE_IN_MEMORY_COUNTER, !is_sql), (SUBSPACE_IN_MEMORY_VALUE, true), (SUBSPACE_PROPERTY, true), - (SUBSPACE_SETTINGS, true), + (SUBSPACE_REGISTRY, true), (SUBSPACE_QUEUE_MESSAGE, true), (SUBSPACE_QUEUE_EVENT, true), (SUBSPACE_QUOTA, !is_sql),