use std::fmt::Write;
use std::sync::Arc;
use serde::Deserialize;
pub trait DeliveryPolicy: Send + Sync {
fn on_rcpt(&self, recipient: &str, mail_from: Option<&str>) -> RcptDecision;
fn on_data(
&self,
mail_from: Option<&str>,
recipients: &[RcptContext],
data: &[u8],
) -> Vec<DsnRecipient>;
}
pub type SharedDeliveryPolicy = Arc<dyn DeliveryPolicy>;
#[derive(Clone, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum DsnAction {
Reject,
Bounce,
Accept,
}
#[derive(Clone, Debug, Deserialize)]
pub struct DsnRule {
#[serde(default)]
pub recipient: Option<String>,
#[serde(default)]
pub sender: Option<String>,
pub action: DsnAction,
#[serde(default)]
pub status: Option<String>,
#[serde(default)]
pub diagnostic: Option<String>,
#[serde(default)]
pub code: Option<u16>,
}
#[derive(Clone, Debug)]
pub enum RcptDecision {
Accept,
Reject { code: u16, message: String },
Bounce { status: String, diagnostic: String },
}
#[derive(Clone, Debug)]
pub struct RcptContext {
pub address: String,
pub user: String,
pub decision: RcptDecision,
}
#[derive(Clone, Debug)]
pub struct DsnRecipient {
pub address: String,
pub status: String,
pub diagnostic: String,
}
#[derive(Clone, Debug, Default)]
pub struct DsnPolicy {
rules: Vec<DsnRule>,
}
impl DsnPolicy {
pub fn from_rules(rules: Vec<DsnRule>) -> Self {
Self { rules }
}
pub fn from_env() -> Self {
let raw = match std::env::var("ELEKTROMAIL_DSN_RULES") {
Ok(value) => value.trim().to_string(),
Err(_) => return Self::default(),
};
if raw.is_empty() {
return Self::default();
}
let rules: Vec<DsnRule> = serde_json::from_str(&raw).unwrap_or_default();
Self { rules }
}
}
impl DeliveryPolicy for DsnPolicy {
fn on_rcpt(&self, recipient: &str, mail_from: Option<&str>) -> RcptDecision {
for rule in &self.rules {
if rule.matches(recipient, mail_from) {
return rule.to_decision();
}
}
RcptDecision::Accept
}
fn on_data(
&self,
_mail_from: Option<&str>,
recipients: &[RcptContext],
_data: &[u8],
) -> Vec<DsnRecipient> {
recipients
.iter()
.filter_map(|recipient| match &recipient.decision {
RcptDecision::Bounce { status, diagnostic } => Some(DsnRecipient {
address: recipient.address.clone(),
status: status.clone(),
diagnostic: diagnostic.clone(),
}),
_ => None,
})
.collect()
}
}
impl DsnRule {
fn matches(&self, recipient: &str, mail_from: Option<&str>) -> bool {
let recipient_ok = match &self.recipient {
Some(pattern) => matches_pattern(pattern, recipient),
None => true,
};
let sender_ok = match (&self.sender, mail_from) {
(Some(pattern), Some(sender)) => matches_pattern(pattern, sender),
(Some(_), None) => false,
(None, _) => true,
};
recipient_ok && sender_ok
}
fn to_decision(&self) -> RcptDecision {
match self.action {
DsnAction::Accept => RcptDecision::Accept,
DsnAction::Reject => RcptDecision::Reject {
code: self.code.unwrap_or(550),
message: self
.diagnostic
.clone()
.unwrap_or_else(|| "Rejected".to_string()),
},
DsnAction::Bounce => RcptDecision::Bounce {
status: self.status.clone().unwrap_or_else(|| "5.1.1".to_string()),
diagnostic: self
.diagnostic
.clone()
.unwrap_or_else(|| "Delivery failed".to_string()),
},
}
}
}
fn matches_pattern(pattern: &str, value: &str) -> bool {
let pattern = pattern.to_ascii_lowercase();
let value = value.to_ascii_lowercase();
if pattern == "*" {
return true;
}
if !pattern.contains('*') {
return pattern == value;
}
let parts: Vec<&str> = pattern.split('*').collect();
let mut index = 0usize;
let mut first = true;
for part in &parts {
if part.is_empty() {
continue;
}
if first && !pattern.starts_with('*') {
if !value.starts_with(part) {
return false;
}
index = part.len();
first = false;
continue;
}
if let Some(pos) = value[index..].find(part) {
index += pos + part.len();
} else {
return false;
}
first = false;
}
if !pattern.ends_with('*') {
if let Some(last) = parts.iter().rev().find(|part| !part.is_empty()) {
return value.ends_with(last);
}
}
true
}
pub(crate) fn build_dsn_message(sender: &str, recipients: &[DsnRecipient]) -> Vec<u8> {
let boundary = "dsn-boundary";
let mut delivery = String::new();
for recipient in recipients {
let _ = write!(
delivery,
"Final-Recipient: rfc822; {}\r\nAction: failed\r\nStatus: {}\r\nDiagnostic-Code: smtp; {}\r\n\r\n",
recipient.address, recipient.status, recipient.diagnostic
);
}
let message = format!(
"From: MAILER-DAEMON <mailer-daemon@localhost>\r\nTo: {sender}\r\nSubject: Delivery Status Notification (Failure)\r\nMIME-Version: 1.0\r\nContent-Type: multipart/report; report-type=delivery-status; boundary=\"{boundary}\"\r\n\r\n--{boundary}\r\nContent-Type: text/plain; charset=utf-8\r\n\r\nThis is a delivery status notification generated by Elektromail.\r\n\r\n--{boundary}\r\nContent-Type: message/delivery-status\r\n\r\n{delivery}--{boundary}--\r\n"
);
message.into_bytes()
}