postfix_log_parser/components/
postlogd.rs

1use crate::events::postlogd::{PostlogdEvent, PostlogdEventType};
2use regex::Regex;
3use std::sync::LazyLock;
4
5/// POSTLOGD组件解析器
6/// 处理Postfix内部日志服务器相关事件
7pub struct PostlogdParser;
8
9/// 配置覆盖警告正则表达式
10/// 格式: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=...
11static CONFIG_OVERRIDE_WARNING_RE: LazyLock<Regex> = LazyLock::new(|| {
12    Regex::new(r"^(\S+ \d+ \d+:\d+:\d+) \S+ postfix/postlogd\[(\d+)\]: warning: (.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$")
13        .expect("Invalid CONFIG_OVERRIDE_WARNING_RE regex")
14});
15
16impl PostlogdParser {
17    /// 创建新的POSTLOGD解析器实例
18    pub fn new() -> Self {
19        Self
20    }
21
22    /// 解析POSTLOGD日志行
23    pub fn parse_log_line(&self, line: &str) -> Result<PostlogdEvent, String> {
24        // 处理配置覆盖警告
25        if let Some(caps) = CONFIG_OVERRIDE_WARNING_RE.captures(line) {
26            let timestamp = caps.get(1).unwrap().as_str().to_string();
27            let process_id = caps.get(2).unwrap().as_str().to_string();
28            let file_path = caps.get(3).unwrap().as_str().to_string();
29            let line_number = caps
30                .get(4)
31                .unwrap()
32                .as_str()
33                .parse::<u32>()
34                .map_err(|_| "Failed to parse line number")?;
35            let parameter = caps.get(5).unwrap().as_str().to_string();
36            let value = caps.get(6).unwrap().as_str().to_string();
37
38            return Ok(PostlogdEvent {
39                timestamp,
40                process_id,
41                event_type: PostlogdEventType::ConfigOverrideWarning {
42                    file_path,
43                    line_number,
44                    parameter,
45                    value,
46                },
47            });
48        }
49
50        Err(format!("Failed to parse postlogd log line: {}", line))
51    }
52
53    /// 获取支持的事件类型数量
54    pub fn supported_event_types(&self) -> usize {
55        1 // ConfigOverrideWarning
56    }
57
58    /// 检查日志行是否属于POSTLOGD组件
59    pub fn matches_component(&self, line: &str) -> bool {
60        line.contains("postfix/postlogd[")
61    }
62
63    /// 解析配置覆盖警告(用于ComponentParser接口)
64    /// 输入是已移除"warning:"前缀的消息内容
65    fn parse_config_override_warning(&self, message: &str) -> Option<PostlogdEvent> {
66        use regex::Regex;
67        use std::sync::LazyLock;
68
69        // 配置覆盖警告正则表达式:/path/to/file, line N: overriding earlier entry: param=value
70        static CONFIG_WARNING_RE: LazyLock<Regex> = LazyLock::new(|| {
71            Regex::new(r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$")
72                .expect("Invalid CONFIG_WARNING_RE regex")
73        });
74
75        if let Some(caps) = CONFIG_WARNING_RE.captures(message.trim()) {
76            let file_path = caps.get(1)?.as_str().to_string();
77            let line_number = caps.get(2)?.as_str().parse::<u32>().ok()?;
78            let parameter = caps.get(3)?.as_str().to_string();
79            let value = caps.get(4)?.as_str().to_string();
80
81            return Some(PostlogdEvent {
82                timestamp: chrono::Utc::now().format("%b %d %H:%M:%S").to_string(),
83                process_id: "0".to_string(), // 会在主解析器中正确设置
84                event_type: PostlogdEventType::ConfigOverrideWarning {
85                    file_path,
86                    line_number,
87                    parameter,
88                    value,
89                },
90            });
91        }
92        None
93    }
94}
95
96impl Default for PostlogdParser {
97    fn default() -> Self {
98        Self::new()
99    }
100}
101
102impl crate::components::ComponentParser for PostlogdParser {
103    fn parse(
104        &self,
105        message: &str,
106    ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
107        let clean_message = message.trim();
108
109        // 尝试解析配置覆盖警告
110        if let Some(event) = self.parse_config_override_warning(clean_message) {
111            return Ok(crate::events::base::ComponentEvent::Postlogd(event));
112        }
113
114        Err(crate::error::ParseError::ComponentParseError {
115            component: "postlogd".to_string(),
116            reason: format!("Unable to parse message: {}", message),
117        })
118    }
119
120    fn component_name(&self) -> &'static str {
121        "postlogd"
122    }
123
124    fn can_parse(&self, message: &str) -> bool {
125        message.contains("overriding earlier entry:") && message.contains("main.cf")
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132    use crate::components::ComponentParser;
133
134    fn create_parser() -> PostlogdParser {
135        PostlogdParser::new()
136    }
137
138    #[test]
139    fn test_config_override_warning_parsing() {
140        let parser = create_parser();
141        let log_line = "Apr 08 17:54:30 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted,                                                                permit_sasl_authenticated,                                                                permit_mynetworks,                                                                reject_unauth_destination,                                                                pcre:/etc/postfix/filter_default";
142
143        let result = parser.parse_log_line(log_line);
144        assert!(result.is_ok());
145
146        let event = result.unwrap();
147        assert_eq!(event.timestamp, "Apr 08 17:54:30");
148        assert_eq!(event.process_id, "78");
149
150        let PostlogdEventType::ConfigOverrideWarning {
151            file_path,
152            line_number,
153            parameter,
154            value,
155        } = event.event_type;
156
157        assert_eq!(file_path, "/etc/postfix/main.cf");
158        assert_eq!(line_number, 820);
159        assert_eq!(parameter, "smtpd_recipient_restrictions");
160        assert!(value.contains("check_client_access"));
161        assert!(value.contains("permit_sasl_authenticated"));
162    }
163
164    #[test]
165    fn test_client_message_rate_limit_parsing() {
166        let parser = create_parser();
167        let log_line = "Apr 10 11:17:10 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0";
168
169        let result = parser.parse_log_line(log_line);
170        assert!(result.is_ok());
171
172        let event = result.unwrap();
173        assert_eq!(event.timestamp, "Apr 10 11:17:10");
174        assert_eq!(event.process_id, "78");
175
176        let PostlogdEventType::ConfigOverrideWarning {
177            file_path,
178            line_number,
179            parameter,
180            value,
181        } = event.event_type;
182
183        assert_eq!(file_path, "/etc/postfix/main.cf");
184        assert_eq!(line_number, 806);
185        assert_eq!(parameter, "smtpd_client_message_rate_limit");
186        assert_eq!(value, "0");
187    }
188
189    #[test]
190    fn test_discard_ehlo_keywords_parsing() {
191        let parser = create_parser();
192        let log_line = "Apr 10 11:17:10 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn";
193
194        let result = parser.parse_log_line(log_line);
195        assert!(result.is_ok());
196
197        let event = result.unwrap();
198        let PostlogdEventType::ConfigOverrideWarning {
199            parameter, value, ..
200        } = event.event_type;
201
202        assert_eq!(parameter, "smtpd_discard_ehlo_keywords");
203        assert_eq!(value, "silent-discard,dsn,etrn");
204    }
205
206    #[test]
207    fn test_different_line_numbers() {
208        let parser = create_parser();
209        let test_cases = vec![
210            ("line 806", 806),
211            ("line 818", 818),
212            ("line 819", 819),
213            ("line 820", 820),
214            ("line 822", 822),
215            ("line 826", 826),
216        ];
217
218        for (line_text, expected_line) in test_cases {
219            let log_line = format!("Apr 10 11:19:32 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, {}: overriding earlier entry: test_param=test_value", line_text);
220
221            let result = parser.parse_log_line(&log_line);
222            assert!(result.is_ok());
223
224            let event = result.unwrap();
225            let PostlogdEventType::ConfigOverrideWarning { line_number, .. } = event.event_type;
226            assert_eq!(line_number, expected_line);
227        }
228    }
229
230    #[test]
231    fn test_different_process_ids() {
232        let parser = create_parser();
233        let test_cases = vec!["78", "83"];
234
235        for pid in test_cases {
236            let log_line = format!("Apr 08 17:58:29 m01 postfix/postlogd[{}]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test_param=test_value", pid);
237
238            let result = parser.parse_log_line(&log_line);
239            assert!(result.is_ok());
240
241            let event = result.unwrap();
242            assert_eq!(event.process_id, pid);
243        }
244    }
245
246    #[test]
247    fn test_complex_parameter_values() {
248        let parser = create_parser();
249        let log_line = "Apr 10 11:45:49 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 815: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted,                                                                permit_sasl_authenticated,                                                                permit_mynetworks,                                                                reject_unauth_destination,                                                                pcre:/etc/postfix/filter_default,                                                                check_client_access hash:/etc/postfix/access,                                                                check_recipient_access hash:/etc/postfix/recipient_access";
250
251        let result = parser.parse_log_line(log_line);
252        assert!(result.is_ok());
253
254        let event = result.unwrap();
255        let PostlogdEventType::ConfigOverrideWarning { value, .. } = event.event_type;
256        assert!(value.contains("check_client_access pcre:/etc/postfix/filter_trusted"));
257        assert!(value.contains("check_client_access hash:/etc/postfix/access"));
258        assert!(value.contains("check_recipient_access hash:/etc/postfix/recipient_access"));
259    }
260
261    #[test]
262    fn test_component_matching() {
263        let parser = create_parser();
264
265        // 应该匹配的行
266        let matching_lines = vec![
267            "Apr 08 17:54:30 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test=value",
268            "Apr 10 11:17:10 m01 postfix/postlogd[83]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: another=param",
269        ];
270
271        for line in matching_lines {
272            assert!(parser.matches_component(line), "Should match: {}", line);
273        }
274
275        // 不应该匹配的行
276        let non_matching_lines = vec![
277            "Apr 08 17:54:30 m01 postfix/qmgr[78]: info: statistics",
278            "Apr 08 17:54:30 m01 postfix/smtpd[78]: connect from localhost",
279            "Apr 08 17:54:30 m01 postfix/cleanup[78]: message-id=<test@example.com>",
280        ];
281
282        for line in non_matching_lines {
283            assert!(
284                !parser.matches_component(line),
285                "Should not match: {}",
286                line
287            );
288        }
289    }
290
291    #[test]
292    fn test_invalid_log_lines() {
293        let parser = create_parser();
294
295        let invalid_lines = vec![
296            "Invalid log line",
297            "Apr 08 17:54:30 m01 postfix/qmgr[78]: info: statistics",
298            "Apr 08 17:54:30 m01 postfix/postlogd[78]: some other message",
299            "incomplete line",
300        ];
301
302        for line in invalid_lines {
303            let result = parser.parse_log_line(line);
304            assert!(result.is_err(), "Should fail to parse: {}", line);
305        }
306    }
307
308    #[test]
309    fn test_supported_event_types() {
310        let parser = create_parser();
311        assert_eq!(parser.supported_event_types(), 1);
312    }
313
314    #[test]
315    fn test_parser_default() {
316        let parser = PostlogdParser::default();
317        assert_eq!(parser.supported_event_types(), 1);
318    }
319
320    #[test]
321    fn test_component_parser_parse() {
322        let parser = PostlogdParser::new();
323
324        // 测试配置覆盖警告
325        let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted";
326        let result = parser.parse(message);
327
328        assert!(result.is_ok());
329        match result.unwrap() {
330            crate::events::base::ComponentEvent::Postlogd(event) => {
331                assert_eq!(event.process_id, "0"); // 临时进程ID
332                let PostlogdEventType::ConfigOverrideWarning {
333                    file_path,
334                    line_number,
335                    parameter,
336                    ..
337                } = event.event_type;
338
339                assert_eq!(file_path, "/etc/postfix/main.cf");
340                assert_eq!(line_number, 820);
341                assert_eq!(parameter, "smtpd_recipient_restrictions");
342            }
343            _ => panic!("Expected Postlogd ComponentEvent"),
344        }
345    }
346
347    #[test]
348    fn test_component_parser_invalid() {
349        let parser = PostlogdParser::new();
350
351        let message = "some invalid message";
352        let result = parser.parse(message);
353
354        assert!(result.is_err());
355        match result.unwrap_err() {
356            crate::error::ParseError::ComponentParseError { component, .. } => {
357                assert_eq!(component, "postlogd");
358            }
359            _ => panic!("Expected ComponentParseError"),
360        }
361    }
362
363    #[test]
364    fn test_component_name() {
365        let parser = PostlogdParser::new();
366        assert_eq!(parser.component_name(), "postlogd");
367    }
368
369    #[test]
370    fn test_can_parse() {
371        let parser = PostlogdParser::new();
372
373        // 应该能解析的消息
374        assert!(parser
375            .can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
376
377        // 不应该解析的消息
378        assert!(!parser.can_parse("some random message"));
379        assert!(!parser.can_parse("overriding earlier entry: test=value")); // 缺少main.cf
380        assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
381        // 缺少overriding
382    }
383}