postfix_log_parser/components/
bounce.rs1use regex::Regex;
29
30use crate::error::ParseError;
31use crate::events::bounce::{BounceEvent, BounceWarningType};
32use crate::events::{base::BaseEvent, ComponentEvent};
33
34use super::ComponentParser;
35
36pub struct BounceParser {
45 sender_notification_regex: Regex,
48
49 postmaster_notification_regex: Regex,
52
53 warning_malformed_regex: Regex,
56
57 warning_general_regex: Regex,
60}
61
62impl BounceParser {
63 pub fn new() -> Self {
64 Self {
65 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 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 warning_malformed_regex: Regex::new(r"^malformed request$")
79 .expect("BOUNCE格式错误警告正则表达式编译失败"),
80
81 warning_general_regex: Regex::new(r"^(.+)$").expect("BOUNCE一般警告正则表达式编译失败"),
83 }
84 }
85
86 pub fn parse_line(&self, line: &str, base_event: BaseEvent) -> Option<BounceEvent> {
88 if let Some(captures) = self.sender_notification_regex.captures(line) {
90 return self.parse_sender_notification(captures, base_event);
91 }
92
93 if let Some(captures) = self.postmaster_notification_regex.captures(line) {
95 return self.parse_postmaster_notification(captures, base_event);
96 }
97
98 if self.warning_malformed_regex.is_match(line) {
100 return self.parse_malformed_warning(base_event);
101 }
102
103 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 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 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 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 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 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 fn is_bounce_warning(&self, message: &str) -> bool {
186 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 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 self.sender_notification_regex.is_match(message)
228 || self.postmaster_notification_regex.is_match(message)
229 || self.warning_malformed_regex.is_match(message)
230 || 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 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 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 assert!(parser.can_parse("5FC392A20996: sender non-delivery notification: 732B92A209A3"));
360 assert!(parser.can_parse("633F488423: postmaster non-delivery notification: 6DFA788422"));
361 assert!(parser.can_parse("malformed request"));
363 assert!(parser.can_parse("configuration error"));
364
365 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 assert_eq!(parser.component_name(), "bounce");
377
378 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 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}