postfix_log_parser/utils/
common_fields.rs1use lazy_static::lazy_static;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9
10lazy_static! {
11 pub static ref FROM_EMAIL_REGEX: Regex = Regex::new(r"from=<([^>]*)>").unwrap();
14
15 pub static ref TO_EMAIL_REGEX: Regex = Regex::new(r"to=<([^>]+)>").unwrap();
17
18 pub static ref ORIG_TO_EMAIL_REGEX: Regex = Regex::new(r"orig_to=<([^>]+)>").unwrap();
20
21 pub static ref CLIENT_INFO_REGEX: Regex = Regex::new(r"client=([^\[]+)\[([^\]]+)\](?::(\d+))?").unwrap();
24
25 pub static ref CLIENT_SIMPLE_REGEX: Regex = Regex::new(r"([^\[]+)\[([^\]]+)\](?::(\d+))?").unwrap();
27
28 pub static ref RELAY_INFO_REGEX: Regex = Regex::new(r"relay=([^,\[\]]+)(?:\[([^\]]+)\])?(?::(\d+))?").unwrap();
31
32 pub static ref DELAY_REGEX: Regex = Regex::new(r"delay=([\d.]+)").unwrap();
35
36 pub static ref DELAYS_REGEX: Regex = Regex::new(r"delays=([\d./]+)").unwrap();
38
39 pub static ref DSN_REGEX: Regex = Regex::new(r"dsn=([\d.]+)").unwrap();
41
42 pub static ref STATUS_REGEX: Regex = Regex::new(r"status=(\w+)").unwrap();
44
45 pub static ref SIZE_REGEX: Regex = Regex::new(r"size=(\d+)").unwrap();
48
49 pub static ref NRCPT_REGEX: Regex = Regex::new(r"nrcpt=(\d+)").unwrap();
51
52 pub static ref MESSAGE_ID_REGEX: Regex = Regex::new(r"message-id=(?:<([^>]+)>|([^,\s]+))").unwrap();
54
55 pub static ref PROTO_REGEX: Regex = Regex::new(r"proto=(\w+)").unwrap();
58
59 pub static ref HELO_REGEX: Regex = Regex::new(r"helo=<([^>]+)>").unwrap();
61
62 pub static ref SASL_METHOD_REGEX: Regex = Regex::new(r"sasl_method=(\w+)").unwrap();
64
65 pub static ref SASL_USERNAME_REGEX: Regex = Regex::new(r"sasl_username=([^,\s]+)").unwrap();
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71pub struct EmailAddress {
72 pub address: String,
74 pub is_empty: bool,
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
80pub struct ClientInfo {
81 pub hostname: String,
83 pub ip: String,
85 pub port: Option<u16>,
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub struct RelayInfo {
92 pub hostname: String,
94 pub ip: Option<String>,
96 pub port: Option<u16>,
98 pub is_none: bool,
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
104pub struct DelayInfo {
105 pub total: f64,
107 pub breakdown: Option<[f64; 4]>,
109}
110
111#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
113pub struct StatusInfo {
114 pub status: String,
116 pub dsn: Option<String>,
118 pub description: Option<String>,
120}
121
122pub struct CommonFieldsParser;
124
125impl CommonFieldsParser {
126 pub fn extract_from_email(message: &str) -> Option<EmailAddress> {
128 FROM_EMAIL_REGEX.captures(message).map(|caps| {
129 let address = caps
130 .get(1)
131 .map_or(String::new(), |m| m.as_str().to_string());
132 EmailAddress {
133 is_empty: address.is_empty(),
134 address,
135 }
136 })
137 }
138
139 pub fn extract_to_email(message: &str) -> Option<EmailAddress> {
141 TO_EMAIL_REGEX.captures(message).map(|caps| {
142 let address = caps
143 .get(1)
144 .map_or(String::new(), |m| m.as_str().to_string());
145 EmailAddress {
146 is_empty: address.is_empty(),
147 address,
148 }
149 })
150 }
151
152 pub fn extract_orig_to_email(message: &str) -> Option<EmailAddress> {
154 ORIG_TO_EMAIL_REGEX.captures(message).map(|caps| {
155 let address = caps
156 .get(1)
157 .map_or(String::new(), |m| m.as_str().to_string());
158 EmailAddress {
159 is_empty: address.is_empty(),
160 address,
161 }
162 })
163 }
164
165 pub fn extract_client_info(message: &str) -> Option<ClientInfo> {
167 CLIENT_INFO_REGEX.captures(message).map(|caps| ClientInfo {
168 hostname: caps
169 .get(1)
170 .map_or(String::new(), |m| m.as_str().to_string()),
171 ip: caps
172 .get(2)
173 .map_or(String::new(), |m| m.as_str().to_string()),
174 port: caps.get(3).and_then(|m| m.as_str().parse().ok()),
175 })
176 }
177
178 pub fn extract_client_info_simple(client_str: &str) -> Option<ClientInfo> {
180 CLIENT_SIMPLE_REGEX
181 .captures(client_str)
182 .map(|caps| ClientInfo {
183 hostname: caps
184 .get(1)
185 .map_or(String::new(), |m| m.as_str().to_string()),
186 ip: caps
187 .get(2)
188 .map_or(String::new(), |m| m.as_str().to_string()),
189 port: caps.get(3).and_then(|m| m.as_str().parse().ok()),
190 })
191 }
192
193 pub fn extract_relay_info(message: &str) -> Option<RelayInfo> {
195 RELAY_INFO_REGEX.captures(message).map(|caps| {
196 let hostname = caps
197 .get(1)
198 .map_or(String::new(), |m| m.as_str().to_string());
199 let is_none = hostname == "none";
200
201 RelayInfo {
202 hostname,
203 ip: caps.get(2).map(|m| m.as_str().to_string()),
204 port: caps.get(3).and_then(|m| m.as_str().parse().ok()),
205 is_none,
206 }
207 })
208 }
209
210 pub fn extract_delay_info(message: &str) -> Option<DelayInfo> {
212 let total = DELAY_REGEX
213 .captures(message)
214 .and_then(|caps| caps.get(1))
215 .and_then(|m| m.as_str().parse().ok())?;
216
217 let breakdown = DELAYS_REGEX
218 .captures(message)
219 .and_then(|caps| caps.get(1))
220 .and_then(|m| Self::parse_delays_breakdown(m.as_str()));
221
222 Some(DelayInfo { total, breakdown })
223 }
224
225 fn parse_delays_breakdown(delays_str: &str) -> Option<[f64; 4]> {
227 let parts: Vec<&str> = delays_str.split('/').collect();
228 if parts.len() == 4 {
229 let mut breakdown = [0.0; 4];
230 for (i, part) in parts.iter().enumerate() {
231 breakdown[i] = part.parse().ok()?;
232 }
233 Some(breakdown)
234 } else {
235 None
236 }
237 }
238
239 pub fn extract_status_info(message: &str) -> Option<StatusInfo> {
241 let status = STATUS_REGEX
242 .captures(message)
243 .and_then(|caps| caps.get(1))
244 .map(|m| m.as_str().to_string())?;
245
246 let dsn = DSN_REGEX
247 .captures(message)
248 .and_then(|caps| caps.get(1))
249 .map(|m| m.as_str().to_string());
250
251 let description = if let Some(start) = message.find('(') {
253 if let Some(end) = message.rfind(')') {
254 if end > start {
255 Some(message[start + 1..end].to_string())
256 } else {
257 None
258 }
259 } else {
260 None
261 }
262 } else {
263 None
264 };
265
266 Some(StatusInfo {
267 status,
268 dsn,
269 description,
270 })
271 }
272
273 pub fn extract_size(message: &str) -> Option<u64> {
275 SIZE_REGEX
276 .captures(message)
277 .and_then(|caps| caps.get(1))
278 .and_then(|m| m.as_str().parse().ok())
279 }
280
281 pub fn extract_nrcpt(message: &str) -> Option<u32> {
283 NRCPT_REGEX
284 .captures(message)
285 .and_then(|caps| caps.get(1))
286 .and_then(|m| m.as_str().parse().ok())
287 }
288
289 pub fn extract_message_id(message: &str) -> Option<String> {
291 MESSAGE_ID_REGEX.captures(message).and_then(|caps| {
292 if let Some(bracketed) = caps.get(1) {
294 Some(bracketed.as_str().to_string())
295 }
296 else if let Some(unbracketed) = caps.get(2) {
298 Some(unbracketed.as_str().to_string())
299 } else {
300 None
301 }
302 })
303 }
304
305 pub fn extract_protocol(message: &str) -> Option<String> {
307 PROTO_REGEX
308 .captures(message)
309 .and_then(|caps| caps.get(1))
310 .map(|m| m.as_str().to_string())
311 }
312
313 pub fn extract_helo(message: &str) -> Option<String> {
315 HELO_REGEX
316 .captures(message)
317 .and_then(|caps| caps.get(1))
318 .map(|m| m.as_str().to_string())
319 }
320
321 pub fn extract_sasl_method(message: &str) -> Option<String> {
323 SASL_METHOD_REGEX
324 .captures(message)
325 .and_then(|caps| caps.get(1))
326 .map(|m| m.as_str().to_string())
327 }
328
329 pub fn extract_sasl_username(message: &str) -> Option<String> {
331 SASL_USERNAME_REGEX
332 .captures(message)
333 .and_then(|caps| caps.get(1))
334 .map(|m| m.as_str().to_string())
335 }
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_extract_from_email() {
344 let message = "4bG4VR5z: from=<sender@example.com>, size=1234";
345 let result = CommonFieldsParser::extract_from_email(message);
346 assert_eq!(
347 result,
348 Some(EmailAddress {
349 address: "sender@example.com".to_string(),
350 is_empty: false,
351 })
352 );
353
354 let bounce_message = "4bG4VR5z: from=<>, to=<user@example.com>";
356 let bounce_result = CommonFieldsParser::extract_from_email(bounce_message);
357 assert_eq!(
358 bounce_result,
359 Some(EmailAddress {
360 address: "".to_string(),
361 is_empty: true,
362 })
363 );
364 }
365
366 #[test]
367 fn test_extract_client_info() {
368 let message = "4bG4VR5z: client=mail.example.com[192.168.1.100]:25";
369 let result = CommonFieldsParser::extract_client_info(message);
370 assert_eq!(
371 result,
372 Some(ClientInfo {
373 hostname: "mail.example.com".to_string(),
374 ip: "192.168.1.100".to_string(),
375 port: Some(25),
376 })
377 );
378
379 let no_port_message = "4bG4VR5z: client=localhost[127.0.0.1]";
381 let no_port_result = CommonFieldsParser::extract_client_info(no_port_message);
382 assert_eq!(
383 no_port_result,
384 Some(ClientInfo {
385 hostname: "localhost".to_string(),
386 ip: "127.0.0.1".to_string(),
387 port: None,
388 })
389 );
390 }
391
392 #[test]
393 fn test_extract_relay_info() {
394 let message = "4bG4VR5z: to=<user@example.com>, relay=mx.example.com[1.2.3.4]:25";
395 let result = CommonFieldsParser::extract_relay_info(message);
396 assert_eq!(
397 result,
398 Some(RelayInfo {
399 hostname: "mx.example.com".to_string(),
400 ip: Some("1.2.3.4".to_string()),
401 port: Some(25),
402 is_none: false,
403 })
404 );
405
406 let none_message = "4bG4VR5z: to=<user@example.com>, relay=none, delay=0";
408 let none_result = CommonFieldsParser::extract_relay_info(none_message);
409 assert_eq!(
410 none_result,
411 Some(RelayInfo {
412 hostname: "none".to_string(),
413 ip: None,
414 port: None,
415 is_none: true,
416 })
417 );
418 }
419
420 #[test]
421 fn test_extract_delay_info() {
422 let message = "4bG4VR5z: delay=5.5, delays=1.0/0.5/3.0/1.0";
423 let result = CommonFieldsParser::extract_delay_info(message);
424 assert_eq!(
425 result,
426 Some(DelayInfo {
427 total: 5.5,
428 breakdown: Some([1.0, 0.5, 3.0, 1.0]),
429 })
430 );
431
432 let simple_message = "4bG4VR5z: delay=2.3";
434 let simple_result = CommonFieldsParser::extract_delay_info(simple_message);
435 assert_eq!(
436 simple_result,
437 Some(DelayInfo {
438 total: 2.3,
439 breakdown: None,
440 })
441 );
442 }
443
444 #[test]
445 fn test_extract_status_info() {
446 let message = "4bG4VR5z: status=sent (250 2.0.0 OK), dsn=2.0.0";
447 let result = CommonFieldsParser::extract_status_info(message);
448 assert_eq!(
449 result,
450 Some(StatusInfo {
451 status: "sent".to_string(),
452 dsn: Some("2.0.0".to_string()),
453 description: Some("250 2.0.0 OK".to_string()),
454 })
455 );
456 }
457
458 #[test]
459 fn test_extract_message_properties() {
460 let message = "4bG4VR5z: from=<sender@example.com>, size=1234, nrcpt=2";
461
462 assert_eq!(CommonFieldsParser::extract_size(message), Some(1234));
463 assert_eq!(CommonFieldsParser::extract_nrcpt(message), Some(2));
464 }
465
466 #[test]
467 fn test_extract_message_id() {
468 let bracketed_message = "61172636348059648: message-id=<61172636348059648@m01.localdomain>";
470 let bracketed_result = CommonFieldsParser::extract_message_id(bracketed_message);
471 assert_eq!(
472 bracketed_result,
473 Some("61172636348059648@m01.localdomain".to_string())
474 );
475
476 let unbracketed_message = "61172641393807360: message-id=61172636348059648@m01.localdomain";
478 let unbracketed_result = CommonFieldsParser::extract_message_id(unbracketed_message);
479 assert_eq!(
480 unbracketed_result,
481 Some("61172636348059648@m01.localdomain".to_string())
482 );
483
484 let complex_message = "4bG4VR5z: message-id=<test123@example.com>, size=456";
486 let complex_result = CommonFieldsParser::extract_message_id(complex_message);
487 assert_eq!(complex_result, Some("test123@example.com".to_string()));
488 }
489
490 #[test]
491 fn test_extract_protocol_info() {
492 let message = "4bG4VR5z: proto=ESMTP, helo=<mail.example.com>";
493
494 assert_eq!(
495 CommonFieldsParser::extract_protocol(message),
496 Some("ESMTP".to_string())
497 );
498 assert_eq!(
499 CommonFieldsParser::extract_helo(message),
500 Some("mail.example.com".to_string())
501 );
502 }
503}