postfix_log_parser/components/
pickup.rs

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