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