use crate::components::ComponentParser;
use crate::error::ParseError;
use crate::events::base::{BaseEvent, PostfixLogLevel};
use crate::events::postfix_script::{
PostfixScriptEvent, PostfixScriptFatalError, PostfixScriptOperation, PostfixScriptWarningType,
};
use crate::events::ComponentEvent;
use chrono::{DateTime, Utc};
use regex::Regex;
pub struct PostfixScriptParser {
starting_regex: Regex,
running_regex: Regex,
refreshing_regex: Regex,
cannot_execute_postconf_regex: Regex,
cannot_execute_command_regex: Regex,
not_owned_regex: Regex,
group_writable_regex: Regex,
symlink_leaves_regex: Regex,
}
impl PostfixScriptParser {
pub fn new() -> Self {
PostfixScriptParser {
starting_regex: Regex::new(r"^starting the Postfix mail system\s*$").unwrap(),
running_regex: Regex::new(r"^the Postfix mail system is running: PID: (\d+)\s*$")
.unwrap(),
refreshing_regex: Regex::new(r"^refreshing the Postfix mail system\s*$").unwrap(),
cannot_execute_postconf_regex: Regex::new(r"^cannot execute /usr/sbin/postconf!$")
.unwrap(),
cannot_execute_command_regex: Regex::new(r"^cannot execute (.+?)!?$").unwrap(),
not_owned_regex: Regex::new(r"^not owned by (root|postfix): (.+)$").unwrap(),
group_writable_regex: Regex::new(r"^group or other writable: (.+)$").unwrap(),
symlink_leaves_regex: Regex::new(r"^symlink leaves directory: (.+)$").unwrap(),
}
}
fn create_base_event(&self, message: &str) -> BaseEvent {
BaseEvent {
timestamp: DateTime::parse_from_rfc3339("2024-04-27T16:20:48Z")
.unwrap()
.with_timezone(&Utc),
hostname: "unknown".to_string(),
component: "postfix-script".to_string(),
process_id: 0,
log_level: PostfixLogLevel::Info,
raw_message: message.to_string(),
}
}
fn parse_system_operation(&self, message: &str) -> Option<PostfixScriptEvent> {
let base = self.create_base_event(message);
if self.starting_regex.is_match(message) {
return Some(PostfixScriptEvent::SystemOperation {
base,
operation: PostfixScriptOperation::Starting,
});
}
if let Some(caps) = self.running_regex.captures(message) {
let pid = caps.get(1).and_then(|m| m.as_str().parse::<u32>().ok());
return Some(PostfixScriptEvent::SystemOperation {
base,
operation: PostfixScriptOperation::Running { pid },
});
}
if self.refreshing_regex.is_match(message) {
return Some(PostfixScriptEvent::SystemOperation {
base,
operation: PostfixScriptOperation::Refreshing,
});
}
None
}
fn parse_fatal_error(&self, message: &str) -> Option<PostfixScriptEvent> {
let mut base = self.create_base_event(message);
base.log_level = PostfixLogLevel::Fatal;
if self.cannot_execute_postconf_regex.is_match(message) {
return Some(PostfixScriptEvent::FatalError {
base,
error: PostfixScriptFatalError::CannotExecutePostconf,
});
}
if let Some(caps) = self.cannot_execute_command_regex.captures(message) {
let command = caps
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
return Some(PostfixScriptEvent::FatalError {
base,
error: PostfixScriptFatalError::CannotExecuteCommand { command },
});
}
Some(PostfixScriptEvent::FatalError {
base,
error: PostfixScriptFatalError::Other {
message: message.to_string(),
},
})
}
fn parse_warning(&self, message: &str) -> Option<PostfixScriptEvent> {
let mut base = self.create_base_event(message);
base.log_level = PostfixLogLevel::Warning;
if let Some(caps) = self.not_owned_regex.captures(message) {
let expected_owner = caps
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
let path = caps
.get(2)
.map(|m| m.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
return Some(PostfixScriptEvent::Warning {
base,
warning: PostfixScriptWarningType::NotOwnedBy {
path,
expected_owner,
},
});
}
if let Some(caps) = self.group_writable_regex.captures(message) {
let path = caps
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
return Some(PostfixScriptEvent::Warning {
base,
warning: PostfixScriptWarningType::GroupWritable { path },
});
}
if let Some(caps) = self.symlink_leaves_regex.captures(message) {
let path = caps
.get(1)
.map(|m| m.as_str().to_string())
.unwrap_or_else(|| "unknown".to_string());
return Some(PostfixScriptEvent::Warning {
base,
warning: PostfixScriptWarningType::SymlinkLeaves { path },
});
}
Some(PostfixScriptEvent::Warning {
base,
warning: PostfixScriptWarningType::Other {
message: message.to_string(),
},
})
}
}
impl ComponentParser for PostfixScriptParser {
fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
if message.contains("fatal:") || message.contains("cannot execute") {
if let Some(event) = self.parse_fatal_error(message) {
return Ok(ComponentEvent::PostfixScript(event));
}
}
if message.contains("warning:")
|| message.contains("not owned by")
|| message.contains("group or other writable")
{
if let Some(event) = self.parse_warning(message) {
return Ok(ComponentEvent::PostfixScript(event));
}
}
if let Some(event) = self.parse_system_operation(message) {
return Ok(ComponentEvent::PostfixScript(event));
}
Err(ParseError::ComponentParseError {
component: "postfix-script".to_string(),
reason: format!("Unsupported message format: {}", message),
})
}
fn component_name(&self) -> &'static str {
"postfix-script"
}
fn can_parse(&self, component: &str) -> bool {
component == "postfix-script"
}
}
impl Default for PostfixScriptParser {
fn default() -> Self {
Self::new()
}
}