postfix_log_parser/components/
postfix_script.rs1use crate::components::ComponentParser;
7use crate::error::ParseError;
8use crate::events::base::{BaseEvent, PostfixLogLevel};
9use crate::events::postfix_script::{
10 PostfixScriptEvent, PostfixScriptFatalError, PostfixScriptOperation, PostfixScriptWarningType,
11};
12use crate::events::ComponentEvent;
13use chrono::{DateTime, Utc};
14use regex::Regex;
15
16pub struct PostfixScriptParser {
17 starting_regex: Regex,
19 running_regex: Regex,
20 refreshing_regex: Regex,
21
22 cannot_execute_postconf_regex: Regex,
24 cannot_execute_command_regex: Regex,
25
26 not_owned_regex: Regex,
28 group_writable_regex: Regex,
29 symlink_leaves_regex: Regex,
30}
31
32impl PostfixScriptParser {
33 pub fn new() -> Self {
34 PostfixScriptParser {
35 starting_regex: Regex::new(r"^starting the Postfix mail system$").unwrap(),
37 running_regex: Regex::new(r"^the Postfix mail system is running: PID: (\d+)$").unwrap(),
38 refreshing_regex: Regex::new(r"^refreshing the Postfix mail system$").unwrap(),
39
40 cannot_execute_postconf_regex: Regex::new(r"^cannot execute /usr/sbin/postconf!$")
42 .unwrap(),
43 cannot_execute_command_regex: Regex::new(r"^cannot execute (.+?)!?$").unwrap(),
44
45 not_owned_regex: Regex::new(r"^not owned by (root|postfix): (.+)$").unwrap(),
47 group_writable_regex: Regex::new(r"^group or other writable: (.+)$").unwrap(),
48 symlink_leaves_regex: Regex::new(r"^symlink leaves directory: (.+)$").unwrap(),
49 }
50 }
51
52 fn create_base_event(&self, message: &str) -> BaseEvent {
53 BaseEvent {
54 timestamp: DateTime::parse_from_rfc3339("2024-04-27T16:20:48Z")
55 .unwrap()
56 .with_timezone(&Utc),
57 hostname: "unknown".to_string(),
58 component: "postfix-script".to_string(),
59 process_id: 0,
60 log_level: PostfixLogLevel::Info,
61 raw_message: message.to_string(),
62 }
63 }
64
65 fn parse_system_operation(&self, message: &str) -> Option<PostfixScriptEvent> {
66 let base = self.create_base_event(message);
67
68 if self.starting_regex.is_match(message) {
69 return Some(PostfixScriptEvent::SystemOperation {
70 base,
71 operation: PostfixScriptOperation::Starting,
72 });
73 }
74
75 if let Some(caps) = self.running_regex.captures(message) {
76 let pid = caps.get(1).and_then(|m| m.as_str().parse::<u32>().ok());
77 return Some(PostfixScriptEvent::SystemOperation {
78 base,
79 operation: PostfixScriptOperation::Running { pid },
80 });
81 }
82
83 if self.refreshing_regex.is_match(message) {
84 return Some(PostfixScriptEvent::SystemOperation {
85 base,
86 operation: PostfixScriptOperation::Refreshing,
87 });
88 }
89
90 None
91 }
92
93 fn parse_fatal_error(&self, message: &str) -> Option<PostfixScriptEvent> {
94 let mut base = self.create_base_event(message);
95 base.log_level = PostfixLogLevel::Fatal;
96
97 if self.cannot_execute_postconf_regex.is_match(message) {
98 return Some(PostfixScriptEvent::FatalError {
99 base,
100 error: PostfixScriptFatalError::CannotExecutePostconf,
101 });
102 }
103
104 if let Some(caps) = self.cannot_execute_command_regex.captures(message) {
105 let command = caps
106 .get(1)
107 .map(|m| m.as_str().to_string())
108 .unwrap_or_else(|| "unknown".to_string());
109 return Some(PostfixScriptEvent::FatalError {
110 base,
111 error: PostfixScriptFatalError::CannotExecuteCommand { command },
112 });
113 }
114
115 Some(PostfixScriptEvent::FatalError {
117 base,
118 error: PostfixScriptFatalError::Other {
119 message: message.to_string(),
120 },
121 })
122 }
123
124 fn parse_warning(&self, message: &str) -> Option<PostfixScriptEvent> {
125 let mut base = self.create_base_event(message);
126 base.log_level = PostfixLogLevel::Warning;
127
128 if let Some(caps) = self.not_owned_regex.captures(message) {
129 let expected_owner = caps
130 .get(1)
131 .map(|m| m.as_str().to_string())
132 .unwrap_or_else(|| "unknown".to_string());
133 let path = caps
134 .get(2)
135 .map(|m| m.as_str().to_string())
136 .unwrap_or_else(|| "unknown".to_string());
137 return Some(PostfixScriptEvent::Warning {
138 base,
139 warning: PostfixScriptWarningType::NotOwnedBy {
140 path,
141 expected_owner,
142 },
143 });
144 }
145
146 if let Some(caps) = self.group_writable_regex.captures(message) {
147 let path = caps
148 .get(1)
149 .map(|m| m.as_str().to_string())
150 .unwrap_or_else(|| "unknown".to_string());
151 return Some(PostfixScriptEvent::Warning {
152 base,
153 warning: PostfixScriptWarningType::GroupWritable { path },
154 });
155 }
156
157 if let Some(caps) = self.symlink_leaves_regex.captures(message) {
158 let path = caps
159 .get(1)
160 .map(|m| m.as_str().to_string())
161 .unwrap_or_else(|| "unknown".to_string());
162 return Some(PostfixScriptEvent::Warning {
163 base,
164 warning: PostfixScriptWarningType::SymlinkLeaves { path },
165 });
166 }
167
168 Some(PostfixScriptEvent::Warning {
170 base,
171 warning: PostfixScriptWarningType::Other {
172 message: message.to_string(),
173 },
174 })
175 }
176}
177
178impl ComponentParser for PostfixScriptParser {
179 fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
180 if message.contains("fatal:") || message.contains("cannot execute") {
182 if let Some(event) = self.parse_fatal_error(message) {
183 return Ok(ComponentEvent::PostfixScript(event));
184 }
185 }
186
187 if message.contains("warning:")
188 || message.contains("not owned by")
189 || message.contains("group or other writable")
190 {
191 if let Some(event) = self.parse_warning(message) {
192 return Ok(ComponentEvent::PostfixScript(event));
193 }
194 }
195
196 if let Some(event) = self.parse_system_operation(message) {
198 return Ok(ComponentEvent::PostfixScript(event));
199 }
200
201 Err(ParseError::ComponentParseError {
202 component: "postfix-script".to_string(),
203 reason: format!("Unsupported message format: {}", message),
204 })
205 }
206
207 fn component_name(&self) -> &'static str {
208 "postfix-script"
209 }
210
211 fn can_parse(&self, component: &str) -> bool {
212 component == "postfix-script"
213 }
214}
215