postfix_log_parser/components/
bounce.rs

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