postfix_log_parser/components/
smtpd.rs

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