postfix_log_parser/components/
sendmail.rs

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