diff --git a/crates/directory/src/backend/internal/lookup.rs b/crates/directory/src/backend/internal/lookup.rs index 01ce9f1a..c8e1ec41 100644 --- a/crates/directory/src/backend/internal/lookup.rs +++ b/crates/directory/src/backend/internal/lookup.rs @@ -13,6 +13,7 @@ use store::{ write::{DirectoryClass, ValueClass}, }; use trc::AddContext; +use utils::DomainPart; #[allow(async_fn_in_trait)] pub trait DirectoryStore: Sync + Send { @@ -117,7 +118,7 @@ impl DirectoryStore for Store { async fn vrfy(&self, address: &str) -> trc::Result> { let mut results = Vec::new(); - let address = address.split('@').next().unwrap_or(address); + let address = address.try_local_part().unwrap_or(address); if address.len() > 3 { self.iterate( IterateParams::new( @@ -129,7 +130,7 @@ impl DirectoryStore for Store { |key, value| { let key = std::str::from_utf8(key.get(1..).unwrap_or_default()).unwrap_or_default(); - if key.split('@').next().unwrap_or(key).contains(address) + if key.try_local_part().unwrap_or(key).contains(address) && PrincipalInfo::deserialize(value) .caused_by(trc::location!())? .typ diff --git a/crates/directory/src/backend/internal/manage.rs b/crates/directory/src/backend/internal/manage.rs index 77e1a37c..187f2889 100644 --- a/crates/directory/src/backend/internal/manage.rs +++ b/crates/directory/src/backend/internal/manage.rs @@ -27,7 +27,7 @@ use store::{ }; use trc::AddContext; use types::collection::Collection; -use utils::sanitize_email; +use utils::{DomainPart, sanitize_email}; #[derive(Debug, Default, serde::Serialize, serde::Deserialize)] pub struct PrincipalList { @@ -354,7 +354,7 @@ impl ManageDirectory for Store { principal_create.tenant = tenant_id.into(); if !matches!(principal_create.typ, Type::Tenant | Type::Domain) { - if let Some(domain) = name.split('@').nth(1) + if let Some(domain) = name.try_domain_part() && self .get_principal_info(domain) .await @@ -523,7 +523,7 @@ impl ManageDirectory for Store { 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 let Some(domain) = email.try_domain_part() && valid_domains.insert(domain.into()) { self.get_principal_info(domain) @@ -1003,7 +1003,7 @@ impl ManageDirectory for Store { if tenant_id.is_some() && !matches!(principal_type, Type::Tenant | Type::Domain) { - if let Some(domain) = new_name.split('@').nth(1) + if let Some(domain) = new_name.try_domain_part() && self .get_principal_info(domain) .await @@ -2401,7 +2401,7 @@ impl ValidateDirectory for Store { ) -> trc::Result<()> { if self.rcpt(email).await.caused_by(trc::location!())? != RcptType::Invalid { Err(err_exists(PrincipalField::Emails, email.to_string())) - } else if let Some(domain) = email.split('@').nth(1) { + } else if let Some(domain) = email.try_domain_part() { match self .get_principal_info(domain) .await diff --git a/crates/imap-proto/src/protocol/fetch.rs b/crates/imap-proto/src/protocol/fetch.rs index 6e0be0dc..61338c54 100644 --- a/crates/imap-proto/src/protocol/fetch.rs +++ b/crates/imap-proto/src/protocol/fetch.rs @@ -261,7 +261,7 @@ impl EmailAddress<'_> { &self.address }; - if let Some((local, host)) = addr.split_once('@') { + if let Some((local, host)) = addr.rsplit_once('@') { quoted_or_literal_string(buf, local); buf.push(b' '); quoted_or_literal_string(buf, host); diff --git a/crates/smtp/src/core/mod.rs b/crates/smtp/src/core/mod.rs index 6daf546c..82202680 100644 --- a/crates/smtp/src/core/mod.rs +++ b/crates/smtp/src/core/mod.rs @@ -4,31 +4,26 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{ - hash::Hash, - net::IpAddr, - sync::Arc, - time::{Duration, Instant}, -}; - +use crate::{inbound::auth::SaslToken, queue::QueueId}; use common::{ Inner, Server, auth::AccessToken, config::smtp::auth::VerifyStrategy, listener::{ServerInstance, asn::AsnGeoLookupResult}, }; - use directory::Directory; use mail_auth::{IprevOutput, SpfOutput}; use smtp_proto::request::receiver::{ BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver, }; -use tokio::io::{AsyncRead, AsyncWrite}; - -use crate::{ - inbound::auth::SaslToken, - queue::{DomainPart, QueueId}, +use std::{ + hash::Hash, + net::IpAddr, + sync::Arc, + time::{Duration, Instant}, }; +use tokio::io::{AsyncRead, AsyncWrite}; +use utils::DomainPart; pub mod params; pub mod throttle; diff --git a/crates/smtp/src/inbound/data.rs b/crates/smtp/src/inbound/data.rs index fd30a35d..00763140 100644 --- a/crates/smtp/src/inbound/data.rs +++ b/crates/smtp/src/inbound/data.rs @@ -8,10 +8,7 @@ use super::{ArcSeal, AuthResult, DkimSign}; use crate::{ core::{Session, SessionAddress, State}, inbound::milter::Modification, - queue::{ - self, DomainPart, Message, MessageSource, MessageWrapper, QueueEnvelope, - quota::HasQueueQuota, - }, + queue::{self, Message, MessageSource, MessageWrapper, QueueEnvelope, quota::HasQueueQuota}, reporting::analysis::AnalyzeReport, scripts::ScriptResult, }; @@ -44,7 +41,7 @@ use std::{ time::{Instant, SystemTime}, }; use trc::SmtpEvent; -use utils::config::Rate; +use utils::{DomainPart, config::Rate}; impl Session { pub async fn queue_message(&mut self) -> Cow<'static, [u8]> { diff --git a/crates/smtp/src/inbound/mail.rs b/crates/smtp/src/inbound/mail.rs index 29aa6637..00c83824 100644 --- a/crates/smtp/src/inbound/mail.rs +++ b/crates/smtp/src/inbound/mail.rs @@ -6,7 +6,6 @@ use crate::{ core::{Session, SessionAddress}, - queue::DomainPart, scripts::ScriptResult, }; use common::{config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification}; @@ -17,7 +16,7 @@ use std::{ time::{Duration, Instant, SystemTime}, }; use trc::SmtpEvent; -use utils::config::Rate; +use utils::{DomainPart, config::Rate}; impl Session { pub async fn handle_mail_from(&mut self, from: MailFrom>) -> Result<(), ()> { diff --git a/crates/smtp/src/inbound/milter/message.rs b/crates/smtp/src/inbound/milter/message.rs index 6d221e67..f087aca0 100644 --- a/crates/smtp/src/inbound/milter/message.rs +++ b/crates/smtp/src/inbound/milter/message.rs @@ -4,26 +4,22 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::{borrow::Cow, time::Instant}; - +use super::{Action, Error, Macros, Modification}; +use crate::{ + core::{Session, SessionAddress, SessionData}, + inbound::{FilterResponse, milter::MilterClient}, +}; use common::{ DAEMON_NAME, config::smtp::session::{Milter, Stage}, listener::SessionStream, }; - use mail_auth::AuthenticatedMessage; use smtp_proto::{IntoString, request::parser::Rfc5321Parser}; +use std::{borrow::Cow, time::Instant}; use tokio::io::{AsyncRead, AsyncWrite}; use trc::MilterEvent; - -use crate::{ - core::{Session, SessionAddress, SessionData}, - inbound::{FilterResponse, milter::MilterClient}, - queue::DomainPart, -}; - -use super::{Action, Error, Macros, Modification}; +use utils::DomainPart; enum Rejection { Action(Action), diff --git a/crates/smtp/src/inbound/rcpt.rs b/crates/smtp/src/inbound/rcpt.rs index b1300c1c..ab292e28 100644 --- a/crates/smtp/src/inbound/rcpt.rs +++ b/crates/smtp/src/inbound/rcpt.rs @@ -4,24 +4,21 @@ * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL */ -use std::borrow::Cow; - +use crate::{ + core::{Session, SessionAddress}, + scripts::ScriptResult, +}; use common::{ KV_GREYLIST, config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification, }; - use directory::backend::RcptType; use smtp_proto::{ RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, RcptTo, }; +use std::borrow::Cow; use store::dispatch::lookup::KeyValue; use trc::{SecurityEvent, SmtpEvent}; - -use crate::{ - core::{Session, SessionAddress}, - queue::DomainPart, - scripts::ScriptResult, -}; +use utils::DomainPart; impl Session { pub async fn handle_rcpt_to(&mut self, to: RcptTo>) -> Result<(), ()> { diff --git a/crates/smtp/src/queue/mod.rs b/crates/smtp/src/queue/mod.rs index 991440a2..33003674 100644 --- a/crates/smtp/src/queue/mod.rs +++ b/crates/smtp/src/queue/mod.rs @@ -17,6 +17,7 @@ use std::{ }; use store::write::now; use types::blob_hash::BlobHash; +use utils::DomainPart; pub mod dsn; pub mod manager; @@ -460,38 +461,6 @@ impl InstantFromTimestamp for u64 { } } -pub trait DomainPart { - fn to_lowercase_domain(&self) -> String; - fn domain_part(&self) -> &str; -} - -impl> DomainPart for T { - fn to_lowercase_domain(&self) -> String { - let address = self.as_ref(); - if let Some((local, domain)) = address.rsplit_once('@') { - let mut address = String::with_capacity(address.len()); - address.push_str(local); - address.push('@'); - for ch in domain.chars() { - for ch in ch.to_lowercase() { - address.push(ch); - } - } - address - } else { - address.to_string() - } - } - - #[inline(always)] - fn domain_part(&self) -> &str { - self.as_ref() - .rsplit_once('@') - .map(|(_, d)| d) - .unwrap_or_default() - } -} - impl Display for Error { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { diff --git a/crates/smtp/src/queue/quota.rs b/crates/smtp/src/queue/quota.rs index cb9e21de..f37fb007 100644 --- a/crates/smtp/src/queue/quota.rs +++ b/crates/smtp/src/queue/quota.rs @@ -5,10 +5,7 @@ */ use super::{QueueEnvelope, QuotaKey, Status}; -use crate::{ - core::throttle::NewKey, - queue::{DomainPart, MessageWrapper}, -}; +use crate::{core::throttle::NewKey, queue::MessageWrapper}; use ahash::AHashSet; use common::{Server, config::smtp::queue::QueueQuota, expr::functions::ResolveVariable}; use std::future::Future; @@ -17,6 +14,7 @@ use store::{ write::{BatchBuilder, QueueClass, ValueClass}, }; use trc::QueueEvent; +use utils::DomainPart; pub trait HasQueueQuota: Sync + Send { fn has_quota(&self, message: &mut MessageWrapper) -> impl Future + Send; diff --git a/crates/smtp/src/queue/spool.rs b/crates/smtp/src/queue/spool.rs index 8a107f37..3e0ec169 100644 --- a/crates/smtp/src/queue/spool.rs +++ b/crates/smtp/src/queue/spool.rs @@ -10,8 +10,8 @@ use super::{ }; use crate::queue::manager::{LockedMessage, Queue}; use crate::queue::{ - DomainPart, FROM_AUTHENTICATED, FROM_AUTOGENERATED, FROM_DSN, FROM_REPORT, - FROM_UNAUTHENTICATED, FROM_UNAUTHENTICATED_DMARC, MessageWrapper, + FROM_AUTHENTICATED, FROM_AUTOGENERATED, FROM_DSN, FROM_REPORT, FROM_UNAUTHENTICATED, + FROM_UNAUTHENTICATED_DMARC, MessageWrapper, }; use common::config::smtp::queue::QueueName; use common::ipc::QueueEvent; @@ -28,6 +28,7 @@ use store::write::{ use store::{Deserialize, IterateParams, Serialize, SerializeInfallible, U64_LEN, ValueKey}; use trc::{AddContext, ServerEvent}; use types::blob_hash::BlobHash; +use utils::DomainPart; pub const LOCK_EXPIRY: u64 = 10 * 60; // 10 minutes pub const QUEUE_REFRESH: u64 = 5 * 60; // 5 minutes diff --git a/crates/smtp/src/reporting/dmarc.rs b/crates/smtp/src/reporting/dmarc.rs index b4cdfbf8..154858b7 100644 --- a/crates/smtp/src/reporting/dmarc.rs +++ b/crates/smtp/src/reporting/dmarc.rs @@ -5,11 +5,7 @@ */ use super::{AggregateTimestamp, SerializedSize}; -use crate::{ - core::Session, - queue::{DomainPart, RecipientDomain}, - reporting::SmtpReporting, -}; +use crate::{core::Session, queue::RecipientDomain, reporting::SmtpReporting}; use ahash::AHashMap; use common::{ Server, @@ -31,7 +27,7 @@ use store::{ write::{AlignedBytes, Archive, Archiver, BatchBuilder, QueueClass, ReportEvent, ValueClass}, }; use trc::{AddContext, OutgoingReportEvent}; -use utils::config::Rate; +use utils::{DomainPart, config::Rate}; #[derive( Debug, diff --git a/crates/smtp/src/scripts/envelope.rs b/crates/smtp/src/scripts/envelope.rs index 980594f2..d6e9bbd1 100644 --- a/crates/smtp/src/scripts/envelope.rs +++ b/crates/smtp/src/scripts/envelope.rs @@ -9,11 +9,9 @@ use smtp_proto::{ MAIL_BY_NOTIFY, MAIL_BY_RETURN, MAIL_BY_TRACE, MAIL_RET_FULL, MAIL_RET_HDRS, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, }; +use utils::DomainPart; -use crate::{ - core::{SessionAddress, SessionData}, - queue::DomainPart, -}; +use crate::core::{SessionAddress, SessionData}; impl SessionData { pub fn apply_envelope_modification(&mut self, envelope: Envelope, value: String) { diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 2b7b9a4d..4957bee6 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -164,6 +164,50 @@ pub fn rustls_client_config(allow_invalid_certs: bool) -> ClientConfig { } } +pub trait DomainPart { + fn to_lowercase_domain(&self) -> String; + fn domain_part(&self) -> &str; + fn try_domain_part(&self) -> Option<&str>; + fn try_local_part(&self) -> Option<&str>; +} + +impl> DomainPart for T { + fn to_lowercase_domain(&self) -> String { + let address = self.as_ref(); + if let Some((local, domain)) = address.rsplit_once('@') { + let mut address = String::with_capacity(address.len()); + address.push_str(local); + address.push('@'); + for ch in domain.chars() { + for ch in ch.to_lowercase() { + address.push(ch); + } + } + address + } else { + address.to_string() + } + } + + #[inline(always)] + fn try_domain_part(&self) -> Option<&str> { + self.as_ref().rsplit_once('@').map(|(_, d)| d) + } + + #[inline(always)] + fn try_local_part(&self) -> Option<&str> { + self.as_ref().rsplit_once('@').map(|(l, _)| l) + } + + #[inline(always)] + fn domain_part(&self) -> &str { + self.as_ref() + .rsplit_once('@') + .map(|(_, d)| d) + .unwrap_or_default() + } +} + #[derive(Debug)] struct DummyVerifier;