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