use crate::components::ComponentParser;
use crate::error::ParseError;
use crate::events::smtpd::{CommandStats, RejectType};
use crate::events::{ComponentEvent, SmtpdEvent};
use crate::utils::queue_id::create_queue_id_pattern;
use regex::Regex;
pub struct SmtpdParser {
connect_regex: Regex,
disconnect_regex: Regex,
lost_connection_regex: Regex,
timeout_regex: Regex,
client_assignment_regex: Regex,
noqueue_filter_regex: Regex,
sasl_auth_failure_regex: Regex,
reject_noqueue_regex: Regex,
helo_regex: Regex,
system_warning_regex: Regex,
command_stats_regex: Regex,
}
impl SmtpdParser {
pub fn new() -> Self {
Self {
connect_regex: Regex::new(r"^connect from ([^\[]+)\[([^\]]+)\](?::(\d+))?").unwrap(),
disconnect_regex: Regex::new(r"^disconnect from ([^\[]+)\[([^\]]+)\](?::(\d+))?(.*)?").unwrap(),
lost_connection_regex: Regex::new(r"^lost connection after (\w+) from ([^\[]+)\[([^\]]+)\]").unwrap(),
timeout_regex: Regex::new(r"^timeout after (\w+) from ([^\[]+)\[([^\]]+)\]").unwrap(),
client_assignment_regex: Regex::new(&create_queue_id_pattern(r"^{QUEUE_ID}: client=([^\[]+)\[([^\]]+)\](?::(\d+))?")).unwrap(),
noqueue_filter_regex: Regex::new(r"^NOQUEUE: filter: RCPT from ([^\[]+)\[([^\]]+)\]: (.+)").unwrap(),
sasl_auth_failure_regex: Regex::new(r"([^\[]+)\[([^\]]+)\]: SASL (\w+) authentication failed: (.+)").unwrap(),
reject_noqueue_regex: Regex::new(r"^NOQUEUE: reject: (\w+) from ([^\[]+)\[([^\]]+)\]: (\d{3}) (.+?)(?:; from=<([^>]+)> to=<([^>]+)>)?").unwrap(),
helo_regex: Regex::new(r"client=([^\[]+)\[([^\]]+)\], helo=<([^>]+)>").unwrap(),
system_warning_regex: Regex::new(r"^(dict_\w+|hostname \w+|non-SMTP|Illegal|\w+_init|address syntax|TLS|SASL|milter).*").unwrap(),
command_stats_regex: Regex::new(r"\s*(?:ehlo=(\d+))?\s*(?:helo=(\d+))?\s*(?:mail=(\d+))?\s*(?:rcpt=(\d+))?\s*(?:data=(\d+))?\s*(?:bdat=(\d+))?\s*(?:quit=(\d+))?\s*(?:commands=(\d+))?\s*").unwrap(),
}
}
fn parse_command_stats(&self, stats_text: &str) -> Option<CommandStats> {
self.command_stats_regex.captures(stats_text).map(|captures| CommandStats {
ehlo: captures.get(1).and_then(|m| m.as_str().parse().ok()),
helo: captures.get(2).and_then(|m| m.as_str().parse().ok()),
mail: captures.get(3).and_then(|m| m.as_str().parse().ok()),
rcpt: captures.get(4).and_then(|m| m.as_str().parse().ok()),
data: captures.get(5).and_then(|m| m.as_str().parse().ok()),
bdat: captures.get(6).and_then(|m| m.as_str().parse().ok()),
quit: captures.get(7).and_then(|m| m.as_str().parse().ok()),
commands: captures.get(8).and_then(|m| m.as_str().parse().ok()),
})
}
}
impl ComponentParser for SmtpdParser {
fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
if let Some(captures) = self.client_assignment_regex.captures(message) {
let queue_id = captures.get(1).unwrap().as_str().to_string();
let client_hostname = captures.get(2).unwrap().as_str().to_string();
let client_ip = captures.get(3).unwrap().as_str().to_string();
let port = captures.get(4).and_then(|m| m.as_str().parse::<u16>().ok());
return Ok(ComponentEvent::Smtpd(SmtpdEvent::ClientAssignment {
queue_id,
client_ip,
client_hostname,
port,
}));
}
if let Some(captures) = self.connect_regex.captures(message) {
let client_hostname = captures.get(1).unwrap().as_str().to_string();
let client_ip = captures.get(2).unwrap().as_str().to_string();
let port = captures.get(3).and_then(|m| m.as_str().parse::<u16>().ok());
return Ok(ComponentEvent::Smtpd(SmtpdEvent::Connect {
client_ip,
client_hostname,
port,
}));
}
if let Some(captures) = self.disconnect_regex.captures(message) {
let client_hostname = captures.get(1).unwrap().as_str().to_string();
let client_ip = captures.get(2).unwrap().as_str().to_string();
let port = captures.get(3).and_then(|m| m.as_str().parse::<u16>().ok());
let stats_part = captures.get(4).map(|m| m.as_str()).unwrap_or("");
let command_stats = if !stats_part.is_empty() {
self.parse_command_stats(stats_part)
} else {
None
};
return Ok(ComponentEvent::Smtpd(SmtpdEvent::Disconnect {
client_ip,
client_hostname,
port,
command_stats,
}));
}
if let Some(captures) = self.lost_connection_regex.captures(message) {
let last_command = Some(captures.get(1).unwrap().as_str().to_string());
let client_hostname = captures.get(2).unwrap().as_str().to_string();
let client_ip = captures.get(3).unwrap().as_str().to_string();
return Ok(ComponentEvent::Smtpd(SmtpdEvent::LostConnection {
client_ip,
client_hostname,
last_command,
}));
}
if let Some(captures) = self.timeout_regex.captures(message) {
let last_command = Some(captures.get(1).unwrap().as_str().to_string());
let client_hostname = captures.get(2).unwrap().as_str().to_string();
let client_ip = captures.get(3).unwrap().as_str().to_string();
return Ok(ComponentEvent::Smtpd(SmtpdEvent::Timeout {
client_ip,
client_hostname,
last_command,
}));
}
if let Some(captures) = self.reject_noqueue_regex.captures(message) {
let _cmd = captures.get(1).unwrap().as_str();
let client_hostname = captures.get(2).unwrap().as_str().to_string();
let client_ip = captures.get(3).unwrap().as_str().to_string();
let code = captures.get(4).unwrap().as_str().parse::<u16>().ok();
let reason = captures.get(5).unwrap().as_str().to_string();
let from = captures.get(6).map(|m| m.as_str().to_string());
let to = captures.get(7).map(|m| m.as_str().to_string());
return Ok(ComponentEvent::Smtpd(SmtpdEvent::Reject {
reason,
code,
reject_type: RejectType::NoQueue,
from,
to,
client_ip: Some(client_ip),
client_hostname: Some(client_hostname),
}));
}
if let Some(captures) = self.noqueue_filter_regex.captures(message) {
let client_hostname = captures.get(1).unwrap().as_str().to_string();
let client_ip = captures.get(2).unwrap().as_str().to_string();
let filter_info = captures.get(3).unwrap().as_str().to_string();
return Ok(ComponentEvent::Smtpd(SmtpdEvent::NoQueueFilter {
client_ip,
client_hostname,
filter_info: filter_info.clone(),
filter_target: filter_info, }));
}
if let Some(captures) = self.sasl_auth_failure_regex.captures(message) {
let _client_hostname = captures.get(1).unwrap().as_str().to_string();
let _client_ip = captures.get(2).unwrap().as_str().to_string();
let method = captures.get(3).unwrap().as_str().to_string();
let failure_reason = Some(captures.get(4).unwrap().as_str().to_string());
return Ok(ComponentEvent::Smtpd(SmtpdEvent::Auth {
method,
username: "unknown".to_string(), success: false,
failure_reason,
}));
}
if let Some(captures) = self.helo_regex.captures(message) {
let client_hostname = Some(captures.get(1).unwrap().as_str().to_string());
let client_ip = Some(captures.get(2).unwrap().as_str().to_string());
let hostname = captures.get(3).unwrap().as_str().to_string();
return Ok(ComponentEvent::Smtpd(SmtpdEvent::Helo {
hostname,
client_ip,
client_hostname,
}));
}
if let Some(_captures) = self.system_warning_regex.captures(message) {
let warning_message = message.to_string();
let warning_type = if warning_message.contains("dict_nis_init") {
"nis_config".to_string()
} else if warning_message.contains("dict_") {
"dictionary_config".to_string()
} else if warning_message.contains("non-SMTP command") {
"protocol_violation".to_string()
} else if warning_message.contains("Illegal address syntax") {
"address_syntax".to_string()
} else if warning_message.contains("hostname") {
"hostname_config".to_string()
} else if warning_message.contains("TLS") {
"tls_config".to_string()
} else if warning_message.contains("SASL") {
"sasl_config".to_string()
} else {
"general".to_string()
};
return Ok(ComponentEvent::Smtpd(SmtpdEvent::SystemWarning {
warning_type,
message: warning_message,
client_info: None, }));
}
Err(ParseError::ComponentParseError {
component: "smtpd".to_string(),
reason: format!("无法解析消息: {}", message),
})
}
fn component_name(&self) -> &'static str {
"smtpd"
}
fn can_parse(&self, message: &str) -> bool {
self.connect_regex.is_match(message)
|| self.disconnect_regex.is_match(message)
|| self.lost_connection_regex.is_match(message)
|| self.timeout_regex.is_match(message)
|| self.client_assignment_regex.is_match(message)
|| self.reject_noqueue_regex.is_match(message)
|| self.noqueue_filter_regex.is_match(message)
|| self.sasl_auth_failure_regex.is_match(message)
|| self.helo_regex.is_match(message)
|| self.system_warning_regex.is_match(message)
}
}
impl Default for SmtpdParser {
fn default() -> Self {
Self::new()
}
}