From f3dfb7cb33f8faeb8247159b4ad129302b45d6d7 Mon Sep 17 00:00:00 2001 From: Maurus Decimus <11444311+mdecimus@users.noreply.github.com> Date: Mon, 13 Apr 2026 19:51:45 +0200 Subject: [PATCH] Fixed registry JMAP helpers --- Cargo.lock | 75 ++++++++++++++- crates/common/Cargo.toml | 2 +- crates/common/src/auth/access_token.rs | 15 ++- crates/common/src/auth/mod.rs | 4 +- crates/common/src/auth/permissions.rs | 36 ++++--- crates/common/src/cache/principals.rs | 8 +- crates/common/src/config/mailstore/email.rs | 5 +- crates/common/src/manager/boot.rs | 9 +- crates/common/src/manager/defaults.rs | 94 ++++--------------- crates/http/Cargo.toml | 2 +- crates/http/src/auth/permissions.rs | 71 +++++++++++++- crates/jmap/Cargo.toml | 2 +- crates/jmap/src/api/query.rs | 4 +- crates/jmap/src/registry/mapping/action.rs | 25 ++--- crates/jmap/src/registry/mapping/log.rs | 28 +++--- .../src/registry/mapping/queued_message.rs | 61 +++++++++--- crates/jmap/src/registry/mapping/task.rs | 62 ++++++++++-- crates/jmap/src/registry/mapping/telemetry.rs | 86 ++++++++++++++++- crates/main/src/test_data.rs | 4 +- crates/migration/Cargo.toml | 2 +- crates/registry/Cargo.toml | 2 +- crates/registry/src/types/id.rs | 5 +- crates/smtp/Cargo.toml | 2 +- crates/smtp/src/queue/spool.rs | 22 +++++ crates/spam-filter/Cargo.toml | 2 +- crates/store/src/registry/local.rs | 12 ++- crates/trc/Cargo.toml | 2 +- crates/utils/Cargo.toml | 4 +- resources/config/config.toml | 73 -------------- tests/Cargo.toml | 4 +- tests/src/smtp/management/queue.rs | 21 ++--- tests/src/utils/server.rs | 25 ++++- 32 files changed, 487 insertions(+), 282 deletions(-) delete mode 100644 resources/config/config.toml diff --git a/Cargo.lock b/Cargo.lock index 0b2ee0e3..41e90fdc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3622,6 +3622,8 @@ dependencies = [ [[package]] name = "jmap-client" version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3357aac1ef357587e09f44262f351f7773120fb695e281b34628ef9d84028038" dependencies = [ "ahash", "async-stream", @@ -3687,6 +3689,36 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link 0.2.1", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + [[package]] name = "jni-sys" version = "0.3.1" @@ -4075,6 +4107,8 @@ dependencies = [ [[package]] name = "mail-auth" version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f026538d37471da028cd5c6a907777c3de2d84e2c71b2b47ae301a10977c5e6" dependencies = [ "aws-lc-rs", "flate2", @@ -5389,7 +5423,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "27c6023962132f4b30eb4c172c91ce92d933da334c59c23cddee82358ddafb0b" dependencies = [ "anyhow", - "itertools 0.13.0", + "itertools 0.14.0", "proc-macro2", "quote", "syn 2.0.117", @@ -5547,7 +5581,7 @@ dependencies = [ "rustc-hash", "rustls 0.23.37", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.6.2", "slab", "thiserror 2.0.18", "tinyvec", @@ -6073,7 +6107,7 @@ dependencies = [ "quinn", "rustls 0.23.37", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.6.2", "serde", "serde_urlencoded", "sync_wrapper", @@ -6417,7 +6451,28 @@ checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" dependencies = [ "core-foundation", "core-foundation-sys", - "jni", + "jni 0.21.1", + "log", + "once_cell", + "rustls 0.23.37", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki 0.103.10", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni 0.22.4", "log", "once_cell", "rustls 0.23.37", @@ -7080,6 +7135,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -8378,7 +8443,7 @@ dependencies = [ "rustls 0.23.37", "rustls-pemfile", "rustls-pki-types", - "rustls-platform-verifier", + "rustls-platform-verifier 0.7.0", "serde", "serde_json", "smtp-proto", diff --git a/crates/common/Cargo.toml b/crates/common/Cargo.toml index e5140730..67f4c67d 100644 --- a/crates/common/Cargo.toml +++ b/crates/common/Cargo.toml @@ -18,7 +18,7 @@ imap_proto = { path = "../imap-proto" } sieve-rs = { version = "0.7", features = ["rkyv", "serde"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } -mail-auth = { path = "/Users/me/code/mail-auth" } +mail-auth = { version = "0.8" } smtp-proto = { version = "0.2", features = ["rkyv"] } dns-update = { path = "/Users/me/code/dns-update" } calcard = { version = "0.3", features = ["rkyv"] } diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index dd1f31b2..ccd7081e 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -8,8 +8,8 @@ use super::AccessToken; use crate::{ Server, auth::{ - AccessScope, AccessTo, AccessTokenInner, AccountTenantIds, FALLBACK_ADMIN_ID, Permissions, - permissions::{BuildPermissions, build_permissions_list}, + AccessScope, AccessTo, AccessTokenInner, AccountTenantIds, RECOVERY_ADMIN_ID, Permissions, + permissions::{BuildPermissions, PermissionsListBuilder}, }, network::limiter::{ConcurrencyLimiter, LimiterResult}, }; @@ -286,7 +286,7 @@ impl Server { self.build_access_token(account, account_id, revision, revision_account) .await? .into() - } else if account_id == FALLBACK_ADMIN_ID { + } else if account_id == RECOVERY_ADMIN_ID { AccessTokenInner::new_admin().into() } else { return Err(trc::SecurityEvent::Unauthorized @@ -627,12 +627,17 @@ impl AccessToken { pub fn permissions(&self) -> Vec { if let Some(scope) = self.inner.scopes.get(self.scope_idx) { - build_permissions_list(&scope.permissions) + scope.permissions.build_permissions_list() } else { vec![] } } + #[inline(always)] + pub fn access_scope(&self) -> Option<&AccessScope> { + self.inner.scopes.get(self.scope_idx) + } + pub(crate) fn permissions_bits(&self) -> &Permissions { &self .inner @@ -773,7 +778,7 @@ impl AccessTokenInner { pub fn new_admin() -> Self { AccessTokenInner { - account_id: FALLBACK_ADMIN_ID, + account_id: RECOVERY_ADMIN_ID, tenant_id: Default::default(), member_of: Default::default(), access_to: Default::default(), diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index 5fce1889..0929c1b9 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -32,7 +32,7 @@ pub mod oauth; pub mod permissions; pub mod rate_limit; -pub const FALLBACK_ADMIN_ID: u32 = u32::MAX; +pub const RECOVERY_ADMIN_ID: u32 = u32::MAX; const PERMISSIONS_BITSET_SIZE: usize = Permission::COUNT.div_ceil(std::mem::size_of::()); pub type Permissions = Bitset; @@ -141,7 +141,7 @@ pub struct AccessTokenInner { } #[derive(Debug, Default, Hash, Clone)] -pub(crate) struct AccessScope { +pub struct AccessScope { pub permissions: Permissions, pub credential_id: u32, pub expires_at: u64, diff --git a/crates/common/src/auth/permissions.rs b/crates/common/src/auth/permissions.rs index e5c8376b..f6a623a7 100644 --- a/crates/common/src/auth/permissions.rs +++ b/crates/common/src/auth/permissions.rs @@ -154,30 +154,36 @@ impl AccessToken { if requested_permissions.is_empty() { Ok(()) } else { - Err(build_permissions_list(&requested_permissions)) + Err(requested_permissions.build_permissions_list()) } } } -pub(crate) fn build_permissions_list(permissions_in: &Permissions) -> Vec { - const USIZE_BITS: usize = std::mem::size_of::() * 8; - const USIZE_MASK: u32 = USIZE_BITS as u32 - 1; - let mut permissions = Vec::new(); +pub trait PermissionsListBuilder { + fn build_permissions_list(&self) -> Vec; +} - for (block_num, bytes) in permissions_in.inner().iter().enumerate() { - let mut bytes = *bytes; +impl PermissionsListBuilder for Permissions { + fn build_permissions_list(&self) -> Vec { + const USIZE_BITS: usize = std::mem::size_of::() * 8; + const USIZE_MASK: u32 = USIZE_BITS as u32 - 1; + let mut permissions = Vec::new(); - 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 u16) - { - permissions.push(permission); + for (block_num, bytes) in self.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 u16) + { + permissions.push(permission); + } } } + permissions } - permissions } pub struct DefaultPermissions { diff --git a/crates/common/src/cache/principals.rs b/crates/common/src/cache/principals.rs index 2e4839f8..2c9eb5bd 100644 --- a/crates/common/src/cache/principals.rs +++ b/crates/common/src/cache/principals.rs @@ -11,8 +11,8 @@ use crate::{ ACCOUNT_FLAG_ENCRYPT_APPEND, ACCOUNT_FLAG_ENCRYPT_METHOD_PGP, ACCOUNT_FLAG_ENCRYPT_METHOD_SMIME, ACCOUNT_FLAG_ENCRYPT_TRAIN_SPAM_FILTER, ACCOUNT_IS_USER, AccountCache, AccountInfo, AccountTenantIds, DOMAIN_FLAG_RELAY, DOMAIN_FLAG_SUB_ADDRESSING, - DomainCache, EmailAddress, EmailAddressRef, EmailCache, FALLBACK_ADMIN_ID, - MailingListCache, PermissionsGroup, RoleCache, TenantCache, permissions::BuildPermissions, + DomainCache, EmailAddress, EmailAddressRef, EmailCache, MailingListCache, PermissionsGroup, + RECOVERY_ADMIN_ID, RoleCache, TenantCache, permissions::BuildPermissions, }, config::smtp::auth::DkimSigner, expr::if_block::BootstrapExprExt, @@ -281,7 +281,7 @@ impl Server { pub async fn account(&self, account_id: u32) -> trc::Result> { if let Some(account) = self.try_account(account_id).await? { Ok(account) - } else if account_id == FALLBACK_ADMIN_ID { + } else if account_id == RECOVERY_ADMIN_ID { Ok(Arc::new(AccountCache { name: self .registry() @@ -289,7 +289,7 @@ impl Server { .map(|(name, _)| name.as_str()) .unwrap_or("recovery-admin") .into(), - id: FALLBACK_ADMIN_ID, + id: RECOVERY_ADMIN_ID, addresses: Default::default(), id_tenant: Default::default(), id_member_of: Default::default(), diff --git a/crates/common/src/config/mailstore/email.rs b/crates/common/src/config/mailstore/email.rs index 644d33b6..5f3dae44 100644 --- a/crates/common/src/config/mailstore/email.rs +++ b/crates/common/src/config/mailstore/email.rs @@ -89,8 +89,9 @@ impl EmailConfig { let auth = bp.setting_infallible::().await; // Obtain default domain name - let default_domain_name = if let Some(default_domain) = - bp.get_infallible::(system.default_domain_id).await + let default_domain_name = if system.default_domain_id.is_valid() + && let Some(default_domain) = + bp.get_infallible::(system.default_domain_id).await { default_domain.name } else { diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index 57b65684..b86bce15 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -136,6 +136,12 @@ impl BootManager { .await .failed("⚠️ Startup failed"); let mut bootstrap = Bootstrap::new(registry).await; + + if matches!(import_export, StoreOp::None) { + // Add safe defaults if missing + bootstrap.insert_safe_defaults().await; + } + let todo = "implement recovery mode, check env_recovery_mode in RegistryStoreInner"; // Start listeners @@ -150,9 +156,6 @@ impl BootManager { match import_export { StoreOp::None => { - // Add safe defaults if missing - bootstrap.insert_safe_defaults().await; - // Parse components let core = Box::pin(Core::parse(&mut bootstrap, storage)).await; let data = Data::parse(&mut bootstrap).await; diff --git a/crates/common/src/manager/defaults.rs b/crates/common/src/manager/defaults.rs index 94b7de09..5f8336c5 100644 --- a/crates/common/src/manager/defaults.rs +++ b/crates/common/src/manager/defaults.rs @@ -24,6 +24,7 @@ use store::{ bootstrap::Bootstrap, write::{RegistryWrite, RegistryWriteResult}, }, + write::BatchBuilder, }; use types::id::Id; @@ -381,88 +382,12 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> { } } - let mut default_domain_id = None; - if bp.registry.count_object(ObjectType::Domain).await? == 0 { - match bp - .registry - .write(RegistryWrite::insert( - &Domain { - name: psl::domain_str(bp.registry.local_hostname()) - .unwrap_or("localhost.localdomain") - .to_string(), - is_enabled: true, - ..Default::default() - } - .into(), - )) - .await? - { - RegistryWriteResult::Success(id) => { - default_domain_id = Some(id); - } - err => { - bp.build_error( - ObjectType::Domain.singleton(), - format!("Failed to insert default domain: {err}"), - ); - } - } - - let todo = "review"; - //#[cfg(not(feature = "test_mode"))] - /*if let Some(domain_id) = default_domain_id { - let now = store::write::now(); - let signature_rsa = DkimSignature::Dkim1RsaSha256(Dkim1Signature { - domain_id, - stage: DkimSignatureStage::Testing, - selector: format!("rsa-{now}"), - private_key: DkimPrivateKey::Value(SecretTextValue { - secret: crate::network::dkim::generate_dkim_private_key( - DkimSignatureType::Dkim1RsaSha256, - ) - .await? - .map_err(|err| { - trc::EventType::Dkim(trc::DkimEvent::BuildError) - .into_err() - .reason(err) - .caused_by(trc::location!()) - })?, - }), - ..Default::default() - }); - let signature_ed = DkimSignature::Dkim1Ed25519Sha256(Dkim1Signature { - domain_id, - enabled: true, - selector: format!("ed-{now}"), - private_key: DkimPrivateKey::Value(SecretTextValue { - secret: crate::network::dkim::generate_dkim_private_key( - DkimSignatureType::Dkim1Ed25519Sha256, - ) - .await? - .map_err(|err| { - trc::EventType::Dkim(trc::DkimEvent::BuildError) - .into_err() - .reason(err) - .caused_by(trc::location!()) - })?, - }), - ..Default::default() - }); - - for signature in [signature_rsa, signature_ed] { - bp.registry - .write(RegistryWrite::insert(&signature.into())) - .await?; - } - }*/ - } - + #[cfg(not(any(feature = "dev_mode", feature = "test_mode")))] if bp.registry.count_object(ObjectType::SystemSettings).await? == 0 { bp.registry .write(RegistryWrite::insert( &SystemSettings { default_hostname: bp.registry.local_hostname().to_string(), - default_domain_id: default_domain_id.unwrap_or(Id::new(0)), ..Default::default() } .into(), @@ -548,5 +473,20 @@ async fn insert_safe_defaults(bp: &mut Bootstrap) -> trc::Result<()> { .await?; } + if bp.registry.count_object(ObjectType::SpamRule).await? == 0 + && bp + .registry + .object::(Id::singleton()) + .await? + .is_none_or(|spam| spam.spam_filter_rules_url.is_some()) + { + let mut batch = BatchBuilder::new(); + batch.schedule_task(Task::SpamFilterMaintenance(TaskSpamFilterMaintenance { + maintenance_type: TaskSpamFilterMaintenanceType::UpdateRules, + status: TaskStatus::now(), + })); + bp.data_store.write(batch.build_all()).await?; + } + Ok(()) } diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index 73240089..5cc44f44 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -23,7 +23,7 @@ registry = { path = "../registry" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = { version = "0.4" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["generate"] } +mail-auth = { version = "0.8", features = ["generate"] } tokio = { version = "1.47", features = ["rt"] } hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } hyper-util = { version = "0.1.1", features = ["tokio"] } diff --git a/crates/http/src/auth/permissions.rs b/crates/http/src/auth/permissions.rs index 79f1e29d..80602c74 100644 --- a/crates/http/src/auth/permissions.rs +++ b/crates/http/src/auth/permissions.rs @@ -4,10 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use common::{Server, auth::AccessToken}; +use common::{ + Server, + auth::{AccessToken, RECOVERY_ADMIN_ID, permissions::PermissionsListBuilder}, +}; use http_proto::{HttpResponse, JsonResponse, ToHttpResponse}; -use registry::schema::enums::{Locale, Permission}; +use registry::{ + schema::enums::{Locale, Permission}, + types::EnumImpl, +}; use serde::Serialize; +use utils::DomainPart; #[derive(Debug, Clone, Serialize)] pub struct Account { @@ -43,9 +50,67 @@ impl AccountApiHandler for Server { // SPDX-SnippetEnd let account_info = self.account_info(access_token.account_id()).await?; + let is_external_directory = if let Some(domain_name) = account_info.name().try_domain_part() + && self.get_directory_for_domain(domain_name).await?.is_some() + { + true + } else { + false + }; + let is_recovery_admin = access_token.account_id() == RECOVERY_ADMIN_ID; + let permissions = if let Some(scope) = access_token.access_scope() { + let mut permissions = scope.permissions.clone(); + + for p in [ + Permission::SysDmarcInternalReportUpdate, + Permission::SysDmarcInternalReportCreate, + Permission::SysDmarcExternalReportUpdate, + Permission::SysDmarcExternalReportCreate, + Permission::SysTlsInternalReportUpdate, + Permission::SysTlsInternalReportCreate, + Permission::SysTlsExternalReportUpdate, + Permission::SysTlsExternalReportCreate, + Permission::SysArfExternalReportCreate, + Permission::SysArfExternalReportUpdate, + Permission::SysQueuedMessageCreate, + Permission::SysLogCreate, + Permission::SysLogDestroy, + Permission::SysLogUpdate, + ] { + permissions.clear(p.to_id() as usize); + } + + if is_external_directory || is_recovery_admin { + permissions.clear(Permission::SysAccountPasswordGet.to_id() as usize); + permissions.clear(Permission::SysAccountPasswordUpdate.to_id() as usize); + } + + if is_recovery_admin { + for p in [ + Permission::SysAccountSettingsGet, + Permission::SysAccountSettingsUpdate, + Permission::SysApiKeyCreate, + Permission::SysApiKeyUpdate, + Permission::SysApiKeyDestroy, + Permission::SysApiKeyQuery, + Permission::SysApiKeyGet, + Permission::SysAppPasswordCreate, + Permission::SysAppPasswordUpdate, + Permission::SysAppPasswordDestroy, + Permission::SysAppPasswordQuery, + Permission::SysAppPasswordGet, + ] { + permissions.clear(p.to_id() as usize); + } + } + + permissions.build_permissions_list() + } else { + Vec::new() + }; Ok(JsonResponse::new(Account { - permissions: access_token.permissions(), + permissions, edition, locale: account_info.locale(), }) diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index d53b45a3..0f9593e0 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -23,7 +23,7 @@ calcard = { version = "0.3" } smtp-proto = { version = "0.2" } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = { version = "0.4" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["generate"] } +mail-auth = { version = "0.8", features = ["generate"] } sieve-rs = { version = "0.7", features = ["rkyv"] } jmap-tools = { version = "0.1", features = ["rkyv"] } serde = { version = "1.0", features = ["derive"]} diff --git a/crates/jmap/src/api/query.rs b/crates/jmap/src/api/query.rs index 4254041f..961ab895 100644 --- a/crates/jmap/src/api/query.rs +++ b/crates/jmap/src/api/query.rs @@ -17,8 +17,8 @@ pub struct QueryResponseBuilder { pub limit: usize, anchor: u64, anchor_offset: i32, - has_anchor: bool, - anchor_found: bool, + pub has_anchor: bool, + pub anchor_found: bool, pub response: QueryResponse, } diff --git a/crates/jmap/src/registry/mapping/action.rs b/crates/jmap/src/registry/mapping/action.rs index f5279a29..a3cb4e32 100644 --- a/crates/jmap/src/registry/mapping/action.rs +++ b/crates/jmap/src/registry/mapping/action.rs @@ -34,6 +34,7 @@ use spam_filter::{ }; use std::time::Instant; use store::write::now; +use utils::map::vec_map::VecMap; pub(crate) async fn action_set( mut set: RegistrySetResponse<'_>, @@ -343,10 +344,11 @@ async fn classify_spam(server: &Server, mut request: SpamClassify) -> Option MAIL_BODY_7BIT, - SpamClassifyParameters::Bit8Mime8BitMIMEMessageContent => MAIL_BODY_BINARYMIME, - SpamClassifyParameters::BinaryMime => MAIL_BODY_8BITMIME, - SpamClassifyParameters::SmtpUtf8 => MAIL_SMTPUTF8, + Some(SpamClassifyParameters::Bit7) => MAIL_BODY_7BIT, + Some(SpamClassifyParameters::Bit8Mime8BitMIMEMessageContent) => MAIL_BODY_BINARYMIME, + Some(SpamClassifyParameters::BinaryMime) => MAIL_BODY_8BITMIME, + Some(SpamClassifyParameters::SmtpUtf8) => MAIL_SMTPUTF8, + None => 0, }, env_rcpt_to: request.env_rcpt_to.iter().map(String::as_str).collect(), is_test: true, @@ -371,20 +373,21 @@ async fn classify_spam(server: &Server, mut request: SpamClassify) -> Option SpamClassifyResult::Reject, }; - let mut tags = Vec::with_capacity(ctx.result.tags.len()); + request.tags = VecMap::with_capacity(ctx.result.tags.len()); for tag in ctx.result.tags { let (score, disposition) = match server.core.spam.lists.scores.get(&tag) { Some(SpamFilterAction::Allow(score)) => (*score, SpamClassifyTagDisposition::Score), Some(SpamFilterAction::Discard) => (0.0, SpamClassifyTagDisposition::Discard), _ => (0.0, SpamClassifyTagDisposition::Reject), }; - tags.push(SpamClassifyTag { - disposition, - name: tag, - score: (score as f64).into(), - }); + request.tags.append( + tag, + SpamClassifyTag { + disposition, + score: (score as f64).into(), + }, + ); } - request.tags = tags.into(); Some(request) } diff --git a/crates/jmap/src/registry/mapping/log.rs b/crates/jmap/src/registry/mapping/log.rs index ea3f2fff..e3d08cea 100644 --- a/crates/jmap/src/registry/mapping/log.rs +++ b/crates/jmap/src/registry/mapping/log.rs @@ -92,6 +92,12 @@ pub(crate) async fn log_query( _ => false, })?; + let anchor = req.request.anchor.map(|id| id.id()).unwrap_or(0); + let limit = std::cmp::min( + req.request.limit.unwrap_or(usize::MAX), + req.server.core.jmap.query_max_results, + ); + let params = req .request .extract_parameters(req.server.core.jmap.query_max_results, Property::Id.into())?; @@ -102,23 +108,14 @@ pub(crate) async fn log_query( .details("Only sorting by 'id' is supported for logs")); } - if req.request.calculate_total.unwrap_or(false) { - return Err(trc::JmapEvent::CannotCalculateChanges - .into_err() - .details("Calculating total is not supported for logs")); - } - - if req.request.anchor_offset.unwrap_or(0) != 0 || req.request.position.unwrap_or(0) != 0 { + if req.request.position.unwrap_or(0) != 0 { return Err(trc::JmapEvent::InvalidArguments .into_err() .details("Pagination is only possible using anchors for logs")); } let (tx, rx) = oneshot::channel(); - let anchor = params.anchor.unwrap_or(0); - let limit = params - .limit - .unwrap_or(req.server.core.jmap.query_max_results); + tokio::task::spawn_blocking(move || { let _ = tx.send(read_log_offsets(path, filter.as_deref(), anchor, limit)); }); @@ -144,6 +141,7 @@ pub(crate) async fn log_query( .details("Failed to read log files") .caused_by(trc::location!()) })?; + response.anchor_found = true; Ok(response) } @@ -159,7 +157,7 @@ fn read_log_offsets( let mut entries = Vec::with_capacity(limit); let mut file_number = 0u64; - let mut found_anchor = false; + let mut found_anchor = anchor == 0; let file_anchor = anchor >> 48; 'outer: for log in logs.into_iter() { @@ -172,10 +170,12 @@ fn read_log_offsets( continue; } - let mut rev_lines = RevLines::new(File::open(log.path())?); + let file = File::open(log.path())?; + let file_size = file.metadata()?.len(); + let mut rev_lines = RevLines::new(file); rev_lines.0.init_reader()?; - let mut offset = rev_lines.0.reader_cursor; + let mut offset = file_size; for line in rev_lines { let line = line?; diff --git a/crates/jmap/src/registry/mapping/queued_message.rs b/crates/jmap/src/registry/mapping/queued_message.rs index aa144086..9c763102 100644 --- a/crates/jmap/src/registry/mapping/queued_message.rs +++ b/crates/jmap/src/registry/mapping/queued_message.rs @@ -28,7 +28,7 @@ use registry::{ QueuedRecipient, RecipientStatus, ServerResponse, }, }, - types::{datetime::UTCDateTime, ipaddr::IpAddr, list::List, map::Map}, + types::{datetime::UTCDateTime, ipaddr::IpAddr, map::Map}, }; use smtp::queue::{ self, ArchivedError, ArchivedErrorDetails, ArchivedMessage, ArchivedStatus, ErrorDetails, @@ -41,11 +41,11 @@ use store::{ Deserialize, IterateParams, U64_LEN, ValueKey, ahash::AHashSet, registry::{RegistryFilterOp, RegistryQuery}, - write::{AlignedBytes, Archive, QueueClass, ValueClass, key::DeserializeBigEndian}, + write::{AlignedBytes, Archive, QueueClass, ValueClass, key::DeserializeBigEndian, now}, }; use trc::AddContext; use types::{blob::BlobId, blob_hash::BlobHash, id::Id}; -use utils::DomainPart; +use utils::{DomainPart, map::vec_map::VecMap}; pub(crate) async fn queued_message_set( mut set: RegistrySetResponse<'_>, @@ -83,6 +83,7 @@ pub(crate) async fn queued_message_set( // Process patches let prev_event = archived_message.inner.next_delivery_event(None); let mut message = map_message(archived_message.inner); + let prev_next_retry = message.next_retry; for (key, value) in value.into_expanded_object() { let ptr = match key { Key::Property(prop) => { @@ -96,6 +97,7 @@ pub(crate) async fn queued_message_set( continue 'outer; } } + let set_next_retry = (message.next_retry != prev_next_retry).then_some(message.next_retry); // Process changes let mut has_changes = false; @@ -112,7 +114,7 @@ pub(crate) async fn queued_message_set( if !message .recipients .iter() - .any(|r| r.address.as_str() == rcpt.address.as_ref()) + .any(|(address, _)| address.as_str() == rcpt.address.as_ref()) { rcpt.status = Status::PermanentFailure(ErrorDetails { entity: "localhost".into(), @@ -121,11 +123,11 @@ pub(crate) async fn queued_message_set( has_changes = true; } } - for rcpt in message.recipients.into_iter() { + for (address, rcpt) in message.recipients.into_iter() { let Some(queued_rcpt) = queued_message .recipients .iter_mut() - .find(|r| r.address.as_ref() == rcpt.address.as_str()) + .find(|r| r.address.as_ref() == address.as_str()) else { continue; }; @@ -162,6 +164,13 @@ pub(crate) async fn queued_message_set( } } + if let Some(next_retry) = set_next_retry + && !matches!(queued_rcpt.status, Status::PermanentFailure(_)) + { + queued_rcpt.retry.due = next_retry.timestamp() as u64; + has_changes = true; + } + if matches!(rcpt.status, RecipientStatus::Scheduled) && !matches!(queued_rcpt.status, Status::Scheduled) { @@ -461,8 +470,12 @@ pub(crate) async fn queued_message_query( &req.request, ); - if response.response.total.is_some() { - response.response.total = Some(0); + let mut total = 0; + if let Some(anchor) = req.request.anchor { + let anchor = anchor.id(); + if anchor > due_from { + due_from = anchor; + } } let from_key = ValueKey::from(ValueClass::Queue(QueueClass::MessageEvent( @@ -493,8 +506,8 @@ pub(crate) async fn queued_message_query( queue_name.as_slice() == key.get(U64_LEN * 2..).unwrap_or_default() }) && seen_ids.insert(id) { - if let Some(total) = response.response.total.as_mut() { - *total += 1; + total += 1; + if response.response.total.is_some() { if !response.is_full() { response.add_id(id.into()); } @@ -510,7 +523,11 @@ pub(crate) async fn queued_message_query( .await .caused_by(trc::location!())?; - if let (Some(total), Some(limit)) = (response.response.total, response.response.limit) + if response.response.total.is_some() { + response.response.total = Some(total); + } + + if let Some(limit) = response.response.limit && total < limit { response.response.limit = None; @@ -546,9 +563,22 @@ fn map_message(message_in: &ArchivedMessage) -> QueuedMessage { priority: message_in.priority.to_native() as i64, received_from_ip: IpAddr(message_in.received_from_ip.as_ipaddr()), received_via_port: message_in.received_via_port.to_native() as u64, - recipients: List::with_capacity(message_in.recipients.len()), - return_path: message_in.return_path.to_string(), + recipients: VecMap::with_capacity(message_in.recipients.len()), + return_path: if !message_in.return_path.is_empty() { + message_in.return_path.to_string() + } else { + "<>".to_string() + }, size: message_in.size.to_native(), + next_retry: UTCDateTime::from_timestamp( + message_in + .next_delivery_event(None) + .unwrap_or_else(now) + .cast_signed(), + ), + next_notify: message_in + .next_notify_event(None) + .map(|ts| UTCDateTime::from_timestamp(ts.cast_signed())), }; // Parse flags @@ -572,7 +602,6 @@ fn map_message(message_in: &ArchivedMessage) -> QueuedMessage { // Parse recipients for rcpt_in in message_in.recipients.iter() { let mut rcpt_out = QueuedRecipient { - address: rcpt_in.address.to_string(), expires: match &rcpt_in.expires { ArchivedQueueExpiry::Ttl(ttl) => QueueExpiry::Ttl(QueueExpiryTtl { expires_at: UTCDateTime::from_timestamp( @@ -620,7 +649,9 @@ fn map_message(message_in: &ArchivedMessage) -> QueuedMessage { } } - message_out.recipients.push(rcpt_out); + message_out + .recipients + .append(rcpt_in.address.to_string(), rcpt_out); } message_out diff --git a/crates/jmap/src/registry/mapping/task.rs b/crates/jmap/src/registry/mapping/task.rs index 6b9e9996..3db271d9 100644 --- a/crates/jmap/src/registry/mapping/task.rs +++ b/crates/jmap/src/registry/mapping/task.rs @@ -376,6 +376,7 @@ pub(crate) async fn task_get( } else { task_ids(get.server, get.server.core.jmap.get_max_objects).await? }; + let has_due_field = get.properties.is_empty() || get.properties.contains(&Property::Due); for id in ids { if let Some(task) = get @@ -386,7 +387,16 @@ pub(crate) async fn task_get( ))) .await? { - get.insert(id, task.into_value()); + let due = task.due_timestamp(); + let mut task = task.into_value(); + if has_due_field && due != u64::MAX { + task.as_object_mut().unwrap().insert_unchecked( + Property::Due, + UTCDateTime::from_timestamp(due as i64).into_value(), + ); + } + + get.insert(id, task); } else { get.not_found(id); } @@ -400,6 +410,7 @@ pub(crate) async fn task_query( ) -> trc::Result { let mut due_from = 100u64; let mut due_to = u64::MAX; + let mut typ = None; req.request .extract_filters(|property, op, value| match property { @@ -435,9 +446,24 @@ pub(crate) async fn task_query( false } } + Property::Type => { + if let Some(typ_) = value.as_str().and_then(TaskType::parse) { + typ = Some(typ_); + true + } else { + false + } + } _ => false, })?; + if let Some(anchor) = req.request.anchor { + let anchor = anchor.id(); + if anchor > due_from { + due_from = anchor; + } + } + if req .request .sort @@ -456,15 +482,13 @@ pub(crate) async fn task_query( // Build response let mut response = QueryResponseBuilder::new( - req.server.core.jmap.query_max_results, + req.server.core.jmap.query_max_results + 1, req.server.core.jmap.query_max_results, State::Initial, &req.request, ); - if response.response.total.is_some() { - response.response.total = Some(0); - } + let mut total = 0; let from_key = ValueKey::from(ValueClass::TaskQueue(TaskQueueClass::Due { id: 0, @@ -480,11 +504,25 @@ pub(crate) async fn task_query( .iterate( IterateParams::new(from_key, to_key) .set_ascending(params.sort_ascending) - .no_values(), - |key, _| { + .set_values(typ.is_some()), + |key, value| { + if let Some(typ) = typ { + let task_type = + TaskType::from_id(value.deserialize_be_u16(0)?).ok_or_else(|| { + trc::StoreEvent::DataCorruption + .into_err() + .ctx(trc::Key::Key, key.to_vec()) + .ctx(trc::Key::Value, value.to_vec()) + .caused_by(trc::location!()) + })?; + if task_type != typ { + return Ok(true); + } + } + let id = key.deserialize_be_u64(U64_LEN)?; - if let Some(total) = response.response.total.as_mut() { - *total += 1; + total += 1; + if response.response.total.is_some() { if !response.is_full() { response.add_id(id.into()); } @@ -497,7 +535,11 @@ pub(crate) async fn task_query( .await .caused_by(trc::location!())?; - if let (Some(total), Some(limit)) = (response.response.total, response.response.limit) + if response.response.total.is_some() { + response.response.total = Some(total); + } + + if let Some(limit) = response.response.limit && total < limit { response.response.limit = None; diff --git a/crates/jmap/src/registry/mapping/telemetry.rs b/crates/jmap/src/registry/mapping/telemetry.rs index 9753e6ac..b0ac3dc7 100644 --- a/crates/jmap/src/registry/mapping/telemetry.rs +++ b/crates/jmap/src/registry/mapping/telemetry.rs @@ -18,10 +18,10 @@ use crate::{ use common::Server; use jmap_proto::types::state::State; use registry::{ - jmap::IntoValue, + jmap::{IntoValue, JmapValue}, schema::{ prelude::Property, - structs::{Metric, Trace}, + structs::{Metric, Trace, TraceEvent, TraceValue}, }, types::datetime::UTCDateTime, }; @@ -36,7 +36,7 @@ use store::{ }, write::{SearchIndex, TelemetryClass, ValueClass, key::DeserializeBigEndian, now}, }; -use trc::{AddContext, EventType, MetricType}; +use trc::{AddContext, EventType, Key, MetricType}; use types::id::Id; use utils::snowflake::SnowflakeIdGenerator; @@ -65,6 +65,11 @@ pub(crate) async fn trace_get( .map(Id::from) .collect() }; + let has_timestamp_field = + get.properties.is_empty() || get.properties.contains(&Property::Timestamp); + let has_from_field = get.properties.is_empty() || get.properties.contains(&Property::From); + let has_to_field = get.properties.is_empty() || get.properties.contains(&Property::To); + let has_size_field = get.properties.is_empty() || get.properties.contains(&Property::Size); for id in ids { let item_id = id.id(); @@ -76,7 +81,44 @@ pub(crate) async fn trace_get( )))) .await? { - get.insert(id, trace.into_value()); + let mut values = Vec::with_capacity(4); + + let mut got_timestamp = !has_timestamp_field; + let mut got_from = !has_from_field; + let mut got_to = !has_to_field; + let mut got_size = !has_size_field; + + for event in trace.events.iter() { + if !got_timestamp { + values.push((Property::Timestamp, event.timestamp.into_value())); + got_timestamp = true; + } + if !got_from + && let Some(value) = find_key_value(event, Key::From).map(value_as_string) + { + values.push((Property::From, value)); + got_from = true; + } + if !got_to && let Some(value) = find_key_value(event, Key::To).map(value_as_string) + { + values.push((Property::To, value)); + got_to = true; + } + if !got_size + && let Some(value) = find_key_value(event, Key::Size).map(value_as_number) + { + values.push((Property::Size, value)); + got_size = true; + } + } + + let mut trace = trace.into_value(); + let obj = trace.as_object_mut().unwrap(); + for (key, value) in values { + obj.insert_unchecked(key, value); + } + + get.insert(id, trace); } else { get.not_found(id); } @@ -85,6 +127,40 @@ pub(crate) async fn trace_get( Ok(get) } +fn find_key_value(span: &TraceEvent, key: Key) -> Option<&TraceValue> { + span.key_values + .iter() + .find_map(|kv| if kv.key == key { Some(&kv.value) } else { None }) +} + +fn value_as_string(value: &TraceValue) -> JmapValue<'static> { + match value { + TraceValue::String(s) => JmapValue::Str(s.value.clone().into()), + TraceValue::List(values) => { + let mut result = String::new(); + for value in values.value.iter() { + if let TraceValue::String(s) = value { + if !result.is_empty() { + result.push_str("; "); + } + result.push_str(&s.value); + } + } + JmapValue::Str(result.into()) + } + _ => JmapValue::Null, + } +} + +fn value_as_number(value: &TraceValue) -> JmapValue<'static> { + match value { + TraceValue::Integer(i) => JmapValue::Number(i.value.into()), + TraceValue::UnsignedInt(u) => JmapValue::Number(u.value.into()), + TraceValue::Float(f) => JmapValue::Number(f.value.into_inner().into()), + _ => JmapValue::Null, + } +} + pub(crate) async fn metric_get( mut get: RegistryGetResponse<'_>, ) -> trc::Result> { @@ -393,7 +469,7 @@ pub(crate) async fn metric_query( .await .caused_by(trc::location!())?; - if response.response.total.is_none() { + if response.response.total.is_some() { response.response.total = Some(total); } diff --git a/crates/main/src/test_data.rs b/crates/main/src/test_data.rs index 568cc4ef..0f397599 100644 --- a/crates/main/src/test_data.rs +++ b/crates/main/src/test_data.rs @@ -209,7 +209,7 @@ fn sample_queued_messages(blob_hashes: Vec) -> Vec { size: raw_messages[1].len() as u64, quota_keys: Box::new([]), }, - // Message 3: Report message with a permanent failure recipient + // Message 3: Report message with a temporary failure recipient Message { created: now, blob_hash: blob_hashes[2].clone(), @@ -226,7 +226,7 @@ fn sample_queued_messages(blob_hashes: Vec) -> Vec { }, expires: QueueExpiry::Ttl(365 * 24 * 3600), queue: Default::default(), - status: Status::PermanentFailure(ErrorDetails { + status: Status::TemporaryFailure(ErrorDetails { entity: "mx.bigcorp.com".into(), details: Error::ConnectionError("Rejected by policy".into()), }), diff --git a/crates/migration/Cargo.toml b/crates/migration/Cargo.toml index eff278a1..a67776b5 100644 --- a/crates/migration/Cargo.toml +++ b/crates/migration/Cargo.toml @@ -17,7 +17,7 @@ groupware = { path = "../groupware" } dav-proto = { path = "../dav-proto" } proc_macros = { path = "../utils/proc-macros" } mail-parser = { version = "0.11", features = ["full_encoding"] } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["rkyv"] } +mail-auth = { version = "0.8", features = ["rkyv"] } smtp-proto = { version = "0.2", features = ["rkyv", "serde"] } sieve-rs = { version = "0.7", features = ["rkyv"] } calcard_latest = { package = "calcard", version = "0.3", features = ["rkyv"] } diff --git a/crates/registry/Cargo.toml b/crates/registry/Cargo.toml index e9a47165..0de3a9f4 100644 --- a/crates/registry/Cargo.toml +++ b/crates/registry/Cargo.toml @@ -13,7 +13,7 @@ hashify = "0.2.7" ahash = { version = "0.8" } jmap-tools = { version = "0.1" } xxhash-rust = { version = "0.8.5", features = ["xxh3"] } -mail-auth = { path = "/Users/me/code/mail-auth" } +mail-auth = { version = "0.8" } tokio = { version = "1.47", features = ["fs"] } [features] diff --git a/crates/registry/src/types/id.rs b/crates/registry/src/types/id.rs index fce10dc6..57057cbc 100644 --- a/crates/registry/src/types/id.rs +++ b/crates/registry/src/types/id.rs @@ -88,10 +88,7 @@ impl Pickle for BlobId { stream.read_bytes(BLOB_HASH_LEN).map(|bytes| { BlobId::new( BlobHash::try_from_hash_slice(bytes).unwrap(), - BlobClass::Reserved { - account_id: 0, - expires: 0, - }, + BlobClass::default(), ) }) } diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index a7940d16..1e29ae81 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -21,7 +21,7 @@ email = { path = "../email" } registry = { path = "../registry" } spam-filter = { path = "../spam-filter" } trc = { path = "../trc" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["rkyv"] } +mail-auth = { version = "0.8", features = ["rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } smtp-proto = { version = "0.2", features = ["rkyv", "serde"] } diff --git a/crates/smtp/src/queue/spool.rs b/crates/smtp/src/queue/spool.rs index 441c1c80..a4cadc33 100644 --- a/crates/smtp/src/queue/spool.rs +++ b/crates/smtp/src/queue/spool.rs @@ -803,4 +803,26 @@ impl ArchivedMessage { next_delivery } + + pub fn next_notify_event(&self, queue: Option) -> Option { + let mut next_notify = None; + + for rcpt in self.recipients.iter().filter(|d| { + matches!( + d.status, + ArchivedStatus::Scheduled | ArchivedStatus::TemporaryFailure(_) + ) && queue.is_none_or(|q| d.queue == q) + }) { + let notify_due = rcpt.notify.due.to_native(); + if let Some(next_notify) = &mut next_notify { + if notify_due < *next_notify { + *next_notify = notify_due; + } + } else { + next_notify = Some(notify_due); + } + } + + next_notify + } } diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml index fda05372..2be80ae9 100644 --- a/crates/spam-filter/Cargo.toml +++ b/crates/spam-filter/Cargo.toml @@ -14,7 +14,7 @@ registry = { path = "../registry" } smtp-proto = { version = "0.2", features = ["rkyv"] } mail-parser = { version = "0.11", features = ["full_encoding"] } mail-builder = { version = "0.4" } -mail-auth = { path = "/Users/me/code/mail-auth" } +mail-auth = { version = "0.8" } tokio = { version = "1.47", features = ["net", "macros"] } psl = "2" hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } diff --git a/crates/store/src/registry/local.rs b/crates/store/src/registry/local.rs index 1ff2cd98..a497c0e8 100644 --- a/crates/store/src/registry/local.rs +++ b/crates/store/src/registry/local.rs @@ -6,7 +6,7 @@ use crate::{RegistryStore, RegistryStoreInner, Store}; use registry::schema::structs::DataStore; -use std::path::PathBuf; +use std::{net::IpAddr, path::PathBuf}; use utils::snowflake::SnowflakeIdGenerator; impl RegistryStoreInner { @@ -39,7 +39,15 @@ impl RegistryStoreInner { env_hostname: std::env::var("STALWART_HOSTNAME") .ok() .filter(|h| !h.is_empty()) - .unwrap_or_else(|| gethostname::gethostname().to_string_lossy().into_owned()) + .unwrap_or_else(|| { + let host = gethostname::gethostname(); + let host = host.to_string_lossy(); + if host.parse::().is_err() { + host.into_owned() + } else { + "localhost".to_string() + } + }) .to_lowercase(), } } diff --git a/crates/trc/Cargo.toml b/crates/trc/Cargo.toml index ed45b2a5..df8ac7c8 100644 --- a/crates/trc/Cargo.toml +++ b/crates/trc/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" [dependencies] event_macro = { path = "./event-macro" } -mail-auth = { path = "/Users/me/code/mail-auth" } +mail-auth = { version = "0.8" } mail-parser = { version = "0.11", features = ["full_encoding"] } base64 = "0.22.1" serde = "1.0" diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 287e4af1..e843c669 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -8,11 +8,11 @@ trc = { path = "../trc" } rustls = { version = "0.23.5", default-features = false, features = ["std", "aws_lc_rs", "tls12"] } rustls-pemfile = "2.0" rustls-pki-types = { version = "1" } -rustls-platform-verifier = "0.6" +rustls-platform-verifier = "0.7" tokio = { version = "1.47", features = ["net", "macros", "signal"] } tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] } serde = { version = "1.0", features = ["derive"]} -mail-auth = { path = "/Users/me/code/mail-auth" } +mail-auth = { version = "0.8" } smtp-proto = { version = "0.2" } ahash = { version = "0.8", features = ["serde"] } chrono = "0.4" diff --git a/resources/config/config.toml b/resources/config/config.toml deleted file mode 100644 index b62ed9fb..00000000 --- a/resources/config/config.toml +++ /dev/null @@ -1,73 +0,0 @@ -############################################# -# Stalwart Configuration File -############################################# - -[server.listener."smtp"] -bind = ["[::]:25"] -protocol = "smtp" - -[server.listener."submission"] -bind = ["[::]:587"] -protocol = "smtp" - -[server.listener."submissions"] -bind = ["[::]:465"] -protocol = "smtp" -tls.implicit = true - -[server.listener."imap"] -bind = ["[::]:143"] -protocol = "imap" - -[server.listener."imaptls"] -bind = ["[::]:993"] -protocol = "imap" -tls.implicit = true - -[server.listener.pop3] -bind = "[::]:110" -protocol = "pop3" - -[server.listener.pop3s] -bind = "[::]:995" -protocol = "pop3" -tls.implicit = true - -[server.listener."sieve"] -bind = ["[::]:4190"] -protocol = "managesieve" - -[server.listener."https"] -protocol = "http" -bind = ["[::]:443"] -tls.implicit = true - -[storage] -data = "rocksdb" -fts = "rocksdb" -blob = "rocksdb" -lookup = "rocksdb" -directory = "internal" - -[store."rocksdb"] -type = "rocksdb" -path = "%{env:STALWART_PATH}%/data" -compression = "lz4" - -[directory."internal"] -type = "internal" -store = "rocksdb" - -[tracer."stdout"] -type = "stdout" -level = "info" -ansi = false -enable = true - -#[server.run-as] -#user = "stalwart" -#group = "stalwart" - -[authentication.fallback-admin] -user = "admin" -secret = "%{env:ADMIN_SECRET}%" diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 17f1b15d..c1b2ced8 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -45,12 +45,12 @@ migration = { path = "../crates/migration", features = ["test_mode", "enterprise trc = { path = "../crates/trc", features = ["enterprise"] } managesieve = { path = "../crates/managesieve", features = ["test_mode", "enterprise"] } smtp-proto = { version = "0.2" } -mail-auth = { path = "/Users/me/code/mail-auth", features = ["test"] } +mail-auth = { version = "0.8", features = ["test"] } mail-parser = { version = "0.11", features = ["full_encoding", "rkyv"] } mail-builder = "0.4.4" sieve-rs = { version = "0.7", features = ["rkyv"] } utils = { path = "../crates/utils", features = ["test_mode"] } -jmap-client = { path = "/Users/me/code/jmap-client", features = ["websockets", "debug", "async"] } +jmap-client = { version = "0.4", features = ["websockets", "debug", "async"] } tokio = { version = "1.47", features = ["full"] } tokio-rustls = { version = "0.26", default-features = false, features = ["aws_lc_rs", "tls12"] } rustls = { version = "0.23.5", default-features = false, features = ["std", "aws_lc_rs", "tls12"] } diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index 8362d9a3..211355fe 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -239,8 +239,8 @@ async fn manage_queue() { let (sender, recipients) = envelopes.get(env_id.as_str()).unwrap(); assert_eq!(&message.return_path, sender); 'outer: for recipient in recipients { - for rcpt in message.recipients.iter() { - if &rcpt.address == recipient { + for (address, _) in message.recipients.iter() { + if address == recipient { continue 'outer; } } @@ -253,7 +253,7 @@ async fn manage_queue() { let next_retry = created + hold_for; let next_notify = created + 2000 + hold_for; let expires = created + 3000 + hold_for; - for rcpt in message.recipients.iter() { + for (rcpt_address, rcpt) in message.recipients.iter() { if env_id == "c" { let mut dt = rcpt.retry_due; dt.add_seconds(-1); @@ -274,7 +274,7 @@ async fn manage_queue() { &message, ); assert_eq!(&rcpt.status, &RecipientStatus::Scheduled, "{message:#?}"); - } else if rcpt.address == "success@foobar.org" { + } else if rcpt_address == "success@foobar.org" { assert_eq!(rcpt.retry_count, 0); assert!( matches!(&rcpt.status, RecipientStatus::Completed(_)), @@ -390,16 +390,15 @@ async fn manage_queue() { .retry_count, 2 ); - for rcpt in admin + for (rcpt_address, rcpt) in admin .registry_get::(id_map["a"]) .await .recipients - .values() { let next_retry = rcpt.retry_due.to_string(); let matched = ["2200-01-01T00:00:00Z", "2199-12-31T23:59:59Z"].contains(&next_retry.as_str()); - if rcpt.address.ends_with("example1.org") { + if rcpt_address.ends_with("example1.org") { assert!(matched, "{next_retry}"); } else { assert!(!matched, "{next_retry}"); @@ -456,18 +455,18 @@ async fn manage_queue() { let message = admin.registry_get::(id_map[id]).await; assert!(!message.recipients.is_empty()); - for rcpt in message.recipients { + for (rcpt_address, rcpt) in message.recipients { match id { "a" => { - if rcpt.address.ends_with("example2.org") { + if rcpt_address.ends_with("example2.org") { assert!(matches!(&rcpt.status, RecipientStatus::PermanentFailure(_))); } else { assert!(matches!(&rcpt.status, RecipientStatus::Scheduled)); } } "c" => { - if rcpt.address.ends_with("example2.com") { - if rcpt.address == "rcpt6@example2.com" { + if rcpt_address.ends_with("example2.com") { + if rcpt_address == "rcpt6@example2.com" { assert!(matches!(&rcpt.status, RecipientStatus::PermanentFailure(_))); } else { assert!(matches!(&rcpt.status, RecipientStatus::Scheduled)); diff --git a/tests/src/utils/server.rs b/tests/src/utils/server.rs index ebbfff80..be44bbcd 100644 --- a/tests/src/utils/server.rs +++ b/tests/src/utils/server.rs @@ -18,7 +18,7 @@ use crate::{ use ahash::AHashMap; use common::{ BuildServer, Caches, Core, Data, DavResources, Inner, Server, - auth::FALLBACK_ADMIN_ID, + auth::RECOVERY_ADMIN_ID, config::{ server::{Listeners, ServerProtocol}, storage::Storage, @@ -29,6 +29,7 @@ use common::{ boot::{IpcReceivers, build_ipc}, defaults::BootstrapDefaults, }, + psl, }; use email::message::metadata::MessageMetadata; use groupware::cache::GroupwareCache; @@ -42,8 +43,8 @@ use registry::{ enums::{DataStoreType, EventPolicy, NetworkListenerProtocol, TracingLevel}, prelude::{Object, ObjectType, SocketAddr}, structs::{ - Authentication, Certificate, NetworkListener, PublicText, SecretKeyFile, SecretText, - Tracer, TracerStdout, + Authentication, Certificate, Domain, NetworkListener, PublicText, SecretKeyFile, + SecretText, SystemSettings, Tracer, TracerStdout, }, }, types::{EnumImpl, datetime::UTCDateTime, map::Map}, @@ -91,7 +92,6 @@ pub struct TestServerBuilder { impl TestServerBuilder { pub async fn new(test_name: &str) -> Self { - //let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); let reset = std::env::var("NO_INSERT").is_err(); Self::new_with_role(test_name, "mail.example.org".to_string(), None, reset).await @@ -265,6 +265,21 @@ impl TestServerBuilder { .map(|log| TracingLevel::parse(&log).expect("Invalid log level")) .ok(); + // Add default domain + let default_domain = psl::domain_str(self.bootstrap.registry.local_hostname()).unwrap(); + let default_domain_id = self + .insert_object(Domain { + name: default_domain.to_string(), + ..Default::default() + }) + .await; + self.insert_object(SystemSettings { + default_hostname: self.bootstrap.registry.local_hostname().to_string(), + default_domain_id, + ..Default::default() + }) + .await; + self.insert_object(Tracer::Stdout(TracerStdout { enable: level.is_some() || self.logging_enabled, level: level.unwrap_or(TracingLevel::Info), @@ -432,7 +447,7 @@ impl TestServerBuilder { "popolna_zapora", &[], "Recovery Admin", - Id::from(FALLBACK_ADMIN_ID), + Id::from(RECOVERY_ADMIN_ID), ); admin.http_listener_port = self.http_listener_port;