postfix_log_parser/components/
postfix_script.rs

1//! Postfix Script component parser
2//!
3//! Parses log entries for the postfix-script component, which handles
4//! administrative commands and system management operations.
5
6use 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    // System operations
18    starting_regex: Regex,
19    running_regex: Regex,
20    refreshing_regex: Regex,
21
22    // Fatal errors
23    cannot_execute_postconf_regex: Regex,
24    cannot_execute_command_regex: Regex,
25
26    // Warnings
27    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            // System operations
36            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            // Fatal errors
41            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            // Warnings
46            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        // Generic fatal error
116        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        // Generic warning
169        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        // Try to detect log level from message content
181        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        // Try system operations
197        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