use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use backbone_email::smtp::{SmtpConfig, SmtpEmailService};
use backbone_email::{EmailAddress, EmailMessage, EmailPriority, EmailRecipients, EmailService, EmailStatus};
use chrono::Utc;
use uuid::Uuid;
use crate::application::service::mail_ports::{
validate_mail_header, MailApiPort, MailSendFailure, MailSendOutcome, MailSendRequest,
TRANSPORT_THREADING_HEADERS,
};
use crate::application::service::MailServerQueryService;
use crate::infrastructure::persistence::smtp_selection_repository::SmtpEndpoint;
fn merge_transport_headers(
in_reply_to: Option<&str>,
per_mail: &HashMap<String, String>,
) -> Result<HashMap<String, String>, MailSendFailure> {
let refused = |detail: String| MailSendFailure {
failure_type: "unknown".into(),
message: format!("refused per-mail headers: {detail}"),
};
let mut merged = HashMap::with_capacity(per_mail.len() + 2);
for (name, value) in per_mail {
if let Err(e) = validate_mail_header(name, value) {
return Err(refused(e.to_string()));
}
if in_reply_to.is_some()
&& TRANSPORT_THREADING_HEADERS
.iter()
.any(|reserved| name.eq_ignore_ascii_case(reserved))
{
return Err(refused(format!(
"both the structured in_reply_to and a per-mail entry carry {name:?} \
({:?}) — ambiguous threading, refusing",
TRANSPORT_THREADING_HEADERS
)));
}
merged.insert(name.clone(), value.clone());
}
if let Some(parent) = in_reply_to {
merged.insert("In-Reply-To".into(), parent.to_string());
merged.insert("References".into(), parent.to_string());
}
Ok(merged)
}
pub struct SmtpMailApi {
servers: MailServerQueryService,
transports: Mutex<HashMap<Uuid, Arc<SmtpEmailService>>>,
}
impl SmtpMailApi {
pub fn new(pool: sqlx::PgPool) -> Self {
Self {
servers: MailServerQueryService::new(pool),
transports: Mutex::new(HashMap::new()),
}
}
fn transport_for(&self, ep: &SmtpEndpoint) -> Result<Arc<SmtpEmailService>, MailSendFailure> {
if let Some(cached) = self
.transports
.lock()
.unwrap_or_else(|e| e.into_inner())
.get(&ep.server_id)
{
return Ok(Arc::clone(cached));
}
let password = match (&ep.smtp_user, &ep.smtp_pass_ref) {
(Some(_), Some(ref_name)) => Some(
std::env::var(ref_name).map_err(|_| MailSendFailure {
failure_type: "mail_server".into(),
message: format!(
"smtp_pass_ref env var {ref_name:?} is not set (server {:?})",
ep.name
),
})?,
),
(None, _) => None,
(Some(_), None) => {
return Err(MailSendFailure {
failure_type: "mail_server".into(),
message: format!(
"server {:?} has smtp_user but no smtp_pass_ref — cannot authenticate",
ep.name
),
})
}
};
let config = SmtpConfig {
host: ep.smtp_host.clone(),
port: ep.smtp_port as u16,
username: ep.smtp_user.clone(),
password,
use_tls: ep.smtp_encryption == "starttls",
use_ssl: ep.smtp_encryption == "ssl",
timeout: 30,
hello_name: None,
};
let service = Arc::new(SmtpEmailService::new(config).map_err(|e| MailSendFailure {
failure_type: "mail_server".into(),
message: format!("transport build failed for server {:?}: {e}", ep.name),
})?);
self.transports
.lock()
.unwrap_or_else(|e| e.into_inner())
.insert(ep.server_id, Arc::clone(&service));
Ok(service)
}
}
#[async_trait::async_trait]
impl MailApiPort for SmtpMailApi {
async fn send(&self, req: &MailSendRequest) -> Result<MailSendOutcome, MailSendFailure> {
if req.from.trim().is_empty() {
return Err(MailSendFailure {
failure_type: "mail_server".into(),
message: "mail_from_missing: the queue row carries no envelope sender".into(),
});
}
let ep = self
.servers
.resolve_endpoint(&req.from)
.await
.map_err(|e| MailSendFailure {
failure_type: "mail_server".into(),
message: format!("server selection failed: {e}"),
})?
.ok_or_else(|| MailSendFailure {
failure_type: "mail_server".into(),
message: format!(
"no active mail server matches {:?} (the from_filter ladder is empty at every rung)",
req.from
),
})?;
if ep.smtp_authentication == "certificate" {
return Err(MailSendFailure {
failure_type: "mail_server".into(),
message: format!(
"server {:?} uses smtp_authentication 'certificate' — declared but unsupported \
(needs a file-based secret store; port-notes §8)",
ep.name
),
});
}
let transport = self.transport_for(&ep)?;
let headers = merge_transport_headers(req.in_reply_to.as_deref(), &req.headers)?;
let message = EmailMessage {
id: req.mail_id.to_string(),
from: EmailAddress::new(&req.from),
reply_to: None,
recipients: EmailRecipients::new(
req.to.iter().map(EmailAddress::new).collect(),
),
subject: req.subject.clone().unwrap_or_default(),
text: None,
html: Some(req.body_html.clone()),
attachments: Vec::new(),
headers,
template_data: None,
created_at: Utc::now(),
scheduled_at: None,
priority: EmailPriority::default(),
tracking: false,
};
let report = transport.send(message).await.map_err(|e| MailSendFailure {
failure_type: "mail_smtp".into(),
message: format!("smtp send failed via {:?}: {e}", ep.name),
})?;
if report.status == EmailStatus::Sent {
Ok(MailSendOutcome::Accepted)
} else {
Err(MailSendFailure {
failure_type: "mail_smtp".into(),
message: format!(
"smtp verdict {:?} via {:?}: {}",
report.status,
ep.name,
report.error.unwrap_or_else(|| "no provider detail".into())
),
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn per_mail(entries: &[(&str, &str)]) -> HashMap<String, String> {
entries.iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
#[test]
fn per_mail_headers_flow_into_the_transport_map() {
let merged =
merge_transport_headers(None, &per_mail(&[("X-Campaign-Id", "summer-2026")]))
.expect("clean per-mail headers merge");
assert_eq!(merged.get("X-Campaign-Id").map(String::as_str), Some("summer-2026"));
assert_eq!(merged.len(), 1);
}
#[test]
fn structured_threading_is_added_on_top_of_per_mail_headers() {
let merged = merge_transport_headers(
Some("<parent@example.com>"),
&per_mail(&[("X-Campaign-Id", "promo")]),
)
.expect("no collision merges");
assert_eq!(merged.get("In-Reply-To").map(String::as_str), Some("<parent@example.com>"));
assert_eq!(merged.get("References").map(String::as_str), Some("<parent@example.com>"));
assert_eq!(merged.get("X-Campaign-Id").map(String::as_str), Some("promo"));
}
#[test]
fn per_mail_threading_headers_pass_when_no_structured_threading() {
let merged = merge_transport_headers(
None,
&per_mail(&[("In-Reply-To", "<self@example.com>"), ("references", "<t@example.com>")]),
)
.expect("sole source of threading flows through");
assert_eq!(merged.get("In-Reply-To").map(String::as_str), Some("<self@example.com>"));
assert_eq!(merged.get("references").map(String::as_str), Some("<t@example.com>"));
}
#[test]
fn threading_collision_is_refused_loudly_case_insensitively() {
for name in ["In-Reply-To", "References", "in-reply-to", "REFERENCES"] {
let refusal = merge_transport_headers(
Some("<parent@example.com>"),
&per_mail(&[(name, "<rogue@example.com>")]),
)
.expect_err("collision must refuse");
assert_eq!(refusal.failure_type, "unknown");
assert!(
refusal.message.contains("ambiguous threading"),
"message must name the collision: {}",
refusal.message
);
}
}
#[test]
fn crlf_smuggle_through_the_gateway_is_refused() {
let refusal = merge_transport_headers(
None,
&per_mail(&[("X-Campaign", "a\r\nBcc: victim@example.com")]),
)
.expect_err("CRLF must refuse at the gateway too (defense in depth)");
assert_eq!(refusal.failure_type, "unknown");
assert!(refusal.message.contains("CR/LF"));
assert!(merge_transport_headers(None, &per_mail(&[("Bad\r\nName", "v")])).is_err());
}
}