use hickory_resolver::TokioResolver;
use super::smtp::try_deliver_via_mx;
use crate::dsn;
use crate::queue::{QueueStatus, QueuedMessage, is_hard_bounce};
use crate::retry::{retry_delay_secs, should_bounce};
use crate::store::{QueueStore, StoreError};
use crate::{DeliveryEvent, DeliveryEventSender};
pub(super) async fn enqueue_dsn(
store: &dyn QueueStore,
hostname: &str,
msg: &QueuedMessage,
error: &str,
) {
if msg.sender.is_empty() || msg.sender == "<>" {
return; }
let dsn_msg = dsn::format_dsn(
hostname,
&msg.sender,
&msg.recipient,
error,
msg.message_id.as_deref(),
);
let sender_domain = msg
.sender
.rsplit_once('@')
.map(|(_, d)| d)
.unwrap_or("unknown");
let now = chrono::Utc::now().timestamp();
let queued = store
.enqueue(
"<>",
&msg.sender,
sender_domain,
dsn_msg.as_bytes(),
None,
now,
false,
)
.await;
logged("enqueue DSN", msg.id, queued.map(drop));
}
#[tracing::instrument(
name = "outbound.deliver_domain",
skip(resolver, hostname, messages, store, event_sender),
fields(domain, n_messages = messages.len(), max_per_conn),
)]
#[allow(clippy::too_many_arguments)]
pub async fn deliver_domain_static(
resolver: &TokioResolver,
hostname: &str,
domain: &str,
messages: Vec<QueuedMessage>,
store: &dyn QueueStore,
port: u16,
max_per_conn: usize,
event_sender: Option<&DeliveryEventSender>,
) {
let batch_start = std::time::Instant::now();
let messages = drop_suppressed(store, messages, event_sender).await;
if messages.is_empty() {
return;
}
let mx_records = match mailrs_smtp_client::resolve_mx(resolver, domain).await {
Ok(records) => records,
Err(e) => {
tracing::warn!("MX resolution failed for {domain}: {e}");
let now = chrono::Utc::now().timestamp();
for msg in &messages {
let delay = retry_delay_secs(msg.attempts);
if should_bounce(msg.attempts + 1, msg.max_attempts) {
let error = format!("MX resolution failed: {e}");
logged(
"mark bounced",
msg.id,
store.mark_bounced(msg.id, &error, now).await,
);
if is_hard_bounce(&error) {
let added = store.add_suppression(&msg.recipient, &error, None).await;
logged("add suppression", msg.id, added);
}
enqueue_dsn(store, hostname, msg, &error).await;
if let Some(es) = event_sender {
es(DeliveryEvent::Bounced {
queue_id: msg.id,
sender: msg.sender.clone(),
});
}
} else {
let error = format!("MX resolution failed: {e}");
let marked = store
.mark_failed(msg.id, &error, now + delay as i64, now)
.await;
logged("mark failed", msg.id, marked);
if let Some(es) = event_sender {
es(DeliveryEvent::Failed {
queue_id: msg.id,
domain: domain.to_string(),
error: format!("MX resolution failed: {e}"),
});
}
}
}
return;
}
};
let chunks: Vec<&[QueuedMessage]> = messages.chunks(max_per_conn).collect();
for mx in &mx_records {
let mut all_ok = true;
for chunk in &chunks {
match try_deliver_via_mx(
hostname,
&mx.exchange,
port,
domain,
chunk,
resolver,
event_sender,
)
.await
{
Ok(()) => {
let now = chrono::Utc::now().timestamp();
let elapsed = batch_start.elapsed().as_secs_f64();
for msg in *chunk {
metrics::histogram!(
"mailrs_outbound_delivery_seconds",
"outcome" => "delivered",
)
.record(elapsed);
logged(
"mark delivered",
msg.id,
store.mark_delivered(msg.id, now).await,
);
if let Some(es) = event_sender {
es(DeliveryEvent::Success {
queue_id: msg.id,
domain: domain.to_string(),
});
}
}
}
Err(e) => {
tracing::warn!("delivery to {} via {} failed: {e}", domain, mx.exchange);
all_ok = false;
break;
}
}
}
if all_ok {
tracing::info!(
"delivered {} messages to {domain} via {}",
messages.len(),
mx.exchange
);
return;
}
}
let now = chrono::Utc::now().timestamp();
for msg in &messages {
if let Ok(Some(current)) = store.get_message(msg.id).await
&& current.status == QueueStatus::Delivered
{
continue;
}
let delay = retry_delay_secs(msg.attempts);
if should_bounce(msg.attempts + 1, msg.max_attempts) {
let marked = store.mark_bounced(msg.id, "all MX hosts failed", now).await;
logged("mark bounced", msg.id, marked);
if let Some(ref err) = msg.last_error
&& is_hard_bounce(err)
{
let added = store.add_suppression(&msg.recipient, err, None).await;
logged("add suppression", msg.id, added);
}
enqueue_dsn(store, hostname, msg, "all MX hosts failed").await;
if let Some(es) = event_sender {
es(DeliveryEvent::Bounced {
queue_id: msg.id,
sender: msg.sender.clone(),
});
}
} else {
let marked = store
.mark_failed(msg.id, "all MX hosts failed", now + delay as i64, now)
.await;
logged("mark failed", msg.id, marked);
if let Some(es) = event_sender {
es(DeliveryEvent::Failed {
queue_id: msg.id,
domain: domain.to_string(),
error: "all MX hosts failed".into(),
});
}
}
}
}
async fn drop_suppressed(
store: &dyn QueueStore,
messages: Vec<QueuedMessage>,
event_sender: Option<&DeliveryEventSender>,
) -> Vec<QueuedMessage> {
let now = chrono::Utc::now().timestamp();
let mut kept = Vec::with_capacity(messages.len());
for msg in messages {
let suppressed = match store.is_suppressed(&msg.recipient).await {
Ok(suppressed) => suppressed,
Err(e) => {
tracing::warn!(
"outbound queue: suppression check for msg {} failed: {e}",
msg.id
);
false
}
};
if !suppressed {
kept.push(msg);
continue;
}
tracing::info!("skipping suppressed recipient: {}", msg.recipient);
let marked = store
.mark_bounced(msg.id, "recipient suppressed (hard bounce history)", now)
.await;
logged("mark bounced", msg.id, marked);
if let Some(es) = event_sender {
es(DeliveryEvent::Bounced {
queue_id: msg.id,
sender: msg.sender.clone(),
});
}
}
kept
}
fn logged(op: &str, id: i64, result: Result<(), StoreError>) {
if let Err(e) = result {
tracing::warn!("outbound queue: {op} for msg {id} failed: {e}");
}
}