Always use rsplit to extract the domain part from email addresses

This commit is contained in:
mdecimus 2025-09-30 11:23:36 +02:00
parent d00d3dd013
commit 5fcf73f070
14 changed files with 87 additions and 96 deletions

View file

@ -13,6 +13,7 @@ use store::{
write::{DirectoryClass, ValueClass}, write::{DirectoryClass, ValueClass},
}; };
use trc::AddContext; use trc::AddContext;
use utils::DomainPart;
#[allow(async_fn_in_trait)] #[allow(async_fn_in_trait)]
pub trait DirectoryStore: Sync + Send { pub trait DirectoryStore: Sync + Send {
@ -117,7 +118,7 @@ impl DirectoryStore for Store {
async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> { async fn vrfy(&self, address: &str) -> trc::Result<Vec<String>> {
let mut results = Vec::new(); 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 { if address.len() > 3 {
self.iterate( self.iterate(
IterateParams::new( IterateParams::new(
@ -129,7 +130,7 @@ impl DirectoryStore for Store {
|key, value| { |key, value| {
let key = let key =
std::str::from_utf8(key.get(1..).unwrap_or_default()).unwrap_or_default(); 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) && PrincipalInfo::deserialize(value)
.caused_by(trc::location!())? .caused_by(trc::location!())?
.typ .typ

View file

@ -27,7 +27,7 @@ use store::{
}; };
use trc::AddContext; use trc::AddContext;
use types::collection::Collection; use types::collection::Collection;
use utils::sanitize_email; use utils::{DomainPart, sanitize_email};
#[derive(Debug, Default, serde::Serialize, serde::Deserialize)] #[derive(Debug, Default, serde::Serialize, serde::Deserialize)]
pub struct PrincipalList<T> { pub struct PrincipalList<T> {
@ -354,7 +354,7 @@ impl ManageDirectory for Store {
principal_create.tenant = tenant_id.into(); principal_create.tenant = tenant_id.into();
if !matches!(principal_create.typ, Type::Tenant | Type::Domain) { 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 && self
.get_principal_info(domain) .get_principal_info(domain)
.await .await
@ -523,7 +523,7 @@ impl ManageDirectory for Store {
if self.rcpt(&email).await.caused_by(trc::location!())? != RcptType::Invalid { if self.rcpt(&email).await.caused_by(trc::location!())? != RcptType::Invalid {
return Err(err_exists(PrincipalField::Emails, email.to_string())); 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()) && valid_domains.insert(domain.into())
{ {
self.get_principal_info(domain) self.get_principal_info(domain)
@ -1003,7 +1003,7 @@ impl ManageDirectory for Store {
if tenant_id.is_some() if tenant_id.is_some()
&& !matches!(principal_type, Type::Tenant | Type::Domain) && !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 && self
.get_principal_info(domain) .get_principal_info(domain)
.await .await
@ -2401,7 +2401,7 @@ impl ValidateDirectory for Store {
) -> trc::Result<()> { ) -> trc::Result<()> {
if self.rcpt(email).await.caused_by(trc::location!())? != RcptType::Invalid { if self.rcpt(email).await.caused_by(trc::location!())? != RcptType::Invalid {
Err(err_exists(PrincipalField::Emails, email.to_string())) 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 match self
.get_principal_info(domain) .get_principal_info(domain)
.await .await

View file

@ -261,7 +261,7 @@ impl EmailAddress<'_> {
&self.address &self.address
}; };
if let Some((local, host)) = addr.split_once('@') { if let Some((local, host)) = addr.rsplit_once('@') {
quoted_or_literal_string(buf, local); quoted_or_literal_string(buf, local);
buf.push(b' '); buf.push(b' ');
quoted_or_literal_string(buf, host); quoted_or_literal_string(buf, host);

View file

@ -4,31 +4,26 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/ */
use std::{ use crate::{inbound::auth::SaslToken, queue::QueueId};
hash::Hash,
net::IpAddr,
sync::Arc,
time::{Duration, Instant},
};
use common::{ use common::{
Inner, Server, Inner, Server,
auth::AccessToken, auth::AccessToken,
config::smtp::auth::VerifyStrategy, config::smtp::auth::VerifyStrategy,
listener::{ServerInstance, asn::AsnGeoLookupResult}, listener::{ServerInstance, asn::AsnGeoLookupResult},
}; };
use directory::Directory; use directory::Directory;
use mail_auth::{IprevOutput, SpfOutput}; use mail_auth::{IprevOutput, SpfOutput};
use smtp_proto::request::receiver::{ use smtp_proto::request::receiver::{
BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver, BdatReceiver, DataReceiver, DummyDataReceiver, DummyLineReceiver, LineReceiver, RequestReceiver,
}; };
use tokio::io::{AsyncRead, AsyncWrite}; use std::{
hash::Hash,
use crate::{ net::IpAddr,
inbound::auth::SaslToken, sync::Arc,
queue::{DomainPart, QueueId}, time::{Duration, Instant},
}; };
use tokio::io::{AsyncRead, AsyncWrite};
use utils::DomainPart;
pub mod params; pub mod params;
pub mod throttle; pub mod throttle;

View file

@ -8,10 +8,7 @@ use super::{ArcSeal, AuthResult, DkimSign};
use crate::{ use crate::{
core::{Session, SessionAddress, State}, core::{Session, SessionAddress, State},
inbound::milter::Modification, inbound::milter::Modification,
queue::{ queue::{self, Message, MessageSource, MessageWrapper, QueueEnvelope, quota::HasQueueQuota},
self, DomainPart, Message, MessageSource, MessageWrapper, QueueEnvelope,
quota::HasQueueQuota,
},
reporting::analysis::AnalyzeReport, reporting::analysis::AnalyzeReport,
scripts::ScriptResult, scripts::ScriptResult,
}; };
@ -44,7 +41,7 @@ use std::{
time::{Instant, SystemTime}, time::{Instant, SystemTime},
}; };
use trc::SmtpEvent; use trc::SmtpEvent;
use utils::config::Rate; use utils::{DomainPart, config::Rate};
impl<T: SessionStream> Session<T> { impl<T: SessionStream> Session<T> {
pub async fn queue_message(&mut self) -> Cow<'static, [u8]> { pub async fn queue_message(&mut self) -> Cow<'static, [u8]> {

View file

@ -6,7 +6,6 @@
use crate::{ use crate::{
core::{Session, SessionAddress}, core::{Session, SessionAddress},
queue::DomainPart,
scripts::ScriptResult, scripts::ScriptResult,
}; };
use common::{config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification}; use common::{config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification};
@ -17,7 +16,7 @@ use std::{
time::{Duration, Instant, SystemTime}, time::{Duration, Instant, SystemTime},
}; };
use trc::SmtpEvent; use trc::SmtpEvent;
use utils::config::Rate; use utils::{DomainPart, config::Rate};
impl<T: SessionStream> Session<T> { impl<T: SessionStream> Session<T> {
pub async fn handle_mail_from(&mut self, from: MailFrom<Cow<'_, str>>) -> Result<(), ()> { pub async fn handle_mail_from(&mut self, from: MailFrom<Cow<'_, str>>) -> Result<(), ()> {

View file

@ -4,26 +4,22 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * 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::{ use common::{
DAEMON_NAME, DAEMON_NAME,
config::smtp::session::{Milter, Stage}, config::smtp::session::{Milter, Stage},
listener::SessionStream, listener::SessionStream,
}; };
use mail_auth::AuthenticatedMessage; use mail_auth::AuthenticatedMessage;
use smtp_proto::{IntoString, request::parser::Rfc5321Parser}; use smtp_proto::{IntoString, request::parser::Rfc5321Parser};
use std::{borrow::Cow, time::Instant};
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use trc::MilterEvent; use trc::MilterEvent;
use utils::DomainPart;
use crate::{
core::{Session, SessionAddress, SessionData},
inbound::{FilterResponse, milter::MilterClient},
queue::DomainPart,
};
use super::{Action, Error, Macros, Modification};
enum Rejection { enum Rejection {
Action(Action), Action(Action),

View file

@ -4,24 +4,21 @@
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL * SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-SEL
*/ */
use std::borrow::Cow; use crate::{
core::{Session, SessionAddress},
scripts::ScriptResult,
};
use common::{ use common::{
KV_GREYLIST, config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification, KV_GREYLIST, config::smtp::session::Stage, listener::SessionStream, scripts::ScriptModification,
}; };
use directory::backend::RcptType; use directory::backend::RcptType;
use smtp_proto::{ use smtp_proto::{
RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, RcptTo, RCPT_NOTIFY_DELAY, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS, RcptTo,
}; };
use std::borrow::Cow;
use store::dispatch::lookup::KeyValue; use store::dispatch::lookup::KeyValue;
use trc::{SecurityEvent, SmtpEvent}; use trc::{SecurityEvent, SmtpEvent};
use utils::DomainPart;
use crate::{
core::{Session, SessionAddress},
queue::DomainPart,
scripts::ScriptResult,
};
impl<T: SessionStream> Session<T> { impl<T: SessionStream> Session<T> {
pub async fn handle_rcpt_to(&mut self, to: RcptTo<Cow<'_, str>>) -> Result<(), ()> { pub async fn handle_rcpt_to(&mut self, to: RcptTo<Cow<'_, str>>) -> Result<(), ()> {

View file

@ -17,6 +17,7 @@ use std::{
}; };
use store::write::now; use store::write::now;
use types::blob_hash::BlobHash; use types::blob_hash::BlobHash;
use utils::DomainPart;
pub mod dsn; pub mod dsn;
pub mod manager; 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<T: AsRef<str>> 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 { impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {

View file

@ -5,10 +5,7 @@
*/ */
use super::{QueueEnvelope, QuotaKey, Status}; use super::{QueueEnvelope, QuotaKey, Status};
use crate::{ use crate::{core::throttle::NewKey, queue::MessageWrapper};
core::throttle::NewKey,
queue::{DomainPart, MessageWrapper},
};
use ahash::AHashSet; use ahash::AHashSet;
use common::{Server, config::smtp::queue::QueueQuota, expr::functions::ResolveVariable}; use common::{Server, config::smtp::queue::QueueQuota, expr::functions::ResolveVariable};
use std::future::Future; use std::future::Future;
@ -17,6 +14,7 @@ use store::{
write::{BatchBuilder, QueueClass, ValueClass}, write::{BatchBuilder, QueueClass, ValueClass},
}; };
use trc::QueueEvent; use trc::QueueEvent;
use utils::DomainPart;
pub trait HasQueueQuota: Sync + Send { pub trait HasQueueQuota: Sync + Send {
fn has_quota(&self, message: &mut MessageWrapper) -> impl Future<Output = bool> + Send; fn has_quota(&self, message: &mut MessageWrapper) -> impl Future<Output = bool> + Send;

View file

@ -10,8 +10,8 @@ use super::{
}; };
use crate::queue::manager::{LockedMessage, Queue}; use crate::queue::manager::{LockedMessage, Queue};
use crate::queue::{ use crate::queue::{
DomainPart, FROM_AUTHENTICATED, FROM_AUTOGENERATED, FROM_DSN, FROM_REPORT, FROM_AUTHENTICATED, FROM_AUTOGENERATED, FROM_DSN, FROM_REPORT, FROM_UNAUTHENTICATED,
FROM_UNAUTHENTICATED, FROM_UNAUTHENTICATED_DMARC, MessageWrapper, FROM_UNAUTHENTICATED_DMARC, MessageWrapper,
}; };
use common::config::smtp::queue::QueueName; use common::config::smtp::queue::QueueName;
use common::ipc::QueueEvent; use common::ipc::QueueEvent;
@ -28,6 +28,7 @@ use store::write::{
use store::{Deserialize, IterateParams, Serialize, SerializeInfallible, U64_LEN, ValueKey}; use store::{Deserialize, IterateParams, Serialize, SerializeInfallible, U64_LEN, ValueKey};
use trc::{AddContext, ServerEvent}; use trc::{AddContext, ServerEvent};
use types::blob_hash::BlobHash; use types::blob_hash::BlobHash;
use utils::DomainPart;
pub const LOCK_EXPIRY: u64 = 10 * 60; // 10 minutes pub const LOCK_EXPIRY: u64 = 10 * 60; // 10 minutes
pub const QUEUE_REFRESH: u64 = 5 * 60; // 5 minutes pub const QUEUE_REFRESH: u64 = 5 * 60; // 5 minutes

View file

@ -5,11 +5,7 @@
*/ */
use super::{AggregateTimestamp, SerializedSize}; use super::{AggregateTimestamp, SerializedSize};
use crate::{ use crate::{core::Session, queue::RecipientDomain, reporting::SmtpReporting};
core::Session,
queue::{DomainPart, RecipientDomain},
reporting::SmtpReporting,
};
use ahash::AHashMap; use ahash::AHashMap;
use common::{ use common::{
Server, Server,
@ -31,7 +27,7 @@ use store::{
write::{AlignedBytes, Archive, Archiver, BatchBuilder, QueueClass, ReportEvent, ValueClass}, write::{AlignedBytes, Archive, Archiver, BatchBuilder, QueueClass, ReportEvent, ValueClass},
}; };
use trc::{AddContext, OutgoingReportEvent}; use trc::{AddContext, OutgoingReportEvent};
use utils::config::Rate; use utils::{DomainPart, config::Rate};
#[derive( #[derive(
Debug, Debug,

View file

@ -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, 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, RCPT_NOTIFY_FAILURE, RCPT_NOTIFY_NEVER, RCPT_NOTIFY_SUCCESS,
}; };
use utils::DomainPart;
use crate::{ use crate::core::{SessionAddress, SessionData};
core::{SessionAddress, SessionData},
queue::DomainPart,
};
impl SessionData { impl SessionData {
pub fn apply_envelope_modification(&mut self, envelope: Envelope, value: String) { pub fn apply_envelope_modification(&mut self, envelope: Envelope, value: String) {

View file

@ -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<T: AsRef<str>> 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)] #[derive(Debug)]
struct DummyVerifier; struct DummyVerifier;