use regex::Regex;
use crate::error::ParseError;
use crate::events::bounce::{BounceEvent, BounceWarningType};
use crate::events::{base::BaseEvent, ComponentEvent};
use super::ComponentParser;
pub struct BounceParser {
sender_notification_regex: Regex,
postmaster_notification_regex: Regex,
warning_malformed_regex: Regex,
warning_general_regex: Regex,
}
impl BounceParser {
pub fn new() -> Self {
Self {
sender_notification_regex: Regex::new(
r"^([A-F0-9]+):\s+sender non-delivery notification:\s+([A-F0-9]+)$",
)
.expect("BOUNCE发送者通知正则表达式编译失败"),
postmaster_notification_regex: Regex::new(
r"^([A-F0-9]+):\s+postmaster non-delivery notification:\s+([A-F0-9]+)$",
)
.expect("BOUNCE邮件管理员通知正则表达式编译失败"),
warning_malformed_regex: Regex::new(r"^malformed request$")
.expect("BOUNCE格式错误警告正则表达式编译失败"),
warning_general_regex: Regex::new(r"^(.+)$").expect("BOUNCE一般警告正则表达式编译失败"),
}
}
pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<BounceEvent> {
if let Some(captures) = self.sender_notification_regex.captures(line) {
return self.parse_sender_notification(captures, base_event);
}
if let Some(captures) = self.postmaster_notification_regex.captures(line) {
return self.parse_postmaster_notification(captures, base_event);
}
if self.warning_malformed_regex.is_match(line) {
return self.parse_malformed_warning(base_event);
}
if self.is_bounce_warning(line) {
if let Some(captures) = self.warning_general_regex.captures(line) {
return self.parse_general_warning(captures, base_event);
}
}
None
}
fn parse_sender_notification(
&self,
captures: regex::Captures,
base_event: BaseEvent,
) -> Option<BounceEvent> {
let original_queue_id = captures.get(1)?.as_str().to_string();
let bounce_queue_id = captures.get(2)?.as_str().to_string();
Some(BounceEvent::SenderNotification {
base: base_event,
original_queue_id,
bounce_queue_id,
})
}
fn parse_postmaster_notification(
&self,
captures: regex::Captures,
base_event: BaseEvent,
) -> Option<BounceEvent> {
let original_queue_id = captures.get(1)?.as_str().to_string();
let bounce_queue_id = captures.get(2)?.as_str().to_string();
Some(BounceEvent::PostmasterNotification {
base: base_event,
original_queue_id,
bounce_queue_id,
})
}
fn parse_malformed_warning(&self, base_event: BaseEvent) -> Option<BounceEvent> {
Some(BounceEvent::Warning {
base: base_event,
warning_type: BounceWarningType::MalformedRequest,
details: "malformed request".to_string(),
})
}
fn parse_general_warning(
&self,
captures: regex::Captures,
base_event: BaseEvent,
) -> Option<BounceEvent> {
let warning_message = captures.get(1)?.as_str();
let warning_type = if warning_message.contains("malformed") {
BounceWarningType::MalformedRequest
} else if warning_message.contains("configuration") || warning_message.contains("config") {
BounceWarningType::Configuration
} else if warning_message.contains("resource")
|| warning_message.contains("memory")
|| warning_message.contains("disk")
{
BounceWarningType::ResourceLimit
} else {
BounceWarningType::Other
};
Some(BounceEvent::Warning {
base: base_event,
warning_type,
details: warning_message.to_string(),
})
}
fn is_bounce_warning(&self, message: &str) -> bool {
message.contains("malformed")
|| message.contains("configuration")
|| message.contains("resource")
|| message.contains("memory")
|| message.contains("disk")
|| message.contains("queue")
|| message.contains("bounce")
}
}
impl ComponentParser for BounceParser {
fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
let base_event = BaseEvent {
timestamp: chrono::Utc::now(),
hostname: "temp".to_string(),
component: "bounce".to_string(),
process_id: 0,
log_level: crate::events::base::PostfixLogLevel::Info,
raw_message: message.to_string(),
};
if let Some(bounce_event) = self.parse_line(message, base_event) {
Ok(ComponentEvent::Bounce(bounce_event))
} else {
Err(ParseError::ComponentParseError {
component: "bounce".to_string(),
reason: "无法识别的bounce日志格式".to_string(),
})
}
}
fn component_name(&self) -> &'static str {
"bounce"
}
fn can_parse(&self, message: &str) -> bool {
self.sender_notification_regex.is_match(message)
|| self.postmaster_notification_regex.is_match(message)
|| self.warning_malformed_regex.is_match(message)
|| self.is_bounce_warning(message)
}
}
impl Default for BounceParser {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events::base::BaseEvent;
use chrono::{DateTime, Utc};
fn create_test_base_event() -> BaseEvent {
BaseEvent {
timestamp: DateTime::parse_from_rfc3339("2024-04-27T16:20:48+00:00")
.unwrap()
.with_timezone(&Utc),
hostname: "m01".to_string(),
component: "bounce".to_string(),
process_id: 133,
log_level: crate::events::base::PostfixLogLevel::Info,
raw_message: "test message".to_string(),
}
}
#[test]
fn test_parse_sender_notification() {
let parser = BounceParser::new();
let base_event = create_test_base_event();
let message = "5FC392A20996: sender non-delivery notification: 732B92A209A3";
let result = parser.parse_line(message, base_event);
assert!(result.is_some());
if let Some(BounceEvent::SenderNotification {
original_queue_id,
bounce_queue_id,
..
}) = result
{
assert_eq!(original_queue_id, "5FC392A20996");
assert_eq!(bounce_queue_id, "732B92A209A3");
} else {
panic!("解析结果类型不正确");
}
}
#[test]
fn test_parse_postmaster_notification() {
let parser = BounceParser::new();
let base_event = create_test_base_event();
let message = "633F488423: postmaster non-delivery notification: 6DFA788422";
let result = parser.parse_line(message, base_event);
assert!(result.is_some());
if let Some(BounceEvent::PostmasterNotification {
original_queue_id,
bounce_queue_id,
..
}) = result
{
assert_eq!(original_queue_id, "633F488423");
assert_eq!(bounce_queue_id, "6DFA788422");
} else {
panic!("解析结果类型不正确");
}
}
#[test]
fn test_parse_malformed_warning() {
let parser = BounceParser::new();
let base_event = create_test_base_event();
let message = "malformed request";
let result = parser.parse_line(message, base_event);
assert!(result.is_some());
if let Some(BounceEvent::Warning {
warning_type,
details,
..
}) = result
{
assert!(matches!(warning_type, BounceWarningType::MalformedRequest));
assert_eq!(details, "malformed request");
} else {
panic!("解析结果类型不正确");
}
}
#[test]
fn test_parse_general_warning() {
let parser = BounceParser::new();
let base_event = create_test_base_event();
let message = "configuration file error";
let result = parser.parse_line(message, base_event);
assert!(result.is_some());
if let Some(BounceEvent::Warning {
warning_type,
details,
..
}) = result
{
assert!(matches!(warning_type, BounceWarningType::Configuration));
assert_eq!(details, "configuration file error");
} else {
panic!("解析结果类型不正确");
}
}
#[test]
fn test_can_parse() {
let parser = BounceParser::new();
assert!(parser.can_parse("5FC392A20996: sender non-delivery notification: 732B92A209A3"));
assert!(parser.can_parse("633F488423: postmaster non-delivery notification: 6DFA788422"));
assert!(parser.can_parse("malformed request"));
assert!(parser.can_parse("configuration error"));
assert!(!parser.can_parse("some random message"));
assert!(!parser.can_parse("info: normal operation"));
assert!(!parser.can_parse("5FC392A20996: to=<user@domain.com>, status=sent"));
}
#[test]
fn test_component_parser_interface() {
let parser = BounceParser::new();
assert_eq!(parser.component_name(), "bounce");
let result = parser.parse("5FC392A20996: sender non-delivery notification: 732B92A209A3");
assert!(result.is_ok());
if let Ok(ComponentEvent::Bounce(bounce_event)) = result {
match bounce_event {
BounceEvent::SenderNotification {
original_queue_id,
bounce_queue_id,
..
} => {
assert_eq!(original_queue_id, "5FC392A20996");
assert_eq!(bounce_queue_id, "732B92A209A3");
}
_ => panic!("解析结果类型不正确"),
}
} else {
panic!("解析失败");
}
let result = parser.parse("some random text that should not match");
assert!(result.is_err());
if let Err(ParseError::ComponentParseError { component, reason }) = result {
assert_eq!(component, "bounce");
assert!(reason.contains("无法识别的bounce日志格式"));
} else {
panic!("错误类型不正确");
}
}
}