use regex::Regex;
use crate::events::sendmail::{SendmailEvent, SendmailEventType};
#[derive(Debug)]
pub struct SendmailParser {
fatal_usage_regex: Regex, usage_only_regex: Regex, }
impl SendmailParser {
pub fn new() -> Self {
Self {
fatal_usage_regex: Regex::new(r"^fatal: (.+)$")
.expect("Failed to compile fatal usage regex"),
usage_only_regex: Regex::new(r"^usage: (.+)$")
.expect("Failed to compile usage only regex"),
}
}
pub fn parse_log_line(&self, line: &str) -> Result<SendmailEvent, String> {
let basic_regex = Regex::new(
r"^((?:\d{4}\s+)?\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+\S+\s+postfix/sendmail\[(\d+)\]:\s+(.+)$",
)
.map_err(|e| format!("Regex compilation error: {}", e))?;
let captures = basic_regex
.captures(line)
.ok_or_else(|| "Line does not match sendmail log format".to_string())?;
let timestamp = captures.get(1).unwrap().as_str();
let process_id = captures.get(2).unwrap().as_str();
let message = captures.get(3).unwrap().as_str();
if let Some(event) = self.parse_fatal_usage_error(timestamp, process_id, message) {
return Ok(event);
}
Err(format!("Unknown sendmail message type: {}", message))
}
pub fn supported_event_types(&self) -> usize {
1
}
pub fn matches_component(&self, line: &str) -> bool {
line.contains("postfix/sendmail[")
}
fn parse_fatal_usage_error(
&self,
timestamp: &str,
process_id: &str,
message: &str,
) -> Option<SendmailEvent> {
if let Some(captures) = self.fatal_usage_regex.captures(message) {
let error_message = captures.get(1).unwrap().as_str();
Some(SendmailEvent {
timestamp: timestamp.to_string(),
process_id: process_id.to_string(),
event_type: SendmailEventType::FatalUsageError {
message: error_message.to_string(),
},
})
} else {
None
}
}
}
impl Default for SendmailParser {
fn default() -> Self {
Self::new()
}
}
impl crate::components::ComponentParser for SendmailParser {
fn parse(
&self,
message: &str,
) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
if let Some(captures) = self.usage_only_regex.captures(message) {
let error_message = captures.get(1).unwrap().as_str();
let event = SendmailEvent {
timestamp: "0".to_string(), process_id: "0".to_string(), event_type: SendmailEventType::FatalUsageError {
message: format!("usage: {}", error_message),
},
};
return Ok(crate::events::base::ComponentEvent::Sendmail(event));
}
Err(crate::error::ParseError::ComponentParseError {
component: self.component_name().to_string(),
reason: format!("Unable to parse sendmail message: {}", message),
})
}
fn component_name(&self) -> &'static str {
"sendmail"
}
fn can_parse(&self, message: &str) -> bool {
self.usage_only_regex.is_match(message)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::components::ComponentParser;
fn create_parser() -> SendmailParser {
SendmailParser::new()
}
#[test]
fn test_fatal_usage_error_parsing() {
let parser = create_parser();
let log_line = "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]";
let result = parser.parse_log_line(log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.timestamp, "Apr 24 17:20:55");
assert_eq!(event.process_id, "180");
let SendmailEventType::FatalUsageError { message } = event.event_type;
assert_eq!(message, "usage: mailq [options]");
}
#[test]
fn test_different_process_ids() {
let parser = create_parser();
let test_cases = vec!["180", "187", "208", "216", "223", "230"];
for pid in test_cases {
let log_line = format!(
"Apr 24 17:20:55 m01 postfix/sendmail[{}]: fatal: usage: mailq [options]",
pid
);
let result = parser.parse_log_line(&log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.process_id, pid);
}
}
#[test]
fn test_different_timestamps() {
let parser = create_parser();
let test_cases = vec![
"Apr 24 17:20:55",
"Apr 24 17:20:56",
"Apr 24 17:23:16",
"Apr 24 17:23:19",
];
for timestamp in test_cases {
let log_line = format!(
"{} m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
timestamp
);
let result = parser.parse_log_line(&log_line);
assert!(result.is_ok());
let event = result.unwrap();
assert_eq!(event.timestamp, timestamp);
}
}
#[test]
fn test_component_matching() {
let parser = create_parser();
let matching_lines = vec![
"Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
"Apr 24 17:20:56 m01 postfix/sendmail[187]: fatal: some other error",
];
for line in matching_lines {
assert!(parser.matches_component(line), "Should match: {}", line);
}
let non_matching_lines = vec![
"Apr 24 17:20:55 m01 postfix/qmgr[78]: info: statistics",
"Apr 24 17:20:55 m01 postfix/smtpd[78]: connect from localhost",
"Apr 24 17:20:55 m01 postfix/cleanup[78]: message-id=<test@example.com>",
];
for line in non_matching_lines {
assert!(
!parser.matches_component(line),
"Should not match: {}",
line
);
}
}
#[test]
fn test_invalid_log_lines() {
let parser = create_parser();
let invalid_lines = vec![
"Invalid log line",
"Apr 24 17:20:55 m01 postfix/qmgr[78]: info: statistics",
"Apr 24 17:20:55 m01 postfix/sendmail[180]: info: some info message",
"incomplete line",
];
for line in invalid_lines {
let result = parser.parse_log_line(line);
assert!(result.is_err(), "Should fail to parse: {}", line);
}
}
#[test]
fn test_supported_event_types() {
let parser = create_parser();
assert_eq!(parser.supported_event_types(), 1);
}
#[test]
fn test_parser_default() {
let parser = SendmailParser::default();
assert_eq!(parser.supported_event_types(), 1);
}
#[test]
fn test_component_parser_parse() {
let parser = SendmailParser::new();
let message = "usage: mailq [options]";
let result = parser.parse(message);
assert!(result.is_ok());
match result.unwrap() {
crate::events::base::ComponentEvent::Sendmail(event) => {
assert_eq!(event.process_id, "0"); let SendmailEventType::FatalUsageError { message } = event.event_type;
assert_eq!(message, "usage: mailq [options]");
}
_ => panic!("Expected Sendmail ComponentEvent"),
}
}
#[test]
fn test_component_parser_invalid() {
let parser = SendmailParser::new();
let message = "some invalid message";
let result = parser.parse(message);
assert!(result.is_err());
match result.unwrap_err() {
crate::error::ParseError::ComponentParseError { component, .. } => {
assert_eq!(component, "sendmail");
}
_ => panic!("Expected ComponentParseError"),
}
}
#[test]
fn test_component_name() {
let parser = SendmailParser::new();
assert_eq!(parser.component_name(), "sendmail");
}
#[test]
fn test_can_parse() {
let parser = SendmailParser::new();
assert!(parser.can_parse("usage: mailq [options]"));
assert!(parser.can_parse("usage: some other command"));
assert!(!parser.can_parse("some random message"));
assert!(!parser.can_parse("info: some info message"));
assert!(!parser.can_parse("warning: some warning"));
assert!(!parser.can_parse("fatal: some other error")); }
#[test]
fn test_parse_real_log_samples() {
let parser = create_parser();
let real_logs = vec![
"Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
"Apr 24 17:20:56 m01 postfix/sendmail[187]: fatal: usage: mailq [options]",
"Apr 24 17:23:16 m01 postfix/sendmail[208]: fatal: usage: mailq [options]",
"Apr 24 17:23:19 m01 postfix/sendmail[216]: fatal: usage: mailq [options]",
"Apr 24 17:23:22 m01 postfix/sendmail[223]: fatal: usage: mailq [options]",
"Apr 24 17:23:24 m01 postfix/sendmail[230]: fatal: usage: mailq [options]",
];
for (i, log_line) in real_logs.iter().enumerate() {
let result = parser.parse_log_line(log_line);
assert!(
result.is_ok(),
"Failed to parse real log sample {}: {}",
i,
log_line
);
let event = result.unwrap();
let SendmailEventType::FatalUsageError { message } = event.event_type;
assert_eq!(message, "usage: mailq [options]");
}
}
}