postfix_log_parser/components/
bounce.rs1use regex::Regex;
2
3use crate::error::ParseError;
4use crate::events::bounce::{BounceEvent, BounceWarningType};
5use crate::events::{base::BaseEvent, ComponentEvent};
6
7use super::ComponentParser;
8
9pub struct BounceParser {
18 sender_notification_regex: Regex,
21
22 postmaster_notification_regex: Regex,
25
26 warning_malformed_regex: Regex,
29
30 warning_general_regex: Regex,
33}
34
35impl BounceParser {
36 pub fn new() -> Self {
37 Self {
38 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 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 warning_malformed_regex: Regex::new(r"^malformed request$")
52 .expect("BOUNCE格式错误警告正则表达式编译失败"),
53
54 warning_general_regex: Regex::new(r"^(.+)$").expect("BOUNCE一般警告正则表达式编译失败"),
56 }
57 }
58
59 pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<BounceEvent> {
61 if let Some(captures) = self.sender_notification_regex.captures(line) {
63 return self.parse_sender_notification(captures, base_event);
64 }
65
66 if let Some(captures) = self.postmaster_notification_regex.captures(line) {
68 return self.parse_postmaster_notification(captures, base_event);
69 }
70
71 if self.warning_malformed_regex.is_match(line) {
73 return self.parse_malformed_warning(base_event);
74 }
75
76 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 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 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 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 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 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 fn is_bounce_warning(&self, message: &str) -> bool {
162 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 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 self.sender_notification_regex.is_match(message)
204 || self.postmaster_notification_regex.is_match(message)
205 || self.warning_malformed_regex.is_match(message)
206 || 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 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 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 assert!(parser.can_parse("5FC392A20996: sender non-delivery notification: 732B92A209A3"));
330 assert!(parser.can_parse("633F488423: postmaster non-delivery notification: 6DFA788422"));
331 assert!(parser.can_parse("malformed request"));
333 assert!(parser.can_parse("configuration error"));
334
335 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 assert_eq!(parser.component_name(), "bounce");
347
348 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 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