7 use std
::cell
::RefCell
;
8 use std
::collections
::HashMap
;
10 use std
::rc
::{Rc, Weak}
;
14 use std
::io
::BufReader
;
15 use std
::io
::BufWriter
;
23 fn main() -> Result
<(), Error
> {
24 let matches
= App
::new(crate_name
!())
25 .version(crate_version
!())
26 .about(crate_description
!())
28 Arg
::with_name("verbose")
31 .help("Verbose output, can be specified multiple times")
36 Arg
::with_name("inputfile")
39 .help("Input file to use instead of /var/log/syslog, or '-' for stdin")
40 .value_name("INPUTFILE"),
43 Arg
::with_name("host")
46 .help("Hostname or Server IP")
50 Arg
::with_name("from")
53 .help("Mails from SENDER")
54 .value_name("SENDER"),
60 .help("Mails to RECIPIENT")
61 .value_name("RECIPIENT"),
64 Arg
::with_name("start")
67 .help("Start time (YYYY-MM-DD HH:MM:SS) or seconds since epoch")
74 .help("End time (YYYY-MM-DD HH:MM:SS) or seconds since epoch")
78 Arg
::with_name("msgid")
81 .help("Message ID (exact match)")
85 Arg
::with_name("qids")
88 .help("Queue ID (exact match), can be specified multiple times")
94 Arg
::with_name("search")
96 .long("search-string")
97 .help("Search for string")
98 .value_name("STRING"),
101 Arg
::with_name("limit")
104 .help("Print MAX entries")
109 Arg
::with_name("exclude_greylist")
111 .long("exclude-greylist")
112 .help("Exclude greylist entries"),
115 Arg
::with_name("exclude_ndr")
118 .help("Exclude NDR entries"),
122 let mut parser
= Parser
::new();
123 parser
.handle_args(matches
)?
;
125 println
!("# LogReader: {}", std
::process
::id());
126 println
!("# Query options");
127 if !parser
.options
.from
.is_empty() {
128 println
!("# Sender: {}", parser
.options
.from
);
130 if !parser
.options
.to
.is_empty() {
131 println
!("# Recipient: {}", parser
.options
.to
);
133 if !parser
.options
.host
.is_empty() {
134 println
!("# Server: {}", parser
.options
.host
);
136 if !parser
.options
.msgid
.is_empty() {
137 println
!("# MsgID: {}", parser
.options
.msgid
);
139 for m
in parser
.options
.match_list
.iter() {
141 Match
::Qid(b
) => println
!("# QID: {}", std
::str::from_utf8(b
)?
),
142 Match
::RelLineNr(t
, l
) => println
!("# QID: T{:8X}L{:08X}", *t
as u32, *l
as u32),
146 if !parser
.options
.string_match
.is_empty() {
147 println
!("# Match: {}", parser
.options
.string_match
);
152 time
::strftime("%F %T", &parser
.start_tm
)?
,
157 time
::strftime("%F %T", &parser
.end_tm
)?
,
161 println
!("# End Query Options\n");
162 parser
.parse_files()?
;
167 // handle log entries for service 'pmg-smtp-filter'
168 // we match 4 entries, all beginning with a QID
169 // accept mail, move mail, block mail and the processing time
170 fn handle_pmg_smtp_filter_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
171 let (qid
, data
) = match parse_qid(msg
, 25) {
172 Some((q
, m
)) => (q
, m
),
175 // skip ': ' following the QID
176 let data
= &data
[2..];
178 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
180 if parser
.string_match
{
181 fe
.borrow_mut().string_match
= parser
.string_match
;
186 .push((complete_line
.into(), parser
.lines
));
188 // we're interested in the 'to' address and the QID when we accept the mail
189 if data
.starts_with(b
"accept mail to <") {
190 let data
= &data
[16..];
191 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
192 let (to
, data
) = data
.split_at(to_count
);
193 if !data
.starts_with(b
"> (") {
196 let data
= &data
[3..];
197 let qid_count
= data
.iter().take_while(|b
| (**b
as char) != '
)'
).count();
198 let qid
= &data
[..qid_count
];
200 // add a ToEntry with the DStatus 'Accept' to the FEntry
202 .add_accept(to
, qid
, parser
.current_record_state
.timestamp
);
204 // if there's a QEntry with the qid and it's not yet filtered
205 // set it to before-queue filtered
206 if let Some(qe
) = parser
.qentries
.get(qid
) {
207 if !qe
.borrow().filtered
{
208 qe
.borrow_mut().bq_filtered
= true;
209 qe
.borrow_mut().filter
= Some(Rc
::clone(&fe
));
210 fe
.borrow_mut().qentry
= Some(Rc
::downgrade(qe
));
217 // same as for the 'accept' case, we're interested in both the 'to'
218 // address as well as the QID
219 if data
.starts_with(b
"moved mail for <") {
220 let data
= &data
[16..];
221 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
222 let (to
, data
) = data
.split_at(to_count
);
224 let qid_index
= match find(data
, b
"quarantine - ") {
228 let data
= &data
[qid_index
+ 13..];
229 let (qid
, _
) = match parse_qid(data
, 25) {
234 // add a ToEntry with the DStatus 'Quarantine' to the FEntry
236 .add_quarantine(to
, qid
, parser
.current_record_state
.timestamp
);
240 // in the 'block' case we're only interested in the 'to' address, there's
241 // no queue for these mails
242 if data
.starts_with(b
"block mail to <") {
243 let data
= &data
[15..];
244 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
245 let to
= &data
[..to_count
];
248 .add_block(to
, parser
.current_record_state
.timestamp
);
252 // here the pmg-smtp-filter is finished and we get the processing time
253 if data
.starts_with(b
"processing time: ") {
254 let data
= &data
[17..];
255 let time_count
= data
.iter().take_while(|b
| !b
.is_ascii_whitespace()).count();
256 let time
= &data
[..time_count
];
258 fe
.borrow_mut().set_processing_time(time
);
263 // handle log entries for postscreen
264 // here only the NOQUEUE: reject is of interest
265 // these are the mails that were rejected before even entering the smtpd by
266 // e.g. checking DNSBL sites
267 fn handle_postscreen_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
268 if !msg
.starts_with(b
"NOQUEUE: reject: RCPT from ") {
271 // skip the string from above
272 let data
= &msg
[27..];
273 let client_index
= match find(data
, b
"; client [") {
277 let data
= &data
[client_index
+ 10..];
279 let client_count
= data
.iter().take_while(|b
| (**b
as char) != '
]'
).count();
280 let (client
, data
) = data
.split_at(client_count
);
282 let from_index
= match find(data
, b
"; from=<") {
286 let data
= &data
[from_index
+ 8..];
288 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
289 let (from
, data
) = data
.split_at(from_count
);
291 if !data
.starts_with(b
">, to=<") {
294 let data
= &data
[7..];
295 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
296 let to
= &data
[..to_count
];
298 let se
= get_or_create_sentry(
299 &mut parser
.sentries
,
300 parser
.current_record_state
.pid
,
302 parser
.current_record_state
.timestamp
,
305 if parser
.string_match
{
306 se
.borrow_mut().string_match
= parser
.string_match
;
311 .push((complete_line
.into(), parser
.lines
));
312 // for postscreeen noqueue log entries we add a NoqueueEntry to the SEntry
313 se
.borrow_mut().add_noqueue_entry(
317 parser
.current_record_state
.timestamp
,
319 // set the connecting client
320 se
.borrow_mut().set_connect(client
);
321 // as there's no more service involved after the postscreen noqueue entry,
322 // we set it to disconnected and print it
323 se
.borrow_mut().disconnected
= true;
324 se
.borrow_mut().print(parser
);
325 parser
.free_sentry(parser
.current_record_state
.pid
);
328 // handle log entries for 'qmgr'
329 // these only appear in the 'after-queue filter' case or when the mail is
330 // 'accepted' in the 'before-queue filter' case
331 fn handle_qmgr_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
332 let (qid
, data
) = match parse_qid(msg
, 15) {
336 let data
= &data
[2..];
338 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
340 if parser
.string_match
{
341 qe
.borrow_mut().string_match
= parser
.string_match
;
343 qe
.borrow_mut().cleanup
= true;
346 .push((complete_line
.into(), parser
.lines
));
348 // we parse 2 log entries, either one with a 'from' and a 'size' or one
349 // that signals that the mail has been removed from the queue (after an
350 // action was taken, e.g. accept, by the filter)
351 if data
.starts_with(b
"from=<") {
352 let data
= &data
[6..];
354 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
355 let (from
, data
) = data
.split_at(from_count
);
357 if !data
.starts_with(b
">, size=") {
360 let data
= &data
[8..];
362 let size_count
= data
364 .take_while(|b
| (**b
as char).is_ascii_digit())
366 let (size
, _
) = data
.split_at(size_count
);
367 qe
.borrow_mut().from
= from
.into();
368 // it is safe here because we had a check before that limits it to just
369 // ascii digits which is valid utf8
370 qe
.borrow_mut().size
= unsafe { std::str::from_utf8_unchecked(size) }
373 } else if data
== b
"removed" {
374 qe
.borrow_mut().removed
= true;
375 qe
.borrow_mut().finalize(parser
);
379 // handle log entries for 'lmtp', 'smtp', 'error' and 'local'
380 fn handle_lmtp_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
381 let (qid
, data
) = match parse_qid(msg
, 15) {
382 Some((q
, t
)) => (q
, t
),
386 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
388 if parser
.string_match
{
389 qe
.borrow_mut().string_match
= parser
.string_match
;
391 qe
.borrow_mut().cleanup
= true;
394 .push((complete_line
.into(), parser
.lines
));
396 let data
= &data
[2..];
397 if !data
.starts_with(b
"to=<") {
400 let data
= &data
[4..];
401 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
402 let (to
, data
) = data
.split_at(to_count
);
404 let relay_index
= match find(data
, b
"relay=") {
408 let data
= &data
[relay_index
+ 6..];
409 let relay_count
= data
.iter().take_while(|b
| (**b
as char) != '
,'
).count();
410 let (relay
, data
) = data
.split_at(relay_count
);
412 // parse the DSN (indicates the delivery status, e.g. 2 == success)
413 // ignore everything after the first digit
414 let dsn_index
= match find(data
, b
"dsn=") {
418 let data
= &data
[dsn_index
+ 4..];
419 let dsn
= match data
.iter().next() {
421 if b
.is_ascii_digit() {
422 (*b
as char).to_digit(10).unwrap()
430 let mut dstatus
= DStatus
::Dsn(dsn
);
432 // the dsn (enhanced status code can only have a class of 2, 4 or 5
433 // see https://tools.ietf.org/html/rfc3463
434 if qe
.borrow_mut().bq_filtered
{
435 dstatus
= match dsn
{
436 2 => DStatus
::BqPass
,
437 4 => DStatus
::BqDefer
,
438 5 => DStatus
::BqReject
,
443 .add_to_entry(to
, relay
, dstatus
, parser
.current_record_state
.timestamp
);
445 // here the match happens between a QEntry and the corresponding FEntry
446 // (only after-queue)
447 if &*parser
.current_record_state
.service
== b
"postfix/lmtp" {
448 let sent_index
= match find(data
, b
"status=sent (250 2.") {
452 let mut data
= &data
[sent_index
+ 19..];
453 if data
.starts_with(b
"5.0 OK") {
455 } else if data
.starts_with(b
"7.0 BLOCKED") {
461 // this is the QID of the associated pmg-smtp-filter
462 let (qid
, _
) = match parse_qid(data
, 25) {
467 // add a reference to the filter
468 qe
.borrow_mut().filtered
= true;
470 // if there's a FEntry with the filter QID, check to see if its
471 // qentry matches this one
472 if let Some(fe
) = parser
.fentries
.get(qid
) {
473 qe
.borrow_mut().filter
= Some(Rc
::clone(fe
));
474 // if we use fe.borrow().qentry() directly we run into a borrow
476 let q
= fe
.borrow().qentry();
478 if !Rc
::ptr_eq(&q
, &qe
) {
479 // QEntries don't match, set all flags to false and
480 // remove the referenced FEntry
481 q
.borrow_mut().filtered
= false;
482 q
.borrow_mut().bq_filtered
= false;
483 q
.borrow_mut().filter
= None
;
484 // update FEntry's QEntry reference to the new one
485 fe
.borrow_mut().qentry
= Some(Rc
::downgrade(&qe
));
492 // handle log entries for 'smtpd'
493 fn handle_smtpd_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
494 let se
= get_or_create_sentry(
495 &mut parser
.sentries
,
496 parser
.current_record_state
.pid
,
498 parser
.current_record_state
.timestamp
,
501 if parser
.string_match
{
502 se
.borrow_mut().string_match
= parser
.string_match
;
506 .push((complete_line
.into(), parser
.lines
));
508 if msg
.starts_with(b
"connect from ") {
509 let addr
= &msg
[13..];
510 // set the client address
511 se
.borrow_mut().set_connect(addr
);
515 // on disconnect we can finalize and print the SEntry
516 if msg
.starts_with(b
"disconnect from") {
517 parser
.sentries
.remove(&parser
.current_record_state
.pid
);
518 se
.borrow_mut().disconnected
= true;
520 if se
.borrow_mut().remove_unneeded_refs(parser
) == 0 {
521 // no QEntries referenced in SEntry so just print the SEntry
522 se
.borrow_mut().print(parser
);
523 // free the referenced FEntry (only happens with before-queue)
524 if let Some(f
) = &se
.borrow().filter() {
525 parser
.free_fentry(&f
.borrow().logid
);
527 parser
.free_sentry(se
.borrow().pid
);
529 se
.borrow_mut().finalize_refs(parser
);
534 // NOQUEUE in smtpd, happens after postscreen
535 if msg
.starts_with(b
"NOQUEUE:") {
536 let data
= &msg
[8..];
537 let colon_index
= match find(data
, b
":") {
541 let data
= &data
[colon_index
+ 1..];
542 let colon_index
= match find(data
, b
":") {
546 let data
= &data
[colon_index
+ 1..];
547 let semicolon_index
= match find(data
, b
";") {
552 // check for the string, if it matches then greylisting is the reason
553 // for the NOQUEUE entry
554 let (grey
, data
) = data
.split_at(semicolon_index
);
555 let dstatus
= if find(
557 b
"Recipient address rejected: Service is unavailable (try later)",
566 if !data
.starts_with(b
"; from=<") {
569 let data
= &data
[8..];
570 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
571 let (from
, data
) = data
.split_at(from_count
);
573 if !data
.starts_with(b
"> to=<") {
576 let data
= &data
[6..];
577 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
578 let to
= &data
[..to_count
];
581 .add_noqueue_entry(from
, to
, dstatus
, parser
.current_record_state
.timestamp
);
585 // only happens with before-queue
586 // here we can match the pmg-smtp-filter
587 // 'proxy-accept' happens if it is accepted for AT LEAST ONE receiver
588 if msg
.starts_with(b
"proxy-accept: ") {
589 let data
= &msg
[14..];
590 if !data
.starts_with(b
"END-OF-MESSAGE: ") {
593 let data
= &data
[16..];
594 if !data
.starts_with(b
"250 2.5.0 OK (") {
597 let data
= &data
[14..];
598 if let Some((qid
, data
)) = parse_qid(data
, 25) {
599 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
600 // set the FEntry to before-queue filtered
601 fe
.borrow_mut().is_bq
= true;
602 // if there's no 'accept mail to' entry because of quarantine
603 // we have to match the pmg-smtp-filter here
604 // for 'accepted' mails it is matched in the 'accept mail to'
606 if !fe
.borrow().is_accepted
{
607 // set the SEntry filter reference as we don't have a QEntry
609 se
.borrow_mut().filter
= Some(Rc
::downgrade(&fe
));
611 if let Some(from_index
) = find(data
, b
"from=<") {
612 let data
= &data
[from_index
+ 6..];
613 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
614 let from
= &data
[..from_count
];
615 // keep the from for later printing
616 // required for the correct 'TO:{}:{}...' syntax required
617 // by PMG/API2/MailTracker.pm
618 se
.borrow_mut().bq_from
= from
.into();
620 } else if let Some(qe
) = &fe
.borrow().qentry() {
621 // mail is 'accepted', add a reference to the QEntry to the
622 // SEntry so we can wait for all to be finished before printing
623 qe
.borrow_mut().bq_sentry
= Some(Rc
::clone(&se
));
624 SEntry
::add_ref(&se
, &qe
, true);
626 // specify that before queue filtering is used and the mail was
627 // accepted, but not necessarily by an 'accept' rule
629 se
.borrow_mut().is_bq_accepted
= true;
635 // before queue filtering and rejected, here we can match the
636 // pmg-smtp-filter same as in the 'proxy-accept' case
637 // only happens if the mail was rejected for ALL receivers, otherwise
638 // a 'proxy-accept' happens
639 if msg
.starts_with(b
"proxy-reject: ") {
640 let data
= &msg
[14..];
641 if !data
.starts_with(b
"END-OF-MESSAGE: ") {
644 let data
= &data
[16..];
646 // specify that before queue filtering is used and the mail
647 // was rejected for all receivers
648 se
.borrow_mut().is_bq_rejected
= true;
650 if let Some(qid_index
) = find(data
, b
"(") {
651 let data
= &data
[qid_index
+ 1..];
652 if let Some((qid
, _
)) = parse_qid(data
, 25) {
653 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
654 // set the FEntry to before-queue filtered
655 fe
.borrow_mut().is_bq
= true;
656 // we never have a QEntry in this case, so just set the SEntry
658 se
.borrow_mut().filter
= Some(Rc
::downgrade(&fe
));
659 if let Some(from_index
) = find(data
, b
"from=<") {
660 let data
= &data
[from_index
+ 6..];
661 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
662 let from
= &data
[..from_count
];
663 // same as for 'proxy-accept' above
664 se
.borrow_mut().bq_from
= from
.into();
667 } else if let Some(from_index
) = find(data
, b
"from=<") {
668 let data
= &data
[from_index
+ 6..];
669 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
670 let from
= &data
[..from_count
];
671 // same as for 'proxy-accept' above
672 se
.borrow_mut().bq_from
= from
.into();
674 if let Some(to_index
) = find(data
, b
"to=<") {
675 let data
= &data
[to_index
+ 4..];
676 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
677 let to
= &data
[..to_count
];
679 se
.borrow_mut().add_noqueue_entry(
683 parser
.current_record_state
.timestamp
,
691 // with none of the other messages matching, we try for a QID to match the
692 // corresponding QEntry to the SEntry
693 let (qid
, data
) = match parse_qid(msg
, 15) {
697 let data
= &data
[2..];
699 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
701 if parser
.string_match
{
702 qe
.borrow_mut().string_match
= parser
.string_match
;
705 SEntry
::add_ref(&se
, &qe
, false);
707 if !data
.starts_with(b
"client=") {
710 let data
= &data
[7..];
711 let client_count
= data
713 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
715 let client
= &data
[..client_count
];
717 qe
.borrow_mut().set_client(client
);
720 // handle log entries for 'cleanup'
721 // happens before the mail is passed to qmgr (after-queue or before-queue
723 fn handle_cleanup_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
724 let (qid
, data
) = match parse_qid(msg
, 15) {
728 let data
= &data
[2..];
730 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
732 if parser
.string_match
{
733 qe
.borrow_mut().string_match
= parser
.string_match
;
737 .push((complete_line
.into(), parser
.lines
));
739 if !data
.starts_with(b
"message-id=") {
742 let data
= &data
[11..];
743 let msgid_count
= data
745 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
747 let msgid
= &data
[..msgid_count
];
749 if !msgid
.is_empty() {
750 if qe
.borrow().msgid
.is_empty() {
751 qe
.borrow_mut().msgid
= msgid
.into();
753 qe
.borrow_mut().cleanup
= true;
757 #[derive(Default, Debug)]
758 struct NoqueueEntry
{
773 impl Default
for ToEntry
{
774 fn default() -> Self {
776 to
: Default
::default(),
777 relay
: (&b
"none"[..]).into(),
778 dstatus
: Default
::default(),
779 timestamp
: Default
::default(),
784 #[derive(Debug, PartialEq, Copy, Clone)]
798 impl Default
for DStatus
{
799 fn default() -> Self {
804 impl std
::fmt
::Display
for DStatus
{
805 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
807 DStatus
::Invalid
=> '
\0'
, // other default
808 DStatus
::Accept
=> 'A'
,
809 DStatus
::Quarantine
=> 'Q'
,
810 DStatus
::Block
=> 'B'
,
811 DStatus
::Greylist
=> 'G'
,
812 DStatus
::Noqueue
=> 'N'
,
813 DStatus
::BqPass
=> 'P'
,
814 DStatus
::BqDefer
=> 'D'
,
815 DStatus
::BqReject
=> 'R'
,
816 DStatus
::Dsn(v
) => std
::char::from_digit(*v
, 10).unwrap(),
822 #[derive(Debug, Default)]
824 log
: Vec
<(Box
<[u8]>, u64)>,
828 // references to QEntries, Weak so they are not kept alive longer than
829 // necessary, RefCell for mutability (Rc<> is immutable)
830 refs
: Vec
<Weak
<RefCell
<QEntry
>>>,
831 nq_entries
: Vec
<NoqueueEntry
>,
833 // only set in case of before queue filtering
834 // used as a fallback in case no QEntry is referenced
835 filter
: Option
<Weak
<RefCell
<FEntry
>>>,
839 // before queue filtering with the mail accepted for at least one receiver
840 is_bq_accepted
: bool
,
841 // before queue filtering with the mail rejected for all receivers
842 is_bq_rejected
: bool
,
843 // from address saved for compatibility with after queue filtering
848 fn add_noqueue_entry(&mut self, from
: &[u8], to
: &[u8], dstatus
: DStatus
, timestamp
: u64) {
849 let ne
= NoqueueEntry
{
855 self.nq_entries
.push(ne
);
858 fn set_connect(&mut self, client
: &[u8]) {
859 if self.connect
.is_empty() {
860 self.connect
= client
.into();
864 // if either 'from' or 'to' are set, check if it matches, if not, set
865 // the status of the noqueue entry to Invalid
866 // if exclude_greylist or exclude_ndr are set, check if it matches
867 // and if so, set the status to Invalid so they are no longer included
868 // don't print if any Invalid entry is found
869 fn filter_matches(&mut self, parser
: &Parser
) -> bool
{
870 if !parser
.options
.from
.is_empty()
871 || !parser
.options
.to
.is_empty()
872 || parser
.options
.exclude_greylist
873 || parser
.options
.exclude_ndr
875 let mut found
= false;
876 for nq
in self.nq_entries
.iter_mut().rev() {
877 if (!parser
.options
.from
.is_empty()
878 && find_lowercase(&nq
.from
, parser
.options
.from
.as_bytes()).is_none())
879 || (parser
.options
.exclude_greylist
&& nq
.dstatus
== DStatus
::Greylist
)
880 || (parser
.options
.exclude_ndr
&& nq
.from
.is_empty())
881 || (!parser
.options
.to
.is_empty()
883 && find_lowercase(&nq
.to
, parser
.options
.to
.as_bytes()).is_none())
885 nq
.dstatus
= DStatus
::Invalid
;
888 if nq
.dstatus
!= DStatus
::Invalid
{
893 // we can early exit the printing if there's no valid Noqueue entry
894 // and we're in the after-queue case
895 if !found
&& self.filter
.is_none() {
899 // self.filter only contains an object in the before-queue case
900 // as we have the FEntry referenced in the SEntry when there's no
901 // queue involved, we can't just check the Noqueue entries, but
902 // have to check for a filter and if it exists, we have to check
903 // them for matching 'from' and 'to' if either of those options
905 // if neither of them is filtered, we can skip this check
906 if let Some(fe
) = &self.filter() {
907 if !parser
.options
.from
.is_empty()
908 && find_lowercase(&self.bq_from
, parser
.options
.from
.as_bytes()).is_none()
912 let to_option_set
= !parser
.options
.to
.is_empty();
913 if to_option_set
&& fe
.borrow().is_bq
&& !fe
.borrow().is_accepted
{
914 fe
.borrow_mut().to_entries
.retain(|to
| {
915 if find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_some() {
930 fn print(&mut self, parser
: &mut Parser
) {
931 // don't print if the output is filtered by the message-id
932 // the message-id is only available in a QEntry
933 if !parser
.options
.msgid
.is_empty() {
937 // don't print if the output is filtered by a host but the connect
938 // field is empty or does not match
939 if !parser
.options
.host
.is_empty() {
940 if self.connect
.is_empty() {
943 if find_lowercase(&self.connect
, parser
.options
.host
.as_bytes()).is_none() {
948 // don't print if the output is filtered by time and line number
950 if !parser
.options
.match_list
.is_empty() {
951 let mut found
= false;
952 for m
in parser
.options
.match_list
.iter() {
954 Match
::Qid(_
) => return,
955 Match
::RelLineNr(t
, l
) => {
956 if (*t
as u64) == self.timestamp
&& *l
== self.rel_line_nr
{
968 if !self.filter_matches(parser
) {
972 // don't print if there's a string match specified, but none of the
973 // log entries matches. in the before-queue case we also have to check
974 // the attached filter for a match
975 if !parser
.options
.string_match
.is_empty() {
976 if let Some(fe
) = &self.filter() {
977 if !self.string_match
&& !fe
.borrow().string_match
{
981 if !self.string_match
{
987 if parser
.options
.verbose
> 0 {
988 parser
.write_all_ok(format
!(
989 "SMTPD: T{:8X}L{:08X}\n",
990 self.timestamp
as u32, self.rel_line_nr
as u32
992 parser
.write_all_ok(format
!("CTIME: {:8X}\n", parser
.ctime
).as_bytes());
994 if !self.connect
.is_empty() {
995 parser
.write_all_ok(b
"CLIENT: ");
996 parser
.write_all_ok(&self.connect
);
997 parser
.write_all_ok(b
"\n");
1001 // only print the entry if the status is not invalid
1002 // rev() for compatibility with the C code which uses a linked list
1003 // that adds entries at the front, while a Vec in Rust adds it at the
1005 for nq
in self.nq_entries
.iter().rev() {
1006 if nq
.dstatus
!= DStatus
::Invalid
{
1007 parser
.write_all_ok(format
!(
1008 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
1009 nq
.timestamp
as i32, self.timestamp
as i32, self.rel_line_nr
, nq
.dstatus
,
1011 parser
.write_all_ok(&nq
.from
);
1012 parser
.write_all_ok(b
"> to <");
1013 parser
.write_all_ok(&nq
.to
);
1014 parser
.write_all_ok(b
">\n");
1019 let print_filter_to_entries_fn
=
1020 |fe
: &Rc
<RefCell
<FEntry
>>, parser
: &mut Parser
, se
: &SEntry
| {
1021 for to
in fe
.borrow().to_entries
.iter().rev() {
1022 parser
.write_all_ok(format
!(
1023 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
1024 to
.timestamp
as i32, se
.timestamp
as i32, se
.rel_line_nr
, to
.dstatus
,
1026 parser
.write_all_ok(&se
.bq_from
);
1027 parser
.write_all_ok(b
"> to <");
1028 parser
.write_all_ok(&to
.to
);
1029 parser
.write_all_ok(b
">\n");
1034 // only true in before queue filtering case
1035 if let Some(fe
) = &self.filter() {
1036 // limited to !fe.is_accepted because otherwise we would have
1037 // a QEntry with all required information instead
1038 if fe
.borrow().is_bq
1039 && !fe
.borrow().is_accepted
1040 && (self.is_bq_accepted
|| self.is_bq_rejected
)
1042 print_filter_to_entries_fn(&fe
, parser
, self);
1046 let print_log
= |parser
: &mut Parser
, logs
: &Vec
<(Box
<[u8]>, u64)>| {
1047 for (log
, line
) in logs
.iter() {
1048 parser
.write_all_ok(format
!("L{:08X} ", *line
as u32));
1049 parser
.write_all_ok(log
);
1050 parser
.write_all_ok(b
"\n");
1054 // if '-vv' is passed to the log tracker, print all the logs
1055 if parser
.options
.verbose
> 1 {
1056 parser
.write_all_ok(b
"LOGS:\n");
1057 let mut logs
= self.log
.clone();
1058 if let Some(f
) = &self.filter() {
1059 logs
.append(&mut f
.borrow().log
.clone());
1060 // as the logs come from 1 SEntry and 1 FEntry,
1061 // interleave them via sort based on line number
1062 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1065 print_log(parser
, &logs
);
1067 parser
.write_all_ok(b
"\n");
1070 fn delete_ref(&mut self, qentry
: &Rc
<RefCell
<QEntry
>>) {
1071 self.refs
.retain(|q
| {
1072 let q
= match q
.upgrade() {
1074 None
=> return false,
1076 if Rc
::ptr_eq(&q
, qentry
) {
1083 fn remove_unneeded_refs(&mut self, parser
: &mut Parser
) -> u32 {
1084 let mut count
: u32 = 0;
1085 let mut to_delete
= Vec
::new();
1086 self.refs
.retain(|q
| {
1087 let q
= match q
.upgrade() {
1089 None
=> return false,
1091 let is_cleanup
= q
.borrow().cleanup
;
1092 // add those that require freeing to a separate Vec as self is
1093 // borrowed mutable here and can't be borrowed again for the
1094 // parser.free_qentry() call
1104 for q
in to_delete
.iter().rev() {
1105 parser
.free_qentry(&q
.borrow().qid
, Some(self));
1110 // print and free all QEntries that are removed and if a filter is set,
1111 // if the filter is finished
1112 fn finalize_refs(&mut self, parser
: &mut Parser
) {
1113 let mut qentries
= Vec
::new();
1114 for q
in self.refs
.iter() {
1115 let q
= match q
.upgrade() {
1120 if !q
.borrow().removed
{
1124 let fe
= &q
.borrow().filter
;
1125 if let Some(f
) = fe
{
1126 if !q
.borrow().bq_filtered
&& !f
.borrow().finished
{
1131 if !self.is_bq_accepted
&& q
.borrow().bq_sentry
.is_some() {
1132 if let Some(se
) = &q
.borrow().bq_sentry
{
1133 // we're already disconnected, but the SEntry referenced
1134 // by the QEntry might not yet be done
1135 if !se
.borrow().disconnected
{
1136 // add a reference to the SEntry referenced by the
1137 // QEntry so it gets deleted when both the SEntry
1138 // and the QEntry is done
1139 Self::add_ref(&se
, &q
, true);
1145 qentries
.push(Rc
::clone(&q
));
1148 for q
in qentries
.iter().rev() {
1149 q
.borrow_mut().print(parser
, Some(self));
1150 parser
.free_qentry(&q
.borrow().qid
, Some(self));
1152 if let Some(f
) = &q
.borrow().filter
{
1153 parser
.free_fentry(&f
.borrow().logid
);
1158 fn add_ref(sentry
: &Rc
<RefCell
<SEntry
>>, qentry
: &Rc
<RefCell
<QEntry
>>, bq
: bool
) {
1159 let smtpd
= qentry
.borrow().smtpd
.clone();
1161 if let Some(s
) = smtpd
{
1162 if !Rc
::ptr_eq(sentry
, &s
) {
1163 eprintln
!("Error: qentry ref already set");
1169 for q
in sentry
.borrow().refs
.iter() {
1170 let q
= match q
.upgrade() {
1174 if Rc
::ptr_eq(&q
, qentry
) {
1179 sentry
.borrow_mut().refs
.push(Rc
::downgrade(qentry
));
1181 qentry
.borrow_mut().smtpd
= Some(Rc
::clone(sentry
));
1185 fn filter(&self) -> Option
<Rc
<RefCell
<FEntry
>>> {
1186 self.filter
.clone().and_then(|f
| f
.upgrade())
1190 #[derive(Default, Debug)]
1192 log
: Vec
<(Box
<[u8]>, u64)>,
1193 smtpd
: Option
<Rc
<RefCell
<SEntry
>>>,
1194 filter
: Option
<Rc
<RefCell
<FEntry
>>>,
1200 to_entries
: Vec
<ToEntry
>,
1206 // will differ from smtpd
1207 bq_sentry
: Option
<Rc
<RefCell
<SEntry
>>>,
1211 fn add_to_entry(&mut self, to
: &[u8], relay
: &[u8], dstatus
: DStatus
, timestamp
: u64) {
1214 relay
: relay
.into(),
1218 self.to_entries
.push(te
);
1221 // finalize and print the QEntry
1222 fn finalize(&mut self, parser
: &mut Parser
) {
1223 // if it is not removed, skip
1225 if let Some(se
) = &self.smtpd
{
1226 // verify that the SEntry it is attached to is disconnected
1227 if !se
.borrow().disconnected
{
1231 if let Some(s
) = &self.bq_sentry
{
1232 if self.bq_filtered
&& !s
.borrow().disconnected
{
1237 if let Some(fe
) = self.filter
.clone() {
1238 // verify that the attached FEntry is finished if it is not
1239 // before queue filtered
1240 if !self.bq_filtered
&& !fe
.borrow().finished
{
1244 // if there's an SEntry, print with the SEntry
1245 // otherwise just print the QEntry (this can happen in certain
1247 match self.smtpd
.clone() {
1248 Some(s
) => self.print(parser
, Some(&*s
.borrow())),
1249 None
=> self.print(parser
, None
),
1251 if let Some(se
) = &self.smtpd
{
1252 parser
.free_qentry(&self.qid
, Some(&mut *se
.borrow_mut()));
1254 parser
.free_qentry(&self.qid
, None
);
1257 if !self.bq_filtered
{
1258 parser
.free_fentry(&fe
.borrow().logid
);
1260 } else if let Some(s
) = self.smtpd
.clone() {
1261 self.print(parser
, Some(&*s
.borrow()));
1262 parser
.free_qentry(&self.qid
, Some(&mut *s
.borrow_mut()));
1264 self.print(parser
, None
);
1265 parser
.free_qentry(&self.qid
, None
);
1270 fn msgid_matches(&self, parser
: &Parser
) -> bool
{
1271 if !parser
.options
.msgid
.is_empty() {
1272 if self.msgid
.is_empty() {
1275 let qentry_msgid_lowercase
= self.msgid
.to_ascii_lowercase();
1276 let msgid_lowercase
= parser
.options
.msgid
.as_bytes().to_ascii_lowercase();
1277 if qentry_msgid_lowercase
!= msgid_lowercase
{
1284 fn match_list_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1285 let fe
= &self.filter
;
1286 if !parser
.options
.match_list
.is_empty() {
1287 let mut found
= false;
1288 for m
in parser
.options
.match_list
.iter() {
1291 if let Some(f
) = fe
{
1292 if &f
.borrow().logid
== q
{
1302 Match
::RelLineNr(t
, l
) => {
1303 if let Some(s
) = se
{
1304 if s
.timestamp
== (*t
as u64) && s
.rel_line_nr
== *l
{
1319 fn host_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1320 if !parser
.options
.host
.is_empty() {
1321 let mut found
= false;
1322 if let Some(s
) = se
{
1323 if !s
.connect
.is_empty()
1324 && find_lowercase(&s
.connect
, parser
.options
.host
.as_bytes()).is_some()
1329 if !self.client
.is_empty()
1330 && find_lowercase(&self.client
, parser
.options
.host
.as_bytes()).is_some()
1342 fn from_to_matches(&mut self, parser
: &Parser
) -> bool
{
1343 if !parser
.options
.from
.is_empty() {
1344 if self.from
.is_empty() {
1347 if find_lowercase(&self.from
, parser
.options
.from
.as_bytes()).is_none() {
1350 } else if parser
.options
.exclude_ndr
&& self.from
.is_empty() {
1354 if !parser
.options
.to
.is_empty() {
1355 let mut found
= false;
1356 self.to_entries
.retain(|to
| {
1357 if find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_none() {
1364 if let Some(fe
) = &self.filter
{
1365 fe
.borrow_mut().to_entries
.retain(|to
| {
1366 if find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_none() {
1381 fn string_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1382 let fe
= &self.filter
;
1383 if !parser
.options
.string_match
.is_empty() {
1384 let mut string_match
= self.string_match
;
1386 if let Some(s
) = se
{
1388 string_match
= true;
1391 if let Some(f
) = fe
{
1392 if f
.borrow().string_match
{
1393 string_match
= true;
1403 // is_se_bq_sentry is true if the QEntry::bq_sentry is the same as passed
1404 // into the print() function via reference
1405 fn print_qentry_boilerplate(
1407 parser
: &mut Parser
,
1408 is_se_bq_sentry
: bool
,
1409 se
: Option
<&SEntry
>,
1411 parser
.write_all_ok(b
"QENTRY: ");
1412 parser
.write_all_ok(&self.qid
);
1413 parser
.write_all_ok(b
"\n");
1414 parser
.write_all_ok(format
!("CTIME: {:8X}\n", parser
.ctime
));
1415 parser
.write_all_ok(format
!("SIZE: {}\n", self.size
));
1417 if !self.client
.is_empty() {
1418 parser
.write_all_ok(b
"CLIENT: ");
1419 parser
.write_all_ok(&self.client
);
1420 parser
.write_all_ok(b
"\n");
1421 } else if !is_se_bq_sentry
{
1422 if let Some(s
) = se
{
1423 if !s
.connect
.is_empty() {
1424 parser
.write_all_ok(b
"CLIENT: ");
1425 parser
.write_all_ok(&s
.connect
);
1426 parser
.write_all_ok(b
"\n");
1429 } else if let Some(s
) = &self.smtpd
{
1430 if !s
.borrow().connect
.is_empty() {
1431 parser
.write_all_ok(b
"CLIENT: ");
1432 parser
.write_all_ok(&s
.borrow().connect
);
1433 parser
.write_all_ok(b
"\n");
1437 if !self.msgid
.is_empty() {
1438 parser
.write_all_ok(b
"MSGID: ");
1439 parser
.write_all_ok(&self.msgid
);
1440 parser
.write_all_ok(b
"\n");
1444 fn print(&mut self, parser
: &mut Parser
, se
: Option
<&SEntry
>) {
1445 let fe
= self.filter
.clone();
1447 if !self.msgid_matches(parser
)
1448 || !self.match_list_matches(parser
, se
)
1449 || !self.host_matches(parser
, se
)
1450 || !self.from_to_matches(parser
)
1451 || !self.string_matches(parser
, se
)
1456 // necessary so we do not attempt to mutable borrow it a second time
1458 let is_se_bq_sentry
= match (&self.bq_sentry
, se
) {
1459 (Some(s
), Some(se
)) => std
::ptr
::eq(s
.as_ptr(), se
),
1463 if is_se_bq_sentry
{
1464 if let Some(s
) = &se
{
1465 if !s
.disconnected
{
1471 if parser
.options
.verbose
> 0 {
1472 self.print_qentry_boilerplate(parser
, is_se_bq_sentry
, se
);
1475 // rev() to match the C code iteration direction (linked list vs Vec)
1476 for to
in self.to_entries
.iter().rev() {
1477 if !to
.to
.is_empty() {
1480 let mut final_to
: &ToEntry
= to
;
1481 // if status == success and there's a filter attached that has
1482 // a matching 'to' in one of the ToEntries, set the ToEntry to
1483 // the one in the filter
1484 if to
.dstatus
== DStatus
::Dsn(2) {
1485 if let Some(f
) = &fe
{
1486 if !self.bq_filtered
|| (f
.borrow().finished
&& f
.borrow().is_bq
) {
1488 final_borrow
= final_rc
.borrow();
1489 for to2
in final_borrow
.to_entries
.iter().rev() {
1490 if to
.to
== to2
.to
{
1499 parser
.write_all_ok(format
!("TO:{:X}:", to
.timestamp
as i32,));
1500 parser
.write_all_ok(&self.qid
);
1501 parser
.write_all_ok(format
!(":{}: from <", final_to
.dstatus
));
1502 parser
.write_all_ok(&self.from
);
1503 parser
.write_all_ok(b
"> to <");
1504 parser
.write_all_ok(&final_to
.to
);
1505 parser
.write_all_ok(b
"> (");
1506 // if we use the relay from the filter ToEntry, it will be
1507 // marked 'is_relay' in PMG/API2/MailTracker.pm and not shown
1508 // in the GUI in the case of before queue filtering
1509 if !self.bq_filtered
{
1510 parser
.write_all_ok(&final_to
.relay
);
1512 parser
.write_all_ok(&to
.relay
);
1514 parser
.write_all_ok(b
")\n");
1519 if self.bq_filtered
{
1520 if let Some(fe
) = &fe
{
1521 if fe
.borrow().finished
&& fe
.borrow().is_bq
{
1522 fe
.borrow_mut().to_entries
.retain(|to
| {
1523 for to2
in self.to_entries
.iter().rev() {
1524 if to
.to
== to2
.to
{
1531 for to
in fe
.borrow().to_entries
.iter().rev() {
1532 parser
.write_all_ok(format
!("TO:{:X}:", to
.timestamp
as i32,));
1533 parser
.write_all_ok(&self.qid
);
1534 parser
.write_all_ok(format
!(":{}: from <", to
.dstatus
));
1535 parser
.write_all_ok(&self.from
);
1536 parser
.write_all_ok(b
"> to <");
1537 parser
.write_all_ok(&to
.to
);
1538 parser
.write_all_ok(b
"> (");
1539 parser
.write_all_ok(&to
.relay
);
1540 parser
.write_all_ok(b
")\n");
1547 // print logs if '-vv' is specified
1548 if parser
.options
.verbose
> 1 {
1549 let print_log
= |parser
: &mut Parser
, logs
: &Vec
<(Box
<[u8]>, u64)>| {
1550 for (log
, line
) in logs
.iter() {
1551 parser
.write_all_ok(format
!("L{:08X} ", *line
as u32));
1552 parser
.write_all_ok(log
);
1553 parser
.write_all_ok(b
"\n");
1556 if !is_se_bq_sentry
{
1557 if let Some(s
) = se
{
1558 let mut logs
= s
.log
.clone();
1559 if let Some(bq_se
) = &self.bq_sentry
{
1560 logs
.append(&mut bq_se
.borrow().log
.clone());
1561 // as the logs come from 2 different SEntries,
1562 // interleave them via sort based on line number
1563 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1565 if !logs
.is_empty() {
1566 parser
.write_all_ok(b
"SMTP:\n");
1567 print_log(parser
, &logs
);
1570 } else if let Some(s
) = &self.smtpd
{
1571 let mut logs
= s
.borrow().log
.clone();
1572 if let Some(se
) = se
{
1573 logs
.append(&mut se
.log
.clone());
1574 // as the logs come from 2 different SEntries,
1575 // interleave them via sort based on line number
1576 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1578 if !logs
.is_empty() {
1579 parser
.write_all_ok(b
"SMTP:\n");
1580 print_log(parser
, &logs
);
1584 if let Some(f
) = fe
{
1585 if (!self.bq_filtered
|| (f
.borrow().finished
&& f
.borrow().is_bq
))
1586 && !f
.borrow().log
.is_empty()
1588 parser
.write_all_ok(format
!("FILTER: {}\n", unsafe {
1589 std
::str::from_utf8_unchecked(&f
.borrow().logid
)
1591 print_log(parser
, &f
.borrow().log
);
1595 if !self.log
.is_empty() {
1596 parser
.write_all_ok(b
"QMGR:\n");
1597 print_log(parser
, &self.log
);
1600 parser
.write_all_ok(b
"\n")
1603 fn set_client(&mut self, client
: &[u8]) {
1604 if self.client
.is_empty() {
1605 self.client
= client
.into();
1610 #[derive(Default, Debug)]
1612 log
: Vec
<(Box
<[u8]>, u64)>,
1614 to_entries
: Vec
<ToEntry
>,
1615 processing_time
: Box
<[u8]>,
1619 qentry
: Option
<Weak
<RefCell
<QEntry
>>>,
1624 fn add_accept(&mut self, to
: &[u8], qid
: &[u8], timestamp
: u64) {
1628 dstatus
: DStatus
::Accept
,
1631 self.to_entries
.push(te
);
1632 self.is_accepted
= true;
1635 fn add_quarantine(&mut self, to
: &[u8], qid
: &[u8], timestamp
: u64) {
1639 dstatus
: DStatus
::Quarantine
,
1642 self.to_entries
.push(te
);
1645 fn add_block(&mut self, to
: &[u8], timestamp
: u64) {
1648 relay
: (&b
"none"[..]).into(),
1649 dstatus
: DStatus
::Block
,
1652 self.to_entries
.push(te
);
1655 fn set_processing_time(&mut self, time
: &[u8]) {
1656 self.processing_time
= time
.into();
1657 self.finished
= true;
1660 fn qentry(&self) -> Option
<Rc
<RefCell
<QEntry
>>> {
1661 self.qentry
.clone().and_then(|q
| q
.upgrade())
1667 sentries
: HashMap
<u64, Rc
<RefCell
<SEntry
>>>,
1668 fentries
: HashMap
<Box
<[u8]>, Rc
<RefCell
<FEntry
>>>,
1669 qentries
: HashMap
<Box
<[u8]>, Rc
<RefCell
<QEntry
>>>,
1671 current_record_state
: RecordState
,
1674 current_year
: [i64; 32],
1676 current_file_index
: usize,
1680 buffered_stdout
: BufWriter
<std
::io
::Stdout
>,
1687 ctime
: libc
::time_t
,
1695 let mut years
: [i64; 32] = [0; 32];
1697 for (i
, year
) in years
.iter_mut().enumerate() {
1698 let mut ts
= time
::get_time();
1699 ts
.sec
-= (3600 * 24 * i
) as i64;
1700 let ltime
= time
::at(ts
);
1701 *year
= (ltime
.tm_year
+ 1900) as i64;
1705 sentries
: HashMap
::new(),
1706 fentries
: HashMap
::new(),
1707 qentries
: HashMap
::new(),
1708 current_record_state
: Default
::default(),
1710 current_year
: years
,
1712 current_file_index
: 0,
1714 buffered_stdout
: BufWriter
::with_capacity(4 * 1024 * 1024, std
::io
::stdout()),
1715 options
: Options
::default(),
1716 start_tm
: time
::empty_tm(),
1717 end_tm
: time
::empty_tm(),
1719 string_match
: false,
1724 fn free_sentry(&mut self, sentry_pid
: u64) {
1725 self.sentries
.remove(&sentry_pid
);
1728 fn free_qentry(&mut self, qid
: &[u8], se
: Option
<&mut SEntry
>) {
1729 if let Some(qe
) = self.qentries
.get(qid
) {
1730 if let Some(se
) = se
{
1735 self.qentries
.remove(qid
);
1738 fn free_fentry(&mut self, fentry_logid
: &[u8]) {
1739 self.fentries
.remove(fentry_logid
);
1742 fn parse_files(&mut self) -> Result
<(), Error
> {
1743 if !self.options
.inputfile
.is_empty() {
1744 if self.options
.inputfile
== "-" {
1746 self.current_file_index
= 0;
1747 let mut reader
= BufReader
::new(std
::io
::stdin());
1748 self.handle_input_by_line(&mut reader
)?
;
1749 } else if let Ok(file
) = File
::open(&self.options
.inputfile
) {
1750 // read from specified file
1751 self.current_file_index
= 0;
1752 let mut reader
= BufReader
::new(file
);
1753 self.handle_input_by_line(&mut reader
)?
;
1756 let filecount
= self.count_files_in_time_range();
1757 for i
in (0..filecount
).rev() {
1758 self.current_month
= 0;
1759 if let Ok(file
) = File
::open(LOGFILES
[i
]) {
1760 self.current_file_index
= i
;
1762 let gzdecoder
= read
::GzDecoder
::new(file
);
1763 let mut reader
= BufReader
::new(gzdecoder
);
1764 self.handle_input_by_line(&mut reader
)?
;
1766 let mut reader
= BufReader
::new(file
);
1767 self.handle_input_by_line(&mut reader
)?
;
1776 fn handle_input_by_line(&mut self, reader
: &mut dyn BufRead
) -> Result
<(), Error
> {
1777 let mut buffer
= Vec
::<u8>::with_capacity(4096);
1778 let mut prev_time
= 0;
1780 if self.options
.limit
> 0 && (self.count
>= self.options
.limit
) {
1781 self.write_all_ok("STATUS: aborted by limit (too many hits)\n");
1782 self.buffered_stdout
.flush()?
;
1783 std
::process
::exit(0);
1787 let size
= match reader
.read_until(b'
\n'
, &mut buffer
) {
1788 Err(e
) => return Err(e
.into()),
1789 Ok(0) => return Ok(()),
1792 // size includes delimiter
1793 let line
= &buffer
[0..size
- 1];
1794 let complete_line
= line
;
1796 let (time
, line
) = match parse_time(
1798 self.current_year
[self.current_file_index
],
1799 &mut self.current_month
,
1805 // relative line number within a single timestamp
1806 if time
!= prev_time
{
1807 self.rel_line_nr
= 0;
1809 self.rel_line_nr
+= 1;
1813 // skip until we're in the specified time frame
1814 if time
< self.options
.start
{
1817 // past the specified time frame, we're done, exit the loop
1818 if time
> self.options
.end
{
1824 let (host
, service
, pid
, line
) = match parse_host_service_pid(line
) {
1825 Some((h
, s
, p
, l
)) => (h
, s
, p
, l
),
1831 self.current_record_state
.host
= host
.into();
1832 self.current_record_state
.service
= service
.into();
1833 self.current_record_state
.pid
= pid
;
1834 self.current_record_state
.timestamp
= time
as u64;
1836 self.string_match
= false;
1837 if !self.options
.string_match
.is_empty()
1838 && find_lowercase(complete_line
, self.options
.string_match
.as_bytes()).is_some()
1840 self.string_match
= true;
1843 // complete_line required for the logs
1844 if service
== b
"pmg-smtp-filter" {
1845 handle_pmg_smtp_filter_message(line
, self, complete_line
);
1846 } else if service
== b
"postfix/postscreen" {
1847 handle_postscreen_message(line
, self, complete_line
);
1848 } else if service
== b
"postfix/qmgr" {
1849 handle_qmgr_message(line
, self, complete_line
);
1850 } else if service
== b
"postfix/lmtp"
1851 || service
== b
"postfix/smtp"
1852 || service
== b
"postfix/local"
1853 || service
== b
"postfix/error"
1855 handle_lmtp_message(line
, self, complete_line
);
1856 } else if service
== b
"postfix/smtpd" {
1857 handle_smtpd_message(line
, self, complete_line
);
1858 } else if service
== b
"postfix/cleanup" {
1859 handle_cleanup_message(line
, self, complete_line
);
1865 /// Returns the number of files to parse. Does not error out if it can't access any file
1866 /// (permission denied)
1867 fn count_files_in_time_range(&mut self) -> usize {
1869 let mut buffer
= Vec
::new();
1871 for (i
, item
) in LOGFILES
.iter().enumerate() {
1872 self.current_month
= 0;
1875 if let Ok(file
) = File
::open(item
) {
1876 self.current_file_index
= i
;
1879 let gzdecoder
= read
::GzDecoder
::new(file
);
1880 let mut reader
= BufReader
::new(gzdecoder
);
1881 // check the first line
1882 if let Ok(size
) = reader
.read_until(b'
\n'
, &mut buffer
) {
1886 if let Some((time
, _
)) = parse_time(
1888 self.current_year
[i
],
1889 &mut self.current_month
,
1891 // found the earliest file in the time frame
1892 if time
< self.options
.start
{
1900 let mut reader
= BufReader
::new(file
);
1901 if let Ok(size
) = reader
.read_until(b'
\n'
, &mut buffer
) {
1905 if let Some((time
, _
)) = parse_time(
1907 self.current_year
[i
],
1908 &mut self.current_month
,
1910 if time
< self.options
.start
{
1926 fn handle_args(&mut self, args
: clap
::ArgMatches
) -> Result
<(), Error
> {
1927 if let Some(inputfile
) = args
.value_of("inputfile") {
1928 self.options
.inputfile
= inputfile
.to_string();
1931 if let Some(start
) = args
.value_of("start") {
1932 if let Ok(res
) = time
::strptime(&start
, "%F %T") {
1933 self.options
.start
= mkgmtime(&res
);
1934 self.start_tm
= res
;
1935 } else if let Ok(res
) = time
::strptime(&start
, "%s") {
1936 let res
= res
.to_local();
1937 self.options
.start
= mkgmtime(&res
);
1938 self.start_tm
= res
;
1940 failure
::bail
!(failure
::err_msg("failed to parse start time"));
1943 let mut ltime
= time
::now();
1947 self.options
.start
= mkgmtime(<ime
);
1948 self.start_tm
= ltime
;
1951 if let Some(end
) = args
.value_of("end") {
1952 if let Ok(res
) = time
::strptime(&end
, "%F %T") {
1953 self.options
.end
= mkgmtime(&res
);
1955 } else if let Ok(res
) = time
::strptime(&end
, "%s") {
1956 let res
= res
.to_local();
1957 self.options
.end
= mkgmtime(&res
);
1960 failure
::bail
!(failure
::err_msg("failed to parse end time"));
1963 let ltime
= time
::now();
1964 self.options
.end
= mkgmtime(<ime
);
1965 self.end_tm
= ltime
;
1968 if self.options
.end
< self.options
.start
{
1969 failure
::bail
!(failure
::err_msg("end time before start time"));
1972 self.options
.limit
= match args
.value_of("limit") {
1973 Some(l
) => l
.parse().unwrap(),
1977 if let Some(qids
) = args
.values_of("qids") {
1979 let ltime
: libc
::time_t
= 0;
1980 let rel_line_nr
: libc
::c_ulong
= 0;
1981 let input
= CString
::new(q
)?
;
1982 let bytes
= concat
!("T%08lXL%08lX", "\0");
1984 unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
;
1986 libc
::sscanf(input
.as_ptr(), format
.as_ptr(), <ime
, &rel_line_nr
) == 2
1990 .push(Match
::RelLineNr(ltime
, rel_line_nr
));
1994 .push(Match
::Qid(q
.as_bytes().into()));
1999 if let Some(from
) = args
.value_of("from") {
2000 self.options
.from
= from
.to_string();
2002 if let Some(to
) = args
.value_of("to") {
2003 self.options
.to
= to
.to_string();
2005 if let Some(host
) = args
.value_of("host") {
2006 self.options
.host
= host
.to_string();
2008 if let Some(msgid
) = args
.value_of("msgid") {
2009 self.options
.msgid
= msgid
.to_string();
2012 self.options
.exclude_greylist
= args
.is_present("exclude_greylist");
2013 self.options
.exclude_ndr
= args
.is_present("exclude_ndr");
2015 self.options
.verbose
= args
.occurrences_of("verbose") as _
;
2017 if let Some(string_match
) = args
.value_of("search") {
2018 self.options
.string_match
= string_match
.to_string();
2024 fn write_all_ok
<T
: AsRef
<[u8]>>(&mut self, data
: T
) {
2025 self.buffered_stdout
2026 .write_all(data
.as_ref())
2027 .expect("failed to write to stdout");
2031 impl Drop
for Parser
{
2032 fn drop(&mut self) {
2033 let mut qentries
= std
::mem
::replace(&mut self.qentries
, HashMap
::new());
2034 for q
in qentries
.values() {
2035 let smtpd
= q
.borrow().smtpd
.clone();
2036 if let Some(s
) = smtpd
{
2037 q
.borrow_mut().print(self, Some(&*s
.borrow()));
2039 q
.borrow_mut().print(self, None
);
2043 let mut sentries
= std
::mem
::replace(&mut self.sentries
, HashMap
::new());
2044 for s
in sentries
.values() {
2045 s
.borrow_mut().print(self);
2051 #[derive(Debug, Default)]
2053 match_list
: Vec
<Match
>,
2055 string_match
: String
,
2060 start
: libc
::time_t
,
2064 exclude_greylist
: bool
,
2071 RelLineNr(libc
::time_t
, u64),
2074 #[derive(Debug, Default)]
2075 struct RecordState
{
2082 fn get_or_create_qentry(
2083 qentries
: &mut HashMap
<Box
<[u8]>, Rc
<RefCell
<QEntry
>>>,
2085 ) -> Rc
<RefCell
<QEntry
>> {
2086 if let Some(qe
) = qentries
.get(qid
) {
2089 let qe
= Rc
::new(RefCell
::new(QEntry
::default()));
2090 qe
.borrow_mut().qid
= qid
.into();
2091 qentries
.insert(qid
.into(), qe
.clone());
2096 fn get_or_create_sentry(
2097 sentries
: &mut HashMap
<u64, Rc
<RefCell
<SEntry
>>>,
2101 ) -> Rc
<RefCell
<SEntry
>> {
2102 if let Some(se
) = sentries
.get(&pid
) {
2105 let se
= Rc
::new(RefCell
::new(SEntry
::default()));
2106 se
.borrow_mut().rel_line_nr
= rel_line_nr
;
2107 se
.borrow_mut().timestamp
= timestamp
;
2108 sentries
.insert(pid
, se
.clone());
2113 fn get_or_create_fentry(
2114 fentries
: &mut HashMap
<Box
<[u8]>, Rc
<RefCell
<FEntry
>>>,
2116 ) -> Rc
<RefCell
<FEntry
>> {
2117 if let Some(fe
) = fentries
.get(qid
) {
2120 let fe
= Rc
::new(RefCell
::new(FEntry
::default()));
2121 fe
.borrow_mut().logid
= qid
.into();
2122 fentries
.insert(qid
.into(), fe
.clone());
2127 fn mkgmtime(tm
: &time
::Tm
) -> libc
::time_t
{
2128 let mut res
: libc
::time_t
;
2130 let mut year
= (tm
.tm_year
+ 1900) as i64;
2131 let mon
= tm
.tm_mon
;
2133 res
= (year
- 1970) * 365 + CAL_MTOD
[mon
as usize];
2139 res
+= (year
- 1968) / 4;
2140 res
-= (year
- 1900) / 100;
2141 res
+= (year
- 1600) / 400;
2143 res
+= (tm
.tm_mday
- 1) as i64;
2144 res
= res
* 24 + tm
.tm_hour
as i64;
2145 res
= res
* 60 + tm
.tm_min
as i64;
2146 res
= res
* 60 + tm
.tm_sec
as i64;
2151 const LOGFILES
: [&str; 32] = [
2153 "/var/log/syslog.1",
2154 "/var/log/syslog.2.gz",
2155 "/var/log/syslog.3.gz",
2156 "/var/log/syslog.4.gz",
2157 "/var/log/syslog.5.gz",
2158 "/var/log/syslog.6.gz",
2159 "/var/log/syslog.7.gz",
2160 "/var/log/syslog.8.gz",
2161 "/var/log/syslog.9.gz",
2162 "/var/log/syslog.10.gz",
2163 "/var/log/syslog.11.gz",
2164 "/var/log/syslog.12.gz",
2165 "/var/log/syslog.13.gz",
2166 "/var/log/syslog.14.gz",
2167 "/var/log/syslog.15.gz",
2168 "/var/log/syslog.16.gz",
2169 "/var/log/syslog.17.gz",
2170 "/var/log/syslog.18.gz",
2171 "/var/log/syslog.19.gz",
2172 "/var/log/syslog.20.gz",
2173 "/var/log/syslog.21.gz",
2174 "/var/log/syslog.22.gz",
2175 "/var/log/syslog.23.gz",
2176 "/var/log/syslog.24.gz",
2177 "/var/log/syslog.25.gz",
2178 "/var/log/syslog.26.gz",
2179 "/var/log/syslog.27.gz",
2180 "/var/log/syslog.28.gz",
2181 "/var/log/syslog.29.gz",
2182 "/var/log/syslog.30.gz",
2183 "/var/log/syslog.31.gz",
2186 /// Parse a QID ([A-Z]+). Returns a tuple of (qid, remaining_text) or None.
2187 fn parse_qid(data
: &[u8], max
: usize) -> Option
<(&[u8], &[u8])> {
2188 // to simplify limit max to data.len()
2189 let max
= max
.min(data
.len());
2190 // take at most max, find the first non-hex-digit
2191 match data
.iter().take(max
).position(|b
| !b
.is_ascii_hexdigit()) {
2192 // if there were less than 5 return nothing
2193 // the QID always has at least 5 characters for the microseconds (see
2194 // http://www.postfix.org/postconf.5.html#enable_long_queue_ids)
2195 Some(n
) if n
< 5 => None
,
2196 // otherwise split at the first non-hex-digit
2197 Some(n
) => Some(data
.split_at(n
)),
2198 // or return 'max' length QID if no non-hex-digit is found
2199 None
=> Some(data
.split_at(max
)),
2203 /// Parse a number. Returns a tuple of (parsed_number, remaining_text) or None.
2204 fn parse_number(data
: &[u8], max_digits
: usize) -> Option
<(usize, &[u8])> {
2205 let max
= max_digits
.min(data
.len());
2210 match data
.iter().take(max
).position(|b
| !b
.is_ascii_digit()) {
2211 Some(n
) if n
== 0 => None
,
2213 let (number
, data
) = data
.split_at(n
);
2214 // number only contains ascii digits
2215 let number
= unsafe { std::str::from_utf8_unchecked(number) }
2218 Some((number
, data
))
2221 let (number
, data
) = data
.split_at(max
);
2222 // number only contains ascii digits
2223 let number
= unsafe { std::str::from_utf8_unchecked(number) }
2226 Some((number
, data
))
2231 /// Parse time. Returns a tuple of (parsed_time, remaining_text) or None.
2235 cur_month
: &mut i64,
2236 ) -> Option
<(libc
::time_t
, &'a
[u8])> {
2237 if data
.len() < 15 {
2241 let mon
= match &data
[0..3] {
2256 let data
= &data
[3..];
2258 let mut ltime
: libc
::time_t
;
2259 let mut year
= cur_year
;
2261 if *cur_month
== 11 && mon
== 0 {
2264 if mon
> *cur_month
{
2268 ltime
= (year
- 1970) * 365 + CAL_MTOD
[mon
as usize];
2274 ltime
+= (year
- 1968) / 4;
2275 ltime
-= (year
- 1900) / 100;
2276 ltime
+= (year
- 1600) / 400;
2278 let whitespace_count
= data
.iter().take_while(|b
| b
.is_ascii_whitespace()).count();
2279 let data
= &data
[whitespace_count
..];
2281 let (mday
, data
) = match parse_number(data
, 2) {
2291 ltime
+= (mday
- 1) as i64;
2293 if data
.len() == 0 {
2297 let data
= &data
[1..];
2299 let (hour
, data
) = match parse_number(data
, 2) {
2307 ltime
+= hour
as i64;
2309 if let Some(c
) = data
.iter().next() {
2310 if (*c
as char) != '
:'
{
2316 let data
= &data
[1..];
2318 let (min
, data
) = match parse_number(data
, 2) {
2326 ltime
+= min
as i64;
2328 if let Some(c
) = data
.iter().next() {
2329 if (*c
as char) != '
:'
{
2335 let data
= &data
[1..];
2337 let (sec
, data
) = match parse_number(data
, 2) {
2345 ltime
+= sec
as i64;
2347 let data
= match data
.len() {
2355 const CAL_MTOD
: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
2357 type ByteSlice
<'a
> = &'a
[u8];
2358 /// Parse Host, Service and PID at the beginning of data. Returns a tuple of (host, service, pid, remaining_text).
2359 fn parse_host_service_pid(data
: &[u8]) -> Option
<(ByteSlice
, ByteSlice
, u64, ByteSlice
)> {
2360 let host_count
= data
2362 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
2364 let host
= &data
[0..host_count
];
2365 let data
= &data
[host_count
+ 1..]; // whitespace after host
2367 let service_count
= data
2370 (**b
as char).is_ascii_alphabetic() || (**b
as char) == '
/'
|| (**b
as char) == '
-'
2373 let service
= &data
[0..service_count
];
2374 let data
= &data
[service_count
..];
2375 if data
.get(0) != Some(&b'
['
) {
2378 let data
= &data
[1..];
2380 let pid_count
= data
2382 .take_while(|b
| (**b
as char).is_ascii_digit())
2384 let pid
= match unsafe { std::str::from_utf8_unchecked(&data[0..pid_count]) }
.parse() {
2385 // all ascii digits so valid utf8
2387 Err(_
) => return None
,
2389 let data
= &data
[pid_count
..];
2390 if !data
.starts_with(b
"]: ") {
2393 let data
= &data
[3..];
2395 Some((host
, service
, pid
, data
))
2398 /// A find implementation for [u8]. Returns the index or None.
2399 fn find
<T
: PartialOrd
>(data
: &[T
], needle
: &[T
]) -> Option
<usize> {
2400 data
.windows(needle
.len()).position(|d
| d
== needle
)
2403 /// A find implementation for [u8] that converts to lowercase before the comparison. Returns the
2405 fn find_lowercase(data
: &[u8], needle
: &[u8]) -> Option
<usize> {
2406 let data
= data
.to_ascii_lowercase();
2407 let needle
= needle
.to_ascii_lowercase();
2408 data
.windows(needle
.len()).position(|d
| d
== &needle
[..])