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

@ -294,7 +294,7 @@ impl QueueManagement for Server {
Status::Scheduled | Status::TemporaryFailure(_) Status::Scheduled | Status::TemporaryFailure(_)
) && item ) && item
.as_ref() .as_ref()
.is_none_or(|item| recipient.address.contains(item)) .is_none_or(|item| recipient.address().contains(item))
{ {
recipient.retry.due = time; recipient.retry.due = time;
if recipient if recipient
@ -383,7 +383,7 @@ impl QueueManagement for Server {
if let Some(item) = params.get("filter") { if let Some(item) = params.get("filter") {
// Cancel delivery for all recipients that match // Cancel delivery for all recipients that match
for rcpt in &mut message.message.recipients { for rcpt in &mut message.message.recipients {
if rcpt.address.contains(item) { if rcpt.address().contains(item) {
rcpt.status = Status::PermanentFailure(ErrorDetails { rcpt.status = Status::PermanentFailure(ErrorDetails {
entity: "localhost".to_string(), entity: "localhost".to_string(),
details: queue::Error::Io("Delivery canceled.".to_string()), details: queue::Error::Io("Delivery canceled.".to_string()),
@ -585,7 +585,7 @@ impl Message {
.recipients .recipients
.iter() .iter()
.map(|rcpt| Recipient { .map(|rcpt| Recipient {
address: rcpt.address.to_string(), address: rcpt.address().to_string(),
queue: rcpt.queue.to_string(), queue: rcpt.queue.to_string(),
status: match &rcpt.status { status: match &rcpt.status {
ArchivedStatus::Scheduled => Status::Scheduled, ArchivedStatus::Scheduled => Status::Scheduled,
@ -685,13 +685,16 @@ async fn fetch_queued_messages(
.as_ref() .as_ref()
.map(|text| { .map(|text| {
message.return_path.contains(text) message.return_path.contains(text)
|| message.recipients.iter().any(|r| r.address.contains(text)) || message
.recipients
.iter()
.any(|r| r.address().contains(text))
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
from.as_ref() from.as_ref()
.is_none_or(|from| message.return_path.contains(from)) .is_none_or(|from| message.return_path.contains(from))
&& to.as_ref().is_none_or(|to| { && to.as_ref().is_none_or(|to| {
message.recipients.iter().any(|r| r.address.contains(to)) message.recipients.iter().any(|r| r.address().contains(to))
}) })
}) })
&& before.as_ref().is_none_or(|before| { && before.as_ref().is_none_or(|before| {

View file

@ -113,7 +113,7 @@ impl EmailSubmissionGet for Server {
.unarchive::<Message>() .unarchive::<Message>()
.caused_by(trc::location!())?; .caused_by(trc::location!())?;
for rcpt in queued_message.recipients.iter() { for rcpt in queued_message.recipients.iter() {
*delivery_status.get_mut_or_insert(rcpt.address.to_string()) = *delivery_status.get_mut_or_insert(rcpt.address().to_string()) =
DeliveryStatus { DeliveryStatus {
smtp_reply: match &rcpt.status { smtp_reply: match &rcpt.status {
ArchivedStatus::Completed(reply) => { ArchivedStatus::Completed(reply) => {

View file

@ -278,33 +278,32 @@ where
.into_iter() .into_iter()
.map(|r| { .map(|r| {
let domain = &domains[r.domain_idx.as_u64() as usize]; let domain = &domains[r.domain_idx.as_u64() as usize];
Recipient { let mut rcpt = Recipient::new(r.address);
address: r.address_lcase, rcpt.status = match r.status {
status: match r.status { Status::Scheduled => match &domain.status {
Status::Scheduled => match &domain.status { Status::Scheduled | Status::Completed(_) => Status::Scheduled,
Status::Scheduled | Status::Completed(_) => Status::Scheduled,
Status::TemporaryFailure(err) => Status::TemporaryFailure(
migrate_legacy_error(&domain.domain, err),
),
Status::PermanentFailure(err) => Status::PermanentFailure(
migrate_legacy_error(&domain.domain, err),
),
},
Status::Completed(details) => Status::Completed(details),
Status::TemporaryFailure(err) => { Status::TemporaryFailure(err) => {
Status::TemporaryFailure(migrate_host_response(err)) Status::TemporaryFailure(migrate_legacy_error(&domain.domain, err))
} }
Status::PermanentFailure(err) => { Status::PermanentFailure(err) => {
Status::PermanentFailure(migrate_host_response(err)) Status::PermanentFailure(migrate_legacy_error(&domain.domain, err))
} }
}, },
flags: r.flags, Status::Completed(details) => Status::Completed(details),
orcpt: r.orcpt, Status::TemporaryFailure(err) => {
retry: domain.retry.clone(), Status::TemporaryFailure(migrate_host_response(err))
notify: domain.notify.clone(), }
queue: QueueName::default(), Status::PermanentFailure(err) => {
expires: QueueExpiry::Ttl(domain.expires.saturating_sub(now())), Status::PermanentFailure(migrate_host_response(err))
} }
};
rcpt.flags = r.flags;
rcpt.orcpt = r.orcpt;
rcpt.retry = domain.retry.clone();
rcpt.notify = domain.notify.clone();
rcpt.queue = QueueName::default();
rcpt.expires = QueueExpiry::Ttl(domain.expires.saturating_sub(now()));
rcpt
}) })
.collect(), .collect(),
flags: message.flags, flags: message.flags,

View file

@ -9,7 +9,8 @@ use crate::{
core::{Session, SessionAddress, State}, core::{Session, SessionAddress, State},
inbound::milter::Modification, inbound::milter::Modification,
queue::{ queue::{
self, Message, MessageSource, MessageWrapper, QueueEnvelope, Schedule, quota::HasQueueQuota, self, DomainPart, Message, MessageSource, MessageWrapper, QueueEnvelope,
quota::HasQueueQuota,
}, },
reporting::analysis::AnalyzeReport, reporting::analysis::AnalyzeReport,
scripts::ScriptResult, scripts::ScriptResult,
@ -709,7 +710,7 @@ impl<T: SessionStream> Session<T> {
.map_or(0, |d| d.as_secs()); .map_or(0, |d| d.as_secs());
let mut message = Message { let mut message = Message {
created, created,
return_path: mail_from.address_lcase, return_path: mail_from.address.to_lowercase_domain(),
recipients: Vec::with_capacity(rcpt_to.len()), recipients: Vec::with_capacity(rcpt_to.len()),
flags: mail_from.flags, flags: mail_from.flags,
priority: self.data.priority, priority: self.data.priority,
@ -725,26 +726,23 @@ impl<T: SessionStream> Session<T> {
let future_release = self.data.future_release; let future_release = self.data.future_release;
rcpt_to.sort_unstable(); rcpt_to.sort_unstable();
for rcpt in rcpt_to { for rcpt in rcpt_to {
message.recipients.push(queue::Recipient { message.recipients.push(
address: rcpt.address_lcase, queue::Recipient::new(rcpt.address)
status: queue::Status::Scheduled, .with_flags(
flags: if rcpt.flags if rcpt.flags
& (RCPT_NOTIFY_DELAY & (RCPT_NOTIFY_DELAY
| RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_FAILURE
| RCPT_NOTIFY_SUCCESS | RCPT_NOTIFY_SUCCESS
| RCPT_NOTIFY_NEVER) | RCPT_NOTIFY_NEVER)
!= 0 != 0
{ {
rcpt.flags rcpt.flags
} else { } else {
rcpt.flags | RCPT_NOTIFY_DELAY | RCPT_NOTIFY_FAILURE rcpt.flags | RCPT_NOTIFY_DELAY | RCPT_NOTIFY_FAILURE
}, },
orcpt: rcpt.dsn_info, )
retry: Schedule::now(), .with_orcpt(rcpt.dsn_info),
notify: Schedule::now(), );
expires: QueueExpiry::Attempts(0),
queue: QueueName::default(),
});
let envelope = QueueEnvelope::new(&message, message.recipients.last().unwrap()); let envelope = QueueEnvelope::new(&message, message.recipients.last().unwrap());

View file

@ -18,8 +18,7 @@ use crate::queue::dsn::SendDsn;
use crate::queue::spool::SmtpSpool; use crate::queue::spool::SmtpSpool;
use crate::queue::throttle::IsAllowed; use crate::queue::throttle::IsAllowed;
use crate::queue::{ use crate::queue::{
DomainPart, Error, FROM_REPORT, HostResponse, MessageWrapper, QueueEnvelope, QueuedMessage, Error, FROM_REPORT, HostResponse, MessageWrapper, QueueEnvelope, QueuedMessage, Status,
Status,
}; };
use crate::reporting::SmtpReporting; use crate::reporting::SmtpReporting;
use crate::{queue::ErrorDetails, reporting::tls::TlsRptOptions}; use crate::{queue::ErrorDetails, reporting::tls::TlsRptOptions};
@ -74,7 +73,7 @@ impl QueuedMessage {
Status::Scheduled | Status::TemporaryFailure(_) Status::Scheduled | Status::TemporaryFailure(_)
) && r.queue == message.queue_name ) && r.queue == message.queue_name
{ {
Some(trc::Value::String(r.address.as_str().into())) Some(trc::Value::String(r.address().into()))
} else { } else {
None None
} }
@ -236,7 +235,7 @@ impl QueuedMessage {
); );
routes routes
.entry((rcpt.address.domain_part(), route)) .entry((rcpt.domain_part(), route))
.or_default() .or_default()
.push(rcpt_idx); .push(rcpt_idx);
} }
@ -1325,7 +1324,7 @@ impl MessageWrapper {
SpanId = self.span_id, SpanId = self.span_id,
QueueId = self.queue_id, QueueId = self.queue_id,
QueueName = self.queue_name.as_str().to_string(), QueueName = self.queue_name.as_str().to_string(),
To = rcpt.address.clone(), To = rcpt.address().to_string(),
Reason = from_error_details(&err.details), Reason = from_error_details(&err.details),
Details = trc::Value::Timestamp(now), Details = trc::Value::Timestamp(now),
Expires = rcpt Expires = rcpt
@ -1344,7 +1343,7 @@ impl MessageWrapper {
SpanId = self.span_id, SpanId = self.span_id,
QueueId = self.queue_id, QueueId = self.queue_id,
QueueName = self.queue_name.as_str().to_string(), QueueName = self.queue_name.as_str().to_string(),
To = rcpt.address.clone(), To = rcpt.address().to_string(),
Reason = "Message expired without any delivery attempts made.", Reason = "Message expired without any delivery attempts made.",
Details = trc::Value::Timestamp(now), Details = trc::Value::Timestamp(now),
Expires = rcpt Expires = rcpt
@ -1355,7 +1354,7 @@ impl MessageWrapper {
); );
rcpt.status = Status::PermanentFailure(ErrorDetails { rcpt.status = Status::PermanentFailure(ErrorDetails {
entity: rcpt.address.domain_part().to_string(), entity: rcpt.domain_part().to_string(),
details: Error::Io( details: Error::Io(
"Message expired without any delivery attempts made.".into(), "Message expired without any delivery attempts made.".into(),
), ),

View file

@ -29,8 +29,8 @@ impl MessageWrapper {
let mut pending_recipients = Vec::new(); let mut pending_recipients = Vec::new();
let mut recipient_addresses = Vec::new(); let mut recipient_addresses = Vec::new();
for &rcpt_idx in rcpt_idxs { for &rcpt_idx in rcpt_idxs {
let rcpt_addr = &self.message.recipients[rcpt_idx].address; let rcpt_addr = self.message.recipients[rcpt_idx].address();
recipient_addresses.push(rcpt_addr.clone()); recipient_addresses.push(rcpt_addr.to_lowercase());
pending_recipients.push((rcpt_idx, rcpt_addr)); pending_recipients.push((rcpt_idx, rcpt_addr));
} }
@ -93,8 +93,7 @@ impl MessageWrapper {
// Process autogenerated messages // Process autogenerated messages
for autogenerated in delivery_result.autogenerated { for autogenerated in delivery_result.autogenerated {
let mut message = let mut message = server.new_message(autogenerated.sender_address, self.span_id);
server.new_message(autogenerated.sender_address.to_lowercase(), self.span_id);
for rcpt in autogenerated.recipients { for rcpt in autogenerated.recipients {
message.add_recipient(rcpt, server).await; message.add_recipient(rcpt, server).await;
} }
@ -130,7 +129,7 @@ impl MessageWrapper {
.message .message
.recipients .recipients
.into_iter() .into_iter()
.map(|r| trc::Value::from(r.address)) .map(|r| trc::Value::from(r.address().to_string()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }

View file

@ -173,7 +173,7 @@ impl MessageWrapper {
Delivery(DeliveryEvent::RcptTo), Delivery(DeliveryEvent::RcptTo),
SpanId = params.session_id, SpanId = params.session_id,
Hostname = params.hostname.to_string(), Hostname = params.hostname.to_string(),
To = rcpt.address.to_string(), To = rcpt.address().to_string(),
Code = response.code, Code = response.code,
Details = response.message.to_string(), Details = response.message.to_string(),
Elapsed = time.elapsed(), Elapsed = time.elapsed(),
@ -193,7 +193,7 @@ impl MessageWrapper {
Delivery(DeliveryEvent::RcptToRejected), Delivery(DeliveryEvent::RcptToRejected),
SpanId = params.session_id, SpanId = params.session_id,
Hostname = params.hostname.to_string(), Hostname = params.hostname.to_string(),
To = rcpt.address.to_string(), To = rcpt.address().to_string(),
Code = response.code, Code = response.code,
Details = response.message.to_string(), Details = response.message.to_string(),
Elapsed = time.elapsed(), Elapsed = time.elapsed(),
@ -221,7 +221,7 @@ impl MessageWrapper {
Delivery(DeliveryEvent::RcptToFailed), Delivery(DeliveryEvent::RcptToFailed),
SpanId = params.session_id, SpanId = params.session_id,
Hostname = params.hostname.to_string(), Hostname = params.hostname.to_string(),
To = rcpt.address.to_string(), To = rcpt.address().to_string(),
CausedBy = from_mail_send_error(&err), CausedBy = from_mail_send_error(&err),
Elapsed = time.elapsed(), Elapsed = time.elapsed(),
); );
@ -272,7 +272,7 @@ impl MessageWrapper {
Delivery(DeliveryEvent::Delivered), Delivery(DeliveryEvent::Delivered),
SpanId = params.session_id, SpanId = params.session_id,
Hostname = params.hostname.to_string(), Hostname = params.hostname.to_string(),
To = rcpt.address.to_string(), To = rcpt.address().to_string(),
Code = response.code, Code = response.code,
Details = response.message.to_string(), Details = response.message.to_string(),
Elapsed = time.elapsed(), Elapsed = time.elapsed(),
@ -332,7 +332,7 @@ impl MessageWrapper {
Delivery(DeliveryEvent::Delivered), Delivery(DeliveryEvent::Delivered),
SpanId = params.session_id, SpanId = params.session_id,
Hostname = params.hostname.to_string(), Hostname = params.hostname.to_string(),
To = rcpt.address.to_string(), To = rcpt.address().to_string(),
Code = response.code, Code = response.code,
Details = response.message.to_string(), Details = response.message.to_string(),
Elapsed = time.elapsed(), Elapsed = time.elapsed(),
@ -348,7 +348,7 @@ impl MessageWrapper {
Delivery(DeliveryEvent::RcptToRejected), Delivery(DeliveryEvent::RcptToRejected),
SpanId = params.session_id, SpanId = params.session_id,
Hostname = params.hostname.to_string(), Hostname = params.hostname.to_string(),
To = rcpt.address.to_string(), To = rcpt.address().to_string(),
Code = response.code, Code = response.code,
Details = response.message.to_string(), Details = response.message.to_string(),
Elapsed = time.elapsed(), Elapsed = time.elapsed(),
@ -420,8 +420,8 @@ impl MessageWrapper {
} }
fn build_rcpt_to(&self, rcpt: &Recipient, capabilities: &EhloResponse<String>) -> String { fn build_rcpt_to(&self, rcpt: &Recipient, capabilities: &EhloResponse<String>) -> String {
let mut rcpt_to = String::with_capacity(rcpt.address.len() + 60); let mut rcpt_to = String::with_capacity(rcpt.address().len() + 60);
let _ = write!(rcpt_to, "RCPT TO:<{}>", rcpt.address); let _ = write!(rcpt_to, "RCPT TO:<{}>", rcpt.address());
if capabilities.has_capability(EXT_DSN) { if capabilities.has_capability(EXT_DSN) {
if rcpt.has_flag(RCPT_NOTIFY_SUCCESS | RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_DELAY) { if rcpt.has_flag(RCPT_NOTIFY_SUCCESS | RCPT_NOTIFY_FAILURE | RCPT_NOTIFY_DELAY) {
rcpt_to.push_str(" NOTIFY="); rcpt_to.push_str(" NOTIFY=");

View file

@ -39,7 +39,7 @@ impl SendDsn for Server {
if let Some(dsn) = message.build_dsn(self).await { if let Some(dsn) = message.build_dsn(self).await {
let mut dsn_message = self.new_message("", message.span_id); let mut dsn_message = self.new_message("", message.span_id);
dsn_message dsn_message
.add_recipient_parts(message.message.return_path.as_str(), self) .add_recipient(message.message.return_path.as_str(), self)
.await; .await;
// Sign message // 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()) + 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 { pub trait InstantFromTimestamp {
fn to_instant(&self) -> Instant; fn to_instant(&self) -> Instant;
} }
@ -418,10 +461,28 @@ impl InstantFromTimestamp for u64 {
} }
pub trait DomainPart { pub trait DomainPart {
fn to_lowercase_domain(&self) -> String;
fn domain_part(&self) -> &str; fn domain_part(&self) -> &str;
} }
impl<T: AsRef<str>> DomainPart for T { 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)] #[inline(always)]
fn domain_part(&self) -> &str { fn domain_part(&self) -> &str {
self.as_ref() self.as_ref()

View file

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

View file

@ -114,7 +114,7 @@ impl SmtpReporting for Server {
parent_session_id: u64, parent_session_id: u64,
) { ) {
// Build message // Build message
let mut message = self.new_message(from_addr.to_lowercase(), parent_session_id); let mut message = self.new_message(from_addr, parent_session_id);
for rcpt_ in rcpts { for rcpt_ in rcpts {
message.add_recipient(rcpt_.as_ref(), self).await; message.add_recipient(rcpt_.as_ref(), self).await;
} }
@ -161,7 +161,7 @@ impl SmtpReporting for Server {
parent_session_id: u64, parent_session_id: u64,
) { ) {
// Build message // Build message
let mut message = self.new_message(from_addr.as_ref().to_lowercase(), parent_session_id); let mut message = self.new_message(from_addr.as_ref(), parent_session_id);
for rcpt in rcpts { for rcpt in rcpts {
message.add_recipient(rcpt, self).await; message.add_recipient(rcpt, self).await;
} }

View file

@ -158,8 +158,7 @@ impl RunScript for Server {
message_id, message_id,
} => { } => {
// Build message // Build message
let mut message = let mut message = self.new_message(params.return_path.as_str(), session_id);
self.new_message(params.return_path.to_lowercase(), session_id);
match recipient { match recipient {
Recipient::Address(rcpt) => { Recipient::Address(rcpt) => {
message.add_recipient(rcpt, self).await; message.add_recipient(rcpt, self).await;
@ -327,7 +326,7 @@ impl RunScript for Server {
.message .message
.recipients .recipients
.into_iter() .into_iter()
.map(|r| trc::Value::from(r.address)) .map(|r| trc::Value::from(r.address().to_string()))
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
); );
} }

View file

@ -175,7 +175,7 @@ async fn dmarc() {
// Expect SPF auth failure report // Expect SPF auth failure report
let message = qr.expect_message().await; let message = qr.expect_message().await;
assert_eq!( assert_eq!(
message.message.recipients.last().unwrap().address, message.message.recipients.last().unwrap().address(),
"spf-failures@example.com" "spf-failures@example.com"
); );
message message
@ -206,7 +206,7 @@ async fn dmarc() {
// Expect DKIM auth failure report // Expect DKIM auth failure report
let message = qr.expect_message().await; let message = qr.expect_message().await;
assert_eq!( assert_eq!(
message.message.recipients.last().unwrap().address, message.message.recipients.last().unwrap().address(),
"dkim-failures@example.com" "dkim-failures@example.com"
); );
message message
@ -257,7 +257,7 @@ async fn dmarc() {
// Expect DMARC auth failure report // Expect DMARC auth failure report
let message = qr.expect_message().await; let message = qr.expect_message().await;
assert_eq!( assert_eq!(
message.message.recipients.last().unwrap().address, message.message.recipients.last().unwrap().address(),
"dmarc-failures@example.com" "dmarc-failures@example.com"
); );
message message

View file

@ -246,11 +246,11 @@ async fn sieve_scripts() {
assert_eq!(notification.message.return_path, ""); assert_eq!(notification.message.return_path, "");
assert_eq!(notification.message.recipients.len(), 2); assert_eq!(notification.message.recipients.len(), 2);
assert_eq!( assert_eq!(
notification.message.recipients.first().unwrap().address, notification.message.recipients.first().unwrap().address(),
"john@example.net" "john@example.net"
); );
assert_eq!( assert_eq!(
notification.message.recipients.last().unwrap().address, notification.message.recipients.last().unwrap().address(),
"jane@example.org" "jane@example.org"
); );
notification notification
@ -326,7 +326,7 @@ async fn sieve_scripts() {
assert_eq!(redirect.message.return_path, ""); assert_eq!(redirect.message.return_path, "");
assert_eq!(redirect.message.recipients.len(), 1); assert_eq!(redirect.message.recipients.len(), 1);
assert_eq!( assert_eq!(
redirect.message.recipients.first().unwrap().address, redirect.message.recipients.first().unwrap().address(),
"redirect@here.email" "redirect@here.email"
); );
redirect redirect
@ -354,7 +354,7 @@ async fn sieve_scripts() {
assert_eq!(redirect.message.return_path, ""); assert_eq!(redirect.message.return_path, "");
assert_eq!(redirect.message.recipients.len(), 1); assert_eq!(redirect.message.recipients.len(), 1);
assert_eq!( assert_eq!(
redirect.message.recipients.first().unwrap().address, redirect.message.recipients.first().unwrap().address(),
"redirect@somewhere.email" "redirect@somewhere.email"
); );
redirect redirect

View file

@ -170,9 +170,9 @@ async fn manage_queue() {
.message .message
.recipients .recipients
.into_iter() .into_iter()
.map(|r| r.address) .map(|r| r.address().to_string())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec!["success@foobar.org".to_string()] vec!["success@foobar.org"]
); );
// Fetch and validate messages // Fetch and validate messages
@ -322,7 +322,7 @@ async fn manage_queue() {
.message .message
.recipients .recipients
.into_iter() .into_iter()
.map(|r| r.address) .map(|r| r.address().to_string())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec!["john@foobar.org".to_string()] vec!["john@foobar.org".to_string()]
); );

View file

@ -193,7 +193,7 @@ async fn lmtp_delivery() {
.message .message
.recipients .recipients
.into_iter() .into_iter()
.map(|r| r.address) .map(|r| r.address().to_string())
.collect::<Vec<_>>(), .collect::<Vec<_>>(),
vec![ vec![
"bill@foobar.org".to_string(), "bill@foobar.org".to_string(),

View file

@ -260,7 +260,7 @@ async fn smtp_delivery() {
.message .message
.recipients .recipients
.into_iter() .into_iter()
.map(|r| r.address) .map(|r| r.address().to_string())
.collect::<Vec<_>>(); .collect::<Vec<_>>();
recipients.extend( recipients.extend(
remote remote
@ -270,7 +270,7 @@ async fn smtp_delivery() {
.message .message
.recipients .recipients
.into_iter() .into_iter()
.map(|r| r.address), .map(|r| r.address().to_string()),
); );
recipients.sort(); recipients.sort();
assert_eq!( assert_eq!(

View file

@ -11,7 +11,7 @@ use crate::smtp::{
session::TestSession, session::TestSession,
}; };
use mail_auth::MX; use mail_auth::MX;
use smtp::queue::{DomainPart, Message, QueueEnvelope, Recipient, throttle::IsAllowed}; use smtp::queue::{Message, QueueEnvelope, Recipient, throttle::IsAllowed};
use std::{ use std::{
net::{IpAddr, Ipv4Addr}, net::{IpAddr, Ipv4Addr},
time::{Duration, Instant}, time::{Duration, Instant},
@ -286,7 +286,7 @@ impl<'x> TestQueueEnvelope<'x> for QueueEnvelope<'x> {
mx, mx,
remote_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), remote_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
local_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)), local_ip: IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
domain: rcpt.address.domain_part(), domain: rcpt.domain_part(),
rcpt, rcpt,
} }
} }

View file

@ -220,14 +220,14 @@ impl TestMessage for Message {
fn rcpt(&self, name: &str) -> &Recipient { fn rcpt(&self, name: &str) -> &Recipient {
self.recipients self.recipients
.iter() .iter()
.find(|d| d.address == name) .find(|d| d.address() == name)
.unwrap_or_else(|| panic!("Expected rcpt {name} not found in {:?}", self.recipients)) .unwrap_or_else(|| panic!("Expected rcpt {name} not found in {:?}", self.recipients))
} }
fn rcpt_mut(&mut self, name: &str) -> &mut Recipient { fn rcpt_mut(&mut self, name: &str) -> &mut Recipient {
self.recipients self.recipients
.iter_mut() .iter_mut()
.find(|d| d.address == name) .find(|d| d.address() == name)
.unwrap() .unwrap()
} }
} }

View file

@ -75,7 +75,7 @@ async fn queue_retry() {
let message = qr.expect_message().await; let message = qr.expect_message().await;
assert_eq!(message.message.return_path, ""); assert_eq!(message.message.return_path, "");
assert_eq!( assert_eq!(
message.message.recipients.first().unwrap().address, message.message.recipients.first().unwrap().address(),
"john@test.org" "john@test.org"
); );
message message

View file

@ -113,7 +113,7 @@ async fn report_dmarc() {
qr.assert_no_events(); qr.assert_no_events();
assert_eq!(message.message.recipients.len(), 1); assert_eq!(message.message.recipients.len(), 1);
assert_eq!( assert_eq!(
message.message.recipients.last().unwrap().address, message.message.recipients.last().unwrap().address(),
"reports@foobar.net" "reports@foobar.net"
); );
assert_eq!(message.message.return_path, "reports@example.org"); assert_eq!(message.message.return_path, "reports@example.org");

View file

@ -112,7 +112,7 @@ async fn report_tls() {
// Expect report // Expect report
let message = qr.expect_message().await; let message = qr.expect_message().await;
assert_eq!( assert_eq!(
message.message.recipients.last().unwrap().address, message.message.recipients.last().unwrap().address(),
"reports@foobar.org" "reports@foobar.org"
); );
assert_eq!(message.message.return_path, "reports@example.org"); assert_eq!(message.message.return_path, "reports@example.org");

View file

@ -267,7 +267,7 @@ impl TestSession for Session<DummyIo> {
let rcpts = ["a@foobar.org", "b@test.net", "c@foobar.org", "d@test.net"]; let rcpts = ["a@foobar.org", "b@test.net", "c@foobar.org", "d@test.net"];
for rcpt in &message.message.recipients { for rcpt in &message.message.recipients {
let idx = (rcpt.flags - 1) as usize; let idx = (rcpt.flags - 1) as usize;
assert_eq!(rcpts[idx], rcpt.address); assert_eq!(rcpts[idx], rcpt.address());
} }
} }
} }