postfix_log_parser/components/
sendmail.rs

1use regex::Regex;
2
3use crate::events::sendmail::{SendmailEvent, SendmailEventType};
4
5/// SENDMAIL parser for Sendmail compatibility interface
6#[derive(Debug)]
7pub struct SendmailParser {
8    fatal_usage_regex: Regex, // 用于完整日志行: "fatal: usage: ..."
9    usage_only_regex: Regex,  // 用于组件消息: "usage: ..."
10}
11
12impl SendmailParser {
13    /// Creates a new SendmailParser
14    pub fn new() -> Self {
15        Self {
16            fatal_usage_regex: Regex::new(r"^fatal: (.+)$")
17                .expect("Failed to compile fatal usage regex"),
18            usage_only_regex: Regex::new(r"^usage: (.+)$")
19                .expect("Failed to compile usage only regex"),
20        }
21    }
22
23    /// Parse a complete log line into a SendmailEvent
24    pub fn parse_log_line(&self, line: &str) -> Result<SendmailEvent, String> {
25        // Basic line format: Month Day Time Host postfix/sendmail[PID]: message
26        let basic_regex = Regex::new(
27            r"^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+\S+\s+postfix/sendmail\[(\d+)\]:\s+(.+)$",
28        )
29        .map_err(|e| format!("Regex compilation error: {}", e))?;
30
31        let captures = basic_regex
32            .captures(line)
33            .ok_or_else(|| "Line does not match sendmail log format".to_string())?;
34
35        let timestamp = captures.get(1).unwrap().as_str();
36        let process_id = captures.get(2).unwrap().as_str();
37        let message = captures.get(3).unwrap().as_str();
38
39        // Try to parse as fatal usage error
40        if let Some(event) = self.parse_fatal_usage_error(timestamp, process_id, message) {
41            return Ok(event);
42        }
43
44        Err(format!("Unknown sendmail message type: {}", message))
45    }
46
47    /// Get the number of supported event types
48    pub fn supported_event_types(&self) -> usize {
49        1
50    }
51
52    /// Check if this parser can handle the given line
53    pub fn matches_component(&self, line: &str) -> bool {
54        line.contains("postfix/sendmail[")
55    }
56
57    /// Parse fatal usage error
58    fn parse_fatal_usage_error(
59        &self,
60        timestamp: &str,
61        process_id: &str,
62        message: &str,
63    ) -> Option<SendmailEvent> {
64        if let Some(captures) = self.fatal_usage_regex.captures(message) {
65            let error_message = captures.get(1).unwrap().as_str();
66
67            Some(SendmailEvent {
68                timestamp: timestamp.to_string(),
69                process_id: process_id.to_string(),
70                event_type: SendmailEventType::FatalUsageError {
71                    message: error_message.to_string(),
72                },
73            })
74        } else {
75            None
76        }
77    }
78}
79
80impl Default for SendmailParser {
81    fn default() -> Self {
82        Self::new()
83    }
84}
85
86impl crate::components::ComponentParser for SendmailParser {
87    fn parse(
88        &self,
89        message: &str,
90    ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
91        // Try to parse as fatal usage error (message format: "usage: mailq [options]")
92        if let Some(captures) = self.usage_only_regex.captures(message) {
93            let error_message = captures.get(1).unwrap().as_str();
94
95            let event = SendmailEvent {
96                timestamp: "0".to_string(),  // Temporary timestamp
97                process_id: "0".to_string(), // Temporary process ID
98                event_type: SendmailEventType::FatalUsageError {
99                    message: format!("usage: {}", error_message),
100                },
101            };
102
103            return Ok(crate::events::base::ComponentEvent::Sendmail(event));
104        }
105
106        Err(crate::error::ParseError::ComponentParseError {
107            component: self.component_name().to_string(),
108            reason: format!("Unable to parse sendmail message: {}", message),
109        })
110    }
111
112    fn component_name(&self) -> &'static str {
113        "sendmail"
114    }
115
116    fn can_parse(&self, message: &str) -> bool {
117        self.usage_only_regex.is_match(message)
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::components::ComponentParser;
125
126    fn create_parser() -> SendmailParser {
127        SendmailParser::new()
128    }
129
130    #[test]
131    fn test_fatal_usage_error_parsing() {
132        let parser = create_parser();
133        let log_line = "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]";
134
135        let result = parser.parse_log_line(log_line);
136        assert!(result.is_ok());
137
138        let event = result.unwrap();
139        assert_eq!(event.timestamp, "Apr 24 17:20:55");
140        assert_eq!(event.process_id, "180");
141
142        let SendmailEventType::FatalUsageError { message } = event.event_type;
143        assert_eq!(message, "usage: mailq [options]");
144    }
145
146    #[test]
147    fn test_different_process_ids() {
148        let parser = create_parser();
149        let test_cases = vec!["180", "187", "208", "216", "223", "230"];
150
151        for pid in test_cases {
152            let log_line = format!(
153                "Apr 24 17:20:55 m01 postfix/sendmail[{}]: fatal: usage: mailq [options]",
154                pid
155            );
156
157            let result = parser.parse_log_line(&log_line);
158            assert!(result.is_ok());
159
160            let event = result.unwrap();
161            assert_eq!(event.process_id, pid);
162        }
163    }
164
165    #[test]
166    fn test_different_timestamps() {
167        let parser = create_parser();
168        let test_cases = vec![
169            "Apr 24 17:20:55",
170            "Apr 24 17:20:56",
171            "Apr 24 17:23:16",
172            "Apr 24 17:23:19",
173        ];
174
175        for timestamp in test_cases {
176            let log_line = format!(
177                "{} m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
178                timestamp
179            );
180
181            let result = parser.parse_log_line(&log_line);
182            assert!(result.is_ok());
183
184            let event = result.unwrap();
185            assert_eq!(event.timestamp, timestamp);
186        }
187    }
188
189    #[test]
190    fn test_component_matching() {
191        let parser = create_parser();
192
193        // 应该匹配的行
194        let matching_lines = vec![
195            "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
196            "Apr 24 17:20:56 m01 postfix/sendmail[187]: fatal: some other error",
197        ];
198
199        for line in matching_lines {
200            assert!(parser.matches_component(line), "Should match: {}", line);
201        }
202
203        // 不应该匹配的行
204        let non_matching_lines = vec![
205            "Apr 24 17:20:55 m01 postfix/qmgr[78]: info: statistics",
206            "Apr 24 17:20:55 m01 postfix/smtpd[78]: connect from localhost",
207            "Apr 24 17:20:55 m01 postfix/cleanup[78]: message-id=<test@example.com>",
208        ];
209
210        for line in non_matching_lines {
211            assert!(
212                !parser.matches_component(line),
213                "Should not match: {}",
214                line
215            );
216        }
217    }
218
219    #[test]
220    fn test_invalid_log_lines() {
221        let parser = create_parser();
222
223        let invalid_lines = vec![
224            "Invalid log line",
225            "Apr 24 17:20:55 m01 postfix/qmgr[78]: info: statistics",
226            "Apr 24 17:20:55 m01 postfix/sendmail[180]: info: some info message",
227            "incomplete line",
228        ];
229
230        for line in invalid_lines {
231            let result = parser.parse_log_line(line);
232            assert!(result.is_err(), "Should fail to parse: {}", line);
233        }
234    }
235
236    #[test]
237    fn test_supported_event_types() {
238        let parser = create_parser();
239        assert_eq!(parser.supported_event_types(), 1);
240    }
241
242    #[test]
243    fn test_parser_default() {
244        let parser = SendmailParser::default();
245        assert_eq!(parser.supported_event_types(), 1);
246    }
247
248    #[test]
249    fn test_component_parser_parse() {
250        let parser = SendmailParser::new();
251
252        // 测试fatal usage错误 (主解析器传递的消息格式: "usage: mailq [options]")
253        let message = "usage: mailq [options]";
254        let result = parser.parse(message);
255
256        assert!(result.is_ok());
257        match result.unwrap() {
258            crate::events::base::ComponentEvent::Sendmail(event) => {
259                assert_eq!(event.process_id, "0"); // 临时进程ID
260                let SendmailEventType::FatalUsageError { message } = event.event_type;
261                assert_eq!(message, "usage: mailq [options]");
262            }
263            _ => panic!("Expected Sendmail ComponentEvent"),
264        }
265    }
266
267    #[test]
268    fn test_component_parser_invalid() {
269        let parser = SendmailParser::new();
270
271        let message = "some invalid message";
272        let result = parser.parse(message);
273
274        assert!(result.is_err());
275        match result.unwrap_err() {
276            crate::error::ParseError::ComponentParseError { component, .. } => {
277                assert_eq!(component, "sendmail");
278            }
279            _ => panic!("Expected ComponentParseError"),
280        }
281    }
282
283    #[test]
284    fn test_component_name() {
285        let parser = SendmailParser::new();
286        assert_eq!(parser.component_name(), "sendmail");
287    }
288
289    #[test]
290    fn test_can_parse() {
291        let parser = SendmailParser::new();
292
293        // 应该能解析的消息 (主解析器传递的格式,已去掉fatal前缀)
294        assert!(parser.can_parse("usage: mailq [options]"));
295        assert!(parser.can_parse("usage: some other command"));
296
297        // 不应该解析的消息
298        assert!(!parser.can_parse("some random message"));
299        assert!(!parser.can_parse("info: some info message"));
300        assert!(!parser.can_parse("warning: some warning"));
301        assert!(!parser.can_parse("fatal: some other error")); // fatal前缀已被主解析器处理
302    }
303
304    #[test]
305    fn test_parse_real_log_samples() {
306        let parser = create_parser();
307
308        // 真实日志样本
309        let real_logs = vec![
310            "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
311            "Apr 24 17:20:56 m01 postfix/sendmail[187]: fatal: usage: mailq [options]",
312            "Apr 24 17:23:16 m01 postfix/sendmail[208]: fatal: usage: mailq [options]",
313            "Apr 24 17:23:19 m01 postfix/sendmail[216]: fatal: usage: mailq [options]",
314            "Apr 24 17:23:22 m01 postfix/sendmail[223]: fatal: usage: mailq [options]",
315            "Apr 24 17:23:24 m01 postfix/sendmail[230]: fatal: usage: mailq [options]",
316        ];
317
318        for (i, log_line) in real_logs.iter().enumerate() {
319            let result = parser.parse_log_line(log_line);
320            assert!(
321                result.is_ok(),
322                "Failed to parse real log sample {}: {}",
323                i,
324                log_line
325            );
326
327            let event = result.unwrap();
328            let SendmailEventType::FatalUsageError { message } = event.event_type;
329            assert_eq!(message, "usage: mailq [options]");
330        }
331    }
332}