MTA: Do not convert e-mail local parts to lowercase (fixes #1916)

This commit is contained in:
mdecimus
2025-07-27 11:26:02 +02:00
parent ce98e6d0ff
commit a2ea0f6cee
23 changed files with 161 additions and 117 deletions

View File

@@ -39,7 +39,7 @@ impl SendDsn for Server {
if let Some(dsn) = message.build_dsn(self).await {
let mut dsn_message = self.new_message("", message.span_id);
dsn_message
.add_recipient_parts(message.message.return_path.as_str(), self)
.add_recipient(message.message.return_path.as_str(), self)
.await;
// Sign message

View File

@@ -399,6 +399,49 @@ pub fn instant_to_timestamp(now: Instant, time: Instant) -> u64 {
+ time.checked_duration_since(now).map_or(0, |d| d.as_secs())
}
impl Recipient {
pub fn new(address: impl AsRef<str>) -> Self {
Recipient {
address: address.to_lowercase_domain(),
status: Status::Scheduled,
flags: 0,
orcpt: None,
retry: Schedule::now(),
notify: Schedule::now(),
expires: QueueExpiry::Attempts(0),
queue: QueueName::default(),
}
}
pub fn with_flags(mut self, flags: u64) -> Self {
self.flags = flags;
self
}
pub fn with_orcpt(mut self, orcpt: Option<String>) -> Self {
self.orcpt = orcpt;
self
}
pub fn address(&self) -> &str {
&self.address
}
pub fn domain_part(&self) -> &str {
self.address.domain_part()
}
}
impl ArchivedRecipient {
pub fn address(&self) -> &str {
self.address.as_str()
}
pub fn domain_part(&self) -> &str {
self.address.domain_part()
}
}
pub trait InstantFromTimestamp {
fn to_instant(&self) -> Instant;
}
@@ -418,10 +461,28 @@ 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()

View File

@@ -13,7 +13,7 @@ use crate::queue::{
DomainPart, FROM_AUTHENTICATED, FROM_AUTOGENERATED, FROM_DSN, FROM_REPORT,
FROM_UNAUTHENTICATED, FROM_UNAUTHENTICATED_DMARC, MessageWrapper,
};
use common::config::smtp::queue::{QueueExpiry, QueueName};
use common::config::smtp::queue::QueueName;
use common::ipc::QueueEvent;
use common::{KV_LOCK_QUEUE_MESSAGE, Server};
use std::borrow::Cow;
@@ -39,7 +39,7 @@ pub struct QueuedMessages {
}
pub trait SmtpSpool: Sync + Send {
fn new_message(&self, return_path: impl Into<String>, span_id: u64) -> MessageWrapper;
fn new_message(&self, return_path: impl AsRef<str>, span_id: u64) -> MessageWrapper;
fn next_event(&self, queue: &mut Queue) -> impl Future<Output = QueuedMessages> + Send;
@@ -68,7 +68,7 @@ pub trait SmtpSpool: Sync + Send {
}
impl SmtpSpool for Server {
fn new_message(&self, return_path: impl Into<String>, span_id: u64) -> MessageWrapper {
fn new_message(&self, return_path: impl AsRef<str>, span_id: u64) -> MessageWrapper {
let created = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.map_or(0, |d| d.as_secs());
@@ -80,7 +80,7 @@ impl SmtpSpool for Server {
span_id,
message: Message {
created,
return_path: return_path.into(),
return_path: return_path.to_lowercase_domain(),
recipients: Vec::with_capacity(1),
flags: 0,
env_id: None,
@@ -483,18 +483,9 @@ impl MessageWrapper {
true
}
pub async fn add_recipient_parts(&mut self, rcpt: impl Into<String>, server: &Server) {
pub async fn add_recipient(&mut self, rcpt: impl AsRef<str>, server: &Server) {
// Resolve queue
self.message.recipients.push(Recipient {
address: rcpt.into(),
status: Status::Scheduled,
flags: 0,
orcpt: None,
retry: Schedule::now(),
notify: Schedule::now(),
expires: QueueExpiry::Attempts(0),
queue: QueueName::default(),
});
self.message.recipients.push(Recipient::new(rcpt));
let queue = server.get_queue_or_default(
&server
.eval_if::<String, _>(
@@ -515,11 +506,6 @@ impl MessageWrapper {
recipient.queue = queue.virtual_queue;
}
pub async fn add_recipient(&mut self, rcpt: impl AsRef<str>, server: &Server) {
let rcpt = rcpt.as_ref().to_lowercase();
self.add_recipient_parts(rcpt, server).await;
}
pub async fn save_changes(mut self, server: &Server, prev_event: Option<u64>) -> bool {
// Release quota for completed deliveries
let mut batch = BatchBuilder::new();