postfix_log_parser/components/
postlogd.rs1use crate::events::postlogd::{PostlogdEvent, PostlogdEventType};
29use regex::Regex;
30use std::sync::LazyLock;
31
32pub struct PostlogdParser;
35
36static CONFIG_OVERRIDE_WARNING_RE: LazyLock<Regex> = LazyLock::new(|| {
39 Regex::new(r"^((?:\d{4}\s+)?\S+ \d+ \d+:\d+:\d+(?:\.\d+)?) \S+ postfix/postlogd\[(\d+)\]: warning: (.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$")
40 .expect("Invalid CONFIG_OVERRIDE_WARNING_RE regex")
41});
42
43impl PostlogdParser {
44 pub fn new() -> Self {
46 Self
47 }
48
49 pub fn parse_log_line(&self, line: &str) -> Result<PostlogdEvent, String> {
51 if let Some(caps) = CONFIG_OVERRIDE_WARNING_RE.captures(line) {
53 let timestamp = caps.get(1).unwrap().as_str().to_string();
54 let process_id = caps.get(2).unwrap().as_str().to_string();
55 let file_path = caps.get(3).unwrap().as_str().to_string();
56 let line_number = caps
57 .get(4)
58 .unwrap()
59 .as_str()
60 .parse::<u32>()
61 .map_err(|_| "Failed to parse line number")?;
62 let parameter = caps.get(5).unwrap().as_str().to_string();
63 let value = caps.get(6).unwrap().as_str().to_string();
64
65 return Ok(PostlogdEvent {
66 timestamp,
67 process_id,
68 event_type: PostlogdEventType::ConfigOverrideWarning {
69 file_path,
70 line_number,
71 parameter,
72 value,
73 },
74 });
75 }
76
77 Err(format!("Failed to parse postlogd log line: {}", line))
78 }
79
80 pub fn supported_event_types(&self) -> usize {
82 1 }
84
85 pub fn matches_component(&self, line: &str) -> bool {
87 line.contains("postfix/postlogd[")
88 }
89
90 fn parse_config_override_warning(&self, message: &str) -> Option<PostlogdEvent> {
93 use regex::Regex;
94 use std::sync::LazyLock;
95
96 static CONFIG_WARNING_RE: LazyLock<Regex> = LazyLock::new(|| {
98 Regex::new(r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$")
99 .expect("Invalid CONFIG_WARNING_RE regex")
100 });
101
102 if let Some(caps) = CONFIG_WARNING_RE.captures(message.trim()) {
103 let file_path = caps.get(1)?.as_str().to_string();
104 let line_number = caps.get(2)?.as_str().parse::<u32>().ok()?;
105 let parameter = caps.get(3)?.as_str().to_string();
106 let value = caps.get(4)?.as_str().to_string();
107
108 return Some(PostlogdEvent {
109 timestamp: chrono::Utc::now().format("%b %d %H:%M:%S").to_string(),
110 process_id: "0".to_string(), event_type: PostlogdEventType::ConfigOverrideWarning {
112 file_path,
113 line_number,
114 parameter,
115 value,
116 },
117 });
118 }
119 None
120 }
121}
122
123impl Default for PostlogdParser {
124 fn default() -> Self {
125 Self::new()
126 }
127}
128
129impl crate::components::ComponentParser for PostlogdParser {
130 fn parse(
131 &self,
132 message: &str,
133 ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
134 let clean_message = message.trim();
135
136 if let Some(event) = self.parse_config_override_warning(clean_message) {
138 return Ok(crate::events::base::ComponentEvent::Postlogd(event));
139 }
140
141 Err(crate::error::ParseError::ComponentParseError {
142 component: "postlogd".to_string(),
143 reason: format!("Unable to parse message: {}", message),
144 })
145 }
146
147 fn component_name(&self) -> &'static str {
148 "postlogd"
149 }
150
151 fn can_parse(&self, message: &str) -> bool {
152 message.contains("overriding earlier entry:") && message.contains("main.cf")
153 }
154}
155
156#[cfg(test)]
157mod tests {
158 use super::*;
159 use crate::components::ComponentParser;
160
161 fn create_parser() -> PostlogdParser {
162 PostlogdParser::new()
163 }
164
165 #[test]
166 fn test_config_override_warning_parsing() {
167 let parser = create_parser();
168 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";
169
170 let result = parser.parse_log_line(log_line);
171 assert!(result.is_ok());
172
173 let event = result.unwrap();
174 assert_eq!(event.timestamp, "Apr 08 17:54:30");
175 assert_eq!(event.process_id, "78");
176
177 let PostlogdEventType::ConfigOverrideWarning {
178 file_path,
179 line_number,
180 parameter,
181 value,
182 } = event.event_type;
183
184 assert_eq!(file_path, "/etc/postfix/main.cf");
185 assert_eq!(line_number, 820);
186 assert_eq!(parameter, "smtpd_recipient_restrictions");
187 assert!(value.contains("check_client_access"));
188 assert!(value.contains("permit_sasl_authenticated"));
189 }
190
191 #[test]
192 fn test_client_message_rate_limit_parsing() {
193 let parser = create_parser();
194 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";
195
196 let result = parser.parse_log_line(log_line);
197 assert!(result.is_ok());
198
199 let event = result.unwrap();
200 assert_eq!(event.timestamp, "Apr 10 11:17:10");
201 assert_eq!(event.process_id, "78");
202
203 let PostlogdEventType::ConfigOverrideWarning {
204 file_path,
205 line_number,
206 parameter,
207 value,
208 } = event.event_type;
209
210 assert_eq!(file_path, "/etc/postfix/main.cf");
211 assert_eq!(line_number, 806);
212 assert_eq!(parameter, "smtpd_client_message_rate_limit");
213 assert_eq!(value, "0");
214 }
215
216 #[test]
217 fn test_discard_ehlo_keywords_parsing() {
218 let parser = create_parser();
219 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";
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 {
226 parameter, value, ..
227 } = event.event_type;
228
229 assert_eq!(parameter, "smtpd_discard_ehlo_keywords");
230 assert_eq!(value, "silent-discard,dsn,etrn");
231 }
232
233 #[test]
234 fn test_different_line_numbers() {
235 let parser = create_parser();
236 let test_cases = vec![
237 ("line 806", 806),
238 ("line 818", 818),
239 ("line 819", 819),
240 ("line 820", 820),
241 ("line 822", 822),
242 ("line 826", 826),
243 ];
244
245 for (line_text, expected_line) in test_cases {
246 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);
247
248 let result = parser.parse_log_line(&log_line);
249 assert!(result.is_ok());
250
251 let event = result.unwrap();
252 let PostlogdEventType::ConfigOverrideWarning { line_number, .. } = event.event_type;
253 assert_eq!(line_number, expected_line);
254 }
255 }
256
257 #[test]
258 fn test_different_process_ids() {
259 let parser = create_parser();
260 let test_cases = vec!["78", "83"];
261
262 for pid in test_cases {
263 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);
264
265 let result = parser.parse_log_line(&log_line);
266 assert!(result.is_ok());
267
268 let event = result.unwrap();
269 assert_eq!(event.process_id, pid);
270 }
271 }
272
273 #[test]
274 fn test_complex_parameter_values() {
275 let parser = create_parser();
276 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";
277
278 let result = parser.parse_log_line(log_line);
279 assert!(result.is_ok());
280
281 let event = result.unwrap();
282 let PostlogdEventType::ConfigOverrideWarning { value, .. } = event.event_type;
283 assert!(value.contains("check_client_access pcre:/etc/postfix/filter_trusted"));
284 assert!(value.contains("check_client_access hash:/etc/postfix/access"));
285 assert!(value.contains("check_recipient_access hash:/etc/postfix/recipient_access"));
286 }
287
288 #[test]
289 fn test_component_matching() {
290 let parser = create_parser();
291
292 let matching_lines = vec![
294 "Apr 08 17:54:30 m01 postfix/postlogd[78]: warning: /etc/postfix/main.cf, line 820: overriding earlier entry: test=value",
295 "Apr 10 11:17:10 m01 postfix/postlogd[83]: warning: /etc/postfix/main.cf, line 806: overriding earlier entry: another=param",
296 ];
297
298 for line in matching_lines {
299 assert!(parser.matches_component(line), "Should match: {}", line);
300 }
301
302 let non_matching_lines = vec![
304 "Apr 08 17:54:30 m01 postfix/qmgr[78]: info: statistics",
305 "Apr 08 17:54:30 m01 postfix/smtpd[78]: connect from localhost",
306 "Apr 08 17:54:30 m01 postfix/cleanup[78]: message-id=<test@example.com>",
307 ];
308
309 for line in non_matching_lines {
310 assert!(
311 !parser.matches_component(line),
312 "Should not match: {}",
313 line
314 );
315 }
316 }
317
318 #[test]
319 fn test_invalid_log_lines() {
320 let parser = create_parser();
321
322 let invalid_lines = vec![
323 "Invalid log line",
324 "Apr 08 17:54:30 m01 postfix/qmgr[78]: info: statistics",
325 "Apr 08 17:54:30 m01 postfix/postlogd[78]: some other message",
326 "incomplete line",
327 ];
328
329 for line in invalid_lines {
330 let result = parser.parse_log_line(line);
331 assert!(result.is_err(), "Should fail to parse: {}", line);
332 }
333 }
334
335 #[test]
336 fn test_supported_event_types() {
337 let parser = create_parser();
338 assert_eq!(parser.supported_event_types(), 1);
339 }
340
341 #[test]
342 fn test_parser_default() {
343 let parser = PostlogdParser::default();
344 assert_eq!(parser.supported_event_types(), 1);
345 }
346
347 #[test]
348 fn test_component_parser_parse() {
349 let parser = PostlogdParser::new();
350
351 let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted";
353 let result = parser.parse(message);
354
355 assert!(result.is_ok());
356 match result.unwrap() {
357 crate::events::base::ComponentEvent::Postlogd(event) => {
358 assert_eq!(event.process_id, "0"); let PostlogdEventType::ConfigOverrideWarning {
360 file_path,
361 line_number,
362 parameter,
363 ..
364 } = event.event_type;
365
366 assert_eq!(file_path, "/etc/postfix/main.cf");
367 assert_eq!(line_number, 820);
368 assert_eq!(parameter, "smtpd_recipient_restrictions");
369 }
370 _ => panic!("Expected Postlogd ComponentEvent"),
371 }
372 }
373
374 #[test]
375 fn test_component_parser_invalid() {
376 let parser = PostlogdParser::new();
377
378 let message = "some invalid message";
379 let result = parser.parse(message);
380
381 assert!(result.is_err());
382 match result.unwrap_err() {
383 crate::error::ParseError::ComponentParseError { component, .. } => {
384 assert_eq!(component, "postlogd");
385 }
386 _ => panic!("Expected ComponentParseError"),
387 }
388 }
389
390 #[test]
391 fn test_component_name() {
392 let parser = PostlogdParser::new();
393 assert_eq!(parser.component_name(), "postlogd");
394 }
395
396 #[test]
397 fn test_can_parse() {
398 let parser = PostlogdParser::new();
399
400 assert!(parser
402 .can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
403
404 assert!(!parser.can_parse("some random message"));
406 assert!(!parser.can_parse("overriding earlier entry: test=value")); assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
408 }
410}