postfix_log_parser/components/
smtpd.rs

1//! # SMTPD 邮件接收服务组件解析器
2//!
3//! SMTPD 是 Postfix 的邮件接收服务组件,负责:
4//! - 接收来自客户端的 SMTP 连接
5//! - 处理 SMTP 协议命令和会话管理
6//! - 执行反垃圾邮件策略和访问控制
7//! - 提供 SASL 认证和 TLS 加密支持
8//!
9//! ## 核心功能
10//!
11//! - **连接管理**: 处理客户端连接、断开和超时
12//! - **协议处理**: 支持 SMTP/ESMTP 命令处理
13//! - **安全控制**: SASL 认证、TLS 加密、访问策略
14//! - **反垃圾邮件**: RBL 查询、内容过滤、速率限制
15//! - **队列管理**: 为接收的邮件分配队列 ID
16//!
17//! ## 支持的事件类型
18//!
19//! - **连接事件**: 客户端连接建立和断开
20//! - **认证事件**: SASL 认证成功和失败
21//! - **拒绝事件**: 各种策略拒绝和过滤
22//! - **邮件处理**: 队列 ID 分配和邮件接收
23//! - **系统警告**: 配置问题和性能警告
24//! - **协议交互**: HELO/EHLO 命令和功能协商
25//!
26//! ## 示例日志格式
27//!
28//! ```text
29//! # 连接事件
30//! connect from client.example.com[192.168.1.100]
31//! disconnect from client.example.com[192.168.1.100] ehlo=1 mail=1 rcpt=1 data=1 quit=1 commands=5
32//!
33//! # 邮件处理
34//! queue_id: client=client.example.com[192.168.1.100]
35//!
36//! # 拒绝事件
37//! NOQUEUE: reject: RCPT from client.example.com[192.168.1.100]: 550 5.1.1 User unknown; from=<sender@example.com> to=<user@domain.com>
38//!
39//! # 认证失败
40//! client.example.com[192.168.1.100]: SASL LOGIN authentication failed: Invalid credentials
41//! ```
42
43use crate::components::ComponentParser;
44use crate::error::ParseError;
45use crate::events::smtpd::{CommandStats, RejectType};
46use crate::events::{ComponentEvent, SmtpdEvent};
47use crate::utils::queue_id::create_queue_id_pattern;
48use regex::Regex;
49
50/// SMTPD解析器
51pub struct SmtpdParser {
52    // 连接管理相关
53    connect_regex: Regex,
54    disconnect_regex: Regex,
55    lost_connection_regex: Regex,
56    timeout_regex: Regex,
57
58    // 邮件处理相关
59    client_assignment_regex: Regex,
60    noqueue_filter_regex: Regex,
61
62        // 认证和安全相关
63    sasl_auth_failure_regex: Regex,
64    reject_noqueue_regex: Regex,
65    
66    // 协议相关
67    helo_regex: Regex,
68
69    // 系统警告相关
70    system_warning_regex: Regex,
71
72    // 命令统计解析
73    command_stats_regex: Regex,
74}
75
76impl SmtpdParser {
77    /// 创建新的SMTPD解析器
78    /// 优化了正则表达式性能,使用非贪婪匹配和更精确的模式
79    pub fn new() -> Self {
80        Self {
81            // 连接管理相关 - 使用更高效的非贪婪匹配
82            connect_regex: Regex::new(r"^connect from ([^\[]+)\[([^\]]+)\](?::(\d+))?").unwrap(),
83            disconnect_regex: Regex::new(r"^disconnect from ([^\[]+)\[([^\]]+)\](?::(\d+))?(.*)?").unwrap(),
84            lost_connection_regex: Regex::new(r"^lost connection after (\w+) from ([^\[]+)\[([^\]]+)\]").unwrap(),
85            timeout_regex: Regex::new(r"^timeout after (\w+) from ([^\[]+)\[([^\]]+)\]").unwrap(),
86            
87            // 邮件处理相关 - 优化队列ID格式
88            client_assignment_regex: Regex::new(&create_queue_id_pattern(r"^{QUEUE_ID}: client=([^\[]+)\[([^\]]+)\](?::(\d+))?")).unwrap(),
89            noqueue_filter_regex: Regex::new(r"^NOQUEUE: filter: RCPT from ([^\[]+)\[([^\]]+)\]: (.+)").unwrap(),
90            
91            // 认证和安全相关 - 优化SASL匹配
92            sasl_auth_failure_regex: Regex::new(r"([^\[]+)\[([^\]]+)\]: SASL (\w+) authentication failed: (.+)").unwrap(),
93            reject_noqueue_regex: Regex::new(r"^NOQUEUE: reject: (\w+) from ([^\[]+)\[([^\]]+)\]: (\d{3}) (.+?)(?:; from=<([^>]+)> to=<([^>]+)>)?").unwrap(),
94            
95            // 协议相关 - 更精确的HELO匹配
96            helo_regex: Regex::new(r"client=([^\[]+)\[([^\]]+)\], helo=<([^>]+)>").unwrap(),
97            
98            // 系统警告相关 - 匹配已剥离warning:前缀的消息,识别常见警告模式
99            system_warning_regex: Regex::new(r"^(dict_\w+|hostname \w+|non-SMTP|Illegal|\w+_init|address syntax|TLS|SASL|milter).*").unwrap(),
100            
101            // 命令统计解析 - 修复空格处理问题
102            command_stats_regex: Regex::new(r"\s*(?:ehlo=(\d+))?\s*(?:helo=(\d+))?\s*(?:mail=(\d+))?\s*(?:rcpt=(\d+))?\s*(?:data=(\d+))?\s*(?:bdat=(\d+))?\s*(?:quit=(\d+))?\s*(?:commands=(\d+))?\s*").unwrap(),
103        }
104    }
105
106    /// 解析SMTP命令统计信息
107    fn parse_command_stats(&self, stats_text: &str) -> Option<CommandStats> {
108        self.command_stats_regex.captures(stats_text).map(|captures| CommandStats {
109                ehlo: captures.get(1).and_then(|m| m.as_str().parse().ok()),
110                helo: captures.get(2).and_then(|m| m.as_str().parse().ok()),
111                mail: captures.get(3).and_then(|m| m.as_str().parse().ok()),
112                rcpt: captures.get(4).and_then(|m| m.as_str().parse().ok()),
113                data: captures.get(5).and_then(|m| m.as_str().parse().ok()),
114                bdat: captures.get(6).and_then(|m| m.as_str().parse().ok()),
115                quit: captures.get(7).and_then(|m| m.as_str().parse().ok()),
116                commands: captures.get(8).and_then(|m| m.as_str().parse().ok()),
117            })
118    }
119}
120
121impl ComponentParser for SmtpdParser {
122    fn parse(&self, message: &str) -> Result<ComponentEvent, ParseError> {
123        // 优化解析顺序:按真实日志中出现频率排序
124        // 最常见:客户端队列ID分配 -> 连接 -> 断开连接 -> HELO -> 其他
125        
126        // 1. 尝试匹配客户端队列ID分配事件(最常见)
127        if let Some(captures) = self.client_assignment_regex.captures(message) {
128            let queue_id = captures.get(1).unwrap().as_str().to_string();
129            let client_hostname = captures.get(2).unwrap().as_str().to_string();
130            let client_ip = captures.get(3).unwrap().as_str().to_string();
131            let port = captures.get(4).and_then(|m| m.as_str().parse::<u16>().ok());
132
133            return Ok(ComponentEvent::Smtpd(SmtpdEvent::ClientAssignment {
134                queue_id,
135                client_ip,
136                client_hostname,
137                port,
138            }));
139        }
140
141        // 2. 尝试匹配连接事件
142        if let Some(captures) = self.connect_regex.captures(message) {
143            let client_hostname = captures.get(1).unwrap().as_str().to_string();
144            let client_ip = captures.get(2).unwrap().as_str().to_string();
145            let port = captures.get(3).and_then(|m| m.as_str().parse::<u16>().ok());
146
147            return Ok(ComponentEvent::Smtpd(SmtpdEvent::Connect {
148                client_ip,
149                client_hostname,
150                port,
151            }));
152        }
153
154        // 3. 尝试匹配断开连接事件
155        if let Some(captures) = self.disconnect_regex.captures(message) {
156            let client_hostname = captures.get(1).unwrap().as_str().to_string();
157            let client_ip = captures.get(2).unwrap().as_str().to_string();
158            let port = captures.get(3).and_then(|m| m.as_str().parse::<u16>().ok());
159            let stats_part = captures.get(4).map(|m| m.as_str()).unwrap_or("");
160            
161            let command_stats = if !stats_part.is_empty() {
162                self.parse_command_stats(stats_part)
163            } else {
164                None
165            };
166
167            return Ok(ComponentEvent::Smtpd(SmtpdEvent::Disconnect {
168                client_ip,
169                client_hostname,
170                port,
171                command_stats,
172            }));
173        }
174
175        // 4. 尝试匹配连接丢失事件
176        if let Some(captures) = self.lost_connection_regex.captures(message) {
177            let last_command = Some(captures.get(1).unwrap().as_str().to_string());
178            let client_hostname = captures.get(2).unwrap().as_str().to_string();
179            let client_ip = captures.get(3).unwrap().as_str().to_string();
180
181            return Ok(ComponentEvent::Smtpd(SmtpdEvent::LostConnection {
182                client_ip,
183                client_hostname,
184                last_command,
185            }));
186        }
187
188        // 5. 尝试匹配超时事件
189        if let Some(captures) = self.timeout_regex.captures(message) {
190            let last_command = Some(captures.get(1).unwrap().as_str().to_string());
191            let client_hostname = captures.get(2).unwrap().as_str().to_string();
192            let client_ip = captures.get(3).unwrap().as_str().to_string();
193
194            return Ok(ComponentEvent::Smtpd(SmtpdEvent::Timeout {
195                client_ip,
196                client_hostname,
197                last_command,
198            }));
199        }
200
201        // 6. 尝试匹配NOQUEUE拒绝事件
202        if let Some(captures) = self.reject_noqueue_regex.captures(message) {
203            let _cmd = captures.get(1).unwrap().as_str();
204            let client_hostname = captures.get(2).unwrap().as_str().to_string();
205            let client_ip = captures.get(3).unwrap().as_str().to_string();
206            let code = captures.get(4).unwrap().as_str().parse::<u16>().ok();
207            let reason = captures.get(5).unwrap().as_str().to_string();
208            let from = captures.get(6).map(|m| m.as_str().to_string());
209            let to = captures.get(7).map(|m| m.as_str().to_string());
210
211            return Ok(ComponentEvent::Smtpd(SmtpdEvent::Reject {
212                reason,
213                code,
214                reject_type: RejectType::NoQueue,
215                from,
216                to,
217                client_ip: Some(client_ip),
218                client_hostname: Some(client_hostname),
219            }));
220        }
221
222        // 7. 尝试匹配NOQUEUE过滤器事件
223        if let Some(captures) = self.noqueue_filter_regex.captures(message) {
224            let client_hostname = captures.get(1).unwrap().as_str().to_string();
225            let client_ip = captures.get(2).unwrap().as_str().to_string();
226            let filter_info = captures.get(3).unwrap().as_str().to_string();
227
228            return Ok(ComponentEvent::Smtpd(SmtpdEvent::NoQueueFilter {
229                client_ip,
230                client_hostname,
231                filter_info: filter_info.clone(),
232                filter_target: filter_info, // 可以进一步解析
233            }));
234        }
235
236        // 8. 尝试匹配SASL认证失败事件
237        if let Some(captures) = self.sasl_auth_failure_regex.captures(message) {
238            let _client_hostname = captures.get(1).unwrap().as_str().to_string();
239            let _client_ip = captures.get(2).unwrap().as_str().to_string();
240            let method = captures.get(3).unwrap().as_str().to_string();
241            let failure_reason = Some(captures.get(4).unwrap().as_str().to_string());
242
243            return Ok(ComponentEvent::Smtpd(SmtpdEvent::Auth {
244                method,
245                username: "unknown".to_string(), // SASL失败时通常没有用户名
246                success: false,
247                failure_reason,
248            }));
249        }
250
251        // 9. 尝试匹配HELO事件
252        if let Some(captures) = self.helo_regex.captures(message) {
253            let client_hostname = Some(captures.get(1).unwrap().as_str().to_string());
254            let client_ip = Some(captures.get(2).unwrap().as_str().to_string());
255            let hostname = captures.get(3).unwrap().as_str().to_string();
256
257            return Ok(ComponentEvent::Smtpd(SmtpdEvent::Helo {
258                hostname,
259                client_ip,
260                client_hostname,
261            }));
262        }
263
264        // 10. 尝试匹配系统警告事件
265        if let Some(_captures) = self.system_warning_regex.captures(message) {
266            let warning_message = message.to_string(); // 整个消息就是警告内容
267            
268            // 提取警告类型
269            let warning_type = if warning_message.contains("dict_nis_init") {
270                "nis_config".to_string()
271            } else if warning_message.contains("dict_") {
272                "dictionary_config".to_string()
273            } else if warning_message.contains("non-SMTP command") {
274                "protocol_violation".to_string()
275            } else if warning_message.contains("Illegal address syntax") {
276                "address_syntax".to_string()
277            } else if warning_message.contains("hostname") {
278                "hostname_config".to_string()
279            } else if warning_message.contains("TLS") {
280                "tls_config".to_string()
281            } else if warning_message.contains("SASL") {
282                "sasl_config".to_string()
283            } else {
284                "general".to_string()
285            };
286
287            return Ok(ComponentEvent::Smtpd(SmtpdEvent::SystemWarning {
288                warning_type,
289                message: warning_message,
290                client_info: None, // 系统警告不包含客户端信息
291            }));
292        }
293
294        // 如果都不匹配,返回错误
295        Err(ParseError::ComponentParseError {
296            component: "smtpd".to_string(),
297            reason: format!("无法解析消息: {}", message),
298        })
299    }
300
301    fn component_name(&self) -> &'static str {
302        "smtpd"
303    }
304
305    fn can_parse(&self, message: &str) -> bool {
306        self.connect_regex.is_match(message)
307            || self.disconnect_regex.is_match(message)
308            || self.lost_connection_regex.is_match(message)
309            || self.timeout_regex.is_match(message)
310            || self.client_assignment_regex.is_match(message)
311            || self.reject_noqueue_regex.is_match(message)
312            || self.noqueue_filter_regex.is_match(message)
313            || self.sasl_auth_failure_regex.is_match(message)
314            || self.helo_regex.is_match(message)
315            || self.system_warning_regex.is_match(message)
316    }
317}
318
319impl Default for SmtpdParser {
320    fn default() -> Self {
321        Self::new()
322    }
323}