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