use crate::utils::common_fields::{
ClientInfo, CommonFieldsParser, DelayInfo, EmailAddress, RelayInfo, StatusInfo,
};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct BaseLogEntry {
pub queue_id: Option<String>,
pub message_id: Option<String>,
pub from_email: Option<EmailAddress>,
pub to_email: Option<EmailAddress>,
pub orig_to_email: Option<EmailAddress>,
pub client_info: Option<ClientInfo>,
pub relay_info: Option<RelayInfo>,
pub delay_info: Option<DelayInfo>,
pub status_info: Option<StatusInfo>,
pub size: Option<u64>,
pub nrcpt: Option<u32>,
pub protocol: Option<String>,
pub helo: Option<String>,
pub sasl_method: Option<String>,
pub sasl_username: Option<String>,
pub raw_message: String,
}
impl BaseLogEntry {
pub fn from_message(raw_message: &str, queue_id: Option<String>) -> Self {
let mut entry = BaseLogEntry {
queue_id,
message_id: CommonFieldsParser::extract_message_id(raw_message),
from_email: CommonFieldsParser::extract_from_email(raw_message),
to_email: CommonFieldsParser::extract_to_email(raw_message),
orig_to_email: CommonFieldsParser::extract_orig_to_email(raw_message),
client_info: CommonFieldsParser::extract_client_info(raw_message),
relay_info: CommonFieldsParser::extract_relay_info(raw_message),
delay_info: CommonFieldsParser::extract_delay_info(raw_message),
status_info: CommonFieldsParser::extract_status_info(raw_message),
size: CommonFieldsParser::extract_size(raw_message),
nrcpt: CommonFieldsParser::extract_nrcpt(raw_message),
protocol: CommonFieldsParser::extract_protocol(raw_message),
helo: CommonFieldsParser::extract_helo(raw_message),
sasl_method: CommonFieldsParser::extract_sasl_method(raw_message),
sasl_username: CommonFieldsParser::extract_sasl_username(raw_message),
raw_message: raw_message.to_string(),
};
if entry.queue_id.is_none() {
entry.queue_id = crate::utils::queue_id::extract_queue_id(raw_message);
}
entry
}
pub fn empty(raw_message: &str) -> Self {
BaseLogEntry {
queue_id: None,
message_id: None,
from_email: None,
to_email: None,
orig_to_email: None,
client_info: None,
relay_info: None,
delay_info: None,
status_info: None,
size: None,
nrcpt: None,
protocol: None,
helo: None,
sasl_method: None,
sasl_username: None,
raw_message: raw_message.to_string(),
}
}
pub fn has_delivery_info(&self) -> bool {
self.to_email.is_some()
|| self.relay_info.is_some()
|| self.status_info.is_some()
|| self.delay_info.is_some()
}
pub fn has_client_info(&self) -> bool {
self.client_info.is_some() || self.helo.is_some()
}
pub fn has_auth_info(&self) -> bool {
self.sasl_method.is_some() || self.sasl_username.is_some()
}
pub fn formatted_from(&self) -> String {
self.from_email
.as_ref()
.map(|email| {
if email.is_empty {
"<>".to_string()
} else {
format!("<{}>", email.address)
}
})
.unwrap_or_else(|| "N/A".to_string())
}
pub fn formatted_to(&self) -> String {
self.to_email
.as_ref()
.map(|email| format!("<{}>", email.address))
.unwrap_or_else(|| "N/A".to_string())
}
pub fn formatted_client(&self) -> String {
self.client_info
.as_ref()
.map(|client| {
if let Some(port) = client.port {
format!("{}[{}]:{}", client.hostname, client.ip, port)
} else {
format!("{}[{}]", client.hostname, client.ip)
}
})
.unwrap_or_else(|| "N/A".to_string())
}
pub fn formatted_relay(&self) -> String {
self.relay_info
.as_ref()
.map(|relay| {
if relay.is_none {
"none".to_string()
} else if let Some(ip) = &relay.ip {
if let Some(port) = relay.port {
format!("{}[{}]:{}", relay.hostname, ip, port)
} else {
format!("{}[{}]", relay.hostname, ip)
}
} else {
relay.hostname.clone()
}
})
.unwrap_or_else(|| "N/A".to_string())
}
pub fn formatted_status(&self) -> String {
self.status_info
.as_ref()
.map(|status| {
let mut result = status.status.clone();
if let Some(desc) = &status.description {
result.push_str(&format!(" ({})", desc));
}
result
})
.unwrap_or_else(|| "N/A".to_string())
}
}
pub trait ExtendedLogEntry {
fn base_entry(&self) -> &BaseLogEntry;
fn component_type(&self) -> &'static str;
fn event_type(&self) -> &'static str;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_base_log_entry_creation() {
let message =
"4bG4VR5z: from=<sender@example.com>, to=<recipient@example.com>, size=1234, delay=5.5";
let entry = BaseLogEntry::from_message(message, Some("4bG4VR5z".to_string()));
assert_eq!(entry.queue_id, Some("4bG4VR5z".to_string()));
assert!(entry.from_email.is_some());
assert!(entry.to_email.is_some());
assert_eq!(entry.size, Some(1234));
assert!(entry.delay_info.is_some());
assert_eq!(entry.delay_info.as_ref().unwrap().total, 5.5);
}
#[test]
fn test_formatted_methods() {
let message = "4bG4VR5z: from=<sender@example.com>, to=<recipient@example.com>, client=mail.example.com[192.168.1.100]:25";
let entry = BaseLogEntry::from_message(message, None);
assert_eq!(entry.formatted_from(), "<sender@example.com>");
assert_eq!(entry.formatted_to(), "<recipient@example.com>");
assert_eq!(
entry.formatted_client(),
"mail.example.com[192.168.1.100]:25"
);
}
#[test]
fn test_empty_from_address() {
let message = "4bG4VR5z: from=<>, to=<recipient@example.com>";
let entry = BaseLogEntry::from_message(message, None);
assert_eq!(entry.formatted_from(), "<>");
assert!(entry.from_email.as_ref().unwrap().is_empty);
}
#[test]
fn test_capability_checks() {
let delivery_message = "4bG4VR5z: to=<user@example.com>, relay=mx.example.com, status=sent";
let delivery_entry = BaseLogEntry::from_message(delivery_message, None);
assert!(delivery_entry.has_delivery_info());
let client_message =
"4bG4VR5z: client=mail.example.com[192.168.1.100], helo=<mail.example.com>";
let client_entry = BaseLogEntry::from_message(client_message, None);
assert!(client_entry.has_client_info());
let auth_message = "4bG4VR5z: sasl_method=PLAIN, sasl_username=testuser";
let auth_entry = BaseLogEntry::from_message(auth_message, None);
assert!(auth_entry.has_auth_info());
}
}