postfix_log_parser/components/
sendmail.rs1use regex::Regex;
29
30use crate::events::sendmail::{SendmailEvent, SendmailEventType};
31
32#[derive(Debug)]
34pub struct SendmailParser {
35 fatal_usage_regex: Regex, usage_only_regex: Regex, }
38
39impl SendmailParser {
40 pub fn new() -> Self {
42 Self {
43 fatal_usage_regex: Regex::new(r"^fatal: (.+)$")
44 .expect("Failed to compile fatal usage regex"),
45 usage_only_regex: Regex::new(r"^usage: (.+)$")
46 .expect("Failed to compile usage only regex"),
47 }
48 }
49
50 pub fn parse_log_line(&self, line: &str) -> Result<SendmailEvent, String> {
52 let basic_regex = Regex::new(
54 r"^((?:\d{4}\s+)?\w{3}\s+\d{1,2}\s+\d{2}:\d{2}:\d{2}(?:\.\d+)?)\s+\S+\s+postfix/sendmail\[(\d+)\]:\s+(.+)$",
55 )
56 .map_err(|e| format!("Regex compilation error: {}", e))?;
57
58 let captures = basic_regex
59 .captures(line)
60 .ok_or_else(|| "Line does not match sendmail log format".to_string())?;
61
62 let timestamp = captures.get(1).unwrap().as_str();
63 let process_id = captures.get(2).unwrap().as_str();
64 let message = captures.get(3).unwrap().as_str();
65
66 if let Some(event) = self.parse_fatal_usage_error(timestamp, process_id, message) {
68 return Ok(event);
69 }
70
71 Err(format!("Unknown sendmail message type: {}", message))
72 }
73
74 pub fn supported_event_types(&self) -> usize {
76 1
77 }
78
79 pub fn matches_component(&self, line: &str) -> bool {
81 line.contains("postfix/sendmail[")
82 }
83
84 fn parse_fatal_usage_error(
86 &self,
87 timestamp: &str,
88 process_id: &str,
89 message: &str,
90 ) -> Option<SendmailEvent> {
91 if let Some(captures) = self.fatal_usage_regex.captures(message) {
92 let error_message = captures.get(1).unwrap().as_str();
93
94 Some(SendmailEvent {
95 timestamp: timestamp.to_string(),
96 process_id: process_id.to_string(),
97 event_type: SendmailEventType::FatalUsageError {
98 message: error_message.to_string(),
99 },
100 })
101 } else {
102 None
103 }
104 }
105}
106
107impl Default for SendmailParser {
108 fn default() -> Self {
109 Self::new()
110 }
111}
112
113impl crate::components::ComponentParser for SendmailParser {
114 fn parse(
115 &self,
116 message: &str,
117 ) -> Result<crate::events::base::ComponentEvent, crate::error::ParseError> {
118 if let Some(captures) = self.usage_only_regex.captures(message) {
120 let error_message = captures.get(1).unwrap().as_str();
121
122 let event = SendmailEvent {
123 timestamp: "0".to_string(), process_id: "0".to_string(), event_type: SendmailEventType::FatalUsageError {
126 message: format!("usage: {}", error_message),
127 },
128 };
129
130 return Ok(crate::events::base::ComponentEvent::Sendmail(event));
131 }
132
133 Err(crate::error::ParseError::ComponentParseError {
134 component: self.component_name().to_string(),
135 reason: format!("Unable to parse sendmail message: {}", message),
136 })
137 }
138
139 fn component_name(&self) -> &'static str {
140 "sendmail"
141 }
142
143 fn can_parse(&self, message: &str) -> bool {
144 self.usage_only_regex.is_match(message)
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::components::ComponentParser;
152
153 fn create_parser() -> SendmailParser {
154 SendmailParser::new()
155 }
156
157 #[test]
158 fn test_fatal_usage_error_parsing() {
159 let parser = create_parser();
160 let log_line = "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]";
161
162 let result = parser.parse_log_line(log_line);
163 assert!(result.is_ok());
164
165 let event = result.unwrap();
166 assert_eq!(event.timestamp, "Apr 24 17:20:55");
167 assert_eq!(event.process_id, "180");
168
169 let SendmailEventType::FatalUsageError { message } = event.event_type;
170 assert_eq!(message, "usage: mailq [options]");
171 }
172
173 #[test]
174 fn test_different_process_ids() {
175 let parser = create_parser();
176 let test_cases = vec!["180", "187", "208", "216", "223", "230"];
177
178 for pid in test_cases {
179 let log_line = format!(
180 "Apr 24 17:20:55 m01 postfix/sendmail[{}]: fatal: usage: mailq [options]",
181 pid
182 );
183
184 let result = parser.parse_log_line(&log_line);
185 assert!(result.is_ok());
186
187 let event = result.unwrap();
188 assert_eq!(event.process_id, pid);
189 }
190 }
191
192 #[test]
193 fn test_different_timestamps() {
194 let parser = create_parser();
195 let test_cases = vec![
196 "Apr 24 17:20:55",
197 "Apr 24 17:20:56",
198 "Apr 24 17:23:16",
199 "Apr 24 17:23:19",
200 ];
201
202 for timestamp in test_cases {
203 let log_line = format!(
204 "{} m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
205 timestamp
206 );
207
208 let result = parser.parse_log_line(&log_line);
209 assert!(result.is_ok());
210
211 let event = result.unwrap();
212 assert_eq!(event.timestamp, timestamp);
213 }
214 }
215
216 #[test]
217 fn test_component_matching() {
218 let parser = create_parser();
219
220 let matching_lines = vec![
222 "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
223 "Apr 24 17:20:56 m01 postfix/sendmail[187]: fatal: some other error",
224 ];
225
226 for line in matching_lines {
227 assert!(parser.matches_component(line), "Should match: {}", line);
228 }
229
230 let non_matching_lines = vec![
232 "Apr 24 17:20:55 m01 postfix/qmgr[78]: info: statistics",
233 "Apr 24 17:20:55 m01 postfix/smtpd[78]: connect from localhost",
234 "Apr 24 17:20:55 m01 postfix/cleanup[78]: message-id=<test@example.com>",
235 ];
236
237 for line in non_matching_lines {
238 assert!(
239 !parser.matches_component(line),
240 "Should not match: {}",
241 line
242 );
243 }
244 }
245
246 #[test]
247 fn test_invalid_log_lines() {
248 let parser = create_parser();
249
250 let invalid_lines = vec![
251 "Invalid log line",
252 "Apr 24 17:20:55 m01 postfix/qmgr[78]: info: statistics",
253 "Apr 24 17:20:55 m01 postfix/sendmail[180]: info: some info message",
254 "incomplete line",
255 ];
256
257 for line in invalid_lines {
258 let result = parser.parse_log_line(line);
259 assert!(result.is_err(), "Should fail to parse: {}", line);
260 }
261 }
262
263 #[test]
264 fn test_supported_event_types() {
265 let parser = create_parser();
266 assert_eq!(parser.supported_event_types(), 1);
267 }
268
269 #[test]
270 fn test_parser_default() {
271 let parser = SendmailParser::default();
272 assert_eq!(parser.supported_event_types(), 1);
273 }
274
275 #[test]
276 fn test_component_parser_parse() {
277 let parser = SendmailParser::new();
278
279 let message = "usage: mailq [options]";
281 let result = parser.parse(message);
282
283 assert!(result.is_ok());
284 match result.unwrap() {
285 crate::events::base::ComponentEvent::Sendmail(event) => {
286 assert_eq!(event.process_id, "0"); let SendmailEventType::FatalUsageError { message } = event.event_type;
288 assert_eq!(message, "usage: mailq [options]");
289 }
290 _ => panic!("Expected Sendmail ComponentEvent"),
291 }
292 }
293
294 #[test]
295 fn test_component_parser_invalid() {
296 let parser = SendmailParser::new();
297
298 let message = "some invalid message";
299 let result = parser.parse(message);
300
301 assert!(result.is_err());
302 match result.unwrap_err() {
303 crate::error::ParseError::ComponentParseError { component, .. } => {
304 assert_eq!(component, "sendmail");
305 }
306 _ => panic!("Expected ComponentParseError"),
307 }
308 }
309
310 #[test]
311 fn test_component_name() {
312 let parser = SendmailParser::new();
313 assert_eq!(parser.component_name(), "sendmail");
314 }
315
316 #[test]
317 fn test_can_parse() {
318 let parser = SendmailParser::new();
319
320 assert!(parser.can_parse("usage: mailq [options]"));
322 assert!(parser.can_parse("usage: some other command"));
323
324 assert!(!parser.can_parse("some random message"));
326 assert!(!parser.can_parse("info: some info message"));
327 assert!(!parser.can_parse("warning: some warning"));
328 assert!(!parser.can_parse("fatal: some other error")); }
330
331 #[test]
332 fn test_parse_real_log_samples() {
333 let parser = create_parser();
334
335 let real_logs = vec![
337 "Apr 24 17:20:55 m01 postfix/sendmail[180]: fatal: usage: mailq [options]",
338 "Apr 24 17:20:56 m01 postfix/sendmail[187]: fatal: usage: mailq [options]",
339 "Apr 24 17:23:16 m01 postfix/sendmail[208]: fatal: usage: mailq [options]",
340 "Apr 24 17:23:19 m01 postfix/sendmail[216]: fatal: usage: mailq [options]",
341 "Apr 24 17:23:22 m01 postfix/sendmail[223]: fatal: usage: mailq [options]",
342 "Apr 24 17:23:24 m01 postfix/sendmail[230]: fatal: usage: mailq [options]",
343 ];
344
345 for (i, log_line) in real_logs.iter().enumerate() {
346 let result = parser.parse_log_line(log_line);
347 assert!(
348 result.is_ok(),
349 "Failed to parse real log sample {}: {}",
350 i,
351 log_line
352 );
353
354 let event = result.unwrap();
355 let SendmailEventType::FatalUsageError { message } = event.event_type;
356 assert_eq!(message, "usage: mailq [options]");
357 }
358 }
359}