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