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},
};
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<Vec<String>> {
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

View file

@ -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<T> {
@ -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

View file

@ -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);

View file

@ -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;

View file

@ -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<T: SessionStream> Session<T> {
pub async fn queue_message(&mut self) -> Cow<'static, [u8]> {

View file

@ -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<T: SessionStream> Session<T> {
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
*/
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),

View file

@ -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<T: SessionStream> Session<T> {
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 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<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 {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {

View file

@ -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<Output = bool> + Send;

View file

@ -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

View file

@ -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,

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,
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) {

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)]
struct DummyVerifier;