postfix_log_parser/components/
proxymap.rs1use regex::Regex;
2
3use crate::events::proxymap::{ProxymapEvent, ProxymapEventType};
4
5#[derive(Debug)]
7pub struct ProxymapParser {
8 config_override_regex: Regex,
9}
10
11impl ProxymapParser {
12 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 pub fn parse_log_line(&self, line: &str) -> Result<ProxymapEvent, String> {
24 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 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 pub fn supported_event_types(&self) -> usize {
47 1
48 }
49
50 pub fn matches_component(&self, line: &str) -> bool {
52 line.contains("postfix/proxymap[")
53 }
54
55 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 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(), process_id: "0".to_string(), 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 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 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 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"); 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 assert!(parser
365 .can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
366
367 assert!(!parser.can_parse("some random message"));
369 assert!(!parser.can_parse("overriding earlier entry: test=value")); assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
371 }
373
374 #[test]
375 fn test_parse_real_log_samples() {
376 let parser = create_parser();
377
378 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}