postfix_log_parser/components/
postfix_script.rs1use crate::components::ComponentParser;
40use crate::error::ParseError;
41use crate::events::base::{BaseEvent, PostfixLogLevel};
42use crate::events::postfix_script::{
43 PostfixScriptEvent, PostfixScriptFatalError, PostfixScriptOperation, PostfixScriptWarningType,
44};
45use crate::events::ComponentEvent;
46use chrono::{DateTime, Utc};
47use regex::Regex;
48
49pub struct PostfixScriptParser {
50 starting_regex: Regex,
52 running_regex: Regex,
53 refreshing_regex: Regex,
54
55 cannot_execute_postconf_regex: Regex,
57 cannot_execute_command_regex: Regex,
58
59 not_owned_regex: Regex,
61 group_writable_regex: Regex,
62 symlink_leaves_regex: Regex,
63}
64
65impl PostfixScriptParser {
66 pub fn new() -> Self {
67 PostfixScriptParser {
68 starting_regex: Regex::new(r"^starting the Postfix mail system\s*$").unwrap(),
70 running_regex: Regex::new(r"^the Postfix mail system is running: PID: (\d+)\s*$")
71 .unwrap(),
72 refreshing_regex: Regex::new(r"^refreshing the Postfix mail system\s*$").unwrap(),
73
74 cannot_execute_postconf_regex: Regex::new(r"^cannot execute /usr/sbin/postconf!$")
76 .unwrap(),
77 cannot_execute_command_regex: Regex::new(r"^cannot execute (.+?)!?$").unwrap(),
78
79 not_owned_regex: Regex::new(r"^not owned by (root|postfix): (.+)$").unwrap(),
81 group_writable_regex: Regex::new(r"^group or other writable: (.+)$").unwrap(),
82 symlink_leaves_regex: Regex::new(r"^symlink leaves directory: (.+)$").unwrap(),
83 }
84 }
85
86 fn create_base_event(&self, message: &str) -> BaseEvent {
87 BaseEvent {
88 timestamp: DateTime::parse_from_rfc3339("2024-04-27T16:20:48Z")
89 .unwrap()
90 .with_timezone(&Utc),
91 hostname: "unknown".to_string(),
92 component: "postfix-script".to_string(),
93 process_id: 0,
94 log_level: PostfixLogLevel::Info,
95 raw_message: message.to_string(),
96 }
97 }
98
99 fn parse_system_operation(&self, message: &str) -> Option<PostfixScriptEvent> {
100 let base = self.create_base_event(message);
101
102 if self.starting_regex.is_match(message) {
103 return Some(PostfixScriptEvent::SystemOperation {
104 base,
105 operation: PostfixScriptOperation::Starting,
106 });
107 }
108
109 if let Some(caps) = self.running_regex.captures(message) {
110 let pid = caps.get(1).and_then(|m| m.as_str().parse::<u32>().ok());
111 return Some(PostfixScriptEvent::SystemOperation {
112 base,
113 operation: PostfixScriptOperation::Running { pid },
114 });
115 }
116
117 if self.refreshing_regex.is_match(message) {
118 return Some(PostfixScriptEvent::SystemOperation {
119 base,
120 operation: PostfixScriptOperation::Refreshing,
121 });
122 }
123
124 None
125 }
126
127 fn parse_fatal_error(&self, message: &str) -> Option<PostfixScriptEvent> {
128 let mut base = self.create_base_event(message);
129 base.log_level = PostfixLogLevel::Fatal;
130
131 if self.cannot_execute_postconf_regex.is_match(message) {
132 return Some(PostfixScriptEvent::FatalError {
133 base,
134 error: PostfixScriptFatalError::CannotExecutePostconf,
135 });
136 }
137
138 if let Some(caps) = self.cannot_execute_command_regex.captures(message) {
139 let command = caps
140 .get(1)
141 .map(|m| m.as_str().to_string())
142 .unwrap_or_else(|| "unknown".to_string());
143 return Some(PostfixScriptEvent::FatalError {
144 base,
145 error: PostfixScriptFatalError::CannotExecuteCommand { command },
146 });
147 }
148
149 Some(PostfixScriptEvent::FatalError {
151 base,
152 error: PostfixScriptFatalError::Other {
153 message: message.to_string(),
154 },
155 })
156 }
157
158 fn parse_warning(&self, message: &str) -> Option<PostfixScriptEvent> {
159 let mut base = self.create_base_event(message);
160 base.log_level = PostfixLogLevel::Warning;
161
162 if let Some(caps) = self.not_owned_regex.captures(message) {
163 let expected_owner = caps
164 .get(1)
165 .map(|m| m.as_str().to_string())
166 .unwrap_or_else(|| "unknown".to_string());
167 let path = caps
168 .get(2)
169 .map(|m| m.as_str().to_string())
170 .unwrap_or_else(|| "unknown".to_string());
171 return Some(PostfixScriptEvent::Warning {
172 base,
173 warning: PostfixScriptWarningType::NotOwnedBy {
174 path,
175 expected_owner,
176 },
177 });
178 }
179
180 if let Some(caps) = self.group_writable_regex.captures(message) {
181 let path = caps
182 .get(1)
183 .map(|m| m.as_str().to_string())
184 .unwrap_or_else(|| "unknown".to_string());
185 return Some(PostfixScriptEvent::Warning {
186 base,
187 warning: PostfixScriptWarningType::GroupWritable { path },
188 });
189 }
190
191 if let Some(caps) = self.symlink_leaves_regex.captures(message) {
192 let path = caps
193 .get(1)
194 .map(|m| m.as_str().to_string())
195 .unwrap_or_else(|| "unknown".to_string());
196 return Some(PostfixScriptEvent::Warning {
197 base,
198 warning: PostfixScriptWarningType::SymlinkLeaves { path },
199 });
200 }
201
202 Some(PostfixScriptEvent::Warning {
204 base,
205 warning: PostfixScriptWarningType::Other {
206 message: message.to_string(),
207 },
208 })
209 }
210}
211
212impl ComponentParser for PostfixScriptParser {
213 fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
214 if message.contains("fatal:") || message.contains("cannot execute") {
216 if let Some(event) = self.parse_fatal_error(message) {
217 return Ok(ComponentEvent::PostfixScript(event));
218 }
219 }
220
221 if message.contains("warning:")
222 || message.contains("not owned by")
223 || message.contains("group or other writable")
224 {
225 if let Some(event) = self.parse_warning(message) {
226 return Ok(ComponentEvent::PostfixScript(event));
227 }
228 }
229
230 if let Some(event) = self.parse_system_operation(message) {
232 return Ok(ComponentEvent::PostfixScript(event));
233 }
234
235 Err(ParseError::ComponentParseError {
236 component: "postfix-script".to_string(),
237 reason: format!("Unsupported message format: {}", message),
238 })
239 }
240
241 fn component_name(&self) -> &'static str {
242 "postfix-script"
243 }
244
245 fn can_parse(&self, component: &str) -> bool {
246 component == "postfix-script"
247 }
248}
249
250impl Default for PostfixScriptParser {
251 fn default() -> Self {
252 Self::new()
253 }
254}