postfix_log_parser/components/
bounce.rs

1use regex::Regex;
2
3use crate::error::ParseError;
4use crate::events::bounce::{BounceEvent, BounceWarningType};
5use crate::events::{base::BaseEvent, ComponentEvent};
6
7use super::ComponentParser;
8
9/// BOUNCE组件解析器
10///
11/// 基于Postfix BOUNCE守护进程的真实日志格式开发
12/// BOUNCE守护进程的特点:
13/// - 处理投递失败通知(non-delivery notification)
14/// - 生成发送者通知邮件
15/// - 处理退信和延迟通知
16/// - 管理通知邮件的生成和发送
17pub struct BounceParser {
18    /// 发送者投递失败通知事件解析 - 主要模式(95%+频率)
19    /// 格式: "queue_id: sender non-delivery notification: new_queue_id"
20    sender_notification_regex: Regex,
21
22    /// 收件人投递失败通知事件解析
23    /// 格式: "queue_id: postmaster non-delivery notification: new_queue_id"  
24    postmaster_notification_regex: Regex,
25
26    /// 警告事件解析 - malformed request
27    /// 格式: "malformed request" (注意:MasterParser已剥离"warning:"前缀)
28    warning_malformed_regex: Regex,
29
30    /// 其他警告事件解析
31    /// 格式: 各种警告消息 (注意:MasterParser已剥离"warning:"前缀)
32    warning_general_regex: Regex,
33}
34
35impl BounceParser {
36    pub fn new() -> Self {
37        Self {
38            // 主要的发送者投递失败通知事件模式
39            sender_notification_regex: Regex::new(
40                r"^([A-F0-9]+):\s+sender non-delivery notification:\s+([A-F0-9]+)$",
41            )
42            .expect("BOUNCE发送者通知正则表达式编译失败"),
43
44            // 邮件管理员投递失败通知事件模式
45            postmaster_notification_regex: Regex::new(
46                r"^([A-F0-9]+):\s+postmaster non-delivery notification:\s+([A-F0-9]+)$",
47            )
48            .expect("BOUNCE邮件管理员通知正则表达式编译失败"),
49
50            // 格式错误请求警告模式 (MasterParser已剥离"warning:"前缀)
51            warning_malformed_regex: Regex::new(r"^malformed request$")
52                .expect("BOUNCE格式错误警告正则表达式编译失败"),
53
54            // 一般警告事件模式 (MasterParser已剥离"warning:"前缀)
55            warning_general_regex: Regex::new(r"^(.+)$").expect("BOUNCE一般警告正则表达式编译失败"),
56        }
57    }
58
59    /// 解析BOUNCE日志行
60    pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<BounceEvent> {
61        // 尝试解析发送者投递失败通知事件(主要模式)
62        if let Some(captures) = self.sender_notification_regex.captures(line) {
63            return self.parse_sender_notification(captures, base_event);
64        }
65
66        // 尝试解析邮件管理员投递失败通知事件
67        if let Some(captures) = self.postmaster_notification_regex.captures(line) {
68            return self.parse_postmaster_notification(captures, base_event);
69        }
70
71        // 尝试解析格式错误警告
72        if self.warning_malformed_regex.is_match(line) {
73            return self.parse_malformed_warning(base_event);
74        }
75
76        // 尝试解析其他警告事件 (只有在确认是警告消息时才处理)
77        if self.is_bounce_warning(line) {
78            if let Some(captures) = self.warning_general_regex.captures(line) {
79                return self.parse_general_warning(captures, base_event);
80            }
81        }
82
83        None
84    }
85
86    /// 解析发送者投递失败通知事件
87    /// 示例: "5FC392A20996: sender non-delivery notification: 732B92A209A3"
88    fn parse_sender_notification(
89        &self,
90        captures: regex::Captures,
91        base_event: BaseEvent,
92    ) -> Option<BounceEvent> {
93        let original_queue_id = captures.get(1)?.as_str().to_string();
94        let bounce_queue_id = captures.get(2)?.as_str().to_string();
95
96        Some(BounceEvent::SenderNotification {
97            base: base_event,
98            original_queue_id,
99            bounce_queue_id,
100        })
101    }
102
103    /// 解析邮件管理员投递失败通知事件
104    /// 示例: "633F488423: postmaster non-delivery notification: 6DFA788422"
105    fn parse_postmaster_notification(
106        &self,
107        captures: regex::Captures,
108        base_event: BaseEvent,
109    ) -> Option<BounceEvent> {
110        let original_queue_id = captures.get(1)?.as_str().to_string();
111        let bounce_queue_id = captures.get(2)?.as_str().to_string();
112
113        Some(BounceEvent::PostmasterNotification {
114            base: base_event,
115            original_queue_id,
116            bounce_queue_id,
117        })
118    }
119
120    /// 解析格式错误警告事件
121    /// 示例: "warning: malformed request"
122    fn parse_malformed_warning(&self, base_event: BaseEvent) -> Option<BounceEvent> {
123        Some(BounceEvent::Warning {
124            base: base_event,
125            warning_type: BounceWarningType::MalformedRequest,
126            details: "malformed request".to_string(),
127        })
128    }
129
130    /// 解析其他警告事件
131    /// 处理BOUNCE服务的其他警告类型
132    fn parse_general_warning(
133        &self,
134        captures: regex::Captures,
135        base_event: BaseEvent,
136    ) -> Option<BounceEvent> {
137        let warning_message = captures.get(1)?.as_str();
138
139        // 根据警告内容确定警告类型
140        let warning_type = if warning_message.contains("malformed") {
141            BounceWarningType::MalformedRequest
142        } else if warning_message.contains("configuration") || warning_message.contains("config") {
143            BounceWarningType::Configuration
144        } else if warning_message.contains("resource")
145            || warning_message.contains("memory")
146            || warning_message.contains("disk")
147        {
148            BounceWarningType::ResourceLimit
149        } else {
150            BounceWarningType::Other
151        };
152
153        Some(BounceEvent::Warning {
154            base: base_event,
155            warning_type,
156            details: warning_message.to_string(),
157        })
158    }
159
160    /// 判断是否是BOUNCE组件的警告消息
161    fn is_bounce_warning(&self, message: &str) -> bool {
162        // 只有在日志级别为Warning时才考虑作为警告处理
163        // 这个判断会在实际使用中由MasterParser的日志级别提取来处理
164        message.contains("malformed")
165            || message.contains("configuration")
166            || message.contains("resource")
167            || message.contains("memory")
168            || message.contains("disk")
169            || message.contains("queue")
170            || message.contains("bounce")
171    }
172}
173
174impl ComponentParser for BounceParser {
175    fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
176        // 创建一个临时的BaseEvent,用于解析
177        // 在实际使用中,这些字段会被MasterParser正确填充
178        let base_event = BaseEvent {
179            timestamp: chrono::Utc::now(),
180            hostname: "temp".to_string(),
181            component: "bounce".to_string(),
182            process_id: 0,
183            log_level: crate::events::base::PostfixLogLevel::Info,
184            raw_message: message.to_string(),
185        };
186
187        if let Some(bounce_event) = self.parse_line(message, base_event) {
188            Ok(ComponentEvent::Bounce(bounce_event))
189        } else {
190            Err(ParseError::ComponentParseError {
191                component: "bounce".to_string(),
192                reason: "无法识别的bounce日志格式".to_string(),
193            })
194        }
195    }
196
197    fn component_name(&self) -> &'static str {
198        "bounce"
199    }
200
201    fn can_parse(&self, message: &str) -> bool {
202        // 检查是否包含bounce特征
203        self.sender_notification_regex.is_match(message)
204            || self.postmaster_notification_regex.is_match(message)
205            || self.warning_malformed_regex.is_match(message)
206            // 对于一般警告,我们需要更精确的判断,而不是匹配所有内容
207            || self.is_bounce_warning(message)
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::events::base::BaseEvent;
215    use chrono::{DateTime, Utc};
216
217    fn create_test_base_event() -> BaseEvent {
218        BaseEvent {
219            timestamp: DateTime::parse_from_rfc3339("2024-04-27T16:20:48+00:00")
220                .unwrap()
221                .with_timezone(&Utc),
222            hostname: "m01".to_string(),
223            component: "bounce".to_string(),
224            process_id: 133,
225            log_level: crate::events::base::PostfixLogLevel::Info,
226            raw_message: "test message".to_string(),
227        }
228    }
229
230    #[test]
231    fn test_parse_sender_notification() {
232        let parser = BounceParser::new();
233        let base_event = create_test_base_event();
234
235        let message = "5FC392A20996: sender non-delivery notification: 732B92A209A3";
236
237        let result = parser.parse_line(message, base_event);
238        assert!(result.is_some());
239
240        if let Some(BounceEvent::SenderNotification {
241            original_queue_id,
242            bounce_queue_id,
243            ..
244        }) = result
245        {
246            assert_eq!(original_queue_id, "5FC392A20996");
247            assert_eq!(bounce_queue_id, "732B92A209A3");
248        } else {
249            panic!("解析结果类型不正确");
250        }
251    }
252
253    #[test]
254    fn test_parse_postmaster_notification() {
255        let parser = BounceParser::new();
256        let base_event = create_test_base_event();
257
258        let message = "633F488423: postmaster non-delivery notification: 6DFA788422";
259
260        let result = parser.parse_line(message, base_event);
261        assert!(result.is_some());
262
263        if let Some(BounceEvent::PostmasterNotification {
264            original_queue_id,
265            bounce_queue_id,
266            ..
267        }) = result
268        {
269            assert_eq!(original_queue_id, "633F488423");
270            assert_eq!(bounce_queue_id, "6DFA788422");
271        } else {
272            panic!("解析结果类型不正确");
273        }
274    }
275
276    #[test]
277    fn test_parse_malformed_warning() {
278        let parser = BounceParser::new();
279        let base_event = create_test_base_event();
280
281        // 注意:MasterParser已剥离"warning:"前缀
282        let message = "malformed request";
283
284        let result = parser.parse_line(message, base_event);
285        assert!(result.is_some());
286
287        if let Some(BounceEvent::Warning {
288            warning_type,
289            details,
290            ..
291        }) = result
292        {
293            assert!(matches!(warning_type, BounceWarningType::MalformedRequest));
294            assert_eq!(details, "malformed request");
295        } else {
296            panic!("解析结果类型不正确");
297        }
298    }
299
300    #[test]
301    fn test_parse_general_warning() {
302        let parser = BounceParser::new();
303        let base_event = create_test_base_event();
304
305        // 注意:MasterParser已剥离"warning:"前缀
306        let message = "configuration file error";
307
308        let result = parser.parse_line(message, base_event);
309        assert!(result.is_some());
310
311        if let Some(BounceEvent::Warning {
312            warning_type,
313            details,
314            ..
315        }) = result
316        {
317            assert!(matches!(warning_type, BounceWarningType::Configuration));
318            assert_eq!(details, "configuration file error");
319        } else {
320            panic!("解析结果类型不正确");
321        }
322    }
323
324    #[test]
325    fn test_can_parse() {
326        let parser = BounceParser::new();
327
328        // 应该能解析的消息
329        assert!(parser.can_parse("5FC392A20996: sender non-delivery notification: 732B92A209A3"));
330        assert!(parser.can_parse("633F488423: postmaster non-delivery notification: 6DFA788422"));
331        // 注意:MasterParser已剥离"warning:"前缀
332        assert!(parser.can_parse("malformed request"));
333        assert!(parser.can_parse("configuration error"));
334
335        // 不应该解析的消息
336        assert!(!parser.can_parse("some random message"));
337        assert!(!parser.can_parse("info: normal operation"));
338        assert!(!parser.can_parse("5FC392A20996: to=<user@domain.com>, status=sent"));
339    }
340
341    #[test]
342    fn test_component_parser_interface() {
343        let parser = BounceParser::new();
344
345        // 测试component_name
346        assert_eq!(parser.component_name(), "bounce");
347
348        // 测试parse方法成功案例
349        let result = parser.parse("5FC392A20996: sender non-delivery notification: 732B92A209A3");
350        assert!(result.is_ok());
351
352        if let Ok(ComponentEvent::Bounce(bounce_event)) = result {
353            match bounce_event {
354                BounceEvent::SenderNotification {
355                    original_queue_id,
356                    bounce_queue_id,
357                    ..
358                } => {
359                    assert_eq!(original_queue_id, "5FC392A20996");
360                    assert_eq!(bounce_queue_id, "732B92A209A3");
361                }
362                _ => panic!("解析结果类型不正确"),
363            }
364        } else {
365            panic!("解析失败");
366        }
367
368        // 测试parse方法失败案例
369        let result = parser.parse("some random text that should not match");
370        assert!(result.is_err());
371
372        if let Err(ParseError::ComponentParseError { component, reason }) = result {
373            assert_eq!(component, "bounce");
374            assert!(reason.contains("无法识别的bounce日志格式"));
375        } else {
376            panic!("错误类型不正确");
377        }
378    }
379}
380