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