postfix_log_parser/components/
pickup.rs

1use crate::components::ComponentParser;
2use crate::events::pickup::PickupEvent;
3use crate::events::ComponentEvent;
4use chrono::Utc;
5use lazy_static::lazy_static;
6use regex::Regex;
7
8lazy_static! {
9    // 配置覆盖警告:/etc/postfix/main.cf, line 820: overriding earlier entry: parameter=value
10    // 注意:主解析器已经移除了"warning:"前缀
11    static ref CONFIG_OVERRIDE_WARNING_REGEX: Regex = Regex::new(
12        r"^(.+?), line (\d+): overriding earlier entry: (.+?)=(.+)$"
13    ).unwrap();
14
15    // 邮件拾取事件:226751E20F00: uid=0 from=<root>
16    static ref MAIL_PICKUP_REGEX: Regex = Regex::new(
17        r"^([A-F0-9]+): uid=(\d+) from=(.+)$"
18    ).unwrap();
19}
20
21pub struct PickupParser;
22
23impl PickupParser {
24    pub fn new() -> Self {
25        Self
26    }
27
28    /// 解析配置覆盖警告
29    /// 示例: "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=value"
30    /// 注意:主解析器已经移除了"warning:"前缀
31    fn parse_config_override_warning(&self, message: &str) -> Option<PickupEvent> {
32        if let Some(captures) = CONFIG_OVERRIDE_WARNING_REGEX.captures(message) {
33            let file_path = captures.get(1)?.as_str().to_string();
34            let line_number = captures.get(2)?.as_str().parse::<u32>().ok()?;
35            let parameter_name = captures.get(3)?.as_str().to_string();
36            let parameter_value = captures.get(4)?.as_str().to_string();
37
38            Some(PickupEvent::config_override_warning(
39                Utc::now(),
40                None,
41                file_path,
42                line_number,
43                parameter_name,
44                parameter_value,
45            ))
46        } else {
47            None
48        }
49    }
50
51    /// 解析邮件拾取事件
52    /// 示例: "226751E20F00: uid=0 from=<root>"
53    fn parse_mail_pickup(&self, message: &str) -> Option<PickupEvent> {
54        if let Some(captures) = MAIL_PICKUP_REGEX.captures(message) {
55            let queue_id = captures.get(1)?.as_str().to_string();
56            let uid = captures.get(2)?.as_str().parse::<u32>().ok()?;
57            let sender = captures.get(3)?.as_str().to_string();
58
59            Some(PickupEvent::mail_pickup(
60                Utc::now(),
61                None,
62                queue_id,
63                uid,
64                sender,
65            ))
66        } else {
67            None
68        }
69    }
70}
71
72impl ComponentParser for PickupParser {
73    fn parse(&self, message: &str) -> Result<ComponentEvent, crate::error::ParseError> {
74        let clean_message = message.trim();
75
76        // 首先尝试解析配置覆盖警告
77        if let Some(event) = self.parse_config_override_warning(clean_message) {
78            return Ok(ComponentEvent::Pickup(event));
79        }
80
81        // 然后尝试解析邮件拾取事件
82        if let Some(event) = self.parse_mail_pickup(clean_message) {
83            return Ok(ComponentEvent::Pickup(event));
84        }
85
86        Err(crate::error::ParseError::ComponentParseError {
87            component: "pickup".to_string(),
88            reason: format!("Unable to parse message: {}", message),
89        })
90    }
91
92    fn component_name(&self) -> &'static str {
93        "pickup"
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::events::pickup::PickupEventType;
101
102    fn create_parser() -> PickupParser {
103        PickupParser::new()
104    }
105
106    #[test]
107    fn test_parse_config_override_warning() {
108        let parser = create_parser();
109
110        let message = "/etc/postfix/main.cf, line 820: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted,permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination,pcre:/etc/postfix/filter_default";
111        let result = parser.parse_config_override_warning(message);
112
113        assert!(result.is_some());
114        let event = result.unwrap();
115
116        match &event.event_type {
117            PickupEventType::ConfigOverrideWarning {
118                file_path,
119                line_number,
120                parameter_name,
121                parameter_value,
122            } => {
123                assert_eq!(file_path, "/etc/postfix/main.cf");
124                assert_eq!(*line_number, 820);
125                assert_eq!(parameter_name, "smtpd_recipient_restrictions");
126                assert!(parameter_value.contains("check_client_access"));
127            }
128            _ => panic!("Expected ConfigOverrideWarning event type"),
129        }
130    }
131
132    #[test]
133    fn test_parse_mail_pickup() {
134        let parser = create_parser();
135
136        let message = "226751E20F00: uid=0 from=<root>";
137        let result = parser.parse_mail_pickup(message);
138
139        assert!(result.is_some());
140        let event = result.unwrap();
141
142        match &event.event_type {
143            PickupEventType::MailPickup {
144                queue_id,
145                uid,
146                sender,
147            } => {
148                assert_eq!(queue_id, "226751E20F00");
149                assert_eq!(*uid, 0);
150                assert_eq!(sender, "<root>");
151            }
152            _ => panic!("Expected MailPickup event type"),
153        }
154    }
155
156    #[test]
157    fn test_component_parser_parse_config_override() {
158        let parser = create_parser();
159
160        let message = "/etc/postfix/main.cf, line 806: overriding earlier entry: smtpd_client_message_rate_limit=0";
161        let result = parser.parse(message);
162
163        assert!(result.is_ok());
164        match result.unwrap() {
165            ComponentEvent::Pickup(event) => {
166                assert_eq!(event.severity(), "warning");
167                assert_eq!(
168                    event.parameter_name(),
169                    Some("smtpd_client_message_rate_limit")
170                );
171                assert_eq!(event.queue_id(), None);
172                assert_eq!(event.sender(), None);
173            }
174            _ => panic!("Expected Pickup ComponentEvent"),
175        }
176    }
177
178    #[test]
179    fn test_component_parser_parse_mail_pickup() {
180        let parser = create_parser();
181
182        let message = "226751E20F00: uid=0 from=<root>";
183        let result = parser.parse(message);
184
185        assert!(result.is_ok());
186        match result.unwrap() {
187            ComponentEvent::Pickup(event) => {
188                assert_eq!(event.severity(), "info");
189                assert_eq!(event.parameter_name(), None);
190                assert_eq!(event.queue_id(), Some("226751E20F00"));
191                assert_eq!(event.sender(), Some("<root>"));
192                assert_eq!(event.uid(), Some(0));
193            }
194            _ => panic!("Expected Pickup ComponentEvent"),
195        }
196    }
197
198    #[test]
199    fn test_component_parser_parse_invalid() {
200        let parser = create_parser();
201
202        let message = "some unknown message format";
203        let result = parser.parse(message);
204
205        assert!(result.is_err());
206        match result.unwrap_err() {
207            crate::error::ParseError::ComponentParseError { component, .. } => {
208                assert_eq!(component, "pickup");
209            }
210            _ => panic!("Expected ComponentParseError"),
211        }
212    }
213
214    #[test]
215    fn test_component_name() {
216        let parser = create_parser();
217        assert_eq!(parser.component_name(), "pickup");
218    }
219
220    #[test]
221    fn test_parse_real_log_samples() {
222        let parser = create_parser();
223
224        // 真实日志样例1:配置覆盖
225        let message1 = "/etc/postfix/main.cf, line 819: overriding earlier entry: smtpd_recipient_restrictions=check_client_access pcre:/etc/postfix/filter_trusted,permit_sasl_authenticated,permit_mynetworks,reject_unauth_destination,pcre:/etc/postfix/filter_default";
226        let result1 = parser.parse(message1);
227        assert!(result1.is_ok());
228
229        // 真实日志样例2:邮件拾取
230        let message2 = "226751E20F00: uid=0 from=<root>";
231        let result2 = parser.parse(message2);
232        assert!(result2.is_ok());
233
234        // 真实日志样例3:另一个参数覆盖
235        let message3 = "/etc/postfix/main.cf, line 826: overriding earlier entry: smtpd_discard_ehlo_keywords=silent-discard,dsn,etrn";
236        let result3 = parser.parse(message3);
237        assert!(result3.is_ok());
238
239        match result3.unwrap() {
240            ComponentEvent::Pickup(event) => {
241                assert_eq!(event.parameter_name(), Some("smtpd_discard_ehlo_keywords"));
242            }
243            _ => panic!("Expected Pickup ComponentEvent"),
244        }
245    }
246}