postfix_log_parser/components/
smtpd.rs1use 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
12pub struct SmtpdParser {
14 connect_regex: Regex,
16 disconnect_regex: Regex,
17 lost_connection_regex: Regex,
18 timeout_regex: Regex,
19
20 client_assignment_regex: Regex,
22 noqueue_filter_regex: Regex,
23
24 sasl_auth_failure_regex: Regex,
26 reject_noqueue_regex: Regex,
27
28 helo_regex: Regex,
30
31 system_warning_regex: Regex,
33
34 command_stats_regex: Regex,
36}
37
38impl SmtpdParser {
39 pub fn new() -> Self {
42 Self {
43 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 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_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_regex: Regex::new(r"client=([^\[]+)\[([^\]]+)\], helo=<([^>]+)>").unwrap(),
59
60 system_warning_regex: Regex::new(r"^(dict_\w+|hostname \w+|non-SMTP|Illegal|\w+_init|address syntax|TLS|SASL|milter).*").unwrap(),
62
63 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 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 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 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 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 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 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 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 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, }));
200 }
201
202 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(), success: false,
213 failure_reason,
214 }));
215 }
216
217 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 if let Some(_captures) = self.system_warning_regex.captures(message) {
232 let warning_message = message.to_string(); 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, }));
258 }
259
260 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}