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