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