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