postfix_log_parser/components/
smtpd.rs1use 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
50pub struct SmtpdParser {
52 connect_regex: Regex,
54 disconnect_regex: Regex,
55 lost_connection_regex: Regex,
56 timeout_regex: Regex,
57
58 client_assignment_regex: Regex,
60 noqueue_filter_regex: Regex,
61
62 sasl_auth_failure_regex: Regex,
64 reject_noqueue_regex: Regex,
65
66 helo_regex: Regex,
68
69 system_warning_regex: Regex,
71
72 command_stats_regex: Regex,
74}
75
76impl SmtpdParser {
77 pub fn new() -> Self {
80 Self {
81 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 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_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_regex: Regex::new(r"client=([^\[]+)\[([^\]]+)\], helo=<([^>]+)>").unwrap(),
97
98 system_warning_regex: Regex::new(r"^(dict_\w+|hostname \w+|non-SMTP|Illegal|\w+_init|address syntax|TLS|SASL|milter).*").unwrap(),
100
101 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 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 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 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 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 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 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 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 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, }));
234 }
235
236 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(), success: false,
247 failure_reason,
248 }));
249 }
250
251 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 if let Some(_captures) = self.system_warning_regex.captures(message) {
266 let warning_message = message.to_string(); 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, }));
292 }
293
294 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}