]> git.proxmox.com Git - pmg-log-tracker.git/blob - src/main.rs
bump version to 2.5.0
[pmg-log-tracker.git] / src / main.rs
1 use std::cell::RefCell;
2 use std::collections::HashMap;
3 use std::ffi::CString;
4 use std::rc::{Rc, Weak};
5
6 use std::fs::File;
7 use std::io::BufRead;
8 use std::io::BufReader;
9 use std::io::BufWriter;
10 use std::io::Write;
11
12 use anyhow::{bail, Error};
13 use flate2::read;
14 use libc::time_t;
15
16 use clap::{App, Arg};
17
18 mod time;
19 use time::{Tm, CAL_MTOD};
20
21 fn main() -> Result<(), Error> {
22 let matches = App::new(clap::crate_name!())
23 .version(clap::crate_version!())
24 .about(clap::crate_description!())
25 .arg(
26 Arg::with_name("verbose")
27 .short('v')
28 .long("verbose")
29 .help("Verbose output, can be specified multiple times")
30 .action(clap::ArgAction::Count),
31 )
32 .arg(
33 Arg::with_name("inputfile")
34 .short('i')
35 .long("inputfile")
36 .help("Input file to use instead of /var/log/syslog, or '-' for stdin")
37 .value_name("INPUTFILE"),
38 )
39 .arg(
40 Arg::with_name("host")
41 .short('h')
42 .long("host")
43 .help("Hostname or Server IP")
44 .value_name("HOST"),
45 )
46 .arg(
47 Arg::with_name("from")
48 .short('f')
49 .long("from")
50 .help("Mails from SENDER")
51 .value_name("SENDER"),
52 )
53 .arg(
54 Arg::with_name("to")
55 .short('t')
56 .long("to")
57 .help("Mails to RECIPIENT")
58 .value_name("RECIPIENT"),
59 )
60 .arg(
61 Arg::with_name("start")
62 .short('s')
63 .long("starttime")
64 .help("Start time (YYYY-MM-DD HH:MM:SS) or seconds since epoch")
65 .value_name("TIME"),
66 )
67 .arg(
68 Arg::with_name("end")
69 .short('e')
70 .long("endtime")
71 .help("End time (YYYY-MM-DD HH:MM:SS) or seconds since epoch")
72 .value_name("TIME"),
73 )
74 .arg(
75 Arg::with_name("msgid")
76 .short('m')
77 .long("message-id")
78 .help("Message ID (exact match)")
79 .value_name("MSGID"),
80 )
81 .arg(
82 Arg::with_name("qids")
83 .short('q')
84 .long("queue-id")
85 .help("Queue ID (exact match), can be specified multiple times")
86 .value_name("QID")
87 .multiple(true)
88 .number_of_values(1),
89 )
90 .arg(
91 Arg::with_name("search")
92 .short('x')
93 .long("search-string")
94 .help("Search for string")
95 .value_name("STRING"),
96 )
97 .arg(
98 Arg::with_name("limit")
99 .short('l')
100 .long("limit")
101 .help("Print MAX entries")
102 .value_name("MAX")
103 .default_value("0"),
104 )
105 .arg(
106 Arg::with_name("exclude_greylist")
107 .short('g')
108 .long("exclude-greylist")
109 .help("Exclude greylist entries"),
110 )
111 .arg(
112 Arg::with_name("exclude_ndr")
113 .short('n')
114 .long("exclude-ndr")
115 .help("Exclude NDR entries"),
116 )
117 .get_matches();
118
119 let mut parser = Parser::new()?;
120 parser.handle_args(matches)?;
121
122 println!("# LogReader: {}", std::process::id());
123 println!("# Query options");
124 if !parser.options.from.is_empty() {
125 println!("# Sender: {}", parser.options.from);
126 }
127 if !parser.options.to.is_empty() {
128 println!("# Recipient: {}", parser.options.to);
129 }
130 if !parser.options.host.is_empty() {
131 println!("# Server: {}", parser.options.host);
132 }
133 if !parser.options.msgid.is_empty() {
134 println!("# MsgID: {}", parser.options.msgid);
135 }
136 for m in parser.options.match_list.iter() {
137 match m {
138 Match::Qid(b) => println!("# QID: {}", std::str::from_utf8(b)?),
139 Match::RelLineNr(t, l) => println!("# QID: T{:8X}L{:08X}", *t, *l as u32),
140 }
141 }
142
143 if !parser.options.string_match.is_empty() {
144 println!("# Match: {}", parser.options.string_match);
145 }
146
147 println!(
148 "# Start: {} ({})",
149 time::strftime(c_str!("%F %T"), &parser.start_tm)?,
150 parser.options.start
151 );
152 println!(
153 "# End: {} ({})",
154 time::strftime(c_str!("%F %T"), &parser.end_tm)?,
155 parser.options.end
156 );
157
158 println!("# End Query Options\n");
159 parser.parse_files()?;
160
161 Ok(())
162 }
163
164 // handle log entries for service 'pmg-smtp-filter'
165 // we match 4 entries, all beginning with a QID
166 // accept mail, move mail, block mail and the processing time
167 fn handle_pmg_smtp_filter_message(msg: &[u8], parser: &mut Parser, complete_line: &[u8]) {
168 let (qid, data) = match parse_qid(msg, 25) {
169 Some((q, m)) => (q, m),
170 None => return,
171 };
172 // skip ': ' following the QID
173 let data = &data[2..];
174
175 let fe = get_or_create_fentry(&mut parser.fentries, qid);
176
177 if parser.string_match {
178 fe.borrow_mut().string_match = parser.string_match;
179 }
180
181 fe.borrow_mut()
182 .log
183 .push((complete_line.into(), parser.lines));
184
185 // we're interested in the 'to' address and the QID when we accept the mail
186 if data.starts_with(b"accept mail to <") {
187 let data = &data[16..];
188 let to_count = data.iter().take_while(|b| (**b as char) != '>').count();
189 let (to, data) = data.split_at(to_count);
190 if !data.starts_with(b"> (") {
191 return;
192 }
193 let data = &data[3..];
194 let qid_count = data.iter().take_while(|b| (**b as char) != ')').count();
195 let qid = &data[..qid_count];
196
197 // add a ToEntry with the DStatus 'Accept' to the FEntry
198 fe.borrow_mut()
199 .add_accept(to, qid, parser.current_record_state.timestamp);
200
201 // if there's a QEntry with the qid and it's not yet filtered
202 // set it to before-queue filtered
203 if let Some(qe) = parser.qentries.get(qid) {
204 if !qe.borrow().filtered {
205 qe.borrow_mut().bq_filtered = true;
206 qe.borrow_mut().filter = Some(Rc::clone(&fe));
207 fe.borrow_mut().qentry = Some(Rc::downgrade(qe));
208 }
209 }
210
211 return;
212 }
213
214 // same as for the 'accept' case, we're interested in both the 'to'
215 // address as well as the QID
216 if data.starts_with(b"moved mail for <") {
217 let data = &data[16..];
218 let to_count = data.iter().take_while(|b| (**b as char) != '>').count();
219 let (to, data) = data.split_at(to_count);
220
221 let qid_index = match find(data, b"quarantine - ") {
222 Some(i) => i,
223 None => return,
224 };
225 let data = &data[qid_index + 13..];
226 let (qid, _) = match parse_qid(data, 25) {
227 Some(t) => t,
228 None => return,
229 };
230
231 // add a ToEntry with the DStatus 'Quarantine' to the FEntry
232 fe.borrow_mut()
233 .add_quarantine(to, qid, parser.current_record_state.timestamp);
234 return;
235 }
236
237 // in the 'block' case we're only interested in the 'to' address, there's
238 // no queue for these mails
239 if data.starts_with(b"block mail to <") {
240 let data = &data[15..];
241 let to_count = data.iter().take_while(|b| (**b as char) != '>').count();
242 let to = &data[..to_count];
243
244 fe.borrow_mut()
245 .add_block(to, parser.current_record_state.timestamp);
246 return;
247 }
248
249 // here the pmg-smtp-filter is finished and we get the processing time
250 if data.starts_with(b"processing time: ") {
251 let data = &data[17..];
252 let time_count = data.iter().take_while(|b| !b.is_ascii_whitespace()).count();
253 let time = &data[..time_count];
254
255 fe.borrow_mut().set_processing_time(time);
256 }
257 }
258
259 // handle log entries for postscreen
260 // here only the NOQUEUE: reject is of interest
261 // these are the mails that were rejected before even entering the smtpd by
262 // e.g. checking DNSBL sites
263 fn handle_postscreen_message(msg: &[u8], parser: &mut Parser, complete_line: &[u8]) {
264 if !msg.starts_with(b"NOQUEUE: reject: RCPT from ") {
265 return;
266 }
267 // skip the string from above
268 let data = &msg[27..];
269 let client_index = match find(data, b"; client [") {
270 Some(i) => i,
271 None => return,
272 };
273 let data = &data[client_index + 10..];
274
275 let client_count = data.iter().take_while(|b| (**b as char) != ']').count();
276 let (client, data) = data.split_at(client_count);
277
278 let from_index = match find(data, b"; from=<") {
279 Some(i) => i,
280 None => return,
281 };
282 let data = &data[from_index + 8..];
283
284 let from_count = data.iter().take_while(|b| (**b as char) != '>').count();
285 let (from, data) = data.split_at(from_count);
286
287 if !data.starts_with(b">, to=<") {
288 return;
289 }
290 let data = &data[7..];
291 let to_count = data.iter().take_while(|b| (**b as char) != '>').count();
292 let to = &data[..to_count];
293
294 let se = get_or_create_sentry(
295 &mut parser.sentries,
296 parser.current_record_state.pid,
297 parser.rel_line_nr,
298 parser.current_record_state.timestamp,
299 );
300
301 if parser.string_match {
302 se.borrow_mut().string_match = parser.string_match;
303 }
304
305 se.borrow_mut()
306 .log
307 .push((complete_line.into(), parser.lines));
308 // for postscreeen noqueue log entries we add a NoqueueEntry to the SEntry
309 se.borrow_mut().add_noqueue_entry(
310 from,
311 to,
312 DStatus::Noqueue,
313 parser.current_record_state.timestamp,
314 );
315 // set the connecting client
316 se.borrow_mut().set_connect(client);
317 // as there's no more service involved after the postscreen noqueue entry,
318 // we set it to disconnected and print it
319 se.borrow_mut().disconnected = true;
320 se.borrow_mut().print(parser);
321 parser.free_sentry(parser.current_record_state.pid);
322 }
323
324 // handle log entries for 'qmgr'
325 // these only appear in the 'after-queue filter' case or when the mail is
326 // 'accepted' in the 'before-queue filter' case
327 fn handle_qmgr_message(msg: &[u8], parser: &mut Parser, complete_line: &[u8]) {
328 let (qid, data) = match parse_qid(msg, 15) {
329 Some(t) => t,
330 None => return,
331 };
332 let data = &data[2..];
333
334 let qe = get_or_create_qentry(&mut parser.qentries, qid);
335
336 if parser.string_match {
337 qe.borrow_mut().string_match = parser.string_match;
338 }
339 qe.borrow_mut().cleanup = true;
340 qe.borrow_mut()
341 .log
342 .push((complete_line.into(), parser.lines));
343
344 // we parse 2 log entries, either one with a 'from' and a 'size' or one
345 // that signals that the mail has been removed from the queue (after an
346 // action was taken, e.g. accept, by the filter)
347 if data.starts_with(b"from=<") {
348 let data = &data[6..];
349
350 let from_count = data.iter().take_while(|b| (**b as char) != '>').count();
351 let (from, data) = data.split_at(from_count);
352
353 if !data.starts_with(b">, size=") {
354 return;
355 }
356 let data = &data[8..];
357
358 let size_count = data
359 .iter()
360 .take_while(|b| (**b as char).is_ascii_digit())
361 .count();
362 let (size, _) = data.split_at(size_count);
363 qe.borrow_mut().from = from.into();
364 // it is safe here because we had a check before that limits it to just
365 // ascii digits which is valid utf8
366 qe.borrow_mut().size = unsafe { std::str::from_utf8_unchecked(size) }
367 .parse()
368 .unwrap();
369 } else if data == b"removed" {
370 qe.borrow_mut().removed = true;
371 qe.borrow_mut().finalize(parser);
372 }
373 }
374
375 // handle log entries for 'lmtp', 'smtp', 'error' and 'local'
376 fn handle_lmtp_message(msg: &[u8], parser: &mut Parser, complete_line: &[u8]) {
377 if msg.starts_with(b"Trusted TLS connection established to")
378 || msg.starts_with(b"Untrusted TLS connection established to")
379 {
380 // the only way to match outgoing TLS connections is by smtp pid
381 // this message has to appear before the 'qmgr: <QID>: removed' entry in the log
382 parser.smtp_tls_log_by_pid.insert(
383 parser.current_record_state.pid,
384 (complete_line.into(), parser.lines),
385 );
386 return;
387 }
388
389 let (qid, data) = match parse_qid(msg, 15) {
390 Some((q, t)) => (q, t),
391 None => return,
392 };
393
394 let qe = get_or_create_qentry(&mut parser.qentries, qid);
395
396 if parser.string_match {
397 qe.borrow_mut().string_match = parser.string_match;
398 }
399 qe.borrow_mut().cleanup = true;
400 qe.borrow_mut()
401 .log
402 .push((complete_line.into(), parser.lines));
403
404 // assume the TLS log entry always appears before as it is the same process
405 if let Some(log_line) = parser
406 .smtp_tls_log_by_pid
407 .remove(&parser.current_record_state.pid)
408 {
409 qe.borrow_mut().log.push(log_line);
410 }
411
412 let data = &data[2..];
413 if !data.starts_with(b"to=<") {
414 return;
415 }
416 let data = &data[4..];
417 let to_count = data.iter().take_while(|b| (**b as char) != '>').count();
418 let (to, data) = data.split_at(to_count);
419
420 let relay_index = match find(data, b"relay=") {
421 Some(i) => i,
422 None => return,
423 };
424 let data = &data[relay_index + 6..];
425 let relay_count = data.iter().take_while(|b| (**b as char) != ',').count();
426 let (relay, data) = data.split_at(relay_count);
427
428 // parse the DSN (indicates the delivery status, e.g. 2 == success)
429 // ignore everything after the first digit
430 let dsn_index = match find(data, b"dsn=") {
431 Some(i) => i,
432 None => return,
433 };
434 let data = &data[dsn_index + 4..];
435 let dsn = match data.iter().next() {
436 Some(b) => {
437 if b.is_ascii_digit() {
438 (*b as char).to_digit(10).unwrap()
439 } else {
440 return;
441 }
442 }
443 None => return,
444 };
445
446 let dstatus = DStatus::Dsn(dsn);
447
448 qe.borrow_mut()
449 .add_to_entry(to, relay, dstatus, parser.current_record_state.timestamp);
450
451 // here the match happens between a QEntry and the corresponding FEntry
452 // (only after-queue)
453 if &*parser.current_record_state.service == b"postfix/lmtp" {
454 let sent_index = match find(data, b"status=sent (250 2.") {
455 Some(i) => i,
456 None => return,
457 };
458 let mut data = &data[sent_index + 19..];
459 if data.starts_with(b"5.0 OK") {
460 data = &data[8..];
461 } else if data.starts_with(b"7.0 BLOCKED") {
462 data = &data[13..];
463 } else {
464 return;
465 }
466
467 // this is the QID of the associated pmg-smtp-filter
468 let (qid, _) = match parse_qid(data, 25) {
469 Some(t) => t,
470 None => return,
471 };
472
473 // add a reference to the filter
474 qe.borrow_mut().filtered = true;
475
476 // if there's a FEntry with the filter QID, check to see if its
477 // qentry matches this one
478 if let Some(fe) = parser.fentries.get(qid) {
479 qe.borrow_mut().filter = Some(Rc::clone(fe));
480 // if we use fe.borrow().qentry() directly we run into a borrow
481 // issue at runtime
482 let q = fe.borrow().qentry();
483 if let Some(q) = q {
484 if !Rc::ptr_eq(&q, &qe) {
485 // QEntries don't match, set all flags to false and
486 // remove the referenced FEntry
487 q.borrow_mut().filtered = false;
488 q.borrow_mut().bq_filtered = false;
489 q.borrow_mut().filter = None;
490 // update FEntry's QEntry reference to the new one
491 fe.borrow_mut().qentry = Some(Rc::downgrade(&qe));
492 }
493 }
494 }
495 }
496 }
497
498 // handle log entries for 'smtpd'
499 fn handle_smtpd_message(msg: &[u8], parser: &mut Parser, complete_line: &[u8]) {
500 let se = get_or_create_sentry(
501 &mut parser.sentries,
502 parser.current_record_state.pid,
503 parser.rel_line_nr,
504 parser.current_record_state.timestamp,
505 );
506
507 if parser.string_match {
508 se.borrow_mut().string_match = parser.string_match;
509 }
510 se.borrow_mut()
511 .log
512 .push((complete_line.into(), parser.lines));
513
514 if msg.starts_with(b"connect from ") {
515 let addr = &msg[13..];
516 // set the client address
517 se.borrow_mut().set_connect(addr);
518 return;
519 }
520
521 // on disconnect we can finalize and print the SEntry
522 if msg.starts_with(b"disconnect from") {
523 parser.sentries.remove(&parser.current_record_state.pid);
524 se.borrow_mut().disconnected = true;
525
526 if se.borrow_mut().remove_unneeded_refs(parser) == 0 {
527 // no QEntries referenced in SEntry so just print the SEntry
528 se.borrow_mut().print(parser);
529 // free the referenced FEntry (only happens with before-queue)
530 if let Some(f) = &se.borrow().filter() {
531 parser.free_fentry(&f.borrow().logid);
532 }
533 parser.free_sentry(se.borrow().pid);
534 } else {
535 se.borrow_mut().finalize_refs(parser);
536 }
537 return;
538 }
539
540 // NOQUEUE in smtpd, happens after postscreen
541 if msg.starts_with(b"NOQUEUE:") {
542 let data = &msg[8..];
543 let colon_index = match find(data, b":") {
544 Some(i) => i,
545 None => return,
546 };
547 let data = &data[colon_index + 1..];
548 let colon_index = match find(data, b":") {
549 Some(i) => i,
550 None => return,
551 };
552 let data = &data[colon_index + 1..];
553 let semicolon_index = match find(data, b";") {
554 Some(i) => i,
555 None => return,
556 };
557
558 // check for the string, if it matches then greylisting is the reason
559 // for the NOQUEUE entry
560 let (grey, data) = data.split_at(semicolon_index);
561 let dstatus = if find(
562 grey,
563 b"Recipient address rejected: Service is unavailable (try later)",
564 )
565 .is_some()
566 {
567 DStatus::Greylist
568 } else {
569 DStatus::Noqueue
570 };
571
572 if !data.starts_with(b"; from=<") {
573 return;
574 }
575 let data = &data[8..];
576 let from_count = data.iter().take_while(|b| (**b as char) != '>').count();
577 let (from, data) = data.split_at(from_count);
578
579 if !data.starts_with(b"> to=<") {
580 return;
581 }
582 let data = &data[6..];
583 let to_count = data.iter().take_while(|b| (**b as char) != '>').count();
584 let to = &data[..to_count];
585
586 se.borrow_mut()
587 .add_noqueue_entry(from, to, dstatus, parser.current_record_state.timestamp);
588 return;
589 }
590
591 // only happens with before-queue
592 // here we can match the pmg-smtp-filter
593 // 'proxy-accept' happens if it is accepted for AT LEAST ONE receiver
594 if msg.starts_with(b"proxy-accept: ") {
595 let data = &msg[14..];
596 if !data.starts_with(b"END-OF-MESSAGE: ") {
597 return;
598 }
599 let data = &data[16..];
600 if !data.starts_with(b"250 2.5.0 OK (") {
601 return;
602 }
603 let data = &data[14..];
604 if let Some((qid, data)) = parse_qid(data, 25) {
605 let fe = get_or_create_fentry(&mut parser.fentries, qid);
606 // set the FEntry to before-queue filtered
607 fe.borrow_mut().is_bq = true;
608 // if there's no 'accept mail to' entry because of quarantine
609 // we have to match the pmg-smtp-filter here
610 // for 'accepted' mails it is matched in the 'accept mail to'
611 // log entry
612 if !fe.borrow().is_accepted {
613 // set the SEntry filter reference as we don't have a QEntry
614 // in this case
615 se.borrow_mut().filter = Some(Rc::downgrade(&fe));
616
617 if let Some(from_index) = find(data, b"from=<") {
618 let data = &data[from_index + 6..];
619 let from_count = data.iter().take_while(|b| (**b as char) != '>').count();
620 let from = &data[..from_count];
621 // keep the from for later printing
622 // required for the correct 'TO:{}:{}...' syntax required
623 // by PMG/API2/MailTracker.pm
624 se.borrow_mut().bq_from = from.into();
625 }
626 } else if let Some(qe) = &fe.borrow().qentry() {
627 // mail is 'accepted', add a reference to the QEntry to the
628 // SEntry so we can wait for all to be finished before printing
629 qe.borrow_mut().bq_sentry = Some(Rc::clone(&se));
630 SEntry::add_ref(&se, qe, true);
631 }
632 // specify that before queue filtering is used and the mail was
633 // accepted, but not necessarily by an 'accept' rule
634 // (e.g. quarantine)
635 se.borrow_mut().is_bq_accepted = true;
636 }
637
638 return;
639 }
640
641 // before queue filtering and rejected, here we can match the
642 // pmg-smtp-filter same as in the 'proxy-accept' case
643 // only happens if the mail was rejected for ALL receivers, otherwise
644 // a 'proxy-accept' happens
645 if msg.starts_with(b"proxy-reject: ") {
646 let data = &msg[14..];
647 if !data.starts_with(b"END-OF-MESSAGE: ") {
648 return;
649 }
650 let data = &data[16..];
651
652 // specify that before queue filtering is used and the mail
653 // was rejected for all receivers
654 se.borrow_mut().is_bq_rejected = true;
655
656 if let Some(qid_index) = find(data, b"(") {
657 let data = &data[qid_index + 1..];
658 if let Some((qid, _)) = parse_qid(data, 25) {
659 let fe = get_or_create_fentry(&mut parser.fentries, qid);
660 // set the FEntry to before-queue filtered
661 fe.borrow_mut().is_bq = true;
662 // we never have a QEntry in this case, so just set the SEntry
663 // filter reference
664 se.borrow_mut().filter = Some(Rc::downgrade(&fe));
665 if let Some(from_index) = find(data, b"from=<") {
666 let data = &data[from_index + 6..];
667 let from_count = data.iter().take_while(|b| (**b as char) != '>').count();
668 let from = &data[..from_count];
669 // same as for 'proxy-accept' above
670 se.borrow_mut().bq_from = from.into();
671 }
672 }
673 } else if let Some(from_index) = find(data, b"from=<") {
674 let data = &data[from_index + 6..];
675 let from_count = data.iter().take_while(|b| (**b as char) != '>').count();
676 let from = &data[..from_count];
677 // same as for 'proxy-accept' above
678 se.borrow_mut().bq_from = from.into();
679
680 if let Some(to_index) = find(data, b"to=<") {
681 let data = &data[to_index + 4..];
682 let to_count = data.iter().take_while(|b| (**b as char) != '>').count();
683 let to = &data[..to_count];
684
685 se.borrow_mut().add_noqueue_entry(
686 from,
687 to,
688 DStatus::Noqueue,
689 parser.current_record_state.timestamp,
690 );
691 };
692 }
693
694 return;
695 }
696
697 // with none of the other messages matching, we try for a QID to match the
698 // corresponding QEntry to the SEntry
699 let (qid, data) = match parse_qid(msg, 15) {
700 Some(t) => t,
701 None => return,
702 };
703 let data = &data[2..];
704
705 let qe = get_or_create_qentry(&mut parser.qentries, qid);
706
707 if parser.string_match {
708 qe.borrow_mut().string_match = parser.string_match;
709 }
710
711 SEntry::add_ref(&se, &qe, false);
712
713 if !data.starts_with(b"client=") {
714 return;
715 }
716 let data = &data[7..];
717 let client_count = data
718 .iter()
719 .take_while(|b| !(**b as char).is_ascii_whitespace())
720 .count();
721 let client = &data[..client_count];
722
723 qe.borrow_mut().set_client(client);
724 }
725
726 // handle log entries for 'cleanup'
727 // happens before the mail is passed to qmgr (after-queue or before-queue
728 // accepted only)
729 fn handle_cleanup_message(msg: &[u8], parser: &mut Parser, complete_line: &[u8]) {
730 let (qid, data) = match parse_qid(msg, 15) {
731 Some(t) => t,
732 None => return,
733 };
734 let data = &data[2..];
735
736 let qe = get_or_create_qentry(&mut parser.qentries, qid);
737
738 if parser.string_match {
739 qe.borrow_mut().string_match = parser.string_match;
740 }
741 qe.borrow_mut()
742 .log
743 .push((complete_line.into(), parser.lines));
744
745 if !data.starts_with(b"message-id=") {
746 return;
747 }
748 let data = &data[11..];
749 let msgid_count = data
750 .iter()
751 .take_while(|b| !(**b as char).is_ascii_whitespace())
752 .count();
753 let msgid = &data[..msgid_count];
754
755 if !msgid.is_empty() {
756 if qe.borrow().msgid.is_empty() {
757 qe.borrow_mut().msgid = msgid.into();
758 }
759 qe.borrow_mut().cleanup = true;
760
761 // does not work correctly if there's a duplicate message id in the logfiles
762 if let Some(q) = parser.msgid_lookup.remove(msgid) {
763 let q_clone = Weak::clone(&q);
764 if let Some(q) = q.upgrade() {
765 // check to make sure it's not the same QEntry
766 // this can happen if the cleanup line is duplicated in the log
767 if Rc::ptr_eq(&q, &qe) {
768 parser.msgid_lookup.insert(msgid.into(), q_clone);
769 } else {
770 qe.borrow_mut().aq_qentry = Some(q_clone);
771 q.borrow_mut().aq_qentry = Some(Rc::downgrade(&qe));
772 }
773 }
774 } else {
775 parser.msgid_lookup.insert(msgid.into(), Rc::downgrade(&qe));
776 }
777 }
778 }
779
780 #[derive(Default, Debug)]
781 struct NoqueueEntry {
782 from: Box<[u8]>,
783 to: Box<[u8]>,
784 dstatus: DStatus,
785 timestamp: time_t,
786 }
787
788 #[derive(Debug)]
789 struct ToEntry {
790 to: Box<[u8]>,
791 relay: Box<[u8]>,
792 dstatus: DStatus,
793 timestamp: time_t,
794 }
795
796 impl Default for ToEntry {
797 fn default() -> Self {
798 ToEntry {
799 to: Default::default(),
800 relay: (&b"none"[..]).into(),
801 dstatus: Default::default(),
802 timestamp: Default::default(),
803 }
804 }
805 }
806
807 #[derive(Debug, PartialEq, Copy, Clone)]
808 enum DStatus {
809 Invalid,
810 Accept,
811 Quarantine,
812 Block,
813 Greylist,
814 Noqueue,
815 BqPass,
816 BqDefer,
817 BqReject,
818 Dsn(u32),
819 }
820
821 impl Default for DStatus {
822 fn default() -> Self {
823 DStatus::Invalid
824 }
825 }
826
827 impl std::fmt::Display for DStatus {
828 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
829 let c = match self {
830 DStatus::Invalid => '\0', // other default
831 DStatus::Accept => 'A',
832 DStatus::Quarantine => 'Q',
833 DStatus::Block => 'B',
834 DStatus::Greylist => 'G',
835 DStatus::Noqueue => 'N',
836 DStatus::BqPass => 'P',
837 DStatus::BqDefer => 'D',
838 DStatus::BqReject => 'R',
839 DStatus::Dsn(v) => std::char::from_digit(*v, 10).unwrap(),
840 };
841 write!(f, "{}", c)
842 }
843 }
844
845 #[derive(Debug, Default)]
846 struct SEntry {
847 log: Vec<(Box<[u8]>, u64)>,
848 connect: Box<[u8]>,
849 pid: u64,
850 // references to QEntries, Weak so they are not kept alive longer than
851 // necessary, RefCell for mutability (Rc<> is immutable)
852 refs: Vec<Weak<RefCell<QEntry>>>,
853 nq_entries: Vec<NoqueueEntry>,
854 disconnected: bool,
855 // only set in case of before queue filtering
856 // used as a fallback in case no QEntry is referenced
857 filter: Option<Weak<RefCell<FEntry>>>,
858 string_match: bool,
859 timestamp: time_t,
860 rel_line_nr: u64,
861 // before queue filtering with the mail accepted for at least one receiver
862 is_bq_accepted: bool,
863 // before queue filtering with the mail rejected for all receivers
864 is_bq_rejected: bool,
865 // from address saved for compatibility with after queue filtering
866 bq_from: Box<[u8]>,
867 }
868
869 impl SEntry {
870 fn add_noqueue_entry(&mut self, from: &[u8], to: &[u8], dstatus: DStatus, timestamp: time_t) {
871 let ne = NoqueueEntry {
872 to: to.into(),
873 from: from.into(),
874 dstatus,
875 timestamp,
876 };
877 self.nq_entries.push(ne);
878 }
879
880 fn set_connect(&mut self, client: &[u8]) {
881 if self.connect.is_empty() {
882 self.connect = client.into();
883 }
884 }
885
886 // if either 'from' or 'to' are set, check if it matches, if not, set
887 // the status of the noqueue entry to Invalid
888 // if exclude_greylist or exclude_ndr are set, check if it matches
889 // and if so, set the status to Invalid so they are no longer included
890 // don't print if any Invalid entry is found
891 fn filter_matches(&mut self, parser: &Parser) -> bool {
892 if !parser.options.from.is_empty()
893 || !parser.options.to.is_empty()
894 || parser.options.exclude_greylist
895 || parser.options.exclude_ndr
896 {
897 let mut found = false;
898 for nq in self.nq_entries.iter_mut().rev() {
899 if (!parser.options.from.is_empty()
900 && find_lowercase(&nq.from, parser.options.from.as_bytes()).is_none())
901 || (parser.options.exclude_greylist && nq.dstatus == DStatus::Greylist)
902 || (parser.options.exclude_ndr && nq.from.is_empty())
903 || (!parser.options.to.is_empty()
904 && ((!nq.to.is_empty()
905 && find_lowercase(&nq.to, parser.options.to.as_bytes()).is_none())
906 || nq.to.is_empty()))
907 {
908 nq.dstatus = DStatus::Invalid;
909 }
910
911 if nq.dstatus != DStatus::Invalid {
912 found = true;
913 }
914 }
915
916 // we can early exit the printing if there's no valid Noqueue entry
917 // and we're in the after-queue case
918 if !found && self.filter.is_none() {
919 return false;
920 }
921
922 // self.filter only contains an object in the before-queue case
923 // as we have the FEntry referenced in the SEntry when there's no
924 // queue involved, we can't just check the Noqueue entries, but
925 // have to check for a filter and if it exists, we have to check
926 // them for matching 'from' and 'to' if either of those options
927 // are set.
928 // if neither of them is filtered, we can skip this check
929 if let Some(fe) = &self.filter() {
930 if !parser.options.from.is_empty()
931 && find_lowercase(&self.bq_from, parser.options.from.as_bytes()).is_none()
932 {
933 return false;
934 }
935 let to_option_set = !parser.options.to.is_empty();
936 if to_option_set && fe.borrow().is_bq && !fe.borrow().is_accepted {
937 fe.borrow_mut().to_entries.retain(|to| {
938 if find_lowercase(&to.to, parser.options.to.as_bytes()).is_some() {
939 found = true;
940 return true;
941 }
942 false
943 });
944 if !found {
945 return false;
946 }
947 }
948 }
949 }
950 true
951 }
952
953 fn print(&mut self, parser: &mut Parser) {
954 // don't print if the output is filtered by the message-id
955 // the message-id is only available in a QEntry
956 if !parser.options.msgid.is_empty() {
957 return;
958 }
959
960 // don't print if the output is filtered by a host but the connect
961 // field is empty or does not match
962 if !parser.options.host.is_empty() {
963 if self.connect.is_empty() {
964 return;
965 }
966 if find_lowercase(&self.connect, parser.options.host.as_bytes()).is_none() {
967 return;
968 }
969 }
970
971 // don't print if the output is filtered by time and line number
972 // and none match
973 if !parser.options.match_list.is_empty() {
974 let mut found = false;
975 for m in parser.options.match_list.iter() {
976 match m {
977 Match::Qid(_) => return,
978 Match::RelLineNr(t, l) => {
979 if *t == self.timestamp && *l == self.rel_line_nr {
980 found = true;
981 break;
982 }
983 }
984 }
985 }
986 if !found {
987 return;
988 }
989 }
990
991 if !self.filter_matches(parser) {
992 return;
993 }
994
995 // don't print if there's a string match specified, but none of the log entries matches.
996 // in the before-queue case we also have to check the attached filter for a match
997 if !parser.options.string_match.is_empty() {
998 if let Some(fe) = &self.filter() {
999 if !self.string_match && !fe.borrow().string_match {
1000 return;
1001 }
1002 } else if !self.string_match {
1003 return;
1004 }
1005 }
1006
1007 if parser.options.verbose > 0 {
1008 parser.write_all_ok(format!(
1009 "SMTPD: T{:8X}L{:08X}\n",
1010 self.timestamp, self.rel_line_nr as u32
1011 ));
1012 parser.write_all_ok(format!("CTIME: {:8X}\n", parser.ctime).as_bytes());
1013
1014 if !self.connect.is_empty() {
1015 parser.write_all_ok(b"CLIENT: ");
1016 parser.write_all_ok(&self.connect);
1017 parser.write_all_ok(b"\n");
1018 }
1019 }
1020
1021 // only print the entry if the status is not invalid
1022 // rev() for compatibility with the C code which uses a linked list
1023 // that adds entries at the front, while a Vec in Rust adds it at the
1024 // back
1025 for nq in self.nq_entries.iter().rev() {
1026 if nq.dstatus != DStatus::Invalid {
1027 parser.write_all_ok(format!(
1028 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
1029 nq.timestamp, self.timestamp, self.rel_line_nr, nq.dstatus,
1030 ));
1031 parser.write_all_ok(&nq.from);
1032 parser.write_all_ok(b"> to <");
1033 parser.write_all_ok(&nq.to);
1034 parser.write_all_ok(b">\n");
1035 parser.count += 1;
1036 }
1037 }
1038
1039 let print_filter_to_entries_fn =
1040 |fe: &Rc<RefCell<FEntry>>, parser: &mut Parser, se: &SEntry| {
1041 for to in fe.borrow().to_entries.iter().rev() {
1042 parser.write_all_ok(format!(
1043 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
1044 to.timestamp, se.timestamp, se.rel_line_nr, to.dstatus,
1045 ));
1046 parser.write_all_ok(&se.bq_from);
1047 parser.write_all_ok(b"> to <");
1048 parser.write_all_ok(&to.to);
1049 parser.write_all_ok(b">\n");
1050 parser.count += 1;
1051 }
1052 };
1053
1054 // only true in before queue filtering case
1055 if let Some(fe) = &self.filter() {
1056 // limited to !fe.is_accepted because otherwise we would have
1057 // a QEntry with all required information instead
1058 if fe.borrow().is_bq
1059 && !fe.borrow().is_accepted
1060 && (self.is_bq_accepted || self.is_bq_rejected)
1061 {
1062 print_filter_to_entries_fn(fe, parser, self);
1063 }
1064 }
1065
1066 let print_log = |parser: &mut Parser, logs: &Vec<(Box<[u8]>, u64)>| {
1067 for (log, line) in logs.iter() {
1068 parser.write_all_ok(format!("L{:08X} ", *line as u32));
1069 parser.write_all_ok(log);
1070 parser.write_all_ok(b"\n");
1071 }
1072 };
1073
1074 // if '-vv' is passed to the log tracker, print all the logs
1075 if parser.options.verbose > 1 {
1076 parser.write_all_ok(b"LOGS:\n");
1077 let mut logs = self.log.clone();
1078 if let Some(f) = &self.filter() {
1079 logs.append(&mut f.borrow().log.clone());
1080 // as the logs come from 1 SEntry and 1 FEntry,
1081 // interleave them via sort based on line number
1082 logs.sort_by(|a, b| a.1.cmp(&b.1));
1083 }
1084
1085 print_log(parser, &logs);
1086 }
1087 parser.write_all_ok(b"\n");
1088 }
1089
1090 fn delete_ref(&mut self, qentry: &Rc<RefCell<QEntry>>) {
1091 self.refs.retain(|q| {
1092 let q = match q.upgrade() {
1093 Some(q) => q,
1094 None => return false,
1095 };
1096 if Rc::ptr_eq(&q, qentry) {
1097 return false;
1098 }
1099 true
1100 });
1101 }
1102
1103 fn remove_unneeded_refs(&mut self, parser: &mut Parser) -> u32 {
1104 let mut count: u32 = 0;
1105 let mut to_delete = Vec::new();
1106 self.refs.retain(|q| {
1107 let q = match q.upgrade() {
1108 Some(q) => q,
1109 None => return false,
1110 };
1111 let is_cleanup = q.borrow().cleanup;
1112 // add those that require freeing to a separate Vec as self is
1113 // borrowed mutable here and can't be borrowed again for the
1114 // parser.free_qentry() call
1115 if !is_cleanup {
1116 to_delete.push(q);
1117 false
1118 } else {
1119 count += 1;
1120 true
1121 }
1122 });
1123
1124 for q in to_delete.iter().rev() {
1125 parser.free_qentry(&q.borrow().qid, Some(self));
1126 }
1127 count
1128 }
1129
1130 // print and free all QEntries that are removed and if a filter is set,
1131 // if the filter is finished
1132 fn finalize_refs(&mut self, parser: &mut Parser) {
1133 let mut qentries = Vec::new();
1134 for q in self.refs.iter() {
1135 let q = match q.upgrade() {
1136 Some(q) => q,
1137 None => continue,
1138 };
1139
1140 if !q.borrow().removed {
1141 continue;
1142 }
1143
1144 let fe = &q.borrow().filter;
1145 if let Some(f) = fe {
1146 if !q.borrow().bq_filtered && !f.borrow().finished {
1147 continue;
1148 }
1149 }
1150
1151 if !self.is_bq_accepted && q.borrow().bq_sentry.is_some() {
1152 if let Some(se) = &q.borrow().bq_sentry {
1153 // we're already disconnected, but the SEntry referenced
1154 // by the QEntry might not yet be done
1155 if !se.borrow().disconnected {
1156 // add a reference to the SEntry referenced by the
1157 // QEntry so it gets deleted when both the SEntry
1158 // and the QEntry is done
1159 Self::add_ref(se, &q, true);
1160 continue;
1161 }
1162 }
1163 }
1164
1165 qentries.push(Rc::clone(&q));
1166 }
1167
1168 for q in qentries.iter().rev() {
1169 q.borrow_mut().print(parser, Some(self));
1170 parser.free_qentry(&q.borrow().qid, Some(self));
1171
1172 if let Some(f) = &q.borrow().filter {
1173 parser.free_fentry(&f.borrow().logid);
1174 }
1175 }
1176 }
1177
1178 fn add_ref(sentry: &Rc<RefCell<SEntry>>, qentry: &Rc<RefCell<QEntry>>, bq: bool) {
1179 let smtpd = qentry.borrow().smtpd.clone();
1180 if !bq {
1181 if let Some(s) = smtpd {
1182 if !Rc::ptr_eq(sentry, &s) {
1183 eprintln!("Error: qentry ref already set");
1184 }
1185 return;
1186 }
1187 }
1188
1189 for q in sentry.borrow().refs.iter() {
1190 let q = match q.upgrade() {
1191 Some(q) => q,
1192 None => continue,
1193 };
1194 if Rc::ptr_eq(&q, qentry) {
1195 return;
1196 }
1197 }
1198
1199 sentry.borrow_mut().refs.push(Rc::downgrade(qentry));
1200 if !bq {
1201 qentry.borrow_mut().smtpd = Some(Rc::clone(sentry));
1202 }
1203 }
1204
1205 fn filter(&self) -> Option<Rc<RefCell<FEntry>>> {
1206 self.filter.clone().and_then(|f| f.upgrade())
1207 }
1208 }
1209
1210 #[derive(Default, Debug)]
1211 struct QEntry {
1212 log: Vec<(Box<[u8]>, u64)>,
1213 smtpd: Option<Rc<RefCell<SEntry>>>,
1214 filter: Option<Rc<RefCell<FEntry>>>,
1215 qid: Box<[u8]>,
1216 from: Box<[u8]>,
1217 client: Box<[u8]>,
1218 msgid: Box<[u8]>,
1219 size: u64,
1220 to_entries: Vec<ToEntry>,
1221 cleanup: bool,
1222 removed: bool,
1223 filtered: bool,
1224 string_match: bool,
1225 bq_filtered: bool,
1226 // will differ from smtpd
1227 bq_sentry: Option<Rc<RefCell<SEntry>>>,
1228 aq_qentry: Option<Weak<RefCell<QEntry>>>,
1229 }
1230
1231 impl QEntry {
1232 fn add_to_entry(&mut self, to: &[u8], relay: &[u8], dstatus: DStatus, timestamp: time_t) {
1233 let te = ToEntry {
1234 to: to.into(),
1235 relay: relay.into(),
1236 dstatus,
1237 timestamp,
1238 };
1239 self.to_entries.push(te);
1240 }
1241
1242 // finalize and print the QEntry
1243 fn finalize(&mut self, parser: &mut Parser) {
1244 // if it is not removed, skip
1245 if self.removed {
1246 if let Some(se) = &self.smtpd {
1247 // verify that the SEntry it is attached to is disconnected
1248 if !se.borrow().disconnected {
1249 return;
1250 }
1251 }
1252 if let Some(s) = &self.bq_sentry {
1253 if self.bq_filtered && !s.borrow().disconnected {
1254 return;
1255 }
1256 }
1257
1258 if let Some(qe) = &self.aq_qentry {
1259 if let Some(qe) = qe.upgrade() {
1260 if !qe.borrow().removed {
1261 return;
1262 }
1263 qe.borrow_mut().aq_qentry = None;
1264 qe.borrow_mut().finalize(parser);
1265 }
1266 }
1267
1268 if let Some(fe) = self.filter.clone() {
1269 // verify that the attached FEntry is finished if it is not
1270 // before queue filtered
1271 if !self.bq_filtered && !fe.borrow().finished {
1272 return;
1273 }
1274
1275 // if there's an SEntry, print with the SEntry
1276 // otherwise just print the QEntry (this can happen in certain
1277 // situations)
1278 match self.smtpd.clone() {
1279 Some(s) => self.print(parser, Some(&*s.borrow())),
1280 None => self.print(parser, None),
1281 };
1282 if let Some(se) = &self.smtpd {
1283 parser.free_qentry(&self.qid, Some(&mut *se.borrow_mut()));
1284 } else {
1285 parser.free_qentry(&self.qid, None);
1286 }
1287
1288 if !self.bq_filtered {
1289 parser.free_fentry(&fe.borrow().logid);
1290 }
1291 } else if let Some(s) = self.smtpd.clone() {
1292 self.print(parser, Some(&*s.borrow()));
1293 parser.free_qentry(&self.qid, Some(&mut *s.borrow_mut()));
1294 } else {
1295 self.print(parser, None);
1296 parser.free_qentry(&self.qid, None);
1297 }
1298 }
1299 }
1300
1301 fn msgid_matches(&self, parser: &Parser) -> bool {
1302 if !parser.options.msgid.is_empty() {
1303 if self.msgid.is_empty() {
1304 return false;
1305 }
1306 let qentry_msgid_lowercase = self.msgid.to_ascii_lowercase();
1307 let msgid_lowercase = parser.options.msgid.as_bytes().to_ascii_lowercase();
1308 if qentry_msgid_lowercase != msgid_lowercase {
1309 return false;
1310 }
1311 }
1312 true
1313 }
1314
1315 fn match_list_matches(&self, parser: &Parser, se: Option<&SEntry>) -> bool {
1316 let fe = &self.filter;
1317 if !parser.options.match_list.is_empty() {
1318 let mut found = false;
1319 for m in parser.options.match_list.iter() {
1320 match m {
1321 Match::Qid(q) => {
1322 if let Some(f) = fe {
1323 if &f.borrow().logid == q {
1324 found = true;
1325 break;
1326 }
1327 }
1328 if &self.qid == q {
1329 found = true;
1330 break;
1331 }
1332 }
1333 Match::RelLineNr(t, l) => {
1334 if let Some(s) = se {
1335 if s.timestamp == *t && s.rel_line_nr == *l {
1336 found = true;
1337 break;
1338 }
1339 }
1340 }
1341 }
1342 }
1343 if !found {
1344 return false;
1345 }
1346 }
1347 true
1348 }
1349
1350 fn host_matches(&self, parser: &Parser, se: Option<&SEntry>) -> bool {
1351 if !parser.options.host.is_empty() {
1352 let mut found = false;
1353 if let Some(s) = se {
1354 if !s.connect.is_empty()
1355 && find_lowercase(&s.connect, parser.options.host.as_bytes()).is_some()
1356 {
1357 found = true;
1358 }
1359 }
1360 if !self.client.is_empty()
1361 && find_lowercase(&self.client, parser.options.host.as_bytes()).is_some()
1362 {
1363 found = true;
1364 }
1365
1366 if !found {
1367 return false;
1368 }
1369 }
1370 true
1371 }
1372
1373 fn from_to_matches(&mut self, parser: &Parser) -> bool {
1374 if !parser.options.from.is_empty() {
1375 if self.from.is_empty() {
1376 return false;
1377 }
1378 if find_lowercase(&self.from, parser.options.from.as_bytes()).is_none() {
1379 return false;
1380 }
1381 } else if parser.options.exclude_ndr && self.from.is_empty() {
1382 return false;
1383 }
1384
1385 if !parser.options.to.is_empty() {
1386 let mut found = false;
1387 self.to_entries.retain(|to| {
1388 if find_lowercase(&to.to, parser.options.to.as_bytes()).is_none() {
1389 false
1390 } else {
1391 found = true;
1392 true
1393 }
1394 });
1395 if let Some(fe) = &self.filter {
1396 fe.borrow_mut().to_entries.retain(|to| {
1397 if find_lowercase(&to.to, parser.options.to.as_bytes()).is_none() {
1398 false
1399 } else {
1400 found = true;
1401 true
1402 }
1403 });
1404 }
1405 if !found {
1406 return false;
1407 }
1408 }
1409 true
1410 }
1411
1412 fn string_matches(&self, parser: &Parser, se: Option<&SEntry>) -> bool {
1413 let fe = &self.filter;
1414 if !parser.options.string_match.is_empty() {
1415 let mut string_match = self.string_match;
1416
1417 if let Some(s) = se {
1418 if s.string_match {
1419 string_match = true;
1420 }
1421 }
1422 if let Some(f) = fe {
1423 if f.borrow().string_match {
1424 string_match = true;
1425 }
1426 }
1427 if !string_match {
1428 return false;
1429 }
1430 }
1431 true
1432 }
1433
1434 // is_se_bq_sentry is true if the QEntry::bq_sentry is the same as passed
1435 // into the print() function via reference
1436 fn print_qentry_boilerplate(
1437 &mut self,
1438 parser: &mut Parser,
1439 is_se_bq_sentry: bool,
1440 se: Option<&SEntry>,
1441 ) {
1442 parser.write_all_ok(b"QENTRY: ");
1443 parser.write_all_ok(&self.qid);
1444 parser.write_all_ok(b"\n");
1445 parser.write_all_ok(format!("CTIME: {:8X}\n", parser.ctime));
1446 parser.write_all_ok(format!("SIZE: {}\n", self.size));
1447
1448 if !self.client.is_empty() {
1449 parser.write_all_ok(b"CLIENT: ");
1450 parser.write_all_ok(&self.client);
1451 parser.write_all_ok(b"\n");
1452 } else if !is_se_bq_sentry {
1453 if let Some(s) = se {
1454 if !s.connect.is_empty() {
1455 parser.write_all_ok(b"CLIENT: ");
1456 parser.write_all_ok(&s.connect);
1457 parser.write_all_ok(b"\n");
1458 }
1459 }
1460 } else if let Some(s) = &self.smtpd {
1461 if !s.borrow().connect.is_empty() {
1462 parser.write_all_ok(b"CLIENT: ");
1463 parser.write_all_ok(&s.borrow().connect);
1464 parser.write_all_ok(b"\n");
1465 }
1466 }
1467
1468 if !self.msgid.is_empty() {
1469 parser.write_all_ok(b"MSGID: ");
1470 parser.write_all_ok(&self.msgid);
1471 parser.write_all_ok(b"\n");
1472 }
1473 }
1474
1475 fn print(&mut self, parser: &mut Parser, se: Option<&SEntry>) {
1476 let fe = self.filter.clone();
1477
1478 if !self.msgid_matches(parser)
1479 || !self.match_list_matches(parser, se)
1480 || !self.host_matches(parser, se)
1481 || !self.from_to_matches(parser)
1482 || !self.string_matches(parser, se)
1483 {
1484 return;
1485 }
1486
1487 // necessary so we do not attempt to mutable borrow it a second time
1488 // which will panic
1489 let is_se_bq_sentry = match (&self.bq_sentry, se) {
1490 (Some(s), Some(se)) => std::ptr::eq(s.as_ptr(), se),
1491 _ => false,
1492 };
1493
1494 if is_se_bq_sentry {
1495 if let Some(s) = &se {
1496 if !s.disconnected {
1497 return;
1498 }
1499 }
1500 }
1501
1502 if parser.options.verbose > 0 {
1503 self.print_qentry_boilerplate(parser, is_se_bq_sentry, se);
1504 }
1505
1506 if self.bq_filtered {
1507 for to in self.to_entries.iter_mut() {
1508 to.dstatus = match to.dstatus {
1509 // the dsn (enhanced status code can only have a class of 2, 4 or 5
1510 // see https://tools.ietf.org/html/rfc3463
1511 DStatus::Dsn(2) => DStatus::BqPass,
1512 DStatus::Dsn(4) => DStatus::BqDefer,
1513 DStatus::Dsn(5) => DStatus::BqReject,
1514 _ => to.dstatus,
1515 };
1516 }
1517 }
1518
1519 // rev() to match the C code iteration direction (linked list vs Vec)
1520 for to in self.to_entries.iter().rev() {
1521 if !to.to.is_empty() {
1522 let final_rc;
1523 let final_borrow;
1524 let mut final_to: &ToEntry = to;
1525
1526 // if status == success and there's a filter attached that has
1527 // a matching 'to' in one of the ToEntries, set the ToEntry to
1528 // the one in the filter
1529 if to.dstatus == DStatus::Dsn(2) {
1530 if let Some(f) = &fe {
1531 if !self.bq_filtered || (f.borrow().finished && f.borrow().is_bq) {
1532 final_rc = f;
1533 final_borrow = final_rc.borrow();
1534 for to2 in final_borrow.to_entries.iter().rev() {
1535 if to.to == to2.to {
1536 final_to = to2;
1537 break;
1538 }
1539 }
1540 }
1541 }
1542 }
1543
1544 parser.write_all_ok(format!("TO:{:X}:", to.timestamp));
1545 parser.write_all_ok(&self.qid);
1546 parser.write_all_ok(format!(":{}: from <", final_to.dstatus));
1547 parser.write_all_ok(&self.from);
1548 parser.write_all_ok(b"> to <");
1549 parser.write_all_ok(&final_to.to);
1550 parser.write_all_ok(b"> (");
1551 // if we use the relay from the filter ToEntry, it will be
1552 // marked 'is_relay' in PMG/API2/MailTracker.pm and not shown
1553 // in the GUI in the case of before queue filtering
1554 if !self.bq_filtered {
1555 parser.write_all_ok(&final_to.relay);
1556 } else {
1557 parser.write_all_ok(&to.relay);
1558 }
1559 parser.write_all_ok(b")\n");
1560 parser.count += 1;
1561 }
1562 }
1563
1564 if self.bq_filtered {
1565 if let Some(fe) = &fe {
1566 if fe.borrow().finished && fe.borrow().is_bq {
1567 fe.borrow_mut().to_entries.retain(|to| {
1568 for to2 in self.to_entries.iter().rev() {
1569 if to.to == to2.to {
1570 return false;
1571 }
1572 }
1573 true
1574 });
1575
1576 for to in fe.borrow().to_entries.iter().rev() {
1577 parser.write_all_ok(format!("TO:{:X}:", to.timestamp));
1578 parser.write_all_ok(&self.qid);
1579 parser.write_all_ok(format!(":{}: from <", to.dstatus));
1580 parser.write_all_ok(&self.from);
1581 parser.write_all_ok(b"> to <");
1582 parser.write_all_ok(&to.to);
1583 parser.write_all_ok(b"> (");
1584 parser.write_all_ok(&to.relay);
1585 parser.write_all_ok(b")\n");
1586 parser.count += 1;
1587 }
1588 }
1589 }
1590 }
1591
1592 // print logs if '-vv' is specified
1593 if parser.options.verbose > 1 {
1594 let print_log = |parser: &mut Parser, logs: &Vec<(Box<[u8]>, u64)>| {
1595 for (log, line) in logs.iter() {
1596 parser.write_all_ok(format!("L{:08X} ", *line as u32));
1597 parser.write_all_ok(log);
1598 parser.write_all_ok(b"\n");
1599 }
1600 };
1601 if !is_se_bq_sentry {
1602 if let Some(s) = se {
1603 let mut logs = s.log.clone();
1604 if let Some(bq_se) = &self.bq_sentry {
1605 logs.append(&mut bq_se.borrow().log.clone());
1606 // as the logs come from 2 different SEntries,
1607 // interleave them via sort based on line number
1608 logs.sort_by(|a, b| a.1.cmp(&b.1));
1609 }
1610 if !logs.is_empty() {
1611 parser.write_all_ok(b"SMTP:\n");
1612 print_log(parser, &logs);
1613 }
1614 }
1615 } else if let Some(s) = &self.smtpd {
1616 let mut logs = s.borrow().log.clone();
1617 if let Some(se) = se {
1618 logs.append(&mut se.log.clone());
1619 // as the logs come from 2 different SEntries,
1620 // interleave them via sort based on line number
1621 logs.sort_by(|a, b| a.1.cmp(&b.1));
1622 }
1623 if !logs.is_empty() {
1624 parser.write_all_ok(b"SMTP:\n");
1625 print_log(parser, &logs);
1626 }
1627 }
1628
1629 if let Some(f) = fe {
1630 if (!self.bq_filtered || (f.borrow().finished && f.borrow().is_bq))
1631 && !f.borrow().log.is_empty()
1632 {
1633 parser.write_all_ok(format!("FILTER: {}\n", unsafe {
1634 std::str::from_utf8_unchecked(&f.borrow().logid)
1635 }));
1636 print_log(parser, &f.borrow().log);
1637 }
1638 }
1639
1640 if !self.log.is_empty() {
1641 parser.write_all_ok(b"QMGR:\n");
1642 self.log.sort_by(|a, b| a.1.cmp(&b.1));
1643 print_log(parser, &self.log);
1644 }
1645 }
1646 parser.write_all_ok(b"\n")
1647 }
1648
1649 fn set_client(&mut self, client: &[u8]) {
1650 if self.client.is_empty() {
1651 self.client = client.into();
1652 }
1653 }
1654 }
1655
1656 #[derive(Default, Debug)]
1657 struct FEntry {
1658 log: Vec<(Box<[u8]>, u64)>,
1659 logid: Box<[u8]>,
1660 to_entries: Vec<ToEntry>,
1661 processing_time: Box<[u8]>,
1662 string_match: bool,
1663 finished: bool,
1664 is_accepted: bool,
1665 qentry: Option<Weak<RefCell<QEntry>>>,
1666 is_bq: bool,
1667 }
1668
1669 impl FEntry {
1670 fn add_accept(&mut self, to: &[u8], qid: &[u8], timestamp: time_t) {
1671 let te = ToEntry {
1672 to: to.into(),
1673 relay: qid.into(),
1674 dstatus: DStatus::Accept,
1675 timestamp,
1676 };
1677 self.to_entries.push(te);
1678 self.is_accepted = true;
1679 }
1680
1681 fn add_quarantine(&mut self, to: &[u8], qid: &[u8], timestamp: time_t) {
1682 let te = ToEntry {
1683 to: to.into(),
1684 relay: qid.into(),
1685 dstatus: DStatus::Quarantine,
1686 timestamp,
1687 };
1688 self.to_entries.push(te);
1689 }
1690
1691 fn add_block(&mut self, to: &[u8], timestamp: time_t) {
1692 let te = ToEntry {
1693 to: to.into(),
1694 relay: (&b"none"[..]).into(),
1695 dstatus: DStatus::Block,
1696 timestamp,
1697 };
1698 self.to_entries.push(te);
1699 }
1700
1701 fn set_processing_time(&mut self, time: &[u8]) {
1702 self.processing_time = time.into();
1703 self.finished = true;
1704 }
1705
1706 fn qentry(&self) -> Option<Rc<RefCell<QEntry>>> {
1707 self.qentry.clone().and_then(|q| q.upgrade())
1708 }
1709 }
1710
1711 #[derive(Debug)]
1712 struct Parser {
1713 sentries: HashMap<u64, Rc<RefCell<SEntry>>>,
1714 fentries: HashMap<Box<[u8]>, Rc<RefCell<FEntry>>>,
1715 qentries: HashMap<Box<[u8]>, Rc<RefCell<QEntry>>>,
1716 msgid_lookup: HashMap<Box<[u8]>, Weak<RefCell<QEntry>>>,
1717
1718 smtp_tls_log_by_pid: HashMap<u64, (Box<[u8]>, u64)>,
1719
1720 current_record_state: RecordState,
1721 rel_line_nr: u64,
1722
1723 current_year: i64,
1724 current_month: i64,
1725 current_file_index: usize,
1726
1727 count: u64,
1728
1729 buffered_stdout: BufWriter<std::io::Stdout>,
1730
1731 options: Options,
1732
1733 start_tm: time::Tm,
1734 end_tm: time::Tm,
1735
1736 ctime: time_t,
1737 string_match: bool,
1738
1739 lines: u64,
1740 }
1741
1742 impl Parser {
1743 fn new() -> Result<Self, Error> {
1744 let ltime = Tm::now_local()?;
1745
1746 Ok(Self {
1747 sentries: HashMap::new(),
1748 fentries: HashMap::new(),
1749 qentries: HashMap::new(),
1750 msgid_lookup: HashMap::new(),
1751 smtp_tls_log_by_pid: HashMap::new(),
1752 current_record_state: Default::default(),
1753 rel_line_nr: 0,
1754 current_year: (ltime.tm_year + 1900) as i64,
1755 current_month: ltime.tm_mon as i64,
1756 current_file_index: 0,
1757 count: 0,
1758 buffered_stdout: BufWriter::with_capacity(4 * 1024 * 1024, std::io::stdout()),
1759 options: Options::default(),
1760 start_tm: Tm::zero(),
1761 end_tm: Tm::zero(),
1762 ctime: 0,
1763 string_match: false,
1764 lines: 0,
1765 })
1766 }
1767
1768 fn free_sentry(&mut self, sentry_pid: u64) {
1769 self.sentries.remove(&sentry_pid);
1770 }
1771
1772 fn free_qentry(&mut self, qid: &[u8], se: Option<&mut SEntry>) {
1773 if let Some(qe) = self.qentries.get(qid) {
1774 if let Some(se) = se {
1775 se.delete_ref(qe);
1776 }
1777 }
1778
1779 self.qentries.remove(qid);
1780 }
1781
1782 fn free_fentry(&mut self, fentry_logid: &[u8]) {
1783 self.fentries.remove(fentry_logid);
1784 }
1785
1786 fn parse_files(&mut self) -> Result<(), Error> {
1787 if !self.options.inputfile.is_empty() {
1788 if self.options.inputfile == "-" {
1789 // read from STDIN
1790 self.current_file_index = 0;
1791 let mut reader = BufReader::new(std::io::stdin());
1792 self.handle_input_by_line(&mut reader)?;
1793 } else if let Ok(file) = File::open(&self.options.inputfile) {
1794 // read from specified file
1795 self.current_file_index = 0;
1796 let mut reader = BufReader::new(file);
1797 self.handle_input_by_line(&mut reader)?;
1798 }
1799 } else {
1800 let filecount = self.count_files_in_time_range();
1801 for i in (0..filecount).rev() {
1802 if let Ok(file) = File::open(LOGFILES[i]) {
1803 self.current_file_index = i;
1804 if i > 1 {
1805 let gzdecoder = read::GzDecoder::new(file);
1806 let mut reader = BufReader::new(gzdecoder);
1807 self.handle_input_by_line(&mut reader)?;
1808 } else {
1809 let mut reader = BufReader::new(file);
1810 self.handle_input_by_line(&mut reader)?;
1811 }
1812 }
1813 }
1814 }
1815
1816 Ok(())
1817 }
1818
1819 fn handle_input_by_line(&mut self, reader: &mut dyn BufRead) -> Result<(), Error> {
1820 let mut buffer = Vec::<u8>::with_capacity(4096);
1821 let mut prev_time = 0;
1822 loop {
1823 if self.options.limit > 0 && (self.count >= self.options.limit) {
1824 self.write_all_ok("STATUS: aborted by limit (too many hits)\n");
1825 self.buffered_stdout.flush()?;
1826 std::process::exit(0);
1827 }
1828
1829 buffer.clear();
1830 let size = match reader.read_until(b'\n', &mut buffer) {
1831 Err(e) => return Err(e.into()),
1832 Ok(0) => return Ok(()),
1833 Ok(s) => s,
1834 };
1835 // size includes delimiter
1836 let line = &buffer[0..size - 1];
1837 let complete_line = line;
1838
1839 let (time, line) = match parse_time(line, self.current_year, self.current_month) {
1840 Some(t) => t,
1841 None => continue,
1842 };
1843
1844 // relative line number within a single timestamp
1845 if time != prev_time {
1846 self.rel_line_nr = 0;
1847 } else {
1848 self.rel_line_nr += 1;
1849 }
1850 prev_time = time;
1851
1852 // skip until we're in the specified time frame
1853 if time < self.options.start {
1854 continue;
1855 }
1856 // past the specified time frame, we're done, exit the loop
1857 if time > self.options.end {
1858 break;
1859 }
1860
1861 self.lines += 1;
1862
1863 let (host, service, pid, line) = match parse_host_service_pid(line) {
1864 Some((h, s, p, l)) => (h, s, p, l),
1865 None => continue,
1866 };
1867
1868 self.ctime = time;
1869
1870 self.current_record_state.host = host.into();
1871 self.current_record_state.service = service.into();
1872 self.current_record_state.pid = pid;
1873 self.current_record_state.timestamp = time;
1874
1875 self.string_match = false;
1876 if !self.options.string_match.is_empty()
1877 && find_lowercase(complete_line, self.options.string_match.as_bytes()).is_some()
1878 {
1879 self.string_match = true;
1880 }
1881
1882 // complete_line required for the logs
1883 if service == b"pmg-smtp-filter" {
1884 handle_pmg_smtp_filter_message(line, self, complete_line);
1885 } else if service == b"postfix/postscreen" {
1886 handle_postscreen_message(line, self, complete_line);
1887 } else if service == b"postfix/qmgr" {
1888 handle_qmgr_message(line, self, complete_line);
1889 } else if service == b"postfix/lmtp"
1890 || service == b"postfix/smtp"
1891 || service == b"postfix/local"
1892 || service == b"postfix/error"
1893 {
1894 handle_lmtp_message(line, self, complete_line);
1895 } else if service == b"postfix/smtpd" {
1896 handle_smtpd_message(line, self, complete_line);
1897 } else if service == b"postfix/cleanup" {
1898 handle_cleanup_message(line, self, complete_line);
1899 }
1900 }
1901 Ok(())
1902 }
1903
1904 /// Returns the number of files to parse. Does not error out if it can't access any file
1905 /// (permission denied)
1906 fn count_files_in_time_range(&mut self) -> usize {
1907 let mut count = 0;
1908 let mut buffer = Vec::new();
1909
1910 for (i, item) in LOGFILES.iter().enumerate() {
1911 count = i;
1912 if let Ok(file) = File::open(item) {
1913 self.current_file_index = i;
1914 buffer.clear();
1915 if i > 1 {
1916 let gzdecoder = read::GzDecoder::new(file);
1917 let mut reader = BufReader::new(gzdecoder);
1918 // check the first line
1919 if let Ok(size) = reader.read_until(b'\n', &mut buffer) {
1920 if size == 0 {
1921 return count;
1922 }
1923 if let Some((time, _)) =
1924 parse_time(&buffer[0..size], self.current_year, self.current_month)
1925 {
1926 // found the earliest file in the time frame
1927 if time < self.options.start {
1928 break;
1929 }
1930 }
1931 } else {
1932 return count;
1933 }
1934 } else {
1935 let mut reader = BufReader::new(file);
1936 if let Ok(size) = reader.read_until(b'\n', &mut buffer) {
1937 if size == 0 {
1938 return count;
1939 }
1940 if let Some((time, _)) =
1941 parse_time(&buffer[0..size], self.current_year, self.current_month)
1942 {
1943 if time < self.options.start {
1944 break;
1945 }
1946 }
1947 } else {
1948 return count;
1949 }
1950 }
1951 } else {
1952 return count;
1953 }
1954 }
1955
1956 count + 1
1957 }
1958
1959 fn handle_args(&mut self, args: clap::ArgMatches) -> Result<(), Error> {
1960 if let Some(inputfile) = args.value_of("inputfile") {
1961 self.options.inputfile = inputfile.to_string();
1962 }
1963
1964 if let Some(start) = args.value_of("start") {
1965 if let Ok(res) = time::strptime(start, c_str!("%F %T")) {
1966 self.options.start = res.as_utc_to_epoch();
1967 self.start_tm = res;
1968 } else if let Ok(res) = time::strptime(start, c_str!("%s")) {
1969 self.options.start = res.as_utc_to_epoch();
1970 self.start_tm = res;
1971 } else {
1972 bail!("failed to parse start time");
1973 }
1974 } else {
1975 let mut ltime = Tm::now_local()?;
1976 ltime.tm_sec = 0;
1977 ltime.tm_min = 0;
1978 ltime.tm_hour = 0;
1979 self.options.start = ltime.as_utc_to_epoch();
1980 self.start_tm = ltime;
1981 }
1982
1983 if let Some(end) = args.value_of("end") {
1984 if let Ok(res) = time::strptime(end, c_str!("%F %T")) {
1985 self.options.end = res.as_utc_to_epoch();
1986 self.end_tm = res;
1987 } else if let Ok(res) = time::strptime(end, c_str!("%s")) {
1988 self.options.end = res.as_utc_to_epoch();
1989 self.end_tm = res;
1990 } else {
1991 bail!("failed to parse end time");
1992 }
1993 } else {
1994 self.options.end = unsafe { libc::time(std::ptr::null_mut()) };
1995 self.end_tm = Tm::at_local(self.options.end)?;
1996 }
1997
1998 if self.options.end < self.options.start {
1999 bail!("end time before start time");
2000 }
2001
2002 self.options.limit = match args.value_of("limit") {
2003 Some(l) => l.parse().unwrap(),
2004 None => 0,
2005 };
2006
2007 if let Some(qids) = args.values_of("qids") {
2008 for q in qids {
2009 let ltime: time_t = 0;
2010 let rel_line_nr: libc::c_ulong = 0;
2011 let input = CString::new(q)?;
2012 let bytes = concat!("T%08lXL%08lX", "\0");
2013 let format =
2014 unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) };
2015 if unsafe {
2016 libc::sscanf(input.as_ptr(), format.as_ptr(), &ltime, &rel_line_nr) == 2
2017 } {
2018 self.options
2019 .match_list
2020 .push(Match::RelLineNr(ltime, rel_line_nr));
2021 } else {
2022 self.options
2023 .match_list
2024 .push(Match::Qid(q.as_bytes().into()));
2025 }
2026 }
2027 }
2028
2029 if let Some(from) = args.value_of("from") {
2030 self.options.from = from.to_string();
2031 }
2032 if let Some(to) = args.value_of("to") {
2033 self.options.to = to.to_string();
2034 }
2035 if let Some(host) = args.value_of("host") {
2036 self.options.host = host.to_string();
2037 }
2038 if let Some(msgid) = args.value_of("msgid") {
2039 self.options.msgid = msgid.to_string();
2040 }
2041
2042 self.options.exclude_greylist = args.is_present("exclude_greylist");
2043 self.options.exclude_ndr = args.is_present("exclude_ndr");
2044
2045 self.options.verbose = args.get_count("verbose") as _;
2046
2047 if let Some(string_match) = args.value_of("search") {
2048 self.options.string_match = string_match.to_string();
2049 }
2050
2051 Ok(())
2052 }
2053
2054 fn write_all_ok<T: AsRef<[u8]>>(&mut self, data: T) {
2055 self.buffered_stdout
2056 .write_all(data.as_ref())
2057 .expect("failed to write to stdout");
2058 }
2059 }
2060
2061 impl Drop for Parser {
2062 fn drop(&mut self) {
2063 let mut qentries = std::mem::take(&mut self.qentries);
2064 for q in qentries.values() {
2065 let smtpd = q.borrow().smtpd.clone();
2066 if let Some(s) = smtpd {
2067 q.borrow_mut().print(self, Some(&*s.borrow()));
2068 } else {
2069 q.borrow_mut().print(self, None);
2070 }
2071 }
2072 qentries.clear();
2073 let mut sentries = std::mem::take(&mut self.sentries);
2074 for s in sentries.values() {
2075 s.borrow_mut().print(self);
2076 }
2077 sentries.clear();
2078 }
2079 }
2080
2081 #[derive(Debug, Default)]
2082 struct Options {
2083 match_list: Vec<Match>,
2084 inputfile: String,
2085 string_match: String,
2086 host: String,
2087 msgid: String,
2088 from: String,
2089 to: String,
2090 start: time_t,
2091 end: time_t,
2092 limit: u64,
2093 verbose: u32,
2094 exclude_greylist: bool,
2095 exclude_ndr: bool,
2096 }
2097
2098 #[derive(Debug)]
2099 enum Match {
2100 Qid(Box<[u8]>),
2101 RelLineNr(time_t, u64),
2102 }
2103
2104 #[derive(Debug, Default)]
2105 struct RecordState {
2106 host: Box<[u8]>,
2107 service: Box<[u8]>,
2108 pid: u64,
2109 timestamp: time_t,
2110 }
2111
2112 fn get_or_create_qentry(
2113 qentries: &mut HashMap<Box<[u8]>, Rc<RefCell<QEntry>>>,
2114 qid: &[u8],
2115 ) -> Rc<RefCell<QEntry>> {
2116 if let Some(qe) = qentries.get(qid) {
2117 Rc::clone(qe)
2118 } else {
2119 let qe = Rc::new(RefCell::new(QEntry::default()));
2120 qe.borrow_mut().qid = qid.into();
2121 qentries.insert(qid.into(), qe.clone());
2122 qe
2123 }
2124 }
2125
2126 fn get_or_create_sentry(
2127 sentries: &mut HashMap<u64, Rc<RefCell<SEntry>>>,
2128 pid: u64,
2129 rel_line_nr: u64,
2130 timestamp: time_t,
2131 ) -> Rc<RefCell<SEntry>> {
2132 if let Some(se) = sentries.get(&pid) {
2133 Rc::clone(se)
2134 } else {
2135 let se = Rc::new(RefCell::new(SEntry::default()));
2136 se.borrow_mut().rel_line_nr = rel_line_nr;
2137 se.borrow_mut().timestamp = timestamp;
2138 sentries.insert(pid, se.clone());
2139 se
2140 }
2141 }
2142
2143 fn get_or_create_fentry(
2144 fentries: &mut HashMap<Box<[u8]>, Rc<RefCell<FEntry>>>,
2145 qid: &[u8],
2146 ) -> Rc<RefCell<FEntry>> {
2147 if let Some(fe) = fentries.get(qid) {
2148 Rc::clone(fe)
2149 } else {
2150 let fe = Rc::new(RefCell::new(FEntry::default()));
2151 fe.borrow_mut().logid = qid.into();
2152 fentries.insert(qid.into(), fe.clone());
2153 fe
2154 }
2155 }
2156
2157 const LOGFILES: [&str; 32] = [
2158 "/var/log/syslog",
2159 "/var/log/syslog.1",
2160 "/var/log/syslog.2.gz",
2161 "/var/log/syslog.3.gz",
2162 "/var/log/syslog.4.gz",
2163 "/var/log/syslog.5.gz",
2164 "/var/log/syslog.6.gz",
2165 "/var/log/syslog.7.gz",
2166 "/var/log/syslog.8.gz",
2167 "/var/log/syslog.9.gz",
2168 "/var/log/syslog.10.gz",
2169 "/var/log/syslog.11.gz",
2170 "/var/log/syslog.12.gz",
2171 "/var/log/syslog.13.gz",
2172 "/var/log/syslog.14.gz",
2173 "/var/log/syslog.15.gz",
2174 "/var/log/syslog.16.gz",
2175 "/var/log/syslog.17.gz",
2176 "/var/log/syslog.18.gz",
2177 "/var/log/syslog.19.gz",
2178 "/var/log/syslog.20.gz",
2179 "/var/log/syslog.21.gz",
2180 "/var/log/syslog.22.gz",
2181 "/var/log/syslog.23.gz",
2182 "/var/log/syslog.24.gz",
2183 "/var/log/syslog.25.gz",
2184 "/var/log/syslog.26.gz",
2185 "/var/log/syslog.27.gz",
2186 "/var/log/syslog.28.gz",
2187 "/var/log/syslog.29.gz",
2188 "/var/log/syslog.30.gz",
2189 "/var/log/syslog.31.gz",
2190 ];
2191
2192 /// Parse a QID ([A-Z]+). Returns a tuple of (qid, remaining_text) or None.
2193 fn parse_qid(data: &[u8], max: usize) -> Option<(&[u8], &[u8])> {
2194 // to simplify limit max to data.len()
2195 let max = max.min(data.len());
2196 // take at most max, find the first non-hex-digit
2197 match data.iter().take(max).position(|b| !b.is_ascii_hexdigit()) {
2198 // if there were less than 5 return nothing
2199 // the QID always has at least 5 characters for the microseconds (see
2200 // http://www.postfix.org/postconf.5.html#enable_long_queue_ids)
2201 Some(n) if n < 5 => None,
2202 // otherwise split at the first non-hex-digit
2203 Some(n) => Some(data.split_at(n)),
2204 // or return 'max' length QID if no non-hex-digit is found
2205 None => Some(data.split_at(max)),
2206 }
2207 }
2208
2209 /// Parse a number. Returns a tuple of (parsed_number, remaining_text) or None.
2210 fn parse_number(data: &[u8], max_digits: usize) -> Option<(usize, &[u8])> {
2211 let max = max_digits.min(data.len());
2212 if max == 0 {
2213 return None;
2214 }
2215
2216 match data.iter().take(max).position(|b| !b.is_ascii_digit()) {
2217 Some(n) if n == 0 => None,
2218 Some(n) => {
2219 let (number, data) = data.split_at(n);
2220 // number only contains ascii digits
2221 let number = unsafe { std::str::from_utf8_unchecked(number) }
2222 .parse::<usize>()
2223 .unwrap();
2224 Some((number, data))
2225 }
2226 None => {
2227 let (number, data) = data.split_at(max);
2228 // number only contains ascii digits
2229 let number = unsafe { std::str::from_utf8_unchecked(number) }
2230 .parse::<usize>()
2231 .unwrap();
2232 Some((number, data))
2233 }
2234 }
2235 }
2236
2237 /// Parse time. Returns a tuple of (parsed_time, remaining_text) or None.
2238 fn parse_time(data: &'_ [u8], cur_year: i64, cur_month: i64) -> Option<(time_t, &'_ [u8])> {
2239 if data.len() < 15 {
2240 return None;
2241 }
2242
2243 let mon = match &data[0..3] {
2244 b"Jan" => 0,
2245 b"Feb" => 1,
2246 b"Mar" => 2,
2247 b"Apr" => 3,
2248 b"May" => 4,
2249 b"Jun" => 5,
2250 b"Jul" => 6,
2251 b"Aug" => 7,
2252 b"Sep" => 8,
2253 b"Oct" => 9,
2254 b"Nov" => 10,
2255 b"Dec" => 11,
2256 _ => return None,
2257 };
2258 let data = &data[3..];
2259
2260 // assume smaller month now than in log line means yearwrap
2261 let mut year = if cur_month < mon {
2262 cur_year - 1
2263 } else {
2264 cur_year
2265 };
2266
2267 let mut ltime: time_t = (year - 1970) * 365 + CAL_MTOD[mon as usize];
2268
2269 // leap year considerations
2270 if mon <= 1 {
2271 year -= 1;
2272 }
2273 ltime += (year - 1968) / 4;
2274 ltime -= (year - 1900) / 100;
2275 ltime += (year - 1600) / 400;
2276
2277 let whitespace_count = data.iter().take_while(|b| b.is_ascii_whitespace()).count();
2278 let data = &data[whitespace_count..];
2279
2280 let (mday, data) = match parse_number(data, 2) {
2281 Some(t) => t,
2282 None => return None,
2283 };
2284 if mday == 0 {
2285 return None;
2286 }
2287
2288 ltime += (mday - 1) as i64;
2289
2290 if data.is_empty() {
2291 return None;
2292 }
2293
2294 let data = &data[1..];
2295
2296 let (hour, data) = match parse_number(data, 2) {
2297 Some(t) => t,
2298 None => return None,
2299 };
2300
2301 ltime *= 24;
2302 ltime += hour as i64;
2303
2304 if let Some(c) = data.iter().next() {
2305 if (*c as char) != ':' {
2306 return None;
2307 }
2308 } else {
2309 return None;
2310 }
2311 let data = &data[1..];
2312
2313 let (min, data) = match parse_number(data, 2) {
2314 Some(t) => t,
2315 None => return None,
2316 };
2317
2318 ltime *= 60;
2319 ltime += min as i64;
2320
2321 if let Some(c) = data.iter().next() {
2322 if (*c as char) != ':' {
2323 return None;
2324 }
2325 } else {
2326 return None;
2327 }
2328 let data = &data[1..];
2329
2330 let (sec, data) = match parse_number(data, 2) {
2331 Some(t) => t,
2332 None => return None,
2333 };
2334
2335 ltime *= 60;
2336 ltime += sec as i64;
2337
2338 let data = match data.len() {
2339 0 => &[],
2340 _ => &data[1..],
2341 };
2342
2343 Some((ltime, data))
2344 }
2345
2346 type ByteSlice<'a> = &'a [u8];
2347 /// Parse Host, Service and PID at the beginning of data. Returns a tuple of (host, service, pid, remaining_text).
2348 fn parse_host_service_pid(data: &[u8]) -> Option<(ByteSlice, ByteSlice, u64, ByteSlice)> {
2349 let host_count = data
2350 .iter()
2351 .take_while(|b| !(**b as char).is_ascii_whitespace())
2352 .count();
2353 let host = &data[0..host_count];
2354 let data = &data[host_count + 1..]; // whitespace after host
2355
2356 let service_count = data
2357 .iter()
2358 .take_while(|b| {
2359 (**b as char).is_ascii_alphabetic() || (**b as char) == '/' || (**b as char) == '-'
2360 })
2361 .count();
2362 let service = &data[0..service_count];
2363 let data = &data[service_count..];
2364 if data.get(0) != Some(&b'[') {
2365 return None;
2366 }
2367 let data = &data[1..];
2368
2369 let pid_count = data
2370 .iter()
2371 .take_while(|b| (**b as char).is_ascii_digit())
2372 .count();
2373 let pid = match unsafe { std::str::from_utf8_unchecked(&data[0..pid_count]) }.parse() {
2374 // all ascii digits so valid utf8
2375 Ok(p) => p,
2376 Err(_) => return None,
2377 };
2378 let data = &data[pid_count..];
2379 if !data.starts_with(b"]: ") {
2380 return None;
2381 }
2382 let data = &data[3..];
2383
2384 Some((host, service, pid, data))
2385 }
2386
2387 /// A find implementation for [u8]. Returns the index or None.
2388 fn find<T: PartialOrd>(data: &[T], needle: &[T]) -> Option<usize> {
2389 data.windows(needle.len()).position(|d| d == needle)
2390 }
2391
2392 /// A find implementation for [u8] that converts to lowercase before the comparison. Returns the
2393 /// index or None.
2394 fn find_lowercase(data: &[u8], needle: &[u8]) -> Option<usize> {
2395 let data = data.to_ascii_lowercase();
2396 let needle = needle.to_ascii_lowercase();
2397 data.windows(needle.len()).position(|d| d == &needle[..])
2398 }