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