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