postfix_log_parser/components/
proxymap.rs

1use regex::Regex;
2
3use crate::events::proxymap::{ProxymapEvent, ProxymapEventType};
4
5/// PROXYMAP parser for Postfix proxy mapping service
6#[derive(Debug)]
7pub struct ProxymapParser {
8    config_override_regex: Regex,
9}
10
11impl ProxymapParser {
12    /// Creates a new ProxymapParser
13    pub fn new() -> Self {
14        Self {
15            config_override_regex: Regex::new(
16                r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$",
17            )
18            .expect("Failed to compile config override regex"),
19        }
20    }
21
22    /// Parse a complete log line into a ProxymapEvent
23    pub fn parse_log_line(&self, line: &str) -> Result<ProxymapEvent, String> {
24        // Basic line format: Month Day Time Host postfix/proxymap[PID]: warning: message
25        let basic_regex = Regex::new(
26            r"^(\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2})\s+\S+\s+postfix/proxymap\[(\d+)\]:\s+warning:\s+(.+)$"
27        ).map_err(|e| format!("Regex compilation error: {}", e))?;
28
29        let captures = basic_regex
30            .captures(line)
31            .ok_or_else(|| "Line does not match proxymap log format".to_string())?;
32
33        let timestamp = captures.get(1).unwrap().as_str();
34        let process_id = captures.get(2).unwrap().as_str();
35        let message = captures.get(3).unwrap().as_str();
36
37        // Try to parse as config override warning
38        if let Some(event) = self.parse_config_override_warning(timestamp, process_id, message) {
39            return Ok(event);
40        }
41
42        Err(format!("Unknown proxymap message type: {}", message))
43    }
44
45    /// Get the number of supported event types
46    pub fn supported_event_types(&self) -> usize {
47        1
48    }
49
50    /// Check if this parser can handle the given line
51    pub fn matches_component(&self, line: &str) -> bool {
52        line.contains("postfix/proxymap[")
53    }
54
55    /// Parse config override warning
56    fn parse_config_override_warning(
57        &self,
58        timestamp: &str,
59        process_id: &str,
60        message: &str,
61    ) -> Option<ProxymapEvent> {
62        if let Some(captures) = self.config_override_regex.captures(message) {
63            let file_path = captures.get(1).unwrap().as_str();
64            let line_number: u32 = captures.get(2).unwrap().as_str().parse().ok()?;
65            let parameter = captures.get(3).unwrap().as_str();
66            let value = captures.get(4).unwrap().as_str();
67
68            Some(ProxymapEvent {
69                timestamp: timestamp.to_string(),
70                process_id: process_id.to_string(),
71                event_type: ProxymapEventType::ConfigOverrideWarning {
72                    file_path: file_path.to_string(),
73                    line_number,
74                    parameter: parameter.to_string(),
75                    value: value.to_string(),
76                },
77            })
78        } else {
79            None
80        }
81    }
82}
83
84impl Default for ProxymapParser {
85    fn default() -> Self {
86        Self::new()
87    }
88}
89
90impl crate::components::ComponentParser for ProxymapParser {
91    fn parse(
92        &self,
93        message: &str,
94    ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
95        // Try to parse as config override warning
96        if let Some(captures) = self.config_override_regex.captures(message) {
97            let file_path = captures.get(1).unwrap().as_str();
98            let line_number: u32 = captures.get(2).unwrap().as_str().parse().map_err(|_| {
99                crate::error::ParseError::ComponentParseError {
100                    component: self.component_name().to_string(),
101                    reason: "Invalid line number format".to_string(),
102                }
103            })?;
104            let parameter = captures.get(3).unwrap().as_str();
105            let value = captures.get(4).unwrap().as_str();
106
107            let event = ProxymapEvent {
108                timestamp: "0".to_string(),  // Temporary timestamp
109                process_id: "0".to_string(), // Temporary process ID
110                event_type: ProxymapEventType::ConfigOverrideWarning {
111                    file_path: file_path.to_string(),
112                    line_number,
113                    parameter: parameter.to_string(),
114                    value: value.to_string(),
115                },
116            };
117
118            return Ok(crate::events::base::ComponentEvent::Proxymap(event));
119        }
120
121        Err(crate::error::ParseError::ComponentParseError {
122            component: self.component_name().to_string(),
123            reason: format!("Unable to parse proxymap message: {}", message),
124        })
125    }
126
127    fn component_name(&self) -> &'static str {
128        "proxymap"
129    }
130
131    fn can_parse(&self, message: &str) -> bool {
132        self.config_override_regex.is_match(message)
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use super::*;
139    use crate::components::ComponentParser;
140
141    fn create_parser() -> ProxymapParser {
142        ProxymapParser::new()
143    }
144
145    #[test]
146    fn test_config_override_warning_parsing() {
147        let parser = create_parser();
148        let log_line = "Apr 08 17:54:42 m01 postfix/proxymap[80]: 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";
149
150        let result = parser.parse_log_line(log_line);
151        assert!(result.is_ok());
152
153        let event = result.unwrap();
154        assert_eq!(event.timestamp, "Apr 08 17:54:42");
155        assert_eq!(event.process_id, "80");
156
157        let ProxymapEventType::ConfigOverrideWarning {
158            file_path,
159            line_number,
160            parameter,
161            value,
162        } = event.event_type;
163
164        assert_eq!(file_path, "/etc/postfix/main.cf");
165        assert_eq!(line_number, 820);
166        assert_eq!(parameter, "smtpd_recipient_restrictions");
167        assert!(value.contains("check_client_access"));
168        assert!(value.contains("permit_sasl_authenticated"));
169    }
170
171    #[test]
172    fn test_client_message_rate_limit_parsing() {
173        let parser = create_parser();
174        let log_line = "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0";
175
176        let result = parser.parse_log_line(log_line);
177        assert!(result.is_ok());
178
179        let event = result.unwrap();
180        assert_eq!(event.timestamp, "Apr 10 11:17:43");
181        assert_eq!(event.process_id, "80");
182
183        let ProxymapEventType::ConfigOverrideWarning {
184            file_path,
185            line_number,
186            parameter,
187            value,
188        } = event.event_type;
189
190        assert_eq!(file_path, "/etc/postfix/main.cf");
191        assert_eq!(line_number, 806);
192        assert_eq!(parameter, "smtpd_client_message_rate_limit");
193        assert_eq!(value, "0");
194    }
195
196    #[test]
197    fn test_discard_ehlo_keywords_parsing() {
198        let parser = create_parser();
199        let log_line = "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn";
200
201        let result = parser.parse_log_line(log_line);
202        assert!(result.is_ok());
203
204        let event = result.unwrap();
205        let ProxymapEventType::ConfigOverrideWarning {
206            parameter, value, ..
207        } = event.event_type;
208
209        assert_eq!(parameter, "smtpd_discard_ehlo_keywords");
210        assert_eq!(value, "silent-discard,dsn,etrn");
211    }
212
213    #[test]
214    fn test_different_line_numbers() {
215        let parser = create_parser();
216        let test_cases = vec![
217            ("line 806", 806),
218            ("line 819", 819),
219            ("line 820", 820),
220            ("line 826", 826),
221        ];
222
223        for (line_text, expected_line) in test_cases {
224            let log_line = format!("Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, {}: overriding earlier entry: test_param=test_value", line_text);
225
226            let result = parser.parse_log_line(&log_line);
227            assert!(result.is_ok());
228
229            let event = result.unwrap();
230            let ProxymapEventType::ConfigOverrideWarning { line_number, .. } = event.event_type;
231            assert_eq!(line_number, expected_line);
232        }
233    }
234
235    #[test]
236    fn test_different_process_ids() {
237        let parser = create_parser();
238        let test_cases = vec!["80", "84"];
239
240        for pid in test_cases {
241            let log_line = format!("Apr 08 17:54:42 m01 postfix/proxymap[{}]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test_param=test_value", pid);
242
243            let result = parser.parse_log_line(&log_line);
244            assert!(result.is_ok());
245
246            let event = result.unwrap();
247            assert_eq!(event.process_id, pid);
248        }
249    }
250
251    #[test]
252    fn test_component_matching() {
253        let parser = create_parser();
254
255        // 应该匹配的行
256        let matching_lines = vec![
257            "Apr 08 17:54:42 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test=value",
258            "Apr 10 11:17:43 m01 postfix/proxymap[84]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: another=param",
259        ];
260
261        for line in matching_lines {
262            assert!(parser.matches_component(line), "Should match: {}", line);
263        }
264
265        // 不应该匹配的行
266        let non_matching_lines = vec![
267            "Apr 08 17:54:42 m01 postfix/qmgr[78]: info: statistics",
268            "Apr 08 17:54:42 m01 postfix/smtpd[78]: connect from localhost",
269            "Apr 08 17:54:42 m01 postfix/cleanup[78]: message-id=<test@example.com>",
270        ];
271
272        for line in non_matching_lines {
273            assert!(
274                !parser.matches_component(line),
275                "Should not match: {}",
276                line
277            );
278        }
279    }
280
281    #[test]
282    fn test_invalid_log_lines() {
283        let parser = create_parser();
284
285        let invalid_lines = vec![
286            "Invalid log line",
287            "Apr 08 17:54:42 m01 postfix/qmgr[78]: info: statistics",
288            "Apr 08 17:54:42 m01 postfix/proxymap[80]: info: some other message",
289            "incomplete line",
290        ];
291
292        for line in invalid_lines {
293            let result = parser.parse_log_line(line);
294            assert!(result.is_err(), "Should fail to parse: {}", line);
295        }
296    }
297
298    #[test]
299    fn test_supported_event_types() {
300        let parser = create_parser();
301        assert_eq!(parser.supported_event_types(), 1);
302    }
303
304    #[test]
305    fn test_parser_default() {
306        let parser = ProxymapParser::default();
307        assert_eq!(parser.supported_event_types(), 1);
308    }
309
310    #[test]
311    fn test_component_parser_parse() {
312        let parser = ProxymapParser::new();
313
314        // 测试配置覆盖警告
315        let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted";
316        let result = parser.parse(message);
317
318        assert!(result.is_ok());
319        match result.unwrap() {
320            crate::events::base::ComponentEvent::Proxymap(event) => {
321                assert_eq!(event.process_id, "0"); // 临时进程ID
322                let ProxymapEventType::ConfigOverrideWarning {
323                    file_path,
324                    line_number,
325                    parameter,
326                    ..
327                } = event.event_type;
328
329                assert_eq!(file_path, "/etc/postfix/main.cf");
330                assert_eq!(line_number, 820);
331                assert_eq!(parameter, "smtpd_recipient_restrictions");
332            }
333            _ => panic!("Expected Proxymap ComponentEvent"),
334        }
335    }
336
337    #[test]
338    fn test_component_parser_invalid() {
339        let parser = ProxymapParser::new();
340
341        let message = "some invalid message";
342        let result = parser.parse(message);
343
344        assert!(result.is_err());
345        match result.unwrap_err() {
346            crate::error::ParseError::ComponentParseError { component, .. } => {
347                assert_eq!(component, "proxymap");
348            }
349            _ => panic!("Expected ComponentParseError"),
350        }
351    }
352
353    #[test]
354    fn test_component_name() {
355        let parser = ProxymapParser::new();
356        assert_eq!(parser.component_name(), "proxymap");
357    }
358
359    #[test]
360    fn test_can_parse() {
361        let parser = ProxymapParser::new();
362
363        // 应该能解析的消息
364        assert!(parser
365            .can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
366
367        // 不应该解析的消息
368        assert!(!parser.can_parse("some random message"));
369        assert!(!parser.can_parse("overriding earlier entry: test=value")); // 缺少main.cf
370        assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
371        // 缺少overriding
372    }
373
374    #[test]
375    fn test_parse_real_log_samples() {
376        let parser = create_parser();
377
378        // 真实日志样本
379        let real_logs = vec![
380            "Apr 08 17:54:42 m01 postfix/proxymap[80]: 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",
381            "Apr 08 17:58:29 m01 postfix/proxymap[84]: 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",
382            "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0",
383            "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 819: 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",
384            "Apr 10 11:17:43 m01 postfix/proxymap[80]: warning: /etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn",
385        ];
386
387        for (i, log_line) in real_logs.iter().enumerate() {
388            let result = parser.parse_log_line(log_line);
389            assert!(
390                result.is_ok(),
391                "Failed to parse real log sample {}: {}",
392                i,
393                log_line
394            );
395
396            let event = result.unwrap();
397            let ProxymapEventType::ConfigOverrideWarning { file_path, .. } = event.event_type;
398            assert_eq!(file_path, "/etc/postfix/main.cf");
399        }
400    }
401}