postfix_log_parser/components/
postlogd.rs1use crate::events::postlogd::{PostlogdEvent, PostlogdEventType};
2use regex::Regex;
3use std::sync::LazyLock;
4
5pub struct PostlogdParser;
8
9static 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 pub fn new() -> Self {
19 Self
20 }
21
22 pub fn parse_log_line(&self, line: &str) -> Result<PostlogdEvent, String> {
24 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 pub fn supported_event_types(&self) -> usize {
55 1 }
57
58 pub fn matches_component(&self, line: &str) -> bool {
60 line.contains("postfix/postlogd[")
61 }
62
63 fn parse_config_override_warning(&self, message: &str) -> Option<PostlogdEvent> {
66 use regex::Regex;
67 use std::sync::LazyLock;
68
69 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(), 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 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 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 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 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"); 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 assert!(parser
375 .can_parse("/etc/postfix/main.cf, line 820: overriding earlier entry: test=value"));
376
377 assert!(!parser.can_parse("some random message"));
379 assert!(!parser.can_parse("overriding earlier entry: test=value")); assert!(!parser.can_parse("/etc/postfix/main.cf, line 820: some other message"));
381 }
383}