use lazy_static::lazy_static;
use regex::Regex;
use serde::{Deserialize, Serialize};
lazy_static! {
pub static ref FROM_EMAIL_REGEX: Regex = Regex::new(r"from=<([^>]*)>").unwrap();
pub static ref TO_EMAIL_REGEX: Regex = Regex::new(r"to=<([^>]+)>").unwrap();
pub static ref ORIG_TO_EMAIL_REGEX: Regex = Regex::new(r"orig_to=<([^>]+)>").unwrap();
pub static ref CLIENT_INFO_REGEX: Regex = Regex::new(r"client=([^\[]+)\[([^\]]+)\](?::(\d+))?").unwrap();
pub static ref CLIENT_SIMPLE_REGEX: Regex = Regex::new(r"([^\[]+)\[([^\]]+)\](?::(\d+))?").unwrap();
pub static ref RELAY_INFO_REGEX: Regex = Regex::new(r"relay=([^,\[\]]+)(?:\[([^\]]+)\])?(?::(\d+))?").unwrap();
pub static ref DELAY_REGEX: Regex = Regex::new(r"delay=([\d.]+)").unwrap();
pub static ref DELAYS_REGEX: Regex = Regex::new(r"delays=([\d./]+)").unwrap();
pub static ref DSN_REGEX: Regex = Regex::new(r"dsn=([\d.]+)").unwrap();
pub static ref STATUS_REGEX: Regex = Regex::new(r"status=(\w+)").unwrap();
pub static ref SIZE_REGEX: Regex = Regex::new(r"size=(\d+)").unwrap();
pub static ref NRCPT_REGEX: Regex = Regex::new(r"nrcpt=(\d+)").unwrap();
pub static ref MESSAGE_ID_REGEX: Regex = Regex::new(r"message-id=(?:<([^>]+)>|([^,\s]+))").unwrap();
pub static ref PROTO_REGEX: Regex = Regex::new(r"proto=(\w+)").unwrap();
pub static ref HELO_REGEX: Regex = Regex::new(r"helo=<([^>]+)>").unwrap();
pub static ref SASL_METHOD_REGEX: Regex = Regex::new(r"sasl_method=(\w+)").unwrap();
pub static ref SASL_USERNAME_REGEX: Regex = Regex::new(r"sasl_username=([^,\s]+)").unwrap();
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct EmailAddress {
pub address: String,
pub is_empty: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ClientInfo {
pub hostname: String,
pub ip: String,
pub port: Option<u16>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct RelayInfo {
pub hostname: String,
pub ip: Option<String>,
pub port: Option<u16>,
pub is_none: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DelayInfo {
pub total: f64,
pub breakdown: Option<[f64; 4]>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StatusInfo {
pub status: String,
pub dsn: Option<String>,
pub description: Option<String>,
}
pub struct CommonFieldsParser;
impl CommonFieldsParser {
pub fn extract_from_email(message: &str) -> Option<EmailAddress> {
FROM_EMAIL_REGEX.captures(message).map(|caps| {
let address = caps
.get(1)
.map_or(String::new(), |m| m.as_str().to_string());
EmailAddress {
is_empty: address.is_empty(),
address,
}
})
}
pub fn extract_to_email(message: &str) -> Option<EmailAddress> {
TO_EMAIL_REGEX.captures(message).map(|caps| {
let address = caps
.get(1)
.map_or(String::new(), |m| m.as_str().to_string());
EmailAddress {
is_empty: address.is_empty(),
address,
}
})
}
pub fn extract_orig_to_email(message: &str) -> Option<EmailAddress> {
ORIG_TO_EMAIL_REGEX.captures(message).map(|caps| {
let address = caps
.get(1)
.map_or(String::new(), |m| m.as_str().to_string());
EmailAddress {
is_empty: address.is_empty(),
address,
}
})
}
pub fn extract_client_info(message: &str) -> Option<ClientInfo> {
CLIENT_INFO_REGEX.captures(message).map(|caps| ClientInfo {
hostname: caps
.get(1)
.map_or(String::new(), |m| m.as_str().to_string()),
ip: caps
.get(2)
.map_or(String::new(), |m| m.as_str().to_string()),
port: caps.get(3).and_then(|m| m.as_str().parse().ok()),
})
}
pub fn extract_client_info_simple(client_str: &str) -> Option<ClientInfo> {
CLIENT_SIMPLE_REGEX
.captures(client_str)
.map(|caps| ClientInfo {
hostname: caps
.get(1)
.map_or(String::new(), |m| m.as_str().to_string()),
ip: caps
.get(2)
.map_or(String::new(), |m| m.as_str().to_string()),
port: caps.get(3).and_then(|m| m.as_str().parse().ok()),
})
}
pub fn extract_relay_info(message: &str) -> Option<RelayInfo> {
RELAY_INFO_REGEX.captures(message).map(|caps| {
let hostname = caps
.get(1)
.map_or(String::new(), |m| m.as_str().to_string());
let is_none = hostname == "none";
RelayInfo {
hostname,
ip: caps.get(2).map(|m| m.as_str().to_string()),
port: caps.get(3).and_then(|m| m.as_str().parse().ok()),
is_none,
}
})
}
pub fn extract_delay_info(message: &str) -> Option<DelayInfo> {
let total = DELAY_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.and_then(|m| m.as_str().parse().ok())?;
let breakdown = DELAYS_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.and_then(|m| Self::parse_delays_breakdown(m.as_str()));
Some(DelayInfo { total, breakdown })
}
fn parse_delays_breakdown(delays_str: &str) -> Option<[f64; 4]> {
let parts: Vec<&str> = delays_str.split('/').collect();
if parts.len() == 4 {
let mut breakdown = [0.0; 4];
for (i, part) in parts.iter().enumerate() {
breakdown[i] = part.parse().ok()?;
}
Some(breakdown)
} else {
None
}
}
pub fn extract_status_info(message: &str) -> Option<StatusInfo> {
let status = STATUS_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())?;
let dsn = DSN_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string());
let description = if let Some(start) = message.find('(') {
if let Some(end) = message.rfind(')') {
if end > start {
Some(message[start + 1..end].to_string())
} else {
None
}
} else {
None
}
} else {
None
};
Some(StatusInfo {
status,
dsn,
description,
})
}
pub fn extract_size(message: &str) -> Option<u64> {
SIZE_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.and_then(|m| m.as_str().parse().ok())
}
pub fn extract_nrcpt(message: &str) -> Option<u32> {
NRCPT_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.and_then(|m| m.as_str().parse().ok())
}
pub fn extract_message_id(message: &str) -> Option<String> {
MESSAGE_ID_REGEX.captures(message).and_then(|caps| {
if let Some(bracketed) = caps.get(1) {
Some(bracketed.as_str().to_string())
}
else if let Some(unbracketed) = caps.get(2) {
Some(unbracketed.as_str().to_string())
} else {
None
}
})
}
pub fn extract_protocol(message: &str) -> Option<String> {
PROTO_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
pub fn extract_helo(message: &str) -> Option<String> {
HELO_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
pub fn extract_sasl_method(message: &str) -> Option<String> {
SASL_METHOD_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
pub fn extract_sasl_username(message: &str) -> Option<String> {
SASL_USERNAME_REGEX
.captures(message)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_from_email() {
let message = "4bG4VR5z: from=<sender@example.com>, size=1234";
let result = CommonFieldsParser::extract_from_email(message);
assert_eq!(
result,
Some(EmailAddress {
address: "sender@example.com".to_string(),
is_empty: false,
})
);
let bounce_message = "4bG4VR5z: from=<>, to=<user@example.com>";
let bounce_result = CommonFieldsParser::extract_from_email(bounce_message);
assert_eq!(
bounce_result,
Some(EmailAddress {
address: "".to_string(),
is_empty: true,
})
);
}
#[test]
fn test_extract_client_info() {
let message = "4bG4VR5z: client=mail.example.com[192.168.1.100]:25";
let result = CommonFieldsParser::extract_client_info(message);
assert_eq!(
result,
Some(ClientInfo {
hostname: "mail.example.com".to_string(),
ip: "192.168.1.100".to_string(),
port: Some(25),
})
);
let no_port_message = "4bG4VR5z: client=localhost[127.0.0.1]";
let no_port_result = CommonFieldsParser::extract_client_info(no_port_message);
assert_eq!(
no_port_result,
Some(ClientInfo {
hostname: "localhost".to_string(),
ip: "127.0.0.1".to_string(),
port: None,
})
);
}
#[test]
fn test_extract_relay_info() {
let message = "4bG4VR5z: to=<user@example.com>, relay=mx.example.com[1.2.3.4]:25";
let result = CommonFieldsParser::extract_relay_info(message);
assert_eq!(
result,
Some(RelayInfo {
hostname: "mx.example.com".to_string(),
ip: Some("1.2.3.4".to_string()),
port: Some(25),
is_none: false,
})
);
let none_message = "4bG4VR5z: to=<user@example.com>, relay=none, delay=0";
let none_result = CommonFieldsParser::extract_relay_info(none_message);
assert_eq!(
none_result,
Some(RelayInfo {
hostname: "none".to_string(),
ip: None,
port: None,
is_none: true,
})
);
}
#[test]
fn test_extract_delay_info() {
let message = "4bG4VR5z: delay=5.5, delays=1.0/0.5/3.0/1.0";
let result = CommonFieldsParser::extract_delay_info(message);
assert_eq!(
result,
Some(DelayInfo {
total: 5.5,
breakdown: Some([1.0, 0.5, 3.0, 1.0]),
})
);
let simple_message = "4bG4VR5z: delay=2.3";
let simple_result = CommonFieldsParser::extract_delay_info(simple_message);
assert_eq!(
simple_result,
Some(DelayInfo {
total: 2.3,
breakdown: None,
})
);
}
#[test]
fn test_extract_status_info() {
let message = "4bG4VR5z: status=sent (250 2.0.0 OK), dsn=2.0.0";
let result = CommonFieldsParser::extract_status_info(message);
assert_eq!(
result,
Some(StatusInfo {
status: "sent".to_string(),
dsn: Some("2.0.0".to_string()),
description: Some("250 2.0.0 OK".to_string()),
})
);
}
#[test]
fn test_extract_message_properties() {
let message = "4bG4VR5z: from=<sender@example.com>, size=1234, nrcpt=2";
assert_eq!(CommonFieldsParser::extract_size(message), Some(1234));
assert_eq!(CommonFieldsParser::extract_nrcpt(message), Some(2));
}
#[test]
fn test_extract_message_id() {
let bracketed_message = "61172636348059648: message-id=<61172636348059648@m01.localdomain>";
let bracketed_result = CommonFieldsParser::extract_message_id(bracketed_message);
assert_eq!(
bracketed_result,
Some("61172636348059648@m01.localdomain".to_string())
);
let unbracketed_message = "61172641393807360: message-id=61172636348059648@m01.localdomain";
let unbracketed_result = CommonFieldsParser::extract_message_id(unbracketed_message);
assert_eq!(
unbracketed_result,
Some("61172636348059648@m01.localdomain".to_string())
);
let complex_message = "4bG4VR5z: message-id=<test123@example.com>, size=456";
let complex_result = CommonFieldsParser::extract_message_id(complex_message);
assert_eq!(complex_result, Some("test123@example.com".to_string()));
}
#[test]
fn test_extract_protocol_info() {
let message = "4bG4VR5z: proto=ESMTP, helo=<mail.example.com>";
assert_eq!(
CommonFieldsParser::extract_protocol(message),
Some("ESMTP".to_string())
);
assert_eq!(
CommonFieldsParser::extract_helo(message),
Some("mail.example.com".to_string())
);
}
}