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