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 qe
.borrow_mut().add_to_entry(
434 parser
.current_record_state
.timestamp
,
437 // here the match happens between a QEntry and the corresponding FEntry
438 // (only after-queue)
439 if &*parser
.current_record_state
.service
== b
"postfix/lmtp" {
440 let sent_index
= match find(data
, b
"status=sent (250 2.") {
444 let mut data
= &data
[sent_index
+ 19..];
445 if data
.starts_with(b
"5.0 OK") {
447 } else if data
.starts_with(b
"7.0 BLOCKED") {
453 // this is the QID of the associated pmg-smtp-filter
454 let (qid
, _
) = match parse_qid(data
, 25) {
459 // add a reference to the filter
460 qe
.borrow_mut().filtered
= true;
462 // if there's a FEntry with the filter QID, check to see if its
463 // qentry matches this one
464 if let Some(fe
) = parser
.fentries
.get(qid
) {
465 qe
.borrow_mut().filter
= Some(Rc
::clone(fe
));
466 // if we use fe.borrow().qentry() directly we run into a borrow
468 let q
= fe
.borrow().qentry();
470 if !Rc
::ptr_eq(&q
, &qe
) {
471 // QEntries don't match, set all flags to false and
472 // remove the referenced FEntry
473 q
.borrow_mut().filtered
= false;
474 q
.borrow_mut().bq_filtered
= false;
475 q
.borrow_mut().filter
= None
;
476 // update FEntry's QEntry reference to the new one
477 fe
.borrow_mut().qentry
= Some(Rc
::downgrade(&qe
));
484 // handle log entries for 'smtpd'
485 fn handle_smtpd_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
486 let se
= get_or_create_sentry(
487 &mut parser
.sentries
,
488 parser
.current_record_state
.pid
,
490 parser
.current_record_state
.timestamp
,
493 if parser
.string_match
{
494 se
.borrow_mut().string_match
= parser
.string_match
;
498 .push((complete_line
.into(), parser
.lines
));
500 if msg
.starts_with(b
"connect from ") {
501 let addr
= &msg
[13..];
502 // set the client address
503 se
.borrow_mut().set_connect(addr
);
507 // on disconnect we can finalize and print the SEntry
508 if msg
.starts_with(b
"disconnect from") {
509 parser
.sentries
.remove(&parser
.current_record_state
.pid
);
510 se
.borrow_mut().disconnected
= true;
512 if se
.borrow_mut().remove_unneeded_refs(parser
) == 0 {
513 // no QEntries referenced in SEntry so just print the SEntry
514 se
.borrow_mut().print(parser
);
515 // free the referenced FEntry (only happens with before-queue)
516 if let Some(f
) = &se
.borrow().filter() {
517 parser
.free_fentry(&f
.borrow().logid
);
519 parser
.free_sentry(se
.borrow().pid
);
521 se
.borrow_mut().finalize_refs(parser
);
526 // NOQUEUE in smtpd, happens after postscreen
527 if msg
.starts_with(b
"NOQUEUE:") {
528 let data
= &msg
[8..];
529 let colon_index
= match find(data
, b
":") {
533 let data
= &data
[colon_index
+ 1..];
534 let colon_index
= match find(data
, b
":") {
538 let data
= &data
[colon_index
+ 1..];
539 let semicolon_index
= match find(data
, b
";") {
544 // check for the string, if it matches then greylisting is the reason
545 // for the NOQUEUE entry
546 let (grey
, data
) = data
.split_at(semicolon_index
);
547 let dstatus
= if find(
549 b
"Recipient address rejected: Service is unavailable (try later)",
558 if !data
.starts_with(b
"; from=<") {
561 let data
= &data
[8..];
562 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
563 let (from
, data
) = data
.split_at(from_count
);
565 if !data
.starts_with(b
"> to=<") {
568 let data
= &data
[6..];
569 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
570 let to
= &data
[..to_count
];
573 .add_noqueue_entry(from
, to
, dstatus
, parser
.current_record_state
.timestamp
);
577 // only happens with before-queue
578 // here we can match the pmg-smtp-filter
579 // 'proxy-accept' happens if it is accepted for AT LEAST ONE receiver
580 if msg
.starts_with(b
"proxy-accept: ") {
581 let data
= &msg
[14..];
582 if !data
.starts_with(b
"END-OF-MESSAGE: ") {
585 let data
= &data
[16..];
586 if !data
.starts_with(b
"250 2.5.0 OK (") {
589 let data
= &data
[14..];
590 if let Some((qid
, data
)) = parse_qid(data
, 25) {
591 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
592 // set the FEntry to before-queue filtered
593 fe
.borrow_mut().is_bq
= true;
594 // if there's no 'accept mail to' entry because of quarantine
595 // we have to match the pmg-smtp-filter here
596 // for 'accepted' mails it is matched in the 'accept mail to'
598 if !fe
.borrow().is_accepted
{
599 // set the SEntry filter reference as we don't have a QEntry
601 se
.borrow_mut().filter
= Some(Rc
::downgrade(&fe
));
603 if let Some(from_index
) = find(data
, b
"from=<") {
604 let data
= &data
[from_index
+ 6..];
605 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
606 let from
= &data
[..from_count
];
607 // keep the from for later printing
608 // required for the correct 'TO:{}:{}...' syntax required
609 // by PMG/API2/MailTracker.pm
610 se
.borrow_mut().bq_from
= from
.into();
612 } else if let Some(qe
) = &fe
.borrow().qentry() {
613 // mail is 'accepted', add a reference to the QEntry to the
614 // SEntry so we can wait for all to be finished before printing
615 qe
.borrow_mut().bq_sentry
= Some(Rc
::clone(&se
));
616 SEntry
::add_ref(&se
, &qe
, true);
618 // specify that before queue filtering is used and the mail was
619 // accepted, but not necessarily by an 'accept' rule
621 se
.borrow_mut().is_bq_accepted
= true;
627 // before queue filtering and rejected, here we can match the
628 // pmg-smtp-filter same as in the 'proxy-accept' case
629 // only happens if the mail was rejected for ALL receivers, otherwise
630 // a 'proxy-accept' happens
631 if msg
.starts_with(b
"proxy-reject: ") {
632 let data
= &msg
[14..];
633 if !data
.starts_with(b
"END-OF-MESSAGE: ") {
636 let data
= &data
[16..];
637 if let Some(qid_index
) = find(data
, b
"(") {
638 let data
= &data
[qid_index
+ 1..];
639 if let Some((qid
, data
)) = parse_qid(data
, 25) {
640 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
641 // set the FEntry to before-queue filtered
642 fe
.borrow_mut().is_bq
= true;
643 // we never have a QEntry in this case, so just set the SEntry
645 se
.borrow_mut().filter
= Some(Rc
::downgrade(&fe
));
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(from_index
) = find(data
, b
"from=<") {
651 let data
= &data
[from_index
+ 6..];
652 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
653 let from
= &data
[..from_count
];
654 // same as for 'proxy-accept' above
655 se
.borrow_mut().bq_from
= from
.into();
663 // with none of the other messages matching, we try for a QID to match the
664 // corresponding QEntry to the SEntry
665 let (qid
, data
) = match parse_qid(msg
, 15) {
669 let data
= &data
[2..];
671 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
673 if parser
.string_match
{
674 qe
.borrow_mut().string_match
= parser
.string_match
;
677 SEntry
::add_ref(&se
, &qe
, false);
679 if !data
.starts_with(b
"client=") {
682 let data
= &data
[7..];
683 let client_count
= data
685 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
687 let client
= &data
[..client_count
];
689 qe
.borrow_mut().set_client(client
);
692 // handle log entries for 'cleanup'
693 // happens before the mail is passed to qmgr (after-queue or before-queue
695 fn handle_cleanup_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
696 let (qid
, data
) = match parse_qid(msg
, 15) {
700 let data
= &data
[2..];
702 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
704 if parser
.string_match
{
705 qe
.borrow_mut().string_match
= parser
.string_match
;
709 .push((complete_line
.into(), parser
.lines
));
711 if !data
.starts_with(b
"message-id=") {
714 let data
= &data
[11..];
715 let msgid_count
= data
717 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
719 let msgid
= &data
[..msgid_count
];
721 if !msgid
.is_empty() {
722 if qe
.borrow().msgid
.is_empty() {
723 qe
.borrow_mut().msgid
= msgid
.into();
725 qe
.borrow_mut().cleanup
= true;
729 #[derive(Default, Debug)]
730 struct NoqueueEntry
{
745 impl Default
for ToEntry
{
746 fn default() -> Self {
748 to
: Default
::default(),
749 relay
: (&b
"none"[..]).into(),
750 dstatus
: Default
::default(),
751 timestamp
: Default
::default(),
756 #[derive(Debug, PartialEq, Copy, Clone)]
767 impl Default
for DStatus
{
768 fn default() -> Self {
773 impl std
::fmt
::Display
for DStatus
{
774 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
776 DStatus
::Invalid
=> '
\0'
, // other default
777 DStatus
::Accept
=> 'A'
,
778 DStatus
::Quarantine
=> 'Q'
,
779 DStatus
::Block
=> 'B'
,
780 DStatus
::Greylist
=> 'G'
,
781 DStatus
::Noqueue
=> 'N'
,
782 DStatus
::Dsn(v
) => std
::char::from_digit(*v
, 10).unwrap(),
788 #[derive(Debug, Default)]
790 log
: Vec
<(Box
<[u8]>, u64)>,
794 // references to QEntries, Weak so they are not kept alive longer than
795 // necessary, RefCell for mutability (Rc<> is immutable)
796 refs
: Vec
<Weak
<RefCell
<QEntry
>>>,
797 nq_entries
: Vec
<NoqueueEntry
>,
799 // only set in case of before queue filtering
800 // used as a fallback in case no QEntry is referenced
801 filter
: Option
<Weak
<RefCell
<FEntry
>>>,
805 // before queue filtering with the mail accepted for at least one receiver
806 is_bq_accepted
: bool
,
807 // before queue filtering with the mail rejected for all receivers
808 is_bq_rejected
: bool
,
809 // from address saved for compatibility with after queue filtering
814 fn add_noqueue_entry(&mut self, from
: &[u8], to
: &[u8], dstatus
: DStatus
, timestamp
: u64) {
815 let ne
= NoqueueEntry
{
821 self.nq_entries
.push(ne
);
824 fn set_connect(&mut self, client
: &[u8]) {
825 if self.connect
.is_empty() {
826 self.connect
= client
.into();
830 // if either 'from' or 'to' are set, check if it matches, if not, set
831 // the status of the noqueue entry to Invalid
832 // if exclude_greylist or exclude_ndr are set, check if it matches
833 // and if so, set the status to Invalid so they are no longer included
834 // don't print if any Invalid entry is found
835 fn filter_matches(&mut self, parser
: &Parser
) -> bool
{
836 if !parser
.options
.from
.is_empty()
837 || !parser
.options
.to
.is_empty()
838 || parser
.options
.exclude_greylist
839 || parser
.options
.exclude_ndr
841 let mut found
= false;
842 for nq
in self.nq_entries
.iter_mut().rev() {
843 if (!parser
.options
.from
.is_empty()
844 && find_lowercase(&nq
.from
, parser
.options
.from
.as_bytes()).is_none())
845 || (parser
.options
.exclude_greylist
&& nq
.dstatus
== DStatus
::Greylist
)
846 || (parser
.options
.exclude_ndr
&& nq
.from
.is_empty())
847 || (!parser
.options
.to
.is_empty()
849 && find_lowercase(&nq
.to
, parser
.options
.to
.as_bytes()).is_none())
851 nq
.dstatus
= DStatus
::Invalid
;
854 if nq
.dstatus
!= DStatus
::Invalid
{
859 // self.filter only contains an object in the before-queue case
860 // as we have the FEntry referenced in the SEntry when there's no
861 // queue involved, we can't just check the Noqueue entries, but
862 // have to check for a filter and if it exists, we have to check
863 // them for matching 'from' and 'to' if either of those options
865 // if neither of them is filtered, we can skip this check
866 if let Some(fe
) = &self.filter() {
867 let is_filtered
= !parser
.options
.from
.is_empty() || !parser
.options
.to
.is_empty();
868 let from_match
= !parser
.options
.from
.is_empty()
869 && find_lowercase(&self.bq_from
, parser
.options
.from
.as_bytes()).is_some();
870 let to_option_set
= !parser
.options
.to
.is_empty();
871 if is_filtered
&& fe
.borrow().is_bq
&& !fe
.borrow().is_accepted
{
872 for to
in fe
.borrow().to_entries
.iter() {
875 && find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_some())
887 // we can early exit the printing if there's no valid Noqueue entry
888 // and we're in the after-queue case
889 if !found
&& self.filter
.is_none() {
896 fn print(&mut self, parser
: &mut Parser
) {
897 // don't print if the output is filtered by the message-id
898 // the message-id is only available in a QEntry
899 if !parser
.options
.msgid
.is_empty() {
903 // don't print if the output is filtered by a host but the connect
904 // field is empty or does not match
905 if !parser
.options
.host
.is_empty() {
906 if self.connect
.is_empty() {
909 if find_lowercase(&self.connect
, parser
.options
.host
.as_bytes()).is_none() {
914 // don't print if the output is filtered by time and line number
916 if !parser
.options
.match_list
.is_empty() {
917 let mut found
= false;
918 for m
in parser
.options
.match_list
.iter() {
920 Match
::Qid(_
) => return,
921 Match
::RelLineNr(t
, l
) => {
922 if (*t
as u64) == self.timestamp
&& *l
== self.rel_line_nr
{
934 if !self.filter_matches(parser
) {
938 // don't print if there's a string match specified, but none of the
939 // log entries matches
940 if !parser
.options
.string_match
.is_empty() && !self.string_match
{
944 if parser
.options
.verbose
> 0 {
945 parser
.write_all_ok(format
!(
946 "SMTPD: T{:8X}L{:08X}\n",
947 self.timestamp
as u32, self.rel_line_nr
as u32
949 parser
.write_all_ok(format
!("CTIME: {:8X}\n", parser
.ctime
).as_bytes());
951 if !self.connect
.is_empty() {
952 parser
.write_all_ok(b
"CLIENT: ");
953 parser
.write_all_ok(&self.connect
);
954 parser
.write_all_ok(b
"\n");
958 // only print the entry if the status is not invalid
959 // rev() for compatibility with the C code which uses a linked list
960 // that adds entries at the front, while a Vec in Rust adds it at the
962 for nq
in self.nq_entries
.iter().rev() {
963 if nq
.dstatus
!= DStatus
::Invalid
{
964 parser
.write_all_ok(format
!(
965 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
966 nq
.timestamp
as i32, self.timestamp
as i32, self.rel_line_nr
, nq
.dstatus
,
968 parser
.write_all_ok(&nq
.from
);
969 parser
.write_all_ok(b
"> to <");
970 parser
.write_all_ok(&nq
.to
);
971 parser
.write_all_ok(b
">\n");
976 let print_filter_to_entries_fn
=
977 |fe
: &Rc
<RefCell
<FEntry
>>,
980 for to
in fe
.borrow().to_entries
.iter().rev() {
981 parser
.write_all_ok(format
!(
982 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
983 to
.timestamp
as i32, se
.timestamp
as i32, se
.rel_line_nr
, to
.dstatus
,
985 parser
.write_all_ok(&se
.bq_from
);
986 parser
.write_all_ok(b
"> to <");
987 parser
.write_all_ok(&to
.to
);
988 parser
.write_all_ok(b
">\n");
993 // only true in before queue filtering case
994 if let Some(fe
) = &self.filter() {
995 // limited to !fe.is_accepted because otherwise we would have
996 // a QEntry with all required information instead
997 if fe
.borrow().is_bq
&& !fe
.borrow().is_accepted
&& (self.is_bq_accepted
|| self.is_bq_rejected
) {
998 print_filter_to_entries_fn(&fe
, parser
, self);
1002 let print_log
= |parser
: &mut Parser
, logs
: &Vec
<(Box
<[u8]>, u64)>| {
1003 for (log
, line
) in logs
.iter() {
1004 parser
.write_all_ok(format
!("L{:08X} ", *line
as u32));
1005 parser
.write_all_ok(log
);
1006 parser
.write_all_ok(b
"\n");
1010 // if '-vv' is passed to the log tracker, print all the logs
1011 if parser
.options
.verbose
> 1 {
1012 parser
.write_all_ok(b
"LOGS:\n");
1013 let mut logs
= self.log
.clone();
1014 if let Some(f
) = &self.filter() {
1015 logs
.append(&mut f
.borrow().log
.clone());
1016 // as the logs come from 1 SEntry and 1 FEntry,
1017 // interleave them via sort based on line number
1018 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1021 print_log(parser
, &logs
);
1023 parser
.write_all_ok(b
"\n");
1026 fn delete_ref(&mut self, qentry
: &Rc
<RefCell
<QEntry
>>) {
1027 self.refs
.retain(|q
| {
1028 let q
= match q
.upgrade() {
1030 None
=> return false,
1032 if Rc
::ptr_eq(&q
, qentry
) {
1039 fn remove_unneeded_refs(&mut self, parser
: &mut Parser
) -> u32 {
1040 let mut count
: u32 = 0;
1041 let mut to_delete
= Vec
::new();
1042 self.refs
.retain(|q
| {
1043 let q
= match q
.upgrade() {
1045 None
=> return false,
1047 let is_cleanup
= q
.borrow().cleanup
;
1048 // add those that require freeing to a separate Vec as self is
1049 // borrowed mutable here and can't be borrowed again for the
1050 // parser.free_qentry() call
1060 for q
in to_delete
.iter().rev() {
1061 parser
.free_qentry(&q
.borrow().qid
, Some(self));
1066 // print and free all QEntries that are removed and if a filter is set,
1067 // if the filter is finished
1068 fn finalize_refs(&mut self, parser
: &mut Parser
) {
1069 let mut qentries
= Vec
::new();
1070 for q
in self.refs
.iter() {
1071 let q
= match q
.upgrade() {
1076 if !q
.borrow().removed
{
1080 let fe
= &q
.borrow().filter
;
1081 if let Some(f
) = fe
{
1082 if !q
.borrow().bq_filtered
&& !f
.borrow().finished
{
1087 if !self.is_bq_accepted
&& q
.borrow().bq_sentry
.is_some() {
1088 if let Some(se
) = &q
.borrow().bq_sentry
{
1089 // we're already disconnected, but the SEntry referenced
1090 // by the QEntry might not yet be done
1091 if !se
.borrow().disconnected
{
1092 // add a reference to the SEntry referenced by the
1093 // QEntry so it gets deleted when both the SEntry
1094 // and the QEntry is done
1095 Self::add_ref(&se
, &q
, true);
1101 qentries
.push(Rc
::clone(&q
));
1104 for q
in qentries
.iter().rev() {
1105 q
.borrow_mut().print(parser
, Some(self));
1106 parser
.free_qentry(&q
.borrow().qid
, Some(self));
1108 if let Some(f
) = &q
.borrow().filter
{
1109 parser
.free_fentry(&f
.borrow().logid
);
1114 fn add_ref(sentry
: &Rc
<RefCell
<SEntry
>>, qentry
: &Rc
<RefCell
<QEntry
>>, bq
: bool
) {
1115 let smtpd
= qentry
.borrow().smtpd
.clone();
1117 if let Some(s
) = smtpd
{
1118 if !Rc
::ptr_eq(sentry
, &s
) {
1119 eprintln
!("Error: qentry ref already set");
1125 for q
in sentry
.borrow().refs
.iter() {
1126 let q
= match q
.upgrade() {
1130 if Rc
::ptr_eq(&q
, qentry
) {
1135 sentry
.borrow_mut().refs
.push(Rc
::downgrade(qentry
));
1137 qentry
.borrow_mut().smtpd
= Some(Rc
::clone(sentry
));
1141 fn filter(&self) -> Option
<Rc
<RefCell
<FEntry
>>> {
1142 self.filter
.clone().and_then(|f
| f
.upgrade())
1146 #[derive(Default, Debug)]
1148 log
: Vec
<(Box
<[u8]>, u64)>,
1149 smtpd
: Option
<Rc
<RefCell
<SEntry
>>>,
1150 filter
: Option
<Rc
<RefCell
<FEntry
>>>,
1156 to_entries
: Vec
<ToEntry
>,
1162 // will differ from smtpd
1163 bq_sentry
: Option
<Rc
<RefCell
<SEntry
>>>,
1167 fn add_to_entry(&mut self, to
: &[u8], relay
: &[u8], dstatus
: DStatus
, timestamp
: u64) {
1170 relay
: relay
.into(),
1174 self.to_entries
.push(te
);
1177 // finalize and print the QEntry
1178 fn finalize(&mut self, parser
: &mut Parser
) {
1179 // if it is not removed, skip
1181 if let Some(se
) = &self.smtpd
{
1182 // verify that the SEntry it is attached to is disconnected
1183 if !se
.borrow().disconnected
{
1187 if let Some(s
) = &self.bq_sentry
{
1188 if self.bq_filtered
&& !s
.borrow().disconnected
{
1193 if let Some(fe
) = self.filter
.clone() {
1194 // verify that the attached FEntry is finished if it is not
1195 // before queue filtered
1196 if !self.bq_filtered
&& !fe
.borrow().finished
{
1200 // if there's an SEntry, print with the SEntry
1201 // otherwise just print the QEntry (this can happen in certain
1203 match self.smtpd
.clone() {
1204 Some(s
) => self.print(parser
, Some(&*s
.borrow())),
1205 None
=> self.print(parser
, None
),
1207 if let Some(se
) = &self.smtpd
{
1208 parser
.free_qentry(&self.qid
, Some(&mut *se
.borrow_mut()));
1210 parser
.free_qentry(&self.qid
, None
);
1213 if !self.bq_filtered
{
1214 parser
.free_fentry(&fe
.borrow().logid
);
1216 } else if let Some(s
) = self.smtpd
.clone() {
1217 self.print(parser
, Some(&*s
.borrow()));
1218 parser
.free_qentry(&self.qid
, Some(&mut *s
.borrow_mut()));
1220 self.print(parser
, None
);
1221 parser
.free_qentry(&self.qid
, None
);
1226 fn msgid_matches(&self, parser
: &Parser
) -> bool
{
1227 if !parser
.options
.msgid
.is_empty() {
1228 if self.msgid
.is_empty() {
1231 let qentry_msgid_lowercase
= self.msgid
.to_ascii_lowercase();
1232 let msgid_lowercase
= parser
.options
.msgid
.as_bytes().to_ascii_lowercase();
1233 if qentry_msgid_lowercase
!= msgid_lowercase
{
1240 fn match_list_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1241 let fe
= &self.filter
;
1242 if !parser
.options
.match_list
.is_empty() {
1243 let mut found
= false;
1244 for m
in parser
.options
.match_list
.iter() {
1247 if let Some(f
) = fe
{
1248 if &f
.borrow().logid
== q
{
1258 Match
::RelLineNr(t
, l
) => {
1259 if let Some(s
) = se
{
1260 if s
.timestamp
== (*t
as u64) && s
.rel_line_nr
== *l
{
1275 fn host_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1276 if !parser
.options
.host
.is_empty() {
1277 let mut found
= false;
1278 if let Some(s
) = se
{
1279 if !s
.connect
.is_empty()
1280 && find_lowercase(&s
.connect
, parser
.options
.host
.as_bytes()).is_some()
1285 if !self.client
.is_empty()
1286 && find_lowercase(&self.client
, parser
.options
.host
.as_bytes()).is_some()
1298 fn from_to_matches(&mut self, parser
: &Parser
) -> bool
{
1299 if !parser
.options
.from
.is_empty() {
1300 if self.from
.is_empty() {
1303 if find_lowercase(&self.from
, parser
.options
.from
.as_bytes()).is_none() {
1306 } else if parser
.options
.exclude_ndr
&& self.from
.is_empty() {
1310 if !parser
.options
.to
.is_empty() {
1311 let mut found
= false;
1312 self.to_entries
.retain(|to
| {
1313 if find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_none() {
1327 fn string_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1328 let fe
= &self.filter
;
1329 if !parser
.options
.string_match
.is_empty() {
1330 let mut string_match
= self.string_match
;
1332 if let Some(s
) = se
{
1334 string_match
= true;
1337 if let Some(f
) = fe
{
1338 if f
.borrow().string_match
{
1339 string_match
= true;
1349 // is_se_bq_sentry is true if the QEntry::bq_sentry is the same as passed
1350 // into the print() function via reference
1351 fn print_qentry_boilerplate(
1353 parser
: &mut Parser
,
1354 is_se_bq_sentry
: bool
,
1355 se
: Option
<&SEntry
>,
1357 parser
.write_all_ok(b
"QENTRY: ");
1358 parser
.write_all_ok(&self.qid
);
1359 parser
.write_all_ok(b
"\n");
1360 parser
.write_all_ok(format
!("CTIME: {:8X}\n", parser
.ctime
));
1361 parser
.write_all_ok(format
!("SIZE: {}\n", self.size
));
1363 if !self.client
.is_empty() {
1364 parser
.write_all_ok(b
"CLIENT: ");
1365 parser
.write_all_ok(&self.client
);
1366 parser
.write_all_ok(b
"\n");
1367 } else if !is_se_bq_sentry
{
1368 if let Some(s
) = se
{
1369 if !s
.connect
.is_empty() {
1370 parser
.write_all_ok(b
"CLIENT: ");
1371 parser
.write_all_ok(&s
.connect
);
1372 parser
.write_all_ok(b
"\n");
1375 } else if let Some(s
) = &self.smtpd
{
1376 if !s
.borrow().connect
.is_empty() {
1377 parser
.write_all_ok(b
"CLIENT: ");
1378 parser
.write_all_ok(&s
.borrow().connect
);
1379 parser
.write_all_ok(b
"\n");
1383 if !self.msgid
.is_empty() {
1384 parser
.write_all_ok(b
"MSGID: ");
1385 parser
.write_all_ok(&self.msgid
);
1386 parser
.write_all_ok(b
"\n");
1390 fn print(&mut self, parser
: &mut Parser
, se
: Option
<&SEntry
>) {
1391 let fe
= self.filter
.clone();
1393 if !self.msgid_matches(parser
)
1394 || !self.match_list_matches(parser
, se
)
1395 || !self.host_matches(parser
, se
)
1396 || !self.from_to_matches(parser
)
1397 || !self.string_matches(parser
, se
)
1402 // necessary so we do not attempt to mutable borrow it a second time
1404 let is_se_bq_sentry
= match (&self.bq_sentry
, se
) {
1405 (Some(s
), Some(se
)) => std
::ptr
::eq(s
.as_ptr(), se
),
1409 if is_se_bq_sentry
{
1410 if let Some(s
) = &se
{
1411 if !s
.disconnected
{
1417 if parser
.options
.verbose
> 0 {
1418 self.print_qentry_boilerplate(parser
, is_se_bq_sentry
, se
);
1421 // rev() to match the C code iteration direction (linked list vs Vec)
1422 for to
in self.to_entries
.iter().rev() {
1423 if !to
.to
.is_empty() {
1426 let mut final_to
: &ToEntry
= to
;
1427 // if status == success and there's a filter attached that has
1428 // a matching 'to' in one of the ToEntries, set the ToEntry to
1429 // the one in the filter
1430 if to
.dstatus
== DStatus
::Dsn(2) {
1431 if let Some(f
) = &fe
{
1432 if !self.bq_filtered
|| (f
.borrow().finished
&& f
.borrow().is_bq
) {
1434 final_borrow
= final_rc
.borrow();
1435 for to2
in final_borrow
.to_entries
.iter().rev() {
1436 if to
.to
== to2
.to
{
1445 parser
.write_all_ok(format
!("TO:{:X}:", to
.timestamp
as i32,));
1446 parser
.write_all_ok(&self.qid
);
1447 parser
.write_all_ok(format
!(":{}: from <", final_to
.dstatus
));
1448 parser
.write_all_ok(&self.from
);
1449 parser
.write_all_ok(b
"> to <");
1450 parser
.write_all_ok(&final_to
.to
);
1451 parser
.write_all_ok(b
"> (");
1452 // if we use the relay from the filter ToEntry, it will be
1453 // marked 'is_relay' in PMG/API2/MailTracker.pm and not shown
1454 // in the GUI in the case of before queue filtering
1455 if !self.bq_filtered
{
1456 parser
.write_all_ok(&final_to
.relay
);
1458 parser
.write_all_ok(&to
.relay
);
1460 parser
.write_all_ok(b
")\n");
1465 // print logs if '-vv' is specified
1466 if parser
.options
.verbose
> 1 {
1467 let print_log
= |parser
: &mut Parser
, logs
: &Vec
<(Box
<[u8]>, u64)>| {
1468 for (log
, line
) in logs
.iter() {
1469 parser
.write_all_ok(format
!("L{:08X} ", *line
as u32));
1470 parser
.write_all_ok(log
);
1471 parser
.write_all_ok(b
"\n");
1474 if !is_se_bq_sentry
{
1475 if let Some(s
) = se
{
1476 let mut logs
= s
.log
.clone();
1477 if let Some(bq_se
) = &self.bq_sentry
{
1478 logs
.append(&mut bq_se
.borrow().log
.clone());
1479 // as the logs come from 2 different SEntries,
1480 // interleave them via sort based on line number
1481 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1483 if !logs
.is_empty() {
1484 parser
.write_all_ok(b
"SMTP:\n");
1485 print_log(parser
, &logs
);
1488 } else if let Some(s
) = &self.smtpd
{
1489 let mut logs
= s
.borrow().log
.clone();
1490 if let Some(se
) = se
{
1491 logs
.append(&mut se
.log
.clone());
1492 // as the logs come from 2 different SEntries,
1493 // interleave them via sort based on line number
1494 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1496 if !logs
.is_empty() {
1497 parser
.write_all_ok(b
"SMTP:\n");
1498 print_log(parser
, &logs
);
1502 if let Some(f
) = fe
{
1503 if (!self.bq_filtered
|| (f
.borrow().finished
&& f
.borrow().is_bq
))
1504 && !f
.borrow().log
.is_empty()
1506 parser
.write_all_ok(format
!("FILTER: {}\n", unsafe {
1507 std
::str::from_utf8_unchecked(&f
.borrow().logid
)
1509 print_log(parser
, &f
.borrow().log
);
1513 if !self.log
.is_empty() {
1514 parser
.write_all_ok(b
"QMGR:\n");
1515 print_log(parser
, &self.log
);
1518 parser
.write_all_ok(b
"\n")
1521 fn set_client(&mut self, client
: &[u8]) {
1522 if self.client
.is_empty() {
1523 self.client
= client
.into();
1528 #[derive(Default, Debug)]
1530 log
: Vec
<(Box
<[u8]>, u64)>,
1532 to_entries
: Vec
<ToEntry
>,
1533 processing_time
: Box
<[u8]>,
1537 qentry
: Option
<Weak
<RefCell
<QEntry
>>>,
1542 fn add_accept(&mut self, to
: &[u8], qid
: &[u8], timestamp
: u64) {
1546 dstatus
: DStatus
::Accept
,
1549 self.to_entries
.push(te
);
1550 self.is_accepted
= true;
1553 fn add_quarantine(&mut self, to
: &[u8], qid
: &[u8], timestamp
: u64) {
1557 dstatus
: DStatus
::Quarantine
,
1560 self.to_entries
.push(te
);
1563 fn add_block(&mut self, to
: &[u8], timestamp
: u64) {
1566 relay
: (&b
"none"[..]).into(),
1567 dstatus
: DStatus
::Block
,
1570 self.to_entries
.push(te
);
1573 fn set_processing_time(&mut self, time
: &[u8]) {
1574 self.processing_time
= time
.into();
1575 self.finished
= true;
1578 fn qentry(&self) -> Option
<Rc
<RefCell
<QEntry
>>> {
1579 self.qentry
.clone().and_then(|q
| q
.upgrade())
1585 sentries
: HashMap
<u64, Rc
<RefCell
<SEntry
>>>,
1586 fentries
: HashMap
<Box
<[u8]>, Rc
<RefCell
<FEntry
>>>,
1587 qentries
: HashMap
<Box
<[u8]>, Rc
<RefCell
<QEntry
>>>,
1589 current_record_state
: RecordState
,
1592 current_year
: [i64; 32],
1594 current_file_index
: usize,
1598 buffered_stdout
: BufWriter
<std
::io
::Stdout
>,
1605 ctime
: libc
::time_t
,
1613 let mut years
: [i64; 32] = [0; 32];
1615 for (i
, year
) in years
.iter_mut().enumerate() {
1616 let mut ts
= time
::get_time();
1617 ts
.sec
-= (3600 * 24 * i
) as i64;
1618 let ltime
= time
::at(ts
);
1619 *year
= (ltime
.tm_year
+ 1900) as i64;
1623 sentries
: HashMap
::new(),
1624 fentries
: HashMap
::new(),
1625 qentries
: HashMap
::new(),
1626 current_record_state
: Default
::default(),
1628 current_year
: years
,
1630 current_file_index
: 0,
1632 buffered_stdout
: BufWriter
::with_capacity(4 * 1024 * 1024, std
::io
::stdout()),
1633 options
: Options
::default(),
1634 start_tm
: time
::empty_tm(),
1635 end_tm
: time
::empty_tm(),
1637 string_match
: false,
1642 fn free_sentry(&mut self, sentry_pid
: u64) {
1643 self.sentries
.remove(&sentry_pid
);
1646 fn free_qentry(&mut self, qid
: &[u8], se
: Option
<&mut SEntry
>) {
1647 if let Some(qe
) = self.qentries
.get(qid
) {
1648 if let Some(se
) = se
{
1653 self.qentries
.remove(qid
);
1656 fn free_fentry(&mut self, fentry_logid
: &[u8]) {
1657 self.fentries
.remove(fentry_logid
);
1660 fn parse_files(&mut self) -> Result
<(), Error
> {
1661 if !self.options
.inputfile
.is_empty() {
1662 if self.options
.inputfile
== "-" {
1664 self.current_file_index
= 0;
1665 let mut reader
= BufReader
::new(std
::io
::stdin());
1666 self.handle_input_by_line(&mut reader
)?
;
1667 } else if let Ok(file
) = File
::open(&self.options
.inputfile
) {
1668 // read from specified file
1669 self.current_file_index
= 0;
1670 let mut reader
= BufReader
::new(file
);
1671 self.handle_input_by_line(&mut reader
)?
;
1674 let filecount
= self.count_files_in_time_range();
1675 for i
in (0..filecount
).rev() {
1676 self.current_month
= 0;
1677 if let Ok(file
) = File
::open(LOGFILES
[i
]) {
1678 self.current_file_index
= i
;
1680 let gzdecoder
= read
::GzDecoder
::new(file
);
1681 let mut reader
= BufReader
::new(gzdecoder
);
1682 self.handle_input_by_line(&mut reader
)?
;
1684 let mut reader
= BufReader
::new(file
);
1685 self.handle_input_by_line(&mut reader
)?
;
1694 fn handle_input_by_line(&mut self, reader
: &mut dyn BufRead
) -> Result
<(), Error
> {
1695 let mut buffer
= Vec
::<u8>::with_capacity(4096);
1696 let mut prev_time
= 0;
1698 if self.options
.limit
> 0 && (self.count
>= self.options
.limit
) {
1699 self.write_all_ok("STATUS: aborted by limit (too many hits)\n");
1700 self.buffered_stdout
.flush()?
;
1701 std
::process
::exit(0);
1705 let size
= match reader
.read_until(b'
\n'
, &mut buffer
) {
1706 Err(e
) => return Err(e
.into()),
1707 Ok(0) => return Ok(()),
1710 // size includes delimiter
1711 let line
= &buffer
[0..size
- 1];
1712 let complete_line
= line
;
1714 let (time
, line
) = match parse_time(
1716 self.current_year
[self.current_file_index
],
1717 &mut self.current_month
,
1723 // relative line number within a single timestamp
1724 if time
!= prev_time
{
1725 self.rel_line_nr
= 0;
1727 self.rel_line_nr
+= 1;
1731 // skip until we're in the specified time frame
1732 if time
< self.options
.start
{
1735 // past the specified time frame, we're done, exit the loop
1736 if time
> self.options
.end
{
1742 let (host
, service
, pid
, line
) = match parse_host_service_pid(line
) {
1743 Some((h
, s
, p
, l
)) => (h
, s
, p
, l
),
1749 self.current_record_state
.host
= host
.into();
1750 self.current_record_state
.service
= service
.into();
1751 self.current_record_state
.pid
= pid
;
1752 self.current_record_state
.timestamp
= time
as u64;
1754 self.string_match
= false;
1755 if !self.options
.string_match
.is_empty()
1756 && find(complete_line
, self.options
.string_match
.as_bytes()).is_some()
1758 self.string_match
= true;
1761 // complete_line required for the logs
1762 if service
== b
"pmg-smtp-filter" {
1763 handle_pmg_smtp_filter_message(line
, self, complete_line
);
1764 } else if service
== b
"postfix/postscreen" {
1765 handle_postscreen_message(line
, self, complete_line
);
1766 } else if service
== b
"postfix/qmgr" {
1767 handle_qmgr_message(line
, self, complete_line
);
1768 } else if service
== b
"postfix/lmtp"
1769 || service
== b
"postfix/smtp"
1770 || service
== b
"postfix/local"
1771 || service
== b
"postfix/error"
1773 handle_lmtp_message(line
, self, complete_line
);
1774 } else if service
== b
"postfix/smtpd" {
1775 handle_smtpd_message(line
, self, complete_line
);
1776 } else if service
== b
"postfix/cleanup" {
1777 handle_cleanup_message(line
, self, complete_line
);
1783 /// Returns the number of files to parse. Does not error out if it can't access any file
1784 /// (permission denied)
1785 fn count_files_in_time_range(&mut self) -> usize {
1787 let mut buffer
= Vec
::new();
1789 for (i
, item
) in LOGFILES
.iter().enumerate() {
1790 self.current_month
= 0;
1793 if let Ok(file
) = File
::open(item
) {
1794 self.current_file_index
= i
;
1797 let gzdecoder
= read
::GzDecoder
::new(file
);
1798 let mut reader
= BufReader
::new(gzdecoder
);
1799 // check the first line
1800 if let Ok(size
) = reader
.read_until(b'
\n'
, &mut buffer
) {
1804 if let Some((time
, _
)) = parse_time(
1806 self.current_year
[i
],
1807 &mut self.current_month
,
1809 // found the earliest file in the time frame
1810 if time
< self.options
.start
{
1818 let mut reader
= BufReader
::new(file
);
1819 if let Ok(size
) = reader
.read_until(b'
\n'
, &mut buffer
) {
1823 if let Some((time
, _
)) = parse_time(
1825 self.current_year
[i
],
1826 &mut self.current_month
,
1828 if time
< self.options
.start
{
1844 fn handle_args(&mut self, args
: clap
::ArgMatches
) -> Result
<(), Error
> {
1845 if let Some(inputfile
) = args
.value_of("inputfile") {
1846 self.options
.inputfile
= inputfile
.to_string();
1849 if let Some(start
) = args
.value_of("start") {
1850 if let Ok(res
) = time
::strptime(&start
, "%F %T") {
1851 self.options
.start
= mkgmtime(&res
);
1852 self.start_tm
= res
;
1853 } else if let Ok(res
) = time
::strptime(&start
, "%s") {
1854 let res
= res
.to_local();
1855 self.options
.start
= mkgmtime(&res
);
1856 self.start_tm
= res
;
1858 failure
::bail
!(failure
::err_msg("failed to parse start time"));
1861 let mut ltime
= time
::now();
1865 self.options
.start
= mkgmtime(<ime
);
1866 self.start_tm
= ltime
;
1869 if let Some(end
) = args
.value_of("end") {
1870 if let Ok(res
) = time
::strptime(&end
, "%F %T") {
1871 self.options
.end
= mkgmtime(&res
);
1873 } else if let Ok(res
) = time
::strptime(&end
, "%s") {
1874 let res
= res
.to_local();
1875 self.options
.end
= mkgmtime(&res
);
1878 failure
::bail
!(failure
::err_msg("failed to parse end time"));
1881 let ltime
= time
::now();
1882 self.options
.end
= mkgmtime(<ime
);
1883 self.end_tm
= ltime
;
1886 if self.options
.end
< self.options
.start
{
1887 failure
::bail
!(failure
::err_msg("end time before start time"));
1890 self.options
.limit
= match args
.value_of("limit") {
1891 Some(l
) => l
.parse().unwrap(),
1895 if let Some(qids
) = args
.values_of("qids") {
1897 let ltime
: libc
::time_t
= 0;
1898 let rel_line_nr
: libc
::c_ulong
= 0;
1899 let input
= CString
::new(q
)?
;
1900 let bytes
= concat
!("T%08lXL%08lX", "\0");
1902 unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
;
1904 libc
::sscanf(input
.as_ptr(), format
.as_ptr(), <ime
, &rel_line_nr
) == 2
1908 .push(Match
::RelLineNr(ltime
, rel_line_nr
));
1912 .push(Match
::Qid(q
.as_bytes().into()));
1917 if let Some(from
) = args
.value_of("from") {
1918 self.options
.from
= from
.to_string();
1920 if let Some(to
) = args
.value_of("to") {
1921 self.options
.to
= to
.to_string();
1923 if let Some(host
) = args
.value_of("host") {
1924 self.options
.host
= host
.to_string();
1926 if let Some(msgid
) = args
.value_of("msgid") {
1927 self.options
.msgid
= msgid
.to_string();
1930 self.options
.exclude_greylist
= args
.is_present("exclude_greylist");
1931 self.options
.exclude_ndr
= args
.is_present("exclude_ndr");
1933 self.options
.verbose
= args
.occurrences_of("verbose") as _
;
1935 if let Some(string_match
) = args
.value_of("search") {
1936 self.options
.string_match
= string_match
.to_string();
1942 fn write_all_ok
<T
: AsRef
<[u8]>>(&mut self, data
: T
) {
1943 self.buffered_stdout
1944 .write_all(data
.as_ref())
1945 .expect("failed to write to stdout");
1949 impl Drop
for Parser
{
1950 fn drop(&mut self) {
1951 let mut qentries
= std
::mem
::replace(&mut self.qentries
, HashMap
::new());
1952 for q
in qentries
.values() {
1953 let smtpd
= q
.borrow().smtpd
.clone();
1954 if let Some(s
) = smtpd
{
1955 q
.borrow_mut().print(self, Some(&*s
.borrow()));
1957 q
.borrow_mut().print(self, None
);
1961 let mut sentries
= std
::mem
::replace(&mut self.sentries
, HashMap
::new());
1962 for s
in sentries
.values() {
1963 s
.borrow_mut().print(self);
1969 #[derive(Debug, Default)]
1971 match_list
: Vec
<Match
>,
1973 string_match
: String
,
1978 start
: libc
::time_t
,
1982 exclude_greylist
: bool
,
1989 RelLineNr(libc
::time_t
, u64),
1992 #[derive(Debug, Default)]
1993 struct RecordState
{
2000 fn get_or_create_qentry(
2001 qentries
: &mut HashMap
<Box
<[u8]>, Rc
<RefCell
<QEntry
>>>,
2003 ) -> Rc
<RefCell
<QEntry
>> {
2004 if let Some(qe
) = qentries
.get(qid
) {
2007 let qe
= Rc
::new(RefCell
::new(QEntry
::default()));
2008 qe
.borrow_mut().qid
= qid
.into();
2009 qentries
.insert(qid
.into(), qe
.clone());
2014 fn get_or_create_sentry(
2015 sentries
: &mut HashMap
<u64, Rc
<RefCell
<SEntry
>>>,
2019 ) -> Rc
<RefCell
<SEntry
>> {
2020 if let Some(se
) = sentries
.get(&pid
) {
2023 let se
= Rc
::new(RefCell
::new(SEntry
::default()));
2024 se
.borrow_mut().rel_line_nr
= rel_line_nr
;
2025 se
.borrow_mut().timestamp
= timestamp
;
2026 sentries
.insert(pid
, se
.clone());
2031 fn get_or_create_fentry(
2032 fentries
: &mut HashMap
<Box
<[u8]>, Rc
<RefCell
<FEntry
>>>,
2034 ) -> Rc
<RefCell
<FEntry
>> {
2035 if let Some(fe
) = fentries
.get(qid
) {
2038 let fe
= Rc
::new(RefCell
::new(FEntry
::default()));
2039 fe
.borrow_mut().logid
= qid
.into();
2040 fentries
.insert(qid
.into(), fe
.clone());
2045 fn mkgmtime(tm
: &time
::Tm
) -> libc
::time_t
{
2046 let mut res
: libc
::time_t
;
2048 let mut year
= (tm
.tm_year
+ 1900) as i64;
2049 let mon
= tm
.tm_mon
;
2051 res
= (year
- 1970) * 365 + CAL_MTOD
[mon
as usize];
2057 res
+= (year
- 1968) / 4;
2058 res
-= (year
- 1900) / 100;
2059 res
+= (year
- 1600) / 400;
2061 res
+= (tm
.tm_mday
- 1) as i64;
2062 res
= res
* 24 + tm
.tm_hour
as i64;
2063 res
= res
* 60 + tm
.tm_min
as i64;
2064 res
= res
* 60 + tm
.tm_sec
as i64;
2069 const LOGFILES
: [&str; 32] = [
2071 "/var/log/syslog.1",
2072 "/var/log/syslog.2.gz",
2073 "/var/log/syslog.3.gz",
2074 "/var/log/syslog.4.gz",
2075 "/var/log/syslog.5.gz",
2076 "/var/log/syslog.6.gz",
2077 "/var/log/syslog.7.gz",
2078 "/var/log/syslog.8.gz",
2079 "/var/log/syslog.9.gz",
2080 "/var/log/syslog.10.gz",
2081 "/var/log/syslog.11.gz",
2082 "/var/log/syslog.12.gz",
2083 "/var/log/syslog.13.gz",
2084 "/var/log/syslog.14.gz",
2085 "/var/log/syslog.15.gz",
2086 "/var/log/syslog.16.gz",
2087 "/var/log/syslog.17.gz",
2088 "/var/log/syslog.18.gz",
2089 "/var/log/syslog.19.gz",
2090 "/var/log/syslog.20.gz",
2091 "/var/log/syslog.21.gz",
2092 "/var/log/syslog.22.gz",
2093 "/var/log/syslog.23.gz",
2094 "/var/log/syslog.24.gz",
2095 "/var/log/syslog.25.gz",
2096 "/var/log/syslog.26.gz",
2097 "/var/log/syslog.27.gz",
2098 "/var/log/syslog.28.gz",
2099 "/var/log/syslog.29.gz",
2100 "/var/log/syslog.30.gz",
2101 "/var/log/syslog.31.gz",
2104 /// Parse a QID ([A-Z]+). Returns a tuple of (qid, remaining_text) or None.
2105 fn parse_qid(data
: &[u8], max
: usize) -> Option
<(&[u8], &[u8])> {
2106 // to simplify limit max to data.len()
2107 let max
= max
.min(data
.len());
2108 // take at most max, find the first non-hex-digit
2109 match data
.iter().take(max
).position(|b
| !b
.is_ascii_hexdigit()) {
2110 // if there were less than 2 return nothing
2111 Some(n
) if n
< 2 => None
,
2112 // otherwise split at the first non-hex-digit
2113 Some(n
) => Some(data
.split_at(n
)),
2114 // or return 'max' length QID if no non-hex-digit is found
2115 None
=> Some(data
.split_at(max
)),
2119 /// Parse a number. Returns a tuple of (parsed_number, remaining_text) or None.
2120 fn parse_number(data
: &[u8], max_digits
: usize) -> Option
<(usize, &[u8])> {
2121 let max
= max_digits
.min(data
.len());
2123 match data
.iter().take(max
).position(|b
| !b
.is_ascii_digit()) {
2124 Some(n
) if n
== 0 => None
,
2126 let (number
, data
) = data
.split_at(n
);
2127 // number only contains ascii digits
2128 let number
= unsafe { std::str::from_utf8_unchecked(number) }
2131 Some((number
, data
))
2134 let (number
, data
) = data
.split_at(max
);
2135 // number only contains ascii digits
2136 let number
= unsafe { std::str::from_utf8_unchecked(number) }
2139 Some((number
, data
))
2144 /// Parse time. Returns a tuple of (parsed_time, remaining_text) or None.
2148 cur_month
: &mut i64,
2149 ) -> Option
<(libc
::time_t
, &'a
[u8])> {
2150 if data
.len() < 15 {
2154 let mon
= match &data
[0..3] {
2169 let data
= &data
[3..];
2171 let mut ltime
: libc
::time_t
;
2172 let mut year
= cur_year
;
2174 if *cur_month
== 11 && mon
== 0 {
2177 if mon
> *cur_month
{
2181 ltime
= (year
- 1970) * 365 + CAL_MTOD
[mon
as usize];
2187 ltime
+= (year
- 1968) / 4;
2188 ltime
-= (year
- 1900) / 100;
2189 ltime
+= (year
- 1600) / 400;
2191 let whitespace_count
= data
.iter().take_while(|b
| b
.is_ascii_whitespace()).count();
2192 let data
= &data
[whitespace_count
..];
2194 let (mday
, data
) = match parse_number(data
, 2) {
2204 ltime
+= (mday
- 1) as i64;
2206 let data
= &data
[1..];
2208 let (hour
, data
) = match parse_number(data
, 2) {
2216 ltime
+= hour
as i64;
2218 if let Some(c
) = data
.iter().next() {
2219 if (*c
as char) != '
:'
{
2225 let data
= &data
[1..];
2227 let (min
, data
) = match parse_number(data
, 2) {
2235 ltime
+= min
as i64;
2237 if let Some(c
) = data
.iter().next() {
2238 if (*c
as char) != '
:'
{
2244 let data
= &data
[1..];
2246 let (sec
, data
) = match parse_number(data
, 2) {
2254 ltime
+= sec
as i64;
2256 let data
= &data
[1..];
2261 const CAL_MTOD
: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
2263 type ByteSlice
<'a
> = &'a
[u8];
2264 /// Parse Host, Service and PID at the beginning of data. Returns a tuple of (host, service, pid, remaining_text).
2265 fn parse_host_service_pid(data
: &[u8]) -> Option
<(ByteSlice
, ByteSlice
, u64, ByteSlice
)> {
2266 let host_count
= data
2268 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
2270 let host
= &data
[0..host_count
];
2271 let data
= &data
[host_count
+ 1..]; // whitespace after host
2273 let service_count
= data
2276 (**b
as char).is_ascii_alphabetic() || (**b
as char) == '
/'
|| (**b
as char) == '
-'
2279 let service
= &data
[0..service_count
];
2280 let data
= &data
[service_count
..];
2281 if data
.get(0) != Some(&b'
['
) {
2284 let data
= &data
[1..];
2286 let pid_count
= data
2288 .take_while(|b
| (**b
as char).is_ascii_digit())
2290 let pid
= match unsafe { std::str::from_utf8_unchecked(&data[0..pid_count]) }
.parse() {
2291 // all ascii digits so valid utf8
2293 Err(_
) => return None
,
2295 let data
= &data
[pid_count
..];
2296 if !data
.starts_with(b
"]: ") {
2299 let data
= &data
[3..];
2301 Some((host
, service
, pid
, data
))
2304 /// A find implementation for [u8]. Returns the index or None.
2305 fn find
<T
: PartialOrd
>(data
: &[T
], needle
: &[T
]) -> Option
<usize> {
2306 data
.windows(needle
.len()).position(|d
| d
== needle
)
2309 /// A find implementation for [u8] that converts to lowercase before the comparison. Returns the
2311 fn find_lowercase(data
: &[u8], needle
: &[u8]) -> Option
<usize> {
2312 let data
= data
.to_ascii_lowercase();
2313 let needle
= needle
.to_ascii_lowercase();
2314 data
.windows(needle
.len()).position(|d
| d
== &needle
[..])