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