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

View file

@ -113,7 +113,7 @@ impl EmailSubmissionGet for Server {
.unarchive::<Message>()
.caused_by(trc::location!())?;
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 {
smtp_reply: match &rcpt.status {
ArchivedStatus::Completed(reply) => {

View file

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

View file

@ -9,7 +9,8 @@ use crate::{
core::{Session, SessionAddress, State},
inbound::milter::Modification,
queue::{
self, Message, MessageSource, MessageWrapper, QueueEnvelope, Schedule, quota::HasQueueQuota,
self, DomainPart, Message, MessageSource, MessageWrapper, QueueEnvelope,
quota::HasQueueQuota,
},
reporting::analysis::AnalyzeReport,
scripts::ScriptResult,
@ -709,7 +710,7 @@ impl<T: SessionStream> Session<T> {
.map_or(0, |d| d.as_secs());
let mut message = Message {
created,
return_path: mail_from.address_lcase,
return_path: mail_from.address.to_lowercase_domain(),
recipients: Vec::with_capacity(rcpt_to.len()),
flags: mail_from.flags,
priority: self.data.priority,
@ -725,26 +726,23 @@ impl<T: SessionStream> Session<T> {
let future_release = self.data.future_release;
rcpt_to.sort_unstable();
for rcpt in rcpt_to {
message.recipients.push(queue::Recipient {
address: rcpt.address_lcase,
status: queue::Status::Scheduled,
flags: if rcpt.flags
& (RCPT_NOTIFY_DELAY
| RCPT_NOTIFY_FAILURE
| RCPT_NOTIFY_SUCCESS
| RCPT_NOTIFY_NEVER)
!= 0
{
rcpt.flags
} else {
rcpt.flags | RCPT_NOTIFY_DELAY | RCPT_NOTIFY_FAILURE
},
orcpt: rcpt.dsn_info,
retry: Schedule::now(),
notify: Schedule::now(),
expires: QueueExpiry::Attempts(0),
queue: QueueName::default(),
});
message.recipients.push(
queue::Recipient::new(rcpt.address)
.with_flags(
if rcpt.flags
& (RCPT_NOTIFY_DELAY
| RCPT_NOTIFY_FAILURE
| RCPT_NOTIFY_SUCCESS
| RCPT_NOTIFY_NEVER)
!= 0
{
rcpt.flags
} else {
rcpt.flags | RCPT_NOTIFY_DELAY | RCPT_NOTIFY_FAILURE
},
)
.with_orcpt(rcpt.dsn_info),
);
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::throttle::IsAllowed;
use crate::queue::{
DomainPart, Error, FROM_REPORT, HostResponse, MessageWrapper, QueueEnvelope, QueuedMessage,
Status,
Error, FROM_REPORT, HostResponse, MessageWrapper, QueueEnvelope, QueuedMessage, Status,
};
use crate::reporting::SmtpReporting;
use crate::{queue::ErrorDetails, reporting::tls::TlsRptOptions};
@ -74,7 +73,7 @@ impl QueuedMessage {
Status::Scheduled | Status::TemporaryFailure(_)
) && r.queue == message.queue_name
{
Some(trc::Value::String(r.address.as_str().into()))
Some(trc::Value::String(r.address().into()))
} else {
None
}
@ -236,7 +235,7 @@ impl QueuedMessage {
);
routes
.entry((rcpt.address.domain_part(), route))
.entry((rcpt.domain_part(), route))
.or_default()
.push(rcpt_idx);
}
@ -1325,7 +1324,7 @@ impl MessageWrapper {
SpanId = self.span_id,
QueueId = self.queue_id,
QueueName = self.queue_name.as_str().to_string(),
To = rcpt.address.clone(),
To = rcpt.address().to_string(),
Reason = from_error_details(&err.details),
Details = trc::Value::Timestamp(now),
Expires = rcpt
@ -1344,7 +1343,7 @@ impl MessageWrapper {
SpanId = self.span_id,
QueueId = self.queue_id,
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.",
Details = trc::Value::Timestamp(now),
Expires = rcpt
@ -1355,7 +1354,7 @@ impl MessageWrapper {
);
rcpt.status = Status::PermanentFailure(ErrorDetails {
entity: rcpt.address.domain_part().to_string(),
entity: rcpt.domain_part().to_string(),
details: Error::Io(
"Message expired without any delivery attempts made.".into(),
),

View file

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

View file

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

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

View file

@ -114,7 +114,7 @@ impl SmtpReporting for Server {
parent_session_id: u64,
) {
// 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 {
message.add_recipient(rcpt_.as_ref(), self).await;
}
@ -161,7 +161,7 @@ impl SmtpReporting for Server {
parent_session_id: u64,
) {
// 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 {
message.add_recipient(rcpt, self).await;
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -11,7 +11,7 @@ use crate::smtp::{
session::TestSession,
};
use mail_auth::MX;
use smtp::queue::{DomainPart, Message, QueueEnvelope, Recipient, throttle::IsAllowed};
use smtp::queue::{Message, QueueEnvelope, Recipient, throttle::IsAllowed};
use std::{
net::{IpAddr, Ipv4Addr},
time::{Duration, Instant},
@ -286,7 +286,7 @@ impl<'x> TestQueueEnvelope<'x> for QueueEnvelope<'x> {
mx,
remote_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,
}
}

View file

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

View file

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

View file

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

View file

@ -112,7 +112,7 @@ async fn report_tls() {
// Expect report
let message = qr.expect_message().await;
assert_eq!(
message.message.recipients.last().unwrap().address,
message.message.recipients.last().unwrap().address(),
"reports@foobar.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"];
for rcpt in &message.message.recipients {
let idx = (rcpt.flags - 1) as usize;
assert_eq!(rcpts[idx], rcpt.address);
assert_eq!(rcpts[idx], rcpt.address());
}
}
}