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