1 use std
::cell
::RefCell
;
2 use std
::collections
::HashMap
;
4 use std
::rc
::{Rc, Weak}
;
8 use std
::io
::BufReader
;
9 use std
::io
::BufWriter
;
12 use anyhow
::{bail, Error}
;
19 use time
::{Tm, CAL_MTOD}
;
21 fn main() -> Result
<(), Error
> {
22 let matches
= App
::new(clap
::crate_name
!())
23 .version(clap
::crate_version
!())
24 .about(clap
::crate_description
!())
26 Arg
::with_name("verbose")
29 .help("Verbose output, can be specified multiple times")
30 .action(clap
::ArgAction
::Count
),
33 Arg
::with_name("inputfile")
36 .help("Input file to use instead of /var/log/syslog, or '-' for stdin")
37 .value_name("INPUTFILE"),
40 Arg
::with_name("host")
43 .help("Hostname or Server IP")
47 Arg
::with_name("from")
50 .help("Mails from SENDER")
51 .value_name("SENDER"),
57 .help("Mails to RECIPIENT")
58 .value_name("RECIPIENT"),
61 Arg
::with_name("start")
64 .help("Start time (YYYY-MM-DD HH:MM:SS) or seconds since epoch")
71 .help("End time (YYYY-MM-DD HH:MM:SS) or seconds since epoch")
75 Arg
::with_name("msgid")
78 .help("Message ID (exact match)")
82 Arg
::with_name("qids")
85 .help("Queue ID (exact match), can be specified multiple times")
91 Arg
::with_name("search")
93 .long("search-string")
94 .help("Search for string")
95 .value_name("STRING"),
98 Arg
::with_name("limit")
101 .help("Print MAX entries")
106 Arg
::with_name("exclude_greylist")
108 .long("exclude-greylist")
109 .help("Exclude greylist entries"),
112 Arg
::with_name("exclude_ndr")
115 .help("Exclude NDR entries"),
119 let mut parser
= Parser
::new()?
;
120 parser
.handle_args(matches
)?
;
122 println
!("# LogReader: {}", std
::process
::id());
123 println
!("# Query options");
124 if !parser
.options
.from
.is_empty() {
125 println
!("# Sender: {}", parser
.options
.from
);
127 if !parser
.options
.to
.is_empty() {
128 println
!("# Recipient: {}", parser
.options
.to
);
130 if !parser
.options
.host
.is_empty() {
131 println
!("# Server: {}", parser
.options
.host
);
133 if !parser
.options
.msgid
.is_empty() {
134 println
!("# MsgID: {}", parser
.options
.msgid
);
136 for m
in parser
.options
.match_list
.iter() {
138 Match
::Qid(b
) => println
!("# QID: {}", std
::str::from_utf8(b
)?
),
139 Match
::RelLineNr(t
, l
) => println
!("# QID: T{:8X}L{:08X}", *t
, *l
as u32),
143 if !parser
.options
.string_match
.is_empty() {
144 println
!("# Match: {}", parser
.options
.string_match
);
149 time
::strftime(c_str
!("%F %T"), &parser
.start_tm
)?
,
154 time
::strftime(c_str
!("%F %T"), &parser
.end_tm
)?
,
158 println
!("# End Query Options\n");
159 parser
.parse_files()?
;
164 // handle log entries for service 'pmg-smtp-filter'
165 // we match 4 entries, all beginning with a QID
166 // accept mail, move mail, block mail and the processing time
167 fn handle_pmg_smtp_filter_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
168 let (qid
, data
) = match parse_qid(msg
, 25) {
169 Some((q
, m
)) => (q
, m
),
172 // skip ': ' following the QID
173 let data
= &data
[2..];
175 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
177 if parser
.string_match
{
178 fe
.borrow_mut().string_match
= parser
.string_match
;
183 .push((complete_line
.into(), parser
.lines
));
185 // we're interested in the 'to' address and the QID when we accept the mail
186 if data
.starts_with(b
"accept mail to <") {
187 let data
= &data
[16..];
188 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
189 let (to
, data
) = data
.split_at(to_count
);
190 if !data
.starts_with(b
"> (") {
193 let data
= &data
[3..];
194 let qid_count
= data
.iter().take_while(|b
| (**b
as char) != '
)'
).count();
195 let qid
= &data
[..qid_count
];
197 // add a ToEntry with the DStatus 'Accept' to the FEntry
199 .add_accept(to
, qid
, parser
.current_record_state
.timestamp
);
201 // if there's a QEntry with the qid and it's not yet filtered
202 // set it to before-queue filtered
203 if let Some(qe
) = parser
.qentries
.get(qid
) {
204 if !qe
.borrow().filtered
{
205 qe
.borrow_mut().bq_filtered
= true;
206 qe
.borrow_mut().filter
= Some(Rc
::clone(&fe
));
207 fe
.borrow_mut().qentry
= Some(Rc
::downgrade(qe
));
214 // same as for the 'accept' case, we're interested in both the 'to'
215 // address as well as the QID
216 if data
.starts_with(b
"moved mail for <") {
217 let data
= &data
[16..];
218 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
219 let (to
, data
) = data
.split_at(to_count
);
221 let qid_index
= match find(data
, b
"quarantine - ") {
225 let data
= &data
[qid_index
+ 13..];
226 let (qid
, _
) = match parse_qid(data
, 25) {
231 // add a ToEntry with the DStatus 'Quarantine' to the FEntry
233 .add_quarantine(to
, qid
, parser
.current_record_state
.timestamp
);
237 // in the 'block' case we're only interested in the 'to' address, there's
238 // no queue for these mails
239 if data
.starts_with(b
"block mail to <") {
240 let data
= &data
[15..];
241 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
242 let to
= &data
[..to_count
];
245 .add_block(to
, parser
.current_record_state
.timestamp
);
249 // here the pmg-smtp-filter is finished and we get the processing time
250 if data
.starts_with(b
"processing time: ") {
251 let data
= &data
[17..];
252 let time_count
= data
.iter().take_while(|b
| !b
.is_ascii_whitespace()).count();
253 let time
= &data
[..time_count
];
255 fe
.borrow_mut().set_processing_time(time
);
259 // handle log entries for postscreen
260 // here only the NOQUEUE: reject is of interest
261 // these are the mails that were rejected before even entering the smtpd by
262 // e.g. checking DNSBL sites
263 fn handle_postscreen_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
264 if !msg
.starts_with(b
"NOQUEUE: reject: RCPT from ") {
267 // skip the string from above
268 let data
= &msg
[27..];
269 let client_index
= match find(data
, b
"; client [") {
273 let data
= &data
[client_index
+ 10..];
275 let client_count
= data
.iter().take_while(|b
| (**b
as char) != '
]'
).count();
276 let (client
, data
) = data
.split_at(client_count
);
278 let from_index
= match find(data
, b
"; from=<") {
282 let data
= &data
[from_index
+ 8..];
284 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
285 let (from
, data
) = data
.split_at(from_count
);
287 if !data
.starts_with(b
">, to=<") {
290 let data
= &data
[7..];
291 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
292 let to
= &data
[..to_count
];
294 let se
= get_or_create_sentry(
295 &mut parser
.sentries
,
296 parser
.current_record_state
.pid
,
298 parser
.current_record_state
.timestamp
,
301 if parser
.string_match
{
302 se
.borrow_mut().string_match
= parser
.string_match
;
307 .push((complete_line
.into(), parser
.lines
));
308 // for postscreeen noqueue log entries we add a NoqueueEntry to the SEntry
309 se
.borrow_mut().add_noqueue_entry(
313 parser
.current_record_state
.timestamp
,
315 // set the connecting client
316 se
.borrow_mut().set_connect(client
);
317 // as there's no more service involved after the postscreen noqueue entry,
318 // we set it to disconnected and print it
319 se
.borrow_mut().disconnected
= true;
320 se
.borrow_mut().print(parser
);
321 parser
.free_sentry(parser
.current_record_state
.pid
);
324 // handle log entries for 'qmgr'
325 // these only appear in the 'after-queue filter' case or when the mail is
326 // 'accepted' in the 'before-queue filter' case
327 fn handle_qmgr_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
328 let (qid
, data
) = match parse_qid(msg
, 15) {
332 let data
= &data
[2..];
334 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
336 if parser
.string_match
{
337 qe
.borrow_mut().string_match
= parser
.string_match
;
339 qe
.borrow_mut().cleanup
= true;
342 .push((complete_line
.into(), parser
.lines
));
344 // we parse 2 log entries, either one with a 'from' and a 'size' or one
345 // that signals that the mail has been removed from the queue (after an
346 // action was taken, e.g. accept, by the filter)
347 if data
.starts_with(b
"from=<") {
348 let data
= &data
[6..];
350 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
351 let (from
, data
) = data
.split_at(from_count
);
353 if !data
.starts_with(b
">, size=") {
356 let data
= &data
[8..];
358 let size_count
= data
360 .take_while(|b
| (**b
as char).is_ascii_digit())
362 let (size
, _
) = data
.split_at(size_count
);
363 qe
.borrow_mut().from
= from
.into();
364 // it is safe here because we had a check before that limits it to just
365 // ascii digits which is valid utf8
366 qe
.borrow_mut().size
= unsafe { std::str::from_utf8_unchecked(size) }
369 } else if data
== b
"removed" {
370 qe
.borrow_mut().removed
= true;
371 qe
.borrow_mut().finalize(parser
);
375 // handle log entries for 'lmtp', 'smtp', 'error' and 'local'
376 fn handle_lmtp_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
377 if msg
.starts_with(b
"Trusted TLS connection established to")
378 || msg
.starts_with(b
"Untrusted TLS connection established to")
380 // the only way to match outgoing TLS connections is by smtp pid
381 // this message has to appear before the 'qmgr: <QID>: removed' entry in the log
382 parser
.smtp_tls_log_by_pid
.insert(
383 parser
.current_record_state
.pid
,
384 (complete_line
.into(), parser
.lines
),
389 let (qid
, data
) = match parse_qid(msg
, 15) {
390 Some((q
, t
)) => (q
, t
),
394 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
396 if parser
.string_match
{
397 qe
.borrow_mut().string_match
= parser
.string_match
;
399 qe
.borrow_mut().cleanup
= true;
402 .push((complete_line
.into(), parser
.lines
));
404 // assume the TLS log entry always appears before as it is the same process
405 if let Some(log_line
) = parser
407 .remove(&parser
.current_record_state
.pid
)
409 qe
.borrow_mut().log
.push(log_line
);
412 let data
= &data
[2..];
413 if !data
.starts_with(b
"to=<") {
416 let data
= &data
[4..];
417 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
418 let (to
, data
) = data
.split_at(to_count
);
420 let relay_index
= match find(data
, b
"relay=") {
424 let data
= &data
[relay_index
+ 6..];
425 let relay_count
= data
.iter().take_while(|b
| (**b
as char) != '
,'
).count();
426 let (relay
, data
) = data
.split_at(relay_count
);
428 // parse the DSN (indicates the delivery status, e.g. 2 == success)
429 // ignore everything after the first digit
430 let dsn_index
= match find(data
, b
"dsn=") {
434 let data
= &data
[dsn_index
+ 4..];
435 let dsn
= match data
.iter().next() {
437 if b
.is_ascii_digit() {
438 (*b
as char).to_digit(10).unwrap()
446 let dstatus
= DStatus
::Dsn(dsn
);
449 .add_to_entry(to
, relay
, dstatus
, parser
.current_record_state
.timestamp
);
451 // here the match happens between a QEntry and the corresponding FEntry
452 // (only after-queue)
453 if &*parser
.current_record_state
.service
== b
"postfix/lmtp" {
454 let sent_index
= match find(data
, b
"status=sent (250 2.") {
458 let mut data
= &data
[sent_index
+ 19..];
459 if data
.starts_with(b
"5.0 OK") {
461 } else if data
.starts_with(b
"7.0 BLOCKED") {
467 // this is the QID of the associated pmg-smtp-filter
468 let (qid
, _
) = match parse_qid(data
, 25) {
473 // add a reference to the filter
474 qe
.borrow_mut().filtered
= true;
476 // if there's a FEntry with the filter QID, check to see if its
477 // qentry matches this one
478 if let Some(fe
) = parser
.fentries
.get(qid
) {
479 qe
.borrow_mut().filter
= Some(Rc
::clone(fe
));
480 // if we use fe.borrow().qentry() directly we run into a borrow
482 let q
= fe
.borrow().qentry();
484 if !Rc
::ptr_eq(&q
, &qe
) {
485 // QEntries don't match, set all flags to false and
486 // remove the referenced FEntry
487 q
.borrow_mut().filtered
= false;
488 q
.borrow_mut().bq_filtered
= false;
489 q
.borrow_mut().filter
= None
;
490 // update FEntry's QEntry reference to the new one
491 fe
.borrow_mut().qentry
= Some(Rc
::downgrade(&qe
));
498 // handle log entries for 'smtpd'
499 fn handle_smtpd_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
500 let se
= get_or_create_sentry(
501 &mut parser
.sentries
,
502 parser
.current_record_state
.pid
,
504 parser
.current_record_state
.timestamp
,
507 if parser
.string_match
{
508 se
.borrow_mut().string_match
= parser
.string_match
;
512 .push((complete_line
.into(), parser
.lines
));
514 if msg
.starts_with(b
"connect from ") {
515 let addr
= &msg
[13..];
516 // set the client address
517 se
.borrow_mut().set_connect(addr
);
521 // on disconnect we can finalize and print the SEntry
522 if msg
.starts_with(b
"disconnect from") {
523 parser
.sentries
.remove(&parser
.current_record_state
.pid
);
524 se
.borrow_mut().disconnected
= true;
526 if se
.borrow_mut().remove_unneeded_refs(parser
) == 0 {
527 // no QEntries referenced in SEntry so just print the SEntry
528 se
.borrow_mut().print(parser
);
529 // free the referenced FEntry (only happens with before-queue)
530 if let Some(f
) = &se
.borrow().filter() {
531 parser
.free_fentry(&f
.borrow().logid
);
533 parser
.free_sentry(se
.borrow().pid
);
535 se
.borrow_mut().finalize_refs(parser
);
540 // NOQUEUE in smtpd, happens after postscreen
541 if msg
.starts_with(b
"NOQUEUE:") {
542 let data
= &msg
[8..];
543 let colon_index
= match find(data
, b
":") {
547 let data
= &data
[colon_index
+ 1..];
548 let colon_index
= match find(data
, b
":") {
552 let data
= &data
[colon_index
+ 1..];
553 let semicolon_index
= match find(data
, b
";") {
558 // check for the string, if it matches then greylisting is the reason
559 // for the NOQUEUE entry
560 let (grey
, data
) = data
.split_at(semicolon_index
);
561 let dstatus
= if find(
563 b
"Recipient address rejected: Service is unavailable (try later)",
572 if !data
.starts_with(b
"; from=<") {
575 let data
= &data
[8..];
576 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
577 let (from
, data
) = data
.split_at(from_count
);
579 if !data
.starts_with(b
"> to=<") {
582 let data
= &data
[6..];
583 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
584 let to
= &data
[..to_count
];
587 .add_noqueue_entry(from
, to
, dstatus
, parser
.current_record_state
.timestamp
);
591 // only happens with before-queue
592 // here we can match the pmg-smtp-filter
593 // 'proxy-accept' happens if it is accepted for AT LEAST ONE receiver
594 if msg
.starts_with(b
"proxy-accept: ") {
595 let data
= &msg
[14..];
596 if !data
.starts_with(b
"END-OF-MESSAGE: ") {
599 let data
= &data
[16..];
600 if !data
.starts_with(b
"250 2.5.0 OK (") {
603 let data
= &data
[14..];
604 if let Some((qid
, data
)) = parse_qid(data
, 25) {
605 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
606 // set the FEntry to before-queue filtered
607 fe
.borrow_mut().is_bq
= true;
608 // if there's no 'accept mail to' entry because of quarantine
609 // we have to match the pmg-smtp-filter here
610 // for 'accepted' mails it is matched in the 'accept mail to'
612 if !fe
.borrow().is_accepted
{
613 // set the SEntry filter reference as we don't have a QEntry
615 se
.borrow_mut().filter
= Some(Rc
::downgrade(&fe
));
617 if let Some(from_index
) = find(data
, b
"from=<") {
618 let data
= &data
[from_index
+ 6..];
619 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
620 let from
= &data
[..from_count
];
621 // keep the from for later printing
622 // required for the correct 'TO:{}:{}...' syntax required
623 // by PMG/API2/MailTracker.pm
624 se
.borrow_mut().bq_from
= from
.into();
626 } else if let Some(qe
) = &fe
.borrow().qentry() {
627 // mail is 'accepted', add a reference to the QEntry to the
628 // SEntry so we can wait for all to be finished before printing
629 qe
.borrow_mut().bq_sentry
= Some(Rc
::clone(&se
));
630 SEntry
::add_ref(&se
, qe
, true);
632 // specify that before queue filtering is used and the mail was
633 // accepted, but not necessarily by an 'accept' rule
635 se
.borrow_mut().is_bq_accepted
= true;
641 // before queue filtering and rejected, here we can match the
642 // pmg-smtp-filter same as in the 'proxy-accept' case
643 // only happens if the mail was rejected for ALL receivers, otherwise
644 // a 'proxy-accept' happens
645 if msg
.starts_with(b
"proxy-reject: ") {
646 let data
= &msg
[14..];
647 if !data
.starts_with(b
"END-OF-MESSAGE: ") {
650 let data
= &data
[16..];
652 // specify that before queue filtering is used and the mail
653 // was rejected for all receivers
654 se
.borrow_mut().is_bq_rejected
= true;
656 if let Some(qid_index
) = find(data
, b
"(") {
657 let data
= &data
[qid_index
+ 1..];
658 if let Some((qid
, _
)) = parse_qid(data
, 25) {
659 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
660 // set the FEntry to before-queue filtered
661 fe
.borrow_mut().is_bq
= true;
662 // we never have a QEntry in this case, so just set the SEntry
664 se
.borrow_mut().filter
= Some(Rc
::downgrade(&fe
));
665 if let Some(from_index
) = find(data
, b
"from=<") {
666 let data
= &data
[from_index
+ 6..];
667 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
668 let from
= &data
[..from_count
];
669 // same as for 'proxy-accept' above
670 se
.borrow_mut().bq_from
= from
.into();
673 } else if let Some(from_index
) = find(data
, b
"from=<") {
674 let data
= &data
[from_index
+ 6..];
675 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
676 let from
= &data
[..from_count
];
677 // same as for 'proxy-accept' above
678 se
.borrow_mut().bq_from
= from
.into();
680 if let Some(to_index
) = find(data
, b
"to=<") {
681 let data
= &data
[to_index
+ 4..];
682 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
683 let to
= &data
[..to_count
];
685 se
.borrow_mut().add_noqueue_entry(
689 parser
.current_record_state
.timestamp
,
697 // with none of the other messages matching, we try for a QID to match the
698 // corresponding QEntry to the SEntry
699 let (qid
, data
) = match parse_qid(msg
, 15) {
703 let data
= &data
[2..];
705 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
707 if parser
.string_match
{
708 qe
.borrow_mut().string_match
= parser
.string_match
;
711 SEntry
::add_ref(&se
, &qe
, false);
713 if !data
.starts_with(b
"client=") {
716 let data
= &data
[7..];
717 let client_count
= data
719 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
721 let client
= &data
[..client_count
];
723 qe
.borrow_mut().set_client(client
);
726 // handle log entries for 'cleanup'
727 // happens before the mail is passed to qmgr (after-queue or before-queue
729 fn handle_cleanup_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
730 let (qid
, data
) = match parse_qid(msg
, 15) {
734 let data
= &data
[2..];
736 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
738 if parser
.string_match
{
739 qe
.borrow_mut().string_match
= parser
.string_match
;
743 .push((complete_line
.into(), parser
.lines
));
745 if !data
.starts_with(b
"message-id=") {
748 let data
= &data
[11..];
749 let msgid_count
= data
751 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
753 let msgid
= &data
[..msgid_count
];
755 if !msgid
.is_empty() {
756 if qe
.borrow().msgid
.is_empty() {
757 qe
.borrow_mut().msgid
= msgid
.into();
759 qe
.borrow_mut().cleanup
= true;
761 // does not work correctly if there's a duplicate message id in the logfiles
762 if let Some(q
) = parser
.msgid_lookup
.remove(msgid
) {
763 let q_clone
= Weak
::clone(&q
);
764 if let Some(q
) = q
.upgrade() {
765 // check to make sure it's not the same QEntry
766 // this can happen if the cleanup line is duplicated in the log
767 if Rc
::ptr_eq(&q
, &qe
) {
768 parser
.msgid_lookup
.insert(msgid
.into(), q_clone
);
770 qe
.borrow_mut().aq_qentry
= Some(q_clone
);
771 q
.borrow_mut().aq_qentry
= Some(Rc
::downgrade(&qe
));
775 parser
.msgid_lookup
.insert(msgid
.into(), Rc
::downgrade(&qe
));
780 #[derive(Default, Debug)]
781 struct NoqueueEntry
{
796 impl Default
for ToEntry
{
797 fn default() -> Self {
799 to
: Default
::default(),
800 relay
: (&b
"none"[..]).into(),
801 dstatus
: Default
::default(),
802 timestamp
: Default
::default(),
807 #[derive(Debug, PartialEq, Copy, Clone)]
821 impl Default
for DStatus
{
822 fn default() -> Self {
827 impl std
::fmt
::Display
for DStatus
{
828 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
830 DStatus
::Invalid
=> '
\0'
, // other default
831 DStatus
::Accept
=> 'A'
,
832 DStatus
::Quarantine
=> 'Q'
,
833 DStatus
::Block
=> 'B'
,
834 DStatus
::Greylist
=> 'G'
,
835 DStatus
::Noqueue
=> 'N'
,
836 DStatus
::BqPass
=> 'P'
,
837 DStatus
::BqDefer
=> 'D'
,
838 DStatus
::BqReject
=> 'R'
,
839 DStatus
::Dsn(v
) => std
::char::from_digit(*v
, 10).unwrap(),
845 #[derive(Debug, Default)]
847 log
: Vec
<(Box
<[u8]>, u64)>,
850 // references to QEntries, Weak so they are not kept alive longer than
851 // necessary, RefCell for mutability (Rc<> is immutable)
852 refs
: Vec
<Weak
<RefCell
<QEntry
>>>,
853 nq_entries
: Vec
<NoqueueEntry
>,
855 // only set in case of before queue filtering
856 // used as a fallback in case no QEntry is referenced
857 filter
: Option
<Weak
<RefCell
<FEntry
>>>,
861 // before queue filtering with the mail accepted for at least one receiver
862 is_bq_accepted
: bool
,
863 // before queue filtering with the mail rejected for all receivers
864 is_bq_rejected
: bool
,
865 // from address saved for compatibility with after queue filtering
870 fn add_noqueue_entry(&mut self, from
: &[u8], to
: &[u8], dstatus
: DStatus
, timestamp
: time_t
) {
871 let ne
= NoqueueEntry
{
877 self.nq_entries
.push(ne
);
880 fn set_connect(&mut self, client
: &[u8]) {
881 if self.connect
.is_empty() {
882 self.connect
= client
.into();
886 // if either 'from' or 'to' are set, check if it matches, if not, set
887 // the status of the noqueue entry to Invalid
888 // if exclude_greylist or exclude_ndr are set, check if it matches
889 // and if so, set the status to Invalid so they are no longer included
890 // don't print if any Invalid entry is found
891 fn filter_matches(&mut self, parser
: &Parser
) -> bool
{
892 if !parser
.options
.from
.is_empty()
893 || !parser
.options
.to
.is_empty()
894 || parser
.options
.exclude_greylist
895 || parser
.options
.exclude_ndr
897 let mut found
= false;
898 for nq
in self.nq_entries
.iter_mut().rev() {
899 if (!parser
.options
.from
.is_empty()
900 && find_lowercase(&nq
.from
, parser
.options
.from
.as_bytes()).is_none())
901 || (parser
.options
.exclude_greylist
&& nq
.dstatus
== DStatus
::Greylist
)
902 || (parser
.options
.exclude_ndr
&& nq
.from
.is_empty())
903 || (!parser
.options
.to
.is_empty()
904 && ((!nq
.to
.is_empty()
905 && find_lowercase(&nq
.to
, parser
.options
.to
.as_bytes()).is_none())
906 || nq
.to
.is_empty()))
908 nq
.dstatus
= DStatus
::Invalid
;
911 if nq
.dstatus
!= DStatus
::Invalid
{
916 // we can early exit the printing if there's no valid Noqueue entry
917 // and we're in the after-queue case
918 if !found
&& self.filter
.is_none() {
922 // self.filter only contains an object in the before-queue case
923 // as we have the FEntry referenced in the SEntry when there's no
924 // queue involved, we can't just check the Noqueue entries, but
925 // have to check for a filter and if it exists, we have to check
926 // them for matching 'from' and 'to' if either of those options
928 // if neither of them is filtered, we can skip this check
929 if let Some(fe
) = &self.filter() {
930 if !parser
.options
.from
.is_empty()
931 && find_lowercase(&self.bq_from
, parser
.options
.from
.as_bytes()).is_none()
935 let to_option_set
= !parser
.options
.to
.is_empty();
936 if to_option_set
&& fe
.borrow().is_bq
&& !fe
.borrow().is_accepted
{
937 fe
.borrow_mut().to_entries
.retain(|to
| {
938 if find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_some() {
953 fn print(&mut self, parser
: &mut Parser
) {
954 // don't print if the output is filtered by the message-id
955 // the message-id is only available in a QEntry
956 if !parser
.options
.msgid
.is_empty() {
960 // don't print if the output is filtered by a host but the connect
961 // field is empty or does not match
962 if !parser
.options
.host
.is_empty() {
963 if self.connect
.is_empty() {
966 if find_lowercase(&self.connect
, parser
.options
.host
.as_bytes()).is_none() {
971 // don't print if the output is filtered by time and line number
973 if !parser
.options
.match_list
.is_empty() {
974 let mut found
= false;
975 for m
in parser
.options
.match_list
.iter() {
977 Match
::Qid(_
) => return,
978 Match
::RelLineNr(t
, l
) => {
979 if *t
== self.timestamp
&& *l
== self.rel_line_nr
{
991 if !self.filter_matches(parser
) {
995 // don't print if there's a string match specified, but none of the log entries matches.
996 // in the before-queue case we also have to check the attached filter for a match
997 if !parser
.options
.string_match
.is_empty() {
998 if let Some(fe
) = &self.filter() {
999 if !self.string_match
&& !fe
.borrow().string_match
{
1002 } else if !self.string_match
{
1007 if parser
.options
.verbose
> 0 {
1008 parser
.write_all_ok(format
!(
1009 "SMTPD: T{:8X}L{:08X}\n",
1010 self.timestamp
, self.rel_line_nr
as u32
1012 parser
.write_all_ok(format
!("CTIME: {:8X}\n", parser
.ctime
).as_bytes());
1014 if !self.connect
.is_empty() {
1015 parser
.write_all_ok(b
"CLIENT: ");
1016 parser
.write_all_ok(&self.connect
);
1017 parser
.write_all_ok(b
"\n");
1021 // only print the entry if the status is not invalid
1022 // rev() for compatibility with the C code which uses a linked list
1023 // that adds entries at the front, while a Vec in Rust adds it at the
1025 for nq
in self.nq_entries
.iter().rev() {
1026 if nq
.dstatus
!= DStatus
::Invalid
{
1027 parser
.write_all_ok(format
!(
1028 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
1029 nq
.timestamp
, self.timestamp
, self.rel_line_nr
, nq
.dstatus
,
1031 parser
.write_all_ok(&nq
.from
);
1032 parser
.write_all_ok(b
"> to <");
1033 parser
.write_all_ok(&nq
.to
);
1034 parser
.write_all_ok(b
">\n");
1039 let print_filter_to_entries_fn
=
1040 |fe
: &Rc
<RefCell
<FEntry
>>, parser
: &mut Parser
, se
: &SEntry
| {
1041 for to
in fe
.borrow().to_entries
.iter().rev() {
1042 parser
.write_all_ok(format
!(
1043 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
1044 to
.timestamp
, se
.timestamp
, se
.rel_line_nr
, to
.dstatus
,
1046 parser
.write_all_ok(&se
.bq_from
);
1047 parser
.write_all_ok(b
"> to <");
1048 parser
.write_all_ok(&to
.to
);
1049 parser
.write_all_ok(b
">\n");
1054 // only true in before queue filtering case
1055 if let Some(fe
) = &self.filter() {
1056 // limited to !fe.is_accepted because otherwise we would have
1057 // a QEntry with all required information instead
1058 if fe
.borrow().is_bq
1059 && !fe
.borrow().is_accepted
1060 && (self.is_bq_accepted
|| self.is_bq_rejected
)
1062 print_filter_to_entries_fn(fe
, parser
, self);
1066 let print_log
= |parser
: &mut Parser
, logs
: &Vec
<(Box
<[u8]>, u64)>| {
1067 for (log
, line
) in logs
.iter() {
1068 parser
.write_all_ok(format
!("L{:08X} ", *line
as u32));
1069 parser
.write_all_ok(log
);
1070 parser
.write_all_ok(b
"\n");
1074 // if '-vv' is passed to the log tracker, print all the logs
1075 if parser
.options
.verbose
> 1 {
1076 parser
.write_all_ok(b
"LOGS:\n");
1077 let mut logs
= self.log
.clone();
1078 if let Some(f
) = &self.filter() {
1079 logs
.append(&mut f
.borrow().log
.clone());
1080 // as the logs come from 1 SEntry and 1 FEntry,
1081 // interleave them via sort based on line number
1082 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1085 print_log(parser
, &logs
);
1087 parser
.write_all_ok(b
"\n");
1090 fn delete_ref(&mut self, qentry
: &Rc
<RefCell
<QEntry
>>) {
1091 self.refs
.retain(|q
| {
1092 let q
= match q
.upgrade() {
1094 None
=> return false,
1096 if Rc
::ptr_eq(&q
, qentry
) {
1103 fn remove_unneeded_refs(&mut self, parser
: &mut Parser
) -> u32 {
1104 let mut count
: u32 = 0;
1105 let mut to_delete
= Vec
::new();
1106 self.refs
.retain(|q
| {
1107 let q
= match q
.upgrade() {
1109 None
=> return false,
1111 let is_cleanup
= q
.borrow().cleanup
;
1112 // add those that require freeing to a separate Vec as self is
1113 // borrowed mutable here and can't be borrowed again for the
1114 // parser.free_qentry() call
1124 for q
in to_delete
.iter().rev() {
1125 parser
.free_qentry(&q
.borrow().qid
, Some(self));
1130 // print and free all QEntries that are removed and if a filter is set,
1131 // if the filter is finished
1132 fn finalize_refs(&mut self, parser
: &mut Parser
) {
1133 let mut qentries
= Vec
::new();
1134 for q
in self.refs
.iter() {
1135 let q
= match q
.upgrade() {
1140 if !q
.borrow().removed
{
1144 let fe
= &q
.borrow().filter
;
1145 if let Some(f
) = fe
{
1146 if !q
.borrow().bq_filtered
&& !f
.borrow().finished
{
1151 if !self.is_bq_accepted
&& q
.borrow().bq_sentry
.is_some() {
1152 if let Some(se
) = &q
.borrow().bq_sentry
{
1153 // we're already disconnected, but the SEntry referenced
1154 // by the QEntry might not yet be done
1155 if !se
.borrow().disconnected
{
1156 // add a reference to the SEntry referenced by the
1157 // QEntry so it gets deleted when both the SEntry
1158 // and the QEntry is done
1159 Self::add_ref(se
, &q
, true);
1165 qentries
.push(Rc
::clone(&q
));
1168 for q
in qentries
.iter().rev() {
1169 q
.borrow_mut().print(parser
, Some(self));
1170 parser
.free_qentry(&q
.borrow().qid
, Some(self));
1172 if let Some(f
) = &q
.borrow().filter
{
1173 parser
.free_fentry(&f
.borrow().logid
);
1178 fn add_ref(sentry
: &Rc
<RefCell
<SEntry
>>, qentry
: &Rc
<RefCell
<QEntry
>>, bq
: bool
) {
1179 let smtpd
= qentry
.borrow().smtpd
.clone();
1181 if let Some(s
) = smtpd
{
1182 if !Rc
::ptr_eq(sentry
, &s
) {
1183 eprintln
!("Error: qentry ref already set");
1189 for q
in sentry
.borrow().refs
.iter() {
1190 let q
= match q
.upgrade() {
1194 if Rc
::ptr_eq(&q
, qentry
) {
1199 sentry
.borrow_mut().refs
.push(Rc
::downgrade(qentry
));
1201 qentry
.borrow_mut().smtpd
= Some(Rc
::clone(sentry
));
1205 fn filter(&self) -> Option
<Rc
<RefCell
<FEntry
>>> {
1206 self.filter
.clone().and_then(|f
| f
.upgrade())
1210 #[derive(Default, Debug)]
1212 log
: Vec
<(Box
<[u8]>, u64)>,
1213 smtpd
: Option
<Rc
<RefCell
<SEntry
>>>,
1214 filter
: Option
<Rc
<RefCell
<FEntry
>>>,
1220 to_entries
: Vec
<ToEntry
>,
1226 // will differ from smtpd
1227 bq_sentry
: Option
<Rc
<RefCell
<SEntry
>>>,
1228 aq_qentry
: Option
<Weak
<RefCell
<QEntry
>>>,
1232 fn add_to_entry(&mut self, to
: &[u8], relay
: &[u8], dstatus
: DStatus
, timestamp
: time_t
) {
1235 relay
: relay
.into(),
1239 self.to_entries
.push(te
);
1242 // finalize and print the QEntry
1243 fn finalize(&mut self, parser
: &mut Parser
) {
1244 // if it is not removed, skip
1246 if let Some(se
) = &self.smtpd
{
1247 // verify that the SEntry it is attached to is disconnected
1248 if !se
.borrow().disconnected
{
1252 if let Some(s
) = &self.bq_sentry
{
1253 if self.bq_filtered
&& !s
.borrow().disconnected
{
1258 if let Some(qe
) = &self.aq_qentry
{
1259 if let Some(qe
) = qe
.upgrade() {
1260 if !qe
.borrow().removed
{
1263 qe
.borrow_mut().aq_qentry
= None
;
1264 qe
.borrow_mut().finalize(parser
);
1268 if let Some(fe
) = self.filter
.clone() {
1269 // verify that the attached FEntry is finished if it is not
1270 // before queue filtered
1271 if !self.bq_filtered
&& !fe
.borrow().finished
{
1275 // if there's an SEntry, print with the SEntry
1276 // otherwise just print the QEntry (this can happen in certain
1278 match self.smtpd
.clone() {
1279 Some(s
) => self.print(parser
, Some(&*s
.borrow())),
1280 None
=> self.print(parser
, None
),
1282 if let Some(se
) = &self.smtpd
{
1283 parser
.free_qentry(&self.qid
, Some(&mut *se
.borrow_mut()));
1285 parser
.free_qentry(&self.qid
, None
);
1288 if !self.bq_filtered
{
1289 parser
.free_fentry(&fe
.borrow().logid
);
1291 } else if let Some(s
) = self.smtpd
.clone() {
1292 self.print(parser
, Some(&*s
.borrow()));
1293 parser
.free_qentry(&self.qid
, Some(&mut *s
.borrow_mut()));
1295 self.print(parser
, None
);
1296 parser
.free_qentry(&self.qid
, None
);
1301 fn msgid_matches(&self, parser
: &Parser
) -> bool
{
1302 if !parser
.options
.msgid
.is_empty() {
1303 if self.msgid
.is_empty() {
1306 let qentry_msgid_lowercase
= self.msgid
.to_ascii_lowercase();
1307 let msgid_lowercase
= parser
.options
.msgid
.as_bytes().to_ascii_lowercase();
1308 if qentry_msgid_lowercase
!= msgid_lowercase
{
1315 fn match_list_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1316 let fe
= &self.filter
;
1317 if !parser
.options
.match_list
.is_empty() {
1318 let mut found
= false;
1319 for m
in parser
.options
.match_list
.iter() {
1322 if let Some(f
) = fe
{
1323 if &f
.borrow().logid
== q
{
1333 Match
::RelLineNr(t
, l
) => {
1334 if let Some(s
) = se
{
1335 if s
.timestamp
== *t
&& s
.rel_line_nr
== *l
{
1350 fn host_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1351 if !parser
.options
.host
.is_empty() {
1352 let mut found
= false;
1353 if let Some(s
) = se
{
1354 if !s
.connect
.is_empty()
1355 && find_lowercase(&s
.connect
, parser
.options
.host
.as_bytes()).is_some()
1360 if !self.client
.is_empty()
1361 && find_lowercase(&self.client
, parser
.options
.host
.as_bytes()).is_some()
1373 fn from_to_matches(&mut self, parser
: &Parser
) -> bool
{
1374 if !parser
.options
.from
.is_empty() {
1375 if self.from
.is_empty() {
1378 if find_lowercase(&self.from
, parser
.options
.from
.as_bytes()).is_none() {
1381 } else if parser
.options
.exclude_ndr
&& self.from
.is_empty() {
1385 if !parser
.options
.to
.is_empty() {
1386 let mut found
= false;
1387 self.to_entries
.retain(|to
| {
1388 if find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_none() {
1395 if let Some(fe
) = &self.filter
{
1396 fe
.borrow_mut().to_entries
.retain(|to
| {
1397 if find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_none() {
1412 fn string_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1413 let fe
= &self.filter
;
1414 if !parser
.options
.string_match
.is_empty() {
1415 let mut string_match
= self.string_match
;
1417 if let Some(s
) = se
{
1419 string_match
= true;
1422 if let Some(f
) = fe
{
1423 if f
.borrow().string_match
{
1424 string_match
= true;
1434 // is_se_bq_sentry is true if the QEntry::bq_sentry is the same as passed
1435 // into the print() function via reference
1436 fn print_qentry_boilerplate(
1438 parser
: &mut Parser
,
1439 is_se_bq_sentry
: bool
,
1440 se
: Option
<&SEntry
>,
1442 parser
.write_all_ok(b
"QENTRY: ");
1443 parser
.write_all_ok(&self.qid
);
1444 parser
.write_all_ok(b
"\n");
1445 parser
.write_all_ok(format
!("CTIME: {:8X}\n", parser
.ctime
));
1446 parser
.write_all_ok(format
!("SIZE: {}\n", self.size
));
1448 if !self.client
.is_empty() {
1449 parser
.write_all_ok(b
"CLIENT: ");
1450 parser
.write_all_ok(&self.client
);
1451 parser
.write_all_ok(b
"\n");
1452 } else if !is_se_bq_sentry
{
1453 if let Some(s
) = se
{
1454 if !s
.connect
.is_empty() {
1455 parser
.write_all_ok(b
"CLIENT: ");
1456 parser
.write_all_ok(&s
.connect
);
1457 parser
.write_all_ok(b
"\n");
1460 } else if let Some(s
) = &self.smtpd
{
1461 if !s
.borrow().connect
.is_empty() {
1462 parser
.write_all_ok(b
"CLIENT: ");
1463 parser
.write_all_ok(&s
.borrow().connect
);
1464 parser
.write_all_ok(b
"\n");
1468 if !self.msgid
.is_empty() {
1469 parser
.write_all_ok(b
"MSGID: ");
1470 parser
.write_all_ok(&self.msgid
);
1471 parser
.write_all_ok(b
"\n");
1475 fn print(&mut self, parser
: &mut Parser
, se
: Option
<&SEntry
>) {
1476 let fe
= self.filter
.clone();
1478 if !self.msgid_matches(parser
)
1479 || !self.match_list_matches(parser
, se
)
1480 || !self.host_matches(parser
, se
)
1481 || !self.from_to_matches(parser
)
1482 || !self.string_matches(parser
, se
)
1487 // necessary so we do not attempt to mutable borrow it a second time
1489 let is_se_bq_sentry
= match (&self.bq_sentry
, se
) {
1490 (Some(s
), Some(se
)) => std
::ptr
::eq(s
.as_ptr(), se
),
1494 if is_se_bq_sentry
{
1495 if let Some(s
) = &se
{
1496 if !s
.disconnected
{
1502 if parser
.options
.verbose
> 0 {
1503 self.print_qentry_boilerplate(parser
, is_se_bq_sentry
, se
);
1506 if self.bq_filtered
{
1507 for to
in self.to_entries
.iter_mut() {
1508 to
.dstatus
= match to
.dstatus
{
1509 // the dsn (enhanced status code can only have a class of 2, 4 or 5
1510 // see https://tools.ietf.org/html/rfc3463
1511 DStatus
::Dsn(2) => DStatus
::BqPass
,
1512 DStatus
::Dsn(4) => DStatus
::BqDefer
,
1513 DStatus
::Dsn(5) => DStatus
::BqReject
,
1519 // rev() to match the C code iteration direction (linked list vs Vec)
1520 for to
in self.to_entries
.iter().rev() {
1521 if !to
.to
.is_empty() {
1524 let mut final_to
: &ToEntry
= to
;
1526 // if status == success and there's a filter attached that has
1527 // a matching 'to' in one of the ToEntries, set the ToEntry to
1528 // the one in the filter
1529 if to
.dstatus
== DStatus
::Dsn(2) {
1530 if let Some(f
) = &fe
{
1531 if !self.bq_filtered
|| (f
.borrow().finished
&& f
.borrow().is_bq
) {
1533 final_borrow
= final_rc
.borrow();
1534 for to2
in final_borrow
.to_entries
.iter().rev() {
1535 if to
.to
== to2
.to
{
1544 parser
.write_all_ok(format
!("TO:{:X}:", to
.timestamp
));
1545 parser
.write_all_ok(&self.qid
);
1546 parser
.write_all_ok(format
!(":{}: from <", final_to
.dstatus
));
1547 parser
.write_all_ok(&self.from
);
1548 parser
.write_all_ok(b
"> to <");
1549 parser
.write_all_ok(&final_to
.to
);
1550 parser
.write_all_ok(b
"> (");
1551 // if we use the relay from the filter ToEntry, it will be
1552 // marked 'is_relay' in PMG/API2/MailTracker.pm and not shown
1553 // in the GUI in the case of before queue filtering
1554 if !self.bq_filtered
{
1555 parser
.write_all_ok(&final_to
.relay
);
1557 parser
.write_all_ok(&to
.relay
);
1559 parser
.write_all_ok(b
")\n");
1564 if self.bq_filtered
{
1565 if let Some(fe
) = &fe
{
1566 if fe
.borrow().finished
&& fe
.borrow().is_bq
{
1567 fe
.borrow_mut().to_entries
.retain(|to
| {
1568 for to2
in self.to_entries
.iter().rev() {
1569 if to
.to
== to2
.to
{
1576 for to
in fe
.borrow().to_entries
.iter().rev() {
1577 parser
.write_all_ok(format
!("TO:{:X}:", to
.timestamp
));
1578 parser
.write_all_ok(&self.qid
);
1579 parser
.write_all_ok(format
!(":{}: from <", to
.dstatus
));
1580 parser
.write_all_ok(&self.from
);
1581 parser
.write_all_ok(b
"> to <");
1582 parser
.write_all_ok(&to
.to
);
1583 parser
.write_all_ok(b
"> (");
1584 parser
.write_all_ok(&to
.relay
);
1585 parser
.write_all_ok(b
")\n");
1592 // print logs if '-vv' is specified
1593 if parser
.options
.verbose
> 1 {
1594 let print_log
= |parser
: &mut Parser
, logs
: &Vec
<(Box
<[u8]>, u64)>| {
1595 for (log
, line
) in logs
.iter() {
1596 parser
.write_all_ok(format
!("L{:08X} ", *line
as u32));
1597 parser
.write_all_ok(log
);
1598 parser
.write_all_ok(b
"\n");
1601 if !is_se_bq_sentry
{
1602 if let Some(s
) = se
{
1603 let mut logs
= s
.log
.clone();
1604 if let Some(bq_se
) = &self.bq_sentry
{
1605 logs
.append(&mut bq_se
.borrow().log
.clone());
1606 // as the logs come from 2 different SEntries,
1607 // interleave them via sort based on line number
1608 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1610 if !logs
.is_empty() {
1611 parser
.write_all_ok(b
"SMTP:\n");
1612 print_log(parser
, &logs
);
1615 } else if let Some(s
) = &self.smtpd
{
1616 let mut logs
= s
.borrow().log
.clone();
1617 if let Some(se
) = se
{
1618 logs
.append(&mut se
.log
.clone());
1619 // as the logs come from 2 different SEntries,
1620 // interleave them via sort based on line number
1621 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1623 if !logs
.is_empty() {
1624 parser
.write_all_ok(b
"SMTP:\n");
1625 print_log(parser
, &logs
);
1629 if let Some(f
) = fe
{
1630 if (!self.bq_filtered
|| (f
.borrow().finished
&& f
.borrow().is_bq
))
1631 && !f
.borrow().log
.is_empty()
1633 parser
.write_all_ok(format
!("FILTER: {}\n", unsafe {
1634 std
::str::from_utf8_unchecked(&f
.borrow().logid
)
1636 print_log(parser
, &f
.borrow().log
);
1640 if !self.log
.is_empty() {
1641 parser
.write_all_ok(b
"QMGR:\n");
1642 self.log
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1643 print_log(parser
, &self.log
);
1646 parser
.write_all_ok(b
"\n")
1649 fn set_client(&mut self, client
: &[u8]) {
1650 if self.client
.is_empty() {
1651 self.client
= client
.into();
1656 #[derive(Default, Debug)]
1658 log
: Vec
<(Box
<[u8]>, u64)>,
1660 to_entries
: Vec
<ToEntry
>,
1661 processing_time
: Box
<[u8]>,
1665 qentry
: Option
<Weak
<RefCell
<QEntry
>>>,
1670 fn add_accept(&mut self, to
: &[u8], qid
: &[u8], timestamp
: time_t
) {
1674 dstatus
: DStatus
::Accept
,
1677 self.to_entries
.push(te
);
1678 self.is_accepted
= true;
1681 fn add_quarantine(&mut self, to
: &[u8], qid
: &[u8], timestamp
: time_t
) {
1685 dstatus
: DStatus
::Quarantine
,
1688 self.to_entries
.push(te
);
1691 fn add_block(&mut self, to
: &[u8], timestamp
: time_t
) {
1694 relay
: (&b
"none"[..]).into(),
1695 dstatus
: DStatus
::Block
,
1698 self.to_entries
.push(te
);
1701 fn set_processing_time(&mut self, time
: &[u8]) {
1702 self.processing_time
= time
.into();
1703 self.finished
= true;
1706 fn qentry(&self) -> Option
<Rc
<RefCell
<QEntry
>>> {
1707 self.qentry
.clone().and_then(|q
| q
.upgrade())
1713 sentries
: HashMap
<u64, Rc
<RefCell
<SEntry
>>>,
1714 fentries
: HashMap
<Box
<[u8]>, Rc
<RefCell
<FEntry
>>>,
1715 qentries
: HashMap
<Box
<[u8]>, Rc
<RefCell
<QEntry
>>>,
1716 msgid_lookup
: HashMap
<Box
<[u8]>, Weak
<RefCell
<QEntry
>>>,
1718 smtp_tls_log_by_pid
: HashMap
<u64, (Box
<[u8]>, u64)>,
1720 current_record_state
: RecordState
,
1725 current_file_index
: usize,
1729 buffered_stdout
: BufWriter
<std
::io
::Stdout
>,
1743 fn new() -> Result
<Self, Error
> {
1744 let ltime
= Tm
::now_local()?
;
1747 sentries
: HashMap
::new(),
1748 fentries
: HashMap
::new(),
1749 qentries
: HashMap
::new(),
1750 msgid_lookup
: HashMap
::new(),
1751 smtp_tls_log_by_pid
: HashMap
::new(),
1752 current_record_state
: Default
::default(),
1754 current_year
: (ltime
.tm_year
+ 1900) as i64,
1755 current_month
: ltime
.tm_mon
as i64,
1756 current_file_index
: 0,
1758 buffered_stdout
: BufWriter
::with_capacity(4 * 1024 * 1024, std
::io
::stdout()),
1759 options
: Options
::default(),
1760 start_tm
: Tm
::zero(),
1763 string_match
: false,
1768 fn free_sentry(&mut self, sentry_pid
: u64) {
1769 self.sentries
.remove(&sentry_pid
);
1772 fn free_qentry(&mut self, qid
: &[u8], se
: Option
<&mut SEntry
>) {
1773 if let Some(qe
) = self.qentries
.get(qid
) {
1774 if let Some(se
) = se
{
1779 self.qentries
.remove(qid
);
1782 fn free_fentry(&mut self, fentry_logid
: &[u8]) {
1783 self.fentries
.remove(fentry_logid
);
1786 fn parse_files(&mut self) -> Result
<(), Error
> {
1787 if !self.options
.inputfile
.is_empty() {
1788 if self.options
.inputfile
== "-" {
1790 self.current_file_index
= 0;
1791 let mut reader
= BufReader
::new(std
::io
::stdin());
1792 self.handle_input_by_line(&mut reader
)?
;
1793 } else if let Ok(file
) = File
::open(&self.options
.inputfile
) {
1794 // read from specified file
1795 self.current_file_index
= 0;
1796 let mut reader
= BufReader
::new(file
);
1797 self.handle_input_by_line(&mut reader
)?
;
1800 let filecount
= self.count_files_in_time_range();
1801 for i
in (0..filecount
).rev() {
1802 if let Ok(file
) = File
::open(LOGFILES
[i
]) {
1803 self.current_file_index
= i
;
1805 let gzdecoder
= read
::GzDecoder
::new(file
);
1806 let mut reader
= BufReader
::new(gzdecoder
);
1807 self.handle_input_by_line(&mut reader
)?
;
1809 let mut reader
= BufReader
::new(file
);
1810 self.handle_input_by_line(&mut reader
)?
;
1819 fn handle_input_by_line(&mut self, reader
: &mut dyn BufRead
) -> Result
<(), Error
> {
1820 let mut buffer
= Vec
::<u8>::with_capacity(4096);
1821 let mut prev_time
= 0;
1823 if self.options
.limit
> 0 && (self.count
>= self.options
.limit
) {
1824 self.write_all_ok("STATUS: aborted by limit (too many hits)\n");
1825 self.buffered_stdout
.flush()?
;
1826 std
::process
::exit(0);
1830 let size
= match reader
.read_until(b'
\n'
, &mut buffer
) {
1831 Err(e
) => return Err(e
.into()),
1832 Ok(0) => return Ok(()),
1835 // size includes delimiter
1836 let line
= &buffer
[0..size
- 1];
1837 let complete_line
= line
;
1839 let (time
, line
) = match parse_time(line
, self.current_year
, self.current_month
) {
1844 // relative line number within a single timestamp
1845 if time
!= prev_time
{
1846 self.rel_line_nr
= 0;
1848 self.rel_line_nr
+= 1;
1852 // skip until we're in the specified time frame
1853 if time
< self.options
.start
{
1856 // past the specified time frame, we're done, exit the loop
1857 if time
> self.options
.end
{
1863 let (host
, service
, pid
, line
) = match parse_host_service_pid(line
) {
1864 Some((h
, s
, p
, l
)) => (h
, s
, p
, l
),
1870 self.current_record_state
.host
= host
.into();
1871 self.current_record_state
.service
= service
.into();
1872 self.current_record_state
.pid
= pid
;
1873 self.current_record_state
.timestamp
= time
;
1875 self.string_match
= false;
1876 if !self.options
.string_match
.is_empty()
1877 && find_lowercase(complete_line
, self.options
.string_match
.as_bytes()).is_some()
1879 self.string_match
= true;
1882 // complete_line required for the logs
1883 if service
== b
"pmg-smtp-filter" {
1884 handle_pmg_smtp_filter_message(line
, self, complete_line
);
1885 } else if service
== b
"postfix/postscreen" {
1886 handle_postscreen_message(line
, self, complete_line
);
1887 } else if service
== b
"postfix/qmgr" {
1888 handle_qmgr_message(line
, self, complete_line
);
1889 } else if service
== b
"postfix/lmtp"
1890 || service
== b
"postfix/smtp"
1891 || service
== b
"postfix/local"
1892 || service
== b
"postfix/error"
1894 handle_lmtp_message(line
, self, complete_line
);
1895 } else if service
== b
"postfix/smtpd" {
1896 handle_smtpd_message(line
, self, complete_line
);
1897 } else if service
== b
"postfix/cleanup" {
1898 handle_cleanup_message(line
, self, complete_line
);
1904 /// Returns the number of files to parse. Does not error out if it can't access any file
1905 /// (permission denied)
1906 fn count_files_in_time_range(&mut self) -> usize {
1908 let mut buffer
= Vec
::new();
1910 for (i
, item
) in LOGFILES
.iter().enumerate() {
1912 if let Ok(file
) = File
::open(item
) {
1913 self.current_file_index
= i
;
1916 let gzdecoder
= read
::GzDecoder
::new(file
);
1917 let mut reader
= BufReader
::new(gzdecoder
);
1918 // check the first line
1919 if let Ok(size
) = reader
.read_until(b'
\n'
, &mut buffer
) {
1923 if let Some((time
, _
)) =
1924 parse_time(&buffer
[0..size
], self.current_year
, self.current_month
)
1926 // found the earliest file in the time frame
1927 if time
< self.options
.start
{
1935 let mut reader
= BufReader
::new(file
);
1936 if let Ok(size
) = reader
.read_until(b'
\n'
, &mut buffer
) {
1940 if let Some((time
, _
)) =
1941 parse_time(&buffer
[0..size
], self.current_year
, self.current_month
)
1943 if time
< self.options
.start
{
1959 fn handle_args(&mut self, args
: clap
::ArgMatches
) -> Result
<(), Error
> {
1960 if let Some(inputfile
) = args
.value_of("inputfile") {
1961 self.options
.inputfile
= inputfile
.to_string();
1964 if let Some(start
) = args
.value_of("start") {
1965 if let Ok(res
) = time
::strptime(start
, c_str
!("%F %T")) {
1966 self.options
.start
= res
.as_utc_to_epoch();
1967 self.start_tm
= res
;
1968 } else if let Ok(res
) = time
::strptime(start
, c_str
!("%s")) {
1969 self.options
.start
= res
.as_utc_to_epoch();
1970 self.start_tm
= res
;
1972 bail
!("failed to parse start time");
1975 let mut ltime
= Tm
::now_local()?
;
1979 self.options
.start
= ltime
.as_utc_to_epoch();
1980 self.start_tm
= ltime
;
1983 if let Some(end
) = args
.value_of("end") {
1984 if let Ok(res
) = time
::strptime(end
, c_str
!("%F %T")) {
1985 self.options
.end
= res
.as_utc_to_epoch();
1987 } else if let Ok(res
) = time
::strptime(end
, c_str
!("%s")) {
1988 self.options
.end
= res
.as_utc_to_epoch();
1991 bail
!("failed to parse end time");
1994 self.options
.end
= unsafe { libc::time(std::ptr::null_mut()) }
;
1995 self.end_tm
= Tm
::at_local(self.options
.end
)?
;
1998 if self.options
.end
< self.options
.start
{
1999 bail
!("end time before start time");
2002 self.options
.limit
= match args
.value_of("limit") {
2003 Some(l
) => l
.parse().unwrap(),
2007 if let Some(qids
) = args
.values_of("qids") {
2009 let ltime
: time_t
= 0;
2010 let rel_line_nr
: libc
::c_ulong
= 0;
2011 let input
= CString
::new(q
)?
;
2012 let bytes
= concat
!("T%08lXL%08lX", "\0");
2014 unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
;
2016 libc
::sscanf(input
.as_ptr(), format
.as_ptr(), <ime
, &rel_line_nr
) == 2
2020 .push(Match
::RelLineNr(ltime
, rel_line_nr
));
2024 .push(Match
::Qid(q
.as_bytes().into()));
2029 if let Some(from
) = args
.value_of("from") {
2030 self.options
.from
= from
.to_string();
2032 if let Some(to
) = args
.value_of("to") {
2033 self.options
.to
= to
.to_string();
2035 if let Some(host
) = args
.value_of("host") {
2036 self.options
.host
= host
.to_string();
2038 if let Some(msgid
) = args
.value_of("msgid") {
2039 self.options
.msgid
= msgid
.to_string();
2042 self.options
.exclude_greylist
= args
.is_present("exclude_greylist");
2043 self.options
.exclude_ndr
= args
.is_present("exclude_ndr");
2045 self.options
.verbose
= args
.get_count("verbose") as _
;
2047 if let Some(string_match
) = args
.value_of("search") {
2048 self.options
.string_match
= string_match
.to_string();
2054 fn write_all_ok
<T
: AsRef
<[u8]>>(&mut self, data
: T
) {
2055 self.buffered_stdout
2056 .write_all(data
.as_ref())
2057 .expect("failed to write to stdout");
2061 impl Drop
for Parser
{
2062 fn drop(&mut self) {
2063 let mut qentries
= std
::mem
::take(&mut self.qentries
);
2064 for q
in qentries
.values() {
2065 let smtpd
= q
.borrow().smtpd
.clone();
2066 if let Some(s
) = smtpd
{
2067 q
.borrow_mut().print(self, Some(&*s
.borrow()));
2069 q
.borrow_mut().print(self, None
);
2073 let mut sentries
= std
::mem
::take(&mut self.sentries
);
2074 for s
in sentries
.values() {
2075 s
.borrow_mut().print(self);
2081 #[derive(Debug, Default)]
2083 match_list
: Vec
<Match
>,
2085 string_match
: String
,
2094 exclude_greylist
: bool
,
2101 RelLineNr(time_t
, u64),
2104 #[derive(Debug, Default)]
2105 struct RecordState
{
2112 fn get_or_create_qentry(
2113 qentries
: &mut HashMap
<Box
<[u8]>, Rc
<RefCell
<QEntry
>>>,
2115 ) -> Rc
<RefCell
<QEntry
>> {
2116 if let Some(qe
) = qentries
.get(qid
) {
2119 let qe
= Rc
::new(RefCell
::new(QEntry
::default()));
2120 qe
.borrow_mut().qid
= qid
.into();
2121 qentries
.insert(qid
.into(), qe
.clone());
2126 fn get_or_create_sentry(
2127 sentries
: &mut HashMap
<u64, Rc
<RefCell
<SEntry
>>>,
2131 ) -> Rc
<RefCell
<SEntry
>> {
2132 if let Some(se
) = sentries
.get(&pid
) {
2135 let se
= Rc
::new(RefCell
::new(SEntry
::default()));
2136 se
.borrow_mut().rel_line_nr
= rel_line_nr
;
2137 se
.borrow_mut().timestamp
= timestamp
;
2138 sentries
.insert(pid
, se
.clone());
2143 fn get_or_create_fentry(
2144 fentries
: &mut HashMap
<Box
<[u8]>, Rc
<RefCell
<FEntry
>>>,
2146 ) -> Rc
<RefCell
<FEntry
>> {
2147 if let Some(fe
) = fentries
.get(qid
) {
2150 let fe
= Rc
::new(RefCell
::new(FEntry
::default()));
2151 fe
.borrow_mut().logid
= qid
.into();
2152 fentries
.insert(qid
.into(), fe
.clone());
2157 const LOGFILES
: [&str; 32] = [
2159 "/var/log/syslog.1",
2160 "/var/log/syslog.2.gz",
2161 "/var/log/syslog.3.gz",
2162 "/var/log/syslog.4.gz",
2163 "/var/log/syslog.5.gz",
2164 "/var/log/syslog.6.gz",
2165 "/var/log/syslog.7.gz",
2166 "/var/log/syslog.8.gz",
2167 "/var/log/syslog.9.gz",
2168 "/var/log/syslog.10.gz",
2169 "/var/log/syslog.11.gz",
2170 "/var/log/syslog.12.gz",
2171 "/var/log/syslog.13.gz",
2172 "/var/log/syslog.14.gz",
2173 "/var/log/syslog.15.gz",
2174 "/var/log/syslog.16.gz",
2175 "/var/log/syslog.17.gz",
2176 "/var/log/syslog.18.gz",
2177 "/var/log/syslog.19.gz",
2178 "/var/log/syslog.20.gz",
2179 "/var/log/syslog.21.gz",
2180 "/var/log/syslog.22.gz",
2181 "/var/log/syslog.23.gz",
2182 "/var/log/syslog.24.gz",
2183 "/var/log/syslog.25.gz",
2184 "/var/log/syslog.26.gz",
2185 "/var/log/syslog.27.gz",
2186 "/var/log/syslog.28.gz",
2187 "/var/log/syslog.29.gz",
2188 "/var/log/syslog.30.gz",
2189 "/var/log/syslog.31.gz",
2192 /// Parse a QID ([A-Z]+). Returns a tuple of (qid, remaining_text) or None.
2193 fn parse_qid(data
: &[u8], max
: usize) -> Option
<(&[u8], &[u8])> {
2194 // to simplify limit max to data.len()
2195 let max
= max
.min(data
.len());
2196 // take at most max, find the first non-hex-digit
2197 match data
.iter().take(max
).position(|b
| !b
.is_ascii_hexdigit()) {
2198 // if there were less than 5 return nothing
2199 // the QID always has at least 5 characters for the microseconds (see
2200 // http://www.postfix.org/postconf.5.html#enable_long_queue_ids)
2201 Some(n
) if n
< 5 => None
,
2202 // otherwise split at the first non-hex-digit
2203 Some(n
) => Some(data
.split_at(n
)),
2204 // or return 'max' length QID if no non-hex-digit is found
2205 None
=> Some(data
.split_at(max
)),
2209 /// Parse a number. Returns a tuple of (parsed_number, remaining_text) or None.
2210 fn parse_number(data
: &[u8], max_digits
: usize) -> Option
<(usize, &[u8])> {
2211 let max
= max_digits
.min(data
.len());
2216 match data
.iter().take(max
).position(|b
| !b
.is_ascii_digit()) {
2217 Some(n
) if n
== 0 => None
,
2219 let (number
, data
) = data
.split_at(n
);
2220 // number only contains ascii digits
2221 let number
= unsafe { std::str::from_utf8_unchecked(number) }
2224 Some((number
, data
))
2227 let (number
, data
) = data
.split_at(max
);
2228 // number only contains ascii digits
2229 let number
= unsafe { std::str::from_utf8_unchecked(number) }
2232 Some((number
, data
))
2237 /// Parse time. Returns a tuple of (parsed_time, remaining_text) or None.
2238 fn parse_time(data
: &'_
[u8], cur_year
: i64, cur_month
: i64) -> Option
<(time_t
, &'_
[u8])> {
2239 if data
.len() < 15 {
2243 let mon
= match &data
[0..3] {
2258 let data
= &data
[3..];
2260 // assume smaller month now than in log line means yearwrap
2261 let mut year
= if cur_month
< mon
{
2267 let mut ltime
: time_t
= (year
- 1970) * 365 + CAL_MTOD
[mon
as usize];
2269 // leap year considerations
2273 ltime
+= (year
- 1968) / 4;
2274 ltime
-= (year
- 1900) / 100;
2275 ltime
+= (year
- 1600) / 400;
2277 let whitespace_count
= data
.iter().take_while(|b
| b
.is_ascii_whitespace()).count();
2278 let data
= &data
[whitespace_count
..];
2280 let (mday
, data
) = match parse_number(data
, 2) {
2282 None
=> return None
,
2288 ltime
+= (mday
- 1) as i64;
2290 if data
.is_empty() {
2294 let data
= &data
[1..];
2296 let (hour
, data
) = match parse_number(data
, 2) {
2298 None
=> return None
,
2302 ltime
+= hour
as i64;
2304 if let Some(c
) = data
.iter().next() {
2305 if (*c
as char) != '
:'
{
2311 let data
= &data
[1..];
2313 let (min
, data
) = match parse_number(data
, 2) {
2315 None
=> return None
,
2319 ltime
+= min
as i64;
2321 if let Some(c
) = data
.iter().next() {
2322 if (*c
as char) != '
:'
{
2328 let data
= &data
[1..];
2330 let (sec
, data
) = match parse_number(data
, 2) {
2332 None
=> return None
,
2336 ltime
+= sec
as i64;
2338 let data
= match data
.len() {
2346 type ByteSlice
<'a
> = &'a
[u8];
2347 /// Parse Host, Service and PID at the beginning of data. Returns a tuple of (host, service, pid, remaining_text).
2348 fn parse_host_service_pid(data
: &[u8]) -> Option
<(ByteSlice
, ByteSlice
, u64, ByteSlice
)> {
2349 let host_count
= data
2351 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
2353 let host
= &data
[0..host_count
];
2354 let data
= &data
[host_count
+ 1..]; // whitespace after host
2356 let service_count
= data
2359 (**b
as char).is_ascii_alphabetic() || (**b
as char) == '
/'
|| (**b
as char) == '
-'
2362 let service
= &data
[0..service_count
];
2363 let data
= &data
[service_count
..];
2364 if data
.get(0) != Some(&b'
['
) {
2367 let data
= &data
[1..];
2369 let pid_count
= data
2371 .take_while(|b
| (**b
as char).is_ascii_digit())
2373 let pid
= match unsafe { std::str::from_utf8_unchecked(&data[0..pid_count]) }
.parse() {
2374 // all ascii digits so valid utf8
2376 Err(_
) => return None
,
2378 let data
= &data
[pid_count
..];
2379 if !data
.starts_with(b
"]: ") {
2382 let data
= &data
[3..];
2384 Some((host
, service
, pid
, data
))
2387 /// A find implementation for [u8]. Returns the index or None.
2388 fn find
<T
: PartialOrd
>(data
: &[T
], needle
: &[T
]) -> Option
<usize> {
2389 data
.windows(needle
.len()).position(|d
| d
== needle
)
2392 /// A find implementation for [u8] that converts to lowercase before the comparison. Returns the
2394 fn find_lowercase(data
: &[u8], needle
: &[u8]) -> Option
<usize> {
2395 let data
= data
.to_ascii_lowercase();
2396 let needle
= needle
.to_ascii_lowercase();
2397 data
.windows(needle
.len()).position(|d
| d
== &needle
[..])