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 let q
= fe
.borrow().qentry
.clone();
468 if let Some(q
) = q
.upgrade() {
469 if !Rc
::ptr_eq(&q
, &qe
) {
470 // QEntries don't match, set all flags to false and
471 // remove the referenced FEntry
472 q
.borrow_mut().filtered
= false;
473 q
.borrow_mut().bq_filtered
= false;
474 q
.borrow_mut().filter
= None
;
475 // update FEntry's QEntry reference to the new one
476 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 if let Some(f
) = f
.upgrade() {
518 parser
.free_fentry(&f
.borrow().logid
);
521 parser
.free_sentry(se
.borrow().pid
);
523 se
.borrow_mut().finalize_refs(parser
);
528 // NOQUEUE in smtpd, happens after postscreen
529 if msg
.starts_with(b
"NOQUEUE:") {
530 let data
= &msg
[8..];
531 let colon_index
= match find(data
, b
":") {
535 let data
= &data
[colon_index
+ 1..];
536 let colon_index
= match find(data
, b
":") {
540 let data
= &data
[colon_index
+ 1..];
541 let semicolon_index
= match find(data
, b
";") {
546 // check for the string, if it matches then greylisting is the reason
547 // for the NOQUEUE entry
548 let (grey
, data
) = data
.split_at(semicolon_index
);
549 let dstatus
= if find(
551 b
"Recipient address rejected: Service is unavailable (try later)",
560 if !data
.starts_with(b
"; from=<") {
563 let data
= &data
[8..];
564 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
565 let (from
, data
) = data
.split_at(from_count
);
567 if !data
.starts_with(b
"> to=<") {
570 let data
= &data
[6..];
571 let to_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
572 let to
= &data
[..to_count
];
575 .add_noqueue_entry(from
, to
, dstatus
, parser
.current_record_state
.timestamp
);
579 // only happens with before-queue
580 // here we can match the pmg-smtp-filter
581 // 'proxy-accept' happens if it is accepted for AT LEAST ONE receiver
582 if msg
.starts_with(b
"proxy-accept: ") {
583 let data
= &msg
[14..];
584 if !data
.starts_with(b
"END-OF-MESSAGE: ") {
587 let data
= &data
[16..];
588 if !data
.starts_with(b
"250 2.5.0 OK (") {
591 let data
= &data
[14..];
592 if let Some((qid
, data
)) = parse_qid(data
, 25) {
593 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
594 // set the FEntry to before-queue filtered
595 fe
.borrow_mut().is_bq
= true;
596 // if there's no 'accept mail to' entry because of quarantine
597 // we have to match the pmg-smtp-filter here
598 // for 'accepted' mails it is matched in the 'accept mail to'
600 if !fe
.borrow().is_accepted
{
601 // set the SEntry filter reference as we don't have a QEntry
603 se
.borrow_mut().filter
= Some(Rc
::downgrade(&fe
));
605 if let Some(from_index
) = find(data
, b
"from=<") {
606 let data
= &data
[from_index
+ 6..];
607 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
608 let from
= &data
[..from_count
];
609 // keep the from for later printing
610 // required for the correct 'TO:{}:{}...' syntax required
611 // by PMG/API2/MailTracker.pm
612 se
.borrow_mut().bq_from
= from
.into();
614 } else if let Some(qe
) = &fe
.borrow().qentry
{
615 // mail is 'accepted', add a reference to the QEntry to the
616 // SEntry so we can wait for all to be finished before printing
617 if let Some(qe
) = qe
.upgrade() {
618 qe
.borrow_mut().bq_sentry
= Some(Rc
::clone(&se
));
619 SEntry
::add_ref(&se
, &qe
, true);
622 // specify that before queue filtering is used and the mail was
623 // accepted, but not necessarily by an 'accept' rule
625 se
.borrow_mut().is_bq_accepted
= true;
631 // before queue filtering and rejected, here we can match the
632 // pmg-smtp-filter same as in the 'proxy-accept' case
633 // only happens if the mail was rejected for ALL receivers, otherwise
634 // a 'proxy-accept' happens
635 if msg
.starts_with(b
"proxy-reject: ") {
636 let data
= &msg
[14..];
637 if !data
.starts_with(b
"END-OF-MESSAGE: ") {
640 let data
= &data
[16..];
641 if let Some(qid_index
) = find(data
, b
"(") {
642 let data
= &data
[qid_index
+ 1..];
643 if let Some((qid
, data
)) = parse_qid(data
, 25) {
644 let fe
= get_or_create_fentry(&mut parser
.fentries
, qid
);
645 // set the FEntry to before-queue filtered
646 fe
.borrow_mut().is_bq
= true;
647 // we never have a QEntry in this case, so just set the SEntry
649 se
.borrow_mut().filter
= Some(Rc
::downgrade(&fe
));
650 // specify that before queue filtering is used and the mail
651 // was rejected for all receivers
652 se
.borrow_mut().is_bq_rejected
= true;
654 if let Some(from_index
) = find(data
, b
"from=<") {
655 let data
= &data
[from_index
+ 6..];
656 let from_count
= data
.iter().take_while(|b
| (**b
as char) != '
>'
).count();
657 let from
= &data
[..from_count
];
658 // same as for 'proxy-accept' above
659 se
.borrow_mut().bq_from
= from
.into();
667 // with none of the other messages matching, we try for a QID to match the
668 // corresponding QEntry to the SEntry
669 let (qid
, data
) = match parse_qid(msg
, 15) {
673 let data
= &data
[2..];
675 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
677 if parser
.string_match
{
678 qe
.borrow_mut().string_match
= parser
.string_match
;
681 SEntry
::add_ref(&se
, &qe
, false);
683 if !data
.starts_with(b
"client=") {
686 let data
= &data
[7..];
687 let client_count
= data
689 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
691 let client
= &data
[..client_count
];
693 qe
.borrow_mut().set_client(client
);
696 // handle log entries for 'cleanup'
697 // happens before the mail is passed to qmgr (after-queue or before-queue
699 fn handle_cleanup_message(msg
: &[u8], parser
: &mut Parser
, complete_line
: &[u8]) {
700 let (qid
, data
) = match parse_qid(msg
, 15) {
704 let data
= &data
[2..];
706 let qe
= get_or_create_qentry(&mut parser
.qentries
, qid
);
708 if parser
.string_match
{
709 qe
.borrow_mut().string_match
= parser
.string_match
;
713 .push((complete_line
.into(), parser
.lines
));
715 if !data
.starts_with(b
"message-id=") {
718 let data
= &data
[11..];
719 let msgid_count
= data
721 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
723 let msgid
= &data
[..msgid_count
];
725 if !msgid
.is_empty() {
726 if qe
.borrow().msgid
.is_empty() {
727 qe
.borrow_mut().msgid
= msgid
.into();
729 qe
.borrow_mut().cleanup
= true;
733 #[derive(Default, Debug)]
734 struct NoqueueEntry
{
749 impl Default
for ToEntry
{
750 fn default() -> Self {
752 to
: Default
::default(),
753 relay
: (&b
"none"[..]).into(),
754 dstatus
: Default
::default(),
755 timestamp
: Default
::default(),
760 #[derive(Debug, PartialEq, Copy, Clone)]
771 impl Default
for DStatus
{
772 fn default() -> Self {
777 impl std
::fmt
::Display
for DStatus
{
778 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
780 DStatus
::Invalid
=> '
\0'
, // other default
781 DStatus
::Accept
=> 'A'
,
782 DStatus
::Quarantine
=> 'Q'
,
783 DStatus
::Block
=> 'B'
,
784 DStatus
::Greylist
=> 'G'
,
785 DStatus
::Noqueue
=> 'N'
,
786 DStatus
::Dsn(v
) => std
::char::from_digit(*v
, 10).unwrap(),
792 #[derive(Debug, Default)]
794 log
: Vec
<(Box
<[u8]>, u64)>,
798 // references to QEntries, Weak so they are not kept alive longer than
799 // necessary, RefCell for mutability (Rc<> is immutable)
800 refs
: Vec
<Weak
<RefCell
<QEntry
>>>,
801 nq_entries
: Vec
<NoqueueEntry
>,
803 // only set in case of before queue filtering
804 // used as a fallback in case no QEntry is referenced
805 filter
: Option
<Weak
<RefCell
<FEntry
>>>,
809 // before queue filtering with the mail accepted for at least one receiver
810 is_bq_accepted
: bool
,
811 // before queue filtering with the mail rejected for all receivers
812 is_bq_rejected
: bool
,
813 // from address saved for compatibility with after queue filtering
818 fn add_noqueue_entry(&mut self, from
: &[u8], to
: &[u8], dstatus
: DStatus
, timestamp
: u64) {
819 let ne
= NoqueueEntry
{
825 self.nq_entries
.push(ne
);
828 fn set_connect(&mut self, client
: &[u8]) {
829 if self.connect
.is_empty() {
830 self.connect
= client
.into();
834 fn print(&mut self, parser
: &mut Parser
) {
835 // don't print if the output is filtered by the message-id
836 // the message-id is only available in a QEntry
837 if !parser
.options
.msgid
.is_empty() {
841 // don't print if the output is filtered by a host but the connect
842 // field is empty or does not match
843 if !parser
.options
.host
.is_empty() {
844 if self.connect
.is_empty() {
847 if find_lowercase(&self.connect
, parser
.options
.host
.as_bytes()).is_none() {
852 // don't print if the output is filtered by time and line number
854 if !parser
.options
.match_list
.is_empty() {
855 let mut found
= false;
856 for m
in parser
.options
.match_list
.iter() {
858 Match
::Qid(_
) => return,
859 Match
::RelLineNr(t
, l
) => {
860 if (*t
as u64) == self.timestamp
&& *l
== self.rel_line_nr
{
872 // if either ;from' or 'to' are set, check if it matches, if not, set
873 // the status of the noqueue entry to Invalid
874 // if exclude_greylist or exclude_ndr are set, check if it matches
875 // and if so, set the status to Invalid so they are no longer included
876 // don't print if any Invalid entry is found
877 if !parser
.options
.from
.is_empty()
878 || !parser
.options
.to
.is_empty()
879 || parser
.options
.exclude_greylist
880 || parser
.options
.exclude_ndr
882 let mut found
= false;
883 for nq
in self.nq_entries
.iter_mut().rev() {
884 if (!parser
.options
.from
.is_empty()
885 && find_lowercase(&nq
.from
, parser
.options
.from
.as_bytes()).is_none())
886 || (parser
.options
.exclude_greylist
&& nq
.dstatus
== DStatus
::Greylist
)
887 || (parser
.options
.exclude_ndr
&& nq
.from
.is_empty())
888 || (!parser
.options
.to
.is_empty()
890 && find_lowercase(&nq
.to
, parser
.options
.to
.as_bytes()).is_none())
892 nq
.dstatus
= DStatus
::Invalid
;
895 if nq
.dstatus
!= DStatus
::Invalid
{
905 // don't print if there's a string match specified, but none of the
906 // log entries matches
907 if !parser
.options
.string_match
.is_empty() && !self.string_match
{
911 if parser
.options
.verbose
> 0 {
912 parser
.write_all_ok(format
!(
913 "SMTPD: T{:8X}L{:08X}\n",
914 self.timestamp
as u32, self.rel_line_nr
as u32
916 parser
.write_all_ok(format
!("CTIME: {:8X}\n", parser
.ctime
).as_bytes());
918 if !self.connect
.is_empty() {
919 parser
.write_all_ok(b
"CLIENT: ");
920 parser
.write_all_ok(&self.connect
);
921 parser
.write_all_ok(b
"\n");
925 // only print the entry if the status is not invalid
926 // rev() for compatibility with the C code which uses a linked list
927 // that adds entries at the front, while a Vec in Rust adds it at the
929 for nq
in self.nq_entries
.iter().rev() {
930 if nq
.dstatus
!= DStatus
::Invalid
{
931 parser
.write_all_ok(format
!(
932 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
933 nq
.timestamp
as i32, self.timestamp
as i32, self.rel_line_nr
, nq
.dstatus
,
935 parser
.write_all_ok(&nq
.from
);
936 parser
.write_all_ok(b
"> to <");
937 parser
.write_all_ok(&nq
.to
);
938 parser
.write_all_ok(b
">\n");
943 let print_filter_to_entries_fn
=
944 |fe
: &Rc
<RefCell
<FEntry
>>,
947 dstatus
: Option
<DStatus
>| {
948 let mut dstatus
= match dstatus
{
950 None
=> DStatus
::Invalid
,
952 for to
in fe
.borrow().to_entries
.iter().rev() {
953 if dstatus
== DStatus
::Invalid
{
954 dstatus
= to
.dstatus
;
956 parser
.write_all_ok(format
!(
957 "TO:{:X}:T{:08X}L{:08X}:{}: from <",
958 to
.timestamp
as i32, se
.timestamp
as i32, se
.rel_line_nr
, dstatus
,
960 parser
.write_all_ok(&se
.bq_from
);
961 parser
.write_all_ok(b
"> to <");
962 parser
.write_all_ok(&to
.to
);
963 parser
.write_all_ok(b
">\n");
968 // only true in before queue filtering case
969 if let Some(fe
) = &self.filter
{
970 if let Some(fe
) = fe
.upgrade() {
971 // limited to !fe.is_accepted because otherwise we would have
972 // a QEntry with all required information instead
973 if fe
.borrow().is_bq
&& !fe
.borrow().is_accepted
&& self.is_bq_accepted
{
974 print_filter_to_entries_fn(&fe
, parser
, self, None
);
975 } else if fe
.borrow().is_bq
&& !fe
.borrow().is_accepted
&& self.is_bq_rejected
{
976 print_filter_to_entries_fn(&fe
, parser
, self, Some(DStatus
::Noqueue
));
981 let print_log
= |parser
: &mut Parser
, logs
: &Vec
<(Box
<[u8]>, u64)>| {
982 for (log
, line
) in logs
.iter() {
983 parser
.write_all_ok(format
!("L{:08X} ", *line
as u32));
984 parser
.write_all_ok(log
);
985 parser
.write_all_ok(b
"\n");
989 // if '-vv' is passed to the log tracker, print all the logs
990 if parser
.options
.verbose
> 1 {
991 parser
.write_all_ok(b
"LOGS:\n");
992 let mut logs
= self.log
.clone();
993 if let Some(f
) = &self.filter
{
994 if let Some(f
) = f
.upgrade() {
995 logs
.append(&mut f
.borrow().log
.clone());
996 // as the logs come from 1 SEntry and 1 FEntry,
997 // interleave them via sort based on line number
998 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1002 print_log(parser
, &logs
);
1004 parser
.write_all_ok(b
"\n");
1007 fn delete_ref(&mut self, qentry
: &Rc
<RefCell
<QEntry
>>) {
1008 self.refs
.retain(|q
| {
1009 let q
= match q
.upgrade() {
1011 None
=> return false,
1013 if Rc
::ptr_eq(&q
, qentry
) {
1020 fn remove_unneeded_refs(&mut self, parser
: &mut Parser
) -> u32 {
1021 let mut count
: u32 = 0;
1022 let mut to_delete
= Vec
::new();
1023 self.refs
.retain(|q
| {
1024 let q
= match q
.upgrade() {
1026 None
=> return false,
1028 let is_cleanup
= q
.borrow().cleanup
;
1029 // add those that require freeing to a separate Vec as self is
1030 // borrowed mutable here and can't be borrowed again for the
1031 // parser.free_qentry() call
1041 for q
in to_delete
.iter().rev() {
1042 parser
.free_qentry(&q
.borrow().qid
, Some(self));
1047 // print and free all QEntries that are removed and if a filter is set,
1048 // if the filter is finished
1049 fn finalize_refs(&mut self, parser
: &mut Parser
) {
1050 let mut qentries
= Vec
::new();
1051 for q
in self.refs
.iter() {
1052 let q
= match q
.upgrade() {
1057 if !q
.borrow().removed
{
1061 let fe
= &q
.borrow().filter
;
1062 if let Some(f
) = fe
{
1063 if !q
.borrow().bq_filtered
&& !f
.borrow().finished
{
1068 if !self.is_bq_accepted
&& q
.borrow().bq_sentry
.is_some() {
1069 if let Some(se
) = &q
.borrow().bq_sentry
{
1070 // we're already disconnected, but the SEntry referenced
1071 // by the QEntry might not yet be done
1072 if !se
.borrow().disconnected
{
1073 // add a reference to the SEntry referenced by the
1074 // QEntry so it gets deleted when both the SEntry
1075 // and the QEntry is done
1076 Self::add_ref(&se
, &q
, true);
1082 qentries
.push(Rc
::clone(&q
));
1085 for q
in qentries
.iter().rev() {
1086 q
.borrow_mut().print(parser
, Some(self));
1087 parser
.free_qentry(&q
.borrow().qid
, Some(self));
1089 if let Some(f
) = &q
.borrow().filter
{
1090 parser
.free_fentry(&f
.borrow().logid
);
1095 fn add_ref(sentry
: &Rc
<RefCell
<SEntry
>>, qentry
: &Rc
<RefCell
<QEntry
>>, bq
: bool
) {
1096 let smtpd
= qentry
.borrow().smtpd
.clone();
1098 if let Some(s
) = smtpd
{
1099 if !Rc
::ptr_eq(sentry
, &s
) {
1100 eprintln
!("Error: qentry ref already set");
1106 for q
in sentry
.borrow().refs
.iter() {
1107 let q
= match q
.upgrade() {
1111 if Rc
::ptr_eq(&q
, qentry
) {
1116 sentry
.borrow_mut().refs
.push(Rc
::downgrade(qentry
));
1118 qentry
.borrow_mut().smtpd
= Some(Rc
::clone(sentry
));
1123 #[derive(Default, Debug)]
1125 log
: Vec
<(Box
<[u8]>, u64)>,
1126 smtpd
: Option
<Rc
<RefCell
<SEntry
>>>,
1127 filter
: Option
<Rc
<RefCell
<FEntry
>>>,
1133 to_entries
: Vec
<ToEntry
>,
1139 // will differ from smtpd
1140 bq_sentry
: Option
<Rc
<RefCell
<SEntry
>>>,
1144 fn add_to_entry(&mut self, to
: &[u8], relay
: &[u8], dstatus
: DStatus
, timestamp
: u64) {
1147 relay
: relay
.into(),
1151 self.to_entries
.push(te
);
1154 // finalize and print the QEntry
1155 fn finalize(&mut self, parser
: &mut Parser
) {
1156 // if it is not removed, skip
1158 if let Some(se
) = &self.smtpd
{
1159 // verify that the SEntry it is attached to is disconnected
1160 if !se
.borrow().disconnected
{
1164 if let Some(s
) = &self.bq_sentry
{
1165 if self.bq_filtered
&& !s
.borrow().disconnected
{
1170 if let Some(fe
) = self.filter
.clone() {
1171 // verify that the attached FEntry is finished if it is not
1172 // before queue filtered
1173 if !self.bq_filtered
&& !fe
.borrow().finished
{
1177 // if there's an SEntry, print with the SEntry
1178 // otherwise just print the QEntry (this can happen in certain
1180 match self.smtpd
.clone() {
1181 Some(s
) => self.print(parser
, Some(&*s
.borrow())),
1182 None
=> self.print(parser
, None
),
1184 if let Some(se
) = &self.smtpd
{
1185 parser
.free_qentry(&self.qid
, Some(&mut *se
.borrow_mut()));
1187 parser
.free_qentry(&self.qid
, None
);
1190 if !self.bq_filtered
{
1191 parser
.free_fentry(&fe
.borrow().logid
);
1193 } else if let Some(s
) = self.smtpd
.clone() {
1194 self.print(parser
, Some(&*s
.borrow()));
1195 parser
.free_qentry(&self.qid
, Some(&mut *s
.borrow_mut()));
1197 self.print(parser
, None
);
1198 parser
.free_qentry(&self.qid
, None
);
1203 fn msgid_matches(&self, parser
: &Parser
) -> bool
{
1204 if !parser
.options
.msgid
.is_empty() {
1205 if self.msgid
.is_empty() {
1208 let qentry_msgid_lowercase
= self.msgid
.to_ascii_lowercase();
1209 let msgid_lowercase
= parser
.options
.msgid
.as_bytes().to_ascii_lowercase();
1210 if qentry_msgid_lowercase
!= msgid_lowercase
{
1217 fn match_list_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1218 let fe
= &self.filter
;
1219 if !parser
.options
.match_list
.is_empty() {
1220 let mut found
= false;
1221 for m
in parser
.options
.match_list
.iter() {
1224 if let Some(f
) = fe
{
1225 if &f
.borrow().logid
== q
{
1235 Match
::RelLineNr(t
, l
) => {
1236 if let Some(s
) = se
{
1237 if s
.timestamp
== (*t
as u64) && s
.rel_line_nr
== *l
{
1252 fn host_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1253 if !parser
.options
.host
.is_empty() {
1254 let mut found
= false;
1255 if let Some(s
) = se
{
1256 if !s
.connect
.is_empty()
1257 && find_lowercase(&s
.connect
, parser
.options
.host
.as_bytes()).is_some()
1262 if !self.client
.is_empty()
1263 && find_lowercase(&self.client
, parser
.options
.host
.as_bytes()).is_some()
1275 fn from_to_matches(&mut self, parser
: &Parser
) -> bool
{
1276 if !parser
.options
.from
.is_empty() {
1277 if self.from
.is_empty() {
1280 if find_lowercase(&self.from
, parser
.options
.from
.as_bytes()).is_none() {
1283 } else if parser
.options
.exclude_ndr
&& self.from
.is_empty() {
1287 if !parser
.options
.to
.is_empty() {
1288 let mut found
= false;
1289 self.to_entries
.retain(|to
| {
1290 if find_lowercase(&to
.to
, parser
.options
.to
.as_bytes()).is_none() {
1304 fn string_matches(&self, parser
: &Parser
, se
: Option
<&SEntry
>) -> bool
{
1305 let fe
= &self.filter
;
1306 if !parser
.options
.string_match
.is_empty() {
1307 let mut string_match
= self.string_match
;
1309 if let Some(s
) = se
{
1311 string_match
= true;
1314 if let Some(f
) = fe
{
1315 if f
.borrow().string_match
{
1316 string_match
= true;
1326 // is_se_bq_sentry is true if the QEntry::bq_sentry is the same as passed
1327 // into the print() function via reference
1328 fn print_qentry_boilerplate(
1330 parser
: &mut Parser
,
1331 is_se_bq_sentry
: bool
,
1332 se
: Option
<&SEntry
>,
1334 parser
.write_all_ok(b
"QENTRY: ");
1335 parser
.write_all_ok(&self.qid
);
1336 parser
.write_all_ok(b
"\n");
1337 parser
.write_all_ok(format
!("CTIME: {:8X}\n", parser
.ctime
));
1338 parser
.write_all_ok(format
!("SIZE: {}\n", self.size
));
1340 if !self.client
.is_empty() {
1341 parser
.write_all_ok(b
"CLIENT: ");
1342 parser
.write_all_ok(&self.client
);
1343 parser
.write_all_ok(b
"\n");
1344 } else if !is_se_bq_sentry
{
1345 if let Some(s
) = se
{
1346 if !s
.connect
.is_empty() {
1347 parser
.write_all_ok(b
"CLIENT: ");
1348 parser
.write_all_ok(&s
.connect
);
1349 parser
.write_all_ok(b
"\n");
1352 } else if let Some(s
) = &self.smtpd
{
1353 if !s
.borrow().connect
.is_empty() {
1354 parser
.write_all_ok(b
"CLIENT: ");
1355 parser
.write_all_ok(&s
.borrow().connect
);
1356 parser
.write_all_ok(b
"\n");
1360 if !self.msgid
.is_empty() {
1361 parser
.write_all_ok(b
"MSGID: ");
1362 parser
.write_all_ok(&self.msgid
);
1363 parser
.write_all_ok(b
"\n");
1367 fn print(&mut self, parser
: &mut Parser
, se
: Option
<&SEntry
>) {
1368 let fe
= self.filter
.clone();
1370 if !self.msgid_matches(parser
)
1371 || !self.match_list_matches(parser
, se
)
1372 || !self.host_matches(parser
, se
)
1373 || !self.from_to_matches(parser
)
1374 || !self.string_matches(parser
, se
)
1379 // necessary so we do not attempt to mutable borrow it a second time
1381 let is_se_bq_sentry
= match (&self.bq_sentry
, se
) {
1382 (Some(s
), Some(se
)) => std
::ptr
::eq(s
.as_ptr(), se
),
1386 if is_se_bq_sentry
{
1387 if let Some(s
) = &se
{
1388 if !s
.disconnected
{
1394 if parser
.options
.verbose
> 0 {
1395 self.print_qentry_boilerplate(parser
, is_se_bq_sentry
, se
);
1398 // rev() to match the C code iteration direction (linked list vs Vec)
1399 for to
in self.to_entries
.iter().rev() {
1400 if !to
.to
.is_empty() {
1403 let mut final_to
: &ToEntry
= to
;
1404 // if status == success and there's a filter attached that has
1405 // a matching 'to' in one of the ToEntries, set the ToEntry to
1406 // the one in the filter
1407 if to
.dstatus
== DStatus
::Dsn(2) {
1408 if let Some(f
) = &fe
{
1409 if !self.bq_filtered
|| (f
.borrow().finished
&& f
.borrow().is_bq
) {
1411 final_borrow
= final_rc
.borrow();
1412 for to2
in final_borrow
.to_entries
.iter().rev() {
1413 if to
.to
== to2
.to
{
1422 parser
.write_all_ok(format
!("TO:{:X}:", to
.timestamp
as i32,));
1423 parser
.write_all_ok(&self.qid
);
1424 parser
.write_all_ok(format
!(":{}: from <", final_to
.dstatus
));
1425 parser
.write_all_ok(&self.from
);
1426 parser
.write_all_ok(b
"> to <");
1427 parser
.write_all_ok(&final_to
.to
);
1428 parser
.write_all_ok(b
"> (");
1429 // if we use the relay from the filter ToEntry, it will be
1430 // marked 'is_relay' in PMG/API2/MailTracker.pm and not shown
1431 // in the GUI in the case of before queue filtering
1432 if !self.bq_filtered
{
1433 parser
.write_all_ok(&final_to
.relay
);
1435 parser
.write_all_ok(&to
.relay
);
1437 parser
.write_all_ok(b
")\n");
1442 // print logs if '-vv' is specified
1443 if parser
.options
.verbose
> 1 {
1444 let print_log
= |parser
: &mut Parser
, logs
: &Vec
<(Box
<[u8]>, u64)>| {
1445 for (log
, line
) in logs
.iter() {
1446 parser
.write_all_ok(format
!("L{:08X} ", *line
as u32));
1447 parser
.write_all_ok(log
);
1448 parser
.write_all_ok(b
"\n");
1451 if !is_se_bq_sentry
{
1452 if let Some(s
) = se
{
1453 let mut logs
= s
.log
.clone();
1454 if let Some(bq_se
) = &self.bq_sentry
{
1455 logs
.append(&mut bq_se
.borrow().log
.clone());
1456 // as the logs come from 2 different SEntries,
1457 // interleave them via sort based on line number
1458 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1460 if !logs
.is_empty() {
1461 parser
.write_all_ok(b
"SMTP:\n");
1462 print_log(parser
, &logs
);
1465 } else if let Some(s
) = &self.smtpd
{
1466 let mut logs
= s
.borrow().log
.clone();
1467 if let Some(se
) = se
{
1468 logs
.append(&mut se
.log
.clone());
1469 // as the logs come from 2 different SEntries,
1470 // interleave them via sort based on line number
1471 logs
.sort_by(|a
, b
| a
.1.cmp(&b
.1));
1473 if !logs
.is_empty() {
1474 parser
.write_all_ok(b
"SMTP:\n");
1475 print_log(parser
, &logs
);
1479 if let Some(f
) = fe
{
1480 if (!self.bq_filtered
|| (f
.borrow().finished
&& f
.borrow().is_bq
))
1481 && !f
.borrow().log
.is_empty()
1483 parser
.write_all_ok(format
!("FILTER: {}\n", unsafe {
1484 std
::str::from_utf8_unchecked(&f
.borrow().logid
)
1486 print_log(parser
, &f
.borrow().log
);
1490 if !self.log
.is_empty() {
1491 parser
.write_all_ok(b
"QMGR:\n");
1492 print_log(parser
, &self.log
);
1495 parser
.write_all_ok(b
"\n")
1498 fn set_client(&mut self, client
: &[u8]) {
1499 if self.client
.is_empty() {
1500 self.client
= client
.into();
1505 #[derive(Default, Debug)]
1507 log
: Vec
<(Box
<[u8]>, u64)>,
1509 to_entries
: Vec
<ToEntry
>,
1510 processing_time
: Box
<[u8]>,
1514 qentry
: Option
<Weak
<RefCell
<QEntry
>>>,
1519 fn add_accept(&mut self, to
: &[u8], qid
: &[u8], timestamp
: u64) {
1523 dstatus
: DStatus
::Accept
,
1526 self.to_entries
.push(te
);
1527 self.is_accepted
= true;
1530 fn add_quarantine(&mut self, to
: &[u8], qid
: &[u8], timestamp
: u64) {
1534 dstatus
: DStatus
::Quarantine
,
1537 self.to_entries
.push(te
);
1540 fn add_block(&mut self, to
: &[u8], timestamp
: u64) {
1543 relay
: (&b
"none"[..]).into(),
1544 dstatus
: DStatus
::Block
,
1547 self.to_entries
.push(te
);
1550 fn set_processing_time(&mut self, time
: &[u8]) {
1551 self.processing_time
= time
.into();
1552 self.finished
= true;
1558 sentries
: HashMap
<u64, Rc
<RefCell
<SEntry
>>>,
1559 fentries
: HashMap
<Box
<[u8]>, Rc
<RefCell
<FEntry
>>>,
1560 qentries
: HashMap
<Box
<[u8]>, Rc
<RefCell
<QEntry
>>>,
1562 current_record_state
: RecordState
,
1565 current_year
: [i64; 32],
1567 current_file_index
: usize,
1571 buffered_stdout
: BufWriter
<std
::io
::Stdout
>,
1578 ctime
: libc
::time_t
,
1586 let mut years
: [i64; 32] = [0; 32];
1588 for (i
, year
) in years
.iter_mut().enumerate() {
1589 let mut ts
= time
::get_time();
1590 ts
.sec
-= (3600 * 24 * i
) as i64;
1591 let ltime
= time
::at(ts
);
1592 *year
= (ltime
.tm_year
+ 1900) as i64;
1596 sentries
: HashMap
::new(),
1597 fentries
: HashMap
::new(),
1598 qentries
: HashMap
::new(),
1599 current_record_state
: Default
::default(),
1601 current_year
: years
,
1603 current_file_index
: 0,
1605 buffered_stdout
: BufWriter
::with_capacity(4 * 1024 * 1024, std
::io
::stdout()),
1606 options
: Options
::default(),
1607 start_tm
: time
::empty_tm(),
1608 end_tm
: time
::empty_tm(),
1610 string_match
: false,
1615 fn free_sentry(&mut self, sentry_pid
: u64) {
1616 self.sentries
.remove(&sentry_pid
);
1619 fn free_qentry(&mut self, qid
: &[u8], se
: Option
<&mut SEntry
>) {
1620 if let Some(qe
) = self.qentries
.get(qid
) {
1621 if let Some(se
) = se
{
1626 self.qentries
.remove(qid
);
1629 fn free_fentry(&mut self, fentry_logid
: &[u8]) {
1630 self.fentries
.remove(fentry_logid
);
1633 fn parse_files(&mut self) -> Result
<(), Error
> {
1634 if !self.options
.inputfile
.is_empty() {
1635 if self.options
.inputfile
== "-" {
1637 self.current_file_index
= 0;
1638 let mut reader
= BufReader
::new(std
::io
::stdin());
1639 self.handle_input_by_line(&mut reader
)?
;
1640 } else if let Ok(file
) = File
::open(&self.options
.inputfile
) {
1641 // read from specified file
1642 self.current_file_index
= 0;
1643 let mut reader
= BufReader
::new(file
);
1644 self.handle_input_by_line(&mut reader
)?
;
1647 let filecount
= self.count_files_in_time_range();
1648 for i
in (0..filecount
).rev() {
1649 self.current_month
= 0;
1650 if let Ok(file
) = File
::open(LOGFILES
[i
]) {
1651 self.current_file_index
= i
;
1653 let gzdecoder
= read
::GzDecoder
::new(file
);
1654 let mut reader
= BufReader
::new(gzdecoder
);
1655 self.handle_input_by_line(&mut reader
)?
;
1657 let mut reader
= BufReader
::new(file
);
1658 self.handle_input_by_line(&mut reader
)?
;
1667 fn handle_input_by_line(&mut self, reader
: &mut dyn BufRead
) -> Result
<(), Error
> {
1668 let mut buffer
= Vec
::<u8>::with_capacity(4096);
1669 let mut prev_time
= 0;
1671 if self.options
.limit
> 0 && (self.count
>= self.options
.limit
) {
1672 self.write_all_ok("STATUS: aborted by limit (too many hits)\n");
1673 self.buffered_stdout
.flush()?
;
1674 std
::process
::exit(0);
1678 let size
= match reader
.read_until(b'
\n'
, &mut buffer
) {
1679 Err(e
) => return Err(e
.into()),
1680 Ok(0) => return Ok(()),
1683 // size includes delimiter
1684 let line
= &buffer
[0..size
- 1];
1685 let complete_line
= line
;
1687 let (time
, line
) = match parse_time(
1689 self.current_year
[self.current_file_index
],
1690 &mut self.current_month
,
1696 // relative line number within a single timestamp
1697 if time
!= prev_time
{
1698 self.rel_line_nr
= 0;
1700 self.rel_line_nr
+= 1;
1704 // skip until we're in the specified time frame
1705 if time
< self.options
.start
{
1708 // past the specified time frame, we're done, exit the loop
1709 if time
> self.options
.end
{
1715 let (host
, service
, pid
, line
) = match parse_host_service_pid(line
) {
1716 Some((h
, s
, p
, l
)) => (h
, s
, p
, l
),
1722 self.current_record_state
.host
= host
.into();
1723 self.current_record_state
.service
= service
.into();
1724 self.current_record_state
.pid
= pid
;
1725 self.current_record_state
.timestamp
= time
as u64;
1727 self.string_match
= false;
1728 if !self.options
.string_match
.is_empty()
1729 && find(complete_line
, self.options
.string_match
.as_bytes()).is_some()
1731 self.string_match
= true;
1734 // complete_line required for the logs
1735 if service
== b
"pmg-smtp-filter" {
1736 handle_pmg_smtp_filter_message(line
, self, complete_line
);
1737 } else if service
== b
"postfix/postscreen" {
1738 handle_postscreen_message(line
, self, complete_line
);
1739 } else if service
== b
"postfix/qmgr" {
1740 handle_qmgr_message(line
, self, complete_line
);
1741 } else if service
== b
"postfix/lmtp"
1742 || service
== b
"postfix/smtp"
1743 || service
== b
"postfix/local"
1744 || service
== b
"postfix/error"
1746 handle_lmtp_message(line
, self, complete_line
);
1747 } else if service
== b
"postfix/smtpd" {
1748 handle_smtpd_message(line
, self, complete_line
);
1749 } else if service
== b
"postfix/cleanup" {
1750 handle_cleanup_message(line
, self, complete_line
);
1756 /// Returns the number of files to parse. Does not error out if it can't access any file
1757 /// (permission denied)
1758 fn count_files_in_time_range(&mut self) -> usize {
1760 let mut buffer
= Vec
::new();
1762 for (i
, item
) in LOGFILES
.iter().enumerate() {
1763 self.current_month
= 0;
1766 if let Ok(file
) = File
::open(item
) {
1767 self.current_file_index
= i
;
1770 let gzdecoder
= read
::GzDecoder
::new(file
);
1771 let mut reader
= BufReader
::new(gzdecoder
);
1772 // check the first line
1773 if let Ok(size
) = reader
.read_until(b'
\n'
, &mut buffer
) {
1777 if let Some((time
, _
)) = parse_time(
1779 self.current_year
[i
],
1780 &mut self.current_month
,
1782 // found the earliest file in the time frame
1783 if time
< self.options
.start
{
1791 let mut reader
= BufReader
::new(file
);
1792 if let Ok(size
) = reader
.read_until(b'
\n'
, &mut buffer
) {
1796 if let Some((time
, _
)) = parse_time(
1798 self.current_year
[i
],
1799 &mut self.current_month
,
1801 if time
< self.options
.start
{
1817 fn handle_args(&mut self, args
: clap
::ArgMatches
) -> Result
<(), Error
> {
1818 if let Some(inputfile
) = args
.value_of("inputfile") {
1819 self.options
.inputfile
= inputfile
.to_string();
1822 if let Some(start
) = args
.value_of("start") {
1823 if let Ok(res
) = time
::strptime(&start
, "%F %T") {
1824 self.options
.start
= mkgmtime(&res
);
1825 self.start_tm
= res
;
1826 } else if let Ok(res
) = time
::strptime(&start
, "%s") {
1827 let res
= res
.to_local();
1828 self.options
.start
= mkgmtime(&res
);
1829 self.start_tm
= res
;
1831 failure
::bail
!(failure
::err_msg("failed to parse start time"));
1834 let mut ltime
= time
::now();
1838 self.options
.start
= mkgmtime(<ime
);
1839 self.start_tm
= ltime
;
1842 if let Some(end
) = args
.value_of("end") {
1843 if let Ok(res
) = time
::strptime(&end
, "%F %T") {
1844 self.options
.end
= mkgmtime(&res
);
1846 } else if let Ok(res
) = time
::strptime(&end
, "%s") {
1847 let res
= res
.to_local();
1848 self.options
.end
= mkgmtime(&res
);
1851 failure
::bail
!(failure
::err_msg("failed to parse end time"));
1854 let ltime
= time
::now();
1855 self.options
.end
= mkgmtime(<ime
);
1856 self.end_tm
= ltime
;
1859 if self.options
.end
< self.options
.start
{
1860 failure
::bail
!(failure
::err_msg("end time before start time"));
1863 self.options
.limit
= match args
.value_of("limit") {
1864 Some(l
) => l
.parse().unwrap(),
1868 if let Some(qids
) = args
.values_of("qids") {
1870 let ltime
: libc
::time_t
= 0;
1871 let rel_line_nr
: libc
::c_ulong
= 0;
1872 let input
= CString
::new(q
)?
;
1873 let bytes
= concat
!("T%08lXL%08lX", "\0");
1875 unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(bytes.as_bytes()) }
;
1877 libc
::sscanf(input
.as_ptr(), format
.as_ptr(), <ime
, &rel_line_nr
) == 2
1881 .push(Match
::RelLineNr(ltime
, rel_line_nr
));
1885 .push(Match
::Qid(q
.as_bytes().into()));
1890 if let Some(from
) = args
.value_of("from") {
1891 self.options
.from
= from
.to_string();
1893 if let Some(to
) = args
.value_of("to") {
1894 self.options
.to
= to
.to_string();
1896 if let Some(host
) = args
.value_of("host") {
1897 self.options
.host
= host
.to_string();
1899 if let Some(msgid
) = args
.value_of("msgid") {
1900 self.options
.msgid
= msgid
.to_string();
1903 self.options
.exclude_greylist
= args
.is_present("exclude_greylist");
1904 self.options
.exclude_ndr
= args
.is_present("exclude_ndr");
1906 self.options
.verbose
= args
.occurrences_of("verbose") as _
;
1908 if let Some(string_match
) = args
.value_of("search") {
1909 self.options
.string_match
= string_match
.to_string();
1915 fn write_all_ok
<T
: AsRef
<[u8]>>(&mut self, data
: T
) {
1916 self.buffered_stdout
1917 .write_all(data
.as_ref())
1918 .expect("failed to write to stdout");
1922 impl Drop
for Parser
{
1923 fn drop(&mut self) {
1924 let mut qentries
= std
::mem
::replace(&mut self.qentries
, HashMap
::new());
1925 for q
in qentries
.values() {
1926 let smtpd
= q
.borrow().smtpd
.clone();
1927 if let Some(s
) = smtpd
{
1928 q
.borrow_mut().print(self, Some(&*s
.borrow()));
1930 q
.borrow_mut().print(self, None
);
1934 let mut sentries
= std
::mem
::replace(&mut self.sentries
, HashMap
::new());
1935 for s
in sentries
.values() {
1936 s
.borrow_mut().print(self);
1942 #[derive(Debug, Default)]
1944 match_list
: Vec
<Match
>,
1946 string_match
: String
,
1951 start
: libc
::time_t
,
1955 exclude_greylist
: bool
,
1962 RelLineNr(libc
::time_t
, u64),
1965 #[derive(Debug, Default)]
1966 struct RecordState
{
1973 fn get_or_create_qentry(
1974 qentries
: &mut HashMap
<Box
<[u8]>, Rc
<RefCell
<QEntry
>>>,
1976 ) -> Rc
<RefCell
<QEntry
>> {
1977 if let Some(qe
) = qentries
.get(qid
) {
1980 let qe
= Rc
::new(RefCell
::new(QEntry
::default()));
1981 qe
.borrow_mut().qid
= qid
.into();
1982 qentries
.insert(qid
.into(), qe
.clone());
1987 fn get_or_create_sentry(
1988 sentries
: &mut HashMap
<u64, Rc
<RefCell
<SEntry
>>>,
1992 ) -> Rc
<RefCell
<SEntry
>> {
1993 if let Some(se
) = sentries
.get(&pid
) {
1996 let se
= Rc
::new(RefCell
::new(SEntry
::default()));
1997 se
.borrow_mut().rel_line_nr
= rel_line_nr
;
1998 se
.borrow_mut().timestamp
= timestamp
;
1999 sentries
.insert(pid
, se
.clone());
2004 fn get_or_create_fentry(
2005 fentries
: &mut HashMap
<Box
<[u8]>, Rc
<RefCell
<FEntry
>>>,
2007 ) -> Rc
<RefCell
<FEntry
>> {
2008 if let Some(fe
) = fentries
.get(qid
) {
2011 let fe
= Rc
::new(RefCell
::new(FEntry
::default()));
2012 fe
.borrow_mut().logid
= qid
.into();
2013 fentries
.insert(qid
.into(), fe
.clone());
2018 fn mkgmtime(tm
: &time
::Tm
) -> libc
::time_t
{
2019 let mut res
: libc
::time_t
;
2021 let mut year
= (tm
.tm_year
+ 1900) as i64;
2022 let mon
= tm
.tm_mon
;
2024 res
= (year
- 1970) * 365 + CAL_MTOD
[mon
as usize];
2030 res
+= (year
- 1968) / 4;
2031 res
-= (year
- 1900) / 100;
2032 res
+= (year
- 1600) / 400;
2034 res
+= (tm
.tm_mday
- 1) as i64;
2035 res
= res
* 24 + tm
.tm_hour
as i64;
2036 res
= res
* 60 + tm
.tm_min
as i64;
2037 res
= res
* 60 + tm
.tm_sec
as i64;
2042 const LOGFILES
: [&str; 32] = [
2044 "/var/log/syslog.1",
2045 "/var/log/syslog.2.gz",
2046 "/var/log/syslog.3.gz",
2047 "/var/log/syslog.4.gz",
2048 "/var/log/syslog.5.gz",
2049 "/var/log/syslog.6.gz",
2050 "/var/log/syslog.7.gz",
2051 "/var/log/syslog.8.gz",
2052 "/var/log/syslog.9.gz",
2053 "/var/log/syslog.10.gz",
2054 "/var/log/syslog.11.gz",
2055 "/var/log/syslog.12.gz",
2056 "/var/log/syslog.13.gz",
2057 "/var/log/syslog.14.gz",
2058 "/var/log/syslog.15.gz",
2059 "/var/log/syslog.16.gz",
2060 "/var/log/syslog.17.gz",
2061 "/var/log/syslog.18.gz",
2062 "/var/log/syslog.19.gz",
2063 "/var/log/syslog.20.gz",
2064 "/var/log/syslog.21.gz",
2065 "/var/log/syslog.22.gz",
2066 "/var/log/syslog.23.gz",
2067 "/var/log/syslog.24.gz",
2068 "/var/log/syslog.25.gz",
2069 "/var/log/syslog.26.gz",
2070 "/var/log/syslog.27.gz",
2071 "/var/log/syslog.28.gz",
2072 "/var/log/syslog.29.gz",
2073 "/var/log/syslog.30.gz",
2074 "/var/log/syslog.31.gz",
2077 /// Parse a QID ([A-Z]+). Returns a tuple of (qid, remaining_text) or None.
2078 fn parse_qid(data
: &[u8], max
: usize) -> Option
<(&[u8], &[u8])> {
2079 // to simplify limit max to data.len()
2080 let max
= max
.min(data
.len());
2081 // take at most max, find the first non-hex-digit
2082 match data
.iter().take(max
).position(|b
| !b
.is_ascii_hexdigit()) {
2083 // if there were less than 2 return nothing
2084 Some(n
) if n
< 2 => None
,
2085 // otherwise split at the first non-hex-digit
2086 Some(n
) => Some(data
.split_at(n
)),
2087 // or return 'max' length QID if no non-hex-digit is found
2088 None
=> Some(data
.split_at(max
)),
2092 /// Parse a number. Returns a tuple of (parsed_number, remaining_text) or None.
2093 fn parse_number(data
: &[u8], max_digits
: usize) -> Option
<(usize, &[u8])> {
2094 let max
= max_digits
.min(data
.len());
2096 match data
.iter().take(max
).position(|b
| !b
.is_ascii_digit()) {
2097 Some(n
) if n
== 0 => None
,
2099 let (number
, data
) = data
.split_at(n
);
2100 // number only contains ascii digits
2101 let number
= unsafe { std::str::from_utf8_unchecked(number) }
2104 Some((number
, data
))
2107 let (number
, data
) = data
.split_at(max
);
2108 // number only contains ascii digits
2109 let number
= unsafe { std::str::from_utf8_unchecked(number) }
2112 Some((number
, data
))
2117 /// Parse time. Returns a tuple of (parsed_time, remaining_text) or None.
2121 cur_month
: &mut i64,
2122 ) -> Option
<(libc
::time_t
, &'a
[u8])> {
2123 if data
.len() < 15 {
2127 let mon
= match &data
[0..3] {
2142 let data
= &data
[3..];
2144 let mut ltime
: libc
::time_t
;
2145 let mut year
= cur_year
;
2147 if *cur_month
== 11 && mon
== 0 {
2150 if mon
> *cur_month
{
2154 ltime
= (year
- 1970) * 365 + CAL_MTOD
[mon
as usize];
2160 ltime
+= (year
- 1968) / 4;
2161 ltime
-= (year
- 1900) / 100;
2162 ltime
+= (year
- 1600) / 400;
2164 let whitespace_count
= data
.iter().take_while(|b
| b
.is_ascii_whitespace()).count();
2165 let data
= &data
[whitespace_count
..];
2167 let (mday
, data
) = match parse_number(data
, 2) {
2177 ltime
+= (mday
- 1) as i64;
2179 let data
= &data
[1..];
2181 let (hour
, data
) = match parse_number(data
, 2) {
2189 ltime
+= hour
as i64;
2191 if let Some(c
) = data
.iter().next() {
2192 if (*c
as char) != '
:'
{
2198 let data
= &data
[1..];
2200 let (min
, data
) = match parse_number(data
, 2) {
2208 ltime
+= min
as i64;
2210 if let Some(c
) = data
.iter().next() {
2211 if (*c
as char) != '
:'
{
2217 let data
= &data
[1..];
2219 let (sec
, data
) = match parse_number(data
, 2) {
2227 ltime
+= sec
as i64;
2229 let data
= &data
[1..];
2234 const CAL_MTOD
: [i64; 12] = [0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334];
2236 type ByteSlice
<'a
> = &'a
[u8];
2237 /// Parse Host, Service and PID at the beginning of data. Returns a tuple of (host, service, pid, remaining_text).
2238 fn parse_host_service_pid(data
: &[u8]) -> Option
<(ByteSlice
, ByteSlice
, u64, ByteSlice
)> {
2239 let host_count
= data
2241 .take_while(|b
| !(**b
as char).is_ascii_whitespace())
2243 let host
= &data
[0..host_count
];
2244 let data
= &data
[host_count
+ 1..]; // whitespace after host
2246 let service_count
= data
2249 (**b
as char).is_ascii_alphabetic() || (**b
as char) == '
/'
|| (**b
as char) == '
-'
2252 let service
= &data
[0..service_count
];
2253 let data
= &data
[service_count
..];
2254 if data
.get(0) != Some(&b'
['
) {
2257 let data
= &data
[1..];
2259 let pid_count
= data
2261 .take_while(|b
| (**b
as char).is_ascii_digit())
2263 let pid
= match unsafe { std::str::from_utf8_unchecked(&data[0..pid_count]) }
.parse() {
2264 // all ascii digits so valid utf8
2266 Err(_
) => return None
,
2268 let data
= &data
[pid_count
..];
2269 if !data
.starts_with(b
"]: ") {
2272 let data
= &data
[3..];
2274 Some((host
, service
, pid
, data
))
2277 /// A find implementation for [u8]. Returns the index or None.
2278 fn find
<T
: PartialOrd
>(data
: &[T
], needle
: &[T
]) -> Option
<usize> {
2279 data
.windows(needle
.len()).position(|d
| d
== needle
)
2282 /// A find implementation for [u8] that converts to lowercase before the comparison. Returns the
2284 fn find_lowercase(data
: &[u8], needle
: &[u8]) -> Option
<usize> {
2285 let data
= data
.to_ascii_lowercase();
2286 let needle
= needle
.to_ascii_lowercase();
2287 data
.windows(needle
.len()).position(|d
| d
== &needle
[..])