use crate::error::ParseError;
use crate::events::local::{LocalEvent, ConfigurationWarning, LocalDelivery, ExternalDelivery, WarningType, DeliveryStatus, DeliveryMethod};
use crate::events::base::BaseEvent;
use crate::events::ComponentEvent;
use super::ComponentParser;
use regex::Regex;
use std::collections::HashMap;
pub struct LocalParser {
nis_domain_regex: Regex,
alias_not_found_regex: Regex,
local_delivery_regex: Regex,
external_delivery_regex: Regex,
}
impl LocalParser {
pub fn new() -> Self {
Self {
nis_domain_regex: Regex::new(
r"^dict_nis_init: NIS domain name not set - NIS lookups disabled$"
).expect("LOCAL NIS域名警告正则表达式编译失败"),
alias_not_found_regex: Regex::new(
r"^required alias not found: (.+)$"
).expect("LOCAL别名未找到警告正则表达式编译失败"),
local_delivery_regex: Regex::new(
r"^([0-9A-F]+): to=<([^>]+)>(?:, orig_to=<([^>]+)>)?, relay=([^,]+), delay=([0-9.]+), delays=([0-9.]+/[0-9.]+/[0-9.]+/[0-9.]+), dsn=([^,]+), status=(\w+)(?: \(([^)]+)\))?$"
).expect("LOCAL本地投递正则表达式编译失败"),
external_delivery_regex: Regex::new(
r"([0-9A-F]+): to=<([^>]+)>, relay=([^,]+), delay=([0-9.]+), (?:cmd|file)=(.+), status=(\w+)"
).expect("LOCAL外部投递正则表达式编译失败"),
}
}
pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<LocalEvent> {
if let Some(event) = self.parse_configuration_warning(line, base_event.clone()) {
return Some(event);
}
if let Some(event) = self.parse_local_delivery(line, base_event.clone()) {
return Some(event);
}
if let Some(event) = self.parse_external_delivery(line, base_event) {
return Some(event);
}
None
}
fn parse_configuration_warning(&self, line: &str, base_event: BaseEvent) -> Option<LocalEvent> {
if self.nis_domain_regex.is_match(line) {
let warning = ConfigurationWarning {
timestamp: base_event.timestamp,
warning_type: WarningType::NisDomainNotSet,
message: "NIS domain name not set - NIS lookups disabled".to_string(),
details: HashMap::new(),
};
return Some(LocalEvent::ConfigurationWarning(warning));
}
if let Some(caps) = self.alias_not_found_regex.captures(line) {
let alias_name = caps.get(1).unwrap().as_str();
let mut details = HashMap::new();
details.insert("alias_name".to_string(), alias_name.to_string());
let warning = ConfigurationWarning {
timestamp: base_event.timestamp,
warning_type: WarningType::RequiredAliasNotFound,
message: format!("required alias not found: {}", alias_name),
details,
};
return Some(LocalEvent::ConfigurationWarning(warning));
}
None
}
fn parse_local_delivery(&self, line: &str, base_event: BaseEvent) -> Option<LocalEvent> {
if let Some(caps) = self.local_delivery_regex.captures(line) {
let queue_id = caps.get(1).unwrap().as_str().to_string();
let recipient = caps.get(2).unwrap().as_str().to_string();
let original_recipient = caps.get(3).map(|m| m.as_str().to_string());
let relay = caps.get(4).unwrap().as_str().to_string();
let delay = caps.get(5).unwrap().as_str().parse::<f64>().unwrap_or(0.0);
let delays_str = caps.get(6).unwrap().as_str();
let dsn = caps.get(7).unwrap().as_str().to_string();
let status_str = caps.get(8).unwrap().as_str();
let extra_info = caps.get(9).map(|m| m.as_str());
let delays: Vec<f64> = delays_str
.split('/')
.map(|s| s.parse::<f64>().unwrap_or(0.0))
.collect();
let status = match status_str.to_lowercase().as_str() {
"sent" => DeliveryStatus::Sent,
"bounced" => DeliveryStatus::Bounced,
"deferred" => DeliveryStatus::Deferred,
_ => DeliveryStatus::Sent,
};
let delivery_method = if let Some(info) = extra_info {
if info.contains("discarded") {
DeliveryMethod::Discarded
} else if info.contains("forwarded") {
DeliveryMethod::Forwarded
} else if info.contains("piped") {
DeliveryMethod::Piped
} else if info.contains("file") {
DeliveryMethod::File
} else {
DeliveryMethod::Mailbox
}
} else {
DeliveryMethod::Mailbox
};
let delivery = LocalDelivery {
timestamp: base_event.timestamp,
queue_id,
recipient,
original_recipient,
relay,
delay,
delays,
dsn,
status,
delivery_method,
size: None,
nrcpt: None,
};
return Some(LocalEvent::LocalDelivery(delivery));
}
None
}
fn parse_external_delivery(&self, line: &str, base_event: BaseEvent) -> Option<LocalEvent> {
if let Some(caps) = self.external_delivery_regex.captures(line) {
let queue_id = caps.get(1).unwrap().as_str().to_string();
let recipient = caps.get(2).unwrap().as_str().to_string();
let relay = caps.get(3).unwrap().as_str().to_string();
let delay = caps.get(4).unwrap().as_str().parse::<f64>().unwrap_or(0.0);
let external_target = caps.get(5).unwrap().as_str().to_string();
let status_str = caps.get(6).unwrap().as_str();
let status = match status_str.to_lowercase().as_str() {
"sent" => DeliveryStatus::Sent,
"bounced" => DeliveryStatus::Bounced,
"deferred" => DeliveryStatus::Deferred,
_ => DeliveryStatus::Sent,
};
let mut details = HashMap::new();
details.insert("relay".to_string(), relay);
let (command, file_path) = if external_target.starts_with('/') {
(None, Some(external_target))
} else {
(Some(external_target), None)
};
let delivery = ExternalDelivery {
timestamp: base_event.timestamp,
queue_id,
recipient,
original_recipient: None,
command,
file_path,
status,
delay,
details,
};
return Some(LocalEvent::ExternalDelivery(delivery));
}
None
}
}
impl ComponentParser for LocalParser {
fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
let base_event = BaseEvent {
timestamp: chrono::Utc::now(),
hostname: "temp".to_string(),
component: "local".to_string(),
process_id: 0,
log_level: crate::events::base::PostfixLogLevel::Info,
raw_message: message.to_string(),
};
if let Some(local_event) = self.parse_line(message, base_event) {
Ok(ComponentEvent::Local(local_event))
} else {
Err(ParseError::ComponentParseError {
component: "local".to_string(),
reason: "无法识别的local日志格式".to_string(),
})
}
}
fn component_name(&self) -> &'static str {
"local"
}
}
impl Default for LocalParser {
fn default() -> Self {
Self::new()
}
}