From d6dc6ee8c50347b594446c8d90010780589b2a5c Mon Sep 17 00:00:00 2001 From: mdecimus Date: Tue, 15 Apr 2025 19:34:13 +0200 Subject: [PATCH] Zero-copy principals + CompactString partial impl --- Cargo.lock | 13 + crates/common/src/addresses.rs | 11 +- crates/common/src/auth/access_token.rs | 47 +- crates/common/src/auth/mod.rs | 7 +- crates/common/src/auth/oauth/introspect.rs | 13 +- crates/common/src/auth/oauth/oidc.rs | 33 +- crates/common/src/auth/oauth/registration.rs | 59 +- crates/common/src/auth/oauth/token.rs | 18 +- crates/common/src/auth/roles.rs | 33 +- crates/common/src/config/network.rs | 17 +- crates/common/src/config/smtp/queue.rs | 4 +- crates/common/src/config/smtp/session.rs | 11 +- crates/common/src/config/spamfilter.rs | 4 +- crates/common/src/enterprise/mod.rs | 27 +- crates/common/src/expr/eval.rs | 84 +- crates/common/src/expr/functions/asynch.rs | 88 +- crates/common/src/expr/functions/email.rs | 18 +- crates/common/src/expr/functions/misc.rs | 22 +- crates/common/src/expr/functions/mod.rs | 6 +- crates/common/src/expr/functions/text.rs | 152 ++-- crates/common/src/expr/if_block.rs | 13 +- crates/common/src/expr/mod.rs | 117 ++- crates/common/src/expr/tokenizer.rs | 4 +- crates/common/src/ipc.rs | 7 +- crates/common/src/lib.rs | 2 + crates/common/src/listener/acme/directory.rs | 4 +- crates/common/src/listener/acme/order.rs | 5 +- crates/common/src/listener/mod.rs | 5 +- crates/common/src/manager/boot.rs | 14 +- crates/common/src/manager/reload.rs | 2 +- crates/common/src/scripts/mod.rs | 3 +- crates/common/src/sharing/acl.rs | 15 +- crates/common/src/telemetry/tracers/otel.rs | 6 +- crates/common/src/telemetry/tracers/store.rs | 6 +- crates/dav/Cargo.toml | 1 + crates/dav/src/common/acl.rs | 10 +- crates/dav/src/common/lock.rs | 4 +- crates/dav/src/common/propfind.rs | 2 +- crates/dav/src/principal/propfind.rs | 22 +- crates/dav/src/principal/propsearch.rs | 2 +- crates/directory/Cargo.toml | 2 + crates/directory/src/backend/imap/lookup.rs | 9 +- .../directory/src/backend/internal/lookup.rs | 62 +- .../directory/src/backend/internal/manage.rs | 851 +++++++++++------- crates/directory/src/backend/internal/mod.rs | 317 +------ crates/directory/src/backend/ldap/lookup.rs | 151 ++-- crates/directory/src/backend/ldap/pool.rs | 2 +- crates/directory/src/backend/memory/config.rs | 39 +- crates/directory/src/backend/memory/lookup.rs | 22 +- crates/directory/src/backend/mod.rs | 4 +- crates/directory/src/backend/oidc/lookup.rs | 24 +- crates/directory/src/backend/smtp/lookup.rs | 15 +- crates/directory/src/backend/sql/lookup.rs | 84 +- crates/directory/src/core/dispatch.rs | 5 +- crates/directory/src/core/principal.rs | 474 +++++++--- crates/directory/src/core/secret.rs | 5 +- crates/directory/src/lib.rs | 86 +- crates/email/src/identity/mod.rs | 17 +- crates/email/src/mailbox/mod.rs | 9 +- crates/email/src/message/cache.rs | 3 + crates/email/src/message/crypto.rs | 4 +- crates/email/src/message/delivery.rs | 9 +- crates/email/src/message/metadata.rs | 6 +- crates/email/src/push/mod.rs | 11 +- crates/email/src/sieve/index.rs | 2 +- crates/email/src/sieve/ingest.rs | 29 +- crates/email/src/sieve/mod.rs | 19 +- crates/email/src/submission/mod.rs | 4 +- crates/groupware/Cargo.toml | 1 + crates/groupware/src/calendar/mod.rs | 23 +- crates/groupware/src/contact/mod.rs | 6 +- crates/groupware/src/file/mod.rs | 4 +- crates/http-proto/Cargo.toml | 1 + crates/http-proto/src/context.rs | 18 +- crates/http/Cargo.toml | 1 + crates/http/src/auth/oauth/auth.rs | 3 +- crates/http/src/auth/oauth/mod.rs | 4 +- crates/http/src/auth/oauth/openid.rs | 85 +- crates/http/src/auth/oauth/registration.rs | 18 +- crates/http/src/auth/oauth/token.rs | 13 +- crates/http/src/autoconfig/mod.rs | 19 +- crates/http/src/form/mod.rs | 7 +- .../src/management/enterprise/telemetry.rs | 1 - .../src/management/enterprise/undelete.rs | 2 +- crates/http/src/management/principal.rs | 116 ++- crates/http/src/management/queue.rs | 53 +- crates/http/src/management/report.rs | 21 +- crates/http/src/management/spam.rs | 3 +- crates/http/src/request.rs | 13 +- crates/imap-proto/src/protocol/mod.rs | 4 +- crates/imap/src/core/client.rs | 4 +- crates/imap/src/core/mailbox.rs | 22 +- crates/imap/src/op/acl.rs | 17 +- crates/imap/src/op/rename.rs | 13 +- crates/jmap-proto/src/types/collection.rs | 3 +- crates/jmap/Cargo.toml | 1 + crates/jmap/src/api/request.rs | 1 + crates/jmap/src/api/session.rs | 17 +- crates/jmap/src/identity/get.rs | 23 +- crates/jmap/src/identity/set.rs | 23 +- crates/jmap/src/lib.rs | 2 + crates/jmap/src/mailbox/set.rs | 2 +- crates/jmap/src/principal/get.rs | 8 +- crates/jmap/src/push/set.rs | 7 +- crates/jmap/src/quota/get.rs | 8 +- crates/jmap/src/sieve/set.rs | 5 +- crates/jmap/src/submission/set.rs | 49 +- crates/jmap/src/vacation/set.rs | 6 +- crates/jmap/src/websocket/stream.rs | 1 + crates/jmap/src/websocket/upgrade.rs | 20 +- crates/main/src/main.rs | 4 +- crates/managesieve/src/core/client.rs | 4 +- crates/managesieve/src/core/mod.rs | 4 +- crates/services/Cargo.toml | 1 + crates/services/src/index/mod.rs | 7 +- crates/services/src/state_manager/http.rs | 5 +- crates/services/src/state_manager/mod.rs | 9 +- crates/smtp/Cargo.toml | 1 + crates/smtp/src/core/mod.rs | 37 +- crates/smtp/src/core/params.rs | 3 +- crates/smtp/src/inbound/auth.rs | 3 +- crates/smtp/src/inbound/data.rs | 9 +- crates/smtp/src/inbound/ehlo.rs | 11 +- crates/smtp/src/inbound/hooks/message.rs | 35 +- crates/smtp/src/inbound/hooks/mod.rs | 66 +- crates/smtp/src/inbound/mail.rs | 24 +- crates/smtp/src/inbound/milter/macros.rs | 8 + crates/smtp/src/inbound/milter/message.rs | 32 +- crates/smtp/src/inbound/milter/mod.rs | 29 +- crates/smtp/src/inbound/milter/protocol.rs | 11 +- crates/smtp/src/inbound/rcpt.rs | 17 +- crates/smtp/src/inbound/session.rs | 38 +- crates/smtp/src/inbound/spawn.rs | 11 +- crates/smtp/src/inbound/vrfy.rs | 5 +- crates/smtp/src/lib.rs | 2 + crates/smtp/src/outbound/client.rs | 4 +- crates/smtp/src/outbound/dane/verify.rs | 12 +- crates/smtp/src/outbound/delivery.rs | 54 +- crates/smtp/src/outbound/local.rs | 16 +- crates/smtp/src/outbound/lookup.rs | 49 +- crates/smtp/src/outbound/mod.rs | 97 +- crates/smtp/src/outbound/session.rs | 13 +- crates/smtp/src/queue/dsn.rs | 48 +- crates/smtp/src/queue/mod.rs | 53 +- crates/smtp/src/queue/spool.rs | 33 +- crates/smtp/src/reporting/analysis.rs | 33 +- crates/smtp/src/reporting/dkim.rs | 7 +- crates/smtp/src/reporting/dmarc.rs | 29 +- crates/smtp/src/reporting/mod.rs | 11 +- crates/smtp/src/reporting/spf.rs | 7 +- crates/smtp/src/reporting/tls.rs | 13 +- crates/smtp/src/scripts/envelope.rs | 15 +- crates/smtp/src/scripts/event_loop.rs | 11 +- crates/smtp/src/scripts/exec.rs | 9 +- crates/smtp/src/scripts/mod.rs | 11 +- crates/spam-filter/Cargo.toml | 1 + crates/spam-filter/src/analysis/domain.rs | 5 +- crates/spam-filter/src/analysis/ehlo.rs | 2 +- crates/spam-filter/src/analysis/init.rs | 16 +- crates/spam-filter/src/analysis/messageid.rs | 4 +- crates/spam-filter/src/analysis/mod.rs | 3 +- crates/spam-filter/src/analysis/recipient.rs | 9 +- crates/spam-filter/src/analysis/rules.rs | 17 +- crates/spam-filter/src/lib.rs | 17 +- crates/spam-filter/src/modules/dnsbl.rs | 19 +- crates/spam-filter/src/modules/expression.rs | 123 ++- crates/spam-filter/src/modules/html.rs | 48 +- crates/spam-filter/src/modules/sanitize.rs | 14 +- crates/store/Cargo.toml | 1 + crates/store/src/lib.rs | 69 +- crates/trc/src/event/conv.rs | 12 +- crates/trc/src/event/mod.rs | 13 +- crates/trc/src/lib.rs | 4 +- crates/trc/src/serializers/binary.rs | 7 +- crates/trc/src/serializers/json.rs | 1 - crates/trc/src/serializers/text.rs | 4 - crates/utils/Cargo.toml | 1 + crates/utils/src/config/utils.rs | 21 + crates/utils/src/lib.rs | 5 +- tests/Cargo.toml | 5 +- tests/src/directory/internal.rs | 255 +++--- tests/src/directory/ldap.rs | 77 +- tests/src/directory/mod.rs | 82 +- tests/src/directory/oidc.rs | 4 +- tests/src/directory/smtp.rs | 60 +- tests/src/directory/sql.rs | 89 +- tests/src/jmap/auth_oauth.rs | 11 +- tests/src/jmap/mod.rs | 23 +- tests/src/jmap/permissions.rs | 143 +-- tests/src/smtp/config.rs | 67 +- tests/src/smtp/inbound/antispam.rs | 145 +-- tests/src/smtp/inbound/auth.rs | 4 +- tests/src/smtp/inbound/data.rs | 6 +- tests/src/smtp/inbound/dmarc.rs | 4 +- tests/src/smtp/inbound/ehlo.rs | 6 +- tests/src/smtp/inbound/limits.rs | 6 +- tests/src/smtp/inbound/mail.rs | 4 +- tests/src/smtp/inbound/milter.rs | 67 +- tests/src/smtp/inbound/rcpt.rs | 4 +- tests/src/smtp/inbound/rewrite.rs | 2 +- tests/src/smtp/inbound/scripts.rs | 6 +- tests/src/smtp/inbound/sign.rs | 2 +- tests/src/smtp/inbound/throttle.rs | 22 +- tests/src/smtp/inbound/vrfy.rs | 4 +- tests/src/smtp/lookup/sql.rs | 23 +- tests/src/smtp/lookup/utils.rs | 12 +- tests/src/smtp/management/queue.rs | 2 +- tests/src/smtp/mod.rs | 1 + tests/src/smtp/outbound/dane.rs | 6 +- tests/src/smtp/outbound/extensions.rs | 4 +- tests/src/smtp/outbound/fallback_relay.rs | 2 +- tests/src/smtp/outbound/ip_lookup.rs | 2 +- tests/src/smtp/outbound/lmtp.rs | 2 +- tests/src/smtp/outbound/mta_sts.rs | 2 +- tests/src/smtp/outbound/smtp.rs | 4 +- tests/src/smtp/outbound/throttle.rs | 18 +- tests/src/smtp/outbound/tls.rs | 2 +- tests/src/smtp/queue/concurrent.rs | 2 +- tests/src/smtp/queue/dsn.rs | 36 +- tests/src/smtp/queue/manager.rs | 8 +- tests/src/smtp/queue/retry.rs | 4 +- tests/src/smtp/reporting/analyze.rs | 2 +- tests/src/smtp/session.rs | 34 +- 223 files changed, 3509 insertions(+), 2896 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 554515c1..74debb06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1702,6 +1702,7 @@ version = "0.11.7" dependencies = [ "calcard", "common", + "compact_str", "dav-proto", "directory", "groupware", @@ -1900,6 +1901,7 @@ dependencies = [ "argon2", "async-trait", "base64 0.22.1", + "compact_str", "deadpool 0.10.0", "futures", "jmap_proto", @@ -1914,6 +1916,7 @@ dependencies = [ "pwhash", "regex", "reqwest 0.12.12", + "rkyv 0.8.10", "rustls 0.23.23", "rustls-pki-types", "scrypt", @@ -2772,6 +2775,7 @@ version = "0.11.7" dependencies = [ "calcard", "common", + "compact_str", "dav-proto", "directory", "hashify", @@ -3063,6 +3067,7 @@ dependencies = [ "base64 0.22.1", "chrono", "common", + "compact_str", "dav", "directory", "email", @@ -3168,6 +3173,7 @@ name = "http_proto" version = "0.11.7" dependencies = [ "common", + "compact_str", "form_urlencoded", "http-body-util", "hyper 1.6.0", @@ -3761,6 +3767,7 @@ dependencies = [ "bincode", "chrono", "common", + "compact_str", "directory", "email", "futures-util", @@ -6679,6 +6686,7 @@ dependencies = [ "aes-gcm-siv", "base64 0.22.1", "common", + "compact_str", "directory", "email", "hkdf", @@ -6845,6 +6853,7 @@ dependencies = [ "blake3", "chrono", "common", + "compact_str", "directory", "email", "form_urlencoded", @@ -6931,6 +6940,7 @@ name = "spam-filter" version = "0.11.8" dependencies = [ "common", + "compact_str", "decancer", "hyper 1.6.0", "idna", @@ -7025,6 +7035,7 @@ dependencies = [ "bitpacking", "blake3", "bytes", + "compact_str", "deadpool 0.12.2", "deadpool-postgres", "elasticsearch", @@ -7233,6 +7244,7 @@ dependencies = [ "bytes", "chrono", "common", + "compact_str", "csv", "directory", "ece", @@ -7927,6 +7939,7 @@ dependencies = [ "base64 0.22.1", "blake3", "chrono", + "compact_str", "downcast-rs", "erased-serde", "fast-float", diff --git a/crates/common/src/addresses.rs b/crates/common/src/addresses.rs index b850614a..5f67a4b6 100644 --- a/crates/common/src/addresses.rs +++ b/crates/common/src/addresses.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; +use compact_str::CompactString; use directory::{Directory, backend::RcptType}; use utils::config::{Config, utils::AsKey}; @@ -99,7 +100,7 @@ impl Server { directory: &Directory, address: &str, session_id: u64, - ) -> trc::Result> { + ) -> trc::Result> { directory .vrfy( self.core @@ -119,7 +120,7 @@ impl Server { directory: &Directory, address: &str, session_id: u64, - ) -> trc::Result> { + ) -> trc::Result> { directory .expn( self.core @@ -195,7 +196,7 @@ impl AddressMapping { } AddressMapping::Custom(if_block) => { if let Some(result) = core - .eval_if::(if_block, &Address(address), session_id) + .eval_if::(if_block, &Address(address), session_id) .await { return result.into(); @@ -219,9 +220,9 @@ impl AddressMapping { .map(|(_, domain_part)| format!("@{}", domain_part)) .map(Cow::Owned), AddressMapping::Custom(if_block) => core - .eval_if::(if_block, &Address(address), session_id) + .eval_if::(if_block, &Address(address), session_id) .await - .map(Cow::Owned), + .map(|s| Cow::Owned(s.into())), AddressMapping::Disable => None, } } diff --git a/crates/common/src/auth/access_token.rs b/crates/common/src/auth/access_token.rs index e34faa1c..9571bb28 100644 --- a/crates/common/src/auth/access_token.rs +++ b/crates/common/src/auth/access_token.rs @@ -8,7 +8,6 @@ use ahash::AHashSet; use directory::{ Permission, Principal, QueryBy, Type, backend::internal::{ - PrincipalField, lookup::DirectoryStore, manage::{ChangedPrincipals, ManageDirectory}, }, @@ -49,26 +48,16 @@ impl Server { let mut role_permissions = RolePermissions::default(); // Apply role permissions - for role_id in principal.iter_int(PrincipalField::Roles) { - role_permissions.union(self.get_role_permissions(role_id as u32).await?.as_ref()); + for role_id in principal.roles() { + role_permissions.union(self.get_role_permissions(*role_id).await?.as_ref()); } // Add principal permissions - for (permissions, field) in [ - ( - &mut role_permissions.enabled, - PrincipalField::EnabledPermissions, - ), - ( - &mut role_permissions.disabled, - PrincipalField::DisabledPermissions, - ), - ] { - for permission in principal.iter_int(field) { - let permission = permission as usize; - if permission < Permission::COUNT { - permissions.set(permission); - } + for permission in principal.permissions() { + if permission.grant { + role_permissions.enabled.set(permission.permission.id()); + } else { + role_permissions.disabled.set(permission.permission.id()); } } @@ -82,7 +71,7 @@ impl Server { let mut tenant = None; #[cfg(feature = "enterprise")] if self.is_enterprise_edition() { - if let Some(tenant_id) = principal.get_int(PrincipalField::Tenant).map(|v| v as u32) { + if let Some(tenant_id) = principal.tenant { // Limit tenant permissions permissions.intersection(&self.get_role_permissions(tenant_id).await?.enabled); @@ -101,7 +90,7 @@ impl Server { .id(tenant_id) .caused_by(trc::location!()) })? - .get_int(PrincipalField::Quota) + .quota .unwrap_or_default(), }); } @@ -113,17 +102,15 @@ impl Server { let mut access_token = AccessToken { primary_id: principal.id(), member_of: principal - .iter_int(PrincipalField::MemberOf) - .map(|v| v as u32) - .collect(), + .member_of_mut() + .map(std::mem::take) + .unwrap_or_default(), access_to: VecMap::new(), tenant, - name: principal.take_str(PrincipalField::Name).unwrap_or_default(), - description: principal.take_str(PrincipalField::Description), - emails: principal - .take_str_array(PrincipalField::Emails) - .unwrap_or_default(), - quota: principal.quota(), + name: principal.name, + description: principal.description, + emails: principal.emails, + quota: principal.quota.unwrap_or_default(), permissions, concurrent_imap_requests: self.core.imap.rate_concurrent.map(ConcurrencyLimiter::new), concurrent_http_requests: self @@ -280,7 +267,7 @@ impl Server { None, (*id).into(), &[Type::Individual, Type::Group, Type::Role, Type::ApiKey], - &[PrincipalField::Name], + false, 0, 0, ) diff --git a/crates/common/src/auth/mod.rs b/crates/common/src/auth/mod.rs index ec7297f2..81f6e7b9 100644 --- a/crates/common/src/auth/mod.rs +++ b/crates/common/src/auth/mod.rs @@ -6,6 +6,7 @@ use std::{net::IpAddr, sync::Arc}; +use compact_str::CompactString; use directory::{ Directory, Permission, Permissions, Principal, QueryBy, core::secret::verify_secret_hash, }; @@ -30,9 +31,9 @@ pub struct AccessToken { pub primary_id: u32, pub member_of: Vec, pub access_to: VecMap>, - pub name: String, - pub description: Option, - pub emails: Vec, + pub name: CompactString, + pub description: Option, + pub emails: Vec, pub quota: u64, pub permissions: Permissions, pub tenant: Option, diff --git a/crates/common/src/auth/oauth/introspect.rs b/crates/common/src/auth/oauth/introspect.rs index 243c4d2c..ca5161bd 100644 --- a/crates/common/src/auth/oauth/introspect.rs +++ b/crates/common/src/auth/oauth/introspect.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use serde::{Deserialize, Serialize}; use trc::{AddContext, AuthEvent, EventType}; @@ -16,19 +17,19 @@ pub struct OAuthIntrospect { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub scope: Option, + pub scope: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub client_id: Option, + pub client_id: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub username: Option, + pub username: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub token_type: Option, + pub token_type: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -44,7 +45,7 @@ pub struct OAuthIntrospect { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub sub: Option, + pub sub: Option, } impl Server { @@ -67,7 +68,7 @@ impl Server { .clone() } .into(), - token_type: "bearer".to_string().into(), + token_type: Some("bearer".into()), exp: Some(token_info.expiry as i64), iat: Some(token_info.issued_at as i64), ..Default::default() diff --git a/crates/common/src/auth/oauth/oidc.rs b/crates/common/src/auth/oauth/oidc.rs index bde3b2f3..c1385793 100644 --- a/crates/common/src/auth/oauth/oidc.rs +++ b/crates/common/src/auth/oauth/oidc.rs @@ -7,6 +7,7 @@ use std::fmt; use biscuit::{ClaimsSet, JWT, RegisteredClaims, SingleOrMultiple, jws::RegisteredHeader}; +use compact_str::CompactString; use serde::{ Deserialize, Deserializer, Serialize, de::{self, Visitor}, @@ -19,47 +20,47 @@ use crate::Server; pub struct Userinfo { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub sub: Option, + pub sub: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub name: Option, + pub name: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub given_name: Option, + pub given_name: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub family_name: Option, + pub family_name: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub middle_name: Option, + pub middle_name: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub nickname: Option, + pub nickname: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub preferred_username: Option, + pub preferred_username: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub profile: Option, + pub profile: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub picture: Option, + pub picture: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub website: Option, + pub website: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub email: Option, + pub email: Option, #[serde(default, deserialize_with = "any_bool")] #[serde(skip_serializing_if = "std::ops::Not::not")] @@ -67,11 +68,11 @@ pub struct Userinfo { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub zoneinfo: Option, + pub zoneinfo: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub locale: Option, + pub locale: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -82,15 +83,15 @@ pub struct Userinfo { pub struct StandardClaims { #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] - pub nonce: Option, + pub nonce: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] - pub preferred_username: Option, + pub preferred_username: Option, #[serde(skip_serializing_if = "Option::is_none")] #[serde(default)] - pub email: Option, + pub email: Option, } impl Server { diff --git a/crates/common/src/auth/oauth/registration.rs b/crates/common/src/auth/oauth/registration.rs index 2408aba5..0f93a889 100644 --- a/crates/common/src/auth/oauth/registration.rs +++ b/crates/common/src/auth/oauth/registration.rs @@ -4,21 +4,22 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "snake_case")] pub struct ClientRegistrationRequest { - pub redirect_uris: Vec, + pub redirect_uris: Vec, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] - pub response_types: Vec, + pub response_types: Vec, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] - pub grant_types: Vec, + pub grant_types: Vec, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -26,31 +27,31 @@ pub struct ClientRegistrationRequest { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] - pub contacts: Vec, + pub contacts: Vec, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub client_name: Option, + pub client_name: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub logo_uri: Option, + pub logo_uri: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub client_uri: Option, + pub client_uri: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub policy_uri: Option, + pub policy_uri: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub tos_uri: Option, + pub tos_uri: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub jwks_uri: Option, + pub jwks_uri: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -58,7 +59,7 @@ pub struct ClientRegistrationRequest { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub sector_identifier_uri: Option, + pub sector_identifier_uri: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -66,39 +67,39 @@ pub struct ClientRegistrationRequest { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub id_token_signed_response_alg: Option, + pub id_token_signed_response_alg: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub id_token_encrypted_response_alg: Option, + pub id_token_encrypted_response_alg: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub id_token_encrypted_response_enc: Option, + pub id_token_encrypted_response_enc: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub userinfo_signed_response_alg: Option, + pub userinfo_signed_response_alg: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub userinfo_encrypted_response_alg: Option, + pub userinfo_encrypted_response_alg: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub userinfo_encrypted_response_enc: Option, + pub userinfo_encrypted_response_enc: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub request_object_signing_alg: Option, + pub request_object_signing_alg: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub request_object_encryption_alg: Option, + pub request_object_encryption_alg: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub request_object_encryption_enc: Option, + pub request_object_encryption_enc: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -106,7 +107,7 @@ pub struct ClientRegistrationRequest { #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub token_endpoint_auth_signing_alg: Option, + pub token_endpoint_auth_signing_alg: Option, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] @@ -118,34 +119,34 @@ pub struct ClientRegistrationRequest { #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] - pub default_acr_values: Vec, + pub default_acr_values: Vec, #[serde(default)] #[serde(skip_serializing_if = "Option::is_none")] - pub initiate_login_uri: Option, + pub initiate_login_uri: Option, #[serde(default)] #[serde(skip_serializing_if = "Vec::is_empty")] - pub request_uris: Vec, + pub request_uris: Vec, #[serde(flatten)] #[serde(skip_serializing_if = "HashMap::is_empty")] - pub additional_fields: HashMap, + pub additional_fields: HashMap, } #[derive(Serialize, Deserialize, Debug, Default)] #[serde(rename_all = "snake_case")] pub struct ClientRegistrationResponse { // Required fields - pub client_id: String, + pub client_id: CompactString, // Optional fields specific to the response #[serde(skip_serializing_if = "Option::is_none")] - pub client_secret: Option, + pub client_secret: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub registration_access_token: Option, + pub registration_access_token: Option, #[serde(skip_serializing_if = "Option::is_none")] - pub registration_client_uri: Option, + pub registration_client_uri: Option, #[serde(skip_serializing_if = "Option::is_none")] pub client_id_issued_at: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/crates/common/src/auth/oauth/token.rs b/crates/common/src/auth/oauth/token.rs index 32316e65..4cdf2b10 100644 --- a/crates/common/src/auth/oauth/token.rs +++ b/crates/common/src/auth/oauth/token.rs @@ -6,7 +6,8 @@ use std::time::SystemTime; -use directory::{QueryBy, backend::internal::PrincipalField}; +use compact_str::CompactString; +use directory::QueryBy; use mail_builder::encoders::base64::base64_encode; use mail_parser::decoders::base64::base64_decode; use store::{ @@ -23,7 +24,7 @@ use super::{CLIENT_ID_MAX_LEN, GrantType, RANDOM_CODE_LEN, crypto::SymmetricEncr pub struct TokenInfo { pub grant_type: GrantType, pub account_id: u32, - pub client_id: String, + pub client_id: CompactString, pub expiry: u64, pub issued_at: u64, pub expires_in: u64, @@ -52,7 +53,7 @@ impl Server { .await .caused_by(trc::location!())? } else { - String::new() + "".into() }; let key = &self.core.oauth.oauth_key; @@ -127,7 +128,7 @@ impl Server { GrantType::from_id(bytes.next().copied()?)?, bytes.next_leb128::()?, bytes.next_leb128::()?, - bytes.copied().map(char::from).collect::(), + bytes.copied().map(char::from).collect::(), ) .into() }) @@ -161,7 +162,7 @@ impl Server { .await .map_err(|err| trc::AuthEvent::Error.into_err().ctx(trc::Key::Details, err))? } else { - String::new() + "".into() }; // Build context @@ -215,7 +216,7 @@ impl Server { }) } - pub async fn password_hash(&self, account_id: u32) -> trc::Result { + pub async fn password_hash(&self, account_id: u32) -> trc::Result { if account_id != u32::MAX { self.core .storage @@ -228,8 +229,7 @@ impl Server { .into_err() .details("Account no longer exists") })? - .take_str_array(PrincipalField::Secrets) - .unwrap_or_default() + .secrets .into_iter() .next() .ok_or( @@ -239,7 +239,7 @@ impl Server { .caused_by(trc::location!()), ) } else if let Some((_, secret)) = &self.core.jmap.fallback_admin { - Ok(secret.clone()) + Ok(secret.into()) } else { Err(trc::AuthEvent::Error .into_err() diff --git a/crates/common/src/auth/roles.rs b/crates/common/src/auth/roles.rs index 642f65be..359fe2c6 100644 --- a/crates/common/src/auth/roles.rs +++ b/crates/common/src/auth/roles.rs @@ -9,7 +9,7 @@ use std::sync::{Arc, LazyLock}; use ahash::AHashSet; use directory::{ Permission, Permissions, QueryBy, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, - backend::internal::{PrincipalField, lookup::DirectoryStore}, + backend::internal::lookup::DirectoryStore, }; use trc::AddContext; use utils::cache::CacheItemWeight; @@ -77,7 +77,7 @@ impl Server { role_id: u32, revision: u64, ) -> trc::Result> { - let mut role_ids = vec![role_id as u64].into_iter(); + let mut role_ids = vec![role_id].into_iter(); let mut role_ids_stack = vec![]; let mut fetched_role_ids = AHashSet::new(); let mut return_permissions = RolePermissions { @@ -87,8 +87,6 @@ impl Server { 'outer: loop { if let Some(role_id) = role_ids.next() { - let role_id = role_id as u32; - // Skip if already fetched if !fetched_role_ids.insert(role_id) { continue; @@ -149,21 +147,11 @@ impl Server { })?; // Add permissions - for (permissions, field) in [ - ( - &mut role_permissions.enabled, - PrincipalField::EnabledPermissions, - ), - ( - &mut role_permissions.disabled, - PrincipalField::DisabledPermissions, - ), - ] { - for permission in principal.iter_int(field) { - let permission = permission as usize; - if permission < Permission::COUNT { - permissions.set(permission); - } + for permission in principal.permissions() { + if permission.grant { + role_permissions.enabled.set(permission.permission.id()); + } else { + role_permissions.disabled.set(permission.permission.id()); } } @@ -171,12 +159,11 @@ impl Server { return_permissions.union(&role_permissions); // Add parent roles - if let Some(parent_role_ids) = principal - .take_int_array(PrincipalField::Roles) - .filter(|r| !r.is_empty()) + if let Some(parent_role_ids) = + principal.roles_mut().filter(|r| !r.is_empty()) { role_ids_stack.push(role_ids); - role_ids = parent_role_ids.into_iter(); + role_ids = std::mem::take(parent_role_ids).into_iter(); } else { // Cache role self.inner diff --git a/crates/common/src/config/network.rs b/crates/common/src/config/network.rs index 392ab4ce..294df513 100644 --- a/crates/common/src/config/network.rs +++ b/crates/common/src/config/network.rs @@ -8,6 +8,7 @@ use std::time::Duration; use crate::expr::{if_block::IfBlock, tokenizer::TokenMap}; use ahash::AHashSet; +use compact_str::CompactString; use utils::config::{Config, Rate}; use super::*; @@ -16,8 +17,8 @@ use super::*; pub struct Network { pub node_id: u64, pub roles: ClusterRoles, - pub server_name: String, - pub report_domain: String, + pub server_name: CompactString, + pub report_domain: CompactString, pub security: Security, pub contact_form: Option, pub http_response_url: IfBlock, @@ -27,14 +28,14 @@ pub struct Network { #[derive(Clone)] pub struct ContactForm { - pub rcpt_to: Vec, + pub rcpt_to: Vec, pub max_size: usize, pub rate: Option, pub validate_domain: bool, pub from_email: FieldOrDefault, pub from_subject: FieldOrDefault, pub from_name: FieldOrDefault, - pub field_honey_pot: Option, + pub field_honey_pot: Option, } #[derive(Clone)] @@ -128,7 +129,7 @@ impl ContactForm { .values("form.deliver-to") .filter_map(|(_, addr)| { if addr.contains('@') && addr.contains('.') { - Some(addr.trim().to_lowercase()) + Some(CompactString::from_str_to_lowercase(addr.trim())) } else { None } @@ -141,7 +142,7 @@ impl ContactForm { from_email: FieldOrDefault::parse(config, "form.email", "postmaster@localhost"), from_subject: FieldOrDefault::parse(config, "form.subject", "Contact form submission"), from_name: FieldOrDefault::parse(config, "form.name", "Anonymous"), - field_honey_pot: config.value("form.honey-pot.field").map(|v| v.to_string()), + field_honey_pot: config.value("form.honey-pot.field").map(|v| v.into()), rate: config .property_or_default::>("form.rate-limit", "5/1h") .unwrap_or_default(), @@ -199,8 +200,8 @@ impl Network { let mut network = Network { node_id: config.property("cluster.node-id").unwrap_or(1), - report_domain, - server_name, + report_domain: report_domain.into(), + server_name: server_name.into(), security: Security::parse(config), contact_form: ContactForm::parse(config), asn_geo_lookup: AsnGeoLookupConfig::parse(config).unwrap_or_default(), diff --git a/crates/common/src/config/smtp/queue.rs b/crates/common/src/config/smtp/queue.rs index 44123a69..401a671c 100644 --- a/crates/common/src/config/smtp/queue.rs +++ b/crates/common/src/config/smtp/queue.rs @@ -636,7 +636,9 @@ impl<'x> TryFrom> for IpLookupStrategy { 5 => Ok(IpLookupStrategy::Ipv4thenIpv6), _ => Err(()), }, - Variable::String(value) => IpLookupStrategy::parse_value(&value).map_err(|_| ()), + Variable::String(value) => { + IpLookupStrategy::parse_value(value.as_str()).map_err(|_| ()) + } _ => Err(()), } } diff --git a/crates/common/src/config/smtp/session.rs b/crates/common/src/config/smtp/session.rs index 9f7365fb..ac5d5889 100644 --- a/crates/common/src/config/smtp/session.rs +++ b/crates/common/src/config/smtp/session.rs @@ -12,6 +12,7 @@ use std::{ use ahash::AHashSet; use base64::{Engine, engine::general_purpose::STANDARD}; +use compact_str::CompactString; use hyper::{ HeaderMap, header::{AUTHORIZATION, CONTENT_TYPE, HeaderName, HeaderValue}, @@ -142,9 +143,9 @@ pub struct Data { #[derive(Clone)] pub struct Milter { pub enable: IfBlock, - pub id: Arc, + pub id: Arc, pub addrs: Vec, - pub hostname: String, + pub hostname: CompactString, pub port: u16, pub timeout_connect: Duration, pub timeout_command: Duration, @@ -461,7 +462,7 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option(format!("session.milter.{id}.enable"), [], "false") }), - id: id.to_string().into(), + id: Arc::new(id.into()), addrs: format!("{}:{}", hostname, port) .to_socket_addrs() .map_err(|err| { @@ -472,7 +473,7 @@ fn parse_milter(config: &mut Config, id: &str, token_map: &TokenMap) -> Option TryFrom> for MtPriority { 4 => Ok(MtPriority::Nsep), _ => Err(()), }, - Variable::String(value) => MtPriority::parse_value(&value).map_err(|_| ()), + Variable::String(value) => MtPriority::parse_value(value.as_str()).map_err(|_| ()), _ => Err(()), } } diff --git a/crates/common/src/config/spamfilter.rs b/crates/common/src/config/spamfilter.rs index 865bb6db..4f387046 100644 --- a/crates/common/src/config/spamfilter.rs +++ b/crates/common/src/config/spamfilter.rs @@ -845,8 +845,8 @@ pub struct IpResolver { impl ResolveVariable for IpResolver { fn resolve_variable(&self, variable: u32) -> Variable<'_> { match variable { - V_IP => Variable::String(self.ip_string.as_str().into()), - V_IP_REVERSE => Variable::String(self.reverse.as_str().into()), + V_IP => self.ip_string.as_str().into(), + V_IP_REVERSE => self.reverse.as_str().into(), V_IP_OCTETS => self.octets.clone(), V_IP_IS_V4 => Variable::Integer(self.ip.is_ipv4() as _), V_IP_IS_V6 => Variable::Integer(self.ip.is_ipv6() as _), diff --git a/crates/common/src/enterprise/mod.rs b/crates/common/src/enterprise/mod.rs index ac7d7ee2..07cfe51b 100644 --- a/crates/common/src/enterprise/mod.rs +++ b/crates/common/src/enterprise/mod.rs @@ -17,10 +17,8 @@ pub mod undelete; use std::{sync::Arc, time::Duration}; use ahash::{AHashMap, AHashSet}; -use directory::{ - QueryBy, Type, - backend::internal::{PrincipalField, lookup::DirectoryStore}, -}; +use compact_str::CompactString; +use directory::{QueryBy, Type, backend::internal::lookup::DirectoryStore}; use license::LicenseKey; use llm::AiApiConfig; use mail_parser::DateTime; @@ -166,21 +164,18 @@ impl Server { .caused_by(trc::location!())? .filter(|p| p.typ() == Type::Domain) { - if let Some(logo) = principal - .take_str(PrincipalField::Picture) - .filter(|l| l.starts_with("http")) - { - logo.into() - } else if let Some(tenant_id) = principal.get_int(PrincipalField::Tenant) { + if let Some(logo) = principal.picture_mut().filter(|l| l.starts_with("http")) { + std::mem::take(logo).into() + } else if let Some(tenant_id) = principal.tenant { if let Some(logo) = self .store() - .query(QueryBy::Id(tenant_id as u32), false) + .query(QueryBy::Id(tenant_id), false) .await .caused_by(trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Picture)) + .and_then(|mut p| p.picture_mut().map(std::mem::take)) .filter(|l| l.starts_with("http")) { - logo.into() + logo.clone().into() } else { self.default_logo_url() } @@ -193,7 +188,7 @@ impl Server { let mut logo = None; if let Some(logo_url) = logo_url { - let response = reqwest::get(&logo_url).await.map_err(|err| { + let response = reqwest::get(logo_url.as_str()).await.map_err(|err| { trc::ResourceEvent::DownloadExternal .into_err() .details("Failed to download logo") @@ -238,10 +233,10 @@ impl Server { } } - fn default_logo_url(&self) -> Option { + fn default_logo_url(&self) -> Option { self.core .enterprise .as_ref() - .and_then(|e| e.logo_url.as_ref().map(|l| l.to_string())) + .and_then(|e| e.logo_url.as_ref().map(|l| l.into())) } } diff --git a/crates/common/src/expr/eval.rs b/crates/common/src/expr/eval.rs index 4d6cf5e2..5eb5853b 100644 --- a/crates/common/src/expr/eval.rs +++ b/crates/common/src/expr/eval.rs @@ -4,15 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Cow, cmp::Ordering, fmt::Display}; +use std::{cmp::Ordering, fmt::Display}; +use compact_str::{CompactString, ToCompactString, format_compact}; use hyper::StatusCode; use trc::EvalEvent; use crate::Server; use super::{ - BinaryOperator, Constant, Expression, ExpressionItem, Setting, UnaryOperator, Variable, + BinaryOperator, Constant, Expression, ExpressionItem, Setting, StringCow, UnaryOperator, + Variable, functions::{FUNCTIONS, ResolveVariable}, if_block::IfBlock, }; @@ -105,7 +107,7 @@ impl Server { trc::event!( Eval(EvalEvent::Result), SpanId = session_id, - Id = expr_id.to_string(), + Id = expr_id.to_compact_string(), Result = format!("{result:?}"), ); @@ -115,7 +117,7 @@ impl Server { trc::event!( Eval(EvalEvent::Error), SpanId = session_id, - Id = expr_id.to_string(), + Id = expr_id.to_compact_string(), Details = "Failed to convert result", ); @@ -127,7 +129,7 @@ impl Server { trc::event!( Eval(EvalEvent::Error), SpanId = session_id, - Id = expr_id.to_string(), + Id = expr_id.to_compact_string(), CausedBy = err, ); @@ -145,7 +147,7 @@ struct EvalContext<'x, V: ResolveVariable, T, C> { session_id: u64, } -impl<'x, V: ResolveVariable> EvalContext<'x, V, IfBlock, Vec> { +impl<'x, V: ResolveVariable> EvalContext<'x, V, IfBlock, Vec> { async fn eval(&mut self) -> trc::Result> { for if_then in &self.expr.if_then { if (EvalContext { @@ -183,7 +185,7 @@ impl<'x, V: ResolveVariable> EvalContext<'x, V, IfBlock, Vec> { } } -impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec> { +impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec> { async fn eval(&mut self) -> trc::Result> { let mut stack = Vec::new(); let mut exprs = self.expr.items.iter(); @@ -200,12 +202,12 @@ impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec> { stack.push(Variable::from(val)); } ExpressionItem::Capture(v) => { - stack.push(Variable::String(Cow::Owned( + stack.push(Variable::String(StringCow::Owned( self.captures .get(*v as usize) .map(|v| v.as_str()) .unwrap_or_default() - .to_string(), + .to_compact_string(), ))); } ExpressionItem::Setting(setting) => match setting { @@ -224,6 +226,7 @@ impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec> { .get(key) .await? .unwrap_or_default() + .to_compact_string() .into(), ), }, @@ -305,7 +308,7 @@ impl<'x, V: ResolveVariable> EvalContext<'x, V, Expression, &mut Vec> { if let Some(captures_) = regex.captures(value.as_ref()) { for capture in captures_.iter() { self.captures - .push(capture.map_or("", |m| m.as_str()).to_string()); + .push(capture.map_or("", |m| m.as_str()).to_compact_string()); } } @@ -346,14 +349,14 @@ impl<'x> Variable<'x> { } (Variable::String(a), b) => { if !a.is_empty() { - Variable::String(format!("{}{}", a, b).into()) + Variable::String(StringCow::Owned(format_compact!("{}{}", a, b))) } else { b } } (a, Variable::String(b)) => { if !b.is_empty() { - Variable::String(format!("{}{}", a, b).into()) + Variable::String(StringCow::Owned(format_compact!("{}{}", a, b))) } else { a } @@ -453,9 +456,9 @@ impl<'x> Variable<'x> { pub fn parse_number(&self) -> Variable<'static> { match self { Variable::String(s) if !s.is_empty() => { - if let Ok(n) = s.parse::() { + if let Ok(n) = s.as_str().parse::() { Variable::Integer(n) - } else if let Ok(n) = s.parse::() { + } else if let Ok(n) = s.as_str().parse::() { Variable::Float(n) } else { Variable::Integer(0) @@ -479,7 +482,7 @@ impl<'x> Variable<'x> { pub fn to_ref<'y: 'x>(&'y self) -> Variable<'x> { match self { - Variable::String(s) => Variable::String(Cow::Borrowed(s.as_ref())), + Variable::String(s) => Variable::String(StringCow::Borrowed(s.as_str())), Variable::Integer(n) => Variable::Integer(*n), Variable::Float(n) => Variable::Float(*n), Variable::Array(l) => Variable::Array(l.iter().map(|v| v.to_ref()).collect::>()), @@ -495,48 +498,48 @@ impl<'x> Variable<'x> { } } - pub fn to_string(&self) -> Cow<'_, str> { + pub fn to_string(&self) -> StringCow { match self { - Variable::String(s) => Cow::Borrowed(s.as_ref()), - Variable::Integer(n) => Cow::Owned(n.to_string()), - Variable::Float(n) => Cow::Owned(n.to_string()), + Variable::String(s) => StringCow::Borrowed(s.as_str()), + Variable::Integer(n) => StringCow::Owned(n.to_compact_string()), + Variable::Float(n) => StringCow::Owned(n.to_compact_string()), Variable::Array(l) => { - let mut result = String::with_capacity(self.len() * 10); + let mut result = CompactString::with_capacity(self.len() * 10); for item in l { if !result.is_empty() { result.push_str("\r\n"); } match item { - Variable::String(v) => result.push_str(v), - Variable::Integer(v) => result.push_str(&v.to_string()), - Variable::Float(v) => result.push_str(&v.to_string()), + Variable::String(v) => result.push_str(v.as_str()), + Variable::Integer(v) => result.push_str(&v.to_compact_string()), + Variable::Float(v) => result.push_str(&v.to_compact_string()), Variable::Array(_) => {} } } - Cow::Owned(result) + StringCow::Owned(result) } } } - pub fn into_string(self) -> Cow<'x, str> { + pub fn into_string(self) -> StringCow<'x> { match self { Variable::String(s) => s, - Variable::Integer(n) => Cow::Owned(n.to_string()), - Variable::Float(n) => Cow::Owned(n.to_string()), + Variable::Integer(n) => StringCow::Owned(n.to_compact_string()), + Variable::Float(n) => StringCow::Owned(n.to_compact_string()), Variable::Array(l) => { - let mut result = String::with_capacity(l.len() * 10); + let mut result = CompactString::with_capacity(l.len() * 10); for item in l { if !result.is_empty() { result.push_str("\r\n"); } match item { Variable::String(v) => result.push_str(v.as_ref()), - Variable::Integer(v) => result.push_str(&v.to_string()), - Variable::Float(v) => result.push_str(&v.to_string()), + Variable::Integer(v) => result.push_str(&v.to_compact_string()), + Variable::Float(v) => result.push_str(&v.to_compact_string()), Variable::Array(_) => {} } } - Cow::Owned(result) + StringCow::Owned(result) } } } @@ -545,7 +548,7 @@ impl<'x> Variable<'x> { match self { Variable::Integer(n) => Some(*n), Variable::Float(n) => Some(*n as i64), - Variable::String(s) if !s.is_empty() => s.parse::().ok(), + Variable::String(s) if !s.is_empty() => s.as_str().parse::().ok(), _ => None, } } @@ -554,7 +557,7 @@ impl<'x> Variable<'x> { match self { Variable::Integer(n) => Some(*n as usize), Variable::Float(n) => Some(*n as usize), - Variable::String(s) if !s.is_empty() => s.parse::().ok(), + Variable::String(s) if !s.is_empty() => s.as_str().parse::().ok(), _ => None, } } @@ -599,7 +602,7 @@ impl<'x> Variable<'x> { pub fn into_owned(self) -> Variable<'static> { match self { - Variable::String(s) => Variable::String(Cow::Owned(s.into_owned())), + Variable::String(s) => Variable::String(StringCow::Owned(s.into_owned())), Variable::Integer(n) => Variable::Integer(n), Variable::Float(n) => Variable::Float(n), Variable::Array(l) => Variable::Array(l.into_iter().map(|v| v.into_owned()).collect()), @@ -615,7 +618,7 @@ impl PartialEq for Variable<'_> { (Self::Integer(a), Self::Float(b)) | (Self::Float(b), Self::Integer(a)) => { *a as f64 == *b } - (Self::String(a), Self::String(b)) => a == b, + (Self::String(a), Self::String(b)) => a.as_str() == b.as_str(), (Self::String(_), Self::Integer(_) | Self::Float(_)) => &self.parse_number() == other, (Self::Integer(_) | Self::Float(_), Self::String(_)) => self == &other.parse_number(), (Self::Array(a), Self::Array(b)) => a == b, @@ -634,7 +637,7 @@ impl PartialOrd for Variable<'_> { (Self::Float(a), Self::Float(b)) => a.partial_cmp(b), (Self::Integer(a), Self::Float(b)) => (*a as f64).partial_cmp(b), (Self::Float(a), Self::Integer(b)) => a.partial_cmp(&(*b as f64)), - (Self::String(a), Self::String(b)) => a.partial_cmp(b), + (Self::String(a), Self::String(b)) => a.as_str().partial_cmp(b.as_str()), (Self::String(_), Self::Integer(_) | Self::Float(_)) => { self.parse_number().partial_cmp(other) } @@ -678,17 +681,20 @@ impl<'x> From<&'x Constant> for Variable<'x> { match value { Constant::Integer(i) => Variable::Integer(*i), Constant::Float(f) => Variable::Float(*f), - Constant::String(s) => Variable::String(s.as_str().into()), + Constant::String(s) => Variable::String(StringCow::Borrowed(s.as_str())), } } } -impl<'x> TryFrom> for String { +impl<'x> TryFrom> for CompactString { type Error = (); fn try_from(value: Variable<'x>) -> Result { if let Variable::String(s) = value { - Ok(s.into_owned()) + Ok(match s { + StringCow::Borrowed(v) => v.into(), + StringCow::Owned(v) => v, + }) } else { Err(()) } diff --git a/crates/common/src/expr/functions/asynch.rs b/crates/common/src/expr/functions/asynch.rs index c44be31f..68970054 100644 --- a/crates/common/src/expr/functions/asynch.rs +++ b/crates/common/src/expr/functions/asynch.rs @@ -6,12 +6,13 @@ use std::{cmp::Ordering, net::IpAddr, vec::IntoIter}; +use compact_str::{CompactString, ToCompactString}; use directory::backend::RcptType; use mail_auth::IpLookupStrategy; use store::{Deserialize, Rows, Value, dispatch::lookup::KeyValue}; use trc::AddContext; -use crate::Server; +use crate::{Server, expr::StringCow}; use super::*; @@ -49,8 +50,8 @@ impl Server { let store = params.next_as_string(); let key = params.next_as_string(); - self.get_in_memory_store_or_default(store.as_ref(), session_id) - .key_get::(key) + self.get_in_memory_store_or_default(store.as_str(), session_id) + .key_get::(key.as_str()) .await .map(|value| value.map(|v| v.into_inner()).unwrap_or_default()) .caused_by(trc::location!()) @@ -59,8 +60,8 @@ impl Server { let store = params.next_as_string(); let key = params.next_as_string(); - self.get_in_memory_store_or_default(store.as_ref(), session_id) - .key_exists(key) + self.get_in_memory_store_or_default(store.as_str(), session_id) + .key_exists(key.as_str()) .await .caused_by(trc::location!()) .map(|v| v.into()) @@ -72,8 +73,8 @@ impl Server { self.get_in_memory_store_or_default(store.as_ref(), session_id) .key_set(KeyValue::new( - key.into_owned().into_bytes(), - value.into_owned().into_bytes(), + key.as_bytes().to_vec(), + value.as_bytes().to_vec(), )) .await .map(|_| true) @@ -96,7 +97,7 @@ impl Server { let key = params.next_as_string(); self.get_in_memory_store_or_default(store.as_ref(), session_id) - .counter_get(key.into_owned().into_bytes()) + .counter_get(key.as_bytes().to_vec()) .await .map(Variable::Integer) .caused_by(trc::location!()) @@ -134,7 +135,7 @@ impl Server { .is_some_and(|q| q.eq_ignore_ascii_case(b"SELECT")) { let mut rows = store - .sql_query::(&query, arguments) + .sql_query::(query.as_str(), arguments) .await .caused_by(trc::location!())?; Ok(match rows.rows.len().cmp(&1) { @@ -162,7 +163,7 @@ impl Server { }) } else { store - .sql_query::(&query, arguments) + .sql_query::(query.as_str(), arguments) .await .caused_by(trc::location!()) .map(|v| v.into()) @@ -173,7 +174,7 @@ impl Server { let entry = arguments.next_as_string(); let record_type = arguments.next_as_string(); - if record_type.eq_ignore_ascii_case("ip") { + if record_type.as_str().eq_ignore_ascii_case("ip") { self.core .smtp .resolvers @@ -190,16 +191,16 @@ impl Server { .map(|result| { result .iter() - .map(|ip| Variable::from(ip.to_string())) + .map(|ip| Variable::from(ip.to_compact_string())) .collect::>() .into() }) - } else if record_type.eq_ignore_ascii_case("mx") { + } else if record_type.as_str().eq_ignore_ascii_case("mx") { self.core .smtp .resolvers .dns - .mx_lookup(entry.as_ref(), Some(&self.inner.cache.dns_mx)) + .mx_lookup(entry.as_str(), Some(&self.inner.cache.dns_mx)) .await .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) .map(|result| { @@ -207,33 +208,32 @@ impl Server { .iter() .flat_map(|mx| { mx.exchanges.iter().map(|host| { - Variable::String( + Variable::String(StringCow::Owned( host.strip_suffix('.') .unwrap_or(host.as_str()) - .to_string() - .into(), - ) + .to_compact_string(), + )) }) }) .collect::>() .into() }) - } else if record_type.eq_ignore_ascii_case("txt") { + } else if record_type.as_str().eq_ignore_ascii_case("txt") { self.core .smtp .resolvers .dns - .txt_raw_lookup(entry.as_ref()) + .txt_raw_lookup(entry.as_str()) .await .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) - .map(|result| Variable::from(String::from_utf8(result).unwrap_or_default())) - } else if record_type.eq_ignore_ascii_case("ptr") { + .map(|result| Variable::from(CompactString::from_utf8(result).unwrap_or_default())) + } else if record_type.as_str().eq_ignore_ascii_case("ptr") { self.core .smtp .resolvers .dns .ptr_lookup( - entry.parse::().map_err(|err| { + entry.as_str().parse::().map_err(|err| { trc::EventType::Eval(trc::EvalEvent::Error) .into_err() .details("Failed to parse IP address") @@ -246,37 +246,37 @@ impl Server { .map(|result| { result .iter() - .map(|host| Variable::from(host.to_string())) + .map(|host| Variable::from(host.to_compact_string())) .collect::>() .into() }) - } else if record_type.eq_ignore_ascii_case("ipv4") { + } else if record_type.as_str().eq_ignore_ascii_case("ipv4") { self.core .smtp .resolvers .dns - .ipv4_lookup(entry.as_ref(), Some(&self.inner.cache.dns_ipv4)) + .ipv4_lookup(entry.as_str(), Some(&self.inner.cache.dns_ipv4)) .await .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) .map(|result| { result .iter() - .map(|ip| Variable::from(ip.to_string())) + .map(|ip| Variable::from(ip.to_compact_string())) .collect::>() .into() }) - } else if record_type.eq_ignore_ascii_case("ipv6") { + } else if record_type.as_str().eq_ignore_ascii_case("ipv6") { self.core .smtp .resolvers .dns - .ipv6_lookup(entry.as_ref(), Some(&self.inner.cache.dns_ipv6)) + .ipv6_lookup(entry.as_str(), Some(&self.inner.cache.dns_ipv6)) .await .map_err(|err| trc::Error::from(err).caused_by(trc::location!())) .map(|result| { result .iter() - .map(|ip| Variable::from(ip.to_string())) + .map(|ip| Variable::from(ip.to_compact_string())) .collect::>() .into() }) @@ -297,7 +297,7 @@ impl<'x> FncParams<'x> { } } - pub fn next_as_string(&mut self) -> Cow<'x, str> { + pub fn next_as_string(&mut self) -> StringCow<'x> { self.params.next().unwrap().into_string() } @@ -321,7 +321,9 @@ impl From for VariableWrapper { impl Deserialize for VariableWrapper { fn deserialize(bytes: &[u8]) -> trc::Result { - String::deserialize(bytes).map(|v| VariableWrapper(Variable::String(v.into()))) + Ok(VariableWrapper(Variable::String(StringCow::Owned( + CompactString::from_utf8_lossy(bytes), + )))) } } @@ -331,12 +333,12 @@ impl From> for VariableWrapper { Value::Integer(v) => Variable::Integer(v), Value::Bool(v) => Variable::Integer(v as i64), Value::Float(v) => Variable::Float(v), - Value::Text(v) => Variable::String(v), - Value::Blob(v) => Variable::String(match v { - std::borrow::Cow::Borrowed(v) => String::from_utf8_lossy(v), - std::borrow::Cow::Owned(v) => String::from_utf8_lossy(&v).into_owned().into(), - }), - Value::Null => Variable::String("".into()), + Value::Text(v) => Variable::String(StringCow::Owned(v.into())), + Value::Blob(v) => Variable::String(StringCow::Owned(match v { + std::borrow::Cow::Borrowed(v) => CompactString::from_utf8_lossy(v), + std::borrow::Cow::Owned(v) => CompactString::from_utf8_lossy(&v), + })), + Value::Null => Variable::String(StringCow::Borrowed("")), }) } } @@ -349,7 +351,7 @@ impl VariableWrapper { fn to_store_value(value: Variable) -> Value { match value { - Variable::String(v) => Value::Text(v), + Variable::String(v) => Value::Text(v.to_string().into()), Variable::Integer(v) => Value::Integer(v), Variable::Float(v) => Value::Float(v), v => Value::Text(v.to_string().into_owned().into()), @@ -361,12 +363,8 @@ fn into_variable(value: Value) -> Variable { Value::Integer(v) => Variable::Integer(v), Value::Bool(v) => Variable::Integer(i64::from(v)), Value::Float(v) => Variable::Float(v), - Value::Text(v) => Variable::String(v), - Value::Blob(v) => Variable::String( - String::from_utf8(v.into_owned()) - .unwrap_or_else(|err| String::from_utf8_lossy(err.as_bytes()).into_owned()) - .into(), - ), + Value::Text(v) => Variable::String(v.into()), + Value::Blob(v) => Variable::String(StringCow::Owned(CompactString::from_utf8_lossy(&v))), Value::Null => Variable::default(), } } diff --git a/crates/common/src/expr/functions/email.rs b/crates/common/src/expr/functions/email.rs index 072eb5fe..be12861e 100644 --- a/crates/common/src/expr/functions/email.rs +++ b/crates/common/src/expr/functions/email.rs @@ -4,9 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; +use compact_str::CompactString; -use crate::expr::Variable; +use crate::expr::{StringCow, Variable}; pub(crate) fn fn_is_email(v: Vec) -> Variable { let mut last_ch = 0; @@ -16,7 +16,7 @@ pub(crate) fn fn_is_email(v: Vec) -> Variable { let mut lp_len = 0; let mut value = 0; - for ch in v[0].to_string().bytes() { + for &ch in v[0].to_string().as_bytes() { match ch { b'0'..=b'9' | b'a'..=b'z' @@ -84,19 +84,19 @@ pub(crate) fn fn_email_part(v: Vec) -> Variable { let part = v.next().unwrap().into_string(); value.transform(|s| match s { - Cow::Borrowed(s) => s + StringCow::Borrowed(s) => s .rsplit_once('@') - .map(|(u, d)| match part.as_ref() { + .map(|(u, d)| match part.as_str() { "local" => Variable::from(u.trim()), "domain" => Variable::from(d.trim()), _ => Variable::default(), }) .unwrap_or_default(), - Cow::Owned(s) => s + StringCow::Owned(s) => s .rsplit_once('@') - .map(|(u, d)| match part.as_ref() { - "local" => Variable::from(u.trim().to_string()), - "domain" => Variable::from(d.trim().to_string()), + .map(|(u, d)| match part.as_str() { + "local" => Variable::from(CompactString::new(u.trim())), + "domain" => Variable::from(CompactString::new(d.trim())), _ => Variable::default(), }) .unwrap_or_default(), diff --git a/crates/common/src/expr/functions/misc.rs b/crates/common/src/expr/functions/misc.rs index 13cfac3c..300cf457 100644 --- a/crates/common/src/expr/functions/misc.rs +++ b/crates/common/src/expr/functions/misc.rs @@ -6,6 +6,7 @@ use std::net::IpAddr; +use compact_str::CompactString; use mail_auth::common::resolver::ToReverseName; use crate::expr::Variable; @@ -24,11 +25,16 @@ pub(crate) fn fn_is_number(v: Vec) -> Variable { } pub(crate) fn fn_is_ip_addr(v: Vec) -> Variable { - v[0].to_string().parse::().is_ok().into() + v[0].to_string() + .as_str() + .parse::() + .is_ok() + .into() } pub(crate) fn fn_is_ipv4_addr(v: Vec) -> Variable { v[0].to_string() + .as_str() .parse::() .is_ok_and(|ip| matches!(ip, IpAddr::V4(_))) .into() @@ -36,17 +42,21 @@ pub(crate) fn fn_is_ipv4_addr(v: Vec) -> Variable { pub(crate) fn fn_is_ipv6_addr(v: Vec) -> Variable { v[0].to_string() + .as_str() .parse::() .is_ok_and(|ip| matches!(ip, IpAddr::V6(_))) .into() } pub(crate) fn fn_ip_reverse_name(v: Vec) -> Variable { - v[0].to_string() - .parse::() - .map(|ip| ip.to_reverse_name()) - .unwrap_or_default() - .into() + CompactString::new( + v[0].to_string() + .as_str() + .parse::() + .map(|ip| ip.to_reverse_name()) + .unwrap_or_default(), + ) + .into() } pub(crate) fn fn_if_then(v: Vec) -> Variable { diff --git a/crates/common/src/expr/functions/mod.rs b/crates/common/src/expr/functions/mod.rs index b547f6e2..cc80b77e 100644 --- a/crates/common/src/expr/functions/mod.rs +++ b/crates/common/src/expr/functions/mod.rs @@ -4,9 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; - -use super::Variable; +use super::{StringCow, Variable}; pub mod array; pub mod asynch; @@ -20,7 +18,7 @@ pub trait ResolveVariable: Sync + Send { } impl<'x> Variable<'x> { - fn transform(self, f: impl Fn(Cow<'x, str>) -> Variable<'x>) -> Variable<'x> { + fn transform(self, f: impl Fn(StringCow<'x>) -> Variable<'x>) -> Variable<'x> { match self { Variable::String(s) => f(s), Variable::Array(list) => Variable::Array( diff --git a/crates/common/src/expr/functions/text.rs b/crates/common/src/expr/functions/text.rs index eccaafee..ab965ee7 100644 --- a/crates/common/src/expr/functions/text.rs +++ b/crates/common/src/expr/functions/text.rs @@ -4,31 +4,30 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; - +use compact_str::{CompactString, ToCompactString, format_compact}; use sha1::Sha1; use sha2::{Sha256, Sha512}; -use crate::expr::Variable; +use crate::expr::{StringCow, Variable}; pub(crate) fn fn_trim(mut v: Vec) -> Variable { v.remove(0).transform(|s| match s { - Cow::Borrowed(s) => Variable::from(s.trim()), - Cow::Owned(s) => Variable::from(s.trim().to_string()), + StringCow::Borrowed(s) => Variable::from(s.trim()), + StringCow::Owned(s) => Variable::from(s.trim().to_compact_string()), }) } pub(crate) fn fn_trim_end(mut v: Vec) -> Variable { v.remove(0).transform(|s| match s { - Cow::Borrowed(s) => Variable::from(s.trim_end()), - Cow::Owned(s) => Variable::from(s.trim_end().to_string()), + StringCow::Borrowed(s) => Variable::from(s.trim_end()), + StringCow::Owned(s) => Variable::from(s.trim_end().to_compact_string()), }) } pub(crate) fn fn_trim_start(mut v: Vec) -> Variable { v.remove(0).transform(|s| match s { - Cow::Borrowed(s) => Variable::from(s.trim_start()), - Cow::Owned(s) => Variable::from(s.trim_start().to_string()), + StringCow::Borrowed(s) => Variable::from(s.trim_start()), + StringCow::Owned(s) => Variable::from(s.trim_start().to_compact_string()), }) } @@ -42,16 +41,19 @@ pub(crate) fn fn_len(v: Vec) -> Variable { } pub(crate) fn fn_to_lowercase(mut v: Vec) -> Variable { - v.remove(0).transform(|s| Variable::from(s.to_lowercase())) + v.remove(0) + .transform(|s| Variable::from(CompactString::from_str_to_lowercase(s.as_str()))) } pub(crate) fn fn_to_uppercase(mut v: Vec) -> Variable { - v.remove(0).transform(|s| Variable::from(s.to_uppercase())) + v.remove(0) + .transform(|s| Variable::from(CompactString::from_str_to_uppercase(s.as_str()))) } pub(crate) fn fn_is_uppercase(mut v: Vec) -> Variable { v.remove(0).transform(|s| { - s.chars() + s.as_str() + .chars() .filter(|c| c.is_alphabetic()) .all(|c| c.is_uppercase()) .into() @@ -60,7 +62,8 @@ pub(crate) fn fn_is_uppercase(mut v: Vec) -> Variable { pub(crate) fn fn_is_lowercase(mut v: Vec) -> Variable { v.remove(0).transform(|s| { - s.chars() + s.as_str() + .chars() .filter(|c| c.is_alphabetic()) .all(|c| c.is_lowercase()) .into() @@ -69,21 +72,22 @@ pub(crate) fn fn_is_lowercase(mut v: Vec) -> Variable { pub(crate) fn fn_has_digits(mut v: Vec) -> Variable { v.remove(0) - .transform(|s| s.chars().any(|c| c.is_ascii_digit()).into()) + .transform(|s| s.as_str().chars().any(|c| c.is_ascii_digit()).into()) } pub(crate) fn fn_split_words(v: Vec) -> Variable { v[0].to_string() + .as_str() .split_whitespace() .filter(|word| word.chars().all(|c| c.is_alphanumeric())) - .map(|word| Variable::from(word.to_string())) + .map(|word| Variable::from(CompactString::new(word))) .collect::>() .into() } pub(crate) fn fn_count_spaces(v: Vec) -> Variable { v[0].to_string() - .as_ref() + .as_str() .chars() .filter(|c| c.is_whitespace()) .count() @@ -92,7 +96,7 @@ pub(crate) fn fn_count_spaces(v: Vec) -> Variable { pub(crate) fn fn_count_uppercase(v: Vec) -> Variable { v[0].to_string() - .as_ref() + .as_str() .chars() .filter(|c| c.is_alphabetic() && c.is_uppercase()) .count() @@ -101,7 +105,7 @@ pub(crate) fn fn_count_uppercase(v: Vec) -> Variable { pub(crate) fn fn_count_lowercase(v: Vec) -> Variable { v[0].to_string() - .as_ref() + .as_str() .chars() .filter(|c| c.is_alphabetic() && c.is_lowercase()) .count() @@ -109,20 +113,21 @@ pub(crate) fn fn_count_lowercase(v: Vec) -> Variable { } pub(crate) fn fn_count_chars(v: Vec) -> Variable { - v[0].to_string().as_ref().chars().count().into() + v[0].to_string().as_str().chars().count().into() } pub(crate) fn fn_eq_ignore_case(v: Vec) -> Variable { v[0].to_string() - .eq_ignore_ascii_case(v[1].to_string().as_ref()) + .as_str() + .eq_ignore_ascii_case(v[1].to_string().as_str()) .into() } pub(crate) fn fn_contains(v: Vec) -> Variable { match &v[0] { - Variable::String(s) => s.contains(v[1].to_string().as_ref()), + Variable::String(s) => s.as_str().contains(v[1].to_string().as_str()), Variable::Array(arr) => arr.contains(&v[1]), - val => val.to_string().contains(v[1].to_string().as_ref()), + val => val.to_string().as_str().contains(v[1].to_string().as_str()), } .into() } @@ -130,31 +135,39 @@ pub(crate) fn fn_contains(v: Vec) -> Variable { pub(crate) fn fn_contains_ignore_case(v: Vec) -> Variable { let needle = v[1].to_string(); match &v[0] { - Variable::String(s) => s.to_lowercase().contains(&needle.to_lowercase()), + Variable::String(s) => s + .as_str() + .to_lowercase() + .contains(&needle.as_str().to_lowercase()), Variable::Array(arr) => arr.iter().any(|v| match v { - Variable::String(s) => s.eq_ignore_ascii_case(needle.as_ref()), + Variable::String(s) => s.as_str().eq_ignore_ascii_case(needle.as_str()), _ => false, }), - val => val.to_string().contains(needle.as_ref()), + val => val.to_string().as_str().contains(needle.as_str()), } .into() } pub(crate) fn fn_starts_with(v: Vec) -> Variable { v[0].to_string() - .starts_with(v[1].to_string().as_ref()) + .as_str() + .starts_with(v[1].to_string().as_str()) .into() } pub(crate) fn fn_ends_with(v: Vec) -> Variable { - v[0].to_string().ends_with(v[1].to_string().as_ref()).into() + v[0].to_string() + .as_str() + .ends_with(v[1].to_string().as_str()) + .into() } pub(crate) fn fn_lines(mut v: Vec) -> Variable { match v.remove(0) { Variable::String(s) => s + .as_str() .lines() - .map(|s| Variable::from(s.to_string())) + .map(|s| Variable::from(CompactString::new(s))) .collect::>() .into(), val => val, @@ -163,10 +176,11 @@ pub(crate) fn fn_lines(mut v: Vec) -> Variable { pub(crate) fn fn_substring(v: Vec) -> Variable { v[0].to_string() + .as_str() .chars() .skip(v[1].to_usize().unwrap_or_default()) .take(v[2].to_usize().unwrap_or_default()) - .collect::() + .collect::() .into() } @@ -176,13 +190,13 @@ pub(crate) fn fn_strip_prefix(v: Vec) -> Variable { let prefix = v.next().unwrap().into_string(); value.transform(|s| match s { - Cow::Borrowed(s) => s - .strip_prefix(prefix.as_ref()) + StringCow::Borrowed(s) => s + .strip_prefix(prefix.as_str()) .map(Variable::from) .unwrap_or_default(), - Cow::Owned(s) => s - .strip_prefix(prefix.as_ref()) - .map(|s| Variable::from(s.to_string())) + StringCow::Owned(s) => s + .strip_prefix(prefix.as_str()) + .map(|s| Variable::from(CompactString::new(s))) .unwrap_or_default(), }) } @@ -193,13 +207,13 @@ pub(crate) fn fn_strip_suffix(v: Vec) -> Variable { let suffix = v.next().unwrap().into_string(); value.transform(|s| match s { - Cow::Borrowed(s) => s - .strip_suffix(suffix.as_ref()) + StringCow::Borrowed(s) => s + .strip_suffix(suffix.as_str()) .map(Variable::from) .unwrap_or_default(), - Cow::Owned(s) => s - .strip_suffix(suffix.as_ref()) - .map(|s| Variable::from(s.to_string())) + StringCow::Owned(s) => s + .strip_suffix(suffix.as_str()) + .map(|s| Variable::from(CompactString::new(s))) .unwrap_or_default(), }) } @@ -210,14 +224,14 @@ pub(crate) fn fn_split(v: Vec) -> Variable { let arg = v.next().unwrap().into_string(); match value { - Cow::Borrowed(s) => s - .split(arg.as_ref()) + StringCow::Borrowed(s) => s + .split(arg.as_str()) .map(Variable::from) .collect::>() .into(), - Cow::Owned(s) => s - .split(arg.as_ref()) - .map(|s| Variable::from(s.to_string())) + StringCow::Owned(s) => s + .split(arg.as_str()) + .map(|s| Variable::from(CompactString::new(s))) .collect::>() .into(), } @@ -229,14 +243,14 @@ pub(crate) fn fn_rsplit(v: Vec) -> Variable { let arg = v.next().unwrap().into_string(); match value { - Cow::Borrowed(s) => s - .rsplit(arg.as_ref()) + StringCow::Borrowed(s) => s + .rsplit(arg.as_str()) .map(Variable::from) .collect::>() .into(), - Cow::Owned(s) => s - .rsplit(arg.as_ref()) - .map(|s| Variable::from(s.to_string())) + StringCow::Owned(s) => s + .rsplit(arg.as_str()) + .map(|s| Variable::from(CompactString::new(s))) .collect::>() .into(), } @@ -263,9 +277,9 @@ pub(crate) fn fn_split_n(v: Vec) -> Variable { let mut result = Vec::new(); match value { - Cow::Borrowed(s) => split_n(s, arg.as_ref(), num, |s| result.push(Variable::from(s))), - Cow::Owned(s) => split_n(&s, arg.as_ref(), num, |s| { - result.push(Variable::from(s.to_string())) + StringCow::Borrowed(s) => split_n(s, arg.as_str(), num, |s| result.push(Variable::from(s))), + StringCow::Owned(s) => split_n(&s, arg.as_str(), num, |s| { + result.push(Variable::from(CompactString::new(s))) }), } @@ -278,16 +292,16 @@ pub(crate) fn fn_split_once(v: Vec) -> Variable { let arg = v.next().unwrap().into_string(); match value { - Cow::Borrowed(s) => s - .split_once(arg.as_ref()) + StringCow::Borrowed(s) => s + .split_once(arg.as_str()) .map(|(a, b)| Variable::Array(vec![Variable::from(a), Variable::from(b)])) .unwrap_or_default(), - Cow::Owned(s) => s - .split_once(arg.as_ref()) + StringCow::Owned(s) => s + .split_once(arg.as_str()) .map(|(a, b)| { Variable::Array(vec![ - Variable::from(a.to_string()), - Variable::from(b.to_string()), + Variable::from(CompactString::new(a)), + Variable::from(CompactString::new(b)), ]) }) .unwrap_or_default(), @@ -300,16 +314,16 @@ pub(crate) fn fn_rsplit_once(v: Vec) -> Variable { let arg = v.next().unwrap().into_string(); match value { - Cow::Borrowed(s) => s - .rsplit_once(arg.as_ref()) + StringCow::Borrowed(s) => s + .rsplit_once(arg.as_str()) .map(|(a, b)| Variable::Array(vec![Variable::from(a), Variable::from(b)])) .unwrap_or_default(), - Cow::Owned(s) => s - .rsplit_once(arg.as_ref()) + StringCow::Owned(s) => s + .rsplit_once(arg.as_str()) .map(|(a, b)| { Variable::Array(vec![ - Variable::from(a.to_string()), - Variable::from(b.to_string()), + Variable::from(CompactString::new(a)), + Variable::from(CompactString::new(b)), ]) }) .unwrap_or_default(), @@ -322,22 +336,22 @@ pub(crate) fn fn_hash(v: Vec) -> Variable { let value = v.next().unwrap().into_string(); let algo = v.next().unwrap().into_string(); - match algo.as_ref() { - "md5" => format!("{:x}", md5::compute(value.as_bytes())).into(), + match algo.as_str() { + "md5" => format_compact!("{:x}", md5::compute(value.as_bytes())).into(), "sha1" => { let mut hasher = Sha1::new(); hasher.update(value.as_bytes()); - format!("{:x}", hasher.finalize()).into() + format_compact!("{:x}", hasher.finalize()).into() } "sha256" => { let mut hasher = Sha256::new(); hasher.update(value.as_bytes()); - format!("{:x}", hasher.finalize()).into() + format_compact!("{:x}", hasher.finalize()).into() } "sha512" => { let mut hasher = Sha512::new(); hasher.update(value.as_bytes()); - format!("{:x}", hasher.finalize()).into() + format_compact!("{:x}", hasher.finalize()).into() } _ => Variable::default(), } diff --git a/crates/common/src/expr/if_block.rs b/crates/common/src/expr/if_block.rs index be643129..ae91cc6f 100644 --- a/crates/common/src/expr/if_block.rs +++ b/crates/common/src/expr/if_block.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use utils::config::{Config, utils::AsKey}; use crate::expr::{Constant, Expression}; @@ -22,14 +23,14 @@ pub struct IfThen { #[derive(Debug, Clone, PartialEq, Eq)] pub struct IfBlock { - pub key: String, + pub key: CompactString, pub if_then: Vec, pub default: Expression, } impl IfBlock { pub fn new( - key: impl Into, + key: impl Into, if_thens: impl IntoIterator, default: impl AsRef, ) -> Self { @@ -50,7 +51,7 @@ impl IfBlock { } } - pub fn empty(key: impl Into) -> Self { + pub fn empty(key: impl Into) -> Self { Self { key: key.into(), if_then: Default::default(), @@ -101,7 +102,7 @@ impl IfBlock { // Parse conditions let mut if_block = IfBlock { - key, + key: key.into(), if_then: Default::default(), default: Expression { items: Default::default(), @@ -213,7 +214,7 @@ impl IfBlock { } } - pub fn into_default(self, key: impl Into) -> IfBlock { + pub fn into_default(self, key: impl Into) -> IfBlock { IfBlock { key: key.into(), if_then: Default::default(), @@ -231,7 +232,7 @@ impl IfBlock { None } - pub fn into_default_string(self) -> Option { + pub fn into_default_string(self) -> Option { for expr_item in self.default.items { if let ExpressionItem::Constant(Constant::String(value)) = expr_item { return Some(value); diff --git a/crates/common/src/expr/mod.rs b/crates/common/src/expr/mod.rs index 6e5c4df3..87d26efd 100644 --- a/crates/common/src/expr/mod.rs +++ b/crates/common/src/expr/mod.rs @@ -6,6 +6,7 @@ use std::{ borrow::Cow, + fmt::{Display, Formatter}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, time::Duration, }; @@ -68,6 +69,7 @@ pub const VARIABLES_MAP: &[(&str, u32)] = &[ ("country", V_COUNTRY), ]; +use compact_str::CompactString; use regex::Regex; use utils::config::{Rate, utils::ParseValue}; @@ -87,7 +89,7 @@ pub struct Expression { #[derive(Debug, Clone)] pub enum ExpressionItem { Variable(u32), - Global(String), + Global(CompactString), Setting(Setting), Capture(u32), Constant(Constant), @@ -102,15 +104,21 @@ pub enum ExpressionItem { #[derive(Debug, Clone)] pub enum Variable<'x> { - String(Cow<'x, str>), + String(StringCow<'x>), Integer(i64), Float(f64), Array(Vec>), } +#[derive(Debug, Clone)] +pub enum StringCow<'x> { + Owned(CompactString), + Borrowed(&'x str), +} + impl Default for Variable<'_> { fn default() -> Self { - Variable::String("".into()) + Variable::String(StringCow::Borrowed("")) } } @@ -118,13 +126,13 @@ impl Default for Variable<'_> { pub enum Constant { Integer(i64), Float(f64), - String(String), + String(CompactString), } impl Eq for Constant {} -impl From for Constant { - fn from(value: String) -> Self { +impl From for Constant { + fn from(value: CompactString) -> Self { Constant::String(value) } } @@ -193,7 +201,7 @@ pub enum UnaryOperator { #[derive(Debug, Clone)] pub enum Token { Variable(u32), - Global(String), + Global(CompactString), Capture(u32), Function { name: Cow<'static, str>, @@ -217,11 +225,11 @@ pub enum Setting { Hostname, ReportDomain, NodeId, - Other(String), + Other(CompactString), } -impl From for Setting { - fn from(value: String) -> Self { +impl From for Setting { + fn from(value: CompactString) -> Self { match value.as_str() { "server.hostname" => Setting::Hostname, "report.domain" => Setting::ReportDomain, @@ -281,13 +289,13 @@ impl From for Variable<'_> { impl<'x> From<&'x str> for Variable<'x> { fn from(value: &'x str) -> Self { - Variable::String(Cow::Borrowed(value)) + Variable::String(StringCow::Borrowed(value)) } } -impl From for Variable<'_> { - fn from(value: String) -> Self { - Variable::String(Cow::Owned(value)) +impl From for Variable<'_> { + fn from(value: CompactString) -> Self { + Variable::String(StringCow::Owned(value)) } } @@ -412,13 +420,86 @@ impl<'x> TryFrom> for Duration { Variable::Integer(value) if value > 0 => Ok(Duration::from_millis(value as u64)), Variable::Float(value) if value > 0.0 => Ok(Duration::from_millis(value as u64)), Variable::String(value) if !value.is_empty() => { - Duration::parse_value(&value).map_err(|_| ()) + Duration::parse_value(value.as_str()).map_err(|_| ()) } _ => Err(()), } } } +impl StringCow<'_> { + pub fn as_str(&self) -> &str { + match self { + StringCow::Owned(s) => s.as_str(), + StringCow::Borrowed(s) => s, + } + } + + pub fn as_bytes(&self) -> &[u8] { + match self { + StringCow::Owned(s) => s.as_bytes(), + StringCow::Borrowed(s) => s.as_bytes(), + } + } + + pub fn is_empty(&self) -> bool { + match self { + StringCow::Owned(s) => s.is_empty(), + StringCow::Borrowed(s) => s.is_empty(), + } + } + + pub fn len(&self) -> usize { + match self { + StringCow::Owned(s) => s.len(), + StringCow::Borrowed(s) => s.len(), + } + } + + pub fn into_owned(self) -> CompactString { + match self { + StringCow::Owned(s) => s, + StringCow::Borrowed(s) => s.into(), + } + } +} + +impl<'x> From> for StringCow<'x> { + fn from(value: Cow<'x, str>) -> Self { + match value { + Cow::Borrowed(s) => StringCow::Borrowed(s), + Cow::Owned(s) => StringCow::Owned(s.into()), + } + } +} + +impl From for StringCow<'_> { + fn from(value: CompactString) -> Self { + StringCow::Owned(value) + } +} + +impl AsRef for StringCow<'_> { + fn as_ref(&self) -> &str { + self.as_str() + } +} + +impl AsRef<[u8]> for StringCow<'_> { + fn as_ref(&self) -> &[u8] { + self.as_str().as_bytes() + } +} + +impl Display for StringCow<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + StringCow::Owned(s) => write!(f, "{}", s), + StringCow::Borrowed(s) => write!(f, "{}", s), + } + } +} + impl From for Constant { fn from(value: Duration) -> Self { Constant::Integer(value.as_millis() as i64) @@ -453,7 +534,7 @@ impl<'x> TryFrom> for Ipv4Addr { fn try_from(value: Variable<'x>) -> Result { match value { - Variable::String(value) => value.parse().map_err(|_| ()), + Variable::String(value) => value.as_str().parse().map_err(|_| ()), _ => Err(()), } } @@ -464,7 +545,7 @@ impl<'x> TryFrom> for Ipv6Addr { fn try_from(value: Variable<'x>) -> Result { match value { - Variable::String(value) => value.parse().map_err(|_| ()), + Variable::String(value) => value.as_str().parse().map_err(|_| ()), _ => Err(()), } } @@ -475,7 +556,7 @@ impl<'x> TryFrom> for IpAddr { fn try_from(value: Variable<'x>) -> Result { match value { - Variable::String(value) => value.parse().map_err(|_| ()), + Variable::String(value) => value.as_str().parse().map_err(|_| ()), _ => Err(()), } } diff --git a/crates/common/src/expr/tokenizer.rs b/crates/common/src/expr/tokenizer.rs index c050cbd2..ff257be5 100644 --- a/crates/common/src/expr/tokenizer.rs +++ b/crates/common/src/expr/tokenizer.rs @@ -233,7 +233,7 @@ impl<'x> Tokenizer<'x> { Err("Unexpected end of expression".to_string()) } - fn parse_string(&mut self, stop_ch: u8) -> Result { + fn parse_string(&mut self, stop_ch: u8) -> Result { let mut buf = Vec::with_capacity(16); let mut last_ch = 0; let mut found_end = false; @@ -267,7 +267,7 @@ impl<'x> Tokenizer<'x> { } if found_end { - String::from_utf8(buf).map_err(|_| "Invalid UTF-8".to_string()) + CompactString::from_utf8(buf).map_err(|_| "Invalid UTF-8".into()) } else { Err("Unterminated string".to_string()) } diff --git a/crates/common/src/ipc.rs b/crates/common/src/ipc.rs index 7853596e..37245340 100644 --- a/crates/common/src/ipc.rs +++ b/crates/common/src/ipc.rs @@ -7,6 +7,7 @@ use std::{sync::Arc, time::Instant}; use ahash::RandomState; +use compact_str::CompactString; use jmap_proto::types::{state::StateChange, type_state::DataType}; use mail_auth::{ dmarc::Dmarc, @@ -69,8 +70,8 @@ pub enum StateEvent { pub enum UpdateSubscription { Unverified { id: u32, - url: String, - code: String, + url: CompactString, + code: CompactString, keys: Option, }, Verified(PushSubscription), @@ -79,7 +80,7 @@ pub enum UpdateSubscription { #[derive(Debug)] pub struct PushSubscription { pub id: u32, - pub url: String, + pub url: CompactString, pub expires: u64, pub types: Bitmap, pub keys: Option, diff --git a/crates/common/src/lib.rs b/crates/common/src/lib.rs index a2bbbdee..287c397a 100644 --- a/crates/common/src/lib.rs +++ b/crates/common/src/lib.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +#![warn(clippy::large_futures)] + use std::{ hash::{BuildHasher, Hasher}, net::{IpAddr, Ipv4Addr, Ipv6Addr}, diff --git a/crates/common/src/listener/acme/directory.rs b/crates/common/src/listener/acme/directory.rs index e8b0de25..e01a85ec 100644 --- a/crates/common/src/listener/acme/directory.rs +++ b/crates/common/src/listener/acme/directory.rs @@ -12,7 +12,7 @@ use ring::rand::SystemRandom; use ring::signature::{ECDSA_P256_SHA256_FIXED_SIGNING, EcdsaKeyPair, EcdsaSigningAlgorithm}; use serde::Deserialize; use store::write::Archiver; -use store::{SERIALIZE_OBJ_01_V1, Serialize, SerializedVersion}; +use store::{SERIALIZE_CERT_V1, Serialize, SerializedVersion}; use trc::AddContext; use trc::event::conv::AssertSuccess; @@ -212,7 +212,7 @@ pub struct SerializedCert { impl SerializedVersion for SerializedCert { fn serialize_version() -> u8 { - SERIALIZE_OBJ_01_V1 + SERIALIZE_CERT_V1 } } diff --git a/crates/common/src/listener/acme/order.rs b/crates/common/src/listener/acme/order.rs index e13bb8c3..ba5bdb0c 100644 --- a/crates/common/src/listener/acme/order.rs +++ b/crates/common/src/listener/acme/order.rs @@ -1,6 +1,7 @@ // Adapted from rustls-acme (https://github.com/FlorianUekermann/rustls-acme), licensed under MIT/Apache-2.0. use chrono::{DateTime, TimeZone, Utc}; +use compact_str::CompactString; use dns_update::DnsRecord; use futures::future::try_join_all; use rcgen::{CertificateParams, DistinguishedName, PKCS_ECDSA_P256_SHA256}; @@ -201,7 +202,9 @@ impl Server { trc::Key::Contents, auth.challenges .iter() - .map(|c| trc::Value::Static(c.typ.as_str())) + .map(|c| { + trc::Value::String(CompactString::const_new(c.typ.as_str())) + }) .collect::>(), ), )?; diff --git a/crates/common/src/listener/mod.rs b/crates/common/src/listener/mod.rs index 87a282e7..6f79293e 100644 --- a/crates/common/src/listener/mod.rs +++ b/crates/common/src/listener/mod.rs @@ -6,6 +6,7 @@ use std::{borrow::Cow, net::IpAddr, sync::Arc, time::Instant}; +use compact_str::ToCompactString; use rustls::ServerConfig; use std::fmt::Debug; use tokio::{ @@ -223,9 +224,9 @@ pub trait SessionManager: Sync + Send + 'static + Clone { impl ResolveVariable for SessionData { fn resolve_variable(&self, variable: u32) -> crate::expr::Variable<'_> { match variable { - V_REMOTE_IP => self.remote_ip.to_string().into(), + V_REMOTE_IP => self.remote_ip.to_compact_string().into(), V_REMOTE_PORT => self.remote_port.into(), - V_LOCAL_IP => self.local_ip.to_string().into(), + V_LOCAL_IP => self.local_ip.to_compact_string().into(), V_LOCAL_PORT => self.local_port.into(), V_LISTENER => self.instance.id.as_str().into(), V_PROTOCOL => self.protocol.as_str().into(), diff --git a/crates/common/src/manager/boot.rs b/crates/common/src/manager/boot.rs index 4150fe8d..5fe6efd7 100644 --- a/crates/common/src/manager/boot.rs +++ b/crates/common/src/manager/boot.rs @@ -371,7 +371,7 @@ impl BootManager { stores.parse_in_memory(&mut config, false).await; // Parse settings - let core = Core::parse(&mut config, stores, manager).await; + let core = Box::pin(Core::parse(&mut config, stores, manager)).await; // Parse data let data = Data::parse(&mut config); @@ -455,7 +455,7 @@ impl BootManager { telemetry.enable(false); // Parse settings and backup - Core::parse(&mut config, stores, manager) + Box::pin(Core::parse(&mut config, stores, manager)) .await .backup(path) .await; @@ -466,7 +466,7 @@ impl BootManager { telemetry.enable(false); // Parse settings and restore - Core::parse(&mut config, stores, manager) + Box::pin(Core::parse(&mut config, stores, manager)) .await .restore(path) .await; @@ -474,7 +474,13 @@ impl BootManager { } StoreOp::Console => { // Store console - store_console(Core::parse(&mut config, stores, manager).await.storage.data).await; + store_console( + Box::pin(Core::parse(&mut config, stores, manager)) + .await + .storage + .data, + ) + .await; std::process::exit(0); } } diff --git a/crates/common/src/manager/reload.rs b/crates/common/src/manager/reload.rs index 75e163d9..81810373 100644 --- a/crates/common/src/manager/reload.rs +++ b/crates/common/src/manager/reload.rs @@ -103,7 +103,7 @@ impl Server { }; // Parse settings and build shared core - let core = Core::parse(&mut config, stores, manager).await; + let core = Box::pin(Core::parse(&mut config, stores, manager)).await; if !config.errors.is_empty() { return Ok(config.into()); } diff --git a/crates/common/src/scripts/mod.rs b/crates/common/src/scripts/mod.rs index a7fed493..b815f99c 100644 --- a/crates/common/src/scripts/mod.rs +++ b/crates/common/src/scripts/mod.rs @@ -6,6 +6,7 @@ use std::sync::Arc; +use compact_str::CompactString; use sieve::{Envelope, runtime::Variable}; use store::Value; use unicode_security::mixed_script::AugmentedScriptSet; @@ -21,7 +22,7 @@ pub mod plugins; pub enum ScriptModification { SetEnvelope { name: Envelope, - value: String, + value: CompactString, }, AddHeader { name: Arc, diff --git a/crates/common/src/sharing/acl.rs b/crates/common/src/sharing/acl.rs index 0436aa81..07998efa 100644 --- a/crates/common/src/sharing/acl.rs +++ b/crates/common/src/sharing/acl.rs @@ -6,7 +6,10 @@ use directory::{ QueryBy, Type, - backend::internal::{PrincipalField, manage::ChangedPrincipals}, + backend::internal::{ + PrincipalField, + manage::{ChangedPrincipals, ManageDirectory}, + }, }; use jmap_proto::{ error::set::SetError, @@ -91,16 +94,14 @@ impl Server { { let mut acl_obj = jmap_proto::types::value::Object::with_capacity(value.len() / 2); for item in value { - if let Some(mut principal) = self - .core - .storage - .directory - .query(QueryBy::Id(item.account_id), false) + if let Some(name) = self + .store() + .get_principal_name(item.account_id) .await .unwrap_or_default() { acl_obj.append( - Property::_T(principal.take_str(PrincipalField::Name).unwrap_or_default()), + Property::_T(name.into()), item.grants .map(|acl_item| Value::Text(acl_item.to_string())) .collect::>(), diff --git a/crates/common/src/telemetry/tracers/otel.rs b/crates/common/src/telemetry/tracers/otel.rs index 60dec650..6de06bb0 100644 --- a/crates/common/src/telemetry/tracers/otel.rs +++ b/crates/common/src/telemetry/tracers/otel.rs @@ -215,8 +215,7 @@ fn build_key_value(key_value: &(trc::Key, trc::Value)) -> Option { (key_value.0 != trc::Key::SpanId).then(|| KeyValue { key: build_key(&key_value.0), value: match &key_value.1 { - trc::Value::Static(v) => Value::String((*v).into()), - trc::Value::String(v) => Value::String(v.clone().into()), + trc::Value::String(v) => Value::String(v.to_string().into()), trc::Value::UInt(v) => Value::I64(*v as i64), trc::Value::Int(v) => Value::I64(*v), trc::Value::Float(v) => Value::F64(*v), @@ -241,8 +240,7 @@ fn build_key(key: &trc::Key) -> Key { fn build_any_value(value: &trc::Value) -> AnyValue { match value { - trc::Value::Static(v) => AnyValue::String((*v).into()), - trc::Value::String(v) => AnyValue::String(v.clone().into()), + trc::Value::String(v) => AnyValue::String(v.to_string().into()), trc::Value::UInt(v) => AnyValue::Int(*v as i64), trc::Value::Int(v) => AnyValue::Int(*v), trc::Value::Float(v) => AnyValue::Double(*v), diff --git a/crates/common/src/telemetry/tracers/store.rs b/crates/common/src/telemetry/tracers/store.rs index 66a4867e..04ef4ab3 100644 --- a/crates/common/src/telemetry/tracers/store.rs +++ b/crates/common/src/telemetry/tracers/store.rs @@ -67,10 +67,10 @@ pub(crate) fn spawn_store_tracer(builder: SubscriberBuilder, settings: StoreTrac } } (Key::RemoteIp, Value::Ipv4(ip)) => { - values.insert(ip.to_string()); + values.insert(ip.to_string().into()); } (Key::RemoteIp, Value::Ipv6(ip)) => { - values.insert(ip.to_string()); + values.insert(ip.to_string().into()); } _ => {} @@ -112,7 +112,7 @@ pub(crate) fn spawn_store_tracer(builder: SubscriberBuilder, settings: StoreTrac batch.set( ValueClass::Telemetry(TelemetryClass::Index { span_id, - value: value.into_bytes(), + value: value.as_bytes().to_vec(), }), vec![], ); diff --git a/crates/dav/Cargo.toml b/crates/dav/Cargo.toml index 3e586aa3..0102d622 100644 --- a/crates/dav/Cargo.toml +++ b/crates/dav/Cargo.toml @@ -19,6 +19,7 @@ hashify = { version = "0.2" } hyper = { version = "1.0.1", features = ["server", "http1", "http2"] } percent-encoding = "2.3.1" rkyv = { version = "0.8.10", features = ["little_endian"] } +compact_str = "0.9.0" [dev-dependencies] diff --git a/crates/dav/src/common/acl.rs b/crates/dav/src/common/acl.rs index 34004639..88f5d50e 100644 --- a/crates/dav/src/common/acl.rs +++ b/crates/dav/src/common/acl.rs @@ -5,6 +5,7 @@ */ use common::{Server, auth::AccessToken, sharing::EffectiveAcl}; +use compact_str::format_compact; use dav_proto::{ RequestHeaders, schema::{ @@ -13,7 +14,7 @@ use dav_proto::{ response::{Ace, BaseCondition, GrantDeny, Href, MultiStatus, Principal}, }, }; -use directory::{QueryBy, Type, backend::internal::PrincipalField}; +use directory::{QueryBy, Type, backend::internal::manage::ManageDirectory}; use groupware::{ calendar::Calendar, contact::AddressBook, file::FileNode, hierarchy::DavHierarchy, }; @@ -488,12 +489,11 @@ impl DavAclHandler for Server { } let grant_account_name = self - .directory() - .query(QueryBy::Id(grant_account_id), false) + .store() + .get_principal_name(grant_account_id) .await .caused_by(trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Name)) - .unwrap_or_else(|| format!("_{grant_account_id}")); + .unwrap_or_else(|| format_compact!("_{grant_account_id}")); aces.push(Ace::new( Principal::Href(Href(format!( diff --git a/crates/dav/src/common/lock.rs b/crates/dav/src/common/lock.rs index 945c122e..1c29eb49 100644 --- a/crates/dav/src/common/lock.rs +++ b/crates/dav/src/common/lock.rs @@ -18,7 +18,7 @@ use std::collections::HashMap; use store::dispatch::lookup::KeyValue; use store::write::serialize::rkyv_deserialize; use store::write::{AlignedBytes, Archive, Archiver, now}; -use store::{SERIALIZE_OBJ_02_V1, Serialize, SerializedVersion, U32_LEN}; +use store::{SERIALIZE_LOCKDATA_V1, Serialize, SerializedVersion, U32_LEN}; use trc::AddContext; use super::ETag; @@ -733,7 +733,7 @@ impl<'x> LockCaches<'x> { impl SerializedVersion for LockData { fn serialize_version() -> u8 { - SERIALIZE_OBJ_02_V1 + SERIALIZE_LOCKDATA_V1 } } diff --git a/crates/dav/src/common/propfind.rs b/crates/dav/src/common/propfind.rs index 0b96cb1c..b95934e8 100644 --- a/crates/dav/src/common/propfind.rs +++ b/crates/dav/src/common/propfind.rs @@ -226,7 +226,7 @@ impl PropFindRequestHandler for Server { None, access_token.tenant_id(), &[Type::Individual, Type::Group], - &[PrincipalField::Name], + false, 0, 0, ) diff --git a/crates/dav/src/principal/propfind.rs b/crates/dav/src/principal/propfind.rs index 41a53410..bb3e2bcf 100644 --- a/crates/dav/src/principal/propfind.rs +++ b/crates/dav/src/principal/propfind.rs @@ -7,13 +7,14 @@ use std::borrow::Cow; use common::{Server, auth::AccessToken}; +use compact_str::format_compact; use dav_proto::schema::{ Namespace, property::{DavProperty, PrincipalProperty, ReportSet, ResourceType, WebDavProperty}, request::{DavPropertyValue, PropFind}, response::{Href, MultiStatus, PropStat, Response}, }; -use directory::{QueryBy, backend::internal::PrincipalField}; +use directory::{QueryBy, backend::internal::manage::ManageDirectory}; use hyper::StatusCode; use jmap_proto::types::collection::Collection; use percent_encoding::NON_ALPHANUMERIC; @@ -110,14 +111,10 @@ impl PrincipalPropFind for Server { .query(QueryBy::Id(account_id), false) .await .caused_by(trc::location!())? - .map(|mut p| { - let name = p - .take_str(PrincipalField::Name) - .unwrap_or_else(|| format!("_{account_id}")); - let description = p - .take_str(PrincipalField::Description) - .unwrap_or_else(|| name.clone()); - (Cow::Owned(name), description) + .map(|p| { + let name = p.name; + let description = p.description.unwrap_or_else(|| name.clone()); + (Cow::Owned(name.to_string()), description.to_string()) }) .unwrap_or_else(|| { ( @@ -293,12 +290,11 @@ impl PrincipalPropFind for Server { Ok(access_token.current_user_principal()) } else { let name = self - .directory() - .query(QueryBy::Id(account_id), false) + .store() + .get_principal_name(account_id) .await .caused_by(trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Name)) - .unwrap_or_else(|| format!("_{account_id}")); + .unwrap_or_else(|| format_compact!("_{account_id}")); Ok(Href(format!( "{}/{}", DavResource::Principal.base_path(), diff --git a/crates/dav/src/principal/propsearch.rs b/crates/dav/src/principal/propsearch.rs index f4d91987..094d2ea0 100644 --- a/crates/dav/src/principal/propsearch.rs +++ b/crates/dav/src/principal/propsearch.rs @@ -60,7 +60,7 @@ impl PrincipalPropSearch for Server { search_for.as_str().into(), access_token.tenant_id(), &[Type::Individual, Type::Group], - &[PrincipalField::Name], + false, 0, 0, ) diff --git a/crates/directory/Cargo.toml b/crates/directory/Cargo.toml index 3bff0fc5..5dc5c282 100644 --- a/crates/directory/Cargo.toml +++ b/crates/directory/Cargo.toml @@ -37,6 +37,8 @@ totp-rs = { version = "5.5.1", features = ["otpauth"] } reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"] } serde_json = "1.0" base64 = "0.22" +rkyv = { version = "0.8.10", features = ["little_endian"] } +compact_str = { version = "0.9.0", features = ["rkyv", "serde"] } [dev-dependencies] tokio = { version = "1.23", features = ["full"] } diff --git a/crates/directory/src/backend/imap/lookup.rs b/crates/directory/src/backend/imap/lookup.rs index b57cbacd..d71f0dc5 100644 --- a/crates/directory/src/backend/imap/lookup.rs +++ b/crates/directory/src/backend/imap/lookup.rs @@ -4,10 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use mail_send::Credentials; use smtp_proto::{AUTH_CRAM_MD5, AUTH_LOGIN, AUTH_OAUTHBEARER, AUTH_PLAIN, AUTH_XOAUTH2}; -use crate::{IntoError, Principal, QueryBy, backend::RcptType}; +use crate::{IntoError, Principal, QueryBy, Type, backend::RcptType}; use super::{ImapDirectory, ImapError}; @@ -48,7 +49,7 @@ impl ImapDirectory { match client.authenticate(mechanism, credentials).await { Ok(_) => { client.is_valid = false; - Ok(Some(Principal::default())) + Ok(Some(Principal::new(u32::MAX, Type::Individual))) } Err(err) => match &err { ImapError::AuthenticationFailed => Ok(None), @@ -68,11 +69,11 @@ impl ImapDirectory { Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) } - pub async fn vrfy(&self, _address: &str) -> trc::Result> { + pub async fn vrfy(&self, _address: &str) -> trc::Result> { Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) } - pub async fn expn(&self, _address: &str) -> trc::Result> { + pub async fn expn(&self, _address: &str) -> trc::Result> { Err(trc::StoreEvent::NotSupported.caused_by(trc::location!())) } diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs index 90324737..87ceb241 100644 --- a/crates/directory/src/backend/internal/lookup.rs +++ b/crates/directory/src/backend/internal/lookup.rs @@ -4,6 +4,9 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{PrincipalInfo, manage::ManageDirectory}; +use crate::{Principal, PrincipalData, QueryBy, Type, backend::RcptType}; +use compact_str::CompactString; use mail_send::Credentials; use store::{ Deserialize, IterateParams, Store, ValueKey, @@ -11,10 +14,6 @@ use store::{ }; use trc::AddContext; -use crate::{Principal, QueryBy, Type, backend::RcptType}; - -use super::{PrincipalField, PrincipalInfo, manage::ManageDirectory}; - #[allow(async_fn_in_trait)] pub trait DirectoryStore: Sync + Send { async fn query( @@ -25,9 +24,9 @@ pub trait DirectoryStore: Sync + Send { async fn email_to_id(&self, address: &str) -> trc::Result>; async fn is_local_domain(&self, domain: &str) -> trc::Result; async fn rcpt(&self, address: &str) -> trc::Result; - async fn vrfy(&self, address: &str) -> trc::Result>; - async fn expn(&self, address: &str) -> trc::Result>; - async fn expn_by_id(&self, id: u32) -> trc::Result>; + async fn vrfy(&self, address: &str) -> trc::Result>; + async fn expn(&self, address: &str) -> trc::Result>; + async fn expn_by_id(&self, id: u32) -> trc::Result>; } impl DirectoryStore for Store { @@ -63,13 +62,26 @@ impl DirectoryStore for Store { } if return_member_of { + let mut roles = vec![]; + let mut lists = vec![]; + let mut member_of = vec![]; + for member in self.get_member_of(principal.id).await? { - let field = match member.typ { - Type::List => PrincipalField::Lists, - Type::Role => PrincipalField::Roles, - _ => PrincipalField::MemberOf, - }; - principal.append_int(field, member.principal_id); + match member.typ { + Type::List => lists.push(member.principal_id), + Type::Role => roles.push(member.principal_id), + _ => member_of.push(member.principal_id), + } + } + + if !roles.is_empty() { + principal.data.push(PrincipalData::Roles(roles)); + } + if !lists.is_empty() { + principal.data.push(PrincipalData::Lists(lists)); + } + if !member_of.is_empty() { + principal.data.push(PrincipalData::MemberOf(member_of)); } } return Ok(Some(principal)); @@ -111,7 +123,7 @@ impl DirectoryStore for Store { } } - async fn vrfy(&self, address: &str) -> trc::Result> { + async fn vrfy(&self, address: &str) -> trc::Result> { let mut results = Vec::new(); let address = address.split('@').next().unwrap_or(address); if address.len() > 3 { @@ -131,7 +143,7 @@ impl DirectoryStore for Store { .typ != Type::List { - results.push(key.to_string()); + results.push(key.into()); } Ok(true) }, @@ -143,7 +155,7 @@ impl DirectoryStore for Store { Ok(results) } - async fn expn(&self, address: &str) -> trc::Result> { + async fn expn(&self, address: &str) -> trc::Result> { if let Some(ptype) = self .get_value::(ValueKey::from(ValueClass::Directory( DirectoryClass::EmailToId(address.as_bytes().to_vec()), @@ -157,23 +169,27 @@ impl DirectoryStore for Store { } } - async fn expn_by_id(&self, list_id: u32) -> trc::Result> { + async fn expn_by_id(&self, list_id: u32) -> trc::Result> { let mut results = Vec::new(); for account_id in self.get_members(list_id).await? { if let Some(email) = self .get_principal(account_id) .await? - .and_then(|mut p| p.take_str(PrincipalField::Emails)) + .and_then(|p| p.emails.into_iter().next()) { results.push(email); } } - if let Some(emails) = self - .get_principal(list_id) - .await? - .and_then(|mut p| p.take_str_array(PrincipalField::ExternalMembers)) - { + if let Some(emails) = self.get_principal(list_id).await?.and_then(|p| { + p.data.into_iter().find_map(|data| { + if let PrincipalData::ExternalMembers(members) = data { + Some(members) + } else { + None + } + }) + }) { results.extend(emails); } diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index b458830c..791f5d42 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -4,36 +4,30 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use super::{ + PrincipalAction, PrincipalField, PrincipalInfo, PrincipalSet, PrincipalUpdate, PrincipalValue, + SpecialSecrets, lookup::DirectoryStore, +}; +use crate::{ + MemberOf, Permission, PermissionGrant, Permissions, Principal, PrincipalData, PrincipalQuota, + QueryBy, ROLE_ADMIN, ROLE_TENANT_ADMIN, ROLE_USER, Type, backend::RcptType, +}; use ahash::{AHashMap, AHashSet}; +use compact_str::CompactString; use jmap_proto::types::collection::Collection; use store::{ Deserialize, IterateParams, Serialize, SerializeInfallible, Store, U32_LEN, ValueKey, write::{ - BatchBuilder, DirectoryClass, ValueClass, assert::LegacyHashedValue, + AlignedBytes, Archive, Archiver, BatchBuilder, DirectoryClass, ValueClass, key::DeserializeBigEndian, }, }; use trc::AddContext; use utils::sanitize_email; -use crate::{ - MAX_TYPE_ID, Permission, Permissions, Principal, QueryBy, ROLE_ADMIN, ROLE_TENANT_ADMIN, - ROLE_USER, Type, backend::RcptType, -}; - -use super::{ - PrincipalAction, PrincipalField, PrincipalInfo, PrincipalUpdate, PrincipalValue, - SpecialSecrets, lookup::DirectoryStore, -}; - -pub struct MemberOf { - pub principal_id: u32, - pub typ: Type, -} - #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] -pub struct PrincipalList { - pub items: Vec, +pub struct PrincipalList { + pub items: Vec, pub total: u64, } @@ -67,11 +61,12 @@ pub trait ManageDirectory: Sized { async fn get_principal_info(&self, name: &str) -> trc::Result>; async fn get_or_create_principal_id(&self, name: &str, typ: Type) -> trc::Result; async fn get_principal(&self, principal_id: u32) -> trc::Result>; + async fn get_principal_name(&self, principal_id: u32) -> trc::Result>; async fn get_member_of(&self, principal_id: u32) -> trc::Result>; async fn get_members(&self, principal_id: u32) -> trc::Result>; async fn create_principal( &self, - principal: Principal, + principal: PrincipalSet, tenant_id: Option, allowed_permissions: Option<&Permissions>, ) -> trc::Result; @@ -83,21 +78,21 @@ pub trait ManageDirectory: Sized { filter: Option<&str>, tenant_id: Option, types: &[Type], - fields: &[PrincipalField], + fetch: bool, page: usize, limit: usize, - ) -> trc::Result; + ) -> trc::Result>; async fn count_principals( &self, filter: Option<&str>, typ: Option, tenant_id: Option, ) -> trc::Result; - async fn map_field_ids( + async fn map_principal( &self, - principal: &mut Principal, + principal: Principal, fields: &[PrincipalField], - ) -> trc::Result<()>; + ) -> trc::Result; } #[allow(async_fn_in_trait)] @@ -112,17 +107,40 @@ trait ValidateDirectory: Sized { impl ManageDirectory for Store { async fn get_principal(&self, principal_id: u32) -> trc::Result> { - self.get_value::(ValueKey::from(ValueClass::Directory( - DirectoryClass::Principal(principal_id), - ))) - .await - .caused_by(trc::location!()) - .map(|v| { - v.map(|mut v| { - v.id = principal_id; - v - }) - }) + let archive = self + .get_value::>(ValueKey::from(ValueClass::Directory( + DirectoryClass::Principal(principal_id), + ))) + .await + .caused_by(trc::location!())?; + + if let Some(archive) = archive { + let mut principal = archive + .deserialize::() + .caused_by(trc::location!())?; + principal.id = principal_id; + Ok(Some(principal)) + } else { + Ok(None) + } + } + + async fn get_principal_name(&self, principal_id: u32) -> trc::Result> { + let archive = self + .get_value::>(ValueKey::from(ValueClass::Directory( + DirectoryClass::Principal(principal_id), + ))) + .await + .caused_by(trc::location!())?; + + if let Some(archive) = archive { + let principal = archive + .unarchive::() + .caused_by(trc::location!())?; + Ok(Some(principal.name.as_str().into())) + } else { + Ok(None) + } } async fn get_principal_id(&self, name: &str) -> trc::Result> { @@ -164,6 +182,10 @@ impl ManageDirectory for Store { principal_id_ }; + // Prepare principal + let mut principal = Principal::new(principal_id, typ); + principal.name = name.as_str().into(); + // Write principal ID let name_key = ValueClass::Directory(DirectoryClass::NameToId(name.as_bytes().to_vec())); @@ -179,14 +201,9 @@ impl ManageDirectory for Store { ) .set( ValueClass::Directory(DirectoryClass::Principal(principal_id)), - Principal { - id: principal_id, - typ, - ..Default::default() - } - .with_field(PrincipalField::Name, name.to_string()) - .serialize() - .caused_by(trc::location!())?, + Archiver::new(principal) + .serialize() + .caused_by(trc::location!())?, ); // Add default user role @@ -226,16 +243,16 @@ impl ManageDirectory for Store { async fn create_principal( &self, - mut principal: Principal, + mut principal_set: PrincipalSet, mut tenant_id: Option, allowed_permissions: Option<&Permissions>, ) -> trc::Result { // Make sure the principal has a name - let name = principal.name().to_lowercase(); + let name = CompactString::from_str_to_lowercase(principal_set.name()); if name.is_empty() { return Err(err_missing(PrincipalField::Name)); } - let mut valid_domains: AHashSet = AHashSet::new(); + let mut valid_domains: AHashSet = AHashSet::new(); // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd @@ -257,14 +274,12 @@ impl ManageDirectory for Store { // Enforce tenant quotas if let Some(limit) = tenant - .get_int_array(PrincipalField::Quota) - .and_then(|quotas| quotas.get(principal.typ() as usize + 1)) - .copied() + .principal_quota(&principal_set.typ()) .filter(|q| *q > 0) { // Obtain number of principals let total = self - .count_principals(None, principal.typ().into(), tenant_id.into()) + .count_principals(None, principal_set.typ().into(), tenant_id.into()) .await .caused_by(trc::location!())?; @@ -273,7 +288,7 @@ impl ManageDirectory for Store { trc::LimitEvent::TenantQuota .into_err() .details("Tenant principal quota exceeded") - .ctx(trc::Key::Details, principal.typ().as_str()) + .ctx(trc::Key::Details, principal_set.typ().as_str()) .ctx(trc::Key::Limit, limit) .ctx(trc::Key::Total, total) ); @@ -293,13 +308,17 @@ impl ManageDirectory for Store { return Err(err_exists(PrincipalField::Name, name)); } + let mut principal_create = Principal::new(0, principal_set.typ()); + // SPDX-SnippetBegin // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd // SPDX-License-Identifier: LicenseRef-SEL // Obtain tenant id, only if no default tenant is provided #[cfg(feature = "enterprise")] - if let (Some(tenant_name), None) = (principal.take_str(PrincipalField::Tenant), tenant_id) { + if let (Some(tenant_name), None) = + (principal_set.take_str(PrincipalField::Tenant), tenant_id) + { tenant_id = self .get_principal_info(&tenant_name) .await @@ -313,16 +332,16 @@ impl ManageDirectory for Store { // Tenants must provide principal names including a valid domain #[cfg(feature = "enterprise")] if let Some(tenant_id) = tenant_id { - if matches!(principal.typ, Type::Tenant) { + if matches!(principal_set.typ, Type::Tenant) { return Err(error( "Invalid field", "Tenants cannot contain a tenant field".into(), )); } - principal.set(PrincipalField::Tenant, tenant_id); + principal_create.tenant = tenant_id.into(); - if !matches!(principal.typ, Type::Tenant | Type::Domain) { + if !matches!(principal_create.typ, Type::Tenant | Type::Domain) { if let Some(domain) = name.split('@').nth(1) { if self .get_principal_info(domain) @@ -331,7 +350,7 @@ impl ManageDirectory for Store { .filter(|v| v.typ == Type::Domain && v.has_tenant_access(tenant_id.into())) .is_some() { - valid_domains.insert(domain.to_string()); + valid_domains.insert(domain.into()); } } @@ -345,7 +364,43 @@ impl ManageDirectory for Store { } // SPDX-SnippetEnd - principal.set(PrincipalField::Name, name); + // Set fields + principal_create.name = name; + principal_create.description = principal_set.take_str(PrincipalField::Description); + principal_create.secrets = principal_set + .take_str_array(PrincipalField::Secrets) + .unwrap_or_default(); + if let Some(picture) = principal_set.take_str(PrincipalField::Picture) { + principal_create.data.push(PrincipalData::Picture(picture)); + } + if let Some(urls) = principal_set.take_str_array(PrincipalField::Urls) { + principal_create.data.push(PrincipalData::Urls(urls)); + } + if let Some(urls) = principal_set.take_str_array(PrincipalField::ExternalMembers) { + principal_create + .data + .push(PrincipalData::ExternalMembers(urls)); + } + if let Some(quotas) = principal_set.take_int_array(PrincipalField::Quota) { + let mut principal_quotas = Vec::new(); + + for (idx, quota) in quotas.into_iter().take(Type::MAX_ID + 2).enumerate() { + if idx != 0 { + principal_quotas.push(PrincipalQuota { + quota, + typ: Type::from_u8((idx - 1) as u8), + }); + } else if quota != 0 { + principal_create.quota = Some(quota); + } + } + + if !principal_quotas.is_empty() { + principal_create + .data + .push(PrincipalData::PrincipalQuota(principal_quotas)); + } + } // Map member names let mut members = Vec::new(); @@ -357,7 +412,7 @@ impl ManageDirectory for Store { (PrincipalField::Lists, Some(Type::List)), (PrincipalField::Roles, Some(Type::Role)), ] { - if let Some(names) = principal.take_str_array(field) { + if let Some(names) = principal_set.take_str_array(field) { let list = if field == PrincipalField::Members { &mut members } else { @@ -398,29 +453,28 @@ impl ManageDirectory for Store { } // Map permissions + let mut permissions = AHashMap::new(); for field in [ PrincipalField::EnabledPermissions, PrincipalField::DisabledPermissions, ] { - if let Some(names) = principal.take_str_array(field) { - let mut permissions = Vec::with_capacity(names.len()); + let is_disabled = field == PrincipalField::DisabledPermissions; + if let Some(names) = principal_set.take_str_array(field) { for name in names { - let permission = Permission::from_name(&name) - .ok_or_else(|| { - error( - format!("Invalid {} value", field.as_str()), - format!("Permission {name:?} is invalid").into(), - ) - })? - .id() as u64; + let permission = Permission::from_name(&name).ok_or_else(|| { + error( + format!("Invalid {} value", field.as_str()), + format!("Permission {name:?} is invalid").into(), + ) + })?; - if !permissions.contains(&permission) { + if !permissions.contains_key(&permission) { if allowed_permissions .as_ref() .is_none_or(|p| p.get(permission as usize)) - || field == PrincipalField::DisabledPermissions + || is_disabled { - permissions.push(permission); + permissions.insert(permission, is_disabled); } else { return Err(error( "Invalid permission", @@ -429,22 +483,32 @@ impl ManageDirectory for Store { } } } - - if !permissions.is_empty() { - principal.set(field, permissions); - } } } + if !permissions.is_empty() { + principal_create.data.push(PrincipalData::Permissions( + permissions + .into_iter() + .map(|(k, v)| PermissionGrant { + permission: k, + grant: !v, + }) + .collect(), + )); + } // Make sure the e-mail is not taken and validate domain - if principal.typ != Type::OauthClient { - for email in principal.iter_mut_str(PrincipalField::Emails) { - *email = email.to_lowercase(); - if self.rcpt(email).await.caused_by(trc::location!())? != RcptType::Invalid { + if principal_create.typ != Type::OauthClient { + for email in principal_set + .take_str_array(PrincipalField::Emails) + .unwrap_or_default() + { + let email = email.to_lowercase(); + if self.rcpt(&email).await.caused_by(trc::location!())? != RcptType::Invalid { return Err(err_exists(PrincipalField::Emails, email.to_string())); } if let Some(domain) = email.split('@').nth(1) { - if valid_domains.insert(domain.to_string()) { + if valid_domains.insert(domain.into()) { self.get_principal_info(domain) .await .caused_by(trc::location!())? @@ -452,6 +516,7 @@ impl ManageDirectory for Store { .ok_or_else(|| not_found(domain.to_string()))?; } } + principal_create.emails.push(email); } } @@ -460,45 +525,43 @@ impl ManageDirectory for Store { .assign_document_ids(u32::MAX, Collection::Principal, 1) .await .caused_by(trc::location!())?; - principal.id = principal_id; + principal_create.id = principal_id; let mut batch = BatchBuilder::new(); - let pinfo_name = PrincipalInfo::new(principal_id, principal.typ, tenant_id); - let pinfo_email = PrincipalInfo::new(principal_id, principal.typ, None); + let pinfo_name = PrincipalInfo::new(principal_id, principal_create.typ, tenant_id); + let pinfo_email = PrincipalInfo::new(principal_id, principal_create.typ, None); + + // Serialize + let archiver = Archiver::new(principal_create); + let principal_bytes = archiver.serialize().caused_by(trc::location!())?; + let principal_create = archiver.into_inner(); + batch .with_account_id(u32::MAX) .with_collection(Collection::Principal) .create_document(principal_id) .assert_value( ValueClass::Directory(DirectoryClass::NameToId( - principal.name().to_string().into_bytes(), + principal_create.name().as_bytes().to_vec(), )), (), ) .set( ValueClass::Directory(DirectoryClass::Principal(principal_id)), - principal.serialize().caused_by(trc::location!())?, + principal_bytes, ) .set( ValueClass::Directory(DirectoryClass::NameToId( - principal - .take_str(PrincipalField::Name) - .unwrap() - .into_bytes(), + principal_create.name.as_bytes().to_vec(), )), pinfo_name.serialize(), ); // Write email to id mapping - if let Some(emails) = principal - .take(PrincipalField::Emails) - .map(|v| v.into_str_array()) - { - for email in emails { - batch.set( - ValueClass::Directory(DirectoryClass::EmailToId(email.into_bytes())), - pinfo_email.serialize(), - ); - } + for email in principal_create.emails { + batch.set( + ValueClass::Directory(DirectoryClass::EmailToId(email.as_bytes().to_vec())), + pinfo_email.serialize(), + ); } // Write membership @@ -524,7 +587,7 @@ impl ManageDirectory for Store { principal_id: member.id, member_of: principal_id, }), - vec![principal.typ as u8], + vec![principal_create.typ as u8], ); batch.set( ValueClass::Directory(DirectoryClass::Members { @@ -554,7 +617,7 @@ impl ManageDirectory for Store { QueryBy::Id(principal_id) => principal_id, QueryBy::Credentials(_) => unreachable!(), }; - let mut principal = self + let principal = self .get_principal(principal_id) .await .caused_by(trc::location!())? @@ -596,7 +659,7 @@ impl ManageDirectory for Store { Type::Domain, Type::ApiKey, ], - &[PrincipalField::Name], + false, 0, 0, ) @@ -639,7 +702,7 @@ impl ManageDirectory for Store { Type::Other, Type::Location, ], - &[PrincipalField::Name], + false, 0, 0, ) @@ -710,19 +773,12 @@ impl ManageDirectory for Store { // Delete principal batch .with_account_id(principal_id) - .clear(DirectoryClass::NameToId( - principal - .take_str(PrincipalField::Name) - .unwrap_or_default() - .into_bytes(), - )) + .clear(DirectoryClass::NameToId(principal.name.as_bytes().to_vec())) .clear(DirectoryClass::Principal(principal_id)) .clear(DirectoryClass::UsedQuota(principal_id)); - if let Some(emails) = principal.take_str_array(PrincipalField::Emails) { - for email in emails { - batch.clear(DirectoryClass::EmailToId(email.into_bytes())); - } + for email in principal.emails { + batch.clear(DirectoryClass::EmailToId(email.as_bytes().to_vec())); } for member in self @@ -805,15 +861,21 @@ impl ManageDirectory for Store { let tenant_id = params.tenant_id; // Fetch principal - let mut principal = self - .get_value::>(ValueKey::from(ValueClass::Directory( + let principal_ = self + .get_value::>(ValueKey::from(ValueClass::Directory( DirectoryClass::Principal(principal_id), ))) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(principal_id))?; - principal.inner.id = principal_id; - let principal_type = principal.inner.typ; + let prev_principal = principal_ + .to_unarchived::() + .caused_by(trc::location!())?; + let mut principal = prev_principal + .deserialize::() + .caused_by(trc::location!())?; + principal.id = principal_id; + let principal_type = principal.typ; let validate_emails = principal_type != Type::OauthClient; // Keep track of changed principals @@ -832,7 +894,7 @@ impl ManageDirectory for Store { // Prepare changes let mut batch = BatchBuilder::new(); let mut pinfo_name = - PrincipalInfo::new(principal_id, principal_type, principal.inner.tenant()).serialize(); + PrincipalInfo::new(principal_id, principal_type, principal.tenant()).serialize(); let pinfo_email = PrincipalInfo::new(principal_id, principal_type, None).serialize(); let update_principal = !changes.is_empty() && !changes.iter().all(|c| { @@ -848,7 +910,7 @@ impl ManageDirectory for Store { if update_principal { batch.assert_value( ValueClass::Directory(DirectoryClass::Principal(principal_id)), - &principal, + prev_principal, ); } @@ -904,7 +966,7 @@ impl ManageDirectory for Store { (PrincipalAction::Set, PrincipalField::Name, PrincipalValue::String(new_name)) => { // Make sure new name is not taken let new_name = new_name.to_lowercase(); - if principal.inner.name() != new_name { + if principal.name() != new_name { if tenant_id.is_some() && !matches!(principal_type, Type::Tenant | Type::Domain) { @@ -940,15 +1002,16 @@ impl ManageDirectory for Store { } batch.clear(ValueClass::Directory(DirectoryClass::NameToId( - principal.inner.name().as_bytes().to_vec(), + principal.name().as_bytes().to_vec(), ))); - principal.inner.set(PrincipalField::Name, new_name.clone()); - batch.set( - ValueClass::Directory(DirectoryClass::NameToId(new_name.into_bytes())), + ValueClass::Directory(DirectoryClass::NameToId( + new_name.as_bytes().to_vec(), + )), pinfo_name.clone(), ); + principal.name = new_name; // Name changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); @@ -978,13 +1041,13 @@ impl ManageDirectory for Store { )); } - if principal.inner.tenant() == Some(tenant_info.id) { + if principal.tenant() == Some(tenant_info.id) { continue; } // Update quota if let Some(used_quota) = used_quota { - if let Some(old_tenant_id) = principal.inner.tenant() { + if let Some(old_tenant_id) = principal.tenant() { batch.add(DirectoryClass::UsedQuota(old_tenant_id), -used_quota); } batch.add(DirectoryClass::UsedQuota(tenant_info.id), used_quota); @@ -993,11 +1056,11 @@ impl ManageDirectory for Store { // Tenant changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); - principal.inner.set(PrincipalField::Tenant, tenant_info.id); + principal.tenant = tenant_info.id.into(); pinfo_name = PrincipalInfo::new(principal_id, principal_type, tenant_info.id.into()) .serialize(); - } else if let Some(tenant_id) = principal.inner.tenant() { + } else if let Some(tenant_id) = principal.tenant() { // Update quota if let Some(used_quota) = used_quota { batch.add(DirectoryClass::UsedQuota(tenant_id), -used_quota); @@ -1006,7 +1069,7 @@ impl ManageDirectory for Store { // Tenant changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); - principal.inner.remove(PrincipalField::Tenant); + principal.tenant = None; pinfo_name = PrincipalInfo::new(principal_id, principal_type, None).serialize(); } else { @@ -1015,7 +1078,7 @@ impl ManageDirectory for Store { batch.set( ValueClass::Directory(DirectoryClass::NameToId( - principal.inner.name().as_bytes().to_vec(), + principal.name().as_bytes().to_vec(), )), pinfo_name.clone(), ); @@ -1030,20 +1093,17 @@ impl ManageDirectory for Store { // Password changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); - principal.inner.set(PrincipalField::Secrets, value); + principal.secrets = value.into_str_array(); } ( PrincipalAction::AddItem, PrincipalField::Secrets, PrincipalValue::String(secret), ) => { - if !principal - .inner - .has_str_value(PrincipalField::Secrets, &secret) - { + if !principal.secrets.contains(&secret) { if secret.is_otp_auth() { // Add OTP Auth URLs to the beginning of the list - principal.inner.prepend_str(PrincipalField::Secrets, secret); + principal.secrets.insert(0, secret); // Password changed, update changed principals changed_principals.add_change( @@ -1052,7 +1112,7 @@ impl ManageDirectory for Store { change.field, ); } else { - principal.inner.append_str(PrincipalField::Secrets, secret); + principal.secrets.push(secret); // Password changed, update changed principals changed_principals.add_change( principal_id, @@ -1071,17 +1131,13 @@ impl ManageDirectory for Store { changed_principals.add_change(principal_id, principal_type, change.field); if secret.is_app_password() || secret.is_otp_auth() { - principal.inner.retain_str(PrincipalField::Secrets, |v| { - *v != secret && !v.starts_with(&secret) - }); + principal + .secrets + .retain(|v| *v != secret && !v.starts_with(secret.as_str())); } else if !secret.is_empty() { - principal - .inner - .retain_str(PrincipalField::Secrets, |v| *v != secret); + principal.secrets.retain(|v| *v != secret); } else { - principal - .inner - .retain_str(PrincipalField::Secrets, |v| !v.is_password()); + principal.secrets.retain(|v| !v.is_password()); } } ( @@ -1090,9 +1146,9 @@ impl ManageDirectory for Store { PrincipalValue::String(value), ) => { if !value.is_empty() { - principal.inner.set(change.field, value); + principal.description = Some(value); } else { - principal.inner.remove(change.field); + principal.description = None; } } (PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::Integer(quota)) @@ -1103,7 +1159,7 @@ impl ManageDirectory for Store { { // Quota changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); - principal.inner.set(PrincipalField::Quota, quota); + principal.quota = Some(quota); } (PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::String(quota)) if matches!( @@ -1113,17 +1169,38 @@ impl ManageDirectory for Store { { // Quota changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); - - principal.inner.remove(PrincipalField::Quota); + principal.quota = None; } ( PrincipalAction::Set, PrincipalField::Quota, PrincipalValue::IntegerList(quotas), ) if matches!(principal_type, Type::Tenant) - && quotas.len() <= (MAX_TYPE_ID + 2) => + && quotas.len() <= (Type::MAX_ID + 2) => { - principal.inner.set(PrincipalField::Quota, quotas); + let mut new_quota = None; + let mut principal_quotas = Vec::new(); + + for (idx, quota) in quotas.into_iter().enumerate() { + if idx != 0 { + principal_quotas.push(PrincipalQuota { + quota, + typ: Type::from_u8((idx - 1) as u8), + }); + } else if quota != 0 { + new_quota = Some(quota); + } + } + + principal.quota = new_quota; + principal + .data + .retain(|v| !matches!(v, PrincipalData::PrincipalQuota(_))); + if !principal_quotas.is_empty() { + principal + .data + .push(PrincipalData::PrincipalQuota(principal_quotas)); + } } // Emails @@ -1138,7 +1215,7 @@ impl ManageDirectory for Store { .map(|v| v.to_lowercase()) .collect::>(); for email in &emails { - if !principal.inner.has_str_value(PrincipalField::Emails, email) { + if !principal.emails.contains(email) { if validate_emails { self.validate_email(email, tenant_id, params.create_domains) .await?; @@ -1152,7 +1229,7 @@ impl ManageDirectory for Store { } } - for email in principal.inner.iter_str(PrincipalField::Emails) { + for email in &principal.emails { if !emails.contains(email) { batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( email.as_bytes().to_vec(), @@ -1163,7 +1240,7 @@ impl ManageDirectory for Store { // Emails changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); - principal.inner.set(PrincipalField::Emails, emails); + principal.emails = emails; } ( PrincipalAction::AddItem, @@ -1171,10 +1248,7 @@ impl ManageDirectory for Store { PrincipalValue::String(email), ) => { let email = email.to_lowercase(); - if !principal - .inner - .has_str_value(PrincipalField::Emails, &email) - { + if !principal.emails.contains(&email) { if validate_emails { self.validate_email(&email, tenant_id, params.create_domains) .await?; @@ -1185,7 +1259,7 @@ impl ManageDirectory for Store { )), pinfo_email.clone(), ); - principal.inner.append_str(PrincipalField::Emails, email); + principal.emails.push(email); // Emails changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); @@ -1197,15 +1271,10 @@ impl ManageDirectory for Store { PrincipalValue::String(email), ) => { let email = email.to_lowercase(); - if principal - .inner - .has_str_value(PrincipalField::Emails, &email) - { - principal - .inner - .retain_str(PrincipalField::Emails, |v| *v != email); + if let Some(idx) = principal.emails.iter().position(|v| v == email) { + principal.emails.remove(idx); batch.clear(ValueClass::Directory(DirectoryClass::EmailToId( - email.into_bytes(), + email.as_bytes().to_vec(), ))); // Emails changed, update changed principals @@ -1574,25 +1643,24 @@ impl ManageDirectory for Store { PrincipalField::EnabledPermissions | PrincipalField::DisabledPermissions, PrincipalValue::StringList(names), ) => { - let mut permissions = Vec::with_capacity(names.len()); + let is_disabled = change.field == PrincipalField::DisabledPermissions; + let mut permissions = AHashSet::with_capacity(names.len()); for name in names { - let permission = Permission::from_name(&name) - .ok_or_else(|| { - error( - format!("Invalid {} value", change.field.as_str()), - format!("Permission {name:?} is invalid").into(), - ) - })? - .id() as u64; + let permission = Permission::from_name(&name).ok_or_else(|| { + error( + format!("Invalid {} value", change.field.as_str()), + format!("Permission {name:?} is invalid").into(), + ) + })?; if !permissions.contains(&permission) { if params .allowed_permissions .as_ref() .is_none_or(|p| p.get(permission as usize)) - || change.field == PrincipalField::DisabledPermissions + || is_disabled { - permissions.push(permission); + permissions.insert(permission); } else { return Err(error( "Invalid permission", @@ -1603,10 +1671,15 @@ impl ManageDirectory for Store { } } + principal.remove_permissions(!is_disabled); + if !permissions.is_empty() { - principal.inner.set(change.field, permissions); - } else { - principal.inner.remove(change.field); + principal.add_permissions(permissions.into_iter().map(|permission| { + PermissionGrant { + permission, + grant: !is_disabled, + } + })); } // Permissions changed, update changed principals @@ -1617,14 +1690,12 @@ impl ManageDirectory for Store { PrincipalField::EnabledPermissions | PrincipalField::DisabledPermissions, PrincipalValue::String(name), ) => { - let permission = Permission::from_name(&name) - .ok_or_else(|| { - error( - format!("Invalid {} value", change.field.as_str()), - format!("Permission {name:?} is invalid").into(), - ) - })? - .id() as u64; + let permission = Permission::from_name(&name).ok_or_else(|| { + error( + format!("Invalid {} value", change.field.as_str()), + format!("Permission {name:?} is invalid").into(), + ) + })?; if params .allowed_permissions @@ -1632,7 +1703,10 @@ impl ManageDirectory for Store { .is_none_or(|p| p.get(permission as usize)) || change.field == PrincipalField::DisabledPermissions { - principal.inner.append_int(change.field, permission); + principal.add_permission( + permission, + change.field == PrincipalField::EnabledPermissions, + ); // Permissions changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); @@ -1648,50 +1722,57 @@ impl ManageDirectory for Store { PrincipalField::EnabledPermissions | PrincipalField::DisabledPermissions, PrincipalValue::String(name), ) => { - let permission = Permission::from_name(&name) - .ok_or_else(|| { - error( - format!("Invalid {} value", change.field.as_str()), - format!("Permission {name:?} is invalid").into(), - ) - })? - .id() as u64; + let permission = Permission::from_name(&name).ok_or_else(|| { + error( + format!("Invalid {} value", change.field.as_str()), + format!("Permission {name:?} is invalid").into(), + ) + })?; - principal - .inner - .retain_int(change.field, |v| *v != permission); + principal.remove_permission( + permission, + change.field == PrincipalField::EnabledPermissions, + ); // Permissions changed, update changed principals changed_principals.add_change(principal_id, principal_type, change.field); } ( PrincipalAction::Set, - PrincipalField::Urls | PrincipalField::ExternalMembers, - PrincipalValue::StringList(mut items), + PrincipalField::ExternalMembers, + PrincipalValue::StringList(items), ) => { - if matches!(change.field, PrincipalField::ExternalMembers) { - items = items - .into_iter() - .map(|item| { - sanitize_email(&item).ok_or_else(|| { - error( - "Invalid email address", - format!( - "Invalid value {:?} for {}", - item, - change.field.as_str() + principal + .data + .retain(|v| !matches!(v, PrincipalData::ExternalMembers(_))); + if !items.is_empty() { + principal.data.push(PrincipalData::ExternalMembers( + items + .into_iter() + .map(|item| { + sanitize_email(&item).ok_or_else(|| { + error( + "Invalid email address", + format!( + "Invalid value {:?} for {}", + item, + change.field.as_str() + ) + .into(), ) - .into(), - ) + }) }) - }) - .collect::>()?; + .collect::>()?, + )); } + } + (PrincipalAction::Set, PrincipalField::Urls, PrincipalValue::StringList(items)) => { + principal + .data + .retain(|v| !matches!(v, PrincipalData::Urls(_))); if !items.is_empty() { - principal.inner.set(change.field, items); - } else { - principal.inner.remove(change.field); + principal.data.push(PrincipalData::Urls(items)); } } ( @@ -1709,8 +1790,40 @@ impl ManageDirectory for Store { })? } - if !principal.inner.has_str_value(change.field, &item) { - principal.inner.append_str(change.field, item); + let mut found = false; + for data in &mut principal.data { + match (data, change.field) { + (PrincipalData::Urls(urls), PrincipalField::Urls) => { + if !urls.contains(&item) { + urls.push(item.clone()); + } + found = true; + break; + } + ( + PrincipalData::ExternalMembers(emails), + PrincipalField::ExternalMembers, + ) => { + if !emails.contains(&item) { + emails.push(item.clone()); + } + found = true; + break; + } + _ => {} + } + } + + if !found { + match change.field { + PrincipalField::Urls => { + principal.data.push(PrincipalData::Urls(vec![item])) + } + PrincipalField::ExternalMembers => principal + .data + .push(PrincipalData::ExternalMembers(vec![item])), + _ => {} + } } } ( @@ -1718,8 +1831,21 @@ impl ManageDirectory for Store { PrincipalField::Urls | PrincipalField::ExternalMembers, PrincipalValue::String(item), ) => { - if principal.inner.has_str_value(change.field, &item) { - principal.inner.retain_str(change.field, |v| *v != item); + for data in &mut principal.data { + match (data, change.field) { + (PrincipalData::Urls(urls), PrincipalField::Urls) => { + urls.retain(|v| *v != item); + break; + } + ( + PrincipalData::ExternalMembers(emails), + PrincipalField::ExternalMembers, + ) => { + emails.retain(|v| *v != item); + break; + } + _ => {} + } } } @@ -1735,7 +1861,9 @@ impl ManageDirectory for Store { if update_principal { batch.set( ValueClass::Directory(DirectoryClass::Principal(principal_id)), - principal.inner.serialize().caused_by(trc::location!())?, + Archiver::new(principal) + .serialize() + .caused_by(trc::location!())?, ); } @@ -1751,10 +1879,10 @@ impl ManageDirectory for Store { filter: Option<&str>, tenant_id: Option, types: &[Type], - fields: &[PrincipalField], + fetch: bool, page: usize, limit: usize, - ) -> trc::Result { + ) -> trc::Result> { let from_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![]))); let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::NameToId(vec![ u8::MAX; @@ -1769,10 +1897,10 @@ impl ManageDirectory for Store { if (types.is_empty() || types.contains(&pt.typ)) && pt.has_tenant_access(tenant_id) { - results.push(Principal::new(pt.id, pt.typ).with_field( - PrincipalField::Name, - String::from_utf8_lossy(key.get(1..).unwrap_or_default()).into_owned(), - )); + let mut principal = Principal::new(pt.id, pt.typ); + principal.name = + CompactString::from_utf8_lossy(key.get(1..).unwrap_or_default()); + results.push(principal); } Ok(true) @@ -1781,10 +1909,7 @@ impl ManageDirectory for Store { .await .caused_by(trc::location!())?; - if filter.is_none() - && !fields.is_empty() - && fields.iter().all(|f| matches!(f, PrincipalField::Name)) - { + if filter.is_none() && !fetch { return Ok(PrincipalList { total: results.len() as u64, items: results @@ -1795,7 +1920,10 @@ impl ManageDirectory for Store { }); } - let mut result = PrincipalList::default(); + let mut result = PrincipalList { + items: vec![], + total: 0, + }; let filters = filter.and_then(|filter| { let filters = filter .split_whitespace() @@ -1808,49 +1936,32 @@ impl ManageDirectory for Store { } }); + let todo = "fix search"; let mut offset = limit * page.saturating_sub(1); let mut is_done = false; - let map_principals = fields.is_empty() - || fields.iter().any(|f| { - matches!( - f, - PrincipalField::Tenant - | PrincipalField::MemberOf - | PrincipalField::Lists - | PrincipalField::Roles - | PrincipalField::EnabledPermissions - | PrincipalField::DisabledPermissions - | PrincipalField::Members - | PrincipalField::UsedQuota - ) - }); for mut principal in results { if !is_done || filters.is_some() { principal = self - .query(QueryBy::Id(principal.id), map_principals) + .query(QueryBy::Id(principal.id), fetch) .await .caused_by(trc::location!())? .ok_or_else(|| not_found(principal.name().to_string()))?; } - if filters - .as_ref() - .is_none_or(|filters| filters.iter().all(|f| principal.find_str(f))) - { + if filters.as_ref().is_none_or(|filters| { + filters.iter().all(|f| { + principal.name.contains(f) + || principal + .description + .as_ref() + .is_some_and(|n| n.contains(f)) + }) + }) { result.total += 1; if offset == 0 { if !is_done { - if !fields.is_empty() { - principal.fields.retain(|k, _| fields.contains(k)); - } - - if map_principals { - self.map_field_ids(&mut principal, fields) - .await - .caused_by(trc::location!())?; - } result.items.push(principal); is_done = limit != 0 && result.items.len() >= limit; } @@ -1945,44 +2056,108 @@ impl ManageDirectory for Store { Ok(results) } - async fn map_field_ids( + async fn map_principal( &self, - principal: &mut Principal, + principal: Principal, fields: &[PrincipalField], - ) -> trc::Result<()> { - // Map groups - for field in [ - PrincipalField::MemberOf, - PrincipalField::Lists, - PrincipalField::Roles, - ] { - if let Some(member_of) = principal - .take_int_array(field) - .filter(|_| fields.is_empty() || fields.contains(&field)) - { - for principal_id in member_of { - match principal_id as u32 { - ROLE_ADMIN if field == PrincipalField::Roles => { - principal.append_str(field, "admin"); + ) -> trc::Result { + let mut result = PrincipalSet::new(principal.id, principal.typ); + + let has_enabled = fields.is_empty() || fields.contains(&PrincipalField::EnabledPermissions); + let has_disabled = + fields.is_empty() || fields.contains(&PrincipalField::DisabledPermissions); + let mut principal_quotas = Vec::new(); + + for data in principal.data { + match data { + PrincipalData::MemberOf(items) + if fields.is_empty() || fields.contains(&PrincipalField::MemberOf) => + { + for principal_id in items { + if let Some(name) = self + .get_principal_name(principal_id) + .await + .caused_by(trc::location!())? + { + result.append_str(PrincipalField::MemberOf, name); } - ROLE_TENANT_ADMIN if field == PrincipalField::Roles => { - principal.append_str(field, "tenant-admin"); - } - ROLE_USER if field == PrincipalField::Roles => { - principal.append_str(field, "user"); - } - principal_id => { - if let Some(name) = self - .get_principal(principal_id) - .await - .caused_by(trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Name)) - { - principal.append_str(field, name); + } + } + PrincipalData::Roles(items) + if fields.is_empty() || fields.contains(&PrincipalField::Roles) => + { + for principal_id in items { + match principal_id { + ROLE_ADMIN => { + result.append_str(PrincipalField::Roles, "admin"); + } + ROLE_TENANT_ADMIN => { + result.append_str(PrincipalField::Roles, "tenant-admin"); + } + ROLE_USER => { + result.append_str(PrincipalField::Roles, "user"); + } + principal_id => { + if let Some(name) = self + .get_principal_name(principal_id) + .await + .caused_by(trc::location!())? + { + result.append_str(PrincipalField::Roles, name); + } } } } } + PrincipalData::Lists(items) + if fields.is_empty() || fields.contains(&PrincipalField::Lists) => + { + for principal_id in items { + if let Some(name) = self + .get_principal_name(principal_id) + .await + .caused_by(trc::location!())? + { + result.append_str(PrincipalField::Lists, name); + } + } + } + PrincipalData::Permissions(permission_grants) if has_enabled || has_disabled => { + for grant in permission_grants { + if grant.grant { + if has_enabled { + result.append_str( + PrincipalField::EnabledPermissions, + grant.permission.name(), + ); + } + } else if has_disabled { + result.append_str( + PrincipalField::DisabledPermissions, + grant.permission.name(), + ); + } + } + } + PrincipalData::Picture(compact_string) => { + if fields.is_empty() || fields.contains(&PrincipalField::Picture) { + result.set(PrincipalField::Picture, compact_string); + } + } + PrincipalData::ExternalMembers(compact_strings) => { + if fields.is_empty() || fields.contains(&PrincipalField::ExternalMembers) { + result.set(PrincipalField::ExternalMembers, compact_strings); + } + } + PrincipalData::Urls(compact_strings) => { + if fields.is_empty() || fields.contains(&PrincipalField::Urls) { + result.set(PrincipalField::Urls, compact_strings); + } + } + PrincipalData::PrincipalQuota(principal_quotas_) => { + principal_quotas = principal_quotas_; + } + _ => (), } } @@ -1991,12 +2166,10 @@ impl ManageDirectory for Store { match principal.typ { Type::Group | Type::List | Type::Role => { for member_id in self.get_members(principal.id).await? { - if let Some(mut member_principal) = + if let Some(member_principal) = self.query(QueryBy::Id(member_id), false).await? { - if let Some(name) = member_principal.take_str(PrincipalField::Name) { - principal.append_str(PrincipalField::Members, name); - } + result.append_str(PrincipalField::Members, member_principal.name); } } } @@ -2006,7 +2179,7 @@ impl ManageDirectory for Store { let to_key = ValueKey::from(ValueClass::Directory(DirectoryClass::EmailToId( vec![u8::MAX; 10], ))); - let domain_name = principal.name(); + let domain_name = &principal.name; let mut total: u64 = 0; self.iterate( IterateParams::new(from_key, to_key).no_values(), @@ -2023,7 +2196,7 @@ impl ManageDirectory for Store { ) .await .caused_by(trc::location!())?; - principal.set(PrincipalField::Members, total); + result.set(PrincipalField::Members, total); } Type::Tenant => { let from_key = @@ -2047,7 +2220,7 @@ impl ManageDirectory for Store { .await .caused_by(trc::location!())?; - principal.set(PrincipalField::Members, total); + result.set(PrincipalField::Members, total); } _ => {} } @@ -2059,21 +2232,55 @@ impl ManageDirectory for Store { // Map tenant name #[cfg(feature = "enterprise")] - if let Some(tenant_id) = principal.take_int(PrincipalField::Tenant) { + if let Some(tenant_id) = principal.tenant { if fields.is_empty() || fields.contains(&PrincipalField::Tenant) { if let Some(name) = self - .get_principal(tenant_id as u32) + .get_principal_name(tenant_id as u32) .await .caused_by(trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Name)) { - principal.set(PrincipalField::Tenant, name); + result.set(PrincipalField::Tenant, name); } } } // SPDX-SnippetEnd + // Map fields + for (name, value) in [ + (PrincipalField::Name, Some(principal.name)), + (PrincipalField::Description, principal.description), + ] { + if let Some(value) = value { + if fields.is_empty() || fields.contains(&name) { + result.set(name, value); + } + } + } + for (name, value) in [ + (PrincipalField::Secrets, principal.secrets), + (PrincipalField::Emails, principal.emails), + ] { + if fields.is_empty() || fields.contains(&name) { + result.set(name, value); + } + } + if fields.is_empty() || fields.contains(&PrincipalField::Quota) { + if !principal_quotas.is_empty() { + let mut quotas = vec![0u64; Type::MAX_ID + 2]; + if let Some(quota) = principal.quota { + quotas[0] = quota; + } + for quota in principal_quotas { + quotas[quota.typ as usize] = quota.quota; + } + + result.set(PrincipalField::Quota, quotas); + } else if let Some(quota) = principal.quota { + result.set(PrincipalField::Quota, quota); + } + } + // Obtain used quota if matches!(principal.typ, Type::Individual | Type::Group | Type::Tenant) && (fields.is_empty() || fields.contains(&PrincipalField::UsedQuota)) @@ -2083,25 +2290,11 @@ impl ManageDirectory for Store { .await .caused_by(trc::location!())?; if quota > 0 { - principal.set(PrincipalField::UsedQuota, quota as u64); + result.set(PrincipalField::UsedQuota, quota as u64); } } - // Map permissions - for field in [ - PrincipalField::EnabledPermissions, - PrincipalField::DisabledPermissions, - ] { - if let Some(permissions) = principal.take_int_array(field) { - for permission in permissions { - if let Some(name) = Permission::from_id(permission as usize) { - principal.append_str(field, name.name().to_string()); - } - } - } - } - - Ok(()) + Ok(result) } } @@ -2123,9 +2316,9 @@ impl ValidateDirectory for Store { Some(v) if v.typ == Type::Domain && v.has_tenant_access(tenant_id) => Ok(()), None if create_if_missing => self .create_principal( - Principal::new(0, Type::Domain) - .with_field(PrincipalField::Name, domain.to_string()) - .with_field(PrincipalField::Description, domain.to_string()), + PrincipalSet::new(0, Type::Domain) + .with_field(PrincipalField::Name, domain) + .with_field(PrincipalField::Description, domain), tenant_id, None, ) @@ -2398,6 +2591,6 @@ pub fn error(details: impl Into, reason: Option for trc::Value { fn from(value: PrincipalField) -> Self { - trc::Value::Static(value.as_str()) + trc::Value::String(CompactString::const_new(value.as_str())) } } diff --git a/crates/directory/src/backend/internal/mod.rs b/crates/directory/src/backend/internal/mod.rs index 12b961cc..8e371041 100644 --- a/crates/directory/src/backend/internal/mod.rs +++ b/crates/directory/src/backend/internal/mod.rs @@ -7,83 +7,19 @@ pub mod lookup; pub mod manage; -use std::{fmt::Display, slice::Iter}; - +use crate::Type; use ahash::AHashMap; -use store::{Deserialize, Serialize, SerializeInfallible, U32_LEN, write::key::KeySerializer}; +use compact_str::CompactString; +use std::fmt::Display; +use store::{Deserialize, SerializeInfallible, U32_LEN, write::key::KeySerializer}; use utils::codec::leb128::Leb128Iterator; -use crate::{Principal, ROLE_ADMIN, ROLE_USER, Type}; - -const INT_MARKER: u8 = 1 << 7; - pub struct PrincipalInfo { pub id: u32, pub typ: Type, pub tenant: Option, } -impl Serialize for Principal { - fn serialize(&self) -> trc::Result> { - let mut serializer = KeySerializer::new( - U32_LEN - + 2 - + self - .fields - .values() - .map(|v| v.serialized_size() + 1) - .sum::(), - ) - .write(2u8) - .write(self.typ as u8) - .write_leb128(self.fields.len()); - - for (k, v) in &self.fields { - let id = k.id(); - - match v { - PrincipalValue::String(v) => { - serializer = serializer - .write(id) - .write_leb128(1usize) - .write_leb128(v.len()) - .write(v.as_bytes()); - } - PrincipalValue::StringList(l) => { - serializer = serializer.write(id).write_leb128(l.len()); - for v in l { - serializer = serializer.write_leb128(v.len()).write(v.as_bytes()); - } - } - PrincipalValue::Integer(v) => { - serializer = serializer - .write(id | INT_MARKER) - .write_leb128(1usize) - .write_leb128(*v); - } - PrincipalValue::IntegerList(l) => { - serializer = serializer.write(id | INT_MARKER).write_leb128(l.len()); - for v in l { - serializer = serializer.write_leb128(*v); - } - } - } - } - - Ok(serializer.finalize()) - } -} - -impl Deserialize for Principal { - fn deserialize(bytes: &[u8]) -> trc::Result { - deserialize(bytes).ok_or_else(|| { - trc::StoreEvent::DataCorruption - .caused_by(trc::location!()) - .ctx(trc::Key::Value, bytes) - }) - } -} - #[cfg(feature = "enterprise")] impl PrincipalInfo { // SPDX-SnippetBegin @@ -153,231 +89,6 @@ impl PrincipalInfo { } } -fn deserialize(bytes: &[u8]) -> Option { - let mut bytes = bytes.iter(); - - match *bytes.next()? { - 1 => { - // Version 1 (legacy) - let id = bytes.next_leb128()?; - let type_id = *bytes.next()?; - - let mut principal = Principal { - id, - typ: Type::from_u8(type_id), - ..Default::default() - }; - - principal.set(PrincipalField::Quota, bytes.next_leb128::()?); - principal.set(PrincipalField::Name, deserialize_string(&mut bytes)?); - if let Some(description) = deserialize_string(&mut bytes).filter(|s| !s.is_empty()) { - principal.set(PrincipalField::Description, description); - } - for key in [PrincipalField::Secrets, PrincipalField::Emails] { - for _ in 0..bytes.next_leb128::()? { - principal.append_str(key, deserialize_string(&mut bytes)?); - } - } - - principal - .with_field( - PrincipalField::Roles, - if type_id != 4 { ROLE_USER } else { ROLE_ADMIN }, - ) - .into() - } - 2 => { - // Version 2 - let typ = Type::from_u8(*bytes.next()?); - let num_fields = bytes.next_leb128::()?; - - let mut principal = Principal { - id: u32::MAX, - typ, - fields: AHashMap::with_capacity(num_fields), - }; - - for _ in 0..num_fields { - let id = *bytes.next()?; - let num_values = bytes.next_leb128::()?; - - if (id & INT_MARKER) == 0 { - let field = PrincipalField::from_id(id)?; - if num_values == 1 { - principal.set(field, deserialize_string(&mut bytes)?); - } else { - let mut values = Vec::with_capacity(num_values); - for _ in 0..num_values { - values.push(deserialize_string(&mut bytes)?); - } - principal.set(field, values); - } - } else { - let field = PrincipalField::from_id(id & !INT_MARKER)?; - if num_values == 1 { - principal.set(field, bytes.next_leb128::()?); - } else { - let mut values = Vec::with_capacity(num_values); - for _ in 0..num_values { - values.push(bytes.next_leb128::()?); - } - principal.set(field, values); - } - } - } - - principal.into() - } - _ => None, - } -} - -/*pub trait MigrateDirectory: Sync + Send { - fn migrate_directory(&self) -> impl std::future::Future> + Send; -} - -impl MigrateDirectory for Store { - async fn migrate_directory(&self) -> trc::Result<()> { - let mut principals = Vec::new(); - let mut domains = Vec::new(); - - self.iterate( - IterateParams::new( - ValueKey { - account_id: 0, - collection: 0, - document_id: 0, - class: ValueClass::Directory(DirectoryClass::Principal(0)), - }, - ValueKey { - account_id: u32::MAX, - collection: u8::MAX, - document_id: u32::MAX, - class: ValueClass::Any(AnyClass { - subspace: SUBSPACE_DIRECTORY, - key: vec![4u8], - }), - }, - ), - |key, value| { - match (key.first(), value.first()) { - (Some(2), Some(1)) => { - principals.push(( - key.get(1..) - .and_then(|b| b.read_leb128::().map(|(v, _)| v)) - .ok_or_else(|| { - trc::StoreEvent::DataCorruption - .caused_by(trc::location!()) - .ctx(trc::Key::Value, key) - })?, - Principal::deserialize(value)?, - )); - } - (Some(3), _) => { - let domain = std::str::from_utf8(&key[1..]).unwrap_or_default(); - if !domain.is_empty() { - domains.push(domain.to_string()); - } - } - _ => {} - } - - Ok(true) - }, - ) - .await - .caused_by(trc::location!())?; - - let total_principal_count = principals.len(); - for (account_id, mut principal) in principals { - let role = principal.take_int(PrincipalField::Roles).unwrap() as u32; - - let mut batch = BatchBuilder::new(); - batch - .with_account_id(u32::MAX) - .with_collection(Collection::Principal) - .set( - ValueClass::Directory(DirectoryClass::Principal(account_id)), - principal.serialize().caused_by(trc::location!())?, - ); - - if principal.typ() == Type::Individual { - batch - .set( - ValueClass::Directory(DirectoryClass::MemberOf { - principal_id: account_id, - member_of: role, - }), - vec![Type::Role as u8], - ) - .set( - ValueClass::Directory(DirectoryClass::Members { - principal_id: role, - has_member: account_id, - }), - vec![], - ); - } - - self.write(batch.build_all()) - .await - .caused_by(trc::location!())?; - } - - let total_domain_count = domains.len(); - for domain in domains { - let mut batch = BatchBuilder::new(); - - batch - .with_account_id(u32::MAX) - .with_collection(Collection::Principal) - .create_document() - .assert_value( - ValueClass::Directory(DirectoryClass::NameToId( - domain.to_string().into_bytes(), - )), - (), - ) - .set( - ValueClass::Directory(DirectoryClass::Principal(MaybeDynamicId::Dynamic(0))), - Principal::new(0, Type::Domain) - .with_field(PrincipalField::Name, domain.to_string()) - .with_field(PrincipalField::Description, domain.to_string()) - .serialize() - .caused_by(trc::location!())?, - ) - .set( - ValueClass::Directory(DirectoryClass::NameToId(domain.as_bytes().to_vec())), - DynamicPrincipalInfo::new(Type::Domain, None), - ) - .clear(ValueClass::Any(AnyClass { - subspace: SUBSPACE_DIRECTORY, - key: [3u8].iter().chain(domain.as_bytes()).copied().collect(), - })); - - if let Err(err) = self.write(batch.build_all()).await { - trc::error!( - err.caused_by(trc::location!()) - .details("Failed to migrate domain, probably a principal already exists") - .ctx(trc::Key::Domain, domain) - ); - } - } - - if total_domain_count > 0 || total_principal_count > 0 { - trc::event!( - Server(trc::ServerEvent::Startup), - Details = format!( - "Migrated {total_principal_count} principals and {total_domain_count} domains", - ) - ); - } - - Ok(()) - } -} -*/ - #[derive( Debug, Clone, Copy, PartialEq, Hash, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize, )] @@ -402,6 +113,13 @@ pub enum PrincipalField { ExternalMembers, } +#[derive(Debug, Default, Clone, PartialEq, Eq)] +pub struct PrincipalSet { + pub id: u32, + pub typ: Type, + pub fields: AHashMap, +} + #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct PrincipalUpdate { pub action: PrincipalAction, @@ -422,8 +140,8 @@ pub enum PrincipalAction { #[derive(Debug, Clone, PartialEq, Eq, serde::Serialize)] #[serde(untagged)] pub enum PrincipalValue { - String(String), - StringList(Vec), + String(CompactString), + StringList(Vec), Integer(u64), IntegerList(Vec), } @@ -552,15 +270,6 @@ impl PrincipalField { } } -fn deserialize_string(bytes: &mut Iter<'_, u8>) -> Option { - let len = bytes.next_leb128()?; - let mut string = Vec::with_capacity(len); - for _ in 0..len { - string.push(*bytes.next()?); - } - String::from_utf8(string).ok() -} - pub trait SpecialSecrets { fn is_otp_auth(&self) -> bool; fn is_app_password(&self) -> bool; diff --git a/crates/directory/src/backend/ldap/lookup.rs b/crates/directory/src/backend/ldap/lookup.rs index 6803f8ad..f6edebf3 100644 --- a/crates/directory/src/backend/ldap/lookup.rs +++ b/crates/directory/src/backend/ldap/lookup.rs @@ -4,17 +4,17 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use ldap3::{Ldap, LdapConnAsync, ResultEntry, Scope, SearchEntry}; use mail_send::Credentials; use store::xxhash_rust; use trc::AddContext; use crate::{ - IntoError, Principal, QueryBy, ROLE_ADMIN, ROLE_USER, Type, + IntoError, Principal, PrincipalData, QueryBy, ROLE_ADMIN, ROLE_USER, Type, backend::{ RcptType, internal::{ - PrincipalField, lookup::DirectoryStore, manage::{self, ManageDirectory, UpdatePrincipal}, }, @@ -31,16 +31,14 @@ impl LdapDirectory { ) -> trc::Result> { let mut conn = self.pool.get().await.map_err(|err| err.into_error())?; - let (mut external_principal, stored_principal) = match by { + let (mut external_principal, member_of, stored_principal) = match by { QueryBy::Name(username) => { - if let Some(principal) = self + if let Some((mut principal, member_of)) = self .find_principal(&mut conn, &self.mappings.filter_name.build(username)) .await? { - ( - principal.with_field(PrincipalField::Name, username.to_string()), - None, - ) + principal.name = username.into(); + (principal, member_of, None) } else { return Ok(None); } @@ -51,14 +49,14 @@ impl LdapDirectory { .query(QueryBy::Id(uid), return_member_of) .await? { - if let Some(principal) = self + if let Some((principal, member_of)) = self .find_principal( &mut conn, &self.mappings.filter_name.build(stored_principal_.name()), ) .await? { - (principal, Some(stored_principal_)) + (principal, member_of, Some(stored_principal_)) } else { return Ok(None); } @@ -104,10 +102,10 @@ impl LdapDirectory { self.find_principal(&mut conn, filter).await }; match principal { - Ok(Some(principal)) => ( - principal.with_field(PrincipalField::Name, username.to_string()), - None, - ), + Ok(Some((mut principal, member_of))) => { + principal.name = username.into(); + (principal, member_of, None) + } Err(err) if err.matches(trc::EventType::Store(trc::StoreEvent::LdapError)) && err @@ -120,15 +118,13 @@ impl LdapDirectory { Ok(None) => return Ok(None), Err(err) => return Err(err), } - } else if let Some(principal) = self + } else if let Some((mut principal, member_of)) = self .find_principal(&mut conn, &self.mappings.filter_name.build(username)) .await? { if principal.verify_secret(secret).await? { - ( - principal.with_field(PrincipalField::Name, username.to_string()), - None, - ) + principal.name = username.into(); + (principal, member_of, None) } else { return Ok(None); } @@ -139,48 +135,44 @@ impl LdapDirectory { }; // Query groups - match external_principal.take_str_array(PrincipalField::MemberOf) { - Some(names) if return_member_of => { - let mut member_of = Vec::with_capacity(names.len()); - for mut name in names { - if name.contains('=') { - let (rs, _res) = conn - .search( - &name, - Scope::Base, - "objectClass=*", - &self.mappings.attr_name, - ) - .await - .map_err(|err| err.into_error().caused_by(trc::location!()))? - .success() - .map_err(|err| err.into_error().caused_by(trc::location!()))?; - for entry in rs { - 'outer: for (attr, value) in SearchEntry::construct(entry).attrs { - if self.mappings.attr_name.contains(&attr) { - if let Some(group) = value.into_iter().next() { - if !group.is_empty() { - name = group; - break 'outer; - } + if !member_of.is_empty() && return_member_of { + let mut data = Vec::with_capacity(member_of.len()); + for mut name in member_of { + if name.contains('=') { + let (rs, _res) = conn + .search( + &name, + Scope::Base, + "objectClass=*", + &self.mappings.attr_name, + ) + .await + .map_err(|err| err.into_error().caused_by(trc::location!()))? + .success() + .map_err(|err| err.into_error().caused_by(trc::location!()))?; + for entry in rs { + 'outer: for (attr, value) in SearchEntry::construct(entry).attrs { + if self.mappings.attr_name.contains(&attr) { + if let Some(group) = value.into_iter().next() { + if !group.is_empty() { + name = group; + break 'outer; } } } } } - - member_of.push( - self.data_store - .get_or_create_principal_id(&name, Type::Group) - .await - .caused_by(trc::location!())?, - ); } - // Map ids - external_principal.set(PrincipalField::MemberOf, member_of); + data.push( + self.data_store + .get_or_create_principal_id(&name, Type::Group) + .await + .caused_by(trc::location!())?, + ); } - _ => (), + + external_principal.data.push(PrincipalData::MemberOf(data)); } // Obtain account ID if not available @@ -306,11 +298,11 @@ impl LdapDirectory { } } - pub async fn vrfy(&self, address: &str) -> trc::Result> { + pub async fn vrfy(&self, address: &str) -> trc::Result> { self.data_store.vrfy(address).await } - pub async fn expn(&self, address: &str) -> trc::Result> { + pub async fn expn(&self, address: &str) -> trc::Result> { self.data_store.expn(address).await } @@ -324,7 +316,7 @@ impl LdapDirectory { &self, conn: &mut Ldap, filter: &str, - ) -> trc::Result> { + ) -> trc::Result)>> { conn.search( &self.mappings.base_dn, Scope::Subtree, @@ -351,60 +343,58 @@ impl LdapDirectory { } impl LdapMappings { - fn entry_to_principal(&self, entry: SearchEntry) -> Principal { - let mut principal = Principal::default(); + fn entry_to_principal(&self, entry: SearchEntry) -> (Principal, Vec) { + let mut principal = Principal::new(0, Type::Individual); let mut role = ROLE_USER; + let mut member_of = vec![]; for (attr, value) in entry.attrs { if self.attr_name.contains(&attr) { if !self.attr_email_address.contains(&attr) { - principal.set( - PrincipalField::Name, - value.into_iter().next().unwrap_or_default(), - ); + principal.name = value.into_iter().next().unwrap_or_default().into(); } else { for (idx, item) in value.into_iter().enumerate() { - principal.prepend_str(PrincipalField::Emails, item.to_lowercase()); + principal + .emails + .insert(0, CompactString::from_str_to_lowercase(&item)); if idx == 0 { - principal.set(PrincipalField::Name, item); + principal.name = item.into(); } } } } else if self.attr_secret.contains(&attr) { for item in value { - principal.append_str(PrincipalField::Secrets, item); + principal.secrets.push(item.into()); } } else if self.attr_secret_changed.contains(&attr) { // Create a disabled AppPassword, used to indicate that the password has been changed // but cannot be used for authentication. for item in value { - principal.append_str( - PrincipalField::Secrets, - format!("$app${}$", xxhash_rust::xxh3::xxh3_64(item.as_bytes())), + principal.secrets.push( + format!("$app${}$", xxhash_rust::xxh3::xxh3_64(item.as_bytes())).into(), ); } } else if self.attr_email_address.contains(&attr) { for item in value { - principal.prepend_str(PrincipalField::Emails, item.to_lowercase()); + principal + .emails + .insert(0, CompactString::from_str_to_lowercase(&item)); } } else if self.attr_email_alias.contains(&attr) { for item in value { - principal.append_str(PrincipalField::Emails, item.to_lowercase()); + principal + .emails + .push(CompactString::from_str_to_lowercase(&item)); } } else if let Some(idx) = self.attr_description.iter().position(|a| a == &attr) { - if !principal.has_field(PrincipalField::Description) || idx == 0 { - principal.set( - PrincipalField::Description, - value.into_iter().next().unwrap_or_default(), - ); + if principal.description.is_none() || idx == 0 { + principal.description = value.into_iter().next().map(Into::into); } } else if self.attr_groups.contains(&attr) { - for item in value { - principal.append_str(PrincipalField::MemberOf, item); - } + member_of.extend(value); } else if self.attr_quota.contains(&attr) { if let Ok(quota) = value.into_iter().next().unwrap_or_default().parse::() { - principal.set(PrincipalField::Quota, quota); + principal.quota = quota.into(); } } else if self.attr_type.contains(&attr) { for value in value { @@ -426,7 +416,8 @@ impl LdapMappings { } } - principal.with_field(PrincipalField::Roles, role) + principal.data.push(PrincipalData::Roles(vec![role])); + (principal, member_of) } } diff --git a/crates/directory/src/backend/ldap/pool.rs b/crates/directory/src/backend/ldap/pool.rs index 8aa97db0..848e528d 100644 --- a/crates/directory/src/backend/ldap/pool.rs +++ b/crates/directory/src/backend/ldap/pool.rs @@ -26,7 +26,7 @@ impl managed::Manager for LdapConnectionManager { Details = self .bind_dn .as_ref() - .map(|b| trc::Value::String(b.dn.clone())), + .map(|b| trc::Value::String(b.dn.as_str().into())), ); if let Some(bind) = &self.bind_dn { diff --git a/crates/directory/src/backend/memory/config.rs b/crates/directory/src/backend/memory/config.rs index db3f0c68..eda5c484 100644 --- a/crates/directory/src/backend/memory/config.rs +++ b/crates/directory/src/backend/memory/config.rs @@ -4,12 +4,13 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use store::Store; use utils::config::{Config, utils::AsKey}; use crate::{ - Principal, ROLE_ADMIN, ROLE_USER, Type, - backend::internal::{PrincipalField, manage::ManageDirectory}, + Principal, PrincipalData, ROLE_ADMIN, ROLE_USER, Type, + backend::internal::manage::ManageDirectory, }; use super::{EmailType, MemoryDirectory}; @@ -62,15 +63,15 @@ impl MemoryDirectory { .ok()?; // Create principal - let mut principal = Principal { - id, - typ, - ..Default::default() - } - .with_field( - PrincipalField::Roles, - if is_superuser { ROLE_ADMIN } else { ROLE_USER }, - ); + let mut principal = Principal::new(id, typ); + let mut member_of = Vec::with_capacity(2); + principal + .data + .push(PrincipalData::Roles(vec![if is_superuser { + ROLE_ADMIN + } else { + ROLE_USER + }])); // Obtain group ids for group in config @@ -78,8 +79,7 @@ impl MemoryDirectory { .map(|(_, s)| s.to_string()) .collect::>() { - principal.append_int( - PrincipalField::MemberOf, + member_of.push( directory .data_store .get_or_create_principal_id(&group, Type::Group) @@ -96,6 +96,7 @@ impl MemoryDirectory { .ok()?, ); } + principal.data.push(PrincipalData::MemberOf(member_of)); // Parse email addresses for (pos, (_, email)) in config @@ -116,7 +117,9 @@ impl MemoryDirectory { directory.domains.insert(domain.to_lowercase()); } - principal.append_str(PrincipalField::Emails, email.to_lowercase()); + principal + .emails + .push(CompactString::from_str_to_lowercase(email)); } // Parse mailing lists @@ -133,19 +136,19 @@ impl MemoryDirectory { } } - principal.set(PrincipalField::Name, name.clone()); + principal.name = name.as_str().into(); for (_, secret) in config.values((prefix.as_str(), "principals", lookup_id, "secret")) { - principal.append_str(PrincipalField::Secrets, secret.to_string()); + principal.secrets.push(secret.into()); } if let Some(description) = config.value((prefix.as_str(), "principals", lookup_id, "description")) { - principal.set(PrincipalField::Description, description.to_string()); + principal.description = Some(description.into()); } if let Some(quota) = config.property::((prefix.as_str(), "principals", lookup_id, "quota")) { - principal.set(PrincipalField::Quota, quota); + principal.quota = quota.into(); } directory.principals.push(principal); diff --git a/crates/directory/src/backend/memory/lookup.rs b/crates/directory/src/backend/memory/lookup.rs index 554d956e..41a3221e 100644 --- a/crates/directory/src/backend/memory/lookup.rs +++ b/crates/directory/src/backend/memory/lookup.rs @@ -4,14 +4,10 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use mail_send::Credentials; - -use crate::{ - Principal, QueryBy, - backend::{RcptType, internal::PrincipalField}, -}; - use super::{EmailType, MemoryDirectory}; +use crate::{Principal, QueryBy, backend::RcptType}; +use compact_str::CompactString; +use mail_send::Credentials; impl MemoryDirectory { pub async fn query(&self, by: QueryBy<'_>) -> trc::Result> { @@ -66,17 +62,17 @@ impl MemoryDirectory { Ok(self.emails_to_ids.contains_key(address).into()) } - pub async fn vrfy(&self, address: &str) -> trc::Result> { + pub async fn vrfy(&self, address: &str) -> trc::Result> { let mut result = Vec::new(); for (key, value) in &self.emails_to_ids { if key.contains(address) && value.iter().any(|t| matches!(t, EmailType::Primary(_))) { - result.push(key.clone()) + result.push(key.into()) } } Ok(result) } - pub async fn expn(&self, address: &str) -> trc::Result> { + pub async fn expn(&self, address: &str) -> trc::Result> { let mut result = Vec::new(); for (key, value) in &self.emails_to_ids { if key == address { @@ -84,10 +80,8 @@ impl MemoryDirectory { if let EmailType::List(uid) = item { for principal in &self.principals { if principal.id == *uid { - if let Some(addr) = - principal.iter_str(PrincipalField::Emails).next() - { - result.push(addr.to_string()) + if let Some(addr) = principal.emails.first() { + result.push(addr.clone()) } break; } diff --git a/crates/directory/src/backend/mod.rs b/crates/directory/src/backend/mod.rs index 2058580a..6f5d7573 100644 --- a/crates/directory/src/backend/mod.rs +++ b/crates/directory/src/backend/mod.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; + pub mod imap; pub mod internal; pub mod ldap; @@ -15,7 +17,7 @@ pub mod sql; #[derive(Debug, Default, Clone, PartialEq, Eq, Hash)] pub enum RcptType { Mailbox, - List(Vec), + List(Vec), #[default] Invalid, } diff --git a/crates/directory/src/backend/oidc/lookup.rs b/crates/directory/src/backend/oidc/lookup.rs index 42282712..6b7f5383 100644 --- a/crates/directory/src/backend/oidc/lookup.rs +++ b/crates/directory/src/backend/oidc/lookup.rs @@ -5,16 +5,16 @@ */ use ahash::HashMap; +use compact_str::CompactString; use mail_send::Credentials; use reqwest::{StatusCode, header::AUTHORIZATION}; use trc::{AddContext, AuthEvent}; use crate::{ - Principal, QueryBy, ROLE_USER, Type, + Principal, PrincipalData, QueryBy, ROLE_USER, Type, backend::{ RcptType, internal::{ - PrincipalField, lookup::DirectoryStore, manage::{self, ManageDirectory, UpdatePrincipal}, }, @@ -145,11 +145,11 @@ impl OpenIdDirectory { self.data_store.rcpt(address).await } - pub async fn vrfy(&self, address: &str) -> trc::Result> { + pub async fn vrfy(&self, address: &str) -> trc::Result> { self.data_store.vrfy(address).await } - pub async fn expn(&self, address: &str) -> trc::Result> { + pub async fn expn(&self, address: &str) -> trc::Result> { self.data_store.expn(address).await } @@ -185,11 +185,17 @@ impl BuildPrincipal for OpenIdResponse { .as_ref() .and_then(|field| self.take_field(field)); - Ok(Principal::new(u32::MAX, Type::Individual) - .with_field(PrincipalField::Name, username) - .with_field(PrincipalField::Emails, email) - .with_field(PrincipalField::Roles, ROLE_USER) - .with_opt_field(PrincipalField::Description, full_name)) + Ok(Principal { + id: u32::MAX, + typ: Type::Individual, + name: username.into(), + description: full_name.map(Into::into), + secrets: Default::default(), + emails: vec![email.into()], + quota: Default::default(), + tenant: Default::default(), + data: vec![PrincipalData::Roles(vec![ROLE_USER])], + }) } fn take_required_field(&mut self, field: &str) -> trc::Result { diff --git a/crates/directory/src/backend/smtp/lookup.rs b/crates/directory/src/backend/smtp/lookup.rs index 996a540a..f57aac0b 100644 --- a/crates/directory/src/backend/smtp/lookup.rs +++ b/crates/directory/src/backend/smtp/lookup.rs @@ -4,10 +4,11 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use mail_send::{Credentials, smtp::AssertReply}; use smtp_proto::Severity; -use crate::{IntoError, Principal, QueryBy, backend::RcptType}; +use crate::{IntoError, Principal, QueryBy, Type, backend::RcptType}; use super::{SmtpClient, SmtpDirectory}; @@ -66,7 +67,7 @@ impl SmtpDirectory { } } - pub async fn vrfy(&self, address: &str) -> trc::Result> { + pub async fn vrfy(&self, address: &str) -> trc::Result> { self.pool .get() .await @@ -75,7 +76,7 @@ impl SmtpDirectory { .await } - pub async fn expn(&self, address: &str) -> trc::Result> { + pub async fn expn(&self, address: &str) -> trc::Result> { self.pool .get() .await @@ -99,7 +100,7 @@ impl SmtpClient { .authenticate(credentials, &self.capabilities) .await { - Ok(_) => Ok(Some(Principal::default())), + Ok(_) => Ok(Some(Principal::new(u32::MAX, Type::Individual))), Err(err) => match &err { mail_send::Error::AuthenticationFailed(err) if err.code() == 535 => { self.num_auth_failures += 1; @@ -110,7 +111,7 @@ impl SmtpClient { } } - async fn expand(&mut self, command: &str) -> trc::Result> { + async fn expand(&mut self, command: &str) -> trc::Result> { let reply = self .client .cmd(command.as_bytes()) @@ -120,8 +121,8 @@ impl SmtpClient { 250 | 251 => Ok(reply .message() .split('\n') - .map(|p| p.to_string()) - .collect::>()), + .map(|p| p.into()) + .collect::>()), code @ (550 | 551 | 553 | 500 | 502) => { Err(trc::StoreEvent::NotSupported.ctx(trc::Key::Code, code)) } diff --git a/crates/directory/src/backend/sql/lookup.rs b/crates/directory/src/backend/sql/lookup.rs index 9b3bb423..9a4ba238 100644 --- a/crates/directory/src/backend/sql/lookup.rs +++ b/crates/directory/src/backend/sql/lookup.rs @@ -4,23 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use mail_send::Credentials; -use store::{NamedRows, Rows, Value}; -use trc::AddContext; - +use super::{SqlDirectory, SqlMappings}; use crate::{ - Principal, QueryBy, ROLE_ADMIN, ROLE_USER, Type, + Principal, PrincipalData, QueryBy, ROLE_ADMIN, ROLE_USER, Type, backend::{ RcptType, internal::{ - PrincipalField, PrincipalValue, lookup::DirectoryStore, manage::{self, ManageDirectory, UpdatePrincipal}, }, }, }; - -use super::{SqlDirectory, SqlMappings}; +use compact_str::CompactString; +use mail_send::Credentials; +use store::{NamedRows, Rows, Value}; +use trc::AddContext; impl SqlDirectory { pub async fn query( @@ -41,7 +39,10 @@ impl SqlDirectory { .caused_by(trc::location!())?, ) .caused_by(trc::location!())? - .map(|p| p.with_field(PrincipalField::Name, username.to_string())), + .map(|mut p| { + p.name = username.into(); + p + }), None, ), QueryBy::Id(uid) => { @@ -102,10 +103,7 @@ impl SqlDirectory { .caused_by(trc::location!())?; if !secrets.rows.is_empty() { - principal.set( - PrincipalField::Secrets, - PrincipalValue::StringList(secrets.into()), - ); + principal.secrets = secrets.into(); } } @@ -114,13 +112,8 @@ impl SqlDirectory { .await .caused_by(trc::location!())? { - ( - Some( - principal - .with_field(PrincipalField::Name, username.to_string()), - ), - None, - ) + principal.name = username.into(); + (Some(principal), None) } else { (None, None) } @@ -139,6 +132,7 @@ impl SqlDirectory { // Obtain members if return_member_of && !self.mappings.query_members.is_empty() { + let mut data = Vec::new(); for row in self .sql_store .sql_query::( @@ -150,8 +144,7 @@ impl SqlDirectory { .rows { if let Some(Value::Text(account_id)) = row.values.first() { - external_principal.append_int( - PrincipalField::MemberOf, + data.push( self.data_store .get_or_create_principal_id(account_id, Type::Group) .await @@ -159,22 +152,25 @@ impl SqlDirectory { ); } } + if !data.is_empty() { + external_principal.data.push(PrincipalData::MemberOf(data)); + } } // Obtain emails if !self.mappings.query_emails.is_empty() { - external_principal.set( - PrincipalField::Emails, - PrincipalValue::StringList( - self.sql_store - .sql_query::( - &self.mappings.query_emails, - vec![external_principal.name().into()], - ) - .await - .caused_by(trc::location!())? - .into(), - ), + let rows = self + .sql_store + .sql_query::( + &self.mappings.query_emails, + vec![external_principal.name().into()], + ) + .await + .caused_by(trc::location!())?; + external_principal.emails.extend( + rows.rows + .into_iter() + .flat_map(|v| v.values.into_iter().map(|v| v.into_lower_string())), ); } @@ -254,11 +250,11 @@ impl SqlDirectory { } } - pub async fn vrfy(&self, address: &str) -> trc::Result> { + pub async fn vrfy(&self, address: &str) -> trc::Result> { self.data_store.vrfy(address).await } - pub async fn expn(&self, address: &str) -> trc::Result> { + pub async fn expn(&self, address: &str) -> trc::Result> { self.data_store.expn(address).await } @@ -273,14 +269,14 @@ impl SqlMappings { return Ok(None); } - let mut principal = Principal::default(); + let mut principal = Principal::new(u32::MAX, Type::Individual); let mut role = ROLE_USER; if let Some(row) = rows.rows.into_iter().next() { for (name, value) in rows.names.into_iter().zip(row.values) { if name.eq_ignore_ascii_case(&self.column_secret) { if let Value::Text(text) = value { - principal.set(PrincipalField::Secrets, text.into_owned()); + principal.secrets.push(text.as_ref().into()); } } else if name.eq_ignore_ascii_case(&self.column_type) { match value.to_str().as_ref() { @@ -296,20 +292,24 @@ impl SqlMappings { } } else if name.eq_ignore_ascii_case(&self.column_description) { if let Value::Text(text) = value { - principal.set(PrincipalField::Description, text.into_owned()); + principal.description = Some(text.as_ref().into()); } } else if name.eq_ignore_ascii_case(&self.column_email) { if let Value::Text(text) = value { - principal.set(PrincipalField::Emails, text.to_lowercase()); + principal + .emails + .push(CompactString::from_str_to_lowercase(text.as_ref())); } } else if name.eq_ignore_ascii_case(&self.column_quota) { if let Value::Integer(quota) = value { - principal.set(PrincipalField::Quota, quota as u64); + principal.quota = (quota as u64).into(); } } } } - Ok(Some(principal.with_field(PrincipalField::Roles, role))) + principal.data.push(PrincipalData::Roles(vec![role])); + + Ok(Some(principal)) } } diff --git a/crates/directory/src/core/dispatch.rs b/crates/directory/src/core/dispatch.rs index 60bc81dc..f38fce5a 100644 --- a/crates/directory/src/core/dispatch.rs +++ b/crates/directory/src/core/dispatch.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use trc::AddContext; use crate::{ @@ -96,7 +97,7 @@ impl Directory { Ok(result) } - pub async fn vrfy(&self, address: &str) -> trc::Result> { + pub async fn vrfy(&self, address: &str) -> trc::Result> { match &self.store { DirectoryInner::Internal(store) => store.vrfy(address).await, DirectoryInner::Ldap(store) => store.vrfy(address).await, @@ -109,7 +110,7 @@ impl Directory { .caused_by(trc::location!()) } - pub async fn expn(&self, address: &str) -> trc::Result> { + pub async fn expn(&self, address: &str) -> trc::Result> { match &self.store { DirectoryInner::Internal(store) => store.expn(address).await, DirectoryInner::Ldap(store) => store.expn(address).await, diff --git a/crates/directory/src/core/principal.rs b/crates/directory/src/core/principal.rs index 85b67fcf..778f945b 100644 --- a/crates/directory/src/core/principal.rs +++ b/crates/directory/src/core/principal.rs @@ -6,6 +6,7 @@ use std::{collections::hash_map::Entry, fmt, str::FromStr}; +use compact_str::CompactString; use serde::{ Deserializer, Serializer, de::{self, IgnoredAny, Visitor}, @@ -14,11 +15,306 @@ use serde::{ use store::U64_LEN; use crate::{ - Permission, Principal, ROLE_ADMIN, Type, - backend::internal::{PrincipalField, PrincipalUpdate, PrincipalValue}, + Permission, PermissionGrant, Principal, PrincipalData, ROLE_ADMIN, Type, + backend::internal::{PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue}, }; impl Principal { + pub fn new(id: u32, typ: Type) -> Self { + Self { + id, + typ, + name: "".into(), + description: None, + secrets: Default::default(), + emails: Default::default(), + quota: Default::default(), + tenant: Default::default(), + data: Default::default(), + } + } + + pub fn id(&self) -> u32 { + self.id + } + + pub fn typ(&self) -> Type { + self.typ + } + + pub fn name(&self) -> &str { + self.name.as_str() + } + + pub fn quota(&self) -> u64 { + self.quota.unwrap_or_default() + } + + pub fn principal_quota(&self, typ: &Type) -> Option { + self.data + .iter() + .find_map(|d| { + if let PrincipalData::PrincipalQuota(q) = d { + Some(q) + } else { + None + } + }) + .and_then(|quotas| { + quotas + .iter() + .find_map(|q| if q.typ == *typ { Some(q.quota) } else { None }) + }) + } + + // SPDX-SnippetBegin + // SPDX-FileCopyrightText: 2020 Stalwart Labs Ltd + // SPDX-License-Identifier: LicenseRef-SEL + pub fn tenant(&self) -> Option { + self.tenant + } + // SPDX-SnippetEnd + + pub fn description(&self) -> Option<&str> { + self.description.as_ref().map(|d| d.as_str()) + } + + pub fn member_of(&self) -> &[u32] { + self.data + .iter() + .find_map(|item| { + if let PrincipalData::MemberOf(items) = item { + items.as_slice().into() + } else { + None + } + }) + .unwrap_or_default() + } + + pub fn member_of_mut(&mut self) -> Option<&mut Vec> { + self.data.iter_mut().find_map(|item| { + if let PrincipalData::MemberOf(items) = item { + items.into() + } else { + None + } + }) + } + + pub fn roles(&self) -> &[u32] { + self.data + .iter() + .find_map(|item| { + if let PrincipalData::Roles(items) = item { + items.as_slice().into() + } else { + None + } + }) + .unwrap_or_default() + } + + pub fn permissions(&self) -> &[PermissionGrant] { + self.data + .iter() + .find_map(|item| { + if let PrincipalData::Permissions(items) = item { + items.as_slice().into() + } else { + None + } + }) + .unwrap_or_default() + } + + pub fn urls(&self) -> &[CompactString] { + self.data + .iter() + .find_map(|item| { + if let PrincipalData::Urls(items) = item { + items.as_slice().into() + } else { + None + } + }) + .unwrap_or_default() + } + + pub fn roles_mut(&mut self) -> Option<&mut Vec> { + self.data.iter_mut().find_map(|item| { + if let PrincipalData::Roles(items) = item { + items.into() + } else { + None + } + }) + } + + pub fn lists(&self) -> &[u32] { + self.data + .iter() + .find_map(|item| { + if let PrincipalData::Lists(items) = item { + items.as_slice().into() + } else { + None + } + }) + .unwrap_or_default() + } + + pub fn picture(&self) -> Option<&CompactString> { + self.data.iter().find_map(|item| { + if let PrincipalData::Picture(picture) = item { + picture.into() + } else { + None + } + }) + } + + pub fn picture_mut(&mut self) -> Option<&mut CompactString> { + self.data.iter_mut().find_map(|item| { + if let PrincipalData::Picture(picture) = item { + picture.into() + } else { + None + } + }) + } + + pub fn add_permission(&mut self, permission: Permission, grant: bool) { + if let Some(permissions) = self.data.iter_mut().find_map(|item| { + if let PrincipalData::Permissions(permissions) = item { + Some(permissions) + } else { + None + } + }) { + if let Some(current) = permissions.iter_mut().find(|p| p.permission == permission) { + current.grant = grant; + } else { + permissions.push(PermissionGrant { permission, grant }); + } + } else { + self.data + .push(PrincipalData::Permissions(vec![PermissionGrant { + permission, + grant, + }])); + } + } + + pub fn add_permissions(&mut self, iter: impl Iterator) { + if let Some(permissions) = self.data.iter_mut().find_map(|item| { + if let PrincipalData::Permissions(permissions) = item { + Some(permissions) + } else { + None + } + }) { + permissions.extend(iter); + } else { + self.data.push(PrincipalData::Permissions(iter.collect())); + } + } + + pub fn remove_permission(&mut self, permission: Permission, grant: bool) { + if let Some(permissions) = self.data.iter_mut().find_map(|item| { + if let PrincipalData::Permissions(permissions) = item { + Some(permissions) + } else { + None + } + }) { + if let Some(idx) = permissions + .iter_mut() + .position(|p| p.permission == permission && p.grant == grant) + { + permissions.swap_remove(idx); + } + } + } + + pub fn remove_permissions(&mut self, grant: bool) { + if let Some(permissions) = self.data.iter_mut().find_map(|item| { + if let PrincipalData::Permissions(permissions) = item { + Some(permissions) + } else { + None + } + }) { + permissions.retain(|p| p.grant != grant); + } + } + + pub fn update_external(&mut self, mut external: Principal) -> Vec { + let mut updates = Vec::new(); + + // Add external members + if let Some(member_of) = external.member_of_mut().filter(|s| !s.is_empty()) { + self.data + .push(PrincipalData::MemberOf(std::mem::take(member_of))); + } + + // If the principal has no roles, take the ones from the external principal + if let Some(roles) = external.roles_mut().filter(|s| !s.is_empty()) { + if self.roles().is_empty() { + self.data.push(PrincipalData::Roles(std::mem::take(roles))); + } + } + + if external.description.as_ref().is_some_and(|v| !v.is_empty()) + && self.description != external.description + { + self.description = external.description; + updates.push(PrincipalUpdate::set( + PrincipalField::Description, + PrincipalValue::String(self.description.clone().unwrap()), + )); + } + + for (name, field, external_field) in [ + (PrincipalField::Secrets, &mut self.secrets, external.secrets), + (PrincipalField::Emails, &mut self.emails, external.emails), + ] { + if !external_field.is_empty() && &external_field != field { + *field = external_field; + updates.push(PrincipalUpdate::set( + name, + PrincipalValue::StringList(field.clone()), + )); + } + } + + if external.quota.is_some() && self.quota != external.quota { + self.quota = external.quota; + updates.push(PrincipalUpdate::set( + PrincipalField::Quota, + PrincipalValue::Integer(self.quota.unwrap()), + )); + } + + updates + } + + pub fn fallback_admin(fallback_pass: impl Into) -> Self { + Principal { + id: u32::MAX, + typ: Type::Individual, + name: "Fallback Administrator".into(), + secrets: vec![fallback_pass.into()], + data: vec![PrincipalData::MemberOf(vec![ROLE_ADMIN])], + description: Default::default(), + emails: Default::default(), + quota: Default::default(), + tenant: Default::default(), + } + } +} + +impl PrincipalSet { pub fn new(id: u32, typ: Type) -> Self { Self { id, @@ -67,7 +363,7 @@ impl Principal { self.fields.get(&key).and_then(|v| v.as_int()) } - pub fn get_str_array(&self, key: PrincipalField) -> Option<&[String]> { + pub fn get_str_array(&self, key: PrincipalField) -> Option<&[CompactString]> { self.fields.get(&key).and_then(|v| match v { PrincipalValue::StringList(v) => Some(v.as_slice()), PrincipalValue::String(v) => Some(std::slice::from_ref(v)), @@ -87,12 +383,12 @@ impl Principal { self.fields.remove(&key) } - pub fn take_str(&mut self, key: PrincipalField) -> Option { + pub fn take_str(&mut self, key: PrincipalField) -> Option { self.take(key).and_then(|v| match v { PrincipalValue::String(s) => Some(s), PrincipalValue::StringList(l) => l.into_iter().next(), - PrincipalValue::Integer(i) => Some(i.to_string()), - PrincipalValue::IntegerList(l) => l.into_iter().next().map(|i| i.to_string()), + PrincipalValue::Integer(i) => Some(i.to_string().into()), + PrincipalValue::IntegerList(l) => l.into_iter().next().map(|i| i.to_string().into()), }) } @@ -105,7 +401,7 @@ impl Principal { }) } - pub fn take_str_array(&mut self, key: PrincipalField) -> Option> { + pub fn take_str_array(&mut self, key: PrincipalField) -> Option> { self.take(key).map(|v| v.into_str_array()) } @@ -116,7 +412,7 @@ impl Principal { pub fn iter_str( &self, key: PrincipalField, - ) -> Box + Sync + Send + '_> { + ) -> Box + Sync + Send + '_> { self.fields .get(&key) .map(|v| v.iter_str()) @@ -126,7 +422,7 @@ impl Principal { pub fn iter_mut_str( &mut self, key: PrincipalField, - ) -> Box + Sync + Send + '_> { + ) -> Box + Sync + Send + '_> { self.fields .get_mut(&key) .map(|v| v.iter_mut_str()) @@ -192,7 +488,11 @@ impl Principal { self } - pub fn append_str(&mut self, key: PrincipalField, value: impl Into) -> &mut Self { + pub fn append_str( + &mut self, + key: PrincipalField, + value: impl Into, + ) -> &mut Self { let value = value.into(); match self.fields.entry(key) { Entry::Occupied(v) => { @@ -210,12 +510,12 @@ impl Principal { } } PrincipalValue::Integer(i) => { - *v = PrincipalValue::StringList(vec![i.to_string(), value]); + *v = PrincipalValue::StringList(vec![i.to_string().into(), value]); } PrincipalValue::IntegerList(l) => { *v = PrincipalValue::StringList( l.iter() - .map(|i| i.to_string()) + .map(|i| i.to_string().into()) .chain(std::iter::once(value)) .collect(), ); @@ -229,7 +529,11 @@ impl Principal { self } - pub fn prepend_str(&mut self, key: PrincipalField, value: impl Into) -> &mut Self { + pub fn prepend_str( + &mut self, + key: PrincipalField, + value: impl Into, + ) -> &mut Self { let value = value.into(); match self.fields.entry(key) { Entry::Occupied(v) => { @@ -247,12 +551,12 @@ impl Principal { } } PrincipalValue::Integer(i) => { - *v = PrincipalValue::StringList(vec![value, i.to_string()]); + *v = PrincipalValue::StringList(vec![value, i.to_string().into()]); } PrincipalValue::IntegerList(l) => { *v = PrincipalValue::StringList( std::iter::once(value) - .chain(l.iter().map(|i| i.to_string())) + .chain(l.iter().map(|i| i.to_string().into())) .collect(), ); } @@ -325,7 +629,7 @@ impl Principal { pub fn retain_str(&mut self, key: PrincipalField, mut f: F) where - F: FnMut(&String) -> bool, + F: FnMut(&CompactString) -> bool, { if let Some(value) = self.fields.get_mut(&key) { match value { @@ -366,82 +670,6 @@ impl Principal { } } } - - pub fn update_external(&mut self, mut external: Principal) -> Vec { - let mut updates = Vec::new(); - if let Some(name) = external - .take_str(PrincipalField::Description) - .filter(|s| !s.is_empty()) - { - if self.get_str(PrincipalField::Description) != Some(name.as_str()) { - updates.push(PrincipalUpdate::set( - PrincipalField::Description, - PrincipalValue::String(name.clone()), - )); - self.set(PrincipalField::Description, name); - } - } - - for field in [PrincipalField::Secrets, PrincipalField::Emails] { - if let Some(secrets) = external.take_str_array(field).filter(|s| !s.is_empty()) { - if self.get_str_array(field) != Some(secrets.as_ref()) { - updates.push(PrincipalUpdate::set( - field, - PrincipalValue::StringList(secrets.clone()), - )); - self.set(field, secrets); - } - } - } - - if let Some(quota) = external.take_int(PrincipalField::Quota) { - if self.get_int(PrincipalField::Quota) != Some(quota) { - updates.push(PrincipalUpdate::set( - PrincipalField::Quota, - PrincipalValue::Integer(quota), - )); - self.set(PrincipalField::Quota, quota); - } - } - - // Add external members - if let Some(member_of) = external - .take_int_array(PrincipalField::MemberOf) - .filter(|s| !s.is_empty()) - { - self.set(PrincipalField::MemberOf, member_of); - } - - // If the principal has no roles, take the ones from the external principal - if let Some(member_of) = external - .take_int_array(PrincipalField::Roles) - .filter(|s| !s.is_empty()) - { - if self - .get_int_array(PrincipalField::Roles) - .filter(|s| !s.is_empty()) - .is_none() - { - self.set(PrincipalField::Roles, member_of); - } - } - - updates - } - - pub fn fallback_admin(fallback_pass: impl Into) -> Self { - Principal { - id: u32::MAX, - typ: Type::Individual, - ..Default::default() - } - .with_field(PrincipalField::Name, "Fallback Administrator") - .with_field( - PrincipalField::Secrets, - PrincipalValue::String(fallback_pass.into()), - ) - .with_field(PrincipalField::Roles, ROLE_ADMIN) - } } impl PrincipalValue { @@ -461,7 +689,7 @@ impl PrincipalValue { } } - pub fn iter_str(&self) -> Box + Sync + Send + '_> { + pub fn iter_str(&self) -> Box + Sync + Send + '_> { match self { PrincipalValue::String(v) => Box::new(std::iter::once(v)), PrincipalValue::StringList(v) => Box::new(v.iter()), @@ -469,7 +697,9 @@ impl PrincipalValue { } } - pub fn iter_mut_str(&mut self) -> Box + Sync + Send + '_> { + pub fn iter_mut_str( + &mut self, + ) -> Box + Sync + Send + '_> { match self { PrincipalValue::String(v) => Box::new(std::iter::once(v)), PrincipalValue::StringList(v) => Box::new(v.iter_mut()), @@ -501,12 +731,12 @@ impl PrincipalValue { } } - pub fn into_str_array(self) -> Vec { + pub fn into_str_array(self) -> Vec { match self { PrincipalValue::StringList(v) => v, PrincipalValue::String(v) => vec![v], - PrincipalValue::Integer(v) => vec![v.to_string()], - PrincipalValue::IntegerList(v) => v.into_iter().map(|v| v.to_string()).collect(), + PrincipalValue::Integer(v) => vec![v.to_string().into()], + PrincipalValue::IntegerList(v) => v.into_iter().map(|v| v.to_string().into()).collect(), } } @@ -546,20 +776,20 @@ impl From for PrincipalValue { } } -impl From for PrincipalValue { - fn from(v: String) -> Self { +impl From for PrincipalValue { + fn from(v: CompactString) -> Self { Self::String(v) } } impl From<&str> for PrincipalValue { fn from(v: &str) -> Self { - Self::String(v.to_string()) + Self::String(v.into()) } } -impl From> for PrincipalValue { - fn from(v: Vec) -> Self { +impl From> for PrincipalValue { + fn from(v: Vec) -> Self { Self::StringList(v) } } @@ -632,6 +862,8 @@ impl Type { } } + pub const MAX_ID: usize = 11; + pub fn from_u8(value: u8) -> Self { match value { 0 => Type::Individual, @@ -659,7 +891,7 @@ impl FromStr for Type { } } -impl serde::Serialize for Principal { +impl serde::Serialize for PrincipalSet { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -702,7 +934,7 @@ impl<'de> serde::Deserialize<'de> for PrincipalValue { where E: de::Error, { - Ok(PrincipalValue::String(String::new())) + Ok(PrincipalValue::String("".into())) } fn visit_some(self, deserializer: D) -> Result @@ -724,7 +956,7 @@ impl<'de> serde::Deserialize<'de> for PrincipalValue { E: de::Error, { if value.len() <= MAX_STRING_LEN { - Ok(PrincipalValue::String(value)) + Ok(PrincipalValue::String(value.into())) } else { Err(serde::de::Error::custom("string too long")) } @@ -735,7 +967,7 @@ impl<'de> serde::Deserialize<'de> for PrincipalValue { E: de::Error, { if value.len() <= MAX_STRING_LEN { - Ok(PrincipalValue::String(value.to_string())) + Ok(PrincipalValue::String(value.into())) } else { Err(serde::de::Error::custom("string too long")) } @@ -752,7 +984,7 @@ impl<'de> serde::Deserialize<'de> for PrincipalValue { match value { StringOrU64::String(s) => { if s.len() <= MAX_STRING_LEN { - vec_string.push(s); + vec_string.push(s.into()); } else { return Err(serde::de::Error::custom("string too long")); } @@ -774,7 +1006,7 @@ impl<'de> serde::Deserialize<'de> for PrincipalValue { } } -impl<'de> serde::Deserialize<'de> for Principal { +impl<'de> serde::Deserialize<'de> for PrincipalSet { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, @@ -783,7 +1015,7 @@ impl<'de> serde::Deserialize<'de> for Principal { // Deserialize the principal impl<'de> Visitor<'de> for PrincipalVisitor { - type Value = Principal; + type Value = PrincipalSet; fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { formatter.write_str("a valid principal") @@ -793,7 +1025,7 @@ impl<'de> serde::Deserialize<'de> for Principal { where A: de::MapAccess<'de>, { - let mut principal = Principal::default(); + let mut principal = PrincipalSet::default(); while let Some(key) = map.next_key::<&str>()? { let key = PrincipalField::try_parse(key) @@ -810,19 +1042,19 @@ impl<'de> serde::Deserialize<'de> for Principal { })?; let value = match key { - PrincipalField::Name => { - PrincipalValue::String(map.next_value::().and_then(|v| { + PrincipalField::Name => PrincipalValue::String( + map.next_value::().and_then(|v| { if v.len() <= MAX_STRING_LEN { Ok(v) } else { Err(serde::de::Error::custom("string too long")) } - })?) - } + })?, + ), PrincipalField::Description | PrincipalField::Tenant | PrincipalField::Picture => { - if let Some(v) = map.next_value::>()? { + if let Some(v) = map.next_value::>()? { if v.len() <= MAX_STRING_LEN { PrincipalValue::String(v) } else { @@ -867,7 +1099,7 @@ impl<'de> serde::Deserialize<'de> for Principal { } }; - principal.set(key, value); + principal.fields.insert(key, value); } Ok(principal) @@ -934,8 +1166,8 @@ impl<'de> serde::Deserialize<'de> for StringOrU64 { #[derive(Debug)] enum StringOrMany { - One(String), - Many(Vec), + One(CompactString), + Many(Vec), } impl<'de> serde::Deserialize<'de> for StringOrMany { @@ -957,7 +1189,7 @@ impl<'de> serde::Deserialize<'de> for StringOrMany { E: de::Error, { if value.len() <= MAX_STRING_LEN { - Ok(StringOrMany::One(value.to_string())) + Ok(StringOrMany::One(value.into())) } else { Err(serde::de::Error::custom("string too long")) } @@ -968,7 +1200,7 @@ impl<'de> serde::Deserialize<'de> for StringOrMany { E: de::Error, { if v.len() <= MAX_STRING_LEN { - Ok(StringOrMany::One(v)) + Ok(StringOrMany::One(v.into())) } else { Err(serde::de::Error::custom("string too long")) } @@ -980,7 +1212,7 @@ impl<'de> serde::Deserialize<'de> for StringOrMany { { let mut vec = Vec::new(); - while let Some(value) = seq.next_element::()? { + while let Some(value) = seq.next_element::()? { vec.push(value); } diff --git a/crates/directory/src/core/secret.rs b/crates/directory/src/core/secret.rs index 4b4f216c..e17e25d1 100644 --- a/crates/directory/src/core/secret.rs +++ b/crates/directory/src/core/secret.rs @@ -19,7 +19,6 @@ use tokio::sync::oneshot; use totp_rs::TOTP; use crate::Principal; -use crate::backend::internal::PrincipalField; use crate::backend::internal::SpecialSecrets; impl Principal { @@ -31,7 +30,7 @@ impl Principal { let mut is_authenticated = false; let mut is_app_authenticated = false; - for secret in self.iter_str(PrincipalField::Secrets) { + for secret in self.secrets.iter() { if secret.is_otp_auth() { if !is_totp_verified && !is_totp_token_missing { is_totp_required = true; @@ -96,7 +95,7 @@ impl Principal { } else { if is_totp_verified { // TOTP URL appeared after password hash in secrets list - for secret in self.iter_str(PrincipalField::Secrets) { + for secret in &self.secrets { if secret.is_password() && verify_secret_hash(secret, code).await? { return Ok(true); } diff --git a/crates/directory/src/lib.rs b/crates/directory/src/lib.rs index b85c2af6..6d832400 100644 --- a/crates/directory/src/lib.rs +++ b/crates/directory/src/lib.rs @@ -4,23 +4,25 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +#![warn(clippy::large_futures)] + use core::cache::CachedDirectory; use std::{fmt::Debug, sync::Arc}; use ahash::AHashMap; use backend::{ imap::{ImapDirectory, ImapError}, - internal::{PrincipalField, PrincipalValue}, ldap::LdapDirectory, memory::MemoryDirectory, smtp::SmtpDirectory, sql::SqlDirectory, }; +use compact_str::CompactString; use deadpool::managed::PoolError; use ldap3::LdapError; use mail_send::Credentials; use proc_macros::EnumMethods; -use store::Store; +use store::{SERIALIZE_PRINCIPAL_V1, SerializedVersion, Store}; use trc::ipc::bitset::Bitset; pub mod backend; @@ -31,15 +33,68 @@ pub struct Directory { pub cache: Option, } -#[derive(Debug, Default, Clone, PartialEq, Eq)] +#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] pub struct Principal { - pub(crate) id: u32, - pub(crate) typ: Type, - - pub(crate) fields: AHashMap, + pub id: u32, + pub typ: Type, + pub name: CompactString, + pub description: Option, + pub secrets: Vec, + pub emails: Vec, + pub quota: Option, + pub tenant: Option, + pub data: Vec, } -#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +impl SerializedVersion for Principal { + fn serialize_version() -> u8 { + SERIALIZE_PRINCIPAL_V1 + } +} + +#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] +pub enum PrincipalData { + MemberOf(Vec), + Roles(Vec), + Lists(Vec), + Permissions(Vec), + Picture(CompactString), + ExternalMembers(Vec), + Urls(Vec), + PrincipalQuota(Vec), +} + +#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PrincipalQuota { + pub quota: u64, + pub typ: Type, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MemberOf { + pub principal_id: u32, + pub typ: Type, +} + +#[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] +pub struct PermissionGrant { + pub permission: Permission, + pub grant: bool, +} + +#[derive( + rkyv::Archive, + rkyv::Deserialize, + rkyv::Serialize, + Debug, + Default, + Clone, + Copy, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, +)] #[serde(rename_all = "camelCase")] pub enum Type { #[default] @@ -56,10 +111,19 @@ pub enum Type { OauthClient = 11, } -pub const MAX_TYPE_ID: usize = 11; - #[derive( - Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, EnumMethods, + rkyv::Archive, + rkyv::Deserialize, + rkyv::Serialize, + Debug, + Clone, + Copy, + PartialEq, + Eq, + Hash, + serde::Serialize, + serde::Deserialize, + EnumMethods, )] #[serde(rename_all = "kebab-case")] pub enum Permission { diff --git a/crates/email/src/identity/mod.rs b/crates/email/src/identity/mod.rs index fd372741..9a7e73f7 100644 --- a/crates/email/src/identity/mod.rs +++ b/crates/email/src/identity/mod.rs @@ -6,28 +6,29 @@ pub mod index; -use store::{SERIALIZE_OBJ_03_V1, SerializedVersion}; +use compact_str::CompactString; +use store::{SERIALIZE_IDENTITY_V1, SerializedVersion}; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] pub struct Identity { - pub name: String, - pub email: String, + pub name: CompactString, + pub email: CompactString, pub reply_to: Option>, pub bcc: Option>, - pub text_signature: String, - pub html_signature: String, + pub text_signature: CompactString, + pub html_signature: CompactString, } #[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] pub struct EmailAddress { - pub name: Option, - pub email: String, + pub name: Option, + pub email: CompactString, } impl SerializedVersion for Identity { fn serialize_version() -> u8 { - SERIALIZE_OBJ_03_V1 + SERIALIZE_IDENTITY_V1 } } diff --git a/crates/email/src/mailbox/mod.rs b/crates/email/src/mailbox/mod.rs index d7d6e64f..3def19ef 100644 --- a/crates/email/src/mailbox/mod.rs +++ b/crates/email/src/mailbox/mod.rs @@ -5,8 +5,9 @@ */ use common::config::jmap::settings::SpecialUse; +use compact_str::CompactString; use jmap_proto::types::value::AclGrant; -use store::{SERIALIZE_OBJ_04_V1, SerializedVersion}; +use store::{SERIALIZE_MAILBOX_V1, SerializedVersion}; pub mod cache; pub mod destroy; @@ -24,7 +25,7 @@ pub const TOMBSTONE_ID: u32 = u32::MAX - 1; #[derive(rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Clone, PartialEq, Eq)] #[rkyv(derive(Debug))] pub struct Mailbox { - pub name: String, + pub name: CompactString, pub role: SpecialUse, pub parent_id: u32, pub sort_order: Option, @@ -42,12 +43,12 @@ pub struct UidMailbox { impl SerializedVersion for Mailbox { fn serialize_version() -> u8 { - SERIALIZE_OBJ_04_V1 + SERIALIZE_MAILBOX_V1 } } impl Mailbox { - pub fn new(name: impl Into) -> Self { + pub fn new(name: impl Into) -> Self { Mailbox { name: name.into(), role: SpecialUse::None, diff --git a/crates/email/src/message/cache.rs b/crates/email/src/message/cache.rs index 3cd2e3a3..0ba1a5ee 100644 --- a/crates/email/src/message/cache.rs +++ b/crates/email/src/message/cache.rs @@ -197,6 +197,9 @@ async fn full_cache_build( .await .caused_by(trc::location!())?; + cache.items.shrink_to_fit(); + cache.index.shrink_to_fit(); + Ok(Arc::new(cache)) } diff --git a/crates/email/src/message/crypto.rs b/crates/email/src/message/crypto.rs index c1792bf7..07cb7501 100644 --- a/crates/email/src/message/crypto.rs +++ b/crates/email/src/message/crypto.rs @@ -26,7 +26,7 @@ use rasn_cms::{ }; use rsa::{Pkcs1v15Encrypt, RsaPublicKey, pkcs1::DecodeRsaPublicKey}; use sequoia_openpgp as openpgp; -use store::{Deserialize, SERIALIZE_OBJ_05_V1, SerializedVersion, write::Archive}; +use store::{Deserialize, SERIALIZE_CRYPTO_V1, SerializedVersion, write::Archive}; const P: openpgp::policy::StandardPolicy<'static> = openpgp::policy::StandardPolicy::new(); @@ -86,7 +86,7 @@ pub struct EncryptionParams { impl SerializedVersion for EncryptionParams { fn serialize_version() -> u8 { - SERIALIZE_OBJ_05_V1 + SERIALIZE_CRYPTO_V1 } } diff --git a/crates/email/src/message/delivery.rs b/crates/email/src/message/delivery.rs index d7045e53..5be00bd9 100644 --- a/crates/email/src/message/delivery.rs +++ b/crates/email/src/message/delivery.rs @@ -5,6 +5,7 @@ */ use common::Server; +use compact_str::CompactString; use directory::Permission; use jmap_proto::types::{state::StateChange, type_state::DataType}; use mail_parser::MessageParser; @@ -18,8 +19,8 @@ use super::ingest::{EmailIngest, IngestEmail, IngestSource}; #[derive(Debug)] pub struct IngestMessage { - pub sender_address: String, - pub recipients: Vec, + pub sender_address: CompactString, + pub recipients: Vec, pub message_blob: BlobHash, pub message_size: u64, pub session_id: u64, @@ -43,8 +44,8 @@ pub struct LocalDeliveryResult { } pub struct AutogeneratedMessage { - pub sender_address: String, - pub recipients: Vec, + pub sender_address: CompactString, + pub recipients: Vec, pub message: Vec, } diff --git a/crates/email/src/message/metadata.rs b/crates/email/src/message/metadata.rs index 6b1282f0..da26e356 100644 --- a/crates/email/src/message/metadata.rs +++ b/crates/email/src/message/metadata.rs @@ -20,7 +20,7 @@ use rkyv::{ string::ArchivedString, vec::ArchivedVec, }; -use store::{SERIALIZE_OBJ_06_V1, SERIALIZE_OBJ_07_V1, SerializedVersion}; +use store::{SERIALIZE_MSGDATA_V1, SERIALIZE_MSGMETADATA_V1, SerializedVersion}; use utils::BlobHash; use crate::mailbox::{ArchivedUidMailbox, UidMailbox}; @@ -48,13 +48,13 @@ impl IndexableAndSerializableObject for MessageData {} impl SerializedVersion for MessageData { fn serialize_version() -> u8 { - SERIALIZE_OBJ_06_V1 + SERIALIZE_MSGDATA_V1 } } impl SerializedVersion for MessageMetadata { fn serialize_version() -> u8 { - SERIALIZE_OBJ_07_V1 + SERIALIZE_MSGMETADATA_V1 } } diff --git a/crates/email/src/push/mod.rs b/crates/email/src/push/mod.rs index add1187e..09421759 100644 --- a/crates/email/src/push/mod.rs +++ b/crates/email/src/push/mod.rs @@ -4,18 +4,19 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use jmap_proto::types::type_state::DataType; -use store::{SERIALIZE_OBJ_08_V1, SerializedVersion}; +use store::{SERIALIZE_PUSH_V1, SerializedVersion}; use utils::map::bitmap::Bitmap; #[derive( rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Default, Debug, Clone, PartialEq, Eq, )] pub struct PushSubscription { - pub url: String, - pub device_client_id: String, + pub url: CompactString, + pub device_client_id: CompactString, pub expires: u64, - pub verification_code: String, + pub verification_code: CompactString, pub verified: bool, pub types: Bitmap, pub keys: Option, @@ -29,6 +30,6 @@ pub struct Keys { impl SerializedVersion for PushSubscription { fn serialize_version() -> u8 { - SERIALIZE_OBJ_08_V1 + SERIALIZE_PUSH_V1 } } diff --git a/crates/email/src/sieve/index.rs b/crates/email/src/sieve/index.rs index b70fc765..da46efbc 100644 --- a/crates/email/src/sieve/index.rs +++ b/crates/email/src/sieve/index.rs @@ -14,7 +14,7 @@ impl IndexableObject for SieveScript { [ IndexValue::Index { field: Property::Name.into(), - value: self.name.to_lowercase().into(), + value: self.name.as_str().to_lowercase().into(), }, IndexValue::Index { field: Property::IsActive.into(), diff --git a/crates/email/src/sieve/ingest.rs b/crates/email/src/sieve/ingest.rs index ef453f04..92b0fa16 100644 --- a/crates/email/src/sieve/ingest.rs +++ b/crates/email/src/sieve/ingest.rs @@ -20,7 +20,8 @@ use crate::{ use common::{ Server, auth::AccessToken, config::jmap::settings::SpecialUse, scripts::plugins::PluginContext, }; -use directory::{Permission, QueryBy, backend::internal::PrincipalField}; +use compact_str::CompactString; +use directory::{Permission, QueryBy}; use jmap_proto::types::{collection::Collection, id::Id, keyword::Keyword, property::Property}; use mail_parser::MessageParser; use sieve::{Envelope, Event, Input, Mailbox, Recipient, Sieve}; @@ -116,23 +117,23 @@ impl SieveScriptIngest for Server { .query(QueryBy::Id(account_id), false) .await .caused_by(trc::location!())? - .and_then(|mut p| { + .and_then(|p| { instance.set_user_full_name(p.description().unwrap_or_else(|| p.name())); - p.take_str_array(PrincipalField::Emails) - .unwrap_or_default() - .into_iter() - .next() + p.emails.into_iter().next() }); // Set account address - let mail_from = mail_from.unwrap_or_else(|| envelope_to.to_string()); + let mail_from = mail_from.unwrap_or_else(|| envelope_to.into()); instance.set_user_address(&mail_from); // Set envelope instance.set_envelope(Envelope::From, envelope_from); instance.set_envelope(Envelope::To, envelope_to); - let mut input = Input::script(active_script.script_name, active_script.script.clone()); + let mut input = Input::script( + active_script.script_name.to_string(), + active_script.script.clone(), + ); let mut do_discard = false; let mut do_deliver = false; @@ -376,9 +377,11 @@ impl SieveScriptIngest for Server { } => { input = true.into(); if let Some(message) = messages.get(message_id) { - let recipients = match recipient { - Recipient::Address(rcpt) => vec![rcpt], - Recipient::Group(rcpts) => rcpts, + let recipients: Vec = match recipient { + Recipient::Address(rcpt) => vec![rcpt.into()], + Recipient::Group(rcpts) => { + rcpts.into_iter().map(CompactString::from).collect() + } Recipient::List(_) => { // Not yet implemented continue; @@ -646,7 +649,7 @@ impl SieveScriptIngest for Server { { Ok(CompiledScript { script: sieve.inner, - name: unarchived_script.name.to_string(), + name: unarchived_script.name.as_str().into(), hash, }) } else { @@ -719,6 +722,6 @@ impl SieveScriptIngest for Server { pub struct CompiledScript { pub script: Sieve, - pub name: String, + pub name: CompactString, pub hash: u32, } diff --git a/crates/email/src/sieve/mod.rs b/crates/email/src/sieve/mod.rs index 95b6e1c2..29f673d8 100644 --- a/crates/email/src/sieve/mod.rs +++ b/crates/email/src/sieve/mod.rs @@ -7,8 +7,9 @@ use std::sync::Arc; use common::KV_SIEVE_ID; +use compact_str::CompactString; use sieve::Sieve; -use store::{SERIALIZE_OBJ_09_V1, SerializedVersion, blake3}; +use store::{SERIALIZE_SIEVE_V1, SerializedVersion, blake3}; use utils::BlobHash; pub mod activate; @@ -20,7 +21,7 @@ pub mod ingest; pub struct ActiveScript { pub document_id: u32, pub hash: u32, - pub script_name: String, + pub script_name: CompactString, pub script: Arc, } @@ -29,7 +30,7 @@ pub struct ActiveScript { )] #[rkyv(derive(Debug))] pub struct SieveScript { - pub name: String, + pub name: CompactString, pub is_active: bool, pub blob_hash: BlobHash, pub size: u32, @@ -38,7 +39,7 @@ pub struct SieveScript { impl SerializedVersion for SieveScript { fn serialize_version() -> u8 { - SERIALIZE_OBJ_09_V1 + SERIALIZE_SIEVE_V1 } } @@ -49,13 +50,13 @@ impl SerializedVersion for SieveScript { pub struct VacationResponse { pub from_date: Option, pub to_date: Option, - pub subject: Option, - pub text_body: Option, - pub html_body: Option, + pub subject: Option, + pub text_body: Option, + pub html_body: Option, } impl SieveScript { - pub fn new(name: impl Into, blob_hash: BlobHash) -> Self { + pub fn new(name: impl Into, blob_hash: BlobHash) -> Self { SieveScript { name: name.into(), is_active: false, @@ -65,7 +66,7 @@ impl SieveScript { } } - pub fn with_name(mut self, name: impl Into) -> Self { + pub fn with_name(mut self, name: impl Into) -> Self { self.name = name.into(); self } diff --git a/crates/email/src/submission/mod.rs b/crates/email/src/submission/mod.rs index 04e73530..b7959939 100644 --- a/crates/email/src/submission/mod.rs +++ b/crates/email/src/submission/mod.rs @@ -4,7 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use store::{SERIALIZE_OBJ_10_V1, SerializedVersion}; +use store::{SERIALIZE_SUBMISSION_V1, SerializedVersion}; use utils::map::vec_map::VecMap; pub mod index; @@ -25,7 +25,7 @@ pub struct EmailSubmission { impl SerializedVersion for EmailSubmission { fn serialize_version() -> u8 { - SERIALIZE_OBJ_10_V1 + SERIALIZE_SUBMISSION_V1 } } diff --git a/crates/groupware/Cargo.toml b/crates/groupware/Cargo.toml index ebfb6a04..eb8677b1 100644 --- a/crates/groupware/Cargo.toml +++ b/crates/groupware/Cargo.toml @@ -16,6 +16,7 @@ calcard = { path = "/Users/me/code/calcard", features = ["rkyv"] } hashify = "0.2" rkyv = { version = "0.8.10", features = ["little_endian"] } percent-encoding = "2.3.1" +compact_str = "0.9.0" [features] test_mode = [] diff --git a/crates/groupware/src/calendar/mod.rs b/crates/groupware/src/calendar/mod.rs index c14fb283..f2c7b800 100644 --- a/crates/groupware/src/calendar/mod.rs +++ b/crates/groupware/src/calendar/mod.rs @@ -7,9 +7,10 @@ use std::collections::HashMap; use calcard::icalendar::ICalendar; +use compact_str::CompactString; use dav_proto::schema::request::DeadProperty; use jmap_proto::types::{acl::Acl, value::AclGrant}; -use store::{SERIALIZE_OBJ_14_V1, SERIALIZE_OBJ_16_V1, SerializedVersion, ahash}; +use store::{SERIALIZE_CALENDAR_V1, SERIALIZE_CALENDAREVENT_V1, SerializedVersion, ahash}; use utils::map::vec_map::VecMap; use crate::DavName; @@ -18,7 +19,7 @@ use crate::DavName; rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] pub struct Calendar { - pub name: String, + pub name: CompactString, pub preferences: HashMap, pub acls: Vec, pub dead_properties: DeadProperty, @@ -30,16 +31,16 @@ pub struct Calendar { rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] pub struct CalendarPreferences { - pub name: String, - pub description: Option, + pub name: CompactString, + pub description: Option, pub sort_order: u32, - pub color: Option, + pub color: Option, pub is_subscribed: bool, pub is_default: bool, pub is_visible: bool, pub include_in_availability: IncludeInAvailability, - pub default_alerts_with_time: HashMap, - pub default_alerts_without_time: HashMap, + pub default_alerts_with_time: HashMap, + pub default_alerts_without_time: HashMap, pub time_zone: Timezone, } @@ -48,7 +49,7 @@ pub struct CalendarPreferences { )] pub struct CalendarEvent { pub names: Vec, - pub display_name: Option, + pub display_name: Option, pub event: ICalendar, pub user_properties: VecMap, pub may_invite_self: bool, @@ -65,7 +66,7 @@ pub struct CalendarEvent { rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, Debug, Default, Clone, PartialEq, Eq, )] pub enum Timezone { - IANA(String), + IANA(CompactString), Custom(ICalendar), #[default] Default, @@ -128,13 +129,13 @@ impl From for Acl { impl SerializedVersion for Calendar { fn serialize_version() -> u8 { - SERIALIZE_OBJ_14_V1 + SERIALIZE_CALENDAR_V1 } } impl SerializedVersion for CalendarEvent { fn serialize_version() -> u8 { - SERIALIZE_OBJ_16_V1 + SERIALIZE_CALENDAREVENT_V1 } } diff --git a/crates/groupware/src/contact/mod.rs b/crates/groupware/src/contact/mod.rs index 28f27cf0..96a65a99 100644 --- a/crates/groupware/src/contact/mod.rs +++ b/crates/groupware/src/contact/mod.rs @@ -9,7 +9,7 @@ pub mod index; use calcard::vcard::VCard; use dav_proto::schema::request::DeadProperty; use jmap_proto::types::{acl::Acl, value::AclGrant}; -use store::{SERIALIZE_OBJ_15_V1, SERIALIZE_OBJ_16_V1, SerializedVersion}; +use store::{SERIALIZE_ADDRESSBOOK_V1, SERIALIZE_CALENDAREVENT_V1, SerializedVersion}; use crate::DavName; @@ -78,12 +78,12 @@ impl From for Acl { impl SerializedVersion for AddressBook { fn serialize_version() -> u8 { - SERIALIZE_OBJ_15_V1 + SERIALIZE_ADDRESSBOOK_V1 } } impl SerializedVersion for ContactCard { fn serialize_version() -> u8 { - SERIALIZE_OBJ_16_V1 + SERIALIZE_CALENDAREVENT_V1 } } diff --git a/crates/groupware/src/file/mod.rs b/crates/groupware/src/file/mod.rs index 9322e3a5..ff21a5da 100644 --- a/crates/groupware/src/file/mod.rs +++ b/crates/groupware/src/file/mod.rs @@ -8,7 +8,7 @@ pub mod index; use dav_proto::schema::request::DeadProperty; use jmap_proto::types::value::AclGrant; -use store::{SERIALIZE_OBJ_11_V1, SerializedVersion}; +use store::{SERIALIZE_FILENODE_V1, SerializedVersion}; use utils::BlobHash; #[derive( @@ -39,6 +39,6 @@ pub struct FileProperties { impl SerializedVersion for FileNode { fn serialize_version() -> u8 { - SERIALIZE_OBJ_11_V1 + SERIALIZE_FILENODE_V1 } } diff --git a/crates/http-proto/Cargo.toml b/crates/http-proto/Cargo.toml index c6c5b942..4bf89ce6 100644 --- a/crates/http-proto/Cargo.toml +++ b/crates/http-proto/Cargo.toml @@ -14,6 +14,7 @@ hyper-util = { version = "0.1.1", features = ["tokio"] } http-body-util = "0.1.0" form_urlencoded = "1.1.0" percent-encoding = "2.3.1" +compact_str = "0.9.0" [dev-dependencies] diff --git a/crates/http-proto/src/context.rs b/crates/http-proto/src/context.rs index 6dd242df..c04cfa5d 100644 --- a/crates/http-proto/src/context.rs +++ b/crates/http-proto/src/context.rs @@ -8,6 +8,7 @@ use common::{ Server, expr::{functions::ResolveVariable, *}, }; +use compact_str::{CompactString, ToCompactString, format_compact}; use hyper::StatusCode; use crate::{HttpContext, HttpRequest, HttpSessionData}; @@ -17,7 +18,7 @@ impl<'x> HttpContext<'x> { Self { session, req } } - pub async fn resolve_response_url(&self, server: &Server) -> String { + pub async fn resolve_response_url(&self, server: &Server) -> CompactString { server .eval_if( &server.core.network.http_response_url, @@ -26,7 +27,7 @@ impl<'x> HttpContext<'x> { ) .await .unwrap_or_else(|| { - format!( + format_compact!( "http{}://{}:{}", if self.session.is_tls { "s" } else { "" }, self.session.local_ip, @@ -50,14 +51,14 @@ impl<'x> HttpContext<'x> { impl ResolveVariable for HttpContext<'_> { fn resolve_variable(&self, variable: u32) -> Variable<'_> { match variable { - V_REMOTE_IP => self.session.remote_ip.to_string().into(), + V_REMOTE_IP => self.session.remote_ip.to_compact_string().into(), V_REMOTE_PORT => self.session.remote_port.into(), - V_LOCAL_IP => self.session.local_ip.to_string().into(), + V_LOCAL_IP => self.session.local_ip.to_compact_string().into(), V_LOCAL_PORT => self.session.local_port.into(), V_TLS => self.session.is_tls.into(), V_PROTOCOL => if self.session.is_tls { "https" } else { "http" }.into(), V_LISTENER => self.session.instance.id.as_str().into(), - V_URL => self.req.uri().to_string().into(), + V_URL => self.req.uri().to_compact_string().into(), V_URL_PATH => self.req.uri().path().into(), V_METHOD => self.req.method().as_str().into(), V_HEADERS => self @@ -66,7 +67,12 @@ impl ResolveVariable for HttpContext<'_> { .iter() .map(|(h, v)| { Variable::String( - format!("{}: {}", h.as_str(), v.to_str().unwrap_or_default()).into(), + CompactString::new(format_compact!( + "{}: {}", + h.as_str(), + v.to_str().unwrap_or_default() + )) + .into(), ) }) .collect::>() diff --git a/crates/http/Cargo.toml b/crates/http/Cargo.toml index ecb32186..c3bd7137 100644 --- a/crates/http/Cargo.toml +++ b/crates/http/Cargo.toml @@ -42,6 +42,7 @@ rev_lines = "0.3.0" rkyv = { version = "0.8.10", features = ["little_endian"] } form-data = { version = "0.6.0", features = ["sync"], default-features = false } mime = "0.3.17" +compact_str = "0.9.0" [dev-dependencies] diff --git a/crates/http/src/auth/oauth/auth.rs b/crates/http/src/auth/oauth/auth.rs index 268e0db1..c0cccb30 100644 --- a/crates/http/src/auth/oauth/auth.rs +++ b/crates/http/src/auth/oauth/auth.rs @@ -296,7 +296,8 @@ impl OAuthApiHandler for Server { ) -> trc::Result { let base_url = HttpContext::new(&session, &req) .resolve_response_url(self) - .await; + .await + .to_string(); Ok(JsonResponse::new(OAuthMetadata { authorization_endpoint: format!("{base_url}/authorize/code",), diff --git a/crates/http/src/auth/oauth/mod.rs b/crates/http/src/auth/oauth/mod.rs index 51f023a0..929c8d9a 100644 --- a/crates/http/src/auth/oauth/mod.rs +++ b/crates/http/src/auth/oauth/mod.rs @@ -7,7 +7,7 @@ use http_proto::{HttpRequest, request::fetch_body}; use hyper::header::CONTENT_TYPE; use serde::{Deserialize, Serialize}; -use store::{SERIALIZE_OBJ_12_V1, SerializedVersion}; +use store::{SERIALIZE_OAUTHCODE_V1, SerializedVersion}; use utils::map::vec_map::VecMap; pub mod auth; @@ -58,7 +58,7 @@ pub struct OAuthCode { impl SerializedVersion for OAuthCode { fn serialize_version() -> u8 { - SERIALIZE_OBJ_12_V1 + SERIALIZE_OAUTHCODE_V1 } } diff --git a/crates/http/src/auth/oauth/openid.rs b/crates/http/src/auth/oauth/openid.rs index 8ed363fb..beab2c1e 100644 --- a/crates/http/src/auth/oauth/openid.rs +++ b/crates/http/src/auth/oauth/openid.rs @@ -10,24 +10,25 @@ use common::{ Server, auth::{AccessToken, oauth::oidc::Userinfo}, }; +use compact_str::{CompactString, ToCompactString, format_compact}; use serde::{Deserialize, Serialize}; use http_proto::*; #[derive(Debug, Serialize, Deserialize)] pub struct OpenIdMetadata { - pub issuer: String, - pub authorization_endpoint: String, - pub token_endpoint: String, - pub userinfo_endpoint: String, - pub jwks_uri: String, - pub registration_endpoint: String, - pub scopes_supported: Vec, - pub response_types_supported: Vec, - pub subject_types_supported: Vec, - pub grant_types_supported: Vec, - pub id_token_signing_alg_values_supported: Vec, - pub claims_supported: Vec, + pub issuer: CompactString, + pub authorization_endpoint: CompactString, + pub token_endpoint: CompactString, + pub userinfo_endpoint: CompactString, + pub jwks_uri: CompactString, + pub registration_endpoint: CompactString, + pub scopes_supported: Vec, + pub response_types_supported: Vec, + pub subject_types_supported: Vec, + pub grant_types_supported: Vec, + pub id_token_signing_alg_values_supported: Vec, + pub claims_supported: Vec, } pub trait OpenIdHandler: Sync + Send { @@ -49,7 +50,7 @@ impl OpenIdHandler for Server { access_token: &AccessToken, ) -> trc::Result { Ok(JsonResponse::new(Userinfo { - sub: Some(access_token.primary_id.to_string()), + sub: Some(access_token.primary_id.to_compact_string()), name: access_token.description.clone(), preferred_username: Some(access_token.name.clone()), email: access_token.emails.first().cloned(), @@ -70,42 +71,42 @@ impl OpenIdHandler for Server { .await; Ok(JsonResponse::new(OpenIdMetadata { - authorization_endpoint: format!("{base_url}/authorize/code",), - token_endpoint: format!("{base_url}/auth/token"), - userinfo_endpoint: format!("{base_url}/auth/userinfo"), - jwks_uri: format!("{base_url}/auth/jwks.json"), - registration_endpoint: format!("{base_url}/auth/register"), + authorization_endpoint: format_compact!("{base_url}/authorize/code",), + token_endpoint: format_compact!("{base_url}/auth/token"), + userinfo_endpoint: format_compact!("{base_url}/auth/userinfo"), + jwks_uri: format_compact!("{base_url}/auth/jwks.json"), + registration_endpoint: format_compact!("{base_url}/auth/register"), response_types_supported: vec![ - "code".to_string(), - "id_token".to_string(), - "id_token token".to_string(), + "code".into(), + "id_token".into(), + "id_token token".into(), ], grant_types_supported: vec![ - "authorization_code".to_string(), - "implicit".to_string(), - "urn:ietf:params:oauth:grant-type:device_code".to_string(), + "authorization_code".into(), + "implicit".into(), + "urn:ietf:params:oauth:grant-type:device_code".into(), ], - scopes_supported: vec!["openid".to_string(), "offline_access".to_string()], - subject_types_supported: vec!["public".to_string()], + scopes_supported: vec!["openid".into(), "offline_access".into()], + subject_types_supported: vec!["public".into()], id_token_signing_alg_values_supported: vec![ - "RS256".to_string(), - "RS384".to_string(), - "RS512".to_string(), - "ES256".to_string(), - "ES384".to_string(), - "PS256".to_string(), - "PS384".to_string(), - "PS512".to_string(), - "HS256".to_string(), - "HS384".to_string(), - "HS512".to_string(), + "RS256".into(), + "RS384".into(), + "RS512".into(), + "ES256".into(), + "ES384".into(), + "PS256".into(), + "PS384".into(), + "PS512".into(), + "HS256".into(), + "HS384".into(), + "HS512".into(), ], claims_supported: vec![ - "sub".to_string(), - "name".to_string(), - "preferred_username".to_string(), - "email".to_string(), - "email_verified".to_string(), + "sub".into(), + "name".into(), + "preferred_username".into(), + "email".into(), + "email_verified".into(), ], issuer: base_url, }) diff --git a/crates/http/src/auth/oauth/registration.rs b/crates/http/src/auth/oauth/registration.rs index c6d4c021..4c9e1418 100644 --- a/crates/http/src/auth/oauth/registration.rs +++ b/crates/http/src/auth/oauth/registration.rs @@ -10,9 +10,12 @@ use common::{ Server, auth::oauth::registration::{ClientRegistrationRequest, ClientRegistrationResponse}, }; +use compact_str::CompactString; use directory::{ - Permission, Principal, QueryBy, Type, - backend::internal::{PrincipalField, lookup::DirectoryStore, manage::ManageDirectory}, + Permission, QueryBy, Type, + backend::internal::{ + PrincipalField, PrincipalSet, lookup::DirectoryStore, manage::ManageDirectory, + }, }; use store::rand::{Rng, distr::Alphanumeric, rng}; use trc::{AddContext, AuthEvent}; @@ -67,10 +70,10 @@ impl ClientRegistrationHandler for Server { .sample_iter(Alphanumeric) .take(20) .map(|ch| char::from(ch.to_ascii_lowercase())) - .collect::(); + .collect::(); self.store() .create_principal( - Principal::new(u32::MAX, Type::OauthClient) + PrincipalSet::new(u32::MAX, Type::OauthClient) .with_field(PrincipalField::Name, client_id.clone()) .with_field(PrincipalField::Urls, request.redirect_uris.clone()) .with_opt_field(PrincipalField::Description, request.client_name.clone()) @@ -116,12 +119,7 @@ impl ClientRegistrationHandler for Server { .filter(|p| p.typ() == Type::OauthClient) { if let Some(redirect_uri) = redirect_uri { - if client - .get_str_array(PrincipalField::Urls) - .unwrap_or_default() - .iter() - .any(|uri| uri == redirect_uri) - { + if client.urls().iter().any(|uri| uri == redirect_uri) { return Ok(None); } } else { diff --git a/crates/http/src/auth/oauth/token.rs b/crates/http/src/auth/oauth/token.rs index 197b809c..b4ed3cfb 100644 --- a/crates/http/src/auth/oauth/token.rs +++ b/crates/http/src/auth/oauth/token.rs @@ -11,6 +11,7 @@ use common::{ oauth::{GrantType, oidc::StandardClaims}, }, }; +use compact_str::CompactString; use hyper::StatusCode; use std::future::Future; use store::{ @@ -44,8 +45,8 @@ pub trait TokenHandler: Sync + Send { &self, account_id: u32, client_id: &str, - issuer: String, - nonce: Option, + issuer: CompactString, + nonce: Option, with_refresh_token: bool, with_id_token: bool, ) -> impl Future> + Send; @@ -118,7 +119,7 @@ impl TokenHandler for Server { oauth.account_id.into(), &oauth.client_id, issuer, - oauth.nonce.as_ref().map(|s| s.to_string()), + oauth.nonce.as_ref().map(|s| s.as_str().into()), true, true, ) @@ -190,7 +191,7 @@ impl TokenHandler for Server { oauth.account_id.into(), &oauth.client_id, issuer, - oauth.nonce.as_ref().map(|s| s.to_string()), + oauth.nonce.as_ref().map(|s| s.as_str().into()), true, true, ) @@ -288,8 +289,8 @@ impl TokenHandler for Server { &self, account_id: u32, client_id: &str, - issuer: String, - nonce: Option, + issuer: CompactString, + nonce: Option, with_refresh_token: bool, with_id_token: bool, ) -> trc::Result { diff --git a/crates/http/src/autoconfig/mod.rs b/crates/http/src/autoconfig/mod.rs index 80b7301a..a6a64e2c 100644 --- a/crates/http/src/autoconfig/mod.rs +++ b/crates/http/src/autoconfig/mod.rs @@ -7,7 +7,8 @@ use std::fmt::Write; use common::{Server, manager::webadmin::Resource}; -use directory::{QueryBy, backend::internal::PrincipalField}; +use compact_str::CompactString; +use directory::QueryBy; use quick_xml::Reader; use quick_xml::events::Event; use trc::AddContext; @@ -28,7 +29,7 @@ pub trait Autoconfig: Sync + Send { fn autoconfig_parameters<'x>( &self, emailaddress: &'x str, - ) -> impl Future> + Send; + ) -> impl Future> + Send; } impl Autoconfig for Server { @@ -176,7 +177,7 @@ impl Autoconfig for Server { async fn autoconfig_parameters<'x>( &self, emailaddress: &'x str, - ) -> trc::Result<(String, String, &'x str)> { + ) -> trc::Result<(CompactString, CompactString, &'x str)> { let (_, domain) = emailaddress.rsplit_once('@').ok_or_else(|| { trc::ResourceEvent::BadParameters .into_err() @@ -184,10 +185,10 @@ impl Autoconfig for Server { })?; // Obtain server name - let server_name = self.core.network.server_name.to_string(); + let server_name = self.core.network.server_name.clone(); // Find the account name by e-mail address - let mut account_name = emailaddress.to_string(); + let mut account_name = emailaddress.into(); if let Some(id) = self .core .storage @@ -196,7 +197,7 @@ impl Autoconfig for Server { .await .caused_by(trc::location!())? { - if let Ok(Some(mut principal)) = self + if let Ok(Some(principal)) = self .core .storage .directory @@ -204,11 +205,11 @@ impl Autoconfig for Server { .await { if principal - .get_str_array(PrincipalField::Emails) - .and_then(|emails| emails.first()) + .emails + .first() .is_some_and(|email| email.eq_ignore_ascii_case(emailaddress)) { - account_name = principal.take_str(PrincipalField::Name).unwrap_or_default(); + account_name = principal.name; } } } diff --git a/crates/http/src/form/mod.rs b/crates/http/src/form/mod.rs index 5584af07..2830ab5f 100644 --- a/crates/http/src/form/mod.rs +++ b/crates/http/src/form/mod.rs @@ -12,6 +12,7 @@ use common::{ config::network::{ContactForm, FieldOrDefault}, ip_to_bytes, psl, }; +use compact_str::CompactString; use email::message::delivery::{IngestMessage, LocalDeliveryStatus, MailDelivery}; use hyper::StatusCode; use mail_auth::common::cache::NoCache; @@ -85,10 +86,8 @@ impl FormHandler for Server { } // Obtain fields - let from_email = form_data - .get_or_default(&form.from_email) - .trim() - .to_lowercase(); + let from_email = + CompactString::from_str_to_lowercase(form_data.get_or_default(&form.from_email).trim()); let from_subject = form_data.get_or_default(&form.from_subject).trim(); let from_name = form_data.get_or_default(&form.from_name).trim(); diff --git a/crates/http/src/management/enterprise/telemetry.rs b/crates/http/src/management/enterprise/telemetry.rs index 0aed25cf..2cf32fcb 100644 --- a/crates/http/src/management/enterprise/telemetry.rs +++ b/crates/http/src/management/enterprise/telemetry.rs @@ -230,7 +230,6 @@ impl TelemetryApi for Server { { if let Some(needle) = key_filters.get(key).or(filter.as_ref()) { let matches = match value { - Value::Static(haystack) => haystack.contains(needle), Value::String(haystack) => haystack.contains(needle), Value::Timestamp(haystack) => { DateTime::from_timestamp(*haystack as i64) diff --git a/crates/http/src/management/enterprise/undelete.rs b/crates/http/src/management/enterprise/undelete.rs index 645282c8..1f2c1b5c 100644 --- a/crates/http/src/management/enterprise/undelete.rs +++ b/crates/http/src/management/enterprise/undelete.rs @@ -234,7 +234,7 @@ impl UndeleteApi for Server { .take_value(trc::Key::Reason) .and_then(|v| v.into_string()) .unwrap() - .into_owned(), + .to_string(), }); } Err(err) => { diff --git a/crates/http/src/management/principal.rs b/crates/http/src/management/principal.rs index 784638cb..383de1f6 100644 --- a/crates/http/src/management/principal.rs +++ b/crates/http/src/management/principal.rs @@ -7,12 +7,16 @@ use std::sync::Arc; use common::{KV_BAYES_MODEL_USER, Server, auth::AccessToken}; +use compact_str::{CompactString, format_compact}; use directory::{ - DirectoryInner, Permission, Principal, QueryBy, Type, + DirectoryInner, Permission, QueryBy, Type, backend::internal::{ - PrincipalAction, PrincipalField, PrincipalUpdate, PrincipalValue, SpecialSecrets, + PrincipalAction, PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue, + SpecialSecrets, lookup::DirectoryStore, - manage::{self, ChangedPrincipals, ManageDirectory, UpdatePrincipal, not_found}, + manage::{ + self, ChangedPrincipals, ManageDirectory, PrincipalList, UpdatePrincipal, not_found, + }, }, }; @@ -28,11 +32,22 @@ use std::future::Future; #[serde(tag = "type")] #[serde(rename_all = "camelCase")] pub enum AccountAuthRequest { - SetPassword { password: String }, - EnableOtpAuth { url: String }, - DisableOtpAuth { url: Option }, - AddAppPassword { name: String, password: String }, - RemoveAppPassword { name: Option }, + SetPassword { + password: CompactString, + }, + EnableOtpAuth { + url: CompactString, + }, + DisableOtpAuth { + url: Option, + }, + AddAppPassword { + name: CompactString, + password: CompactString, + }, + RemoveAppPassword { + name: Option, + }, } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -40,7 +55,7 @@ pub struct AccountAuthResponse { #[serde(rename = "otpEnabled")] pub otp_auth: bool, #[serde(rename = "appPasswords")] - pub app_passwords: Vec, + pub app_passwords: Vec, } pub trait PrincipalManager: Sync + Send { @@ -79,7 +94,7 @@ impl PrincipalManager for Server { (None, &Method::POST) => { // Parse principal let principal = - serde_json::from_slice::(body.as_deref().unwrap_or_default()) + serde_json::from_slice::(body.as_deref().unwrap_or_default()) .map_err(|err| { trc::EventType::Resource(trc::ResourceEvent::BadParameters) .from_json_error(err) @@ -279,16 +294,38 @@ impl PrincipalManager for Server { // SPDX-SnippetEnd - let mut principals = self - .core - .storage - .data - .list_principals(filter, tenant, &types, &fields, page, limit) + let principals = self + .store() + .list_principals( + filter, + tenant, + &types, + fields.len() != 1 + || fields.first().is_none_or(|v| v != &PrincipalField::Name), + page, + limit, + ) .await?; - if count { - principals.items.clear(); - } + let principals: PrincipalList = if !count { + let mut expanded = PrincipalList { + items: Vec::with_capacity(principals.items.len()), + total: principals.total, + }; + + for principal in principals.items { + expanded + .items + .push(self.store().map_principal(principal, &fields).await?); + } + + expanded + } else { + PrincipalList { + items: vec![], + total: principals.total, + } + }; Ok(JsonResponse::new(json!({ "data": principals, @@ -345,10 +382,8 @@ impl PrincipalManager for Server { } let principals = self - .core - .storage - .data - .list_principals(filter, tenant, &[typ], &[PrincipalField::Name], 0, 0) + .store() + .list_principals(filter, tenant, &[typ], false, 0, 0) .await?; let found = !principals.items.is_empty(); @@ -452,19 +487,18 @@ impl PrincipalManager for Server { } })?; - let mut principal = self - .core - .storage - .data + let principal = self + .store() .query(QueryBy::Id(account_id), true) .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; // Map fields - self.core + let principal = self + .core .storage .data - .map_field_ids(&mut principal, &[]) + .map_principal(principal, &[]) .await .caused_by(trc::location!())?; @@ -677,20 +711,18 @@ impl PrincipalManager for Server { if access_token.primary_id() != u32::MAX { let principal = self - .core - .storage - .directory + .directory() .query(QueryBy::Id(access_token.primary_id()), false) .await? .ok_or_else(|| trc::ManageEvent::NotFound.into_err())?; - for secret in principal.iter_str(PrincipalField::Secrets) { + for secret in &principal.secrets { if secret.is_otp_auth() { response.otp_auth = true; } else if let Some((app_name, _)) = secret.strip_prefix("$app$").and_then(|s| s.split_once('$')) { - response.app_passwords.push(app_name.to_string()); + response.app_passwords.push(app_name.into()); } } } @@ -747,7 +779,10 @@ impl PrincipalManager for Server { self.core .storage .config - .set([("authentication.fallback-admin.secret", password)], true) + .set( + [("authentication.fallback-admin.secret", password.to_string())], + true, + ) .await?; // Increment revision @@ -783,7 +818,7 @@ impl PrincipalManager for Server { actions.push(PrincipalUpdate { action: PrincipalAction::RemoveItem, field: PrincipalField::Secrets, - value: PrincipalValue::String(String::new()), + value: PrincipalValue::String(CompactString::new("")), }); (PrincipalAction::AddItem, password) @@ -791,14 +826,15 @@ impl PrincipalManager for Server { AccountAuthRequest::EnableOtpAuth { url } => (PrincipalAction::AddItem, url), AccountAuthRequest::DisableOtpAuth { url } => ( PrincipalAction::RemoveItem, - url.unwrap_or_else(|| "otpauth://".to_string()), + url.unwrap_or_else(|| "otpauth://".into()), + ), + AccountAuthRequest::AddAppPassword { name, password } => ( + PrincipalAction::AddItem, + format_compact!("$app${name}${password}"), ), - AccountAuthRequest::AddAppPassword { name, password } => { - (PrincipalAction::AddItem, format!("$app${name}${password}")) - } AccountAuthRequest::RemoveAppPassword { name } => ( PrincipalAction::RemoveItem, - format!("$app${}", name.unwrap_or_default()), + format_compact!("$app${}", name.unwrap_or_default()), ), }; diff --git a/crates/http/src/management/queue.rs b/crates/http/src/management/queue.rs index c974b7a6..503e894b 100644 --- a/crates/http/src/management/queue.rs +++ b/crates/http/src/management/queue.rs @@ -8,10 +8,8 @@ use std::{future::Future, sync::atomic::Ordering}; use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use common::{Server, auth::AccessToken, ipc::QueueEvent}; -use directory::{ - Permission, Type, - backend::internal::{PrincipalField, manage::ManageDirectory}, -}; +use compact_str::CompactString; +use directory::{Permission, Type, backend::internal::manage::ManageDirectory}; use hyper::Method; use mail_auth::{ dmarc::URI, @@ -136,7 +134,7 @@ impl QueueManagement for Server { // SPDX-License-Identifier: LicenseRef-SEL // Limit to tenant domains - let mut tenant_domains: Option> = None; + let mut tenant_domains: Option> = None; #[cfg(feature = "enterprise")] if self.core.is_enterprise_edition() { if let Some(tenant) = access_token.tenant { @@ -144,20 +142,13 @@ impl QueueManagement for Server { .core .storage .data - .list_principals( - None, - tenant.id.into(), - &[Type::Domain], - &[PrincipalField::Name], - 0, - 0, - ) + .list_principals(None, tenant.id.into(), &[Type::Domain], false, 0, 0) .await .map(|principals| { principals .items .into_iter() - .filter_map(|mut p| p.take_str(PrincipalField::Name)) + .map(|p| p.name) .collect::>() }) .caused_by(trc::location!())? @@ -474,9 +465,9 @@ impl QueueManagement for Server { if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) { match report_id { QueueClass::DmarcReportHeader(event) - if tenant_domains - .as_ref() - .is_none_or(|domains| domains.contains(&event.domain)) => + if tenant_domains.as_ref().is_none_or(|domains| { + domains.iter().any(|dd| dd == event.domain) + }) => { let mut rua = Vec::new(); if let Some(report) = self @@ -487,9 +478,9 @@ impl QueueManagement for Server { } } QueueClass::TlsReportHeader(event) - if tenant_domains - .as_ref() - .is_none_or(|domains| domains.contains(&event.domain)) => + if tenant_domains.as_ref().is_none_or(|domains| { + domains.iter().any(|dd| dd == event.domain) + }) => { let mut rua = Vec::new(); if let Some(report) = self @@ -547,17 +538,17 @@ impl QueueManagement for Server { if let Some(report_id) = parse_queued_report_id(report_id.as_ref()) { let result = match report_id { QueueClass::DmarcReportHeader(event) - if tenant_domains - .as_ref() - .is_none_or(|domains| domains.contains(&event.domain)) => + if tenant_domains.as_ref().is_none_or(|domains| { + domains.iter().any(|dd| dd == event.domain) + }) => { self.delete_dmarc_report(event).await; true } QueueClass::TlsReportHeader(event) - if tenant_domains - .as_ref() - .is_none_or(|domains| domains.contains(&event.domain)) => + if tenant_domains.as_ref().is_none_or(|domains| { + domains.iter().any(|dd| dd == event.domain) + }) => { self.delete_tls_report(vec![event]).await; true @@ -677,7 +668,7 @@ struct QueuedMessages { async fn fetch_queued_messages( server: &Server, params: &UrlParams<'_>, - tenant_domains: &Option>, + tenant_domains: &Option>, ) -> trc::Result { let text = params.get("text"); let from = params.get("from"); @@ -783,7 +774,7 @@ struct QueuedReports { async fn fetch_queued_reports( server: &Server, params: &UrlParams<'_>, - tenant_domains: &Option>, + tenant_domains: &Option>, ) -> trc::Result { let domain = params.get("domain").map(|d| d.to_lowercase()); let type_ = params.get("type").and_then(|t| match t { @@ -832,7 +823,7 @@ async fn fetch_queued_reports( let event = ReportEvent::deserialize(key)?; if tenant_domains .as_ref() - .is_none_or(|domains| domains.contains(&event.domain)) + .is_none_or(|domains| domains.iter().any(|dd| dd == event.domain)) && event.seq_id != 0 && domain.as_ref().is_none_or(|d| event.domain.contains(d)) { @@ -973,10 +964,10 @@ fn is_zero(num: &i16) -> bool { } trait IsTenantDomain { - fn is_tenant_domain(&self, tenant_domains: &Option>) -> bool; + fn is_tenant_domain(&self, tenant_domains: &Option>) -> bool; } impl IsTenantDomain for ArchivedMessage { - fn is_tenant_domain(&self, tenant_domains: &Option>) -> bool { + fn is_tenant_domain(&self, tenant_domains: &Option>) -> bool { tenant_domains .as_ref() .is_none_or(|domains| self.has_domain(domains)) diff --git a/crates/http/src/management/report.rs b/crates/http/src/management/report.rs index 7df9fdd8..fdc8f8e7 100644 --- a/crates/http/src/management/report.rs +++ b/crates/http/src/management/report.rs @@ -7,10 +7,8 @@ use std::future::Future; use common::{Server, auth::AccessToken}; -use directory::{ - Permission, Type, - backend::internal::{PrincipalField, manage::ManageDirectory}, -}; +use compact_str::CompactString; +use directory::{Permission, Type, backend::internal::manage::ManageDirectory}; use hyper::Method; use mail_auth::report::{ Feedback, @@ -54,7 +52,7 @@ impl ManageReports for Server { // SPDX-License-Identifier: LicenseRef-SEL // Limit to tenant domains - let mut tenant_domains: Option> = None; + let mut tenant_domains: Option> = None; #[cfg(feature = "enterprise")] if self.core.is_enterprise_edition() { if let Some(tenant) = access_token.tenant { @@ -62,20 +60,13 @@ impl ManageReports for Server { .core .storage .data - .list_principals( - None, - tenant.id.into(), - &[Type::Domain], - &[PrincipalField::Name], - 0, - 0, - ) + .list_principals(None, tenant.id.into(), &[Type::Domain], false, 0, 0) .await .map(|principals| { principals .items .into_iter() - .filter_map(|mut p| p.take_str(PrincipalField::Name)) + .map(|p| p.name) .collect::>() }) .caused_by(trc::location!())? @@ -306,7 +297,7 @@ async fn fetch_incoming_reports( server: &Server, class: &str, params: &UrlParams<'_>, - tenant_domains: &Option>, + tenant_domains: &Option>, ) -> trc::Result { let filter = params.get("text"); let page: usize = params.parse::("page").unwrap_or_default(); diff --git a/crates/http/src/management/spam.rs b/crates/http/src/management/spam.rs index cdfa649a..c2af5b12 100644 --- a/crates/http/src/management/spam.rs +++ b/crates/http/src/management/spam.rs @@ -7,6 +7,7 @@ use std::net::IpAddr; use common::{Server, auth::AccessToken, config::spamfilter::SpamFilterAction, psl}; +use compact_str::CompactString; use directory::{ Permission, backend::internal::manage::{self, ManageDirectory}, @@ -65,7 +66,7 @@ pub struct SpamClassifyRequest { #[serde(rename_all = "camelCase")] pub struct SpamClassifyResponse { pub score: f64, - pub tags: AHashMap>, + pub tags: AHashMap>, pub disposition: SpamFilterDisposition, } diff --git a/crates/http/src/request.rs b/crates/http/src/request.rs index 54dbcd93..4678aba2 100644 --- a/crates/http/src/request.rs +++ b/crates/http/src/request.rs @@ -233,7 +233,10 @@ impl ParseHttp for Server { self.authenticate_headers(&req, &session, false).await?; return self - .handle_session_resource(ctx.resolve_response_url(self).await, access_token) + .handle_session_resource( + ctx.resolve_response_url(self).await.into(), + access_token, + ) .await .map(|s| s.into_http_response()); } @@ -732,9 +735,11 @@ async fn handle_session(inner: Arc, session: SessionDat Http(trc::HttpEvent::ResponseBody), SpanId = session.session_id, Contents = match response.body() { - HttpResponseBody::Text(value) => trc::Value::String(value.clone()), - HttpResponseBody::Binary(_) => trc::Value::Static("[binary data]"), - HttpResponseBody::Stream(_) => trc::Value::Static("[stream]"), + HttpResponseBody::Text(value) => + trc::Value::String(value.as_str().into()), + HttpResponseBody::Binary(_) => + trc::Value::String("[binary data]".into()), + HttpResponseBody::Stream(_) => trc::Value::String("[stream]".into()), _ => trc::Value::None, }, Code = response.status().as_u16(), diff --git a/crates/imap-proto/src/protocol/mod.rs b/crates/imap-proto/src/protocol/mod.rs index fcdab80a..042d30b8 100644 --- a/crates/imap-proto/src/protocol/mod.rs +++ b/crates/imap-proto/src/protocol/mod.rs @@ -466,13 +466,13 @@ impl ResponseType { impl From for trc::Value { fn from(value: ResponseCode) -> Self { - trc::Value::Static(value.as_str()) + trc::Value::String(CompactString::const_new(value.as_str())) } } impl From for trc::Value { fn from(value: ResponseType) -> Self { - trc::Value::Static(value.as_str()) + trc::Value::String(CompactString::const_new(value.as_str())) } } diff --git a/crates/imap/src/core/client.rs b/crates/imap/src/core/client.rs index 4f1ff5cf..272992c6 100644 --- a/crates/imap/src/core/client.rs +++ b/crates/imap/src/core/client.rs @@ -56,8 +56,8 @@ impl Session { (&self.state, response.key(trc::Key::Code)), ( State::NotAuthenticated { .. }, - Some(trc::Value::Static("PARSE")) - ) + Some(trc::Value::String(v)) + ) if v == "PARSE" ) { match self.server.is_scanner_fail2banned(self.remote_addr).await { Ok(true) => { diff --git a/crates/imap/src/core/mailbox.rs b/crates/imap/src/core/mailbox.rs index 4a61c21f..0e52aafe 100644 --- a/crates/imap/src/core/mailbox.rs +++ b/crates/imap/src/core/mailbox.rs @@ -14,7 +14,7 @@ use common::{ sharing::EffectiveAcl, }; use compact_str::CompactString; -use directory::{QueryBy, backend::internal::PrincipalField}; +use directory::backend::internal::manage::ManageDirectory; use email::{ mailbox::{ INBOX_ID, @@ -65,14 +65,11 @@ impl SessionData { session.server.core.jmap.shared_folder, session .server - .core - .storage - .directory - .query(QueryBy::Id(account_id), false) + .store() + .get_principal_name(account_id) .await - .unwrap_or_default() - .and_then(|mut p| p.take_str(PrincipalField::Name)) - .unwrap_or_else(|| Id::from(account_id).to_string()) + .caused_by(trc::location!())? + .unwrap_or_else(|| Id::from(account_id).to_string().into()) ) .into(); mailboxes.push( @@ -282,14 +279,11 @@ impl SessionData { "{}/{}", self.server.core.jmap.shared_folder, self.server - .core - .storage - .directory - .query(QueryBy::Id(account_id), false) + .store() + .get_principal_name(account_id) .await .caused_by(trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Name)) - .unwrap_or_else(|| Id::from(account_id).to_string()) + .unwrap_or_else(|| Id::from(account_id).to_string().into()) ) .into(); added_accounts.push( diff --git a/crates/imap/src/op/acl.rs b/crates/imap/src/op/acl.rs index 40004a69..43fb71a8 100644 --- a/crates/imap/src/op/acl.rs +++ b/crates/imap/src/op/acl.rs @@ -10,9 +10,13 @@ use common::{ auth::AccessToken, listener::SessionStream, sharing::EffectiveAcl, storage::index::ObjectIndexBuilder, }; +use compact_str::ToCompactString; use directory::{ Permission, QueryBy, Type, - backend::internal::{PrincipalField, manage::ChangedPrincipals}, + backend::internal::{ + PrincipalField, + manage::{ChangedPrincipals, ManageDirectory}, + }, }; use imap_proto::{ Command, ResponseCode, StatusResponse, @@ -56,13 +60,10 @@ impl Session { for item in mailbox.inner.acls.iter() { if let Some(account_name) = data .server - .core - .storage - .directory - .query(QueryBy::Id(item.account_id.into()), false) + .store() + .get_principal_name(item.account_id.into()) .await .imap_ctx(&arguments.tag, trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Name)) { let mut rights = Vec::new(); @@ -104,7 +105,7 @@ impl Session { } } - permissions.push((account_name.into(), rights)); + permissions.push((account_name, rights)); } } @@ -201,7 +202,7 @@ impl Session { MailboxId = mailbox_id.mailbox_id, Details = rights .iter() - .map(|r| trc::Value::String(r.to_string())) + .map(|r| trc::Value::String(r.to_compact_string())) .collect::>(), Elapsed = op_start.elapsed() ); diff --git a/crates/imap/src/op/rename.rs b/crates/imap/src/op/rename.rs index 691e8751..ee1b92e5 100644 --- a/crates/imap/src/op/rename.rs +++ b/crates/imap/src/op/rename.rs @@ -84,7 +84,7 @@ impl SessionData { }; // Obtain mailbox - let mailbox = self + let mailbox_ = self .server .get_archive(params.account_id, Collection::Mailbox, mailbox_id) .await @@ -96,8 +96,9 @@ impl SessionData { .caused_by(trc::location!()) .code(ResponseCode::NonExistent) .id(arguments.tag.clone()) - })? - .into_deserialized::() + })?; + let mailbox = mailbox_ + .to_unarchived::() .imap_ctx(&arguments.tag, trc::location!())?; // Validate ACL @@ -155,8 +156,10 @@ impl SessionData { create_ids.push(mailbox_id); } - let mut new_mailbox = mailbox.inner.clone(); - new_mailbox.name = new_mailbox_name.to_string(); + let mut new_mailbox = mailbox + .deserialize::() + .caused_by(trc::location!())?; + new_mailbox.name = new_mailbox_name.into(); new_mailbox.parent_id = parent_id; new_mailbox.uid_validity = rand::random::(); batch diff --git a/crates/jmap-proto/src/types/collection.rs b/crates/jmap-proto/src/types/collection.rs index 29c1ba17..4cb21b29 100644 --- a/crates/jmap-proto/src/types/collection.rs +++ b/crates/jmap-proto/src/types/collection.rs @@ -9,6 +9,7 @@ use std::{ str::FromStr, }; +use compact_str::CompactString; use utils::map::bitmap::BitmapItem; use super::{property::Property, type_state::DataType}; @@ -192,7 +193,7 @@ impl FromStr for Collection { impl From for trc::Value { fn from(value: Collection) -> Self { - trc::Value::Static(value.as_str()) + trc::Value::String(CompactString::const_new(value.as_str())) } } diff --git a/crates/jmap/Cargo.toml b/crates/jmap/Cargo.toml index ba0df56e..befd21cc 100644 --- a/crates/jmap/Cargo.toml +++ b/crates/jmap/Cargo.toml @@ -48,6 +48,7 @@ aes-gcm = "0.10.1" aes-gcm-siv = "0.11.1" rsa = "0.9.2" rkyv = { version = "0.8.10", features = ["little_endian"] } +compact_str = "0.9.0" [features] test_mode = [] diff --git a/crates/jmap/src/api/request.rs b/crates/jmap/src/api/request.rs index 9f0055c7..f08ef5d0 100644 --- a/crates/jmap/src/api/request.rs +++ b/crates/jmap/src/api/request.rs @@ -61,6 +61,7 @@ pub trait RequestHandler: Sync + Send { } impl RequestHandler for Server { + #![allow(clippy::large_futures)] async fn handle_jmap_request( &self, request: Request, diff --git a/crates/jmap/src/api/session.rs b/crates/jmap/src/api/session.rs index d44109ef..e632b57b 100644 --- a/crates/jmap/src/api/session.rs +++ b/crates/jmap/src/api/session.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use common::{Server, auth::AccessToken}; -use directory::{QueryBy, backend::internal::PrincipalField}; +use directory::backend::internal::manage::ManageDirectory; use jmap_proto::{ request::capability::{Capability, Session}, types::{acl::Acl, collection::Collection, id::Id}, @@ -33,11 +33,12 @@ impl SessionHandler for Server { session.set_state(access_token.state()); session.set_primary_account( access_token.primary_id().into(), - access_token.name.clone(), + access_token.name.to_string(), access_token .description - .clone() - .unwrap_or_else(|| access_token.name.clone()), + .as_ref() + .unwrap_or(&access_token.name) + .to_string(), None, &self.core.jmap.capabilities.account, ); @@ -54,13 +55,11 @@ impl SessionHandler for Server { session.add_account( (*id).into(), - self.core - .storage - .directory - .query(QueryBy::Id(*id), false) + self.store() + .get_principal_name(*id) .await .caused_by(trc::location!())? - .and_then(|mut p| p.take_str(PrincipalField::Name)) + .map(Into::into) .unwrap_or_else(|| Id::from(*id).to_string()), is_personal, is_readonly, diff --git a/crates/jmap/src/identity/get.rs b/crates/jmap/src/identity/get.rs index 0a62df45..0b9503f4 100644 --- a/crates/jmap/src/identity/get.rs +++ b/crates/jmap/src/identity/get.rs @@ -5,7 +5,8 @@ */ use common::{Server, storage::index::ObjectIndexBuilder}; -use directory::{QueryBy, backend::internal::PrincipalField}; +use compact_str::format_compact; +use directory::QueryBy; use email::identity::{ArchivedEmailAddress, Identity}; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, @@ -143,15 +144,19 @@ impl IdentityGet for Server { } // Obtain principal - let principal = self + let principal = if let Some(principal) = self .core .storage .directory .query(QueryBy::Id(account_id), false) .await .caused_by(trc::location!())? - .unwrap_or_default(); - let num_emails = principal.field_len(PrincipalField::Emails); + { + principal + } else { + return Ok(identity_ids); + }; + let num_emails = principal.emails.len(); if num_emails == 0 { return Ok(identity_ids); } @@ -162,18 +167,14 @@ impl IdentityGet for Server { .with_collection(Collection::Identity); // Create identities - let name = principal - .description() - .unwrap_or(principal.name()) - .trim() - .to_string(); + let name = principal.description.unwrap_or(principal.name); let has_many = num_emails > 1; let mut next_document_id = self .store() .assign_document_ids(account_id, Collection::Identity, num_emails as u64) .await .caused_by(trc::location!())?; - for email in principal.iter_str(PrincipalField::Emails) { + for email in &principal.emails { let email = sanitize_email(email).unwrap_or_default(); if email.is_empty() { continue; @@ -181,7 +182,7 @@ impl IdentityGet for Server { let name = if name.is_empty() { email.clone() } else if has_many { - format!("{} <{}>", name, email) + format_compact!("{} <{}>", name, email) } else { name.clone() }; diff --git a/crates/jmap/src/identity/set.rs b/crates/jmap/src/identity/set.rs index 7eedd668..4bf11ba0 100644 --- a/crates/jmap/src/identity/set.rs +++ b/crates/jmap/src/identity/set.rs @@ -5,7 +5,7 @@ */ use common::{Server, storage::index::ObjectIndexBuilder}; -use directory::{QueryBy, backend::internal::PrincipalField}; +use directory::QueryBy; use email::identity::{EmailAddress, Identity}; use jmap_proto::{ error::set::SetError, @@ -59,14 +59,11 @@ impl IdentitySet for Server { // Validate email address if !identity.email.is_empty() { - if !self - .core - .storage - .directory + if self + .directory() .query(QueryBy::Id(account_id), false) .await? - .unwrap_or_default() - .has_str_value(PrincipalField::Emails, &identity.email) + .is_none_or(|p| !p.emails.iter().any(|e| e == identity.email)) { response.not_created.append( id, @@ -192,7 +189,7 @@ fn validate_identity_value( ) -> Result<(), SetError> { match (property, value) { (Property::Name, MaybePatchValue::Value(Value::Text(value))) if value.len() < 255 => { - identity.name = value; + identity.name = value.into(); } (Property::Email, MaybePatchValue::Value(Value::Text(value))) if is_create && value.len() < 255 => @@ -206,19 +203,19 @@ fn validate_identity_value( (Property::TextSignature, MaybePatchValue::Value(Value::Text(value))) if value.len() < 2048 => { - identity.text_signature = value; + identity.text_signature = value.into(); } (Property::HtmlSignature, MaybePatchValue::Value(Value::Text(value))) if value.len() < 2048 => { - identity.html_signature = value; + identity.html_signature = value.into(); } (Property::ReplyTo | Property::Bcc, MaybePatchValue::Value(Value::List(value))) => { let mut addresses = Vec::with_capacity(value.len()); for addr in value { let mut address = EmailAddress { name: None, - email: String::new(), + email: "".into(), }; let mut is_valid = false; if let Value::Object(obj) = addr { @@ -226,10 +223,10 @@ fn validate_identity_value( match (key, value) { (Property::Email, Value::Text(value)) if value.len() < 255 => { is_valid = true; - address.email = value; + address.email = value.into(); } (Property::Name, Value::Text(value)) if value.len() < 255 => { - address.name = Some(value); + address.name = Some(value.into()); } (Property::Name, Value::Null) => (), _ => { diff --git a/crates/jmap/src/lib.rs b/crates/jmap/src/lib.rs index f5a3f1e2..9a5bfdff 100644 --- a/crates/jmap/src/lib.rs +++ b/crates/jmap/src/lib.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +#![warn(clippy::large_futures)] + use std::{fmt::Display, future::Future}; use changes::state::StateManager; diff --git a/crates/jmap/src/mailbox/set.rs b/crates/jmap/src/mailbox/set.rs index b14027e4..4b6c15e3 100644 --- a/crates/jmap/src/mailbox/set.rs +++ b/crates/jmap/src/mailbox/set.rs @@ -289,7 +289,7 @@ impl MailboxSet for Server { (Property::Name, MaybePatchValue::Value(Value::Text(value))) => { let value = value.trim(); if !value.is_empty() && value.len() < self.core.jmap.mailbox_name_max_len { - changes.name = value.to_string(); + changes.name = value.into(); } else { return Ok(Err(SetError::invalid_properties() .with_property(Property::Name) diff --git a/crates/jmap/src/principal/get.rs b/crates/jmap/src/principal/get.rs index 5e7f61d2..bff1423c 100644 --- a/crates/jmap/src/principal/get.rs +++ b/crates/jmap/src/principal/get.rs @@ -5,7 +5,7 @@ */ use common::Server; -use directory::{QueryBy, backend::internal::PrincipalField}; +use directory::QueryBy; use jmap_proto::{ method::get::{GetRequest, GetResponse, RequestArguments}, types::{ @@ -85,9 +85,9 @@ impl PrincipalGet for Server { .map(|v| Value::Text(v.to_string())) .unwrap_or(Value::Null), Property::Email => principal - .iter_str(PrincipalField::Emails) - .next() - .map(|email| Value::Text(email.clone())) + .emails + .first() + .map(|email| Value::Text(email.to_string())) .unwrap_or(Value::Null), _ => Value::Null, }; diff --git a/crates/jmap/src/push/set.rs b/crates/jmap/src/push/set.rs index 095b4761..30f41a13 100644 --- a/crates/jmap/src/push/set.rs +++ b/crates/jmap/src/push/set.rs @@ -6,6 +6,7 @@ use base64::{Engine, engine::general_purpose}; use common::{Server, auth::AccessToken}; +use compact_str::CompactString; use email::push::{Keys, PushSubscription}; use jmap_proto::{ error::set::SetError, @@ -100,7 +101,7 @@ impl PushSubscriptionSet for Server { .sample_iter(Alphanumeric) .take(VERIFICATION_CODE_LEN) .map(char::from) - .collect::(); + .collect::(); // Insert record let document_id = self @@ -217,12 +218,12 @@ fn validate_push_value( (Property::DeviceClientId, MaybePatchValue::Value(Value::Text(value))) if is_create && value.len() < 255 => { - push.device_client_id = value; + push.device_client_id = value.into(); } (Property::Url, MaybePatchValue::Value(Value::Text(value))) if is_create && value.len() < 512 && value.starts_with("https://") => { - push.url = value; + push.url = value.into(); } (Property::Keys, MaybePatchValue::Value(Value::Object(value))) if is_create && value.0.len() == 2 => diff --git a/crates/jmap/src/quota/get.rs b/crates/jmap/src/quota/get.rs index 6823c5a8..2f70d5ed 100644 --- a/crates/jmap/src/quota/get.rs +++ b/crates/jmap/src/quota/get.rs @@ -78,8 +78,12 @@ impl QuotaGet for Server { Property::Used => (self.get_used_quota(account_id).await? as u64).into(), Property::HardLimit => access_token.quota.into(), Property::Scope => "account".to_string().into(), - Property::Name => access_token.name.clone().into(), - Property::Description => access_token.description.clone().into(), + Property::Name => access_token.name.to_string().into(), + Property::Description => access_token + .description + .as_ref() + .map(|s| s.to_string()) + .into(), Property::Types => vec![ Value::Text(DataType::Email.to_string()), Value::Text(DataType::SieveScript.to_string()), diff --git a/crates/jmap/src/sieve/set.rs b/crates/jmap/src/sieve/set.rs index 787eb4fb..a350a4f9 100644 --- a/crates/jmap/src/sieve/set.rs +++ b/crates/jmap/src/sieve/set.rs @@ -9,6 +9,7 @@ use common::{ auth::{AccessToken, ResourceToken}, storage::index::ObjectIndexBuilder, }; +use compact_str::CompactString; use email::sieve::{ ArchivedSieveScript, SieveScript, activate::SieveScriptActivate, delete::SieveScriptDelete, }; @@ -400,7 +401,7 @@ impl SieveScriptSet for Server { } } - changes.name = value; + changes.name = value.into(); } (Property::BlobId, MaybePatchValue::Value(Value::BlobId(value))) => { blob_id = value.into(); @@ -424,7 +425,7 @@ impl SieveScriptSet for Server { .sample_iter(Alphanumeric) .take(15) .map(char::from) - .collect::(); + .collect::(); } // Set script as inactive diff --git a/crates/jmap/src/submission/set.rs b/crates/jmap/src/submission/set.rs index 7b6a727e..da655a00 100644 --- a/crates/jmap/src/submission/set.rs +++ b/crates/jmap/src/submission/set.rs @@ -11,6 +11,7 @@ use common::{ listener::{ServerInstance, stream::NullIo}, storage::index::ObjectIndexBuilder, }; +use compact_str::CompactString; use email::{ identity::Identity, message::metadata::{ArchivedHeaderName, ArchivedHeaderValue, MessageMetadata}, @@ -38,7 +39,7 @@ use smtp::{ core::{Session, SessionData}, queue::spool::SmtpSpool, }; -use smtp_proto::{MailFrom, RcptTo, request::parser::Rfc5321Parser}; +use smtp_proto::{MailFrom, Mtrk, RcptTo, request::parser::Rfc5321Parser}; use store::write::{BatchBuilder, now}; use trc::AddContext; use utils::{BlobHash, map::vec_map::VecMap, sanitize_email}; @@ -317,7 +318,7 @@ impl EmailSubmissionSet for Server { ..Default::default() }; let mut mail_from = None; - let mut rcpt_to: Vec> = Vec::new(); + let mut rcpt_to: Vec> = Vec::new(); for (property, value) in object.0 { let value = match response.eval_object_references(value) { @@ -353,7 +354,24 @@ impl EmailSubmissionSet for Server { email: addr.address.clone(), parameters: params, }; - mail_from = addr.into(); + mail_from = MailFrom { + address: CompactString::from(addr.address), + flags: addr.flags, + size: addr.size, + trans_id: addr.trans_id.map(Into::into), + by: addr.by, + env_id: addr.env_id.map(Into::into), + solicit: addr.solicit.map(Into::into), + mtrk: addr.mtrk.map(|m| Mtrk { + certifier: m.certifier.into(), + timeout: m.timeout, + }), + auth: addr.auth.map(Into::into), + hold_for: addr.hold_for, + hold_until: addr.hold_until, + mt_priority: addr.mt_priority, + } + .into(); } Err(err) => { return Ok(Err(SetError::invalid_properties() @@ -389,7 +407,14 @@ impl EmailSubmissionSet for Server { email: addr.address.clone(), parameters: params, }); - rcpt_to.push(addr); + rcpt_to.push(RcptTo { + address: CompactString::new( + addr.address, + ), + orcpt: addr.orcpt.map(Into::into), + rrvs: addr.rrvs, + flags: addr.flags, + }); } } Err(err) => { @@ -469,7 +494,7 @@ impl EmailSubmissionSet for Server { parameters: None, }; MailFrom { - address: identity_mail_from, + address: CompactString::new(identity_mail_from.as_str()), ..Default::default() } }; @@ -510,11 +535,11 @@ impl EmailSubmissionSet for Server { if let Some(address) = address.address().and_then(sanitize_email) { if !rcpt_to.iter().any(|rcpt| rcpt.address == address) { submission.envelope.rcpt_to.push(Address { - email: address.clone(), + email: address.to_string(), parameters: None, }); rcpt_to.push(RcptTo { - address, + address: CompactString::new(address.as_str()), ..Default::default() }); } @@ -635,14 +660,16 @@ impl EmailSubmissionSet for Server { .into_iter() .map(|(addr, response)| { ( - addr, + addr.to_string(), DeliveryStatus { delivered: if response.is_none() { Delivered::Unknown } else { Delivered::No }, - smtp_reply: response.unwrap_or_else(|| "250 2.1.5 Queued".to_string()), + smtp_reply: response + .map(|s| s.to_string()) + .unwrap_or_else(|| "250 2.1.5 Queued".to_string()), displayed: false, }, ) @@ -690,9 +717,9 @@ fn parse_envelope_address( } params_text.push('\n'); - Ok((addr, Some(params_list), Some(params_text))) + Ok((addr.to_string(), Some(params_list), Some(params_text))) } else { - Ok((addr, None, None)) + Ok((addr.to_string(), None, None)) } } else { Err(SetError::invalid_properties() diff --git a/crates/jmap/src/vacation/set.rs b/crates/jmap/src/vacation/set.rs index 91064522..ce6b3547 100644 --- a/crates/jmap/src/vacation/set.rs +++ b/crates/jmap/src/vacation/set.rs @@ -174,19 +174,19 @@ impl VacationResponseSet for Server { if value.len() < 512 => { build_script = true; - vacation.subject = Some(value); + vacation.subject = Some(value.into()); } (Property::HtmlBody, MaybePatchValue::Value(Value::Text(value))) if value.len() < 2048 => { build_script = true; - vacation.html_body = Some(value); + vacation.html_body = Some(value.into()); } (Property::TextBody, MaybePatchValue::Value(Value::Text(value))) if value.len() < 2048 => { build_script = true; - vacation.text_body = Some(value); + vacation.text_body = Some(value.into()); } (Property::FromDate, MaybePatchValue::Value(Value::Date(date))) => { vacation.from_date = Some(date.timestamp() as u64); diff --git a/crates/jmap/src/websocket/stream.rs b/crates/jmap/src/websocket/stream.rs index 59afdf32..3a37d85d 100644 --- a/crates/jmap/src/websocket/stream.rs +++ b/crates/jmap/src/websocket/stream.rs @@ -36,6 +36,7 @@ pub trait WebSocketHandler: Sync + Send { } impl WebSocketHandler for Server { + #![allow(clippy::large_futures)] async fn handle_websocket_stream( &self, mut stream: WebSocketStream>, diff --git a/crates/jmap/src/websocket/upgrade.rs b/crates/jmap/src/websocket/upgrade.rs index 9a53eb06..a1f630c3 100644 --- a/crates/jmap/src/websocket/upgrade.rs +++ b/crates/jmap/src/websocket/upgrade.rs @@ -79,15 +79,17 @@ impl WebSocketUpgrade for Server { let session_id = session.session_id; match hyper::upgrade::on(req).await { Ok(upgraded) => { - jmap.handle_websocket_stream( - WebSocketStream::from_raw_socket( - TokioIo::new(upgraded), - Role::Server, - None, - ) - .await, - access_token, - session, + Box::pin( + jmap.handle_websocket_stream( + WebSocketStream::from_raw_socket( + TokioIo::new(upgraded), + Role::Server, + None, + ) + .await, + access_token, + session, + ), ) .await; } diff --git a/crates/main/src/main.rs b/crates/main/src/main.rs index 8aea73df..bbbceb5a 100644 --- a/crates/main/src/main.rs +++ b/crates/main/src/main.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +#![warn(clippy::large_futures)] + use std::time::Duration; use common::{config::server::ServerProtocol, core::BuildServer, manager::boot::BootManager}; @@ -26,7 +28,7 @@ static GLOBAL: Jemalloc = Jemalloc; #[tokio::main] async fn main() -> std::io::Result<()> { // Load config and apply macros - let mut init = BootManager::init().await; + let mut init = Box::pin(BootManager::init()).await; // Init services init.start_services().await; diff --git a/crates/managesieve/src/core/client.rs b/crates/managesieve/src/core/client.rs index 7ca5c2c3..1b4b2aff 100644 --- a/crates/managesieve/src/core/client.rs +++ b/crates/managesieve/src/core/client.rs @@ -54,8 +54,8 @@ impl Session { (&self.state, response.key(trc::Key::Code)), ( State::NotAuthenticated { .. }, - Some(trc::Value::Static("PARSE")) - ) + Some(trc::Value::String(v)) + ) if v == "PARSE" ) { match self.server.is_scanner_fail2banned(self.remote_addr).await { Ok(true) => { diff --git a/crates/managesieve/src/core/mod.rs b/crates/managesieve/src/core/mod.rs index bd014efe..35f02d6d 100644 --- a/crates/managesieve/src/core/mod.rs +++ b/crates/managesieve/src/core/mod.rs @@ -302,12 +302,12 @@ impl SerializeResponse for trc::Error { impl From for trc::Value { fn from(value: ResponseCode) -> Self { - trc::Value::Static(value.as_str()) + trc::Value::String(CompactString::const_new(value.as_str())) } } impl From for trc::Value { fn from(value: ResponseType) -> Self { - trc::Value::Static(value.as_str()) + trc::Value::String(CompactString::const_new(value.as_str())) } } diff --git a/crates/services/Cargo.toml b/crates/services/Cargo.toml index 2713c5b5..8bcc12fe 100644 --- a/crates/services/Cargo.toml +++ b/crates/services/Cargo.toml @@ -26,6 +26,7 @@ hkdf = "0.12.3" sha2 = "0.10" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls-webpki-roots", "http2"]} base64 = "0.22" +compact_str = "0.9.0" [dev-dependencies] diff --git a/crates/services/src/index/mod.rs b/crates/services/src/index/mod.rs index 37f27f12..8ea36492 100644 --- a/crates/services/src/index/mod.rs +++ b/crates/services/src/index/mod.rs @@ -7,10 +7,7 @@ use std::{sync::Arc, time::Instant}; use common::{Inner, KV_LOCK_EMAIL_TASK, Server, core::BuildServer}; -use directory::{ - Type, - backend::internal::{PrincipalField, manage::ManageDirectory}, -}; +use directory::{Type, backend::internal::manage::ManageDirectory}; use email::message::{bayes::EmailBayesTrain, index::IndexMessageText, metadata::MessageMetadata}; use jmap_proto::types::{collection::Collection, property::Property}; use mail_parser::MessageParser; @@ -353,7 +350,7 @@ impl Indexer for Server { None, tenant_id, &[Type::Individual, Type::Group], - &[PrincipalField::Name], + false, 0, 0, ) diff --git a/crates/services/src/state_manager/http.rs b/crates/services/src/state_manager/http.rs index e65edb89..7aa11b44 100644 --- a/crates/services/src/state_manager/http.rs +++ b/crates/services/src/state_manager/http.rs @@ -8,6 +8,7 @@ use std::time::{Duration, Instant}; use base64::Engine; use common::ipc::EncryptionKeys; +use compact_str::CompactString; use jmap_proto::{response::status::StateChangeResponse, types::id::Id}; use reqwest::header::{CONTENT_ENCODING, CONTENT_TYPE}; use tokio::sync::mpsc; @@ -57,7 +58,7 @@ impl PushServer { } pub(crate) async fn http_request( - url: String, + url: CompactString, mut body: String, keys: Option, push_timeout: Duration, @@ -70,7 +71,7 @@ pub(crate) async fn http_request( let mut client = client_builder .build() .unwrap_or_default() - .post(&url) + .post(url.as_str()) .header(CONTENT_TYPE, "application/json") .header("TTL", "86400"); diff --git a/crates/services/src/state_manager/mod.rs b/crates/services/src/state_manager/mod.rs index 6c4fad61..6e392c8d 100644 --- a/crates/services/src/state_manager/mod.rs +++ b/crates/services/src/state_manager/mod.rs @@ -12,6 +12,7 @@ pub mod push; use std::time::{Duration, Instant}; use common::ipc::EncryptionKeys; +use compact_str::CompactString; use jmap_proto::types::{id::Id, state::StateChange, type_state::DataType}; use tokio::sync::mpsc; use utils::map::bitmap::Bitmap; @@ -30,7 +31,7 @@ pub enum SubscriberType { #[derive(Debug)] pub struct PushServer { - url: String, + url: CompactString, keys: Option, num_attempts: u32, last_request: Instant, @@ -62,13 +63,13 @@ pub enum PushUpdate { Verify { id: u32, account_id: u32, - url: String, - code: String, + url: CompactString, + code: CompactString, keys: Option, }, Register { id: Id, - url: String, + url: CompactString, keys: Option, }, Unregister { diff --git a/crates/smtp/Cargo.toml b/crates/smtp/Cargo.toml index a0d2f67b..2374243e 100644 --- a/crates/smtp/Cargo.toml +++ b/crates/smtp/Cargo.toml @@ -54,6 +54,7 @@ num_cpus = "1.15.0" bincode = "1.3.1" chrono = "0.4" rkyv = { version = "0.8.10", features = ["little_endian"] } +compact_str = "0.9.0" [features] test_mode = [] diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index 92d24a44..8a47b772 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -17,6 +17,7 @@ use common::{ config::smtp::auth::VerifyStrategy, listener::{ServerInstance, asn::AsnGeoLookupResult}, }; +use compact_str::{CompactString, ToCompactString}; use directory::Directory; use mail_auth::{IprevOutput, SpfOutput}; use smtp_proto::request::receiver::{ @@ -55,7 +56,7 @@ pub enum State { } pub struct Session { - pub hostname: String, + pub hostname: CompactString, pub state: State, pub instance: Arc, pub server: Server, @@ -67,13 +68,13 @@ pub struct Session { pub struct SessionData { pub session_id: u64, pub local_ip: IpAddr, - pub local_ip_str: String, + pub local_ip_str: CompactString, pub local_port: u16, pub remote_ip: IpAddr, - pub remote_ip_str: String, + pub remote_ip_str: CompactString, pub remote_port: u16, pub asn_geo_data: AsnGeoLookupResult, - pub helo_domain: String, + pub helo_domain: CompactString, pub mail_from: Option, pub rcpt_to: Vec, @@ -100,11 +101,11 @@ pub struct SessionData { #[derive(Clone, Debug)] pub struct SessionAddress { - pub address: String, - pub address_lcase: String, - pub domain: String, + pub address: CompactString, + pub address_lcase: CompactString, + pub domain: CompactString, pub flags: u64, - pub dsn_info: Option, + pub dsn_info: Option, } #[derive(Debug, Default)] @@ -151,11 +152,11 @@ impl SessionData { local_ip, local_port, remote_ip, - local_ip_str: local_ip.to_string(), - remote_ip_str: remote_ip.to_string(), + local_ip_str: local_ip.to_compact_string(), + remote_ip_str: remote_ip.to_compact_string(), remote_port, asn_geo_data, - helo_domain: String::new(), + helo_domain: CompactString::new(""), mail_from: None, rcpt_to: Vec::new(), authenticated_as: None, @@ -219,7 +220,7 @@ impl Session { data: SessionData, ) -> Self { Session { - hostname: "localhost".to_string(), + hostname: "localhost".into(), state: State::None, instance, server, @@ -247,7 +248,7 @@ impl Session { } } - pub fn has_failed(&mut self) -> Option { + pub fn has_failed(&mut self) -> Option { if self.stream.tx_buf.first().is_none_or(|&c| c == b'2') { self.stream.tx_buf.clear(); None @@ -255,7 +256,7 @@ impl Session { let response = std::str::from_utf8(&self.stream.tx_buf) .unwrap() .trim() - .to_string(); + .into(); self.stream.tx_buf.clear(); Some(response) } @@ -273,8 +274,8 @@ impl SessionData { SessionData { local_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), remote_ip: IpAddr::V4(std::net::Ipv4Addr::new(127, 0, 0, 1)), - local_ip_str: "127.0.0.1".to_string(), - remote_ip_str: "127.0.0.1".to_string(), + local_ip_str: "127.0.0.1".into(), + remote_ip_str: "127.0.0.1".into(), remote_port: 0, local_port: 0, session_id, @@ -308,10 +309,10 @@ impl Default for SessionData { } impl SessionAddress { - pub fn new(address: String) -> Self { + pub fn new(address: CompactString) -> Self { let address_lcase = address.to_lowercase(); SessionAddress { - domain: address_lcase.domain_part().to_string(), + domain: address_lcase.domain_part().into(), address_lcase, address, flags: 0, diff --git a/crates/smtp/src/core/params.rs b/crates/smtp/src/core/params.rs index 47827cae..802947c8 100644 --- a/crates/smtp/src/core/params.rs +++ b/crates/smtp/src/core/params.rs @@ -7,6 +7,7 @@ use std::time::Duration; use common::{config::smtp::auth::VerifyStrategy, listener::SessionStream}; +use compact_str::CompactString; use super::Session; @@ -74,7 +75,7 @@ impl Session { let ac = &self.server.core.smtp.session.auth; self.params.auth_directory = self .server - .eval_if::(&ac.directory, self, self.data.session_id) + .eval_if::(&ac.directory, self, self.data.session_id) .await .and_then(|name| self.server.get_directory(&name)) .cloned(); diff --git a/crates/smtp/src/inbound/auth.rs b/crates/smtp/src/inbound/auth.rs index 8b4409ae..bd5b4129 100644 --- a/crates/smtp/src/inbound/auth.rs +++ b/crates/smtp/src/inbound/auth.rs @@ -13,6 +13,7 @@ use common::{ }, listener::SessionStream, }; +use compact_str::CompactString; use directory::Permission; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; @@ -224,7 +225,7 @@ impl Session { self.data.authenticated_as.is_some() } - pub fn authenticated_emails(&self) -> &[String] { + pub fn authenticated_emails(&self) -> &[CompactString] { self.data .authenticated_as .as_ref() diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index 2c73762b..a9ed1387 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -18,6 +18,7 @@ use common::{ psl, scripts::ScriptModification, }; +use compact_str::CompactString; use mail_auth::{ AuthenticatedMessage, AuthenticationResults, DkimResult, DmarcResult, ReceivedSpf, common::{headers::HeaderWriter, verify::VerifySignature}, @@ -169,7 +170,7 @@ impl Session { .unwrap_or(VerifyStrategy::Relaxed); let arc_sealer = self .server - .eval_if::(&ac.arc.seal, self, self.data.session_id) + .eval_if::(&ac.arc.seal, self, self.data.session_id) .await .and_then(|name| self.server.get_arc_sealer(&name, self.data.session_id)); let arc_output = if arc.verify() || arc_sealer.is_some() { @@ -496,7 +497,7 @@ impl Session { // Sieve filtering if let Some((script, script_id)) = self .server - .eval_if::(&dc.script, self, self.data.session_id) + .eval_if::(&dc.script, self, self.data.session_id) .await .and_then(|name| { self.server @@ -563,7 +564,7 @@ impl Session { modifications } ScriptResult::Reject(message) => { - return message.into_bytes().into(); + return message.as_bytes().to_vec().into(); } ScriptResult::Discard => { return (b"250 2.0.0 Message queued for delivery.\r\n"[..]).into(); @@ -635,7 +636,7 @@ impl Session { let raw_message = edited_message.as_deref().unwrap_or(raw_message.as_slice()); for signer in self .server - .eval_if::, _>(&ac.dkim.sign, self, self.data.session_id) + .eval_if::, _>(&ac.dkim.sign, self, self.data.session_id) .await .unwrap_or_default() { diff --git a/crates/smtp/src/inbound/ehlo.rs b/crates/smtp/src/inbound/ehlo.rs index c24ef63a..b4cd636e 100644 --- a/crates/smtp/src/inbound/ehlo.rs +++ b/crates/smtp/src/inbound/ehlo.rs @@ -11,6 +11,7 @@ use common::{ config::smtp::session::{Mechanism, Stage}, listener::SessionStream, }; +use compact_str::CompactString; use mail_auth::{ SpfResult, spf::verify::{HasValidLabels, SpfParameters}, @@ -19,7 +20,11 @@ use smtp_proto::*; use trc::SmtpEvent; impl Session { - pub async fn handle_ehlo(&mut self, domain: String, is_extended: bool) -> Result<(), ()> { + pub async fn handle_ehlo( + &mut self, + domain: CompactString, + is_extended: bool, + ) -> Result<(), ()> { // Set EHLO domain if domain != self.data.helo_domain { @@ -86,7 +91,7 @@ impl Session { // Sieve filtering if let Some((script, script_id)) = self .server - .eval_if::( + .eval_if::( &self.server.core.smtp.session.ehlo.script, self, self.data.session_id, @@ -272,7 +277,7 @@ impl Session { // No soliciting if let Some(value) = self .server - .eval_if::(&ec.no_soliciting, self, self.data.session_id) + .eval_if::(&ec.no_soliciting, self, self.data.session_id) .await { response.capabilities |= EXT_NO_SOLICITING; diff --git a/crates/smtp/src/inbound/hooks/message.rs b/crates/smtp/src/inbound/hooks/message.rs index 2f03eee7..130d94df 100644 --- a/crates/smtp/src/inbound/hooks/message.rs +++ b/crates/smtp/src/inbound/hooks/message.rs @@ -12,6 +12,7 @@ use common::{ config::smtp::session::{MTAHook, Stage}, listener::SessionStream, }; +use compact_str::{CompactString, ToCompactString}; use mail_auth::AuthenticatedMessage; use trc::MtaHookEvent; @@ -88,7 +89,7 @@ impl Session { } super::Modification::ReplaceContents { value } => { Modification::ReplaceBody { - value: value.into_bytes(), + value: value.as_bytes().to_vec(), } } super::Modification::AddHeader { name, value } => { @@ -104,7 +105,7 @@ impl Session { Modification::ChangeHeader { index, name, - value: String::new(), + value: CompactString::new(""), } } }); @@ -131,8 +132,8 @@ impl Session { Action::Reject => FilterResponse::reject(), Action::Quarantine => { modifications.push(Modification::AddHeader { - name: "X-Quarantine".to_string(), - value: "true".to_string(), + name: "X-Quarantine".into(), + value: "true".into(), }); FilterResponse::accept() } @@ -183,7 +184,7 @@ impl Session { context: Context { stage: stage.into(), client: Client { - ip: self.data.remote_ip.to_string(), + ip: self.data.remote_ip.to_compact_string(), port: self.data.remote_port, ptr: self .data @@ -191,29 +192,29 @@ impl Session { .as_ref() .and_then(|ip_rev| ip_rev.ptr.as_ref()) .and_then(|ptrs| ptrs.first()) - .cloned(), + .map(Into::into), helo: (!self.data.helo_domain.is_empty()) .then(|| self.data.helo_domain.clone()), active_connections: 1, }, sasl: self.authenticated_as().map(|name| Sasl { - login: name.to_string(), + login: name.into(), method: None, }), tls: (!tls_version.is_empty()).then(|| Tls { - version: tls_version.to_string(), - cipher: tls_cipher.to_string(), + version: tls_version.as_ref().into(), + cipher: tls_cipher.as_ref().into(), bits: None, issuer: None, subject: None, }), server: Server { - name: DAEMON_NAME.to_string().into(), + name: Some(DAEMON_NAME.into()), port: self.data.local_port, - ip: self.data.local_ip.to_string().into(), + ip: self.data.local_ip.to_compact_string().into(), }, queue: queue_id.map(|id| Queue { - id: format!("{:x}", id), + id: format!("{:x}", id).into(), }), protocol: Protocol { version: 1 }, }, @@ -238,13 +239,13 @@ impl Session { .iter() .map(|(k, v)| { ( - String::from_utf8_lossy(k).into_owned(), - String::from_utf8_lossy(v).into_owned(), + CompactString::from_utf8_lossy(k), + CompactString::from_utf8_lossy(v), ) }) .collect(), server_headers: vec![], - contents: String::from_utf8_lossy(message.raw_body()).into_owned(), + contents: CompactString::from_utf8_lossy(message.raw_body()), size: message.raw_message().len(), }), }; @@ -253,8 +254,8 @@ impl Session { } } -fn flatten_parameters(parameters: AHashMap>) -> String { - let mut arguments = String::new(); +fn flatten_parameters(parameters: AHashMap>) -> CompactString { + let mut arguments = CompactString::new(""); for (key, value) in parameters { if !arguments.is_empty() { arguments.push(' '); diff --git a/crates/smtp/src/inbound/hooks/mod.rs b/crates/smtp/src/inbound/hooks/mod.rs index 2663f3d3..43e6b0d2 100644 --- a/crates/smtp/src/inbound/hooks/mod.rs +++ b/crates/smtp/src/inbound/hooks/mod.rs @@ -8,6 +8,7 @@ pub mod client; pub mod message; use ahash::AHashMap; +use compact_str::CompactString; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize)] @@ -35,46 +36,46 @@ pub struct Context { #[derive(Serialize, Deserialize)] pub struct Sasl { - pub login: String, + pub login: CompactString, #[serde(skip_serializing_if = "Option::is_none")] - pub method: Option, + pub method: Option, } #[derive(Serialize, Deserialize)] pub struct Client { - pub ip: String, + pub ip: CompactString, pub port: u16, - pub ptr: Option, - pub helo: Option, + pub ptr: Option, + pub helo: Option, #[serde(rename = "activeConnections")] pub active_connections: u32, } #[derive(Serialize, Deserialize)] pub struct Tls { - pub version: String, - pub cipher: String, + pub version: CompactString, + pub cipher: CompactString, #[serde(rename = "cipherBits")] #[serde(skip_serializing_if = "Option::is_none")] pub bits: Option, #[serde(rename = "certIssuer")] #[serde(skip_serializing_if = "Option::is_none")] - pub issuer: Option, + pub issuer: Option, #[serde(rename = "certSubject")] #[serde(skip_serializing_if = "Option::is_none")] - pub subject: Option, + pub subject: Option, } #[derive(Serialize, Deserialize)] pub struct Server { - pub name: Option, + pub name: Option, pub port: u16, - pub ip: Option, + pub ip: Option, } #[derive(Serialize, Deserialize)] pub struct Queue { - pub id: String, + pub id: CompactString, } #[derive(Serialize, Deserialize)] @@ -100,9 +101,9 @@ pub enum Stage { #[derive(Serialize, Deserialize)] pub struct Address { - pub address: String, + pub address: CompactString, #[serde(skip_serializing_if = "Option::is_none")] - pub parameters: Option>, + pub parameters: Option>, } #[derive(Serialize, Deserialize)] @@ -113,12 +114,12 @@ pub struct Envelope { #[derive(Serialize, Deserialize)] pub struct Message { - pub headers: Vec<(String, String)>, + pub headers: Vec<(CompactString, CompactString)>, #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(rename = "serverHeaders")] #[serde(default)] - pub server_headers: Vec<(String, String)>, - pub contents: String, + pub server_headers: Vec<(CompactString, CompactString)>, + pub contents: CompactString, pub size: usize, } @@ -148,9 +149,9 @@ pub struct SmtpResponse { #[serde(default)] pub status: Option, #[serde(default)] - pub enhanced_status: Option, + pub enhanced_status: Option, #[serde(default)] - pub message: Option, + pub message: Option, #[serde(default)] pub disconnect: bool, } @@ -160,36 +161,39 @@ pub struct SmtpResponse { pub enum Modification { #[serde(rename = "changeFrom")] ChangeFrom { - value: String, + value: CompactString, #[serde(default)] - parameters: AHashMap>, + parameters: AHashMap>, }, #[serde(rename = "addRecipient")] AddRecipient { - value: String, + value: CompactString, #[serde(default)] - parameters: AHashMap>, + parameters: AHashMap>, }, #[serde(rename = "deleteRecipient")] - DeleteRecipient { value: String }, + DeleteRecipient { value: CompactString }, #[serde(rename = "replaceContents")] - ReplaceContents { value: String }, + ReplaceContents { value: CompactString }, #[serde(rename = "addHeader")] - AddHeader { name: String, value: String }, + AddHeader { + name: CompactString, + value: CompactString, + }, #[serde(rename = "insertHeader")] InsertHeader { index: u32, - name: String, - value: String, + name: CompactString, + value: CompactString, }, #[serde(rename = "changeHeader")] ChangeHeader { index: u32, - name: String, - value: String, + name: CompactString, + value: CompactString, }, #[serde(rename = "deleteHeader")] - DeleteHeader { index: u32, name: String }, + DeleteHeader { index: u32, name: CompactString }, } impl From for Stage { diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs index 371b991d..d61ede11 100644 --- a/crates/smtp/src/inbound/mail.rs +++ b/crates/smtp/src/inbound/mail.rs @@ -7,6 +7,7 @@ use std::time::{Duration, Instant, SystemTime}; use common::{config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification}; +use compact_str::CompactString; use mail_auth::{IprevOutput, IprevResult, SpfOutput, SpfResult, spf::verify::SpfParameters}; use smtp_proto::{MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_REQUIRETLS, MailFrom, MtPriority}; use trc::SmtpEvent; @@ -19,7 +20,7 @@ use crate::{ }; impl Session { - pub async fn handle_mail_from(&mut self, from: MailFrom) -> Result<(), ()> { + pub async fn handle_mail_from(&mut self, from: MailFrom) -> Result<(), ()> { if self.data.helo_domain.is_empty() && (self.params.ehlo_require || self.params.spf_ehlo.verify() @@ -109,10 +110,14 @@ impl Session { let (address, address_lcase, domain) = if !from.address.is_empty() { let address_lcase = from.address.to_lowercase(); - let domain = address_lcase.domain_part().to_string(); + let domain = address_lcase.domain_part().into(); (from.address, address_lcase, domain) } else { - (String::new(), String::new(), String::new()) + ( + CompactString::new(""), + CompactString::new(""), + CompactString::new(""), + ) }; let has_dsn = from.env_id.is_some(); @@ -150,7 +155,7 @@ impl Session { // Sieve filtering if let Some((script, script_id)) = self .server - .eval_if::( + .eval_if::( &self.server.core.smtp.session.mail.script, self, self.data.session_id, @@ -202,7 +207,7 @@ impl Session { // Address rewriting if let Some(new_address) = self .server - .eval_if::( + .eval_if::( &self.server.core.smtp.session.mail.rewrite, self, self.data.session_id, @@ -220,7 +225,7 @@ impl Session { if new_address.contains('@') { mail_from.address_lcase = new_address.to_lowercase(); - mail_from.domain = mail_from.address_lcase.domain_part().to_string(); + mail_from.domain = mail_from.address_lcase.domain_part().into(); mail_from.address = new_address; } else if new_address.is_empty() { mail_from.address_lcase.clear(); @@ -245,19 +250,20 @@ impl Session { let address_lcase = self.data.mail_from.as_ref().unwrap().address_lcase.as_str(); if authenticated_as != address_lcase && !self.authenticated_emails().iter().any(|e| { - e == address_lcase || (e.starts_with('@') && address_lcase.ends_with(e)) + e == address_lcase + || (e.starts_with('@') && address_lcase.ends_with(e.as_str())) }) { trc::event!( Smtp(SmtpEvent::MailFromUnauthorized), SpanId = self.data.session_id, From = address_lcase.to_string(), - Details = [trc::Value::String(authenticated_as.to_string())] + Details = [trc::Value::String(authenticated_as.into())] .into_iter() .chain( self.authenticated_emails() .iter() - .map(|e| trc::Value::String(e.to_string())) + .map(|e| trc::Value::String(e.clone())) ) .collect::>() ); diff --git a/crates/smtp/src/inbound/milter/macros.rs b/crates/smtp/src/inbound/milter/macros.rs index 2d61a1a2..1e0dd506 100644 --- a/crates/smtp/src/inbound/milter/macros.rs +++ b/crates/smtp/src/inbound/milter/macros.rs @@ -6,6 +6,8 @@ use std::{borrow::Cow, net::IpAddr}; +use compact_str::CompactString; + use super::{Macro, Macros}; pub trait IntoMacroValue<'x> { @@ -159,6 +161,12 @@ impl<'x> IntoMacroValue<'x> for &'x str { } } +impl<'x> IntoMacroValue<'x> for &'x CompactString { + fn into_macro_value(self) -> Cow<'x, [u8]> { + Cow::Borrowed(self.as_bytes()) + } +} + impl<'x> IntoMacroValue<'x> for String { fn into_macro_value(self) -> Cow<'x, [u8]> { Cow::Owned(self.into_bytes()) diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs index 64f08a1e..56011c00 100644 --- a/crates/smtp/src/inbound/milter/message.rs +++ b/crates/smtp/src/inbound/milter/message.rs @@ -11,6 +11,7 @@ use common::{ config::smtp::session::{Milter, Stage}, listener::SessionStream, }; +use compact_str::CompactString; use mail_auth::AuthenticatedMessage; use smtp_proto::{IntoString, request::parser::Rfc5321Parser}; use tokio::io::{AsyncRead, AsyncWrite}; @@ -196,10 +197,11 @@ impl Session { .iprev .as_ref() .and_then(|ip_rev| ip_rev.ptr.as_ref()) - .and_then(|ptrs| ptrs.first()); + .and_then(|ptrs| ptrs.first()) + .map(|s| s.as_str()); client .connection( - client_ptr.unwrap_or(&self.data.helo_domain), + client_ptr.unwrap_or(self.data.helo_domain.as_str()), self.data.remote_ip, self.data.remote_port, Macros::new() @@ -207,7 +209,7 @@ impl Session { .with_local_hostname(&self.hostname) .with_client_address(self.data.remote_ip) .with_client_port(self.data.remote_port) - .with_client_ptr(client_ptr.map(|p| p.as_str()).unwrap_or("unknown")), + .with_client_ptr(client_ptr.unwrap_or("unknown")), ) .await? .assert_continue()?; @@ -305,7 +307,7 @@ impl SessionData { let sender = strip_brackets(&sender); let address_lcase = sender.to_lowercase(); let mut mail_from = SessionAddress { - domain: address_lcase.domain_part().to_string(), + domain: address_lcase.domain_part().into(), address_lcase, address: sender, flags: 0, @@ -318,7 +320,7 @@ impl SessionData { { Ok(addr) => { mail_from.flags = addr.flags; - mail_from.dsn_info = addr.env_id; + mail_from.dsn_info = addr.env_id.map(Into::into); } Err(err) => { trc::event!( @@ -341,7 +343,7 @@ impl SessionData { if recipient.contains('@') { let address_lcase = recipient.to_lowercase(); let mut rcpt = SessionAddress { - domain: address_lcase.domain_part().to_string(), + domain: address_lcase.domain_part().into(), address_lcase, address: recipient, flags: 0, @@ -354,7 +356,7 @@ impl SessionData { { Ok(addr) => { rcpt.flags = addr.flags; - rcpt.dsn_info = addr.orcpt; + rcpt.dsn_info = addr.orcpt.map(Into::into); } Err(err) => { trc::event!( @@ -400,7 +402,7 @@ impl SessionData { } } Modification::Quarantine { reason } => { - header_changes.push((0, "X-Quarantine".to_string(), reason, false)); + header_changes.push((0, "X-Quarantine".into(), reason, false)); } } } @@ -439,7 +441,7 @@ impl SessionData { header_count += 1; if header_count == index { if !header_value.is_empty() { - *value = Cow::from(header_value.into_bytes()); + *value = Cow::from(header_value.as_bytes().to_vec()); } else { headers.remove(pos); } @@ -465,8 +467,8 @@ impl SessionData { headers.insert( header_pos, ( - Cow::from(header_name.into_bytes()), - Cow::from(header_value.into_bytes()), + Cow::from(header_name.as_bytes().to_vec()), + Cow::from(header_value.as_bytes().to_vec()), ), ); } @@ -535,15 +537,15 @@ impl From for Rejection { } } -fn strip_brackets(addr: &str) -> String { +fn strip_brackets(addr: &str) -> CompactString { let addr = addr.trim(); if let Some(addr) = addr.strip_prefix('<') { if let Some((addr, _)) = addr.rsplit_once('>') { - addr.trim().to_string() + addr.trim().into() } else { - addr.trim().to_string() + addr.trim().into() } } else { - addr.to_string() + addr.into() } } diff --git a/crates/smtp/src/inbound/milter/mod.rs b/crates/smtp/src/inbound/milter/mod.rs index 596ad82c..60934b51 100644 --- a/crates/smtp/src/inbound/milter/mod.rs +++ b/crates/smtp/src/inbound/milter/mod.rs @@ -7,6 +7,7 @@ use std::{borrow::Cow, fmt::Display, net::IpAddr, sync::Arc, time::Duration}; use common::config::smtp::session::MilterVersion; +use compact_str::CompactString; use serde::{Deserialize, Serialize}; use tokio::io::{AsyncRead, AsyncWrite}; @@ -29,7 +30,7 @@ pub struct MilterClient { options: u32, flags_actions: u32, flags_protocol: u32, - id: Arc, + id: Arc, session_id: u64, } @@ -103,7 +104,7 @@ pub enum Action { Discard, Reject, TempFail, - ReplyCode { code: [u8; 3], text: String }, + ReplyCode { code: [u8; 3], text: CompactString }, Shutdown, ConnectionFailure, } @@ -111,35 +112,35 @@ pub enum Action { #[derive(Debug, Clone, Serialize, Deserialize)] pub enum Modification { ChangeFrom { - sender: String, - args: String, + sender: CompactString, + args: CompactString, }, AddRcpt { - recipient: String, - args: String, + recipient: CompactString, + args: CompactString, }, DeleteRcpt { - recipient: String, + recipient: CompactString, }, ReplaceBody { value: Vec, }, AddHeader { - name: String, - value: String, + name: CompactString, + value: CompactString, }, InsertHeader { index: u32, - name: String, - value: String, + name: CompactString, + value: CompactString, }, ChangeHeader { index: u32, - name: String, - value: String, + name: CompactString, + value: CompactString, }, Quarantine { - reason: String, + reason: CompactString, }, } diff --git a/crates/smtp/src/inbound/milter/protocol.rs b/crates/smtp/src/inbound/milter/protocol.rs index 902d0b5d..9ecf51df 100644 --- a/crates/smtp/src/inbound/milter/protocol.rs +++ b/crates/smtp/src/inbound/milter/protocol.rs @@ -6,6 +6,8 @@ use std::net::IpAddr; +use compact_str::CompactString; + use crate::inbound::milter::Action; use super::{Command, Error, Modification, Options, Response}; @@ -269,7 +271,7 @@ impl Response { match *bytes.next()? { SMFIR_ADDRCPT => Response::Modification(Modification::AddRcpt { recipient: read_nul_terminated(&mut bytes, frame_len)?, - args: String::new(), + args: CompactString::new(""), }), SMFIR_DELRCPT => Response::Modification(Modification::DeleteRcpt { recipient: read_nul_terminated(&mut bytes, frame_len)?, @@ -460,7 +462,10 @@ impl Response { } } -fn read_nul_terminated(bytes: &mut std::slice::Iter, expected_len: usize) -> Option { +fn read_nul_terminated( + bytes: &mut std::slice::Iter, + expected_len: usize, +) -> Option { let mut buf = Vec::with_capacity(expected_len); loop { match bytes.next()? { @@ -468,7 +473,7 @@ fn read_nul_terminated(bytes: &mut std::slice::Iter, expected_len: usize) -> byte => buf.push(*byte), } } - String::from_utf8(buf).ok() + CompactString::from_utf8(buf).ok() } fn read_u32(bytes: &mut std::slice::Iter) -> Option { diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index 6cd1e60a..6efe86e6 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -7,6 +7,7 @@ use common::{ KV_GREYLIST, config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification, }; +use compact_str::{CompactString, format_compact}; use directory::backend::RcptType; use smtp_proto::{ RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, RcptTo, @@ -21,7 +22,7 @@ use crate::{ }; impl Session { - pub async fn handle_rcpt_to(&mut self, to: RcptTo) -> Result<(), ()> { + pub async fn handle_rcpt_to(&mut self, to: RcptTo) -> Result<(), ()> { #[cfg(feature = "test_mode")] if self.instance.id.ends_with("-debug") { if to.address.contains("fail@") { @@ -69,7 +70,7 @@ impl Session { // Build RCPT let address_lcase = to.address.to_lowercase(); let rcpt = SessionAddress { - domain: address_lcase.domain_part().to_string(), + domain: address_lcase.domain_part().into(), address_lcase, address: to.address, flags: to.flags, @@ -90,7 +91,7 @@ impl Session { // Address rewriting and Sieve filtering let rcpt_script = self .server - .eval_if::( + .eval_if::( &self.server.core.smtp.session.rcpt.script, self, self.data.session_id, @@ -157,7 +158,7 @@ impl Session { // Address rewriting if let Some(new_address) = self .server - .eval_if::( + .eval_if::( &self.server.core.smtp.session.rcpt.rewrite, self, self.data.session_id, @@ -175,7 +176,7 @@ impl Session { if new_address.contains('@') { rcpt.address_lcase = new_address.to_lowercase(); - rcpt.domain = rcpt.address_lcase.domain_part().to_string(); + rcpt.domain = rcpt.address_lcase.domain_part().into(); rcpt.address = new_address; } } @@ -199,7 +200,7 @@ impl Session { let mut rcpt_members = None; if let Some(directory) = self .server - .eval_if::( + .eval_if::( &self.server.core.smtp.session.rcpt.directory, self, self.data.session_id, @@ -393,7 +394,7 @@ impl Session { // Expand list if let Some(members) = rcpt_members { let list_addr = self.data.rcpt_to.pop().unwrap(); - let orcpt = format!("rfc822;{}", list_addr.address_lcase); + let orcpt = format_compact!("rfc822;{}", list_addr.address_lcase); for member in members { let mut member_addr = SessionAddress::new(member); if !self.data.rcpt_to.contains(&member_addr) @@ -410,7 +411,7 @@ impl Session { self.write(b"250 2.1.5 OK\r\n").await } - async fn rcpt_error(&mut self, response: &[u8], rcpt: String) -> Result<(), ()> { + async fn rcpt_error(&mut self, response: &[u8], rcpt: CompactString) -> Result<(), ()> { tokio::time::sleep(self.params.rcpt_errors_wait).await; self.data.rcpt_errors += 1; let has_too_many_errors = self.data.rcpt_errors >= self.params.rcpt_errors_max; diff --git a/crates/smtp/src/inbound/session.rs b/crates/smtp/src/inbound/session.rs index a6ae4f4d..dcaa55e3 100644 --- a/crates/smtp/src/inbound/session.rs +++ b/crates/smtp/src/inbound/session.rs @@ -9,6 +9,7 @@ use common::{ expr::{self, functions::ResolveVariable, *}, listener::SessionStream, }; +use compact_str::ToCompactString; use smtp_proto::{ request::receiver::{ BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, @@ -34,14 +35,37 @@ impl Session { match receiver.ingest(&mut iter, bytes) { Ok(request) => match request { Request::Rcpt { to } => { - self.handle_rcpt_to(to).await?; + self.handle_rcpt_to(RcptTo { + address: to.address.into(), + orcpt: to.orcpt.map(Into::into), + rrvs: to.rrvs, + flags: to.flags, + }) + .await?; } Request::Mail { from } => { - self.handle_mail_from(from).await?; + self.handle_mail_from(MailFrom { + address: from.address.into(), + flags: from.flags, + size: from.size, + trans_id: from.trans_id.map(Into::into), + by: from.by, + env_id: from.env_id.map(Into::into), + solicit: from.solicit.map(Into::into), + mtrk: from.mtrk.map(|m| Mtrk { + certifier: m.certifier.into(), + timeout: m.timeout, + }), + auth: from.auth.map(Into::into), + hold_for: from.hold_for, + hold_until: from.hold_until, + mt_priority: from.mt_priority, + }) + .await?; } Request::Ehlo { host } => { if self.instance.protocol == ServerProtocol::Smtp { - self.handle_ehlo(host, true).await?; + self.handle_ehlo(host.into(), true).await?; } else { trc::event!( Smtp(SmtpEvent::LhloExpected), @@ -196,7 +220,7 @@ impl Session { } Request::Helo { host } => { if self.instance.protocol == ServerProtocol::Smtp { - self.handle_ehlo(host, false).await?; + self.handle_ehlo(host.into(), false).await?; } else { trc::event!( Smtp(SmtpEvent::LhloExpected), @@ -209,7 +233,7 @@ impl Session { } Request::Lhlo { host } => { if self.instance.protocol == ServerProtocol::Lmtp { - self.handle_ehlo(host, true).await?; + self.handle_ehlo(host.into(), true).await?; } else { trc::event!( Smtp(SmtpEvent::EhloExpected), @@ -552,7 +576,7 @@ impl ResolveVariable for Session { .data .rcpt_to .iter() - .map(|r| Variable::String(r.address_lcase.as_str().into())) + .map(|r| Variable::from(r.address_lcase.as_str())) .collect::>() .into(), V_SENDER => self @@ -577,7 +601,7 @@ impl ResolveVariable for Session { V_LOCAL_IP => self.data.local_ip_str.as_str().into(), V_LOCAL_PORT => self.data.local_port.into(), V_TLS => self.stream.is_tls().into(), - V_PRIORITY => self.data.priority.to_string().into(), + V_PRIORITY => self.data.priority.to_compact_string().into(), V_PROTOCOL => self.instance.protocol.as_str().into(), V_ASN => self .data diff --git a/crates/smtp/src/inbound/spawn.rs b/crates/smtp/src/inbound/spawn.rs index 2ceca007..06a9db9b 100644 --- a/crates/smtp/src/inbound/spawn.rs +++ b/crates/smtp/src/inbound/spawn.rs @@ -11,6 +11,7 @@ use common::{ core::BuildServer, listener::{self, SessionManager, SessionStream}, }; +use compact_str::CompactString; use tokio_rustls::server::TlsStream; use trc::{SecurityEvent, SmtpEvent}; @@ -33,7 +34,7 @@ impl SessionManager for SmtpSessionManager { server.lookup_asn_country(session.remote_ip).await, session.session_id, ), - hostname: String::new(), + hostname: "".into(), server, instance: session.instance, state: State::default(), @@ -81,7 +82,7 @@ impl Session { // Sieve filtering if let Some((script, script_id)) = self .server - .eval_if::(&config.script, self, self.data.session_id) + .eval_if::(&config.script, self, self.data.session_id) .await .and_then(|name| { self.server @@ -117,7 +118,7 @@ impl Session { // Obtain hostname self.hostname = self .server - .eval_if::(&config.hostname, self, self.data.session_id) + .eval_if::(&config.hostname, self, self.data.session_id) .await .unwrap_or_default(); if self.hostname.is_empty() { @@ -125,13 +126,13 @@ impl Session { Smtp(SmtpEvent::MissingLocalHostname), SpanId = self.data.session_id, ); - self.hostname = "localhost".to_string(); + self.hostname = "localhost".into(); } // Obtain greeting let greeting = self .server - .eval_if::(&config.greeting, self, self.data.session_id) + .eval_if::(&config.greeting, self, self.data.session_id) .await .filter(|g| !g.is_empty()) .map(|g| format!("220 {}\r\n", g)) diff --git a/crates/smtp/src/inbound/vrfy.rs b/crates/smtp/src/inbound/vrfy.rs index a99823d0..6129a273 100644 --- a/crates/smtp/src/inbound/vrfy.rs +++ b/crates/smtp/src/inbound/vrfy.rs @@ -5,6 +5,7 @@ */ use common::listener::SessionStream; +use compact_str::CompactString; use trc::SmtpEvent; use crate::core::Session; @@ -14,7 +15,7 @@ impl Session { pub async fn handle_vrfy(&mut self, address: String) -> Result<(), ()> { match self .server - .eval_if::( + .eval_if::( &self.server.core.smtp.session.rcpt.directory, self, self.data.session_id, @@ -87,7 +88,7 @@ impl Session { pub async fn handle_expn(&mut self, address: String) -> Result<(), ()> { match self .server - .eval_if::( + .eval_if::( &self.server.core.smtp.session.rcpt.directory, self, self.data.session_id, diff --git a/crates/smtp/src/lib.rs b/crates/smtp/src/lib.rs index e98fa060..22efed2a 100644 --- a/crates/smtp/src/lib.rs +++ b/crates/smtp/src/lib.rs @@ -4,6 +4,8 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +#![warn(clippy::large_futures)] + use std::sync::Arc; use common::{ diff --git a/crates/smtp/src/outbound/client.rs b/crates/smtp/src/outbound/client.rs index af4dbad4..78245720 100644 --- a/crates/smtp/src/outbound/client.rs +++ b/crates/smtp/src/outbound/client.rs @@ -221,7 +221,7 @@ impl SmtpClient { CausedBy = trc::location!() ); Err(Status::TemporaryFailure(Error::Io( - "Queue system error.".to_string(), + "Queue system error.".into(), ))) } Err(err) => { @@ -232,7 +232,7 @@ impl SmtpClient { ); Err(Status::TemporaryFailure(Error::Io( - "Queue system error.".to_string(), + "Queue system error.".into(), ))) } } diff --git a/crates/smtp/src/outbound/dane/verify.rs b/crates/smtp/src/outbound/dane/verify.rs index e2d58ac2..de3fc885 100644 --- a/crates/smtp/src/outbound/dane/verify.rs +++ b/crates/smtp/src/outbound/dane/verify.rs @@ -39,8 +39,8 @@ impl TlsaVerify for Tlsa { ); return Err(Status::TemporaryFailure(Error::DaneError(ErrorDetails { - entity: hostname.to_string(), - details: "No certificates were provided by host".to_string(), + entity: hostname.into(), + details: "No certificates were provided by host".into(), }))); }; @@ -59,8 +59,8 @@ impl TlsaVerify for Tlsa { ); return Err(Status::TemporaryFailure(Error::DaneError(ErrorDetails { - entity: hostname.to_string(), - details: "Failed to parse X.509 certificate".to_string(), + entity: hostname.into(), + details: "Failed to parse X.509 certificate".into(), }))); } }; @@ -143,8 +143,8 @@ impl TlsaVerify for Tlsa { ); Err(Status::PermanentFailure(Error::DaneError(ErrorDetails { - entity: hostname.to_string(), - details: "No matching certificates found in TLSA records".to_string(), + entity: hostname.into(), + details: "No matching certificates found in TLSA records".into(), }))) } } diff --git a/crates/smtp/src/outbound/delivery.rs b/crates/smtp/src/outbound/delivery.rs index d5fd449b..4fddc5e9 100644 --- a/crates/smtp/src/outbound/delivery.rs +++ b/crates/smtp/src/outbound/delivery.rs @@ -20,6 +20,7 @@ use common::config::{ smtp::{queue::RequireOptional, report::AggregateFrequency}, }; use common::ipc::{PolicyType, QueueEvent, QueueEventStatus, TlsEvent}; +use compact_str::{CompactString, ToCompactString}; use mail_auth::{ mta_sts::TlsRpt, report::tlsrpt::{FailureDetails, ResultType}, @@ -44,6 +45,7 @@ use crate::queue::{Domain, Error, QueueEnvelope, QueuedMessage, Status}; impl QueuedMessage { pub fn try_deliver(self, server: Server) { + #![allow(clippy::large_futures)] tokio::spawn(async move { // Lock queue event let queue_id = self.queue_id; @@ -58,9 +60,9 @@ impl QueuedMessage { SpanId = message.span_id, QueueId = message.queue_id, From = if !message.return_path.is_empty() { - trc::Value::String(message.return_path.to_string()) + trc::Value::String(message.return_path.as_str().into()) } else { - trc::Value::Static("<>") + trc::Value::String("<>".into()) }, To = message .recipients @@ -70,7 +72,7 @@ impl QueuedMessage { r.status, Status::Scheduled | Status::TemporaryFailure(_) ) { - Some(trc::Value::String(r.address_lcase.to_string())) + Some(trc::Value::String(r.address_lcase.as_str().into())) } else { None } @@ -237,7 +239,7 @@ impl QueuedMessage { // Obtain next hop let (mut remote_hosts, is_smtp) = match server - .eval_if::(&queue_config.next_hop, &envelope, message.span_id) + .eval_if::(&queue_config.next_hop, &envelope, message.span_id) .await .and_then(|name| server.get_relay_host(&name, message.span_id)) { @@ -376,7 +378,7 @@ impl QueuedMessage { Details = mta_sts_policy .mx .iter() - .map(|mx| trc::Value::String(mx.to_string())) + .map(|mx| trc::Value::String(mx.to_compact_string())) .collect::>(), Elapsed = time.elapsed(), ); @@ -493,7 +495,7 @@ impl QueuedMessage { .smtp .resolvers .dns - .mx_lookup(&domain.domain, Some(&server.inner.cache.dns_mx)) + .mx_lookup(domain.domain.as_str(), Some(&server.inner.cache.dns_mx)) .await { Ok(mx) => mx, @@ -543,7 +545,7 @@ impl QueuedMessage { Domain = domain.domain.clone(), Details = remote_hosts_ .iter() - .map(|h| trc::Value::String(h.hostname().to_string())) + .map(|h| trc::Value::String(h.hostname().into())) .collect::>(), Elapsed = time.elapsed(), ); @@ -566,7 +568,7 @@ impl QueuedMessage { .unwrap_or_else(|| vec![Duration::from_secs(60)]); message.domains[domain_idx].set_status( Status::PermanentFailure(Error::DnsError( - "Domain does not accept messages (null MX)".to_string(), + "Domain does not accept messages (null MX)".into(), )), &schedule, ); @@ -610,16 +612,15 @@ impl QueuedMessage { Details = mta_sts_policy .mx .iter() - .map(|mx| trc::Value::String(mx.to_string())) + .map(|mx| trc::Value::String(mx.to_compact_string())) .collect::>(), Strict = strict, ); if strict { - last_status = Status::PermanentFailure(Error::MtaStsError(format!( - "MX {:?} not authorized by policy.", - envelope.mx - ))); + last_status = Status::PermanentFailure(Error::MtaStsError( + format!("MX {:?} not authorized by policy.", envelope.mx).into(), + )); continue 'next_host; } } else { @@ -631,7 +632,7 @@ impl QueuedMessage { Details = mta_sts_policy .mx .iter() - .map(|mx| trc::Value::String(mx.to_string())) + .map(|mx| trc::Value::String(mx.to_compact_string())) .collect::>(), Strict = strict, ); @@ -737,8 +738,8 @@ impl QueuedMessage { if strict { last_status = Status::PermanentFailure(Error::DaneError(ErrorDetails { - entity: envelope.mx.to_string(), - details: "No valid TLSA records were found".to_string(), + entity: envelope.mx.into(), + details: "No valid TLSA records were found".into(), })); continue 'next_host; } @@ -776,8 +777,8 @@ impl QueuedMessage { last_status = Status::PermanentFailure(Error::DaneError(ErrorDetails { - entity: envelope.mx.to_string(), - details: "No TLSA DNSSEC records found".to_string(), + entity: envelope.mx.into(), + details: "No TLSA DNSSEC records found".into(), })); continue 'next_host; } @@ -830,8 +831,8 @@ impl QueuedMessage { } Status::PermanentFailure(Error::DaneError(ErrorDetails { - entity: envelope.mx.to_string(), - details: "No TLSA records found".to_string(), + entity: envelope.mx.into(), + details: "No TLSA records found".into(), })) } else { err.into() @@ -929,7 +930,11 @@ impl QueuedMessage { // Obtain session parameters let local_hostname = server - .eval_if::(&queue_config.hostname, &envelope, message.span_id) + .eval_if::( + &queue_config.hostname, + &envelope, + message.span_id, + ) .await .filter(|s| !s.is_empty()) .unwrap_or_else(|| { @@ -937,7 +942,7 @@ impl QueuedMessage { Delivery(DeliveryEvent::MissingOutboundHostname), SpanId = message.span_id, ); - "local.host".to_string() + "local.host".into() }); let params = SessionParams { session_id: message.span_id, @@ -1387,9 +1392,8 @@ impl Message { } } - domain.status = Status::PermanentFailure(Error::Io( - "Queue rate limit exceeded.".to_string(), - )); + domain.status = + Status::PermanentFailure(Error::Io("Queue rate limit exceeded.".into())); } Status::Completed(_) | Status::PermanentFailure(_) => (), _ => { diff --git a/crates/smtp/src/outbound/local.rs b/crates/smtp/src/outbound/local.rs index 154d091a..11b51e26 100644 --- a/crates/smtp/src/outbound/local.rs +++ b/crates/smtp/src/outbound/local.rs @@ -58,11 +58,11 @@ impl Message { match result { LocalDeliveryStatus::Success => { rcpt.status = Status::Completed(HostResponse { - hostname: "localhost".to_string(), + hostname: "localhost".into(), response: Response { code: 250, esc: [2, 1, 5], - message: "OK".to_string(), + message: "OK".into(), }, }); total_completed += 1; @@ -70,13 +70,13 @@ impl Message { LocalDeliveryStatus::TemporaryFailure { reason } => { rcpt.status = Status::TemporaryFailure(HostResponse { hostname: ErrorDetails { - entity: "localhost".to_string(), - details: format!("RCPT TO:<{}>", rcpt.address), + entity: "localhost".into(), + details: format!("RCPT TO:<{}>", rcpt.address).into(), }, response: Response { code: 451, esc: [4, 3, 0], - message: reason.into_owned(), + message: reason.into(), }, }); } @@ -84,13 +84,13 @@ impl Message { total_completed += 1; rcpt.status = Status::PermanentFailure(HostResponse { hostname: ErrorDetails { - entity: "localhost".to_string(), - details: format!("RCPT TO:<{}>", rcpt.address), + entity: "localhost".into(), + details: format!("RCPT TO:<{}>", rcpt.address).into(), }, response: Response { code: 550, esc: code, - message: reason.into_owned(), + message: reason.into(), }, }); } diff --git a/crates/smtp/src/outbound/lookup.rs b/crates/smtp/src/outbound/lookup.rs index c18800c7..088f3023 100644 --- a/crates/smtp/src/outbound/lookup.rs +++ b/crates/smtp/src/outbound/lookup.rs @@ -132,14 +132,24 @@ impl DnsLookup for Server { .await .map_err(|err| { if let mail_auth::Error::DnsRecordNotFound(_) = &err { - Status::PermanentFailure(Error::ConnectionError(ErrorDetails { - entity: remote_host.hostname().to_string(), - details: "record not found for MX".to_string(), - })) + if matches!( + remote_host, + NextHop::MX { + is_implicit: true, + .. + } + ) { + Status::PermanentFailure(Error::DnsError("No MX record found.".into())) + } else { + Status::PermanentFailure(Error::ConnectionError(ErrorDetails { + entity: remote_host.hostname().into(), + details: "record not found for MX".into(), + })) + } } else { Status::TemporaryFailure(Error::ConnectionError(ErrorDetails { - entity: remote_host.hostname().to_string(), - details: format!("lookup error: {err}"), + entity: remote_host.hostname().into(), + details: format!("lookup error: {err}").into(), })) } })?; @@ -195,10 +205,13 @@ impl DnsLookup for Server { Ok(result) } else { - Err(Status::TemporaryFailure(Error::DnsError(format!( - "No IP addresses found for {:?}.", - envelope.resolve_variable(V_MX).to_string() - )))) + Err(Status::TemporaryFailure(Error::DnsError( + format!( + "No IP addresses found for {:?}.", + envelope.resolve_variable(V_MX).to_string() + ) + .into(), + ))) } } } @@ -226,7 +239,10 @@ impl ToNextHop for Vec { let mut slice = mx.exchanges.iter().collect::>(); slice.shuffle(&mut rand::rng()); for remote_host in slice { - remote_hosts.push(NextHop::MX(remote_host.as_str())); + remote_hosts.push(NextHop::MX { + host: remote_host.as_str(), + is_implicit: false, + }); if remote_hosts.len() == max_mx { break 'outer; } @@ -236,7 +252,10 @@ impl ToNextHop for Vec { if mx.preference == 0 && remote_host == "." { return None; } - remote_hosts.push(NextHop::MX(remote_host.as_str())); + remote_hosts.push(NextHop::MX { + host: remote_host.as_str(), + is_implicit: false, + }); if remote_hosts.len() == max_mx { break; } @@ -246,7 +265,11 @@ impl ToNextHop for Vec { } else { // If an empty list of MXs is returned, the address is treated as if it was // associated with an implicit MX RR with a preference of 0, pointing to that host. - vec![NextHop::MX(domain)].into() + vec![NextHop::MX { + host: domain, + is_implicit: true, + }] + .into() } } } diff --git a/crates/smtp/src/outbound/mod.rs b/crates/smtp/src/outbound/mod.rs index fdaad1ee..f4335bb1 100644 --- a/crates/smtp/src/outbound/mod.rs +++ b/crates/smtp/src/outbound/mod.rs @@ -10,6 +10,7 @@ use common::config::{ server::ServerProtocol, smtp::queue::{RelayHost, RequireOptional}, }; +use compact_str::ToCompactString; use mail_send::Credentials; use smtp_proto::{Response, Severity}; @@ -43,15 +44,15 @@ impl Status<(), Error> { | mail_send::Error::MissingRcptTo | mail_send::Error::Timeout => { Status::TemporaryFailure(Error::ConnectionError(ErrorDetails { - entity: hostname.to_string(), - details: err.to_string(), + entity: hostname.into(), + details: err.to_compact_string(), })) } mail_send::Error::UnexpectedReply(reply) => { let details = ErrorDetails { - entity: hostname.to_string(), - details: command.trim().to_string(), + entity: hostname.into(), + details: command.trim().into(), }; if reply.severity() == Severity::PermanentNegativeCompletion { Status::PermanentFailure(Error::UnexpectedResponse(HostResponse { @@ -71,19 +72,19 @@ impl Status<(), Error> { | mail_send::Error::InvalidTLSName | mail_send::Error::MissingStartTls => { Status::PermanentFailure(Error::ConnectionError(ErrorDetails { - entity: hostname.to_string(), - details: err.to_string(), + entity: hostname.into(), + details: err.to_compact_string(), })) } } } pub fn from_starttls_error(hostname: &str, response: Option>) -> Self { - let entity = hostname.to_string(); + let entity = hostname.into(); if let Some(response) = response { let hostname = ErrorDetails { entity, - details: "STARTTLS".to_string(), + details: "STARTTLS".into(), }; if response.severity() == Severity::PermanentNegativeCompletion { @@ -100,7 +101,7 @@ impl Status<(), Error> { } else { Status::PermanentFailure(Error::TlsError(ErrorDetails { entity, - details: "STARTTLS not advertised by host.".to_string(), + details: "STARTTLS not advertised by host.".into(), })) } } @@ -109,40 +110,40 @@ impl Status<(), Error> { match err { mail_send::Error::InvalidTLSName => { Status::PermanentFailure(Error::TlsError(ErrorDetails { - entity: hostname.to_string(), - details: "Invalid hostname".to_string(), + entity: hostname.into(), + details: "Invalid hostname".into(), })) } mail_send::Error::Timeout => Status::TemporaryFailure(Error::TlsError(ErrorDetails { - entity: hostname.to_string(), - details: "TLS handshake timed out".to_string(), + entity: hostname.into(), + details: "TLS handshake timed out".into(), })), mail_send::Error::Tls(err) => Status::TemporaryFailure(Error::TlsError(ErrorDetails { - entity: hostname.to_string(), - details: format!("Handshake failed: {err}"), + entity: hostname.into(), + details: format!("Handshake failed: {err}").into(), })), mail_send::Error::Io(err) => Status::TemporaryFailure(Error::TlsError(ErrorDetails { - entity: hostname.to_string(), - details: format!("I/O error: {err}"), + entity: hostname.into(), + details: format!("I/O error: {err}").into(), })), _ => Status::PermanentFailure(Error::TlsError(ErrorDetails { - entity: hostname.to_string(), - details: "Other TLS error".to_string(), + entity: hostname.into(), + details: "Other TLS error".into(), })), } } pub fn timeout(hostname: &str, stage: &str) -> Self { Status::TemporaryFailure(Error::ConnectionError(ErrorDetails { - entity: hostname.to_string(), - details: format!("Timeout while {stage}"), + entity: hostname.into(), + details: format!("Timeout while {stage}").into(), })) } pub fn local_error() -> Self { Status::TemporaryFailure(Error::ConnectionError(ErrorDetails { - entity: "localhost".to_string(), - details: "Could not deliver message locally.".to_string(), + entity: "localhost".into(), + details: "Could not deliver message locally.".into(), })) } } @@ -150,10 +151,10 @@ impl Status<(), Error> { impl From for Status<(), Error> { fn from(err: mail_auth::Error) -> Self { match &err { - mail_auth::Error::DnsRecordNotFound(code) => { - Status::PermanentFailure(Error::DnsError(format!("Domain not found: {code:?}"))) - } - _ => Status::TemporaryFailure(Error::DnsError(err.to_string())), + mail_auth::Error::DnsRecordNotFound(code) => Status::PermanentFailure(Error::DnsError( + format!("Domain not found: {code:?}").into(), + )), + _ => Status::TemporaryFailure(Error::DnsError(err.to_compact_string())), } } } @@ -163,38 +164,34 @@ impl From for Status<(), Error> { match &err { mta_sts::Error::Dns(err) => match err { mail_auth::Error::DnsRecordNotFound(code) => Status::PermanentFailure( - Error::MtaStsError(format!("Record not found: {code:?}")), + Error::MtaStsError(format!("Record not found: {code:?}").into()), ), mail_auth::Error::InvalidRecordType => Status::PermanentFailure( - Error::MtaStsError("Failed to parse MTA-STS DNS record.".to_string()), + Error::MtaStsError("Failed to parse MTA-STS DNS record.".into()), ), - _ => { - Status::TemporaryFailure(Error::MtaStsError(format!("DNS lookup error: {err}"))) - } + _ => Status::TemporaryFailure(Error::MtaStsError( + format!("DNS lookup error: {err}").into(), + )), }, mta_sts::Error::Http(err) => { if err.is_timeout() { - Status::TemporaryFailure(Error::MtaStsError( - "Timeout fetching policy.".to_string(), - )) + Status::TemporaryFailure(Error::MtaStsError("Timeout fetching policy.".into())) } else if err.is_connect() { Status::TemporaryFailure(Error::MtaStsError( - "Could not reach policy host.".to_string(), + "Could not reach policy host.".into(), )) } else if err.is_status() & err .status() .is_some_and(|s| s == reqwest::StatusCode::NOT_FOUND) { - Status::PermanentFailure(Error::MtaStsError("Policy not found.".to_string())) + Status::PermanentFailure(Error::MtaStsError("Policy not found.".into())) } else { - Status::TemporaryFailure(Error::MtaStsError( - "Failed to fetch policy.".to_string(), - )) + Status::TemporaryFailure(Error::MtaStsError("Failed to fetch policy.".into())) } } mta_sts::Error::InvalidPolicy(err) => Status::PermanentFailure(Error::MtaStsError( - format!("Failed to parse policy: {err}"), + format!("Failed to parse policy: {err}").into(), )), } } @@ -203,14 +200,14 @@ impl From for Status<(), Error> { #[derive(Debug)] pub enum NextHop<'x> { Relay(&'x RelayHost), - MX(&'x str), + MX { is_implicit: bool, host: &'x str }, } impl NextHop<'_> { #[inline(always)] pub fn hostname(&self) -> &str { match self { - NextHop::MX(host) => { + NextHop::MX { host, .. } => { if let Some(host) = host.strip_suffix('.') { host } else { @@ -224,7 +221,7 @@ impl NextHop<'_> { #[inline(always)] pub fn fqdn_hostname(&self) -> Cow<'_, str> { match self { - NextHop::MX(host) => { + NextHop::MX { host, .. } => { if !host.ends_with('.') { format!("{host}.").into() } else { @@ -239,9 +236,9 @@ impl NextHop<'_> { fn port(&self) -> u16 { match self { #[cfg(feature = "test_mode")] - NextHop::MX(_) => 9925, + NextHop::MX { .. } => 9925, #[cfg(not(feature = "test_mode"))] - NextHop::MX(_) => 25, + NextHop::MX { .. } => 25, NextHop::Relay(host) => host.port, } } @@ -249,7 +246,7 @@ impl NextHop<'_> { #[inline(always)] fn credentials(&self) -> Option<&Credentials> { match self { - NextHop::MX(_) => None, + NextHop::MX { .. } => None, NextHop::Relay(host) => host.auth.as_ref(), } } @@ -262,7 +259,7 @@ impl NextHop<'_> { } #[cfg(not(feature = "test_mode"))] match self { - NextHop::MX(_) => false, + NextHop::MX { .. } => false, NextHop::Relay(host) => host.tls_allow_invalid_certs, } } @@ -270,7 +267,7 @@ impl NextHop<'_> { #[inline(always)] fn implicit_tls(&self) -> bool { match self { - NextHop::MX(_) => false, + NextHop::MX { .. } => false, NextHop::Relay(host) => host.tls_implicit, } } @@ -278,7 +275,7 @@ impl NextHop<'_> { #[inline(always)] fn is_smtp(&self) -> bool { match self { - NextHop::MX(_) => true, + NextHop::MX { .. } => true, NextHop::Relay(host) => host.protocol == ServerProtocol::Smtp, } } diff --git a/crates/smtp/src/outbound/session.rs b/crates/smtp/src/outbound/session.rs index d8b3a02c..5c34aa31 100644 --- a/crates/smtp/src/outbound/session.rs +++ b/crates/smtp/src/outbound/session.rs @@ -181,7 +181,7 @@ impl Message { accepted_rcpts.push(( rcpt, Status::Completed(HostResponse { - hostname: params.hostname.to_string(), + hostname: params.hostname.into(), response, }), )); @@ -199,8 +199,8 @@ impl Message { let response = HostResponse { hostname: ErrorDetails { - entity: params.hostname.to_string(), - details: cmd.trim().to_string(), + entity: params.hostname.into(), + details: cmd.trim().into(), }, response, }; @@ -345,11 +345,8 @@ impl Message { let response = HostResponse { hostname: ErrorDetails { - entity: params.hostname.to_string(), - details: bdat_cmd - .as_deref() - .unwrap_or("DATA") - .to_string(), + entity: params.hostname.into(), + details: bdat_cmd.as_deref().unwrap_or("DATA").into(), }, response, }; diff --git a/crates/smtp/src/queue/dsn.rs b/crates/smtp/src/queue/dsn.rs index 5d8446c1..8c156244 100644 --- a/crates/smtp/src/queue/dsn.rs +++ b/crates/smtp/src/queue/dsn.rs @@ -5,6 +5,7 @@ */ use common::Server; +use compact_str::CompactString; use mail_builder::MessageBuilder; use mail_builder::headers::HeaderType; use mail_builder::headers::content_type::ContentType; @@ -43,9 +44,9 @@ impl SendDsn for Server { let mut dsn_message = self.new_message("", "", "", message.span_id); dsn_message .add_recipient_parts( - &message.return_path, - &message.return_path_lcase, - &message.return_path_domain, + message.return_path.as_str(), + message.return_path_lcase.as_str(), + message.return_path_domain.as_str(), self, ) .await; @@ -347,15 +348,15 @@ impl Message { let from_name = server .eval_if(&config.dsn.name, self, self.span_id) .await - .unwrap_or_else(|| String::from("Mail Delivery Subsystem")); + .unwrap_or_else(|| CompactString::from("Mail Delivery Subsystem")); let from_addr = server .eval_if(&config.dsn.address, self, self.span_id) .await - .unwrap_or_else(|| String::from("MAILER-DAEMON@localhost")); + .unwrap_or_else(|| CompactString::from("MAILER-DAEMON@localhost")); let reporting_mta = server .eval_if(&server.core.smtp.report.submitter, self, self.span_id) .await - .unwrap_or_else(|| String::from("localhost")); + .unwrap_or_else(|| CompactString::from("localhost")); // Prepare DSN let mut dsn_header = String::with_capacity(dsn.len() + 128); @@ -656,22 +657,27 @@ impl Status, HostResponse> { fn write_dsn_remote_mta(&self, dsn: &mut String) { dsn.push_str("Remote-MTA: dns;"); - if let Status::Completed(HostResponse { hostname, .. }) - | Status::PermanentFailure(HostResponse { - hostname: ErrorDetails { - entity: hostname, .. - }, - .. - }) - | Status::TemporaryFailure(HostResponse { - hostname: ErrorDetails { - entity: hostname, .. - }, - .. - }) = self - { - dsn.push_str(hostname); + match self { + Status::Completed(HostResponse { hostname, .. }) => { + dsn.push_str(hostname); + } + Status::PermanentFailure(HostResponse { + hostname: ErrorDetails { + entity: hostname, .. + }, + .. + }) + | Status::TemporaryFailure(HostResponse { + hostname: ErrorDetails { + entity: hostname, .. + }, + .. + }) => { + dsn.push_str(hostname); + } + _ => (), } + dsn.push_str("\r\n"); } diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs index 1943e6b6..34f5316c 100644 --- a/crates/smtp/src/queue/mod.rs +++ b/crates/smtp/src/queue/mod.rs @@ -11,8 +11,9 @@ use std::{ }; use common::expr::{self, functions::ResolveVariable, *}; +use compact_str::{CompactString, ToCompactString}; use smtp_proto::{ArchivedResponse, Response}; -use store::{SERIALIZE_OBJ_13_V1, SerializedVersion, write::now}; +use store::{SERIALIZE_QUEUEMSG_V1, SerializedVersion, write::now}; use utils::BlobHash; pub mod dsn; @@ -50,14 +51,14 @@ pub struct Message { pub created: u64, pub blob_hash: BlobHash, - pub return_path: String, - pub return_path_lcase: String, - pub return_path_domain: String, + pub return_path: CompactString, + pub return_path_lcase: CompactString, + pub return_path_domain: CompactString, pub recipients: Vec, pub domains: Vec, pub flags: u64, - pub env_id: Option, + pub env_id: Option, pub priority: i16, pub size: u64, @@ -69,7 +70,7 @@ pub struct Message { impl SerializedVersion for Message { fn serialize_version() -> u8 { - SERIALIZE_OBJ_13_V1 + SERIALIZE_QUEUEMSG_V1 } } @@ -81,7 +82,7 @@ pub enum QuotaKey { #[derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, PartialEq, Eq)] pub struct Domain { - pub domain: String, + pub domain: CompactString, pub retry: Schedule, pub notify: Schedule, pub expires: u64, @@ -91,11 +92,11 @@ pub struct Domain { #[derive(rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Debug, Clone, PartialEq, Eq)] pub struct Recipient { pub domain_idx: u32, - pub address: String, - pub address_lcase: String, + pub address: CompactString, + pub address_lcase: CompactString, pub status: Status, HostResponse>, pub flags: u64, - pub orcpt: Option, + pub orcpt: Option, } pub const RCPT_DSN_SENT: u64 = 1 << 32; @@ -131,23 +132,23 @@ pub struct HostResponse { #[derive(Debug, Clone, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive)] pub enum Error { - DnsError(String), + DnsError(CompactString), UnexpectedResponse(HostResponse), ConnectionError(ErrorDetails), TlsError(ErrorDetails), DaneError(ErrorDetails), - MtaStsError(String), + MtaStsError(CompactString), RateLimited, ConcurrencyLimited, - Io(String), + Io(CompactString), } #[derive( Debug, Clone, PartialEq, Eq, rkyv::Serialize, rkyv::Deserialize, rkyv::Archive, Default, )] pub struct ErrorDetails { - pub entity: String, - pub details: String, + pub entity: CompactString, + pub details: CompactString, } impl Ord for Schedule { @@ -266,7 +267,7 @@ impl<'x> ResolveVariable for QueueEnvelope<'x> { .into(), V_QUEUE_LAST_STATUS => self .current_domain() - .map(|d| d.status.to_string()) + .map(|d| d.status.to_compact_string()) .unwrap_or_default() .into(), V_QUEUE_LAST_ERROR => self @@ -289,8 +290,8 @@ impl<'x> ResolveVariable for QueueEnvelope<'x> { .into(), V_MX => self.mx.into(), V_PRIORITY => self.message.priority.into(), - V_REMOTE_IP => self.remote_ip.to_string().into(), - V_LOCAL_IP => self.local_ip.to_string().into(), + V_REMOTE_IP => self.remote_ip.to_compact_string().into(), + V_LOCAL_IP => self.local_ip.to_compact_string().into(), _ => "".into(), } } @@ -372,17 +373,13 @@ pub trait DomainPart { fn domain_part(&self) -> &str; } -impl DomainPart for &str { +impl> DomainPart for T { #[inline(always)] fn domain_part(&self) -> &str { - self.rsplit_once('@').map(|(_, d)| d).unwrap_or_default() - } -} - -impl DomainPart for String { - #[inline(always)] - fn domain_part(&self) -> &str { - self.rsplit_once('@').map(|(_, d)| d).unwrap_or_default() + self.as_ref() + .rsplit_once('@') + .map(|(_, d)| d) + .unwrap_or_default() } } @@ -498,7 +495,7 @@ impl Display for Status<(), Error> { } } -impl Display for Status, HostResponse> { +impl Display for Status, HostResponse> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Status::Scheduled => write!(f, "Scheduled"), diff --git a/crates/smtp/src/queue/spool.rs b/crates/smtp/src/queue/spool.rs index 7b894b60..61c19511 100644 --- a/crates/smtp/src/queue/spool.rs +++ b/crates/smtp/src/queue/spool.rs @@ -7,6 +7,7 @@ use crate::queue::DomainPart; use common::ipc::QueueEvent; use common::{KV_LOCK_QUEUE_MESSAGE, Server}; +use compact_str::CompactString; use std::borrow::Cow; use std::future::Future; use std::time::{Duration, SystemTime}; @@ -29,9 +30,9 @@ pub const QUEUE_REFRESH: u64 = 300; pub trait SmtpSpool: Sync + Send { fn new_message( &self, - return_path: impl Into, - return_path_lcase: impl Into, - return_path_domain: impl Into, + return_path: impl Into, + return_path_lcase: impl Into, + return_path_domain: impl Into, span_id: u64, ) -> Message; @@ -52,9 +53,9 @@ pub trait SmtpSpool: Sync + Send { impl SmtpSpool for Server { fn new_message( &self, - return_path: impl Into, - return_path_lcase: impl Into, - return_path_domain: impl Into, + return_path: impl Into, + return_path_lcase: impl Into, + return_path_domain: impl Into, span_id: u64, ) -> Message { let created = SystemTime::now() @@ -254,9 +255,9 @@ impl Message { SpanId = session_id, QueueId = self.queue_id, From = if !self.return_path.is_empty() { - trc::Value::String(self.return_path.to_string()) + trc::Value::String(self.return_path.as_str().into()) } else { - trc::Value::Static("<>") + trc::Value::String("<>".into()) }, To = self .recipients @@ -358,9 +359,9 @@ impl Message { pub async fn add_recipient_parts( &mut self, - rcpt: impl Into, - rcpt_lcase: impl Into, - rcpt_domain: impl Into, + rcpt: impl Into, + rcpt_lcase: impl Into, + rcpt_domain: impl Into, server: &Server, ) { let rcpt_domain = rcpt_domain.into(); @@ -404,7 +405,7 @@ impl Message { }); } - pub async fn add_recipient(&mut self, rcpt: impl Into, server: &Server) { + pub async fn add_recipient(&mut self, rcpt: impl Into, server: &Server) { let rcpt = rcpt.into(); let rcpt_lcase = rcpt.to_lowercase(); let rcpt_domain = rcpt_lcase.domain_part().to_string(); @@ -513,24 +514,24 @@ impl Message { } } - pub fn has_domain(&self, domains: &[String]) -> bool { + pub fn has_domain(&self, domains: &[CompactString]) -> bool { self.domains.iter().any(|d| domains.contains(&d.domain)) || self .return_path .rsplit_once('@') - .is_some_and(|(_, domain)| domains.contains(&domain.to_string())) + .is_some_and(|(_, domain)| domains.iter().any(|dd| dd == domain)) } } impl ArchivedMessage { - pub fn has_domain(&self, domains: &[String]) -> bool { + pub fn has_domain(&self, domains: &[CompactString]) -> bool { self.domains .iter() .any(|d| domains.iter().any(|dd| dd == d.domain.as_str())) || self .return_path .rsplit_once('@') - .is_some_and(|(_, domain)| domains.contains(&domain.to_string())) + .is_some_and(|(_, domain)| domains.iter().any(|dd| dd == domain)) } pub fn next_delivery_event(&self) -> u64 { diff --git a/crates/smtp/src/reporting/analysis.rs b/crates/smtp/src/reporting/analysis.rs index 780121c5..a8f1206e 100644 --- a/crates/smtp/src/reporting/analysis.rs +++ b/crates/smtp/src/reporting/analysis.rs @@ -12,6 +12,7 @@ use std::{ use ahash::AHashMap; use common::Server; +use compact_str::CompactString; use mail_auth::{ flate2::read::GzDecoder, report::{ActionDisposition, DmarcResult, Feedback, Report, tlsrpt::TlsReport}, @@ -45,9 +46,9 @@ struct ReportData<'x> { #[derive(serde::Serialize, serde::Deserialize)] pub struct IncomingReport { - pub from: String, - pub to: Vec, - pub subject: String, + pub from: CompactString, + pub to: Vec, + pub subject: CompactString, pub report: T, } @@ -59,19 +60,19 @@ impl AnalyzeReport for Server { fn analyze_report(&self, message: Message<'static>, session_id: u64) { let core = self.clone(); tokio::spawn(async move { - let from = message + let from: CompactString = message .from() .and_then(|a| a.last()) .and_then(|a| a.address()) .unwrap_or_default() - .to_string(); - let to = message.to().map_or_else(Vec::new, |a| { + .into(); + let to: Vec = message.to().map_or_else(Vec::new, |a| { a.iter() .filter_map(|a| a.address()) - .map(|a| a.to_string()) + .map(|a| a.into()) .collect() }); - let subject = message.subject().unwrap_or_default().to_string(); + let subject: CompactString = message.subject().unwrap_or_default().into(); let mut reports = Vec::new(); for part in &message.parts { @@ -474,15 +475,13 @@ impl LogReport for Feedback<'_> { Domain = self .reported_domain() .iter() - .map(|d| trc::Value::String(d.to_string())) + .map(|d| trc::Value::String(d.as_ref().into())) .collect::>(), - Hostname = self - .reporting_mta() - .map(|d| trc::Value::String(d.to_string())), + Hostname = self.reporting_mta().map(|d| trc::Value::String(d.into())), Url = self .reported_uri() .iter() - .map(|d| trc::Value::String(d.to_string())) + .map(|d| trc::Value::String(d.as_ref().into())) .collect::>(), RemoteIp = self.source_ip(), Total = self.incidents(), @@ -490,17 +489,17 @@ impl LogReport for Feedback<'_> { Details = self .authentication_results() .iter() - .map(|d| trc::Value::String(d.to_string())) + .map(|d| trc::Value::String(d.as_ref().into())) .collect::>(), ); } } impl IncomingReport { - pub fn has_domain(&self, domain: &[String]) -> bool { + pub fn has_domain(&self, domain: &[CompactString]) -> bool { self.to .iter() - .any(|to| domain.iter().any(|d| to.ends_with(d))) - || domain.iter().any(|d| self.from.ends_with(d)) + .any(|to| domain.iter().any(|d| to.ends_with(d.as_str()))) + || domain.iter().any(|d| self.from.ends_with(d.as_str())) } } diff --git a/crates/smtp/src/reporting/dkim.rs b/crates/smtp/src/reporting/dkim.rs index 4aa2489c..685b0e7c 100644 --- a/crates/smtp/src/reporting/dkim.rs +++ b/crates/smtp/src/reporting/dkim.rs @@ -5,6 +5,7 @@ */ use common::listener::SessionStream; +use compact_str::ToCompactString; use mail_auth::{ AuthenticatedMessage, AuthenticationResults, DkimOutput, common::verify::VerifySignature, }; @@ -49,7 +50,7 @@ impl Session { .server .eval_if(&config.address, self, self.data.session_id) .await - .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string()); let mut report = Vec::with_capacity(128); self.new_auth_failure(output.result().into(), rejected) .with_authentication_results( @@ -66,7 +67,7 @@ impl Session { self.server .eval_if(&config.name, self, self.data.session_id) .await - .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) + .unwrap_or_else(|| "Mail Delivery Subsystem".to_compact_string()) .as_str(), from_addr.as_str(), ), @@ -75,7 +76,7 @@ impl Session { .server .eval_if(&config.subject, self, self.data.session_id) .await - .unwrap_or_else(|| "DKIM Report".to_string()), + .unwrap_or_else(|| "DKIM Report".to_compact_string()), &mut report, ) .ok(); diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs index 7807b86a..fe8df435 100644 --- a/crates/smtp/src/reporting/dmarc.rs +++ b/crates/smtp/src/reporting/dmarc.rs @@ -13,6 +13,7 @@ use common::{ ipc::{DmarcEvent, ToHash}, listener::SessionStream, }; +use compact_str::{CompactString, ToCompactString}; use mail_auth::{ ArcOutput, AuthenticatedMessage, AuthenticationResults, DkimOutput, DkimResult, DmarcOutput, SpfResult, @@ -96,7 +97,7 @@ impl Session { Url = dmarc_record .ruf() .iter() - .map(|u| trc::Value::String(u.uri().to_string())) + .map(|u| trc::Value::String(u.uri().to_compact_string())) .collect::>(), ); } @@ -110,7 +111,7 @@ impl Session { Url = dmarc_record .ruf() .iter() - .map(|u| trc::Value::String(u.uri().to_string())) + .map(|u| trc::Value::String(u.uri().to_compact_string())) .collect::>(), ); @@ -125,7 +126,7 @@ impl Session { .server .eval_if(&config.address, self, self.data.session_id) .await - .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string()); let mut auth_failure = self .new_auth_failure(AuthFailureType::Dmarc, rejected) .with_authentication_results(auth_results.to_string()) @@ -208,7 +209,7 @@ impl Session { self.server .eval_if(&config.name, self, self.data.session_id) .await - .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) + .unwrap_or_else(|| "Mail Delivery Subsystem".to_compact_string()) .as_str(), from_addr.as_str(), ), @@ -217,7 +218,7 @@ impl Session { .server .eval_if(&config.subject, self, self.data.session_id) .await - .unwrap_or_else(|| "DMARC Report".to_string()), + .unwrap_or_else(|| "DMARC Report".to_compact_string()), &mut report, ) .ok(); @@ -228,7 +229,7 @@ impl Session { From = from_addr.to_string(), To = rcpts .iter() - .map(|a| trc::Value::String(a.to_string())) + .map(|a| trc::Value::String(a.to_compact_string())) .collect::>(), ); @@ -383,7 +384,7 @@ impl DmarcReporting for Server { SpanId = span_id, Url = rua .iter() - .map(|u| trc::Value::String(u.uri().to_string())) + .map(|u| trc::Value::String(u.uri().to_compact_string())) .collect::>(), ); @@ -397,7 +398,7 @@ impl DmarcReporting for Server { SpanId = span_id, Url = rua .iter() - .map(|u| trc::Value::String(u.uri().to_string())) + .map(|u| trc::Value::String(u.uri().to_compact_string())) .collect::>(), ); @@ -415,7 +416,7 @@ impl DmarcReporting for Server { span_id, ) .await - .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string()); let mut message = Vec::with_capacity(2048); let _ = report.write_rfc5322( &self @@ -425,7 +426,7 @@ impl DmarcReporting for Server { span_id, ) .await - .unwrap_or_else(|| "localhost".to_string()), + .unwrap_or_else(|| "localhost".to_compact_string()), ( self.eval_if( &config.name, @@ -433,7 +434,7 @@ impl DmarcReporting for Server { span_id, ) .await - .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) + .unwrap_or_else(|| "Mail Delivery Subsystem".to_compact_string()) .as_str(), from_addr.as_str(), ), @@ -493,10 +494,10 @@ impl DmarcReporting for Server { span_id, ) .await - .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()), + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string()), ); if let Some(org_name) = self - .eval_if::( + .eval_if::( &config.org_name, &RecipientDomain::new(event.domain.as_str()), span_id, @@ -506,7 +507,7 @@ impl DmarcReporting for Server { report = report.with_org_name(org_name); } if let Some(contact_info) = self - .eval_if::( + .eval_if::( &config.contact_info, &RecipientDomain::new(event.domain.as_str()), span_id, diff --git a/crates/smtp/src/reporting/mod.rs b/crates/smtp/src/reporting/mod.rs index ddaa79eb..aee6a2cf 100644 --- a/crates/smtp/src/reporting/mod.rs +++ b/crates/smtp/src/reporting/mod.rs @@ -12,6 +12,7 @@ use common::{ expr::if_block::IfBlock, ipc::ReportingEvent, }; +use compact_str::CompactString; use mail_auth::{ common::headers::HeaderWriter, report::{AuthFailureType, DeliveryResult, Feedback, FeedbackType}, @@ -86,8 +87,8 @@ pub trait SmtpReporting: Sync + Send { fn send_autogenerated( &self, - from_addr: impl Into + Sync + Send, - rcpts: impl Iterator + Sync + Send> + Sync + Send, + from_addr: impl Into + Sync + Send, + rcpts: impl Iterator + Sync + Send> + Sync + Send, raw_message: Vec, sign_config: Option<&IfBlock>, parent_session_id: u64, @@ -161,8 +162,8 @@ impl SmtpReporting for Server { async fn send_autogenerated( &self, - from_addr: impl Into + Sync + Send, - rcpts: impl Iterator + Sync + Send> + Sync + Send, + from_addr: impl Into + Sync + Send, + rcpts: impl Iterator + Sync + Send> + Sync + Send, raw_message: Vec, sign_config: Option<&IfBlock>, parent_session_id: u64, @@ -218,7 +219,7 @@ impl SmtpReporting for Server { bytes: &[u8], ) -> Option> { let signers = self - .eval_if::, _>(config, message, message.span_id) + .eval_if::, _>(config, message, message.span_id) .await .unwrap_or_default(); if !signers.is_empty() { diff --git a/crates/smtp/src/reporting/spf.rs b/crates/smtp/src/reporting/spf.rs index c152586d..c37794bd 100644 --- a/crates/smtp/src/reporting/spf.rs +++ b/crates/smtp/src/reporting/spf.rs @@ -5,6 +5,7 @@ */ use common::listener::SessionStream; +use compact_str::ToCompactString; use mail_auth::{AuthenticationResults, SpfOutput, report::AuthFailureType}; use trc::OutgoingReportEvent; use utils::config::Rate; @@ -40,7 +41,7 @@ impl Session { .server .eval_if(&config.address, self, self.data.session_id) .await - .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string()); let mut report = Vec::with_capacity(128); self.new_auth_failure(AuthFailureType::Spf, rejected) .with_authentication_results( @@ -66,7 +67,7 @@ impl Session { self.server .eval_if(&config.name, self, self.data.session_id) .await - .unwrap_or_else(|| "Mailer Daemon".to_string()) + .unwrap_or_else(|| "Mailer Daemon".to_compact_string()) .as_str(), from_addr.as_str(), ), @@ -75,7 +76,7 @@ impl Session { .server .eval_if(&config.subject, self, self.data.session_id) .await - .unwrap_or_else(|| "SPF Report".to_string()), + .unwrap_or_else(|| "SPF Report".to_compact_string()), &mut report, ) .ok(); diff --git a/crates/smtp/src/reporting/tls.rs b/crates/smtp/src/reporting/tls.rs index bedc8962..3d89f26e 100644 --- a/crates/smtp/src/reporting/tls.rs +++ b/crates/smtp/src/reporting/tls.rs @@ -15,6 +15,7 @@ use common::{ }, ipc::{TlsEvent, ToHash}, }; +use compact_str::{CompactString, ToCompactString}; use mail_auth::{ flate2::{Compression, write::GzEncoder}, mta_sts::{ReportUri, TlsRpt}, @@ -210,7 +211,7 @@ impl TlsReporting for Server { let from_addr = self .eval_if(&config.address, &RecipientDomain::new(domain_name), span_id) .await - .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_string()); + .unwrap_or_else(|| "MAILER-DAEMON@localhost".to_compact_string()); let mut message = Vec::with_capacity(2048); let _ = report.write_rfc5322_from_bytes( domain_name, @@ -221,11 +222,11 @@ impl TlsReporting for Server { span_id, ) .await - .unwrap_or_else(|| "localhost".to_string()), + .unwrap_or_else(|| "localhost".to_compact_string()), ( self.eval_if(&config.name, &RecipientDomain::new(domain_name), span_id) .await - .unwrap_or_else(|| "Mail Delivery Subsystem".to_string()) + .unwrap_or_else(|| "Mail Delivery Subsystem".to_compact_string()) .as_str(), from_addr.as_str(), ), @@ -267,24 +268,26 @@ impl TlsReporting for Server { let config = &self.core.smtp.report.tls; let mut report = TlsReport { organization_name: self - .eval_if( + .eval_if::( &config.org_name, &RecipientDomain::new(domain_name), span_id, ) .await + .map(Into::into) .clone(), date_range: DateRange { start_datetime: DateTime::from_timestamp(event_from as i64), end_datetime: DateTime::from_timestamp(event_to as i64), }, contact_info: self - .eval_if( + .eval_if::( &config.contact_info, &RecipientDomain::new(domain_name), span_id, ) .await + .map(Into::into) .clone(), report_id: format!("{}_{}", event_from, policy), policies: Vec::with_capacity(events.len()), diff --git a/crates/smtp/src/scripts/envelope.rs b/crates/smtp/src/scripts/envelope.rs index 5cf1177b..1d4c106f 100644 --- a/crates/smtp/src/scripts/envelope.rs +++ b/crates/smtp/src/scripts/envelope.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use sieve::Envelope; use smtp_proto::{ MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_BY_TRACE, MAIL_RET_FULL, MAIL_RET_HDRS, RCPT_NOTIFY_DELAY, @@ -16,15 +17,19 @@ use crate::{ }; impl SessionData { - pub fn apply_envelope_modification(&mut self, envelope: Envelope, value: String) { + pub fn apply_envelope_modification(&mut self, envelope: Envelope, value: CompactString) { match envelope { Envelope::From => { let (address, address_lcase, domain) = if value.contains('@') { - let address_lcase = value.to_lowercase(); - let domain = address_lcase.domain_part().to_string(); + let address_lcase = CompactString::from_str_to_lowercase(&value); + let domain = address_lcase.domain_part().into(); (value, address_lcase, domain) } else if value.is_empty() { - (String::new(), String::new(), String::new()) + ( + CompactString::new(""), + CompactString::new(""), + CompactString::new(""), + ) } else { return; }; @@ -46,7 +51,7 @@ impl SessionData { Envelope::To => { if value.contains('@') { let address_lcase = value.to_lowercase(); - let domain = address_lcase.domain_part().to_string(); + let domain = address_lcase.domain_part().into(); if let Some(rcpt_to) = self.rcpt_to.last_mut() { rcpt_to.address = value; rcpt_to.address_lcase = address_lcase; diff --git a/crates/smtp/src/scripts/event_loop.rs b/crates/smtp/src/scripts/event_loop.rs index b14e6aa6..060a1330 100644 --- a/crates/smtp/src/scripts/event_loop.rs +++ b/crates/smtp/src/scripts/event_loop.rs @@ -7,6 +7,7 @@ use std::{borrow::Cow, future::Future, sync::Arc, time::Instant}; use common::{Server, scripts::plugins::PluginContext}; +use compact_str::CompactString; use mail_auth::common::headers::HeaderWriter; use mail_parser::{Encoding, Message, MessagePart, PartType}; use sieve::{ @@ -29,7 +30,7 @@ use super::{ScriptModification, ScriptParameters, ScriptResult}; pub trait RunScript: Sync + Send { fn run_script( &self, - script_id: String, + script_id: CompactString, script: Arc, params: ScriptParameters<'_>, ) -> impl Future + Send; @@ -38,7 +39,7 @@ pub trait RunScript: Sync + Send { impl RunScript for Server { async fn run_script( &self, - script_id: String, + script_id: CompactString, script: Arc, params: ScriptParameters<'_>, ) -> ScriptResult { @@ -345,7 +346,7 @@ impl RunScript for Server { Event::SetEnvelope { envelope, value } => { modifications.push(ScriptModification::SetEnvelope { name: envelope, - value, + value: value.into(), }); input = true.into(); } @@ -404,9 +405,9 @@ impl RunScript for Server { && matches!(reject_bytes.next(), Some(ch) if ch.is_ascii_digit()) && matches!(reject_bytes.next(), Some(ch) if ch == &b' ' ) { - ScriptResult::Reject(reject_reason) + ScriptResult::Reject(reject_reason.into()) } else { - ScriptResult::Reject(format!("503 5.5.3 {reject_reason}")) + ScriptResult::Reject(format!("503 5.5.3 {reject_reason}").into()) } } else if keep_id != usize::MAX - 1 { if let Some(message) = messages.into_iter().nth(keep_id - 1) { diff --git a/crates/smtp/src/scripts/exec.rs b/crates/smtp/src/scripts/exec.rs index cf60f716..fcb9f31b 100644 --- a/crates/smtp/src/scripts/exec.rs +++ b/crates/smtp/src/scripts/exec.rs @@ -7,6 +7,7 @@ use std::{sync::Arc, time::SystemTime}; use common::listener::SessionStream; +use compact_str::CompactString; use mail_auth::common::resolver::ToReverseName; use sieve::{Envelope, Sieve, runtime::Variable}; use smtp_proto::*; @@ -21,7 +22,7 @@ impl Session { let mut params = ScriptParameters::new() .set_variable("remote_ip", self.data.remote_ip.to_string()) .set_variable("remote_ip.reverse", self.data.remote_ip.to_reverse_name()) - .set_variable("helo_domain", self.data.helo_domain.to_lowercase()) + .set_variable("helo_domain", self.data.helo_domain.as_str().to_lowercase()) .set_variable( "authenticated_as", self.authenticated_as().unwrap_or_default().to_string(), @@ -86,7 +87,7 @@ impl Session { if let Some(env_id) = &mail_from.dsn_info { params .envelope - .push((Envelope::Envid, env_id.to_lowercase().into())); + .push((Envelope::Envid, env_id.as_str().to_lowercase().into())); } if stage != "data" { @@ -97,7 +98,7 @@ impl Session { if let Some(orcpt) = &rcpt.dsn_info { params .envelope - .push((Envelope::Orcpt, orcpt.to_lowercase().into())); + .push((Envelope::Orcpt, orcpt.as_str().to_lowercase().into())); } } } else { @@ -141,7 +142,7 @@ impl Session { pub async fn run_script( &self, - script_id: String, + script_id: CompactString, script: Arc, params: ScriptParameters<'_>, ) -> ScriptResult { diff --git a/crates/smtp/src/scripts/mod.rs b/crates/smtp/src/scripts/mod.rs index 1d17f4a0..0e9aff04 100644 --- a/crates/smtp/src/scripts/mod.rs +++ b/crates/smtp/src/scripts/mod.rs @@ -10,6 +10,7 @@ use ahash::AHashMap; use common::{ Server, auth::AccessToken, expr::functions::ResolveVariable, scripts::ScriptModification, }; +use compact_str::CompactString; use mail_parser::Message; use sieve::{Envelope, runtime::Variable}; @@ -26,7 +27,7 @@ pub enum ScriptResult { message: Vec, modifications: Vec, }, - Reject(String), + Reject(CompactString), Discard, } @@ -35,10 +36,10 @@ pub struct ScriptParameters<'x> { headers: Option<&'x [u8]>, variables: AHashMap, Variable>, envelope: Vec<(Envelope, Variable)>, - from_addr: String, - from_name: String, - return_path: String, - sign: Vec, + from_addr: CompactString, + from_name: CompactString, + return_path: CompactString, + sign: Vec, access_token: Option<&'x AccessToken>, session_id: u64, } diff --git a/crates/spam-filter/Cargo.toml b/crates/spam-filter/Cargo.toml index b5f37794..9fa11fca 100644 --- a/crates/spam-filter/Cargo.toml +++ b/crates/spam-filter/Cargo.toml @@ -25,6 +25,7 @@ unicode-security = "0.1.0" infer = "0.16" sha1 = "0.10" sha2 = "0.10.6" +compact_str = "0.9.0" [features] test_mode = [] diff --git a/crates/spam-filter/src/analysis/domain.rs b/crates/spam-filter/src/analysis/domain.rs index 11b3b827..7c734f52 100644 --- a/crates/spam-filter/src/analysis/domain.rs +++ b/crates/spam-filter/src/analysis/domain.rs @@ -10,6 +10,7 @@ use common::{ Server, config::spamfilter::{Element, Location}, }; +use compact_str::CompactString; use mail_auth::DkimResult; use mail_parser::{HeaderName, HeaderValue, Host, parsers::MessageStream}; use nlp::tokenizers::types::TokenType; @@ -35,7 +36,7 @@ pub trait SpamFilterAnalyzeDomain: Sync + Send { impl SpamFilterAnalyzeDomain for Server { async fn spam_filter_analyze_domain(&self, ctx: &mut SpamFilterContext<'_>) { // Obtain email addresses and domains - let mut domains: HashSet> = HashSet::new(); + let mut domains: HashSet> = HashSet::new(); let mut emails: HashSet> = HashSet::new(); // Add DKIM domains @@ -43,7 +44,7 @@ impl SpamFilterAnalyzeDomain for Server { if dkim.result() == &DkimResult::Pass { if let Some(domain) = dkim.signature().map(|s| &s.d) { domains.insert(ElementLocation::new( - domain.to_lowercase(), + CompactString::from_str_to_lowercase(domain), Location::HeaderDkimPass, )); } diff --git a/crates/spam-filter/src/analysis/ehlo.rs b/crates/spam-filter/src/analysis/ehlo.rs index 3a4896fb..8a106217 100644 --- a/crates/spam-filter/src/analysis/ehlo.rs +++ b/crates/spam-filter/src/analysis/ehlo.rs @@ -32,7 +32,7 @@ impl SpamFilterAnalyzeEhlo for Server { .output .iprev_ptr .as_ref() - .is_some_and(|ptr| ptr != &ctx.output.ehlo_host.fqdn) + .is_some_and(|ptr| ptr != ctx.output.ehlo_host.fqdn) { // Helo does not match reverse IP ctx.result.add_tag("HELO_IPREV_MISMATCH"); diff --git a/crates/spam-filter/src/analysis/init.rs b/crates/spam-filter/src/analysis/init.rs index 34965d26..df8a84ce 100644 --- a/crates/spam-filter/src/analysis/init.rs +++ b/crates/spam-filter/src/analysis/init.rs @@ -5,6 +5,7 @@ */ use common::Server; +use compact_str::CompactString; use mail_parser::{HeaderName, PartType, parsers::fields::thread::thread_name}; use nlp::tokenizers::types::{TokenType, TypesTokenizer}; @@ -41,7 +42,7 @@ impl SpamFilterInit for Server { name: addr.name().and_then(|s| { let s = s.trim(); if !s.is_empty() { - Some(s.to_lowercase()) + Some(CompactString::from_str_to_lowercase(s)) } else { None } @@ -68,7 +69,7 @@ impl SpamFilterInit for Server { name: addr.name().and_then(|s| { let s = s.trim(); if !s.is_empty() { - Some(s.to_lowercase()) + Some(CompactString::from_str_to_lowercase(s)) } else { None } @@ -245,10 +246,9 @@ impl SpamFilterInit for Server { output: SpamFilterOutput { ehlo_host: Hostname::new(input.ehlo_domain.unwrap_or("unknown")), iprev_ptr: input.iprev_result.and_then(|r| { - r.ptr - .as_ref() - .and_then(|ptr| ptr.first()) - .map(|ptr| ptr.strip_suffix('.').unwrap_or(ptr).to_lowercase()) + r.ptr.as_ref().and_then(|ptr| ptr.first()).map(|ptr| { + CompactString::from_str_to_lowercase(ptr.strip_suffix('.').unwrap_or(ptr)) + }) }), env_from_postmaster: env_from_addr.address.is_empty() || POSTMASTER_ADDRESSES.contains(&env_from_addr.local_part.as_str()), @@ -260,7 +260,9 @@ impl SpamFilterInit for Server { .collect(), from: Recipient { email: Email::new(from.and_then(|f| f.address()).unwrap_or_default()), - name: from.and_then(|f| f.name()).map(|s| s.to_lowercase()), + name: from + .and_then(|f| f.name()) + .map(CompactString::from_str_to_lowercase), }, reply_to, subject_thread_lc: subject_thread.trim().to_lowercase(), diff --git a/crates/spam-filter/src/analysis/messageid.rs b/crates/spam-filter/src/analysis/messageid.rs index 4540b5d5..fac567c7 100644 --- a/crates/spam-filter/src/analysis/messageid.rs +++ b/crates/spam-filter/src/analysis/messageid.rs @@ -64,7 +64,7 @@ impl SpamFilterAnalyzeMid for Server { ("ENV_FROM", &ctx.output.env_from_addr), ] { if !sender.address.is_empty() { - if mid.contains(&sender.address) { + if mid.contains(sender.address.as_str()) { ctx.result.add_tag(format!("MID_CONTAINS_{part}")); } else if mid_host.fqdn == sender.domain_part.fqdn { ctx.result.add_tag(format!("MID_RHS_MATCH_{part}")); @@ -77,7 +77,7 @@ impl SpamFilterAnalyzeMid for Server { // To/Cc addresses present in Message-ID checks for rcpt in ctx.output.all_recipients() { - if mid.contains(&rcpt.email.address) { + if mid.contains(rcpt.email.address.as_str()) { ctx.result.add_tag("MID_CONTAINS_TO"); } else if mid_host.fqdn == rcpt.email.domain_part.fqdn { ctx.result.add_tag("MID_RHS_MATCH_TO"); diff --git a/crates/spam-filter/src/analysis/mod.rs b/crates/spam-filter/src/analysis/mod.rs index 9dcff6e0..35f961fb 100644 --- a/crates/spam-filter/src/analysis/mod.rs +++ b/crates/spam-filter/src/analysis/mod.rs @@ -10,6 +10,7 @@ use std::{ }; use common::{Server, config::spamfilter::Location}; +use compact_str::CompactString; use mail_parser::{Header, parsers::MessageStream}; use crate::{ @@ -79,7 +80,7 @@ impl SpamFilterContext<'_> { } impl SpamFilterResult { - pub fn add_tag(&mut self, tag: impl Into) { + pub fn add_tag(&mut self, tag: impl Into) { self.tags.insert(tag.into()); } diff --git a/crates/spam-filter/src/analysis/recipient.rs b/crates/spam-filter/src/analysis/recipient.rs index a1d87e15..f492f91e 100644 --- a/crates/spam-filter/src/analysis/recipient.rs +++ b/crates/spam-filter/src/analysis/recipient.rs @@ -153,7 +153,7 @@ impl SpamFilterAnalyzeRecipient for Server { for rcpt in &unique_recipients { // Validate name if let Some(rcpt_name) = &rcpt.name { - if rcpt_name == &rcpt.email.address { + if rcpt_name == rcpt.email.address { to_dn_eq_addr_count += 1; } else { to_dn_count += 1; @@ -167,10 +167,13 @@ impl SpamFilterAnalyzeRecipient for Server { // Check if the local part is present in the subject if !rcpt.email.local_part.is_empty() { - if ctx.output.subject_lc.contains(&rcpt.email.address) { + if ctx.output.subject_lc.contains(rcpt.email.address.as_str()) { ctx.result.add_tag("RCPT_IN_SUBJECT"); } else if rcpt.email.local_part.len() > 3 - && ctx.output.subject_lc.contains(&rcpt.email.local_part) + && ctx + .output + .subject_lc + .contains(rcpt.email.local_part.as_str()) { ctx.result.add_tag("RCPT_LOCAL_IN_SUBJECT"); } diff --git a/crates/spam-filter/src/analysis/rules.rs b/crates/spam-filter/src/analysis/rules.rs index e80478a3..ed9a3e17 100644 --- a/crates/spam-filter/src/analysis/rules.rs +++ b/crates/spam-filter/src/analysis/rules.rs @@ -10,6 +10,7 @@ use common::{ Server, config::spamfilter::{IpResolver, Location}, }; +use compact_str::CompactString; use crate::{ SpamFilterContext, TextPart, @@ -29,7 +30,7 @@ impl SpamFilterAnalyzeRules for Server { for url in &ctx.output.urls { for rule in &self.core.spam.rules.url { if let Some(tag) = self - .eval_if::( + .eval_if::( rule, &SpamFilterResolver::new(ctx, &url.element, url.location), ctx.input.span_id, @@ -48,7 +49,7 @@ impl SpamFilterAnalyzeRules for Server { for rule in &self.core.spam.rules.domain { if let Some(tag) = self - .eval_if::( + .eval_if::( rule, &SpamFilterResolver::new(ctx, &resolver, domain.location), ctx.input.span_id, @@ -65,7 +66,7 @@ impl SpamFilterAnalyzeRules for Server { for email in &ctx.output.emails { for rule in &self.core.spam.rules.email { if let Some(tag) = self - .eval_if::( + .eval_if::( rule, &SpamFilterResolver::new(ctx, &email.element, email.location), ctx.input.span_id, @@ -85,7 +86,7 @@ impl SpamFilterAnalyzeRules for Server { for email in rcpt { for rule in &self.core.spam.rules.email { if let Some(tag) = self - .eval_if::( + .eval_if::( rule, &SpamFilterResolver::new(ctx, email, location), ctx.input.span_id, @@ -105,7 +106,7 @@ impl SpamFilterAnalyzeRules for Server { for rule in &self.core.spam.rules.ip { if let Some(tag) = self - .eval_if::( + .eval_if::( rule, &SpamFilterResolver::new(ctx, &ip_resolver, ip.location), ctx.input.span_id, @@ -134,7 +135,7 @@ impl SpamFilterAnalyzeRules for Server { for rule in &self.core.spam.rules.header { if let Some(tag) = self - .eval_if::( + .eval_if::( rule, &SpamFilterResolver::new(ctx, &header_resolver, Location::BodyText), ctx.input.span_id, @@ -165,7 +166,7 @@ impl SpamFilterAnalyzeRules for Server { for rule in &self.core.spam.rules.body { if let Some(tag) = self - .eval_if::( + .eval_if::( rule, &SpamFilterResolver::new(ctx, &string_resolver, location), ctx.input.span_id, @@ -182,7 +183,7 @@ impl SpamFilterAnalyzeRules for Server { let dummy_resolver = StringResolver(""); for rule in &self.core.spam.rules.any { if let Some(tag) = self - .eval_if::( + .eval_if::( rule, &SpamFilterResolver::new(ctx, &dummy_resolver, Location::BodyText), ctx.input.span_id, diff --git a/crates/spam-filter/src/lib.rs b/crates/spam-filter/src/lib.rs index 62b30caa..d89f594f 100644 --- a/crates/spam-filter/src/lib.rs +++ b/crates/spam-filter/src/lib.rs @@ -14,6 +14,7 @@ use std::net::{IpAddr, Ipv4Addr}; use analysis::ElementLocation; use analysis::url::UrlParts; +use compact_str::CompactString; use mail_auth::{ArcOutput, DkimOutput, DmarcResult, IprevOutput, SpfOutput, dmarc::Policy}; use mail_parser::Message; use modules::html::HtmlToken; @@ -54,7 +55,7 @@ pub struct SpamFilterInput<'x> { pub struct SpamFilterOutput<'x> { pub ehlo_host: Hostname, - pub iprev_ptr: Option, + pub iprev_ptr: Option, pub env_from_addr: Email, pub env_from_postmaster: bool, @@ -74,7 +75,7 @@ pub struct SpamFilterOutput<'x> { pub ips: AHashSet>, pub urls: HashSet>>, pub emails: HashSet>, - pub domains: HashSet>, + pub domains: HashSet>, pub text_parts: Vec>, } @@ -100,7 +101,7 @@ pub enum TextPart<'x> { #[derive(Debug, Default)] pub struct SpamFilterResult { - pub tags: AHashSet, + pub tags: AHashSet, pub score: f64, pub rbl_ip_checks: usize, pub rbl_domain_checks: usize, @@ -117,22 +118,22 @@ pub struct SpamFilterContext<'x> { #[derive(Debug, Clone)] pub struct Hostname { - pub fqdn: String, + pub fqdn: CompactString, pub ip: Option, - pub sld: Option, + pub sld: Option, } #[derive(Debug, Clone)] pub struct Email { - pub address: String, - pub local_part: String, + pub address: CompactString, + pub local_part: CompactString, pub domain_part: Hostname, } #[derive(Debug, Clone)] pub struct Recipient { pub email: Email, - pub name: Option, + pub name: Option, } impl<'x> SpamFilterInput<'x> { diff --git a/crates/spam-filter/src/modules/dnsbl.rs b/crates/spam-filter/src/modules/dnsbl.rs index b7da585e..4b37e4f3 100644 --- a/crates/spam-filter/src/modules/dnsbl.rs +++ b/crates/spam-filter/src/modules/dnsbl.rs @@ -15,6 +15,7 @@ use common::{ config::spamfilter::{DnsBlServer, Element, IpResolver, Location}, expr::functions::ResolveVariable, }; +use compact_str::CompactString; use mail_auth::{Error, common::resolver::IntoFqdn}; use trc::SpamEvent; @@ -80,10 +81,10 @@ async fn is_dnsbl( resolver: SpamFilterResolver<'_, impl ResolveVariable>, element: Element, checks: &mut usize, -) -> Option { +) -> Option { let time = Instant::now(); let zone = server - .eval_if::(&config.zone, &resolver, resolver.ctx.input.span_id) + .eval_if::(&config.zone, &resolver, resolver.ctx.input.span_id) .await?; #[cfg(feature = "test_mode")] @@ -112,7 +113,7 @@ async fn is_dnsbl( } } - let result = match server.inner.cache.dns_rbl.get(&zone) { + let result = match server.inner.cache.dns_rbl.get(zone.as_str()) { Some(Some(result)) => result, Some(None) => return None, None => { @@ -150,7 +151,7 @@ async fn is_dnsbl( )); server.inner.cache.dns_rbl.insert_with_expiry( - zone, + zone.to_string(), Some(entry.clone()), result.expires, ); @@ -166,11 +167,11 @@ async fn is_dnsbl( Elapsed = time.elapsed() ); - server - .inner - .cache - .dns_rbl - .insert(zone, None, Duration::from_secs(86400)); + server.inner.cache.dns_rbl.insert( + zone.to_string(), + None, + Duration::from_secs(86400), + ); return None; } diff --git a/crates/spam-filter/src/modules/expression.rs b/crates/spam-filter/src/modules/expression.rs index 57fbf2eb..49213da5 100644 --- a/crates/spam-filter/src/modules/expression.rs +++ b/crates/spam-filter/src/modules/expression.rs @@ -6,8 +6,9 @@ use common::{ config::spamfilter::*, - expr::{Variable, functions::ResolveVariable}, + expr::{StringCow, Variable, functions::ResolveVariable}, }; +use compact_str::{CompactString, ToCompactString, format_compact}; use mail_parser::{Header, HeaderValue}; use nlp::tokenizers::types::TokenType; @@ -33,7 +34,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { fn resolve_variable(&self, variable: u32) -> Variable<'_> { match variable { 0..100 => self.item.resolve_variable(variable), - V_SPAM_REMOTE_IP => self.ctx.input.remote_ip.to_string().into(), + V_SPAM_REMOTE_IP => self.ctx.input.remote_ip.to_compact_string().into(), V_SPAM_REMOTE_IP_PTR => self .ctx .output @@ -61,7 +62,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .env_to_addr .iter() - .map(|e| Variable::String(e.address.as_str().into())) + .map(|e| Variable::from(e.address.as_str())) .collect::>() .into(), V_SPAM_FROM => self.ctx.output.from.email.address.as_str().into(), @@ -112,7 +113,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_to .iter() - .map(|r| Variable::String(r.email.address.as_str().into())) + .map(|r| Variable::from(r.email.address.as_str())) .collect::>() .into(), V_SPAM_TO_NAME => self @@ -120,7 +121,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_to .iter() - .filter_map(|r| Variable::String(r.name.as_deref()?.into()).into()) + .filter_map(|r| Variable::from(r.name.as_deref()?).into()) .collect::>() .into(), V_SPAM_TO_LOCAL => self @@ -128,7 +129,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_to .iter() - .map(|r| Variable::String(r.email.local_part.as_str().into())) + .map(|r| Variable::from(r.email.local_part.as_str())) .collect::>() .into(), V_SPAM_TO_DOMAIN => self @@ -136,7 +137,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_to .iter() - .map(|r| Variable::String(r.email.domain_part.fqdn.as_str().into())) + .map(|r| Variable::from(r.email.domain_part.fqdn.as_str())) .collect::>() .into(), V_SPAM_CC => self @@ -144,7 +145,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_cc .iter() - .map(|r| Variable::String(r.email.address.as_str().into())) + .map(|r| Variable::from(r.email.address.as_str())) .collect::>() .into(), V_SPAM_CC_NAME => self @@ -152,7 +153,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_cc .iter() - .filter_map(|r| Variable::String(r.name.as_deref()?.into()).into()) + .filter_map(|r| Variable::from(r.name.as_deref()?).into()) .collect::>() .into(), V_SPAM_CC_LOCAL => self @@ -160,7 +161,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_cc .iter() - .map(|r| Variable::String(r.email.local_part.as_str().into())) + .map(|r| Variable::from(r.email.local_part.as_str())) .collect::>() .into(), V_SPAM_CC_DOMAIN => self @@ -168,7 +169,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_cc .iter() - .map(|r| Variable::String(r.email.domain_part.fqdn.as_str().into())) + .map(|r| Variable::from(r.email.domain_part.fqdn.as_str())) .collect::>() .into(), V_SPAM_BCC => self @@ -176,7 +177,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_bcc .iter() - .map(|r| Variable::String(r.email.address.as_str().into())) + .map(|r| Variable::from(r.email.address.as_str())) .collect::>() .into(), V_SPAM_BCC_NAME => self @@ -184,7 +185,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_bcc .iter() - .filter_map(|r| Variable::String(r.name.as_deref()?.into()).into()) + .filter_map(|r| Variable::from(r.name.as_deref()?).into()) .collect::>() .into(), V_SPAM_BCC_LOCAL => self @@ -192,7 +193,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_bcc .iter() - .map(|r| Variable::String(r.email.local_part.as_str().into())) + .map(|r| Variable::from(r.email.local_part.as_str())) .collect::>() .into(), V_SPAM_BCC_DOMAIN => self @@ -200,7 +201,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .output .recipients_bcc .iter() - .map(|r| Variable::String(r.email.domain_part.fqdn.as_str().into())) + .map(|r| Variable::from(r.email.domain_part.fqdn.as_str())) .collect::>() .into(), V_SPAM_BODY_TEXT => self.ctx.text_body().unwrap_or_default().into(), @@ -213,13 +214,14 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { .and_then(|idx| self.ctx.output.text_parts.get(*idx)) .map(|part| { if let TextPart::Html { text_body, .. } = part { - text_body.as_str().into() + text_body.as_str() } else { - "".into() + "" } }) - .unwrap_or_default(), - V_SPAM_BODY_RAW => Variable::String(String::from_utf8_lossy( + .unwrap_or_default() + .into(), + V_SPAM_BODY_RAW => Variable::from(CompactString::from_utf8_lossy( self.ctx.input.message.raw_message(), )), V_SPAM_SUBJECT => self.ctx.output.subject_lc.as_str().into(), @@ -234,7 +236,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { TokenType::Alphabetic(w) | TokenType::Alphanumeric(w) | TokenType::Integer(w) - | TokenType::Float(w) => Some(Variable::String(w.as_ref().into())), + | TokenType::Float(w) => Some(Variable::from(w.as_ref())), _ => None, }) .collect::>() @@ -253,7 +255,7 @@ impl ResolveVariable for SpamFilterResolver<'_, T> { TokenType::Alphabetic(w) | TokenType::Alphanumeric(w) | TokenType::Integer(w) - | TokenType::Float(w) => Some(Variable::String(w.as_ref().into())), + | TokenType::Float(w) => Some(Variable::from(w.as_ref())), _ => None, }) .collect::>(), @@ -279,11 +281,11 @@ impl ResolveVariable for EmailHeader<'_> { fn resolve_variable(&self, variable: u32) -> Variable<'_> { match variable { V_HEADER_NAME => self.header.name().into(), - V_HEADER_NAME_LOWER => self.header.name().to_ascii_lowercase().into(), + V_HEADER_NAME_LOWER => CompactString::from_str_to_lowercase(self.header.name()).into(), V_HEADER_VALUE | V_HEADER_VALUE_LOWER | V_HEADER_PROPERTY => match &self.header.value { HeaderValue::Text(text) => { if variable == V_HEADER_VALUE_LOWER { - text.to_ascii_lowercase().into() + CompactString::from_str_to_lowercase(text).into() } else { text.as_ref().into() } @@ -292,9 +294,9 @@ impl ResolveVariable for EmailHeader<'_> { list.iter() .map(|text| { Variable::String(if variable == V_HEADER_VALUE_LOWER { - text.to_ascii_lowercase().into() + StringCow::Owned(CompactString::from_str_to_lowercase(text)) } else { - text.as_ref().into() + StringCow::Borrowed(text.as_ref()) }) }) .collect(), @@ -305,9 +307,9 @@ impl ResolveVariable for EmailHeader<'_> { .filter_map(|a| { a.address.as_ref().map(|text| { Variable::String(if variable == V_HEADER_VALUE_LOWER { - text.to_ascii_lowercase().into() + StringCow::Owned(CompactString::from_str_to_lowercase(text)) } else { - text.as_ref().into() + StringCow::Borrowed(text.as_ref()) }) }) }) @@ -318,19 +320,21 @@ impl ResolveVariable for EmailHeader<'_> { .filter_map(|a| { a.name.as_ref().map(|text| { Variable::String(if variable == V_HEADER_VALUE_LOWER { - text.to_ascii_lowercase().into() + StringCow::Owned(CompactString::from_str_to_lowercase(text)) } else { - text.as_ref().into() + StringCow::Borrowed(text.as_ref()) }) }) }) .collect() }), - HeaderValue::DateTime(date_time) => date_time.to_rfc3339().into(), + HeaderValue::DateTime(date_time) => { + CompactString::new(date_time.to_rfc3339()).into() + } HeaderValue::ContentType(ct) => { if variable != V_HEADER_PROPERTY { if let Some(st) = ct.subtype() { - format!("{}/{}", ct.ctype(), st).into() + format_compact!("{}/{}", ct.ctype(), st).into() } else { ct.ctype().into() } @@ -339,7 +343,7 @@ impl ResolveVariable for EmailHeader<'_> { ct.attributes() .map(|attr| { attr.iter() - .map(|(k, v)| Variable::String(format!("{k}={v}").into())) + .map(|(k, v)| Variable::from(format_compact!("{k}={v}"))) .collect::>() }) .unwrap_or_default(), @@ -348,7 +352,7 @@ impl ResolveVariable for EmailHeader<'_> { } HeaderValue::Received(_) => { if variable == V_HEADER_VALUE_LOWER { - self.raw.trim().to_lowercase().into() + CompactString::from_str_to_lowercase(self.raw.trim()).into() } else { self.raw.trim().into() } @@ -356,7 +360,7 @@ impl ResolveVariable for EmailHeader<'_> { HeaderValue::Empty => "".into(), }, V_HEADER_RAW => self.raw.into(), - V_HEADER_RAW_LOWER => self.raw.to_lowercase().into(), + V_HEADER_RAW_LOWER => CompactString::from_str_to_lowercase(self.raw).into(), _ => Variable::Integer(0), } } @@ -369,11 +373,11 @@ impl ResolveVariable for EmailHeader<'_> { impl ResolveVariable for Recipient { fn resolve_variable(&self, variable: u32) -> Variable<'_> { match variable { - V_RCPT_EMAIL => Variable::String(self.email.address.as_str().into()), - V_RCPT_NAME => Variable::String(self.name.as_deref().unwrap_or_default().into()), - V_RCPT_LOCAL => Variable::String(self.email.local_part.as_str().into()), - V_RCPT_DOMAIN => Variable::String(self.email.domain_part.fqdn.as_str().into()), - V_RCPT_DOMAIN_SLD => Variable::String(self.email.domain_part.sld_or_default().into()), + V_RCPT_EMAIL => Variable::from(self.email.address.as_str()), + V_RCPT_NAME => Variable::from(self.name.as_deref().unwrap_or_default()), + V_RCPT_LOCAL => Variable::from(self.email.local_part.as_str()), + V_RCPT_DOMAIN => Variable::from(self.email.domain_part.fqdn.as_str()), + V_RCPT_DOMAIN_SLD => Variable::from(self.email.domain_part.sld_or_default()), _ => Variable::Integer(0), } } @@ -386,55 +390,48 @@ impl ResolveVariable for Recipient { impl ResolveVariable for UrlParts<'_> { fn resolve_variable(&self, variable: u32) -> Variable<'_> { match variable { - V_URL_FULL => Variable::String(self.url.as_str().into()), - V_URL_PATH_QUERY => Variable::String( + V_URL_FULL => Variable::from(self.url.as_str()), + V_URL_PATH_QUERY => Variable::from( self.url_parsed .as_ref() .and_then(|p| p.parts.path_and_query().map(|p| p.as_str())) - .unwrap_or_default() - .into(), + .unwrap_or_default(), ), - V_URL_PATH => Variable::String( + V_URL_PATH => Variable::from( self.url_parsed .as_ref() .map(|p| p.parts.path()) - .unwrap_or_default() - .into(), + .unwrap_or_default(), ), - V_URL_QUERY => Variable::String( + V_URL_QUERY => Variable::from( self.url_parsed .as_ref() .and_then(|p| p.parts.query()) - .unwrap_or_default() - .into(), + .unwrap_or_default(), ), - V_URL_SCHEME => Variable::String( + V_URL_SCHEME => Variable::from( self.url_parsed .as_ref() .and_then(|p| p.parts.scheme_str()) - .unwrap_or_default() - .into(), + .unwrap_or_default(), ), - V_URL_AUTHORITY => Variable::String( + V_URL_AUTHORITY => Variable::from( self.url_parsed .as_ref() .and_then(|p| p.parts.authority().map(|a| a.as_str())) - .unwrap_or_default() - .into(), + .unwrap_or_default(), ), - V_URL_HOST => Variable::String( + V_URL_HOST => Variable::from( self.url_parsed .as_ref() .map(|p| p.host.fqdn.as_str()) - .unwrap_or_default() - .into(), + .unwrap_or_default(), ), - V_URL_HOST_SLD => Variable::String( + V_URL_HOST_SLD => Variable::from( self.url_parsed .as_ref() .map(|p| p.host.sld_or_default()) - .unwrap_or_default() - .into(), + .unwrap_or_default(), ), V_URL_PORT => Variable::Integer( self.url_parsed @@ -455,7 +452,7 @@ pub struct StringResolver<'x>(pub &'x str); impl ResolveVariable for StringResolver<'_> { fn resolve_variable(&self, _: u32) -> Variable<'_> { - Variable::String(self.0.into()) + Variable::from(self.0) } fn resolve_global(&self, _: &str) -> Variable<'_> { @@ -467,7 +464,7 @@ pub struct StringListResolver<'x>(pub &'x [String]); impl ResolveVariable for StringListResolver<'_> { fn resolve_variable(&self, _: u32) -> Variable<'_> { - Variable::Array(self.0.iter().map(|v| Variable::String(v.into())).collect()) + Variable::Array(self.0.iter().map(|v| Variable::from(v.as_str())).collect()) } fn resolve_global(&self, _: &str) -> Variable<'_> { diff --git a/crates/spam-filter/src/modules/html.rs b/crates/spam-filter/src/modules/html.rs index 63ab6d11..ef481856 100644 --- a/crates/spam-filter/src/modules/html.rs +++ b/crates/spam-filter/src/modules/html.rs @@ -4,23 +4,24 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::CompactString; use mail_parser::decoders::html::add_html_token; #[derive(Debug, Eq, PartialEq, Clone)] pub enum HtmlToken { StartTag { name: u64, - attributes: Vec<(u64, Option)>, + attributes: Vec<(u64, Option)>, is_self_closing: bool, }, EndTag { name: u64, }, Comment { - text: String, + text: CompactString, }, Text { - text: String, + text: CompactString, }, } @@ -78,7 +79,7 @@ pub fn html_to_tokens(input: &str) -> Vec { let mut token_start = 0; let mut token_end = 0; - let mut text = String::new(); + let mut text = String::with_capacity(16); while let Some((mut pos, &ch)) = iter.next() { match ch { @@ -94,8 +95,9 @@ pub fn html_to_tokens(input: &str) -> Vec { } if !text.is_empty() { tags.push(HtmlToken::Text { - text: std::mem::take(&mut text), + text: text.as_str().into(), }); + text.clear(); } while matches!(iter.peek(), Some(&(_, &ch)) if ch.is_ascii_whitespace()) { @@ -129,7 +131,7 @@ pub fn html_to_tokens(input: &str) -> Vec { last_ch = ch; } tags.push(HtmlToken::Comment { - text: String::from_utf8(comment).unwrap_or_default(), + text: CompactString::from_utf8(comment).unwrap_or_default(), }); } else { let mut is_end_tag = false; @@ -155,7 +157,7 @@ pub fn html_to_tokens(input: &str) -> Vec { let mut shift = 0; let mut tag = 0; - let mut attributes = vec![]; + let mut attributes: Vec<(u64, Option)> = vec![]; 'outer: while let Some((_, &ch)) = iter.next() { match ch { @@ -201,8 +203,8 @@ pub fn html_to_tokens(input: &str) -> Vec { match ch { b'>' if !in_quote => { if !value.is_empty() { - let value = - String::from_utf8(value).unwrap_or_default(); + let value = CompactString::from_utf8(value) + .unwrap_or_default(); if let Some((_, v)) = attributes.last_mut() { *v = value.into(); } else { @@ -230,7 +232,7 @@ pub fn html_to_tokens(input: &str) -> Vec { } if !value.is_empty() { - let value = String::from_utf8(value).unwrap_or_default(); + let value = CompactString::from_utf8(value).unwrap_or_default(); if let Some((_, v)) = attributes.last_mut() { *v = value.into(); } else { @@ -320,7 +322,9 @@ pub fn html_to_tokens(input: &str) -> Vec { ); } if !text.is_empty() { - tags.push(HtmlToken::Text { text }); + tags.push(HtmlToken::Text { + text: text.as_str().into(), + }); } tags @@ -336,7 +340,7 @@ mod tests { assert_eq!( tokens, vec![HtmlToken::Text { - text: "Hello, world!".to_string() + text: "Hello, world!".into() }] ); } @@ -369,7 +373,7 @@ mod tests { assert_eq!( tokens, vec![HtmlToken::Comment { - text: "!-- This is a comment --".to_string() + text: "!-- This is a comment --".into() }] ); } @@ -387,7 +391,7 @@ mod tests { is_self_closing: false }, HtmlToken::Text { - text: "Hello,".to_string() + text: "Hello,".into() }, HtmlToken::StartTag { name: 1851879539, @@ -395,12 +399,10 @@ mod tests { is_self_closing: false }, HtmlToken::Text { - text: " \" world \"".to_string() + text: " \" world \"".into() }, HtmlToken::EndTag { name: 1851879539 }, - HtmlToken::Text { - text: " !".to_string() - }, + HtmlToken::Text { text: " !".into() }, HtmlToken::EndTag { name: 7760228 } ] ); @@ -416,8 +418,8 @@ mod tests { HtmlToken::StartTag { name: 500186508905, attributes: vec![ - (1701869940, Some("text".to_string())), - (435761734006, Some("test".to_string())) + (1701869940, Some("text".into())), + (435761734006, Some("test".into())) ], is_self_closing: false }, @@ -433,11 +435,7 @@ mod tests { }, HtmlToken::StartTag { name: 97, - attributes: vec![ - (98, Some("1".to_string())), - (98, None), - (99, Some("123".to_string())) - ], + attributes: vec![(98, Some("1".into())), (98, None), (99, Some("123".into()))], is_self_closing: false } ] diff --git a/crates/spam-filter/src/modules/sanitize.rs b/crates/spam-filter/src/modules/sanitize.rs index 2a8d87d4..9c32e11a 100644 --- a/crates/spam-filter/src/modules/sanitize.rs +++ b/crates/spam-filter/src/modules/sanitize.rs @@ -6,15 +6,17 @@ use std::net::IpAddr; +use compact_str::CompactString; + use crate::{Email, Hostname}; impl Hostname { pub fn new(host: &str) -> Self { - let mut fqdn = host.trim_end_matches('.').to_lowercase(); + let mut fqdn = CompactString::from_str_to_lowercase(host.trim_end_matches('.')); // Decode punycode if fqdn.contains("xn--") { - let mut decoded = String::with_capacity(fqdn.len()); + let mut decoded = CompactString::with_capacity(fqdn.len()); for part in fqdn.split('.') { if !decoded.is_empty() { @@ -45,9 +47,7 @@ impl Hostname { sld: if ip.is_none() { psl::domain(fqdn.as_bytes()).and_then(|domain| { if domain.suffix().typ().is_some() { - std::str::from_utf8(domain.as_bytes()) - .ok() - .map(str::to_string) + std::str::from_utf8(domain.as_bytes()).ok().map(Into::into) } else { None } @@ -63,11 +63,11 @@ impl Hostname { impl Email { pub fn new(address: &str) -> Self { - let address = address.to_lowercase(); + let address = CompactString::from_str_to_lowercase(address); let (local_part, domain) = address.rsplit_once('@').unwrap_or_default(); Email { - local_part: local_part.to_string(), + local_part: local_part.into(), domain_part: Hostname::new(domain), address, } diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 4c2995fe..4fb8a6a8 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -52,6 +52,7 @@ bitpacking = "0.9.2" memchr = { version = "2" } rkyv = { version = "0.8.10", features = ["little_endian"] } gxhash = "3.4.1" +compact_str = "0.9.0" [dev-dependencies] tokio = { version = "1.23", features = ["full"] } diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 57071e76..68f8f856 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -15,6 +15,7 @@ pub mod write; pub use ahash; pub use blake3; +use compact_str::CompactString; pub use gxhash; pub use parking_lot; pub use rand; @@ -70,23 +71,23 @@ pub trait SerializeInfallible { } // Max 64 versions (2 ^ 6) -pub const SERIALIZE_OBJ_01_V1: u8 = 0; -pub const SERIALIZE_OBJ_02_V1: u8 = 1; -pub const SERIALIZE_OBJ_03_V1: u8 = 2; -pub const SERIALIZE_OBJ_04_V1: u8 = 3; -pub const SERIALIZE_OBJ_05_V1: u8 = 4; -pub const SERIALIZE_OBJ_06_V1: u8 = 5; -pub const SERIALIZE_OBJ_07_V1: u8 = 6; -pub const SERIALIZE_OBJ_08_V1: u8 = 7; -pub const SERIALIZE_OBJ_09_V1: u8 = 8; -pub const SERIALIZE_OBJ_10_V1: u8 = 9; -pub const SERIALIZE_OBJ_11_V1: u8 = 10; -pub const SERIALIZE_OBJ_12_V1: u8 = 11; -pub const SERIALIZE_OBJ_13_V1: u8 = 12; -pub const SERIALIZE_OBJ_14_V1: u8 = 13; -pub const SERIALIZE_OBJ_15_V1: u8 = 14; -pub const SERIALIZE_OBJ_16_V1: u8 = 15; -pub const SERIALIZE_OBJ_17_V1: u8 = 16; +pub const SERIALIZE_CERT_V1: u8 = 0; +pub const SERIALIZE_LOCKDATA_V1: u8 = 1; +pub const SERIALIZE_IDENTITY_V1: u8 = 2; +pub const SERIALIZE_MAILBOX_V1: u8 = 3; +pub const SERIALIZE_CRYPTO_V1: u8 = 4; +pub const SERIALIZE_MSGDATA_V1: u8 = 5; +pub const SERIALIZE_MSGMETADATA_V1: u8 = 6; +pub const SERIALIZE_PUSH_V1: u8 = 7; +pub const SERIALIZE_SIEVE_V1: u8 = 8; +pub const SERIALIZE_SUBMISSION_V1: u8 = 9; +pub const SERIALIZE_FILENODE_V1: u8 = 10; +pub const SERIALIZE_OAUTHCODE_V1: u8 = 11; +pub const SERIALIZE_QUEUEMSG_V1: u8 = 12; +pub const SERIALIZE_CALENDAR_V1: u8 = 13; +pub const SERIALIZE_ADDRESSBOOK_V1: u8 = 14; +pub const SERIALIZE_CALENDAREVENT_V1: u8 = 15; +pub const SERIALIZE_PRINCIPAL_V1: u8 = 16; pub trait SerializedVersion { fn serialize_version() -> u8; @@ -656,19 +657,30 @@ impl From> for Value<'_> { } impl Value<'_> { - pub fn into_string(self) -> String { + pub fn into_string(self) -> CompactString { match self { - Value::Text(s) => s.into_owned(), - Value::Integer(i) => i.to_string(), - Value::Bool(b) => b.to_string(), - Value::Float(f) => f.to_string(), - Value::Blob(b) => String::from_utf8_lossy(b.as_ref()).into_owned(), - Value::Null => String::new(), + Value::Text(s) => s.as_ref().into(), + Value::Integer(i) => i.to_string().into(), + Value::Bool(b) => b.to_string().into(), + Value::Float(f) => f.to_string().into(), + Value::Blob(b) => CompactString::from_utf8_lossy(b.as_ref()), + Value::Null => "".into(), + } + } + + pub fn into_lower_string(self) -> CompactString { + match self { + Value::Text(s) => CompactString::from_str_to_lowercase(s.as_ref()), + Value::Integer(i) => i.to_string().into(), + Value::Bool(b) => b.to_string().into(), + Value::Float(f) => f.to_string().into(), + Value::Blob(b) => CompactString::from_utf8_lossy(b.as_ref()).to_lowercase(), + Value::Null => "".into(), } } } -impl From for Vec { +impl From for Vec { fn from(value: Row) -> Self { value.values.into_iter().map(|v| v.into_string()).collect() } @@ -690,7 +702,7 @@ impl From for Vec { } } -impl From for Vec { +impl From for Vec { fn from(value: Rows) -> Self { value .rows @@ -788,7 +800,10 @@ impl From> for trc::Value { Value::Integer(v) => trc::Value::Int(v), Value::Bool(v) => trc::Value::Bool(v), Value::Float(v) => trc::Value::Float(v), - Value::Text(v) => trc::Value::String(v.into_owned()), + Value::Text(v) => trc::Value::String(match v { + Cow::Borrowed(v) => v.into(), + Cow::Owned(v) => v.into(), + }), Value::Blob(v) => trc::Value::Bytes(v.into_owned()), Value::Null => trc::Value::None, } diff --git a/crates/trc/src/event/conv.rs b/crates/trc/src/event/conv.rs index d2ed13e4..91825090 100644 --- a/crates/trc/src/event/conv.rs +++ b/crates/trc/src/event/conv.rs @@ -19,19 +19,19 @@ impl AsRef for Error { impl From<&'static str> for Value { fn from(value: &'static str) -> Self { - Self::Static(value) + Self::String(CompactString::const_new(value)) } } impl From for Value { fn from(value: String) -> Self { - Self::String(value) + Self::String(value.into()) } } impl From for Value { fn from(value: CompactString) -> Self { - Self::String(value.to_string()) + Self::String(value) } } @@ -146,8 +146,8 @@ impl From<&[u8]> for Value { impl From> for Value { fn from(value: Cow<'static, str>) -> Self { match value { - Cow::Borrowed(value) => Self::Static(value), - Cow::Owned(value) => Self::String(value), + Cow::Borrowed(value) => Self::String(CompactString::const_new(value)), + Cow::Owned(value) => Self::String(value.into()), } } } @@ -356,7 +356,7 @@ impl From<&mail_auth::IprevOutput> for Error { Key::Details, value.ptr.as_ref().map(|s| { s.iter() - .map(|v| Value::String(v.to_string())) + .map(|v| Value::String(v.into())) .collect::>() }), ) diff --git a/crates/trc/src/event/mod.rs b/crates/trc/src/event/mod.rs index b9a4462f..b5bb2828 100644 --- a/crates/trc/src/event/mod.rs +++ b/crates/trc/src/event/mod.rs @@ -9,7 +9,7 @@ pub mod description; pub mod level; pub mod metrics; -use std::{borrow::Cow, fmt::Display}; +use std::fmt::Display; use crate::*; @@ -653,7 +653,7 @@ impl NetworkEvent { impl Value { pub fn from_maybe_string(value: &[u8]) -> Self { if let Ok(value) = std::str::from_utf8(value) { - Self::String(value.to_string()) + Self::String(value.into()) } else { Self::Bytes(value.to_vec()) } @@ -670,15 +670,13 @@ impl Value { pub fn as_str(&self) -> Option<&str> { match self { Self::String(value) => Some(value.as_str()), - Self::Static(value) => Some(value), _ => None, } } - pub fn into_string(self) -> Option> { + pub fn into_string(self) -> Option { match self { - Self::String(value) => Some(Cow::Owned(value)), - Self::Static(value) => Some(Cow::Borrowed(value)), + Self::String(value) => Some(value), _ => None, } } @@ -729,10 +727,7 @@ impl PartialEq for Error { impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match (self, other) { - (Self::Static(l0), Self::Static(r0)) => l0 == r0, (Self::String(l0), Self::String(r0)) => l0 == r0, - (Self::String(l0), Self::Static(r0)) => l0 == r0, - (Self::Static(l0), Self::String(r0)) => l0 == r0, (Self::UInt(l0), Self::UInt(r0)) => l0 == r0, (Self::Int(l0), Self::Int(r0)) => l0 == r0, (Self::Float(l0), Self::Float(r0)) => l0 == r0, diff --git a/crates/trc/src/lib.rs b/crates/trc/src/lib.rs index ca486187..4da98390 100644 --- a/crates/trc/src/lib.rs +++ b/crates/trc/src/lib.rs @@ -16,6 +16,7 @@ use std::{ }; pub use crate::ipc::collector::Collector; +use compact_str::CompactString; pub use event_macro::event; use event_macro::{event_family, event_type, key_names, total_event_count}; @@ -53,8 +54,7 @@ pub enum Level { #[derive(Debug, Default, Clone)] pub enum Value { - Static(&'static str), - String(String), + String(CompactString), UInt(u64), Int(i64), Float(f64), diff --git a/crates/trc/src/serializers/binary.rs b/crates/trc/src/serializers/binary.rs index e092d824..3e384167 100644 --- a/crates/trc/src/serializers/binary.rs +++ b/crates/trc/src/serializers/binary.rs @@ -127,11 +127,6 @@ impl Event { impl Value { fn serialize(&self, buf: &mut Vec) { match self { - Value::Static(v) => { - buf.push(0u8); - leb128_write(buf, v.len() as u64); - buf.extend(v.as_bytes()); - } Value::String(v) => { buf.push(0u8); leb128_write(buf, v.len() as u64); @@ -205,7 +200,7 @@ impl Value { for byte in buf.iter_mut() { *byte = *iter.next()?; } - Some(Value::String(String::from_utf8(buf).ok()?)) + Some(Value::String(CompactString::from_utf8(buf).ok()?)) } 1 => Some(Value::UInt(leb128_read(iter)?)), 2 => { diff --git a/crates/trc/src/serializers/json.rs b/crates/trc/src/serializers/json.rs index c8801704..c5b67ea2 100644 --- a/crates/trc/src/serializers/json.rs +++ b/crates/trc/src/serializers/json.rs @@ -192,7 +192,6 @@ impl Serialize for JsonEventSerializer<&Value> { S: Serializer, { match &self.inner { - Value::Static(value) => value.serialize(serializer), Value::String(value) => value.serialize(serializer), Value::UInt(value) => value.serialize(serializer), Value::Int(value) => value.serialize(serializer), diff --git a/crates/trc/src/serializers/text.rs b/crates/trc/src/serializers/text.rs index 3b587257..1f471496 100644 --- a/crates/trc/src/serializers/text.rs +++ b/crates/trc/src/serializers/text.rs @@ -176,9 +176,6 @@ impl FmtWriter { async fn write_value(&mut self, value: &Value, indent: usize) -> std::io::Result<()> { Box::pin(async move { match value { - Value::Static(v) => { - self.writer.write_all(v.as_bytes()).await?; - } Value::String(v) => { self.writer.write_all("\"".as_bytes()).await?; for ch in v.as_bytes() { @@ -323,7 +320,6 @@ impl Color { impl Display for Value { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Value::Static(value) => value.fmt(f), Value::String(value) => value.fmt(f), Value::UInt(value) => value.fmt(f), Value::Int(value) => value.fmt(f), diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml index 2e840e1b..76071f75 100644 --- a/crates/utils/Cargo.toml +++ b/crates/utils/Cargo.toml @@ -38,6 +38,7 @@ downcast-rs = "2.0.1" fast-float = "0.2.0" erased-serde = "0.4.5" rkyv = { version = "0.8.10", features = ["little_endian"] } +compact_str = "0.9.0" [target.'cfg(unix)'.dependencies] privdrop = "0.5.3" diff --git a/crates/utils/src/config/utils.rs b/crates/utils/src/config/utils.rs index 861fb20f..4ed29c53 100644 --- a/crates/utils/src/config/utils.rs +++ b/crates/utils/src/config/utils.rs @@ -11,6 +11,7 @@ use std::{ time::Duration, }; +use compact_str::CompactString; use mail_auth::{ IpLookupStrategy, common::crypto::{Algorithm, HashAlgorithm}, @@ -638,6 +639,26 @@ impl AsKey for &str { } } +impl AsKey for CompactString { + fn as_key(&self) -> String { + self.to_string() + } + + fn as_prefix(&self) -> String { + format!("{self}.") + } +} + +impl AsKey for &CompactString { + fn as_key(&self) -> String { + self.to_string() + } + + fn as_prefix(&self) -> String { + format!("{self}.") + } +} + impl AsKey for String { fn as_key(&self) -> String { self.to_string() diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 9783e96b..b27a4fcd 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -17,6 +17,7 @@ pub mod snowflake; pub mod topological; pub mod url_params; +use compact_str::CompactString; use futures::StreamExt; use reqwest::Response; use rustls::{ @@ -364,8 +365,8 @@ impl ServerCertVerifier for DummyVerifier { } // Basic email sanitizer -pub fn sanitize_email(email: &str) -> Option { - let mut result = String::with_capacity(email.len()); +pub fn sanitize_email(email: &str) -> Option { + let mut result = CompactString::with_capacity(email.len()); let mut found_local = false; let mut found_domain = false; let mut last_ch = char::from(0); diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 939aafb1..df9337ec 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -5,8 +5,8 @@ edition = "2024" resolver = "2" [features] -default = ["sqlite", "postgres", "mysql", "rocks", "elastic", "s3", "redis", "azure", "foundationdb"] -#default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis"] +#default = ["sqlite", "postgres", "mysql", "rocks", "elastic", "s3", "redis", "azure", "foundationdb"] +default = ["sqlite", "postgres", "mysql", "rocks", "s3", "redis"] #default = ["rocks", "redis", "s3"] sqlite = ["store/sqlite"] foundationdb = ["store/foundation", "common/foundation"] @@ -70,6 +70,7 @@ ring = { version = "0.17" } biscuit = "0.7.0" form_urlencoded = "1.1.0" rkyv = { version = "0.8.10", features = ["little_endian"] } +compact_str = "0.9.0" [target.'cfg(not(target_env = "msvc"))'.dependencies] jemallocator = "0.5.0" diff --git a/tests/src/directory/internal.rs b/tests/src/directory/internal.rs index 8f3eaff9..cb835aa6 100644 --- a/tests/src/directory/internal.rs +++ b/tests/src/directory/internal.rs @@ -6,11 +6,11 @@ use ahash::AHashSet; use directory::{ - Principal, QueryBy, Type, + QueryBy, Type, backend::{ RcptType, internal::{ - PrincipalField, PrincipalUpdate, PrincipalValue, + PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue, lookup::DirectoryStore, manage::{self, ChangedPrincipals, ManageDirectory, UpdatePrincipal}, }, @@ -37,7 +37,7 @@ async fn internal_directory() { // A principal without name should fail assert_eq!( store - .create_principal(Principal::default(), None, None) + .create_principal(PrincipalSet::default(), None, None) .await, Err(manage::err_missing(PrincipalField::Name)) ); @@ -46,9 +46,9 @@ async fn internal_directory() { let john_id = store .create_principal( TestPrincipal { - name: "john".to_string(), - description: Some("John Doe".to_string()), - secrets: vec!["secret".to_string(), "secret2".to_string()], + name: "john".into(), + description: Some("John Doe".into()), + secrets: vec!["secret".into(), "secret2".into()], ..Default::default() } .into(), @@ -64,7 +64,7 @@ async fn internal_directory() { store .create_principal( TestPrincipal { - name: "john".to_string(), + name: "john".into(), ..Default::default() } .into(), @@ -72,7 +72,7 @@ async fn internal_directory() { None ) .await, - Err(manage::err_exists(PrincipalField::Name, "john".to_string())) + Err(manage::err_exists(PrincipalField::Name, "john")) ); // An account using a non-existent domain should fail @@ -80,8 +80,8 @@ async fn internal_directory() { store .create_principal( TestPrincipal { - name: "jane".to_string(), - emails: vec!["jane@example.org".to_string()], + name: "jane".into(), + emails: vec!["jane@example.org".into()], ..Default::default() } .into(), @@ -89,14 +89,14 @@ async fn internal_directory() { None ) .await, - Err(manage::not_found("example.org".to_string())) + Err(manage::not_found("example.org")) ); // Create a domain name store .create_principal( TestPrincipal { - name: "example.org".to_string(), + name: "example.org".into(), typ: Type::Domain, ..Default::default() } @@ -115,7 +115,7 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::Emails, - PrincipalValue::String("john@example.org".to_string()), + PrincipalValue::String("john@example.org".into()), ) ])) .await @@ -136,21 +136,21 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::Emails, - PrincipalValue::String("john@otherdomain.org".to_string()), + PrincipalValue::String("john@otherdomain.org".into()), ) ])) .await, - Err(manage::not_found("otherdomain.org".to_string())) + Err(manage::not_found("otherdomain.org")) ); // Create an account with an email address let jane_id = store .create_principal( TestPrincipal { - name: "jane".to_string(), - description: Some("Jane Doe".to_string()), - secrets: vec!["my_secret".to_string(), "my_secret2".to_string()], - emails: vec!["jane@example.org".to_string()], + name: "jane".into(), + description: Some("Jane Doe".into()), + secrets: vec!["my_secret".into(), "my_secret2".into()], + emails: vec!["jane@example.org".into()], quota: 123, ..Default::default() } @@ -178,10 +178,7 @@ async fn internal_directory() { assert_eq!( store .query( - QueryBy::Credentials(&Credentials::new( - "jane".to_string(), - "my_secret".to_string() - )), + QueryBy::Credentials(&Credentials::new("jane".into(), "my_secret".into())), true ) .await @@ -189,10 +186,10 @@ async fn internal_directory() { .map(|p| p.into_test()), Some(TestPrincipal { id: jane_id, - name: "jane".to_string(), - description: Some("Jane Doe".to_string()), - emails: vec!["jane@example.org".to_string()], - secrets: vec!["my_secret".to_string(), "my_secret2".to_string()], + name: "jane".into(), + description: Some("Jane Doe".into()), + emails: vec!["jane@example.org".into()], + secrets: vec!["my_secret".into(), "my_secret2".into()], quota: 123, ..Default::default() }) @@ -200,10 +197,7 @@ async fn internal_directory() { assert_eq!( store .query( - QueryBy::Credentials(&Credentials::new( - "jane".to_string(), - "wrong_password".to_string() - )), + QueryBy::Credentials(&Credentials::new("jane".into(), "wrong_password".into())), true ) .await @@ -216,9 +210,9 @@ async fn internal_directory() { store .create_principal( TestPrincipal { - name: "janeth".to_string(), - description: Some("Janeth Doe".to_string()), - emails: vec!["jane@example.org".to_string()], + name: "janeth".into(), + description: Some("Janeth Doe".into()), + emails: vec!["jane@example.org".into()], ..Default::default() } .into(), @@ -228,7 +222,7 @@ async fn internal_directory() { .await, Err(manage::err_exists( PrincipalField::Emails, - "jane@example.org".to_string() + "jane@example.org" )) ); @@ -236,9 +230,9 @@ async fn internal_directory() { let list_id = store .create_principal( TestPrincipal { - name: "list".to_string(), + name: "list".into(), typ: Type::List, - emails: vec!["list@example.org".to_string()], + emails: vec!["list@example.org".into()], ..Default::default() } .into(), @@ -253,13 +247,13 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![ PrincipalUpdate::set( PrincipalField::Members, - PrincipalValue::StringList(vec!["john".to_string(), "jane".to_string()]), + PrincipalValue::StringList(vec!["john".into(), "jane".into()]), ), PrincipalUpdate::set( PrincipalField::ExternalMembers, PrincipalValue::StringList(vec![ - "mike@other.org".to_string(), - "lucy@foobar.net".to_string() + "mike@other.org".into(), + "lucy@foobar.net".into() ]), ) ])) @@ -287,10 +281,10 @@ async fn internal_directory() { .unwrap() .into_test(), TestPrincipal { - name: "list".to_string(), + name: "list".into(), id: list_id, typ: Type::List, - emails: vec!["list@example.org".to_string()], + emails: vec!["list@example.org".into()], ..Default::default() } ); @@ -308,7 +302,7 @@ async fn internal_directory() { "jane@example.org" ] .into_iter() - .map(|s| s.to_string()) + .map(|s| s.into()) .collect::>() ); @@ -316,8 +310,8 @@ async fn internal_directory() { store .create_principal( TestPrincipal { - name: "sales".to_string(), - description: Some("Sales Team".to_string()), + name: "sales".into(), + description: Some("Sales Team".into()), typ: Type::Group, ..Default::default() } @@ -330,8 +324,8 @@ async fn internal_directory() { store .create_principal( TestPrincipal { - name: "support".to_string(), - description: Some("Support Team".to_string()), + name: "support".into(), + description: Some("Support Team".into()), typ: Type::Group, ..Default::default() } @@ -348,32 +342,32 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::MemberOf, - PrincipalValue::String("sales".to_string()), + PrincipalValue::String("sales".into()), ), PrincipalUpdate::add_item( PrincipalField::MemberOf, - PrincipalValue::String("support".to_string()), + PrincipalValue::String("support".into()), ) ])) .await .is_ok() ); - let mut principal = store + let principal = store .query(QueryBy::Name("john"), true) .await .unwrap() .unwrap(); - store.map_field_ids(&mut principal, &[]).await.unwrap(); + let principal = store.map_principal(principal, &[]).await.unwrap(); assert_eq!( principal.into_test().into_sorted(), TestPrincipal { id: john_id, - name: "john".to_string(), - description: Some("John Doe".to_string()), - secrets: vec!["secret".to_string(), "secret2".to_string()], - emails: vec!["john@example.org".to_string()], - member_of: vec!["sales".to_string(), "support".to_string()], - lists: vec!["list".to_string()], + name: "john".into(), + description: Some("John Doe".into()), + secrets: vec!["secret".into(), "secret2".into()], + emails: vec!["john@example.org".into()], + member_of: vec!["sales".into(), "support".into()], + lists: vec!["list".into()], ..Default::default() } ); @@ -384,11 +378,11 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::MemberOf, - PrincipalValue::String("accounting".to_string()), + PrincipalValue::String("accounting".into()), ) ])) .await, - Err(manage::not_found("accounting".to_string())) + Err(manage::not_found("accounting")) ); // Remove a member from a group @@ -397,28 +391,28 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::remove_item( PrincipalField::MemberOf, - PrincipalValue::String("support".to_string()), + PrincipalValue::String("support".into()), ) ])) .await .is_ok() ); - let mut principal = store + let principal = store .query(QueryBy::Name("john"), true) .await .unwrap() .unwrap(); - store.map_field_ids(&mut principal, &[]).await.unwrap(); + let principal = store.map_principal(principal, &[]).await.unwrap(); assert_eq!( principal.into_test().into_sorted(), TestPrincipal { id: john_id, - name: "john".to_string(), - description: Some("John Doe".to_string()), - secrets: vec!["secret".to_string(), "secret2".to_string()], - emails: vec!["john@example.org".to_string()], - member_of: vec!["sales".to_string()], - lists: vec!["list".to_string()], + name: "john".into(), + description: Some("John Doe".into()), + secrets: vec!["secret".into(), "secret2".into()], + emails: vec!["john@example.org".into()], + member_of: vec!["sales".into()], + lists: vec!["list".into()], ..Default::default() } ); @@ -429,48 +423,48 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("john").with_updates(vec![ PrincipalUpdate::set( PrincipalField::Name, - PrincipalValue::String("john.doe".to_string()) + PrincipalValue::String("john.doe".into()) ), PrincipalUpdate::set( PrincipalField::Description, - PrincipalValue::String("Johnny Doe".to_string()) + PrincipalValue::String("Johnny Doe".into()) ), PrincipalUpdate::set( PrincipalField::Secrets, - PrincipalValue::StringList(vec!["12345".to_string()]) + PrincipalValue::StringList(vec!["12345".into()]) ), PrincipalUpdate::set(PrincipalField::Quota, PrincipalValue::Integer(1024)), PrincipalUpdate::remove_item( PrincipalField::Emails, - PrincipalValue::String("john@example.org".to_string()), + PrincipalValue::String("john@example.org".into()), ), PrincipalUpdate::add_item( PrincipalField::Emails, - PrincipalValue::String("john.doe@example.org".to_string()), + PrincipalValue::String("john.doe@example.org".into()), ) ])) .await .is_ok() ); - let mut principal = store + let principal = store .query(QueryBy::Name("john.doe"), true) .await .unwrap() .unwrap(); - store.map_field_ids(&mut principal, &[]).await.unwrap(); + let principal = store.map_principal(principal, &[]).await.unwrap(); assert_eq!( principal.into_test().into_sorted(), TestPrincipal { id: john_id, - name: "john.doe".to_string(), - description: Some("Johnny Doe".to_string()), - secrets: vec!["12345".to_string()], - emails: vec!["john.doe@example.org".to_string()], + name: "john.doe".into(), + description: Some("Johnny Doe".into()), + secrets: vec!["12345".into()], + emails: vec!["john.doe@example.org".into()], quota: 1024, typ: Type::Individual, - member_of: vec!["sales".to_string()], - lists: vec!["list".to_string()], + member_of: vec!["sales".into()], + lists: vec!["list".into()], ..Default::default() } ); @@ -490,7 +484,7 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![ PrincipalUpdate::remove_item( PrincipalField::Members, - PrincipalValue::String("john.doe".to_string()), + PrincipalValue::String("john.doe".into()), ) ])) .await @@ -507,7 +501,7 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("list").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::Members, - PrincipalValue::String("john.doe".to_string()), + PrincipalValue::String("john.doe".into()), ) ])) .await @@ -531,24 +525,24 @@ async fn internal_directory() { .update_principal(UpdatePrincipal::by_name("john.doe").with_updates(vec![ PrincipalUpdate::set( PrincipalField::Name, - PrincipalValue::String("jane".to_string()) + PrincipalValue::String("jane".into()) ), ])) .await, - Err(manage::err_exists(PrincipalField::Name, "jane".to_string())) + Err(manage::err_exists(PrincipalField::Name, "jane")) ); assert_eq!( store .update_principal(UpdatePrincipal::by_name("john.doe").with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::Emails, - PrincipalValue::String("jane@example.org".to_string()) + PrincipalValue::String("jane@example.org".into()) ), ])) .await, Err(manage::err_exists( PrincipalField::Emails, - "jane@example.org".to_string() + "jane@example.org" )) ); @@ -559,7 +553,7 @@ async fn internal_directory() { None, None, &[Type::Individual, Type::Group, Type::List], - &[], + true, 0, 0 ) @@ -567,60 +561,60 @@ async fn internal_directory() { .unwrap() .items .into_iter() - .map(|p| p.name().to_string()) + .map(|p| p.name) .collect::>(), ["jane", "john.doe", "list", "sales", "support"] .into_iter() - .map(|s| s.to_string()) + .map(|s| s.into()) .collect::>() ); assert_eq!( store - .list_principals("john".into(), None, &[], &[], 0, 0) + .list_principals("john".into(), None, &[], true, 0, 0) .await .unwrap() .items .into_iter() - .map(|p| p.name().to_string()) + .map(|p| p.name) .collect::>(), vec!["john.doe"] ); assert_eq!( store - .list_principals(None, None, &[Type::Individual], &[], 0, 0) + .list_principals(None, None, &[Type::Individual], true, 0, 0) .await .unwrap() .items .into_iter() - .map(|p| p.name().to_string()) + .map(|p| p.name) .collect::>(), ["jane", "john.doe"] .into_iter() - .map(|s| s.to_string()) + .map(|s| s.into()) .collect::>() ); assert_eq!( store - .list_principals(None, None, &[Type::Group], &[], 0, 0) + .list_principals(None, None, &[Type::Group], true, 0, 0) .await .unwrap() .items .into_iter() - .map(|p| p.name().to_string()) + .map(|p| p.name) .collect::>(), ["sales", "support"] .into_iter() - .map(|s| s.to_string()) + .map(|s| s.into()) .collect::>() ); assert_eq!( store - .list_principals(None, None, &[Type::List], &[], 0, 0) + .list_principals(None, None, &[Type::List], true, 0, 0) .await .unwrap() .items .into_iter() - .map(|p| p.name().to_string()) + .map(|p| p.name) .collect::>(), vec!["list"] ); @@ -653,7 +647,7 @@ async fn internal_directory() { }) .await .unwrap(), - Some("hello".to_string()) + Some("hello".into()) ); } @@ -674,7 +668,7 @@ async fn internal_directory() { None, None, &[Type::Individual, Type::Group, Type::List], - &[], + true, 0, 0 ) @@ -682,11 +676,11 @@ async fn internal_directory() { .unwrap() .items .into_iter() - .map(|p| p.name().to_string()) + .map(|p| p.name) .collect::>(), ["jane", "list", "sales", "support"] .into_iter() - .map(|s| s.to_string()) + .map(|s| s.into()) .collect::>() ); assert_eq!( @@ -746,7 +740,7 @@ async fn internal_directory() { }) .await .unwrap(), - Some("hello".to_string()) + Some("hello".into()) ); } } @@ -778,19 +772,19 @@ impl TestInternalDirectory for Store { self.update_principal(UpdatePrincipal::by_id(principal.id()).with_updates(vec![ PrincipalUpdate::set( PrincipalField::Secrets, - PrincipalValue::StringList(vec![secret.to_string()]), + PrincipalValue::StringList(vec![secret.into()]), ), PrincipalUpdate::set( PrincipalField::Description, - PrincipalValue::String(name.to_string()), + PrincipalValue::String(name.into()), ), PrincipalUpdate::set( PrincipalField::Emails, - PrincipalValue::StringList(emails.iter().map(|s| s.to_string()).collect()), + PrincipalValue::StringList(emails.iter().map(|s| (*s).into()).collect()), ), PrincipalUpdate::add_item( PrincipalField::Roles, - PrincipalValue::String(role.to_string()), + PrincipalValue::String(role.into()), ), ])) .await @@ -798,20 +792,20 @@ impl TestInternalDirectory for Store { principal.id() } else { self.create_principal( - Principal::new(0, Type::Individual) - .with_field(PrincipalField::Name, login.to_string()) - .with_field(PrincipalField::Description, name.to_string()) + PrincipalSet::new(0, Type::Individual) + .with_field(PrincipalField::Name, login) + .with_field(PrincipalField::Description, name) .with_field( PrincipalField::Secrets, - PrincipalValue::StringList(vec![secret.to_string()]), + PrincipalValue::StringList(vec![secret.into()]), ) .with_field( PrincipalField::Emails, - PrincipalValue::StringList(emails.iter().map(|s| s.to_string()).collect()), + PrincipalValue::StringList(emails.iter().map(|s| (*s).into()).collect()), ) .with_field( PrincipalField::Roles, - PrincipalValue::StringList(vec![role.to_string()]), + PrincipalValue::StringList(vec![role.into()]), ), None, None, @@ -828,16 +822,16 @@ impl TestInternalDirectory for Store { principal.id() } else { self.create_principal( - Principal::new(0, Type::Group) - .with_field(PrincipalField::Name, login.to_string()) - .with_field(PrincipalField::Description, name.to_string()) + PrincipalSet::new(0, Type::Group) + .with_field(PrincipalField::Name, login) + .with_field(PrincipalField::Description, name) .with_field( PrincipalField::Emails, - PrincipalValue::StringList(emails.iter().map(|s| s.to_string()).collect()), + PrincipalValue::StringList(emails.iter().map(|s| (*s).into()).collect()), ) .with_field( PrincipalField::Roles, - PrincipalValue::StringList(vec!["user".to_string()]), + PrincipalValue::StringList(vec!["user".into()]), ), None, None, @@ -854,16 +848,16 @@ impl TestInternalDirectory for Store { } else { self.create_test_domains(&[login]).await; self.create_principal( - Principal::new(0, Type::List) - .with_field(PrincipalField::Name, login.to_string()) - .with_field(PrincipalField::Description, name.to_string()) + PrincipalSet::new(0, Type::List) + .with_field(PrincipalField::Name, login) + .with_field(PrincipalField::Description, name) .with_field( PrincipalField::Members, - PrincipalValue::StringList(members.iter().map(|s| s.to_string()).collect()), + PrincipalValue::StringList(members.iter().map(|s| (*s).into()).collect()), ) .with_field( PrincipalField::Emails, - PrincipalValue::StringList(vec![login.to_string()]), + PrincipalValue::StringList(vec![login.into()]), ), None, None, @@ -886,7 +880,7 @@ impl TestInternalDirectory for Store { self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![ PrincipalUpdate::add_item( PrincipalField::MemberOf, - PrincipalValue::String(group.to_string()), + PrincipalValue::String(group.into()), ), ])) .await @@ -897,7 +891,7 @@ impl TestInternalDirectory for Store { self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![ PrincipalUpdate::remove_item( PrincipalField::MemberOf, - PrincipalValue::String(group.to_string()), + PrincipalValue::String(group.into()), ), ])) .await @@ -908,7 +902,7 @@ impl TestInternalDirectory for Store { self.update_principal(UpdatePrincipal::by_name(login).with_updates(vec![ PrincipalUpdate::remove_item( PrincipalField::Emails, - PrincipalValue::String(alias.to_string()), + PrincipalValue::String(alias.into()), ), ])) .await @@ -925,8 +919,7 @@ impl TestInternalDirectory for Store { .is_none() { self.create_principal( - Principal::new(0, Type::Domain) - .with_field(PrincipalField::Name, domain.to_string()), + PrincipalSet::new(0, Type::Domain).with_field(PrincipalField::Name, domain), None, None, ) @@ -948,7 +941,7 @@ async fn assert_list_members( items.into_iter().collect::>(), members .into_iter() - .map(|s| s.to_string()) + .map(|s| s.into()) .collect::>() ); } diff --git a/tests/src/directory/ldap.rs b/tests/src/directory/ldap.rs index 997a925a..7d466a9f 100644 --- a/tests/src/directory/ldap.rs +++ b/tests/src/directory/ldap.rs @@ -6,6 +6,7 @@ use std::fmt::Debug; +use compact_str::{CompactString, ToCompactString}; use directory::{ QueryBy, ROLE_USER, Type, backend::{RcptType, internal::manage::ManageDirectory}, @@ -37,8 +38,8 @@ async fn ldap_directory() { handle .query( QueryBy::Credentials(&Credentials::Plain { - username: "john".to_string(), - secret: "12345".to_string() + username: "john".into(), + secret: "12345".into() }), true ) @@ -49,20 +50,17 @@ async fn ldap_directory() { .into_sorted(), TestPrincipal { id: base_store.get_principal_id("john").await.unwrap().unwrap(), - name: "john".to_string(), - description: "John Doe".to_string().into(), - secrets: vec!["12345".to_string()], + name: "john".into(), + description: Some("John Doe".into()), + secrets: vec!["12345".into()], typ: Type::Individual, member_of: map_account_ids(base_store, vec!["sales"]) .await .into_iter() - .map(|v| v.to_string()) + .map(|v| v.to_compact_string()) .collect(), - emails: vec![ - "john@example.org".to_string(), - "john.doe@example.org".to_string() - ], - roles: vec![ROLE_USER.to_string()], + emails: vec!["john@example.org".into(), "john.doe@example.org".into()], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } .into_sorted() @@ -71,8 +69,8 @@ async fn ldap_directory() { handle .query( QueryBy::Credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "password".to_string() + username: "bill".into(), + secret: "password".into() }), true ) @@ -83,15 +81,13 @@ async fn ldap_directory() { .into_sorted(), TestPrincipal { id: base_store.get_principal_id("bill").await.unwrap().unwrap(), - name: "bill".to_string(), - description: "Bill Foobar".to_string().into(), - secrets: vec![ - "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".to_string() - ], + name: "bill".into(), + description: Some("Bill Foobar".into()), + secrets: vec!["$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".into()], typ: Type::Individual, quota: 500000, - emails: vec!["bill@example.org".to_string(),], - roles: vec![ROLE_USER.to_string()], + emails: vec!["bill@example.org".into(),], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } .into_sorted() @@ -100,8 +96,8 @@ async fn ldap_directory() { handle .query( QueryBy::Credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "invalid".to_string() + username: "bill".into(), + secret: "invalid".into() }), true ) @@ -121,17 +117,17 @@ async fn ldap_directory() { .into_sorted(), TestPrincipal { id: base_store.get_principal_id("jane").await.unwrap().unwrap(), - name: "jane".to_string(), - description: "Jane Doe".to_string().into(), + name: "jane".into(), + description: Some("Jane Doe".into()), typ: Type::Individual, - secrets: vec!["abcde".to_string()], + secrets: vec!["abcde".into()], member_of: map_account_ids(base_store, vec!["sales", "support"]) .await .into_iter() - .map(|v| v.to_string()) + .map(|v| v.to_compact_string()) .collect(), - emails: vec!["jane@example.org".to_string(),], - roles: vec![ROLE_USER.to_string()], + emails: vec!["jane@example.org".into(),], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } .into_sorted() @@ -147,10 +143,10 @@ async fn ldap_directory() { .into_test(), TestPrincipal { id: base_store.get_principal_id("sales").await.unwrap().unwrap(), - name: "sales".to_string(), - description: "sales".to_string().into(), + name: "sales".into(), + description: Some("sales".into()), typ: Type::Group, - roles: vec![ROLE_USER.to_string()], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } ); @@ -220,26 +216,23 @@ async fn ldap_directory() { // VRFY compare_sorted( core.vrfy(&handle, "jane", 0).await.unwrap(), - vec!["jane@example.org".to_string()], + vec!["jane@example.org".into()], ); compare_sorted( core.vrfy(&handle, "john", 0).await.unwrap(), - vec![ - "john@example.org".to_string(), - "john.doe@example.org".to_string(), - ], + vec!["john@example.org".into(), "john.doe@example.org".into()], ); compare_sorted( core.vrfy(&handle, "jane+alias@example", 0).await.unwrap(), - vec!["jane@example.org".to_string()], + vec!["jane@example.org".into()], ); compare_sorted( core.vrfy(&handle, "info", 0).await.unwrap(), - Vec::::new(), + Vec::::new(), ); compare_sorted( core.vrfy(&handle, "invalid", 0).await.unwrap(), - Vec::::new(), + Vec::::new(), ); // EXPN @@ -247,9 +240,9 @@ async fn ldap_directory() { /*compare_sorted( core.expn(&handle, "info@example.org", 0).await.unwrap(), vec![ - "bill@example.org".to_string(), - "jane@example.org".to_string(), - "john@example.org".to_string(), + "bill@example.org".into(), + "jane@example.org".into(), + "john@example.org".into(), ], ); compare_sorted( diff --git a/tests/src/directory/mod.rs b/tests/src/directory/mod.rs index a7dfd547..a8557ccc 100644 --- a/tests/src/directory/mod.rs +++ b/tests/src/directory/mod.rs @@ -12,9 +12,10 @@ pub mod smtp; pub mod sql; use common::{Core, Server, config::smtp::session::AddressMapping}; +use compact_str::{CompactString, ToCompactString, format_compact}; use directory::{ Directories, Principal, Type, - backend::internal::{PrincipalField, manage::ManageDirectory}, + backend::internal::{PrincipalField, PrincipalSet, manage::ManageDirectory}, }; use mail_send::Credentials; use rustls::ServerConfig; @@ -322,13 +323,13 @@ pub struct TestPrincipal { pub id: u32, pub typ: Type, pub quota: u64, - pub name: String, - pub secrets: Vec, - pub emails: Vec, - pub member_of: Vec, - pub roles: Vec, - pub lists: Vec, - pub description: Option, + pub name: CompactString, + pub secrets: Vec, + pub emails: Vec, + pub member_of: Vec, + pub roles: Vec, + pub lists: Vec, + pub description: Option, } impl DirectoryTest { @@ -493,6 +494,12 @@ trait IntoTestPrincipal { fn into_test(self) -> TestPrincipal; } +impl IntoTestPrincipal for PrincipalSet { + fn into_test(self) -> TestPrincipal { + TestPrincipal::from(self) + } +} + impl IntoTestPrincipal for Principal { fn into_test(self) -> TestPrincipal { TestPrincipal::from(self) @@ -507,8 +514,8 @@ impl TestPrincipal { } } -impl From for TestPrincipal { - fn from(mut value: Principal) -> Self { +impl From for TestPrincipal { + fn from(mut value: PrincipalSet) -> Self { Self { id: value.id(), typ: value.typ(), @@ -534,9 +541,38 @@ impl From for TestPrincipal { } } -impl From for Principal { +impl From for TestPrincipal { + fn from(value: Principal) -> Self { + Self { + id: value.id(), + typ: value.typ(), + quota: value.quota(), + member_of: value + .member_of() + .iter() + .map(|v| v.to_compact_string()) + .collect(), + roles: value + .roles() + .iter() + .map(|v| v.to_compact_string()) + .collect(), + lists: value + .lists() + .iter() + .map(|v| v.to_compact_string()) + .collect(), + name: value.name, + secrets: value.secrets, + emails: value.emails, + description: value.description, + } + } +} + +impl From for PrincipalSet { fn from(value: TestPrincipal) -> Self { - Principal::new(value.id, value.typ) + PrincipalSet::new(value.id, value.typ) .with_field(PrincipalField::Name, value.name) .with_field(PrincipalField::Quota, value.quota) .with_field(PrincipalField::Secrets, value.secrets) @@ -549,23 +585,23 @@ impl From for Principal { #[derive(Clone, PartialEq, Eq, Hash)] pub enum Item { - IsAccount(String), + IsAccount(CompactString), Authenticate(Credentials), - Verify(String), - Expand(String), + Verify(CompactString), + Expand(CompactString), } #[derive(Debug, Clone, PartialEq, Eq)] pub enum LookupResult { True, False, - Values(Vec), + Values(Vec), } impl Item { pub fn append(&self, append: usize) -> Self { match self { - Item::IsAccount(str) => Item::IsAccount(format!("{append}{str}")), + Item::IsAccount(str) => Item::IsAccount(format_compact!("{append}{str}")), Item::Authenticate(str) => Item::Authenticate(match str { Credentials::Plain { username, secret } => Credentials::Plain { username: username.to_string(), @@ -579,8 +615,8 @@ impl Item { secret: format!("{append}{secret}"), }, }), - Item::Verify(str) => Item::Verify(format!("{append}{str}")), - Item::Expand(str) => Item::Expand(format!("{append}{str}")), + Item::Verify(str) => Item::Verify(format_compact!("{append}{str}")), + Item::Expand(str) => Item::Expand(format_compact!("{append}{str}")), } } @@ -601,9 +637,9 @@ impl LookupResult { let mut r = Vec::with_capacity(v.len()); for (pos, val) in v.iter().enumerate() { r.push(if pos == 0 { - format!("{append}{val}") + format_compact!("{append}{val}") } else { - val.to_string() + val.to_compact_string() }); } LookupResult::Values(r) @@ -622,8 +658,8 @@ impl From for LookupResult { } } -impl From> for LookupResult { - fn from(v: Vec) -> Self { +impl From> for LookupResult { + fn from(v: Vec) -> Self { LookupResult::Values(v) } } diff --git a/tests/src/directory/oidc.rs b/tests/src/directory/oidc.rs index 77d651e2..bca99297 100644 --- a/tests/src/directory/oidc.rs +++ b/tests/src/directory/oidc.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use base64::{Engine, engine::general_purpose}; -use directory::{QueryBy, backend::internal::PrincipalField}; +use directory::QueryBy; use http_proto::{JsonProblemResponse, JsonResponse, ToHttpResponse}; use hyper::{Method, StatusCode}; use mail_send::Credentials; @@ -132,7 +132,7 @@ async fn oidc_directory() { .unwrap(); assert_eq!(principal.name(), "jdoe"); assert_eq!( - principal.get_str(PrincipalField::Emails), + principal.emails.first().map(|s| s.as_str()), Some("john@example.org") ); assert_eq!(principal.description(), Some("John Doe")); diff --git a/tests/src/directory/smtp.rs b/tests/src/directory/smtp.rs index 3f74b132..483400c3 100644 --- a/tests/src/directory/smtp.rs +++ b/tests/src/directory/smtp.rs @@ -7,6 +7,7 @@ use std::sync::Arc; use common::listener::limiter::{ConcurrencyLimiter, InFlight}; +use compact_str::ToCompactString; use directory::{QueryBy, backend::RcptType}; use mail_parser::decoders::base64::base64_decode; use mail_send::Credentials; @@ -34,43 +35,40 @@ async fn lmtp_directory() { // Basic lookup let tests = vec![ + (Item::IsAccount("john-ok@domain".into()), LookupResult::True), ( - Item::IsAccount("john-ok@domain".to_string()), - LookupResult::True, - ), - ( - Item::IsAccount("john-bad@domain".to_string()), + Item::IsAccount("john-bad@domain".into()), LookupResult::False, ), ( - Item::Verify("john-ok@domain".to_string()), - LookupResult::Values(vec!["john-ok@domain".to_string()]), + Item::Verify("john-ok@domain".into()), + LookupResult::Values(vec!["john-ok@domain".into()]), ), ( - Item::Verify("doesnot@exist.org".to_string()), + Item::Verify("doesnot@exist.org".into()), LookupResult::False, ), ( - Item::Expand("sales-ok,item1,item2,item3".to_string()), + Item::Expand("sales-ok,item1,item2,item3".into()), LookupResult::Values(vec![ - "sales-ok".to_string(), - "item1".to_string(), - "item2".to_string(), - "item3".to_string(), + "sales-ok".into(), + "item1".into(), + "item2".into(), + "item3".into(), ]), ), - (Item::Expand("other".to_string()), LookupResult::False), + (Item::Expand("other".into()), LookupResult::False), ( Item::Authenticate(Credentials::Plain { - username: "john".to_string(), - secret: "ok".to_string(), + username: "john".into(), + secret: "ok".into(), }), LookupResult::True, ), ( Item::Authenticate(Credentials::Plain { - username: "john".to_string(), - secret: "bad".to_string(), + username: "john".into(), + secret: "bad".into(), }), LookupResult::False, ), @@ -282,24 +280,24 @@ async fn accept_smtp( let buf = std::str::from_utf8(&buf_u8[0..br]).unwrap(); let response = if buf.starts_with("LHLO") { - "250-mx.foobar.org\r\n250 AUTH PLAIN\r\n".to_string() + "250-mx.foobar.org\r\n250 AUTH PLAIN\r\n".into() } else if buf.starts_with("MAIL FROM") { if buf.contains("<>") || buf.contains("ok@") { - "250 OK\r\n".to_string() + "250 OK\r\n".into() } else { - "552-I do not\r\n552 like that MAIL FROM.\r\n".to_string() + "552-I do not\r\n552 like that MAIL FROM.\r\n".into() } } else if buf.starts_with("RCPT TO") { if buf.contains("ok") { - "250 OK\r\n".to_string() + "250 OK\r\n".into() } else { - "550-I refuse to\r\n550 accept that recipient.\r\n".to_string() + "550-I refuse to\r\n550 accept that recipient.\r\n".into() } } else if buf.starts_with("VRFY") { if buf.contains("ok") { format!("250 {}\r\n", buf.split_once(' ').unwrap().1) } else { - "550-I refuse to\r\n550 verify that recipient.\r\n".to_string() + "550-I refuse to\r\n550 verify that recipient.\r\n".into() } } else if buf.starts_with("EXPN") { if buf.contains("ok") { @@ -310,7 +308,7 @@ async fn accept_smtp( .split(',') .filter_map(|s| { if !s.is_empty() { - s.to_string().into() + s.to_compact_string().into() } else { None } @@ -326,21 +324,21 @@ async fn accept_smtp( buf } else { - "550-I refuse to\r\n550 accept that recipient.\r\n".to_string() + "550-I refuse to\r\n550 accept that recipient.\r\n".into() } } else if buf.starts_with("AUTH PLAIN") { let buf = base64_decode(buf.rsplit_once(' ').unwrap().1.as_bytes()).unwrap(); if String::from_utf8_lossy(&buf).contains("ok") { - "235 Great success!\r\n".to_string() + "235 Great success!\r\n".into() } else { - "535 No soup for you\r\n".to_string() + "535 No soup for you\r\n".into() } } else if buf.starts_with("NOOP") { - "250 Siesta time\r\n".to_string() + "250 Siesta time\r\n".into() } else if buf.starts_with("QUIT") { - "250 Arrivederci!\r\n".to_string() + "250 Arrivederci!\r\n".into() } else if buf.starts_with("RSET") { - "250 Your wish is my command.\r\n".to_string() + "250 Your wish is my command.\r\n".into() } else { panic!("Unknown command: {}", buf.trim()); }; diff --git a/tests/src/directory/sql.rs b/tests/src/directory/sql.rs index f8aafee3..45ed551a 100644 --- a/tests/src/directory/sql.rs +++ b/tests/src/directory/sql.rs @@ -4,6 +4,7 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ +use compact_str::{CompactString, ToCompactString}; use directory::{ QueryBy, ROLE_USER, Type, backend::{RcptType, internal::manage::ManageDirectory}, @@ -114,8 +115,8 @@ async fn sql_directory() { handle .query( QueryBy::Credentials(&Credentials::Plain { - username: "john".to_string(), - secret: "12345".to_string() + username: "john".into(), + secret: "12345".into() }), true ) @@ -125,21 +126,21 @@ async fn sql_directory() { .into_test(), TestPrincipal { id: base_store.get_principal_id("john").await.unwrap().unwrap(), - name: "john".to_string(), - description: "John Doe".to_string().into(), - secrets: vec!["12345".to_string()], + name: "john".into(), + description: Some("John Doe".into()), + secrets: vec!["12345".into()], typ: Type::Individual, member_of: map_account_ids(base_store, vec!["sales"]) .await .into_iter() - .map(|v| v.to_string()) + .map(|v| v.to_compact_string()) .collect(), emails: vec![ - "john@example.org".to_string(), - "jdoe@example.org".to_string(), - "john.doe@example.org".to_string() + "john@example.org".into(), + "jdoe@example.org".into(), + "john.doe@example.org".into() ], - roles: vec![ROLE_USER.to_string()], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } ); @@ -147,8 +148,8 @@ async fn sql_directory() { handle .query( QueryBy::Credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "password".to_string() + username: "bill".into(), + secret: "password".into() }), true ) @@ -158,15 +159,15 @@ async fn sql_directory() { .into_test(), TestPrincipal { id: base_store.get_principal_id("bill").await.unwrap().unwrap(), - name: "bill".to_string(), - description: "Bill Foobar".to_string().into(), + name: "bill".into(), + description: Some("Bill Foobar".into()), secrets: vec![ - "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".to_string() + "$2y$05$bvIG6Nmid91Mu9RcmmWZfO5HJIMCT8riNW0hEp8f6/FuA2/mHZFpe".into() ], typ: Type::Individual, quota: 500000, - emails: vec!["bill@example.org".to_string(),], - roles: vec![ROLE_USER.to_string()], + emails: vec!["bill@example.org".into(),], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } ); @@ -174,8 +175,8 @@ async fn sql_directory() { handle .query( QueryBy::Credentials(&Credentials::Plain { - username: "admin".to_string(), - secret: "very_secret".to_string() + username: "admin".into(), + secret: "very_secret".into() }), true ) @@ -185,11 +186,11 @@ async fn sql_directory() { .into_test(), TestPrincipal { id: base_store.get_principal_id("admin").await.unwrap().unwrap(), - name: "admin".to_string(), - description: "Administrator".to_string().into(), - secrets: vec!["very_secret".to_string()], + name: "admin".into(), + description: Some("Administrator".into()), + secrets: vec!["very_secret".into()], typ: Type::Individual, - roles: vec![ROLE_USER.to_string()], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } ); @@ -197,8 +198,8 @@ async fn sql_directory() { handle .query( QueryBy::Credentials(&Credentials::Plain { - username: "bill".to_string(), - secret: "invalid".to_string() + username: "bill".into(), + secret: "invalid".into() }), true ) @@ -217,17 +218,17 @@ async fn sql_directory() { .into_test(), TestPrincipal { id: base_store.get_principal_id("jane").await.unwrap().unwrap(), - name: "jane".to_string(), - description: "Jane Doe".to_string().into(), + name: "jane".into(), + description: Some("Jane Doe".into()), typ: Type::Individual, - secrets: vec!["abcde".to_string()], + secrets: vec!["abcde".into()], member_of: map_account_ids(base_store, vec!["sales", "support"]) .await .into_iter() - .map(|v| v.to_string()) + .map(|v| v.to_compact_string()) .collect(), - emails: vec!["jane@example.org".to_string(),], - roles: vec![ROLE_USER.to_string()], + emails: vec!["jane@example.org".into(),], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } ); @@ -242,10 +243,10 @@ async fn sql_directory() { .into_test(), TestPrincipal { id: base_store.get_principal_id("sales").await.unwrap().unwrap(), - name: "sales".to_string(), - description: "Sales Team".to_string().into(), + name: "sales".into(), + description: Some("Sales Team".into()), typ: Type::Group, - roles: vec![ROLE_USER.to_string()], + roles: vec![ROLE_USER.to_compact_string()], ..Default::default() } ); @@ -315,35 +316,35 @@ async fn sql_directory() { // VRFY assert_eq!( core.vrfy(&handle, "jane", 0).await.unwrap(), - vec!["jane@example.org".to_string()] + vec!["jane@example.org".to_compact_string()] ); assert_eq!( core.vrfy(&handle, "john", 0).await.unwrap(), vec![ - "john.doe@example.org".to_string(), - "john@example.org".to_string(), + "john.doe@example.org".to_compact_string(), + "john@example.org".to_compact_string(), ] ); assert_eq!( core.vrfy(&handle, "jane+alias@example", 0).await.unwrap(), - vec!["jane@example.org".to_string()] + vec!["jane@example.org".to_compact_string()] ); assert_eq!( core.vrfy(&handle, "info", 0).await.unwrap(), - Vec::::new() + Vec::::new() ); assert_eq!( core.vrfy(&handle, "invalid", 0).await.unwrap(), - Vec::::new() + Vec::::new() ); // EXPN (now handled by the internal store) /*assert_eq!( core.expn(&handle, "info@example.org", 0).await.unwrap(), vec![ - "bill@example.org".to_string(), - "jane@example.org".to_string(), - "john@example.org".to_string() + "bill@example.org".into(), + "jane@example.org".into(), + "john@example.org".into() ] ); assert_eq!( @@ -381,7 +382,7 @@ impl DirectoryStore { let query = if self.is_mysql() { query.replace("TEXT", "VARCHAR(255)") } else { - query.to_string() + query.into() }; self.store diff --git a/tests/src/jmap/auth_oauth.rs b/tests/src/jmap/auth_oauth.rs index c15b5ce0..b73119b5 100644 --- a/tests/src/jmap/auth_oauth.rs +++ b/tests/src/jmap/auth_oauth.rs @@ -14,6 +14,7 @@ use common::auth::oauth::{ oidc::StandardClaims, registration::{ClientRegistrationRequest, ClientRegistrationResponse}, }; +use compact_str::ToCompactString; use http::auth::oauth::{ DeviceAuthResponse, ErrorType, OAuthCodeRequest, TokenResponse, auth::OAuthMetadata, openid::OpenIdMetadata, @@ -81,7 +82,7 @@ pub async fn test(params: &mut JMAPTest) { &metadata.registration_endpoint, None, &ClientRegistrationRequest { - redirect_uris: vec!["https://localhost".to_string()], + redirect_uris: vec!["https://localhost".to_compact_string()], ..Default::default() }, ) @@ -164,18 +165,18 @@ pub async fn test(params: &mut JMAPTest) { let claims = id_token.payload().unwrap(); let registered_claims = &claims.registered; let private_claims = &claims.private; - assert_eq!(registered_claims.issuer, Some(oidc_metadata.issuer)); + assert_eq!(registered_claims.issuer, Some(oidc_metadata.issuer.into())); assert_eq!(registered_claims.subject, Some(john_int_id.to_string())); assert_eq!( registered_claims.audience, Some(SingleOrMultiple::Single(client_id.to_string())) ); - assert_eq!(private_claims.nonce, Some("abc1234".to_string())); + assert_eq!(private_claims.nonce, Some("abc1234".into())); assert_eq!( private_claims.preferred_username, - Some("jdoe@example.com".to_string()) + Some("jdoe@example.com".into()) ); - assert_eq!(private_claims.email, Some("jdoe@example.com".to_string())); + assert_eq!(private_claims.email, Some("jdoe@example.com".into())); // Introspect token let access_introspect: OAuthIntrospect = post_with_auth::( diff --git a/tests/src/jmap/mod.rs b/tests/src/jmap/mod.rs index 7f1e7569..13953a84 100644 --- a/tests/src/jmap/mod.rs +++ b/tests/src/jmap/mod.rs @@ -363,8 +363,19 @@ stop; ''' "#; -#[tokio::test(flavor = "multi_thread")] -pub async fn jmap_tests() { +#[test] +fn jmap_tests() { + tokio::runtime::Builder::new_multi_thread() + .thread_stack_size(3 * 1024 * 1024) + .enable_all() + .build() + .unwrap() + .block_on(async { + jmap_tests_().await; + }) +} + +async fn jmap_tests_() { let delete = true; let mut params = init_jmap_tests( &std::env::var("STORE") @@ -373,8 +384,8 @@ pub async fn jmap_tests() { ) .await; - /*webhooks::test(&mut params).await; - email_query::test(&mut params, delete).await; + webhooks::test(&mut params).await; + /*email_query::test(&mut params, delete).await; email_get::test(&mut params).await; email_set::test(&mut params).await; email_parse::test(&mut params).await; @@ -382,7 +393,7 @@ pub async fn jmap_tests() { email_changes::test(&mut params).await; email_query_changes::test(&mut params).await; email_copy::test(&mut params).await; - thread_get::test(&mut params).await;*/ + thread_get::test(&mut params).await; thread_merge::test(&mut params).await; mailbox::test(&mut params).await; delivery::test(&mut params).await; @@ -392,7 +403,7 @@ pub async fn jmap_tests() { event_source::test(&mut params).await; push_subscription::test(&mut params).await; sieve_script::test(&mut params).await; - vacation_response::test(&mut params).await; + vacation_response::test(&mut params).await;*/ email_submission::test(&mut params).await; websocket::test(&mut params).await; quota::test(&mut params).await; diff --git a/tests/src/jmap/permissions.rs b/tests/src/jmap/permissions.rs index 7c4c16e2..7636888b 100644 --- a/tests/src/jmap/permissions.rs +++ b/tests/src/jmap/permissions.rs @@ -8,9 +8,10 @@ use std::sync::Arc; use ahash::AHashSet; use common::auth::{AccessToken, TenantInfo}; +use compact_str::ToCompactString; use directory::{ - Permission, Principal, Type, - backend::internal::{PrincipalField, PrincipalUpdate, PrincipalValue}, + Permission, Type, + backend::internal::{PrincipalField, PrincipalSet, PrincipalUpdate, PrincipalValue}, }; use email::message::delivery::{IngestMessage, LocalDeliveryStatus, MailDelivery}; use utils::BlobHash; @@ -30,12 +31,12 @@ pub async fn test(params: &JMAPTest) { let account_id = api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Individual) + &PrincipalSet::new(u32::MAX, Type::Individual) .with_field(PrincipalField::Name, "role_player") - .with_field(PrincipalField::Roles, vec!["user".to_string()]) + .with_field(PrincipalField::Roles, vec!["user".to_compact_string()]) .with_field( PrincipalField::DisabledPermissions, - vec![Permission::Pop3Dele.name().to_string()], + vec![Permission::Pop3Dele.name().to_compact_string()], ), ) .await @@ -79,20 +80,20 @@ pub async fn test(params: &JMAPTest) { ] { api.post::( "/api/principal", - &Principal::new(u32::MAX, Type::Role) - .with_field(PrincipalField::Name, role.to_string()) + &PrincipalSet::new(u32::MAX, Type::Role) + .with_field(PrincipalField::Name, role.to_compact_string()) .with_field( PrincipalField::EnabledPermissions, permissions .iter() - .map(|p| p.name().to_string()) + .map(|p| p.name().to_compact_string()) .collect::>(), ) .with_field( PrincipalField::Roles, parent_role .iter() - .map(|r| r.to_string()) + .map(|r| r.to_compact_string()) .collect::>(), ), ) @@ -106,7 +107,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/email_user", &vec![PrincipalUpdate::add_item( PrincipalField::DisabledPermissions, - PrincipalValue::String(Permission::ManageEncryption.name().to_string()), + PrincipalValue::String(Permission::ManageEncryption.name().to_compact_string()), )], ) .await @@ -118,7 +119,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/role_player", &vec![PrincipalUpdate::set( PrincipalField::Roles, - PrincipalValue::StringList(vec!["email_user".to_string()]), + PrincipalValue::StringList(vec!["email_user".to_compact_string()]), )], ) .await @@ -141,7 +142,7 @@ pub async fn test(params: &JMAPTest) { .validate_revision(1); // Query all principals - api.get::>("/api/principal") + api.get::>("/api/principal") .await .unwrap() .unwrap_data() @@ -245,11 +246,14 @@ pub async fn test(params: &JMAPTest) { let tenant_id = api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Tenant) + &PrincipalSet::new(u32::MAX, Type::Tenant) .with_field(PrincipalField::Name, "foobar") .with_field( PrincipalField::Roles, - vec!["tenant-admin".to_string(), "user".to_string()], + vec![ + "tenant-admin".to_compact_string(), + "user".to_compact_string(), + ], ) .with_field( PrincipalField::Quota, @@ -262,11 +266,14 @@ pub async fn test(params: &JMAPTest) { let other_tenant_id = api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Tenant) + &PrincipalSet::new(u32::MAX, Type::Tenant) .with_field(PrincipalField::Name, "xanadu") .with_field( PrincipalField::Roles, - vec!["tenant-admin".to_string(), "user".to_string()], + vec![ + "tenant-admin".to_compact_string(), + "user".to_compact_string(), + ], ), ) .await @@ -276,16 +283,19 @@ pub async fn test(params: &JMAPTest) { // Creating a tenant without a valid domain should fail api.post::( "/api/principal", - &Principal::new(u32::MAX, Type::Individual) + &PrincipalSet::new(u32::MAX, Type::Individual) .with_field(PrincipalField::Name, "admin-foobar") - .with_field(PrincipalField::Roles, vec!["tenant-admin".to_string()]) + .with_field( + PrincipalField::Roles, + vec!["tenant-admin".to_compact_string()], + ) .with_field( PrincipalField::Secrets, - PrincipalValue::String("mytenantpass".to_string()), + PrincipalValue::String("mytenantpass".to_compact_string()), ) .with_field( PrincipalField::Tenant, - PrincipalValue::String("foobar".to_string()), + PrincipalValue::String("foobar".to_compact_string()), ), ) .await @@ -295,11 +305,11 @@ pub async fn test(params: &JMAPTest) { // Create domain for the tenant and one outside the tenant api.post::( "/api/principal", - &Principal::new(u32::MAX, Type::Domain) + &PrincipalSet::new(u32::MAX, Type::Domain) .with_field(PrincipalField::Name, "foobar.org") .with_field( PrincipalField::Tenant, - PrincipalValue::String("foobar".to_string()), + PrincipalValue::String("foobar".to_compact_string()), ), ) .await @@ -307,7 +317,7 @@ pub async fn test(params: &JMAPTest) { .unwrap_data(); api.post::( "/api/principal", - &Principal::new(u32::MAX, Type::Domain).with_field(PrincipalField::Name, "example.org"), + &PrincipalSet::new(u32::MAX, Type::Domain).with_field(PrincipalField::Name, "example.org"), ) .await .unwrap() @@ -317,16 +327,19 @@ pub async fn test(params: &JMAPTest) { let tenant_admin_id = api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Individual) + &PrincipalSet::new(u32::MAX, Type::Individual) .with_field(PrincipalField::Name, "admin@foobar.org") - .with_field(PrincipalField::Roles, vec!["tenant-admin".to_string()]) + .with_field( + PrincipalField::Roles, + vec!["tenant-admin".to_compact_string()], + ) .with_field( PrincipalField::Secrets, - PrincipalValue::String("mytenantpass".to_string()), + PrincipalValue::String("mytenantpass".to_compact_string()), ) .with_field( PrincipalField::Tenant, - PrincipalValue::String("foobar".to_string()), + PrincipalValue::String("foobar".to_compact_string()), ), ) .await @@ -348,7 +361,8 @@ pub async fn test(params: &JMAPTest) { tenant_api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Tenant).with_field(PrincipalField::Name, "subfoobar"), + &PrincipalSet::new(u32::MAX, Type::Tenant) + .with_field(PrincipalField::Name, "subfoobar"), ) .await .unwrap() @@ -358,7 +372,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/foobar", &vec![PrincipalUpdate::set( PrincipalField::Tenant, - PrincipalValue::String("subfoobar".to_string()), + PrincipalValue::String("subfoobar".to_compact_string()), )], ) .await @@ -379,7 +393,8 @@ pub async fn test(params: &JMAPTest) { tenant_api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Domain).with_field(PrincipalField::Name, "foobar.com"), + &PrincipalSet::new(u32::MAX, Type::Domain) + .with_field(PrincipalField::Name, "foobar.com"), ) .await .unwrap() @@ -389,7 +404,8 @@ pub async fn test(params: &JMAPTest) { tenant_api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Domain).with_field(PrincipalField::Name, "foobar.net"), + &PrincipalSet::new(u32::MAX, Type::Domain) + .with_field(PrincipalField::Name, "foobar.net"), ) .await .unwrap() @@ -400,9 +416,12 @@ pub async fn test(params: &JMAPTest) { tenant_api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Individual) - .with_field(PrincipalField::Name, user.to_string()) - .with_field(PrincipalField::Roles, vec!["tenant-admin".to_string()]), + &PrincipalSet::new(u32::MAX, Type::Individual) + .with_field(PrincipalField::Name, user.to_compact_string()) + .with_field( + PrincipalField::Roles, + vec!["tenant-admin".to_compact_string()], + ), ) .await .unwrap() @@ -413,19 +432,22 @@ pub async fn test(params: &JMAPTest) { let tenant_user_id = tenant_api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Individual) + &PrincipalSet::new(u32::MAX, Type::Individual) .with_field(PrincipalField::Name, "john@foobar.org") .with_field( PrincipalField::Roles, - vec!["tenant-admin".to_string(), "user".to_string()], + vec![ + "tenant-admin".to_compact_string(), + "user".to_compact_string(), + ], ) .with_field( PrincipalField::Secrets, - PrincipalValue::String("tenantpass".to_string()), + PrincipalValue::String("tenantpass".to_compact_string()), ) .with_field( PrincipalField::Tenant, - PrincipalValue::String("xanadu".to_string()), + PrincipalValue::String("xanadu".to_compact_string()), ), ) .await @@ -446,9 +468,12 @@ pub async fn test(params: &JMAPTest) { tenant_api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Individual) + &PrincipalSet::new(u32::MAX, Type::Individual) .with_field(PrincipalField::Name, "jane@foobar.org") - .with_field(PrincipalField::Roles, vec!["tenant-admin".to_string()]), + .with_field( + PrincipalField::Roles, + vec!["tenant-admin".to_compact_string()], + ), ) .await .unwrap() @@ -458,11 +483,11 @@ pub async fn test(params: &JMAPTest) { tenant_api .post::( "/api/principal", - &Principal::new(u32::MAX, Type::Role) + &PrincipalSet::new(u32::MAX, Type::Role) .with_field(PrincipalField::Name, "no-mail-for-you@foobar.com") .with_field( PrincipalField::DisabledPermissions, - vec![Permission::EmailReceive.name().to_string()], + vec![Permission::EmailReceive.name().to_compact_string()], ), ) .await @@ -475,7 +500,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/john@foobar.org", &vec![PrincipalUpdate::add_item( PrincipalField::Roles, - PrincipalValue::String("imap_user".to_string()), + PrincipalValue::String("imap_user".to_compact_string()), )], ) .await @@ -488,7 +513,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/john@foobar.org", &vec![PrincipalUpdate::add_item( PrincipalField::Roles, - PrincipalValue::String("no-mail-for-you@foobar.com".to_string()), + PrincipalValue::String("no-mail-for-you@foobar.com".to_compact_string()), )], ) .await @@ -511,7 +536,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/john@foobar.org", &vec![PrincipalUpdate::set( PrincipalField::Tenant, - PrincipalValue::String("xanadu".to_string()), + PrincipalValue::String("xanadu".to_compact_string()), )], ) .await @@ -525,7 +550,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/john@foobar.org", &vec![PrincipalUpdate::set( PrincipalField::Name, - PrincipalValue::String(user.to_string()), + PrincipalValue::String(user.to_compact_string()), )], ) .await @@ -540,11 +565,11 @@ pub async fn test(params: &JMAPTest) { &vec![ PrincipalUpdate::set( PrincipalField::Name, - PrincipalValue::String("john.doe@foobar.org".to_string()), + PrincipalValue::String("john.doe@foobar.org".to_compact_string()), ), PrincipalUpdate::add_item( PrincipalField::Emails, - PrincipalValue::String("john@foobar.org".to_string()), + PrincipalValue::String("john@foobar.org".to_compact_string()), ), ], ) @@ -554,7 +579,7 @@ pub async fn test(params: &JMAPTest) { // Tenants should only see their own principals tenant_api - .get::>("/api/principal?types=individual,group,role,list") + .get::>("/api/principal?types=individual,group,role,list") .await .unwrap() .unwrap_data() @@ -606,8 +631,8 @@ pub async fn test(params: &JMAPTest) { assert_eq!( server .deliver_message(IngestMessage { - sender_address: "bill@foobar.org".to_string(), - recipients: vec!["john@foobar.org".to_string()], + sender_address: "bill@foobar.org".to_compact_string(), + recipients: vec!["john@foobar.org".to_compact_string()], message_blob: message_blob.clone(), message_size: TEST_MESSAGE.len() as u64, session_id: 0, @@ -626,7 +651,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/john.doe@foobar.org", &vec![PrincipalUpdate::remove_item( PrincipalField::Roles, - PrincipalValue::String("no-mail-for-you@foobar.com".to_string()), + PrincipalValue::String("no-mail-for-you@foobar.com".to_compact_string()), )], ) .await @@ -644,8 +669,8 @@ pub async fn test(params: &JMAPTest) { assert_eq!( server .deliver_message(IngestMessage { - sender_address: "bill@foobar.org".to_string(), - recipients: vec!["john@foobar.org".to_string()], + sender_address: "bill@foobar.org".to_compact_string(), + recipients: vec!["john@foobar.org".to_compact_string()], message_blob: message_blob.clone(), message_size: TEST_MESSAGE.len() as u64, session_id: 0, @@ -669,8 +694,8 @@ pub async fn test(params: &JMAPTest) { assert_eq!( server .deliver_message(IngestMessage { - sender_address: "bill@foobar.org".to_string(), - recipients: vec!["john@foobar.org".to_string()], + sender_address: "bill@foobar.org".to_compact_string(), + recipients: vec!["john@foobar.org".to_compact_string()], message_blob, message_size: TEST_MESSAGE.len() as u64, session_id: 0, @@ -687,7 +712,7 @@ pub async fn test(params: &JMAPTest) { "/api/principal/john.doe@foobar.org", &vec![PrincipalUpdate::set( PrincipalField::Tenant, - PrincipalValue::String("xanadu".to_string()), + PrincipalValue::String("xanadu".to_compact_string()), )], ) .await @@ -768,7 +793,7 @@ trait ValidatePrincipalList { fn assert_count(self, count: usize) -> Self; } -impl ValidatePrincipalList for List { +impl ValidatePrincipalList for List { fn assert_exists<'x>( self, name: &str, @@ -800,7 +825,7 @@ trait ValidatePrincipal { ); } -impl ValidatePrincipal for Principal { +impl ValidatePrincipal for PrincipalSet { fn validate<'x>( &self, typ: Type, diff --git a/tests/src/smtp/config.rs b/tests/src/smtp/config.rs index d90703d8..8961ccce 100644 --- a/tests/src/smtp/config.rs +++ b/tests/src/smtp/config.rs @@ -14,6 +14,7 @@ use common::{ }, expr::{functions::ResolveVariable, if_block::*, tokenizer::TokenMap, *}, }; +use compact_str::{CompactString, ToCompactString}; use throttle::parse_queue_rate_limiter; use tokio::net::TcpSocket; @@ -62,13 +63,13 @@ fn parse_if_blocks() { assert_eq!( IfBlock::try_parse(&mut config, "durations", &token_map).unwrap(), IfBlock { - key: "durations".to_string(), + key: "durations".into(), if_then: vec![ IfThen { expr: Expression { items: vec![ ExpressionItem::Variable(V_SENDER), - ExpressionItem::Constant(Constant::String("jdoe".to_string())), + ExpressionItem::Constant(Constant::String("jdoe".into())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] }, @@ -85,7 +86,7 @@ fn parse_if_blocks() { ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, ExpressionItem::Variable(V_RECIPIENT), - ExpressionItem::Constant(Constant::String("jane".to_string())), + ExpressionItem::Constant(Constant::String("jane".into())), ExpressionItem::Function { id: 29, num_args: 2 @@ -107,21 +108,21 @@ fn parse_if_blocks() { assert_eq!( IfBlock::try_parse(&mut config, "string-list", &token_map).unwrap(), IfBlock { - key: "string-list".to_string(), + key: "string-list".into(), if_then: vec![ IfThen { expr: Expression { items: vec![ ExpressionItem::Variable(V_SENDER), - ExpressionItem::Constant(Constant::String("jdoe".to_string())), + ExpressionItem::Constant(Constant::String("jdoe".into())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] }, then: Expression { items: vec![ - ExpressionItem::Constant(Constant::String("From".to_string())), - ExpressionItem::Constant(Constant::String("To".to_string())), - ExpressionItem::Constant(Constant::String("Date".to_string())), + ExpressionItem::Constant(Constant::String("From".into())), + ExpressionItem::Constant(Constant::String("To".into())), + ExpressionItem::Constant(Constant::String("Date".into())), ExpressionItem::ArrayBuild(3) ] } @@ -135,7 +136,7 @@ fn parse_if_blocks() { ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, ExpressionItem::Variable(V_RECIPIENT), - ExpressionItem::Constant(Constant::String("jane".to_string())), + ExpressionItem::Constant(Constant::String("jane".into())), ExpressionItem::Function { id: 29, num_args: 2 @@ -145,7 +146,7 @@ fn parse_if_blocks() { }, then: Expression { items: vec![ExpressionItem::Constant(Constant::String( - "Other-ID".to_string() + "Other-ID".into() ))] } } @@ -159,21 +160,21 @@ fn parse_if_blocks() { assert_eq!( IfBlock::try_parse(&mut config, "string-list-bis", &token_map).unwrap(), IfBlock { - key: "string-list-bis".to_string(), + key: "string-list-bis".into(), if_then: vec![ IfThen { expr: Expression { items: vec![ ExpressionItem::Variable(V_SENDER), - ExpressionItem::Constant(Constant::String("jdoe".to_string())), + ExpressionItem::Constant(Constant::String("jdoe".into())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] }, then: Expression { items: vec![ - ExpressionItem::Constant(Constant::String("From".to_string())), - ExpressionItem::Constant(Constant::String("To".to_string())), - ExpressionItem::Constant(Constant::String("Date".to_string())), + ExpressionItem::Constant(Constant::String("From".into())), + ExpressionItem::Constant(Constant::String("To".into())), + ExpressionItem::Constant(Constant::String("Date".into())), ExpressionItem::ArrayBuild(3) ] } @@ -187,7 +188,7 @@ fn parse_if_blocks() { ExpressionItem::BinaryOperator(BinaryOperator::Eq), ExpressionItem::JmpIf { val: true, pos: 4 }, ExpressionItem::Variable(V_RECIPIENT), - ExpressionItem::Constant(Constant::String("jane".to_string())), + ExpressionItem::Constant(Constant::String("jane".into())), ExpressionItem::Function { id: 29, num_args: 2 @@ -202,7 +203,7 @@ fn parse_if_blocks() { ], default: Expression { items: vec![ - ExpressionItem::Constant(Constant::String("ID-Bis".to_string())), + ExpressionItem::Constant(Constant::String("ID-Bis".into())), ExpressionItem::ArrayBuild(1) ] } @@ -212,11 +213,11 @@ fn parse_if_blocks() { assert_eq!( IfBlock::try_parse(&mut config, "single-value", &token_map).unwrap(), IfBlock { - key: "single-value".to_string(), + key: "single-value".into(), if_then: vec![], default: Expression { items: vec![ExpressionItem::Constant(Constant::String( - "hello world".to_string() + "hello world".into() ))] } } @@ -263,11 +264,11 @@ fn parse_throttles() { throttle, vec![ QueueRateLimiter { - id: "0000".to_string(), + id: "0000".into(), expr: Expression { items: vec![ ExpressionItem::Variable(8), - ExpressionItem::Constant(Constant::String("127.0.0.1".to_string())), + ExpressionItem::Constant(Constant::String("127.0.0.1".into())), ExpressionItem::BinaryOperator(BinaryOperator::Eq) ] }, @@ -278,7 +279,7 @@ fn parse_throttles() { } }, QueueRateLimiter { - id: "0001".to_string(), + id: "0001".into(), expr: Expression::default(), keys: THROTTLE_SENDER_DOMAIN, rate: Rate { @@ -306,7 +307,7 @@ fn parse_servers() { let id_generator = Arc::new(utils::snowflake::SnowflakeIdGenerator::new()); let expected_servers = vec![ Listener { - id: "smtp".to_string(), + id: "smtp".into(), protocol: ServerProtocol::Smtp, listeners: vec![TcpListener { socket: TcpSocket::new_v4().unwrap(), @@ -321,7 +322,7 @@ fn parse_servers() { span_id_gen: id_generator.clone(), }, Listener { - id: "smtps".to_string(), + id: "smtps".into(), protocol: ServerProtocol::Smtp, listeners: vec![ TcpListener { @@ -346,7 +347,7 @@ fn parse_servers() { span_id_gen: id_generator.clone(), }, Listener { - id: "submission".to_string(), + id: "submission".into(), protocol: ServerProtocol::Smtp, listeners: vec![TcpListener { socket: TcpSocket::new_v4().unwrap(), @@ -429,7 +430,7 @@ async fn eval_if() { assert_eq!( core.eval_if::( &IfBlock { - key: key.to_string(), + key: key.to_compact_string(), if_then: vec![IfThen { expr: Expression::try_parse(&mut config, key.as_str(), &token_map).unwrap(), then: Expression::from(true), @@ -486,10 +487,12 @@ async fn eval_dynvalue() { .unwrap(); let expected = config .property_require::>(("eval", test_name.as_str(), "expect")) - .unwrap_or_else(|| panic!("Missing expect for test {test_name:?}")); + .unwrap_or_else(|| panic!("Missing expect for test {test_name:?}")) + .map(Into::into); assert_eq!( - core.eval_if::(&if_block, &envelope, 0).await, + core.eval_if::(&if_block, &envelope, 0) + .await, expected, "failed for test {test_name:?}" ); @@ -504,10 +507,10 @@ impl ResolveVariable for TestEnvelope { V_SENDER => self.sender.as_str().into(), V_SENDER_DOMAIN => self.sender_domain.as_str().into(), V_AUTHENTICATED_AS => self.authenticated_as.as_str().into(), - V_LISTENER => self.listener_id.to_string().into(), - V_REMOTE_IP => self.remote_ip.to_string().into(), - V_LOCAL_IP => self.local_ip.to_string().into(), - V_PRIORITY => self.priority.to_string().into(), + V_LISTENER => self.listener_id.to_compact_string().into(), + V_REMOTE_IP => self.remote_ip.to_compact_string().into(), + V_LOCAL_IP => self.local_ip.to_compact_string().into(), + V_PRIORITY => self.priority.to_compact_string().into(), V_MX => self.mx.as_str().into(), V_HELO_DOMAIN => self.helo_domain.as_str().into(), _ => Default::default(), diff --git a/tests/src/smtp/inbound/antispam.rs b/tests/src/smtp/inbound/antispam.rs index d9773e7c..134c049f 100644 --- a/tests/src/smtp/inbound/antispam.rs +++ b/tests/src/smtp/inbound/antispam.rs @@ -18,6 +18,7 @@ use common::{ }, }, }; +use compact_str::{CompactString, ToCompactString}; use http_proto::{JsonResponse, ToHttpResponse}; use hyper::Method; use mail_auth::{ @@ -359,7 +360,7 @@ async fn antispam() { let mut dkim_signatures = vec![]; let mut dmarc_result = None; let mut dmarc_policy = None; - let mut expected_tags = AHashSet::new(); + let mut expected_tags: AHashSet = AHashSet::new(); let mut expect_headers = String::new(); let mut score_set = 0.0; let mut score_final = 0.0; @@ -376,15 +377,15 @@ async fn antispam() { let value = value.trim(); match param { "remote_ip" => { - session.data.remote_ip_str = value.to_string(); + session.data.remote_ip_str = value.to_compact_string(); session.data.remote_ip = value.parse().unwrap(); } "helo_domain" => { - session.data.helo_domain = value.to_string(); + session.data.helo_domain = value.to_compact_string(); } "authenticated_as" => { session.data.authenticated_as = Some(Arc::new(AccessToken { - name: value.to_string(), + name: value.to_compact_string(), ..Default::default() })); } @@ -428,13 +429,14 @@ async fn antispam() { .collect(); } "envelope_from" => { - session.data.mail_from = Some(SessionAddress::new(value.to_string())); + session.data.mail_from = + Some(SessionAddress::new(value.to_compact_string())); } "envelope_to" => { session .data .rcpt_to - .push(SessionAddress::new(value.to_string())); + .push(SessionAddress::new(value.to_compact_string())); } "iprev.ptr" => { session @@ -453,8 +455,11 @@ async fn antispam() { dmarc_policy = Policy::from_str(value).into(); } "expect" => { - expected_tags - .extend(value.split_ascii_whitespace().map(|v| v.to_uppercase())); + expected_tags.extend( + value + .split_ascii_whitespace() + .map(|v| v.to_uppercase().into()), + ); } "expect_header" => { let value = value.trim(); @@ -501,7 +506,7 @@ async fn antispam() { session .data .mail_from - .get_or_insert_with(|| SessionAddress::new("".to_string())) + .get_or_insert_with(|| SessionAddress::new("".to_compact_string())) .flags = body_params; } @@ -775,7 +780,7 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "hello".to_string(), + text: "hello".to_compact_string(), }, HtmlToken::StartTag { name: 29282, @@ -783,7 +788,7 @@ fn html_tokens() { is_self_closing: true, }, HtmlToken::Text { - text: "world".to_string(), + text: "world".to_compact_string(), }, HtmlToken::StartTag { name: 29282, @@ -802,7 +807,7 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "using <>".to_string(), + text: "using <>".to_compact_string(), }, HtmlToken::StartTag { name: 29282, @@ -816,7 +821,7 @@ fn html_tokens() { concat!("test tag
"), vec![ HtmlToken::Text { - text: "test".to_string(), + text: "test".to_compact_string(), }, HtmlToken::StartTag { name: 7630702, @@ -824,7 +829,7 @@ fn html_tokens() { is_self_closing: true, }, HtmlToken::Text { - text: " tag".to_string(), + text: " tag".to_compact_string(), }, HtmlToken::StartTag { name: 29282, @@ -842,7 +847,7 @@ fn html_tokens() { is_self_closing: true, }, HtmlToken::Text { - text: ">hello world".to_string(), + text: ">hello world".to_compact_string(), }, HtmlToken::StartTag { name: 29282, @@ -869,7 +874,7 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "ignore head".to_string(), + text: "ignore head".to_compact_string(), }, HtmlToken::EndTag { name: 435611265396 }, HtmlToken::StartTag { @@ -878,7 +883,7 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "xyz".to_string(), + text: "xyz".to_compact_string(), }, HtmlToken::EndTag { name: 7630702 }, HtmlToken::EndTag { name: 1684104552 }, @@ -888,7 +893,7 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "".to_string(), + text: "".to_compact_string(), }, HtmlToken::EndTag { name: 12648 }, ], @@ -906,7 +911,7 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "what is ♥?".to_string(), + text: "what is ♥?".to_compact_string(), }, HtmlToken::EndTag { name: 112 }, HtmlToken::StartTag { @@ -915,7 +920,7 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "ßĂΒγ don't hurt me.".to_string(), + text: "ßĂΒγ don't hurt me.".to_compact_string(), }, HtmlToken::EndTag { name: 112 }, ], @@ -935,22 +940,22 @@ fn html_tokens() { "table, td, a, p, span, ul, li {font-family: ", "Arial, sans-serif!important;} < < < < ignore > -> here --".to_string(), + text: "!-- <> < < < < ignore > -> here --".to_compact_string(), }, HtmlToken::Text { - text: " the actual".to_string(), + text: " the actual".to_compact_string(), }, HtmlToken::Comment { - text: "!--".to_string(), + text: "!--".to_compact_string(), }, HtmlToken::Text { - text: " text".to_string(), + text: " text".to_compact_string(), }, ], ), @@ -966,7 +971,7 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "hello".to_string(), + text: "hello".to_compact_string(), }, HtmlToken::EndTag { name: 112 }, HtmlToken::StartTag { @@ -975,11 +980,11 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: " world".to_string(), + text: " world".to_compact_string(), }, HtmlToken::EndTag { name: 112 }, HtmlToken::Text { - text: " !!!".to_string(), + text: " !!!".to_compact_string(), }, HtmlToken::StartTag { name: 29282, @@ -997,19 +1002,19 @@ fn html_tokens() { is_self_closing: false, }, HtmlToken::Text { - text: "please unsubscribe".to_string(), + text: "please unsubscribe".to_compact_string(), }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("#".to_string()))], + attributes: vec![(1717924456, Some("#".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: " here".to_string(), + text: " here".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::Text { - text: ".".to_string(), + text: ".".to_compact_string(), }, HtmlToken::EndTag { name: 112 }, ], @@ -1025,65 +1030,65 @@ fn html_tokens() { vec![ HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("a".to_string()))], + attributes: vec![(1717924456, Some("a".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("b".to_string()))], + attributes: vec![(1717924456, Some("b".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("c".to_string()))], + attributes: vec![(1717924456, Some("c".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("d".to_string()))], + attributes: vec![(1717924456, Some("d".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("e".to_string()))], + attributes: vec![(1717924456, Some("e".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(125779835187816, Some("ignore".to_string()))], + attributes: vec![(125779835187816, Some("ignore".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 125822818283105, - attributes: vec![(1717924456, Some("x".to_string()))], + attributes: vec![(1717924456, Some("x".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, ], @@ -1098,65 +1103,65 @@ fn html_tokens() { vec![ HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("a".to_string()))], + attributes: vec![(1717924456, Some("a".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("b".to_string()))], + attributes: vec![(1717924456, Some("b".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("c".to_string()))], + attributes: vec![(1717924456, Some("c".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("d".to_string()))], + attributes: vec![(1717924456, Some("d".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("e".to_string()))], + attributes: vec![(1717924456, Some("e".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(125779835187816, Some("ignore".to_string()))], + attributes: vec![(125779835187816, Some("ignore".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 125822818283105, - attributes: vec![(1717924456, Some("x".to_string()))], + attributes: vec![(1717924456, Some("x".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, ], @@ -1173,49 +1178,49 @@ fn html_tokens() { vec![ HtmlToken::Comment { text: "!-- texttext--text--" - .to_string(), + .to_compact_string(), }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("hello world".to_string()))], + attributes: vec![(1717924456, Some("hello world".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, attributes: vec![ - (1717924456, Some("test".to_string())), + (1717924456, Some("test".to_compact_string())), (111542170183529, None), ], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, attributes: vec![ - (1717924456, Some("fudge".to_string())), + (1717924456, Some("fudge".to_compact_string())), (1717924456, None), (111542170183529, None), ], is_self_closing: false, }, HtmlToken::Text { - text: "text".to_string(), + text: "text".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, HtmlToken::StartTag { name: 97, - attributes: vec![(1717924456, Some("foobar".to_string()))], + attributes: vec![(1717924456, Some("foobar".to_compact_string()))], is_self_closing: false, }, HtmlToken::Text { - text: "a href = \"unknown\"".to_string(), + text: "a href = \"unknown\"".to_compact_string(), }, HtmlToken::EndTag { name: 97 }, ], diff --git a/tests/src/smtp/inbound/auth.rs b/tests/src/smtp/inbound/auth.rs index d3629bd1..c4356e13 100644 --- a/tests/src/smtp/inbound/auth.rs +++ b/tests/src/smtp/inbound/auth.rs @@ -81,7 +81,7 @@ async fn auth() { // EHLO should not advertise plain text auth without TLS let mut session = Session::test(TestSMTP::from_core(core).server); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.stream.tls = false; session @@ -148,7 +148,7 @@ async fn auth() { session.cmd("cDRzc3cwcmQ=", "235 2.7.0").await; // Login should not be advertised to 10.0.0.2 - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session.stream.tls = true; session diff --git a/tests/src/smtp/inbound/data.rs b/tests/src/smtp/inbound/data.rs index 4c87ea11..eaa95f9f 100644 --- a/tests/src/smtp/inbound/data.rs +++ b/tests/src/smtp/inbound/data.rs @@ -117,7 +117,7 @@ async fn data() { let test = TestSMTP::from_core(core); let mut qr = test.queue_receiver; let mut session = Session::test(test.server.clone()); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.test_builder().await; @@ -158,7 +158,7 @@ async fn data() { session.rset().await; // Headers should be added to messages from 10.0.0.3 - session.data.remote_ip_str = "10.0.0.3".to_string(); + session.data.remote_ip_str = "10.0.0.3".into(); session.eval_session_params().await; session .send_message("bill@doe.org", &["mike@test.com"], "test:no_msgid", "250") @@ -178,7 +178,7 @@ async fn data() { .assert_contains("Received-SPF: "); // Only one message is allowed in the queue from john@doe.org - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session .send_message("john@doe.org", &["bill@foobar.org"], "test:no_dkim", "250") diff --git a/tests/src/smtp/inbound/dmarc.rs b/tests/src/smtp/inbound/dmarc.rs index 35e4f5e9..28370281 100644 --- a/tests/src/smtp/inbound/dmarc.rs +++ b/tests/src/smtp/inbound/dmarc.rs @@ -166,7 +166,7 @@ async fn dmarc() { let mut rr = test.report_receiver; let mut qr = test.queue_receiver; let mut session = Session::test(test.server.clone()); - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.eval_session_params().await; session.ehlo("mx.example.com").await; @@ -191,7 +191,7 @@ async fn dmarc() { qr.assert_no_events(); // Invalid DKIM signatures should be rejected - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.eval_session_params().await; session diff --git a/tests/src/smtp/inbound/ehlo.rs b/tests/src/smtp/inbound/ehlo.rs index 607e5147..24a4cea2 100644 --- a/tests/src/smtp/inbound/ehlo.rs +++ b/tests/src/smtp/inbound/ehlo.rs @@ -57,7 +57,7 @@ async fn ehlo() { // Reject non-FQDN domains let mut session = Session::test(server); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.stream.tls = false; session.eval_session_params().await; @@ -79,8 +79,8 @@ async fn ehlo() { ); // Test SPF strict mode - session.data.helo_domain = String::new(); - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.helo_domain = "".into(); + session.data.remote_ip_str = "10.0.0.2".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.stream.tls = true; session.eval_session_params().await; diff --git a/tests/src/smtp/inbound/limits.rs b/tests/src/smtp/inbound/limits.rs index 299a23a0..5f9a2c34 100644 --- a/tests/src/smtp/inbound/limits.rs +++ b/tests/src/smtp/inbound/limits.rs @@ -39,7 +39,7 @@ async fn limits() { // Exceed max line length let mut session = Session::test_with_shutdown(TestSMTP::from_core(core).server, rx); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); let mut buf = vec![b'A'; 2049]; session.ingest(&buf).await.unwrap(); session.ingest(b"\r\n").await.unwrap(); @@ -57,7 +57,7 @@ async fn limits() { session.response().assert_code("452 4.7.28"); // Loitering - session.data.remote_ip_str = "10.0.0.3".to_string(); + session.data.remote_ip_str = "10.0.0.3".into(); session.data.valid_until = Instant::now(); session.eval_session_params().await; tokio::time::sleep(Duration::from_millis(600)).await; @@ -66,7 +66,7 @@ async fn limits() { session.response().assert_code("421 4.3.2"); // Timeout - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.data.valid_until = Instant::now(); session.eval_session_params().await; session.write_rx("MAIL FROM:\r\n"); diff --git a/tests/src/smtp/inbound/mail.rs b/tests/src/smtp/inbound/mail.rs index 7ae37f32..ec5bba08 100644 --- a/tests/src/smtp/inbound/mail.rs +++ b/tests/src/smtp/inbound/mail.rs @@ -106,7 +106,7 @@ async fn mail() { // Be rude and do not say EHLO let mut session = Session::test(server.clone()); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.eval_session_params().await; session @@ -185,7 +185,7 @@ async fn mail() { session.response().assert_code("552 5.3.4"); // Test strict IPREV - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.data.remote_ip = session.data.remote_ip_str.parse().unwrap(); session.data.iprev = None; session.eval_session_params().await; diff --git a/tests/src/smtp/inbound/milter.rs b/tests/src/smtp/inbound/milter.rs index 55c8bf59..ffe5b8f9 100644 --- a/tests/src/smtp/inbound/milter.rs +++ b/tests/src/smtp/inbound/milter.rs @@ -13,6 +13,7 @@ use common::{ expr::if_block::IfBlock, manager::webadmin::Resource, }; +use compact_str::CompactString; use http_proto::{ToHttpResponse, request::fetch_body}; use hyper::{body, server::conn::http1, service::service_fn}; use hyper_util::rt::TokioIo; @@ -112,7 +113,7 @@ async fn milter_session() { let test = TestSMTP::from_core(core); let mut qr = test.queue_receiver; let mut session = Session::test(test.server); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.doe.org").await; @@ -245,7 +246,7 @@ async fn mta_hook_session() { let test = TestSMTP::from_core(core); let mut qr = test.queue_receiver; let mut session = Session::test(test.server); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.doe.org").await; @@ -380,8 +381,8 @@ fn milter_address_modifications() { assert!( data.apply_milter_modifications( vec![Modification::ChangeFrom { - sender: "<>".to_string(), - args: String::new() + sender: "<>".into(), + args: "".into(), }], &parsed_test_message ) @@ -396,8 +397,8 @@ fn milter_address_modifications() { assert!( data.apply_milter_modifications( vec![Modification::ChangeFrom { - sender: "john@example.org".to_string(), - args: "REQUIRETLS ENVID=abc123".to_string(), //"NOTIFY=SUCCESS,FAILURE ENVID=abc123\n".to_string() + sender: "john@example.org".into(), + args: "REQUIRETLS ENVID=abc123".into(), //"NOTIFY=SUCCESS,FAILURE ENVID=abc123\n".into() }], &parsed_test_message ) @@ -406,27 +407,27 @@ fn milter_address_modifications() { let addr = data.mail_from.as_ref().unwrap(); assert_eq!(addr.address_lcase, "john@example.org"); assert_ne!(addr.flags, 0); - assert_eq!(addr.dsn_info, Some("abc123".to_string())); + assert_eq!(addr.dsn_info, Some("abc123".into())); // Add recipients assert!( data.apply_milter_modifications( vec![ Modification::AddRcpt { - recipient: "bill@example.org".to_string(), - args: "".to_string(), + recipient: "bill@example.org".into(), + args: "".into(), }, Modification::AddRcpt { - recipient: "jane@foobar.org".to_string(), - args: "NOTIFY=SUCCESS,FAILURE ORCPT=rfc822;Jane.Doe@Foobar.org".to_string(), + recipient: "jane@foobar.org".into(), + args: "NOTIFY=SUCCESS,FAILURE ORCPT=rfc822;Jane.Doe@Foobar.org".into(), }, Modification::AddRcpt { - recipient: "".to_string(), - args: "".to_string(), + recipient: "".into(), + args: "".into(), }, Modification::AddRcpt { - recipient: "<>".to_string(), - args: "".to_string(), + recipient: "<>".into(), + args: "".into(), }, ], &parsed_test_message @@ -441,17 +442,17 @@ fn milter_address_modifications() { let addr = data.rcpt_to.last().unwrap(); assert_eq!(addr.address_lcase, "jane@foobar.org"); assert_ne!(addr.flags, 0); - assert_eq!(addr.dsn_info, Some("Jane.Doe@Foobar.org".to_string())); + assert_eq!(addr.dsn_info, Some("Jane.Doe@Foobar.org".into())); // Remove recipients assert!( data.apply_milter_modifications( vec![ Modification::DeleteRcpt { - recipient: "bill@example.org".to_string(), + recipient: "bill@example.org".into(), }, Modification::DeleteRcpt { - recipient: "<>".to_string(), + recipient: "<>".into(), }, ], &parsed_test_message @@ -462,7 +463,7 @@ fn milter_address_modifications() { let addr = data.rcpt_to.last().unwrap(); assert_eq!(addr.address_lcase, "jane@foobar.org"); assert_ne!(addr.flags, 0); - assert_eq!(addr.dsn_info, Some("Jane.Doe@Foobar.org".to_string())); + assert_eq!(addr.dsn_info, Some("Jane.Doe@Foobar.org".into())); } #[test] @@ -552,9 +553,9 @@ async fn milter_client_test() { let mut client = MilterClient::connect( &Milter { enable: IfBlock::empty(""), - id: "test".to_string().into(), + id: Arc::new("test".into()), addrs: vec![SocketAddr::from(([127, 0, 0, 1], PORT))], - hostname: "localhost".to_string(), + hostname: "localhost".into(), port: PORT, timeout_connect: Duration::from_secs(10), timeout_command: Duration::from_secs(30), @@ -727,7 +728,7 @@ async fn accept_milter( "conn_fail" => Action::ConnectionFailure, "reply_code" => Action::ReplyCode { code: [b'3', b'2', b'1'], - text: "test".to_string(), + text: "test".into(), }, test_num => { modifications = tests[test_num.parse::().unwrap()] @@ -867,8 +868,8 @@ fn handle_mta_hook(request: Request, tests: Arc>) -> hooks::Resp action: hooks::Action::Reject, response: SmtpResponse { status: 451.into(), - enhanced_status: "4.3.5".to_string().into(), - message: "Unable to accept message at this time.".to_string().into(), + enhanced_status: Some("4.3.5".into()), + message: Some("Unable to accept message at this time.".into()), disconnect: false, } .into(), @@ -878,8 +879,8 @@ fn handle_mta_hook(request: Request, tests: Arc>) -> hooks::Resp action: hooks::Action::Reject, response: SmtpResponse { status: 421.into(), - enhanced_status: "4.3.0".to_string().into(), - message: "Server shutting down".to_string().into(), + enhanced_status: Some("4.3.0".into()), + message: Some("Server shutting down".into()), disconnect: false, } .into(), @@ -898,8 +899,8 @@ fn handle_mta_hook(request: Request, tests: Arc>) -> hooks::Resp action: hooks::Action::Reject, response: SmtpResponse { status: 321.into(), - enhanced_status: "3.1.1".to_string().into(), - message: "Test".to_string().into(), + enhanced_status: Some("3.1.1".into()), + message: Some("Test".into()), disconnect: false, } .into(), @@ -918,7 +919,7 @@ fn handle_mta_hook(request: Request, tests: Arc>) -> hooks::Resp .split_whitespace() .map(|arg| { let (key, value) = arg.split_once('=').unwrap(); - (key.to_string(), Some(value.to_string())) + (key.into(), Some(value.into())) }) .collect(), }, @@ -929,7 +930,7 @@ fn handle_mta_hook(request: Request, tests: Arc>) -> hooks::Resp .split_whitespace() .map(|arg| { let (key, value) = arg.split_once('=').unwrap(); - (key.to_string(), Some(value.to_string())) + (key.into(), Some(value.into())) }) .collect(), } @@ -940,7 +941,7 @@ fn handle_mta_hook(request: Request, tests: Arc>) -> hooks::Resp } } Modification::ReplaceBody { value } => hooks::Modification::ReplaceContents { - value: String::from_utf8(value.clone()).unwrap(), + value: CompactString::from_utf8(value.clone()).unwrap(), }, Modification::AddHeader { name, value } => hooks::Modification::AddHeader { name: name.clone(), @@ -961,8 +962,8 @@ fn handle_mta_hook(request: Request, tests: Arc>) -> hooks::Resp } } Modification::Quarantine { reason } => hooks::Modification::AddHeader { - name: "X-Quarantine".to_string(), - value: reason.to_string(), + name: "X-Quarantine".into(), + value: reason.clone(), }, }) .collect(), diff --git a/tests/src/smtp/inbound/rcpt.rs b/tests/src/smtp/inbound/rcpt.rs index 8021374c..f3741488 100644 --- a/tests/src/smtp/inbound/rcpt.rs +++ b/tests/src/smtp/inbound/rcpt.rs @@ -94,7 +94,7 @@ async fn rcpt() { // RCPT without MAIL FROM let mut session = Session::test(TestSMTP::from_core(core).server); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx1.foobar.org").await; session.rcpt_to("jane@foobar.org", "503 5.5.1").await; @@ -147,7 +147,7 @@ async fn rcpt() { } // Relaying should be allowed for 10.0.0.2 - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session.rset().await; session.mail_from("john@example.net", "250").await; diff --git a/tests/src/smtp/inbound/rewrite.rs b/tests/src/smtp/inbound/rewrite.rs index 7bb55842..a993ba5e 100644 --- a/tests/src/smtp/inbound/rewrite.rs +++ b/tests/src/smtp/inbound/rewrite.rs @@ -75,7 +75,7 @@ async fn address_rewrite() { // Init session let mut session = Session::test(TestSMTP::from_core(core).server); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.doe.org").await; diff --git a/tests/src/smtp/inbound/scripts.rs b/tests/src/smtp/inbound/scripts.rs index 8aa40961..406eb22b 100644 --- a/tests/src/smtp/inbound/scripts.rs +++ b/tests/src/smtp/inbound/scripts.rs @@ -155,11 +155,7 @@ async fn sieve_scripts() { .set_variable("from", "john.doe@example.org") .with_envelope(&test.server, &session, 0) .await; - match test - .server - .run_script(name.to_string(), script, params) - .await - { + match test.server.run_script(name.into(), script, params).await { ScriptResult::Accept { .. } => (), ScriptResult::Reject(message) => panic!("{}", message), err => { diff --git a/tests/src/smtp/inbound/sign.rs b/tests/src/smtp/inbound/sign.rs index 2d09605e..3b00b94e 100644 --- a/tests/src/smtp/inbound/sign.rs +++ b/tests/src/smtp/inbound/sign.rs @@ -174,7 +174,7 @@ async fn sign_and_seal() { // Test DKIM signing let mut qr = test.queue_receiver; let mut session = Session::test(test.server); - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session.ehlo("mx.example.com").await; session diff --git a/tests/src/smtp/inbound/throttle.rs b/tests/src/smtp/inbound/throttle.rs index cce7970b..7599adb4 100644 --- a/tests/src/smtp/inbound/throttle.rs +++ b/tests/src/smtp/inbound/throttle.rs @@ -53,7 +53,7 @@ async fn throttle_inbound() { // Test connection rate limit let mut session = Session::test(TestSMTP::from_core(core).server); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); assert!(session.is_allowed().await, "Rate limiter too strict."); assert!(session.is_allowed().await, "Rate limiter too strict."); assert!(!session.is_allowed().await, "Rate limiter failed."); @@ -65,9 +65,9 @@ async fn throttle_inbound() { // Test mail from rate limit session.data.mail_from = SessionAddress { - address: "sender@test.org".to_string(), - address_lcase: "sender@test.org".to_string(), - domain: "test.org".to_string(), + address: "sender@test.org".into(), + address_lcase: "sender@test.org".into(), + domain: "test.org".into(), flags: 0, dsn_info: None, } @@ -76,9 +76,9 @@ async fn throttle_inbound() { assert!(session.is_allowed().await, "Rate limiter too strict."); assert!(!session.is_allowed().await, "Rate limiter failed."); session.data.mail_from = SessionAddress { - address: "other-sender@test.org".to_string(), - address_lcase: "other-sender@test.org".to_string(), - domain: "test.org".to_string(), + address: "other-sender@test.org".into(), + address_lcase: "other-sender@test.org".into(), + domain: "test.org".into(), flags: 0, dsn_info: None, } @@ -87,15 +87,15 @@ async fn throttle_inbound() { // Test recipient rate limit session.data.rcpt_to.push(SessionAddress { - address: "recipient@example.org".to_string(), - address_lcase: "recipient@example.org".to_string(), - domain: "example.org".to_string(), + address: "recipient@example.org".into(), + address_lcase: "recipient@example.org".into(), + domain: "example.org".into(), flags: 0, dsn_info: None, }); assert!(session.is_allowed().await, "Rate limiter too strict."); assert!(session.is_allowed().await, "Rate limiter too strict."); assert!(!session.is_allowed().await, "Rate limiter failed."); - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); assert!(session.is_allowed().await, "Rate limiter too strict."); } diff --git a/tests/src/smtp/inbound/vrfy.rs b/tests/src/smtp/inbound/vrfy.rs index 1e535f76..9bf8996f 100644 --- a/tests/src/smtp/inbound/vrfy.rs +++ b/tests/src/smtp/inbound/vrfy.rs @@ -79,7 +79,7 @@ async fn vrfy_expn() { // EHLO should not advertise VRFY/EXPN to 10.0.0.2 let mut session = Session::test(TestSMTP::from_core(core).server); - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session .ehlo("mx.foobar.org") @@ -90,7 +90,7 @@ async fn vrfy_expn() { session.cmd("EXPN sales@foobar.org", "252 2.5.1").await; // EHLO should advertise VRFY/EXPN for 10.0.0.1 - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session .ehlo("mx.foobar.org") diff --git a/tests/src/smtp/lookup/sql.rs b/tests/src/smtp/lookup/sql.rs index ecd06fcc..63e5891d 100644 --- a/tests/src/smtp/lookup/sql.rs +++ b/tests/src/smtp/lookup/sql.rs @@ -11,9 +11,10 @@ use common::{ expr::{tokenizer::TokenMap, *}, }; +use compact_str::{CompactString, ToCompactString}; use directory::{ - Principal, QueryBy, Type, - backend::internal::{PrincipalField, PrincipalValue, manage::ManageDirectory}, + QueryBy, Type, + backend::internal::{PrincipalField, PrincipalSet, PrincipalValue, manage::ManageDirectory}, }; use mail_auth::MX; use store::Stores; @@ -158,7 +159,7 @@ async fn lookup_sql() { for name in ["foobar.org", "foobar.net"] { internal_store .create_principal( - Principal::new(0, Type::Domain).with_field(PrincipalField::Name, name), + PrincipalSet::new(0, Type::Domain).with_field(PrincipalField::Name, name), None, None, ) @@ -169,12 +170,12 @@ async fn lookup_sql() { // Create lists internal_store .create_principal( - Principal::new(0, Type::List) + PrincipalSet::new(0, Type::List) .with_field(PrincipalField::Name, "support@foobar.org") .with_field(PrincipalField::Emails, "support@foobar.org") .with_field( PrincipalField::ExternalMembers, - PrincipalValue::StringList(vec!["mike@foobar.net".to_string()]), + PrincipalValue::StringList(vec!["mike@foobar.net".to_compact_string()]), ), None, None, @@ -183,15 +184,15 @@ async fn lookup_sql() { .unwrap(); internal_store .create_principal( - Principal::new(0, Type::List) + PrincipalSet::new(0, Type::List) .with_field(PrincipalField::Name, "sales@foobar.org") .with_field(PrincipalField::Emails, "sales@foobar.org") .with_field( PrincipalField::ExternalMembers, PrincipalValue::StringList(vec![ - "jane@foobar.org".to_string(), - "john@foobar.org".to_string(), - "bill@foobar.org".to_string(), + "jane@foobar.org".to_compact_string(), + "john@foobar.org".to_compact_string(), + "bill@foobar.org".to_compact_string(), ]), ), None, @@ -219,7 +220,7 @@ async fn lookup_sql() { Expression::try_parse(&mut config, ("test", test_name, "expr"), &token_map).unwrap(); assert_eq!( test.server - .eval_expr::(&e, &RecipientDomain::new("test.org"), "text", 0) + .eval_expr::(&e, &RecipientDomain::new("test.org"), "text", 0) .await .unwrap(), config.value(("test", test_name, "expect")).unwrap(), @@ -236,7 +237,7 @@ async fn lookup_sql() { .ehlo("mx.foobar.org") .await .assert_contains("REQUIRETLS"); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session .ehlo("mx1.foobar.org") diff --git a/tests/src/smtp/lookup/utils.rs b/tests/src/smtp/lookup/utils.rs index 7af811a1..c7fdada4 100644 --- a/tests/src/smtp/lookup/utils.rs +++ b/tests/src/smtp/lookup/utils.rs @@ -87,7 +87,10 @@ async fn lookup_ip() { let resolve_result = test .server .resolve_host( - &NextHop::MX("mx.foobar.org"), + &NextHop::MX { + host: "mx.foobar.org", + is_implicit: false, + }, &RecipientDomain::new("envelope"), 2, 0, @@ -124,7 +127,10 @@ async fn lookup_ip() { let resolve_result = test .server .resolve_host( - &NextHop::MX("mx.foobar.org"), + &NextHop::MX { + host: "mx.foobar.org", + is_implicit: false, + }, &RecipientDomain::new("envelope"), 2, 0, @@ -170,7 +176,7 @@ fn to_remote_hosts() { let hosts = mx.to_remote_hosts("domain", 7).unwrap(); assert_eq!(hosts.len(), 7); for host in hosts { - if let NextHop::MX(host) = host { + if let NextHop::MX { host, .. } = host { assert!((*host.as_bytes().last().unwrap() - b'0') <= 8); } } diff --git a/tests/src/smtp/management/queue.rs b/tests/src/smtp/management/queue.rs index e8dd5e6e..e86f5516 100644 --- a/tests/src/smtp/management/queue.rs +++ b/tests/src/smtp/management/queue.rs @@ -138,7 +138,7 @@ async fn manage_queue() { .queue_receiver .queue_rx .spawn(local.server.inner.clone()); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("foobar.net").await; for test_num in 0..6 { diff --git a/tests/src/smtp/mod.rs b/tests/src/smtp/mod.rs index 10028256..84599ec2 100644 --- a/tests/src/smtp/mod.rs +++ b/tests/src/smtp/mod.rs @@ -236,6 +236,7 @@ impl TestSMTP { let stores = Stores::parse_all(&mut config, false).await; let core = Core::parse(&mut config, stores, Default::default()).await; let data = Data::parse(&mut config); + core.storage.data.destroy().await; Self::from_core_and_tempdir(core, data, Some(temp_dir)) } diff --git a/tests/src/smtp/outbound/dane.rs b/tests/src/smtp/outbound/dane.rs index a35241d5..28dc1b7d 100644 --- a/tests/src/smtp/outbound/dane.rs +++ b/tests/src/smtp/outbound/dane.rs @@ -107,7 +107,7 @@ async fn dane_verify() { ); let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session @@ -343,8 +343,8 @@ async fn dane_test() { assert_eq!( tlsa.verify(0, &host, Some(&certs)), Err(Status::PermanentFailure(Error::DaneError(ErrorDetails { - entity: host.to_string(), - details: "No matching certificates found in TLSA records".to_string() + entity: host.into(), + details: "No matching certificates found in TLSA records".into() }))) ); } diff --git a/tests/src/smtp/outbound/extensions.rs b/tests/src/smtp/outbound/extensions.rs index bfca2da4..3386a53a 100644 --- a/tests/src/smtp/outbound/extensions.rs +++ b/tests/src/smtp/outbound/extensions.rs @@ -77,7 +77,7 @@ async fn extensions() { ); let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session @@ -150,7 +150,7 @@ async fn extensions() { .try_deliver(core.clone()); local.queue_receiver.read_event().await.assert_done(); let message = remote.queue_receiver.expect_message().await; - assert_eq!(message.env_id, Some("abc123".to_string())); + assert_eq!(message.env_id, Some("abc123".into())); assert!((message.flags & MAIL_RET_HDRS) != 0); assert!((message.flags & MAIL_REQUIRETLS) != 0); assert!((message.flags & MAIL_SMTPUTF8) != 0); diff --git a/tests/src/smtp/outbound/fallback_relay.rs b/tests/src/smtp/outbound/fallback_relay.rs index d5446fce..943f0821 100644 --- a/tests/src/smtp/outbound/fallback_relay.rs +++ b/tests/src/smtp/outbound/fallback_relay.rs @@ -81,7 +81,7 @@ async fn fallback_relay() { ); let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session diff --git a/tests/src/smtp/outbound/ip_lookup.rs b/tests/src/smtp/outbound/ip_lookup.rs index ed6013fe..4a5c90b2 100644 --- a/tests/src/smtp/outbound/ip_lookup.rs +++ b/tests/src/smtp/outbound/ip_lookup.rs @@ -65,7 +65,7 @@ async fn ip_lookup_strategy() { // Retry on failed STARTTLS let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session diff --git a/tests/src/smtp/outbound/lmtp.rs b/tests/src/smtp/outbound/lmtp.rs index d9bd429e..63e71bbd 100644 --- a/tests/src/smtp/outbound/lmtp.rs +++ b/tests/src/smtp/outbound/lmtp.rs @@ -81,7 +81,7 @@ async fn lmtp_delivery() { ); let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session diff --git a/tests/src/smtp/outbound/mta_sts.rs b/tests/src/smtp/outbound/mta_sts.rs index 05df5a18..36740cf1 100644 --- a/tests/src/smtp/outbound/mta_sts.rs +++ b/tests/src/smtp/outbound/mta_sts.rs @@ -91,7 +91,7 @@ async fn mta_sts_verify() { ); let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session diff --git a/tests/src/smtp/outbound/smtp.rs b/tests/src/smtp/outbound/smtp.rs index 92815e35..67933243 100644 --- a/tests/src/smtp/outbound/smtp.rs +++ b/tests/src/smtp/outbound/smtp.rs @@ -105,7 +105,7 @@ async fn smtp_delivery() { } let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session @@ -242,7 +242,7 @@ async fn smtp_delivery() { // SMTP smuggling for separator in ["\n", "\r"].iter() { - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; diff --git a/tests/src/smtp/outbound/throttle.rs b/tests/src/smtp/outbound/throttle.rs index d7694a6e..639e53cb 100644 --- a/tests/src/smtp/outbound/throttle.rs +++ b/tests/src/smtp/outbound/throttle.rs @@ -67,13 +67,13 @@ async fn throttle_outbound() { // Build test message let mut test_message = new_message(0); - test_message.return_path_domain = "foobar.org".to_string(); + test_message.return_path_domain = "foobar.org".into(); let mut local = TestSMTP::new("smtp_throttle_outbound", CONFIG).await; let core = local.build_smtp(); let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session @@ -102,7 +102,7 @@ async fn throttle_outbound() { local.queue_receiver.read_event().await.assert_on_hold();*/ // Expect rate limit throttle for sender domain 'foobar.net' - test_message.return_path_domain = "foobar.net".to_string(); + test_message.return_path_domain = "foobar.net".into(); for t in &throttle.sender { core.is_allowed(t, &QueueEnvelope::test(&test_message, 0, ""), 0) .await @@ -123,9 +123,9 @@ async fn throttle_outbound() { assert!(due > 0, "Due: {}", due); // Expect concurrency throttle for recipient domain 'example.org' - test_message.return_path_domain = "test.net".to_string(); + test_message.return_path_domain = "test.net".into(); test_message.domains.push(Domain { - domain: "example.org".to_string(), + domain: "example.org".into(), retry: Schedule::now(), notify: Schedule::now(), expires: 0, @@ -155,7 +155,7 @@ async fn throttle_outbound() { // Expect rate limit throttle for recipient domain 'example.net' test_message.domains.push(Domain { - domain: "example.net".to_string(), + domain: "example.net".into(), retry: Schedule::now(), notify: Schedule::now(), expires: 0, @@ -189,7 +189,7 @@ async fn throttle_outbound() { core.mx_add( "test.org", vec![MX { - exchanges: vec!["mx.test.org".to_string()], + exchanges: vec!["mx.test.org".into()], preference: 10, }], Instant::now() + Duration::from_secs(10), @@ -200,7 +200,7 @@ async fn throttle_outbound() { Instant::now() + Duration::from_secs(10), ); test_message.domains.push(Domain { - domain: "test.org".to_string(), + domain: "test.org".into(), retry: Schedule::now(), notify: Schedule::now(), expires: 0, @@ -226,7 +226,7 @@ async fn throttle_outbound() { core.mx_add( "test.net", vec![MX { - exchanges: vec!["mx.test.net".to_string()], + exchanges: vec!["mx.test.net".into()], preference: 10, }], Instant::now() + Duration::from_secs(10), diff --git a/tests/src/smtp/outbound/tls.rs b/tests/src/smtp/outbound/tls.rs index e547fb20..b2cb51fb 100644 --- a/tests/src/smtp/outbound/tls.rs +++ b/tests/src/smtp/outbound/tls.rs @@ -70,7 +70,7 @@ async fn starttls_optional() { ); let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session diff --git a/tests/src/smtp/queue/concurrent.rs b/tests/src/smtp/queue/concurrent.rs index 604ca059..fc2c8f8f 100644 --- a/tests/src/smtp/queue/concurrent.rs +++ b/tests/src/smtp/queue/concurrent.rs @@ -75,7 +75,7 @@ async fn concurrent_queue() { ); let mut session = local.new_session(); - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; diff --git a/tests/src/smtp/queue/dsn.rs b/tests/src/smtp/queue/dsn.rs index 22177deb..3a845ec0 100644 --- a/tests/src/smtp/queue/dsn.rs +++ b/tests/src/smtp/queue/dsn.rs @@ -53,35 +53,35 @@ async fn generate_dsn() { created: SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .map_or(0, |d| d.as_secs()), - return_path: "sender@foobar.org".to_string(), - return_path_lcase: "".to_string(), - return_path_domain: "foobar.org".to_string(), + return_path: "sender@foobar.org".into(), + return_path_lcase: "".into(), + return_path_domain: "foobar.org".into(), recipients: vec![Recipient { domain_idx: 0, - address: "foobar@example.org".to_string(), - address_lcase: "foobar@example.org".to_string(), + address: "foobar@example.org".into(), + address_lcase: "foobar@example.org".into(), status: Status::PermanentFailure(HostResponse { hostname: ErrorDetails { - entity: "mx.example.org".to_string(), - details: "RCPT TO:".to_string(), + entity: "mx.example.org".into(), + details: "RCPT TO:".into(), }, response: Response { code: 550, esc: [5, 1, 2], - message: "User does not exist".to_string(), + message: "User does not exist".into(), }, }), flags: 0, orcpt: None, }], domains: vec![Domain { - domain: "example.org".to_string(), + domain: "example.org".into(), retry: Schedule::now(), notify: Schedule::now(), expires: now() + 10, status: Status::TemporaryFailure(Error::ConnectionError(ErrorDetails { - entity: "mx.domain.org".to_string(), - details: "Connection timeout".to_string(), + entity: "mx.domain.org".into(), + details: "Connection timeout".into(), })), }], flags: 0, @@ -116,14 +116,14 @@ async fn generate_dsn() { // Success DSN message.recipients.push(Recipient { domain_idx: 0, - address: "jane@example.org".to_string(), - address_lcase: "jane@example.org".to_string(), + address: "jane@example.org".into(), + address_lcase: "jane@example.org".into(), status: Status::Completed(HostResponse { - hostname: "mx2.example.org".to_string(), + hostname: "mx2.example.org".into(), response: Response { code: 250, esc: [2, 1, 5], - message: "Message accepted for delivery".to_string(), + message: "Message accepted for delivery".into(), }, }), flags, @@ -136,11 +136,11 @@ async fn generate_dsn() { // Delay DSN message.recipients.push(Recipient { domain_idx: 0, - address: "john.doe@example.org".to_string(), - address_lcase: "john.doe@example.org".to_string(), + address: "john.doe@example.org".into(), + address_lcase: "john.doe@example.org".into(), status: Status::Scheduled, flags, - orcpt: "jdoe@example.org".to_string().into(), + orcpt: Some("jdoe@example.org".into()), }); core.send_dsn(&mut message).await; let dsn_message = qr.expect_message().await; diff --git a/tests/src/smtp/queue/manager.rs b/tests/src/smtp/queue/manager.rs index 7aa03efe..7c0012dd 100644 --- a/tests/src/smtp/queue/manager.rs +++ b/tests/src/smtp/queue/manager.rs @@ -134,9 +134,9 @@ pub fn new_message(queue_id: u64) -> Message { queue_id, span_id: 0, created: 0, - return_path: "sender@foobar.org".to_string(), - return_path_lcase: "".to_string(), - return_path_domain: "foobar.org".to_string(), + return_path: "sender@foobar.org".into(), + return_path_lcase: "".into(), + return_path_domain: "foobar.org".into(), recipients: vec![], domains: vec![], flags: 0, @@ -149,7 +149,7 @@ pub fn new_message(queue_id: u64) -> Message { fn domain(domain: &str, retry: u64, notify: u64, expires: u64) -> Domain { Domain { - domain: domain.to_string(), + domain: domain.into(), retry: Schedule::later(Duration::from_secs(retry)), notify: Schedule::later(Duration::from_secs(notify)), expires: now() + expires, diff --git a/tests/src/smtp/queue/retry.rs b/tests/src/smtp/queue/retry.rs index c0c0b70a..d4ae5449 100644 --- a/tests/src/smtp/queue/retry.rs +++ b/tests/src/smtp/queue/retry.rs @@ -48,7 +48,7 @@ async fn queue_retry() { let mut session = local.new_session(); let qr = &mut local.queue_receiver; - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; session @@ -165,7 +165,7 @@ async fn queue_retry() { .assert_contains("Action: failed"); // Test FUTURERELEASE + DELIVERBY (RETURN) - session.data.remote_ip_str = "10.0.0.2".to_string(); + session.data.remote_ip_str = "10.0.0.2".into(); session.eval_session_params().await; session .send_message( diff --git a/tests/src/smtp/reporting/analyze.rs b/tests/src/smtp/reporting/analyze.rs index 145e3ab6..2eab99d8 100644 --- a/tests/src/smtp/reporting/analyze.rs +++ b/tests/src/smtp/reporting/analyze.rs @@ -37,7 +37,7 @@ async fn report_analyze() { // Create test message let mut session = local.new_session(); let qr = &mut local.queue_receiver; - session.data.remote_ip_str = "10.0.0.1".to_string(); + session.data.remote_ip_str = "10.0.0.1".into(); session.eval_session_params().await; session.ehlo("mx.test.org").await; diff --git a/tests/src/smtp/session.rs b/tests/src/smtp/session.rs index 635f61cb..dcb153c0 100644 --- a/tests/src/smtp/session.rs +++ b/tests/src/smtp/session.rs @@ -116,7 +116,7 @@ impl TestSession for Session { 0, ), params: SessionParameters::default(), - hostname: "localhost".to_string(), + hostname: "localhost".into(), } } @@ -223,38 +223,38 @@ impl TestSession for Session { let message = self .build_message( SessionAddress { - address: "bill@foobar.org".to_string(), - address_lcase: "bill@foobar.org".to_string(), - domain: "foobar.org".to_string(), + address: "bill@foobar.org".into(), + address_lcase: "bill@foobar.org".into(), + domain: "foobar.org".into(), flags: 123, - dsn_info: "envelope1".to_string().into(), + dsn_info: Some("envelope1".into()), }, vec![ SessionAddress { - address: "a@foobar.org".to_string(), - address_lcase: "a@foobar.org".to_string(), - domain: "foobar.org".to_string(), + address: "a@foobar.org".into(), + address_lcase: "a@foobar.org".into(), + domain: "foobar.org".into(), flags: 1, dsn_info: None, }, SessionAddress { - address: "b@test.net".to_string(), - address_lcase: "b@test.net".to_string(), - domain: "test.net".to_string(), + address: "b@test.net".into(), + address_lcase: "b@test.net".into(), + domain: "test.net".into(), flags: 2, dsn_info: None, }, SessionAddress { - address: "c@foobar.org".to_string(), - address_lcase: "c@foobar.org".to_string(), - domain: "foobar.org".to_string(), + address: "c@foobar.org".into(), + address_lcase: "c@foobar.org".into(), + domain: "foobar.org".into(), flags: 3, dsn_info: None, }, SessionAddress { - address: "d@test.net".to_string(), - address_lcase: "d@test.net".to_string(), - domain: "test.net".to_string(), + address: "d@test.net".into(), + address_lcase: "d@test.net".into(), + domain: "test.net".into(), flags: 4, dsn_info: None, },