From 8bace2d7c319306ec0dde116e24c5958d6fb54f2 Mon Sep 17 00:00:00 2001 From: Sven Hoexter Date: Fri, 23 May 2025 10:34:28 +0200 Subject: [PATCH] New upstream version 1.1.6 --- ChangeLog | 30 ++++- README | 4 +- ToDo | 4 - pffrombyto | 244 ++++++++++++++++++++++++++++++++++++++ pffrombyto.1 | 164 +++++++++++++++++++++++++ pflogsumm.pl => pflogsumm | 58 +++++---- pflogsumm-faq.txt | 28 ++--- pflogsumm.1 | 147 ++++++----------------- pftobyfrom | 232 ++++++++++++++++++++++++++++++++++++ pftobyfrom.1 | 162 +++++++++++++++++++++++++ 10 files changed, 918 insertions(+), 155 deletions(-) create mode 100755 pffrombyto create mode 100644 pffrombyto.1 rename pflogsumm.pl => pflogsumm (97%) create mode 100755 pftobyfrom create mode 100644 pftobyfrom.1 diff --git a/ChangeLog b/ChangeLog index f88603f..656ced2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,4 @@ -ChangeLog for pflogsumm.pl +ChangeLog for pflogsumm [Note: Let me know if you would like to be notified as new versions @@ -6,12 +6,40 @@ ChangeLog for pflogsumm.pl http://jimsun.LinxNet.com/postfix_contrib.html.] +rel-1.1.6 20250522 + + Renamed from "pflogsumm.pl" to "pflogsumm" + + Merged Debian patches through 1.1.5-8. Following are excerpted from + Debian changelog. + + Fix regex in milter-rejects patch. Kudos to Andreas Jaggi + for the report and patch. (Closes: + #1027829) + + Update postscreen-rejects patch with a fix for IPv6 + addresses. Kudos to Juri Haberland + for the fix. (Closes: #955627) + + Add patch to count milter rejects provided by Matus Uhlar. + + Import postscreen support patch provided by Matus Uhlar + d/patches/postscreen-rejects (Closes: #861402) + + Now matches "traditional" log date strings with either leading space + or leading zero on single-digit days. (Debian bug report 1068425) + + Improvement to "unprocessed" debugging code—used for maintenance only. + rel-1.1.5 20120205 Fixed RFC 3339 support. Releases 1.1.3 and 1.1.4 were badly broken in this respect. Thanks and a tip o' the hat to Sven Hoexter (sven-at-timegate-dot-de) for the help. + ETA (2025-05-21): From Debian changelog: Thanks to Mihai Stan + for the bugreport and the initial patch. + rel-1.1.4 20120201 Modified for compatibility with -o syslog_name=blurfl/submission diff --git a/README b/README index e0573bd..3cea93f 100644 --- a/README +++ b/README @@ -6,13 +6,13 @@ There's not much to installing pflogsumm, so it's all manual. 1. Unpack the distribution (if you're reading this, you've already gotten that far) - 2. Copy or move pflogsumm.pl to some directory from which you'll + 2. Copy or move pflogsumm to some directory from which you'll want to execute it. Maybe rename it to just "pflogsumm." Watch the ownerships and permissions. Make sure it's executable. E.g.: - cp pflogsumm.pl /usr/local/bin/pflogsumm + cp pflogsumm /usr/local/bin/pflogsumm chown bin:bin /usr/local/bin/pflogsumm chmod 755 /usr/local/bin/pflogsumm diff --git a/ToDo b/ToDo index 36eb13d..36db0b1 100644 --- a/ToDo +++ b/ToDo @@ -3,10 +3,6 @@ To Be Done (Maybe) Fix parsing for "451 4.3.5 Server configuration error;" - Rename pflogsumm.pl -> pflogsumm. See - - http://docs.freebsd.org/info/cvs/cvs.info.Moving_files.html - date ranges, "lastweek", etc.? (options for?) break-down by local vs. non-local?, further diff --git a/pffrombyto b/pffrombyto new file mode 100755 index 0000000..da1f2ba --- /dev/null +++ b/pffrombyto @@ -0,0 +1,244 @@ +#!/usr/bin/perl -w +eval 'exec perl -S $0 "$@"' + if 0; + +=head1 NAME + +pffrombyto - List "from" addresses by "to" whom in Postfix log file + +Copyright (C) 2007-2025 by James S. Seymour, Release 1.2 + +=head1 SYNOPSIS + pffrombyto -[bchrRv] [mailfile] + + If no file(s) specified, reads from stdin. Output is to stdout. + +=head1 DESCRIPTION + + pffrombyto parses Postfix log files to generate a list of "from" addresses, + based on a specified "to" address or address fragment. + +=head1 OPTIONS + + -b Include bounces + + -c Include client hostnames/addrs, as well + + -h Emit help message and exit + + -r Include rejects + + -R Hard rejects only + + -v Emit version and exit + +=head1 RETURN VALUE + + pffrombyto doesn't return anything of interest to the shell. + +=head1 ERRORS + + Error messages are emitted to stderr. + +=head1 EXAMPLES + + Generate a list of all the senders of email to the recipient + "username@example.com" + + pffrombyto username@example.com /var/log/maillog + + As a convenience, pffrombyto tries to intelligently determine how to + handle regexp meta-characters. If it's passed a search expression + that does NOT contain meta-character escapes ("\"), it will assume + that "." and "+" are literals, and will escape them for you. In the + example above, the "." in the FQDN part of the search term would've + been automatically escaped for the user. Likewise: + + pffrombyto username+foo@example.com /var/log/maillog + + would have the "+" and "." escaped. If you wanted to find all + plussed targets for "username," you'd have to do: + + pffrombyto 'username\+.+@example\.com' /var/log/maillog + +=head1 SEE ALSO + + pflogsumm, pftobyfrom + +=head1 NOTES + + All search terms and searched fields are lower-cased. + + The pffrombyto Home Page is at: + + http://jimsun.LinxNet.com/postfix_contrib.html + +=head1 REQUIREMENTS + + Perl + +=head1 LICENSE + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You may have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + USA. + + An on-line copy of the GNU General Public License can be found + http://www.fsf.org/copyleft/gpl.html. + +=cut + +use strict; +use Getopt::Std; + +(my $progName = $0) =~ s/^.*?\///o; + +my $usageMsg = "Usage: $progName -[bchrRv] [mailfile] + -b Include bounces + -c Include client hostnames/addrs, as well + -h Emit this help message and exit + -r Include rejects + -R Hard rejects only (4xx rejects ignored) + -v Emit version and exit"; + +my $revision = '1.2'; + +use vars qw($opt_b $opt_c $opt_h $opt_r $opt_R $opt_v); + +getopts('bchrRv') || die "$usageMsg\n"; +$opt_r = 1 if($opt_R); + +if($opt_h || $opt_v) { + print "$progName $revision\n" if($opt_v); + print "$usageMsg\n" if($opt_h); + exit; +} + +my ($fromQid, $fromWhom, %fromQids, %accList, %rejList, %bncList); +my ($clientQid, $clientID, %clientQids); +my ($toWhom); + +die "$usageMsg\n" unless($toWhom = shift @ARGV); + +my $doEscapes = !($toWhom =~ /\\/); + +# Escape "."s and "+"s? +$toWhom =~ s/([\.\+])/\\$1/g if($doEscapes); + +while(<>) { + if($opt_c) { + if(($clientQid, $clientID) = /:(?: \[ID \d+ mail\.info\])? ([A-F0-9]+): client=(.+)$/o) { +# print "dbg: $clientQid, $clientID\n"; + $clientQids{$clientQid} = $clientID; + next; + } + } + + if(($fromQid, $fromWhom) = /:(?: \[ID \d+ mail\.info\])? ([A-F0-9]+): from=<([^>]+)>/o) { +# print "dbg: $fromQid, $fromWhom\n"; + $fromQids{$fromQid} = $fromWhom; + if($opt_c && $clientQids{$fromQid}) { + $fromQids{$fromQid} .= " $clientQids{$fromQid}"; + } + }elsif($opt_r && (my ($respCode, $fromWhom, $toWhomFull) = /: NOQUEUE: reject: RCPT from \S+: (\d+) .+from=<([^>]+)> to=<(.*$toWhom[^>]*)>/oi)) { + ++$rejList{lc $toWhomFull}{"$fromWhom"} unless($opt_R && $respCode == 450); + + }elsif((my $toQid, $toWhomFull) = /:(?: \[ID \d+ mail\.info\])? ([A-F0-9]+): to=<([^>]*$toWhom[^>]*)>, .+ status=sent/oi) { + if($fromQids{$toQid} && ! $opt_R) { +# print "dbg: $fromQids{$toQid} $toWhomFull\n"; + ++$accList{lc $toWhomFull}{$fromQids{$toQid}}; + } + }elsif($opt_b && (($toQid, $toWhomFull) = /:(?: \[ID \d+ mail\.info\])? ([A-F0-9]+): to=<(.*$toWhom[^>]*)>, .+ status=bounced/oi)) { + if($fromQids{$toQid}) { +# print "dbg: $fromQids{$toQid} $toWhomFull\n"; + ++$bncList{lc $toWhomFull}{$fromQids{$toQid}}; + } + } +} + +if(%accList) { + print "\nDelivered:\n"; + walk_nested_hash(\%accList, 0); +} +if($opt_r && %rejList) { + print "\nRejected:\n"; + walk_nested_hash(\%rejList, 0); +} +if($opt_b && %bncList) { + print "\nBounced:\n"; + walk_nested_hash(\%bncList, 0); +} +print "\n"; + + +# "walk" a "nested" hash +sub walk_nested_hash { + my ($hashRef, $level) = @_; + $level += 2; + my $indents = ' ' x $level; + my ($keyName, $hashVal) = each(%$hashRef); + + if(ref($hashVal) eq 'HASH') { + foreach (sort keys %$hashRef) { + print "$indents$_"; + # If the next hash is finally the data, total the + # counts for the report and print + my $hashVal2 = (each(%{$hashRef->{$_}}))[1]; + keys(%{$hashRef->{$_}}); # "reset" hash iterator + unless(ref($hashVal2) eq 'HASH') { + my $cnt = 0; + $cnt += $_ foreach (values %{$hashRef->{$_}}); + print " (total: $cnt)"; + } + print "\n"; + walk_nested_hash($hashRef->{$_}, $level); + } + } else { + really_print_hash_by_cnt_vals($hashRef, 0, $indents); + } +} + + +# *really* print hash contents sorted by numeric values in descending +# order (i.e.: highest first), then by IP/addr, in ascending order. +sub really_print_hash_by_cnt_vals { + my($hashRef, $cnt, $indents) = @_; + + foreach (map { $_->[0] } + sort { $b->[1] <=> $a->[1] || $a->[2] cmp $b->[2] } + map { [ $_, $hashRef->{$_}, normalize_host($_) ] } + (keys(%$hashRef))) + { + printf "$indents%6d %s\n", $hashRef->{$_}, $_; + last if --$cnt == 0; + } +} + +# Normalize IP addr or hostname +# (Note: Makes no effort to normalize IPv6 addrs. Just returns them +# as they're passed-in.) +sub normalize_host { + # For IP addrs and hostnames: lop off possible " (user@dom.ain)" bit + my $norm1 = (split(/\s/, $_[0]))[0]; + + if((my @octets = ($norm1 =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/o)) == 4) { + # Dotted-quad IP address + return(pack('C4', @octets)); + } else { + # Possibly hostname or user@dom.ain + #return(join( '', map { lc $_ } reverse split /[.@]/, $norm1 )); + return lc $_[0]; + } +} + diff --git a/pffrombyto.1 b/pffrombyto.1 new file mode 100644 index 0000000..3a0e77c --- /dev/null +++ b/pffrombyto.1 @@ -0,0 +1,164 @@ +.\" -*- mode: troff; coding: utf-8 -*- +.\" Automatically generated by Pod::Man 5.01 (Pod::Simple 3.43) +.\" +.\" Standard preamble: +.\" ======================================================================== +.de Sp \" Vertical space (when we can't use .PP) +.if t .sp .5v +.if n .sp +.. +.de Vb \" Begin verbatim text +.ft CW +.nf +.ne \\$1 +.. +.de Ve \" End verbatim text +.ft R +.fi +.. +.\" \*(C` and \*(C' are quotes in nroff, nothing in troff, for use with C<>. +.ie n \{\ +. ds C` "" +. ds C' "" +'br\} +.el\{\ +. ds C` +. ds C' +'br\} +.\" +.\" Escape single quotes in literal strings from groff's Unicode transform. +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" +.\" If the F register is >0, we'll generate index entries on stderr for +.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index +.\" entries marked with X<> in POD. Of course, you'll have to process the +.\" output yourself in some meaningful fashion. +.\" +.\" Avoid warning from groff about undefined register 'F'. +.de IX +.. +.nr rF 0 +.if \n(.g .if rF .nr rF 1 +.if (\n(rF:(\n(.g==0)) \{\ +. if \nF \{\ +. de IX +. tm Index:\\$1\t\\n%\t"\\$2" +.. +. if !\nF==2 \{\ +. nr % 0 +. nr F 2 +. \} +. \} +.\} +.rr rF +.\" ======================================================================== +.\" +.IX Title "PFFROMBYTO 1" +.TH PFFROMBYTO 1 2025-05-22 1.1.6 "User Contributed Perl Documentation" +.\" For nroff, turn off justification. Always turn off hyphenation; it makes +.\" way too many mistakes in technical documents. +.if n .ad l +.nh +.SH NAME +pffrombyto \- List "from" addresses by "to" whom in Postfix log file +.PP +Copyright (C) 2007\-2025 by James S. Seymour, Release 1.2 +.SH "SYNOPSIS pffrombyto \-[bchrRv] [mailfile]" +.IX Header "SYNOPSIS pffrombyto -[bchrRv] [mailfile]" +.Vb 1 +\& If no file(s) specified, reads from stdin. Output is to stdout. +.Ve +.SH DESCRIPTION +.IX Header "DESCRIPTION" +.Vb 2 +\& pffrombyto parses Postfix log files to generate a list of "from" addresses, +\& based on a specified "to" address or address fragment. +.Ve +.SH OPTIONS +.IX Header "OPTIONS" +.Vb 1 +\& \-b Include bounces +\& +\& \-c Include client hostnames/addrs, as well +\& +\& \-h Emit help message and exit +\& +\& \-r Include rejects +\& +\& \-R Hard rejects only +\& +\& \-v Emit version and exit +.Ve +.SH "RETURN VALUE" +.IX Header "RETURN VALUE" +.Vb 1 +\& pffrombyto doesn\*(Aqt return anything of interest to the shell. +.Ve +.SH ERRORS +.IX Header "ERRORS" +.Vb 1 +\& Error messages are emitted to stderr. +.Ve +.SH EXAMPLES +.IX Header "EXAMPLES" +.Vb 2 +\& Generate a list of all the senders of email to the recipient +\& "username@example.com" +\& +\& pffrombyto username@example.com /var/log/maillog +\& +\& As a convenience, pffrombyto tries to intelligently determine how to +\& handle regexp meta\-characters. If it\*(Aqs passed a search expression +\& that does NOT contain meta\-character escapes ("\e"), it will assume +\& that "." and "+" are literals, and will escape them for you. In the +\& example above, the "." in the FQDN part of the search term would\*(Aqve +\& been automatically escaped for the user. Likewise: +\& +\& pffrombyto username+foo@example.com /var/log/maillog +\& +\& would have the "+" and "." escaped. If you wanted to find all +\& plussed targets for "username," you\*(Aqd have to do: +\& +\& pffrombyto \*(Aqusername\e+.+@example\e.com\*(Aq /var/log/maillog +.Ve +.SH "SEE ALSO" +.IX Header "SEE ALSO" +.Vb 1 +\& pflogsumm, pftobyfrom +.Ve +.SH NOTES +.IX Header "NOTES" +.Vb 1 +\& All search terms and searched fields are lower\-cased. +\& +\& The pffrombyto Home Page is at: +\& +\& http://jimsun.LinxNet.com/postfix_contrib.html +.Ve +.SH REQUIREMENTS +.IX Header "REQUIREMENTS" +.Vb 1 +\& Perl +.Ve +.SH LICENSE +.IX Header "LICENSE" +.Vb 4 +\& This program is free software; you can redistribute it and/or +\& modify it under the terms of the GNU General Public License +\& as published by the Free Software Foundation; either version 2 +\& of the License, or (at your option) any later version. +\& +\& This program is distributed in the hope that it will be useful, +\& but WITHOUT ANY WARRANTY; without even the implied warranty of +\& MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +\& GNU General Public License for more details. +\& +\& You may have received a copy of the GNU General Public License +\& along with this program; if not, write to the Free Software +\& Foundation, Inc., 59 Temple Place \- Suite 330, Boston, MA 02111\-1307, +\& USA. +\& +\& An on\-line copy of the GNU General Public License can be found +\& http://www.fsf.org/copyleft/gpl.html. +.Ve diff --git a/pflogsumm.pl b/pflogsumm similarity index 97% rename from pflogsumm.pl rename to pflogsumm index 31de5bd..cb3b8a9 100755 --- a/pflogsumm.pl +++ b/pflogsumm @@ -4,13 +4,13 @@ eval 'exec perl -S $0 "$@"' =head1 NAME -pflogsumm.pl - Produce Postfix MTA logfile summary +pflogsumm - Produce Postfix MTA logfile summary -Copyright (C) 1998-2010 by James S. Seymour, Release 1.1.5 +Copyright (C) 1998-2025 by James S. Seymour, Release 1.1.6 =head1 SYNOPSIS - pflogsumm.pl -[eq] [-d ] [--detail ] + pflogsumm -[eq] [-d ] [--detail ] [--bounce-detail ] [--deferral-detail ] [-h ] [-i|--ignore-case] [--iso-date-time] [--mailq] [-m|--uucp-mung] [--no-no-msg-size] [--problems-first] @@ -19,7 +19,7 @@ Copyright (C) 1998-2010 by James S. Seymour, Release 1.1.5 [--syslog-name=string] [-u ] [--verbose-msg-detail] [--verp-mung[=]] [--zero-fill] [file1 [filen]] - pflogsumm.pl -[help|version] + pflogsumm -[help|version] If no file(s) specified, reads from stdin. Output is to stdout. @@ -140,7 +140,7 @@ Copyright (C) 1998-2010 by James S. Seymour, Release 1.1.5 The message may be delivered long-enough after the (last) qmgr log entry that the information is not in the log(s) processed by a particular run of - pflogsumm.pl. This throws off "Recipients by message + pflogsumm. This throws off "Recipients by message size" and the total for "bytes delivered." These are normally reported by pflogsumm as "Messages with no size data." @@ -255,15 +255,15 @@ Copyright (C) 1998-2010 by James S. Seymour, Release 1.1.5 Produce a report of previous day's activities: - pflogsumm.pl -d yesterday /var/log/maillog + pflogsumm -d yesterday /var/log/maillog A report of prior week's activities (after logs rotated): - pflogsumm.pl /var/log/maillog.0 + pflogsumm /var/log/maillog.0 What's happened so far today: - pflogsumm.pl -d today /var/log/maillog + pflogsumm -d today /var/log/maillog Crontab entry to generate a report of the previous day's activity at 10 minutes after midnight. @@ -402,7 +402,7 @@ eval { require Date::Calc }; my $hasDateCalc = $@ ? 0 : 1; my $mailqCmd = "mailq"; -my $release = "1.1.5"; +my $release = "1.1.6"; # Variables and constants used throughout pflogsumm use vars qw( @@ -617,8 +617,10 @@ End_Of_HELP_DATE_CALC ($dateStr, $dateStrRFC3339) = get_datestrs($opts{'d'}) if(defined($opts{'d'})); # debugging -#open(UNPROCD, "> unprocessed") || -# die "couldn't open \"unprocessed\": $!\n"; +my $unProcdFN = 'unprocessed'; +my $unProcd; +#open($unProcd, "> $unProcdFN") || +# die "couldn't open \"$unProcdFN\": $!\n"; while(<>) { next if(defined($dateStr) && ! (/^${dateStr} / || /^${dateStrRFC3339}T/)); @@ -642,7 +644,7 @@ while(<>) { unless((($cmd, $qid) = $logRmdr =~ m#^(?:postfix|$syslogName)(?:/(?:smtps|submission))?/([^\[:]*).*?: ([^:\s]+)#o) == 2 || (($cmd, $qid) = $logRmdr =~ m#^((?:postfix)(?:-script)?)(?:\[\d+\])?: ([^:\s]+)#o) == 2) { - #print UNPROCD "$_"; + print $unProcd "$_" if $unProcd; next; } chomp; @@ -666,11 +668,11 @@ while(<>) { # regexp rejects happen in "cleanup" if($cmd eq "cleanup" && (my($rejSubTyp, $rejReas, $rejRmdr) = $logRmdr =~ - /\/cleanup\[\d+\]: .*?\b(reject|warning|hold|discard): (header|body) (.*)$/) == 3) + /\/cleanup\[\d+\]: .*?\b((?:milter-)?reject|warning|hold|discard): (header|body|END-OF-MESSAGE) (.*)$/) == 3) { $rejRmdr =~ s/( from \S+?)?; from=<.*$// unless($opts{'verbMsgDetail'}); $rejRmdr = string_trimmer($rejRmdr, 64, $opts{'verbMsgDetail'}); - if($rejSubTyp eq "reject") { + if($rejSubTyp eq "reject" or $rejSubTyp eq "milter-reject") { ++$rejects{$cmd}{$rejReas}{$rejRmdr} unless($opts{'rejectDetail'} == 0); ++$msgsRjctd; } elsif($rejSubTyp eq "warning") { @@ -713,7 +715,7 @@ while(<>) { \${$msgsPerDay{$revMsgDateStr}}[4]); } elsif($cmd eq 'master') { ++$masterMsgs{(split(/^.*master.*: /, $logRmdr))[1]}; - } elsif($cmd eq 'smtpd') { + } elsif($cmd eq 'smtpd' || $cmd eq 'postscreen') { if($logRmdr =~ /\[\d+\]: \w+: client=(.+?)(,|$)/) { # # Warning: this code in two places! @@ -898,7 +900,7 @@ while(<>) { ++${$msgsPerDay{$revMsgDateStr}}[3]; ++$msgsBncd; } else { -# print UNPROCD "$_\n"; + print $unProcd "$_\n" if $unProcd; } } elsif($cmd eq 'pickup' && $logRmdr =~ /: (sender|uid)=/) { @@ -917,19 +919,21 @@ while(<>) { } elsif($logRmdr =~ /.* connect to ([^[]+)\[\S+?\]: (.+?) \(port \d+\)$/) { ++$smtpMsgs{lc($2)}{$1}; } else { -# print UNPROCD "$_\n"; + print $unProcd "$_\n" if $unProcd; } } else { -# print UNPROCD "$_\n"; + print $unProcd "$_\n" if $unProcd; } } } # debugging -#close(UNPROCD) || -# die "problem closing \"unprocessed\": $!\n"; +if($unProcd) { + close($unProcd) || + warn "problem closing \"$unProcdFN\": $!\n"; +} # Calculate percentage of messages rejected and discarded my $msgsRjctdPct = 0; @@ -1518,7 +1522,8 @@ sub get_datestrs { } my ($t_mday, $t_mon, $t_year) = (localtime($time))[3,4,5]; - return sprintf("%s %2d", $monthNames[$t_mon], $t_mday), sprintf("%04d-%02d-%02d", $t_year+1900, $t_mon+1, $t_mday); + return @{[map {s/ (\d)$/\[ 0\]$1/; $_} sprintf("%s %2d", $monthNames[$t_mon], $t_mday)]}, + sprintf("%04d-%02d-%02d", $t_year+1900, $t_mon+1, $t_mday); } # if there's a real domain: uses that. Otherwise uses the IP addr. @@ -1536,14 +1541,14 @@ sub gimme_domain { # split domain/ipaddr into separates # newer versions of Postfix have them "dom.ain[i.p.add.ress]" # older versions of Postfix have them "dom.ain/i.p.add.ress" - unless((($domain, $ipAddr) = /^([^\[]+)\[((?:\d{1,3}\.){3}\d{1,3})\]/) == 2 || - (($domain, $ipAddr) = /^([^\/]+)\/([0-9a-f.:]+)/i) == 2) { + unless((($domain, $ipAddr) = /^([^\[]*)\[((?:\d{1,3}\.){3}\d{1,3})\]/) == 2|| + (($domain, $ipAddr) = /^([^\/]*)\/([0-9a-f.:]+)/i) == 2) { # more exhaustive method - ($domain, $ipAddr) = /^([^\[\(\/]+)[\[\(\/]([^\]\)]+)[\]\)]?:?\s*$/; + ($domain, $ipAddr) = /^([^\[\(\/]*)[\[\(\/]([^\]\)]+)[\]\)]:?\d*$/; } # "mach.host.dom"/"mach.host.do.co" to "host.dom"/"host.do.co" - if($domain eq 'unknown') { + if($domain eq "" || $domain eq 'unknown') { $domain = $ipAddr; # For identifying the host part on a Class C network (commonly # seen with dial-ups) the following is handy. @@ -1656,9 +1661,10 @@ sub proc_smtpd_reject { # those--incl. stuff that'll screw up subsequent parsing. So just # get rid of it right off. $rejReas =~ s/^(\d{3} <).*?(>:)/$1$2/; + $rejReas =~ s/^(?:\d{3} \d\.\d\.\d )(Protocol error);.*$/$1/; $rejReas =~ s/^(?:.*?[:;] )(?:\[[^\]]+\] )?([^;,]+)[;,].*$/$1/; $rejReas =~ s/^((?:Sender|Recipient) address rejected: [^:]+):.*$/$1/; - $rejReas =~ s/(Client host|Sender address) .+? blocked/blocked/; + $rejReas =~ s/(client|Client host|Sender address) .+? blocked/blocked/; } elsif($rejTyp eq "MAIL") { # *more* special treatment :-( grrrr... $rejReas =~ s/^\d{3} (?:<.+>: )?([^;:]+)[;:]?.*$/$1/; } else { diff --git a/pflogsumm-faq.txt b/pflogsumm-faq.txt index 0e3e4a9..094efcd 100644 --- a/pflogsumm-faq.txt +++ b/pflogsumm-faq.txt @@ -1,5 +1,5 @@ -FAQ for Pflogsumm.pl - A Log Summarizer/Analyzer for the Postfix MTA +FAQ for Pflogsumm - A Log Summarizer/Analyzer for the Postfix MTA Introduction @@ -7,13 +7,13 @@ Introduction hearted exercise in improving my facility with Perl--with the hope that something useful would come out of it as well--has turned out to be a somewhat popular utility. And as more Admins find out about - postfix, and more end up trying pflogsumm.pl, many of the questions, + postfix, and more end up trying pflogsumm, many of the questions, suggestions, and enhancement requests are becoming "frequently asked". So odd as it seems (to me, at any rate), it looks like it's time for a FAQ. -Index of pflogsumm.pl Frequently Asked Questions (in no particular order) +Index of pflogsumm Frequently Asked Questions (in no particular order) 1. Project Status 2. "Could You Make" or "Here's A Patch To Make" Pflogsumm Do ... @@ -137,11 +137,11 @@ Index of pflogsumm.pl Frequently Asked Questions (in no particular order) "creeping over-feature-itis" if I can. My position is *not* set in stone on this issue. In the mean-time: - zcat /var/log/maillog.0.gz |pflogsumm.pl + zcat /var/log/maillog.0.gz |pflogsumm or - gunzip + gunzip should do the trick quite nicely for you. @@ -149,7 +149,7 @@ Index of pflogsumm.pl Frequently Asked Questions (in no particular order) exactly at midnight, you might try something like: (zcat /var/log/maillog.0.gz; cat /var/log/maillog) \ - |pflogsumm.pl -d yesterday + |pflogsumm -d yesterday See Also: 5. Processing Multiple Log Files 17. How Do I Get Pflogsumm To Email Reports To Me @@ -175,7 +175,7 @@ Index of pflogsumm.pl Frequently Asked Questions (in no particular order) A more complex example, where compressed logs are involved: (zcat `ls -rt /var/log/maillog.*.gz`; cat /var/log/maillog) \ - |pflogsumm.pl + |pflogsumm Obviously, this depends on the file modification times for your logs being reflective of their chronological order. If that can't be @@ -185,12 +185,12 @@ Index of pflogsumm.pl Frequently Asked Questions (in no particular order) (for each in 3 2 1 0; do zcat "/var/log/maillog.$each.gz" done - cat /var/log/maillog) |pflogsumm.pl + cat /var/log/maillog) |pflogsumm or (somewhat more efficiently--by running zcat only once): (zcat `for ea in 3 2 1 0; do echo "/var/log/maillog.$ea.gz"; - done`; cat /var/log/maillog) |pflogsumm.pl + done`; cat /var/log/maillog) |pflogsumm [Note: I didn't actually run these. So you would be well-advised to double-check them.] @@ -305,7 +305,7 @@ Index of pflogsumm.pl Frequently Asked Questions (in no particular order) Message size is reported only by the queue manager. The message may be delivered long-enough after the (last) qmgr log entry that the information is not in the log(s) processed by a particular run - of pflogsumm.pl. + of pflogsumm. The Result: @@ -337,7 +337,7 @@ Index of pflogsumm.pl Frequently Asked Questions (in no particular order) 10. Pflogsumm misses/mis-diagnoses/mis-reports, etc. - Are you using a real old version of VMailer? As of pflogsumm.pl + Are you using a real old version of VMailer? As of pflogsumm version 19990220-06, versions of VMailer prior to 19981023 are no longer supported. Sorry. Pflogsumm-19990121-01.pl will be made permanently available from now on for those with out-of-date versions @@ -346,7 +346,7 @@ Index of pflogsumm.pl Frequently Asked Questions (in no particular order) Are you processing your log files in chronological order? See item "5: "Processing Multiple Log Files". - Pflogsumm.pl is being developed by me on my rather small-scale server + Pflogsumm is being developed by me on my rather small-scale server at home. There are only two users on the system. And I do no mail-forwarding. So the log samples I have to work with are commensurately limited. @@ -366,13 +366,13 @@ Index of pflogsumm.pl Frequently Asked Questions (in no particular order) 11. Pflogsumm is generating lots of "uninitialized value" warnings Are you using a version of Perl lower than 5.004_04? Perhaps with a - "beta" version of pflogsumm.pl? If so, try turning off the "-w" + "beta" version of pflogsumm? If so, try turning off the "-w" switch. Pflogsumm as of 19990413-02beta appeared to work correctly with Perl 5.003 in spite of the warnings. (Those warnings didn't appear with Perl 5.004.) I don't guarantee that I'll remember to test future versions of - pflogsumm.pl against 5.003, but I'll try to :-). + pflogsumm against 5.003, but I'll try to :-). You really should consider upgrading your Perl to 5.004 or later. diff --git a/pflogsumm.1 b/pflogsumm.1 index dbad81d..c22cf4b 100644 --- a/pflogsumm.1 +++ b/pflogsumm.1 @@ -1,4 +1,5 @@ -.\" Automatically generated by Pod::Man 2.1801 (Pod::Simple 3.13) +.\" -*- mode: troff; coding: utf-8 -*- +.\" Automatically generated by Pod::Man 5.01 (Pod::Simple 3.43) .\" .\" Standard preamble: .\" ======================================================================== @@ -15,128 +16,58 @@ .ft R .fi .. -.\" Set up some character translations and predefined strings. \*(-- will -.\" give an unbreakable dash, \*(PI will give pi, \*(L" will give a left -.\" double quote, and \*(R" will give a right double quote. \*(C+ will -.\" give a nicer C++. Capital omega is used to do unbreakable dashes and -.\" therefore won't be available. \*(C` and \*(C' expand to `' in nroff, -.\" nothing in troff, for use with C<>. -.tr \(*W- -.ds C+ C\v'-.1v'\h'-1p'\s-2+\h'-1p'+\s0\v'.1v'\h'-1p' +.\" \*(C` and \*(C' are quotes in nroff, nothing in troff, for use with C<>. .ie n \{\ -. ds -- \(*W- -. ds PI pi -. if (\n(.H=4u)&(1m=24u) .ds -- \(*W\h'-12u'\(*W\h'-12u'-\" diablo 10 pitch -. if (\n(.H=4u)&(1m=20u) .ds -- \(*W\h'-12u'\(*W\h'-8u'-\" diablo 12 pitch -. ds L" "" -. ds R" "" . ds C` "" . ds C' "" 'br\} .el\{\ -. ds -- \|\(em\| -. ds PI \(*p -. ds L" `` -. ds R" '' +. ds C` +. ds C' 'br\} .\" .\" Escape single quotes in literal strings from groff's Unicode transform. .ie \n(.g .ds Aq \(aq .el .ds Aq ' .\" -.\" If the F register is turned on, we'll generate index entries on stderr for +.\" If the F register is >0, we'll generate index entries on stderr for .\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index .\" entries marked with X<> in POD. Of course, you'll have to process the .\" output yourself in some meaningful fashion. -.ie \nF \{\ -. de IX -. tm Index:\\$1\t\\n%\t"\\$2" +.\" +.\" Avoid warning from groff about undefined register 'F'. +.de IX .. -. nr % 0 -. rr F -.\} -.el \{\ -. de IX +.nr rF 0 +.if \n(.g .if rF .nr rF 1 +.if (\n(rF:(\n(.g==0)) \{\ +. if \nF \{\ +. de IX +. tm Index:\\$1\t\\n%\t"\\$2" .. +. if !\nF==2 \{\ +. nr % 0 +. nr F 2 +. \} +. \} .\} -.\" -.\" Accent mark definitions (@(#)ms.acc 1.5 88/02/08 SMI; from UCB 4.2). -.\" Fear. Run. Save yourself. No user-serviceable parts. -. \" fudge factors for nroff and troff -.if n \{\ -. ds #H 0 -. ds #V .8m -. ds #F .3m -. ds #[ \f1 -. ds #] \fP -.\} -.if t \{\ -. ds #H ((1u-(\\\\n(.fu%2u))*.13m) -. ds #V .6m -. ds #F 0 -. ds #[ \& -. ds #] \& -.\} -. \" simple accents for nroff and troff -.if n \{\ -. ds ' \& -. ds ` \& -. ds ^ \& -. ds , \& -. ds ~ ~ -. ds / -.\} -.if t \{\ -. ds ' \\k:\h'-(\\n(.wu*8/10-\*(#H)'\'\h"|\\n:u" -. ds ` \\k:\h'-(\\n(.wu*8/10-\*(#H)'\`\h'|\\n:u' -. ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'^\h'|\\n:u' -. ds , \\k:\h'-(\\n(.wu*8/10)',\h'|\\n:u' -. ds ~ \\k:\h'-(\\n(.wu-\*(#H-.1m)'~\h'|\\n:u' -. ds / \\k:\h'-(\\n(.wu*8/10-\*(#H)'\z\(sl\h'|\\n:u' -.\} -. \" troff and (daisy-wheel) nroff accents -.ds : \\k:\h'-(\\n(.wu*8/10-\*(#H+.1m+\*(#F)'\v'-\*(#V'\z.\h'.2m+\*(#F'.\h'|\\n:u'\v'\*(#V' -.ds 8 \h'\*(#H'\(*b\h'-\*(#H' -.ds o \\k:\h'-(\\n(.wu+\w'\(de'u-\*(#H)/2u'\v'-.3n'\*(#[\z\(de\v'.3n'\h'|\\n:u'\*(#] -.ds d- \h'\*(#H'\(pd\h'-\w'~'u'\v'-.25m'\f2\(hy\fP\v'.25m'\h'-\*(#H' -.ds D- D\\k:\h'-\w'D'u'\v'-.11m'\z\(hy\v'.11m'\h'|\\n:u' -.ds th \*(#[\v'.3m'\s+1I\s-1\v'-.3m'\h'-(\w'I'u*2/3)'\s-1o\s+1\*(#] -.ds Th \*(#[\s+2I\s-2\h'-\w'I'u*3/5'\v'-.3m'o\v'.3m'\*(#] -.ds ae a\h'-(\w'a'u*4/10)'e -.ds Ae A\h'-(\w'A'u*4/10)'E -. \" corrections for vroff -.if v .ds ~ \\k:\h'-(\\n(.wu*9/10-\*(#H)'\s-2\u~\d\s+2\h'|\\n:u' -.if v .ds ^ \\k:\h'-(\\n(.wu*10/11-\*(#H)'\v'-.4m'^\v'.4m'\h'|\\n:u' -. \" for low resolution devices (crt and lpr) -.if \n(.H>23 .if \n(.V>19 \ -\{\ -. ds : e -. ds 8 ss -. ds o a -. ds d- d\h'-1'\(ga -. ds D- D\h'-1'\(hy -. ds th \o'bp' -. ds Th \o'LP' -. ds ae ae -. ds Ae AE -.\} -.rm #[ #] #H #V #F C +.rr rF .\" ======================================================================== .\" .IX Title "PFLOGSUMM 1" -.TH PFLOGSUMM 1 "2012-02-05" "1.1.5" "User Contributed Perl Documentation" +.TH PFLOGSUMM 1 2025-05-22 1.1.6 "User Contributed Perl Documentation" .\" For nroff, turn off justification. Always turn off hyphenation; it makes .\" way too many mistakes in technical documents. .if n .ad l .nh -.SH "NAME" -pflogsumm.pl \- Produce Postfix MTA logfile summary +.SH NAME +pflogsumm \- Produce Postfix MTA logfile summary .PP -Copyright (C) 1998\-2010 by James S. Seymour, Release 1.1.5 -.SH "SYNOPSIS" +Copyright (C) 1998\-2025 by James S. Seymour, Release 1.1.6 +.SH SYNOPSIS .IX Header "SYNOPSIS" .Vb 8 -\& pflogsumm.pl \-[eq] [\-d ] [\-\-detail ] +\& pflogsumm \-[eq] [\-d ] [\-\-detail ] \& [\-\-bounce\-detail ] [\-\-deferral\-detail ] \& [\-h ] [\-i|\-\-ignore\-case] [\-\-iso\-date\-time] [\-\-mailq] \& [\-m|\-\-uucp\-mung] [\-\-no\-no\-msg\-size] [\-\-problems\-first] @@ -145,11 +76,11 @@ Copyright (C) 1998\-2010 by James S. Seymour, Release 1.1.5 \& [\-\-syslog\-name=string] [\-u ] [\-\-verbose\-msg\-detail] \& [\-\-verp\-mung[=]] [\-\-zero\-fill] [file1 [filen]] \& -\& pflogsumm.pl \-[help|version] +\& pflogsumm \-[help|version] \& \& If no file(s) specified, reads from stdin. Output is to stdout. .Ve -.SH "DESCRIPTION" +.SH DESCRIPTION .IX Header "DESCRIPTION" .Vb 4 \& Pflogsumm is a log analyzer/summarizer for the Postfix MTA. It is @@ -161,7 +92,7 @@ Copyright (C) 1998\-2010 by James S. Seymour, Release 1.1.5 \& mail server traffic volumes, rejected and bounced email, and server \& warnings, errors and panics. .Ve -.SH "OPTIONS" +.SH OPTIONS .IX Header "OPTIONS" .Vb 1 \& \-\-bounce\-detail @@ -268,7 +199,7 @@ Copyright (C) 1998\-2010 by James S. Seymour, Release 1.1.5 \& The message may be delivered long\-enough after the \& (last) qmgr log entry that the information is not in \& the log(s) processed by a particular run of -\& pflogsumm.pl. This throws off "Recipients by message +\& pflogsumm. This throws off "Recipients by message \& size" and the total for "bytes delivered." These are \& normally reported by pflogsumm as "Messages with no \& size data." @@ -376,25 +307,25 @@ Copyright (C) 1998\-2010 by James S. Seymour, Release 1.1.5 .Vb 1 \& Pflogsumm doesn\*(Aqt return anything of interest to the shell. .Ve -.SH "ERRORS" +.SH ERRORS .IX Header "ERRORS" .Vb 1 \& Error messages are emitted to stderr. .Ve -.SH "EXAMPLES" +.SH EXAMPLES .IX Header "EXAMPLES" .Vb 1 \& Produce a report of previous day\*(Aqs activities: \& -\& pflogsumm.pl \-d yesterday /var/log/maillog +\& pflogsumm \-d yesterday /var/log/maillog \& \& A report of prior week\*(Aqs activities (after logs rotated): \& -\& pflogsumm.pl /var/log/maillog.0 +\& pflogsumm /var/log/maillog.0 \& \& What\*(Aqs happened so far today: \& -\& pflogsumm.pl \-d today /var/log/maillog +\& pflogsumm \-d today /var/log/maillog \& \& Crontab entry to generate a report of the previous day\*(Aqs activity \& at 10 minutes after midnight. @@ -418,7 +349,7 @@ Copyright (C) 1998\-2010 by James S. Seymour, Release 1.1.5 .Vb 1 \& The pflogsumm FAQ: pflogsumm\-faq.txt. .Ve -.SH "NOTES" +.SH NOTES .IX Header "NOTES" .Vb 3 \& Pflogsumm makes no attempt to catch/parse non\-Postfix log @@ -496,7 +427,7 @@ Copyright (C) 1998\-2010 by James S. Seymour, Release 1.1.5 \& \& http://jimsun.LinxNet.com/postfix_contrib.html .Ve -.SH "REQUIREMENTS" +.SH REQUIREMENTS .IX Header "REQUIREMENTS" .Vb 3 \& For certain options (e.g.: \-\-smtpd\-stats), Pflogsumm requires the @@ -507,7 +438,7 @@ Copyright (C) 1998\-2010 by James S. Seymour, Release 1.1.5 \& As of version 19990413\-02, pflogsumm worked with Perl 5.003, but \& future compatibility is not guaranteed. .Ve -.SH "LICENSE" +.SH LICENSE .IX Header "LICENSE" .Vb 4 \& This program is free software; you can redistribute it and/or diff --git a/pftobyfrom b/pftobyfrom new file mode 100755 index 0000000..7c05f3c --- /dev/null +++ b/pftobyfrom @@ -0,0 +1,232 @@ +#!/usr/bin/perl -w +eval 'exec perl -S $0 "$@"' + if 0; + +=head1 NAME + +pftobyfrom - List "to" addresses by "from" whom in Postfix log file + +Copyright (C) 2007-2025 by James S. Seymour, Release 1.3 + +=head1 SYNOPSIS + pftobyfrom -[bhrRv] [mailfile] + + If no file(s) specified, reads from stdin. Output is to stdout. + +=head1 DESCRIPTION + + pftobyfrom parses Postfix log files to generate a list of "to" addresses, + based on a specified "from" address or address fragment. + +=head1 OPTIONS + + -b Include bounces + + -h Emit help message and exit + + -r Include rejects + + -R Hard rejects only + + -v Emit version and exit + +=head1 RETURN VALUE + + pftobyfrom doesn't return anything of interest to the shell. + +=head1 ERRORS + + Error messages are emitted to stderr. + +=head1 EXAMPLES + + pftobyfrom example.com /var/log/maillog + + Generates a list of all the recipients of email from any senders + in "example.com" + + As a convenience, pftobyfrom tries to intelligently determine how to + handle regexp meta-characters. If it's passed a search expression + that does NOT contain meta-character escapes ("\"), it will assume + that "." and "+" are literals, and will escape them for you. In the + example above, the "." in the FQDN part of the search term would've + been automatically escaped for the user. Likewise: + + pftobyfrom username+foo@example.com /var/log/maillog + + would have the "+" and "." escaped. If you wanted to find all + plussed senders for "username," you'd have to do: + + pftobyfrom 'username\+.+@example\.com' /var/log/maillog + +=head1 SEE ALSO + + pflogsumm, pffrombyto + +=head1 NOTES + + All search terms and searched fields are lower-cased. + + The pftobyfrom Home Page is at: + + http://jimsun.LinxNet.com/postfix_contrib.html + +=head1 REQUIREMENTS + + Perl + +=head1 LICENSE + + This program is free software; you can redistribute it and/or + modify it under the terms of the GNU General Public License + as published by the Free Software Foundation; either version 2 + of the License, or (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You may have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, + USA. + + An on-line copy of the GNU General Public License can be found + http://www.fsf.org/copyleft/gpl.html. + +=cut + +use strict; +use Getopt::Std; + +(my $progName = $0) =~ s/^.*?\///o; + +my $usageMsg = "Usage: $progName -[bhrRv] [mailfile] + -b Include bounces + -h Emit this help message and exit + -r Include rejects + -R Hard rejects only + -v Emit version and exit"; + +my $revision = '1.3'; + +use vars qw($opt_b $opt_h $opt_r $opt_R $opt_v); + +getopts('bhrRv') || die "$usageMsg\n"; +$opt_r = 1 if($opt_R); + +if($opt_h || $opt_v) { + print "$progName $revision\n" if($opt_v); + print "$usageMsg\n" if($opt_h); + exit; +} + +my ($fromQid, %fromQids, %accList, $fromWhomFull, %rejList, %bncList); + +my ($fromWhom); + +die "$usageMsg\n" unless($fromWhom = shift @ARGV); + +my $doEscapes = !($fromWhom =~ /\\/); + +# Escape "."s and "+"s? +$fromWhom =~ s/([\.\+])/\\$1/g if($doEscapes); + +while(<>) { + if(($fromQid, $fromWhomFull) = /:(?: \[ID \d+ mail\.info\])? ([A-F0-9]+): from=<(.*$fromWhom[^>]*)>/oi) { +# print "dbg: from: $fromQid $fromWhomFull\n"; + $fromQids{$fromQid} = lc $fromWhomFull; + next; + }elsif($opt_r && (my ($respCode, $fromWhomFull, $to) = /: NOQUEUE: reject: RCPT from \S+: (\d+) .+from=<(.*$fromWhom[^>]*)> to=<([^>]+)>/oi)) { + ++$rejList{lc $fromWhomFull}{"$to"} unless($opt_R && $respCode == 450); + }elsif((my $toQid, $to) = /:(?: \[ID \d+ mail\.info\])? ([A-F0-9]+): to=<([^>]+)>, .+ status=sent/o) { +# print "dbg: to: $toQid $to\n"; + if($fromQids{$toQid} && ! $opt_R) { +# print "dbg: match!\n"; + ++$accList{$fromQids{$toQid}}{$to}; + } + }elsif($opt_b && (($toQid, $to) = /:(?: \[ID \d+ mail\.info\])? ([A-F0-9]+): to=<([^>]+)>, .+ status=bounced/o)) { +# print "dbg: to: $toQid $to\n"; + if($fromQids{$toQid}) { +# print "dbg: match!\n"; + ++$bncList{$fromQids{$toQid}}{$to}; + } + } +} + +if(%accList) { + print "\nDelivered:\n"; + walk_nested_hash(\%accList, 0); +} +if($opt_r && %rejList) { + print "\nRejected:\n"; + walk_nested_hash(\%rejList, 0); +} +if($opt_b && %bncList) { + print "\nBounced:\n"; + walk_nested_hash(\%bncList, 0); +} +print "\n"; + + +# "walk" a "nested" hash +sub walk_nested_hash { + my ($hashRef, $level) = @_; + $level += 2; + my $indents = ' ' x $level; + my ($keyName, $hashVal) = each(%$hashRef); + + if(ref($hashVal) eq 'HASH') { + foreach (sort keys %$hashRef) { + print "$indents$_"; + # If the next hash is finally the data, total the + # counts for the report and print + my $hashVal2 = (each(%{$hashRef->{$_}}))[1]; + keys(%{$hashRef->{$_}}); # "reset" hash iterator + unless(ref($hashVal2) eq 'HASH') { + my $cnt = 0; + $cnt += $_ foreach (values %{$hashRef->{$_}}); + print " (total: $cnt)"; + } + print "\n"; + walk_nested_hash($hashRef->{$_}, $level); + } + } else { + really_print_hash_by_cnt_vals($hashRef, 0, $indents); + } +} + + +# *really* print hash contents sorted by numeric values in descending +# order (i.e.: highest first), then by IP/addr, in ascending order. +sub really_print_hash_by_cnt_vals { + my($hashRef, $cnt, $indents) = @_; + + foreach (map { $_->[0] } + sort { $b->[1] <=> $a->[1] || $a->[2] cmp $b->[2] } + map { [ $_, $hashRef->{$_}, normalize_host($_) ] } + (keys(%$hashRef))) + { + printf "$indents%6d %s\n", $hashRef->{$_}, $_; + last if --$cnt == 0; + } +} + +# Normalize IP addr or hostname +# (Note: Makes no effort to normalize IPv6 addrs. Just returns them +# as they're passed-in.) +sub normalize_host { + # For IP addrs and hostnames: lop off possible " (user@dom.ain)" bit + my $norm1 = (split(/\s/, $_[0]))[0]; + + if((my @octets = ($norm1 =~ /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/o)) == 4) { + # Dotted-quad IP address + return(pack('C4', @octets)); + } else { + # Possibly hostname or user@dom.ain + #return(join( '', map { lc $_ } reverse split /[.@]/, $norm1 )); + return lc $_[0]; + } +} + diff --git a/pftobyfrom.1 b/pftobyfrom.1 new file mode 100644 index 0000000..986515e --- /dev/null +++ b/pftobyfrom.1 @@ -0,0 +1,162 @@ +.\" -*- mode: troff; coding: utf-8 -*- +.\" Automatically generated by Pod::Man 5.01 (Pod::Simple 3.43) +.\" +.\" Standard preamble: +.\" ======================================================================== +.de Sp \" Vertical space (when we can't use .PP) +.if t .sp .5v +.if n .sp +.. +.de Vb \" Begin verbatim text +.ft CW +.nf +.ne \\$1 +.. +.de Ve \" End verbatim text +.ft R +.fi +.. +.\" \*(C` and \*(C' are quotes in nroff, nothing in troff, for use with C<>. +.ie n \{\ +. ds C` "" +. ds C' "" +'br\} +.el\{\ +. ds C` +. ds C' +'br\} +.\" +.\" Escape single quotes in literal strings from groff's Unicode transform. +.ie \n(.g .ds Aq \(aq +.el .ds Aq ' +.\" +.\" If the F register is >0, we'll generate index entries on stderr for +.\" titles (.TH), headers (.SH), subsections (.SS), items (.Ip), and index +.\" entries marked with X<> in POD. Of course, you'll have to process the +.\" output yourself in some meaningful fashion. +.\" +.\" Avoid warning from groff about undefined register 'F'. +.de IX +.. +.nr rF 0 +.if \n(.g .if rF .nr rF 1 +.if (\n(rF:(\n(.g==0)) \{\ +. if \nF \{\ +. de IX +. tm Index:\\$1\t\\n%\t"\\$2" +.. +. if !\nF==2 \{\ +. nr % 0 +. nr F 2 +. \} +. \} +.\} +.rr rF +.\" ======================================================================== +.\" +.IX Title "PFTOBYFROM 1" +.TH PFTOBYFROM 1 2025-05-22 1.1.6 "User Contributed Perl Documentation" +.\" For nroff, turn off justification. Always turn off hyphenation; it makes +.\" way too many mistakes in technical documents. +.if n .ad l +.nh +.SH NAME +pftobyfrom \- List "to" addresses by "from" whom in Postfix log file +.PP +Copyright (C) 2007\-2025 by James S. Seymour, Release 1.3 +.SH "SYNOPSIS pftobyfrom \-[bhrRv] [mailfile]" +.IX Header "SYNOPSIS pftobyfrom -[bhrRv] [mailfile]" +.Vb 1 +\& If no file(s) specified, reads from stdin. Output is to stdout. +.Ve +.SH DESCRIPTION +.IX Header "DESCRIPTION" +.Vb 2 +\& pftobyfrom parses Postfix log files to generate a list of "to" addresses, +\& based on a specified "from" address or address fragment. +.Ve +.SH OPTIONS +.IX Header "OPTIONS" +.Vb 1 +\& \-b Include bounces +\& +\& \-h Emit help message and exit +\& +\& \-r Include rejects +\& +\& \-R Hard rejects only +\& +\& \-v Emit version and exit +.Ve +.SH "RETURN VALUE" +.IX Header "RETURN VALUE" +.Vb 1 +\& pftobyfrom doesn\*(Aqt return anything of interest to the shell. +.Ve +.SH ERRORS +.IX Header "ERRORS" +.Vb 1 +\& Error messages are emitted to stderr. +.Ve +.SH EXAMPLES +.IX Header "EXAMPLES" +.Vb 1 +\& pftobyfrom example.com /var/log/maillog +\& +\& Generates a list of all the recipients of email from any senders +\& in "example.com" +\& +\& As a convenience, pftobyfrom tries to intelligently determine how to +\& handle regexp meta\-characters. If it\*(Aqs passed a search expression +\& that does NOT contain meta\-character escapes ("\e"), it will assume +\& that "." and "+" are literals, and will escape them for you. In the +\& example above, the "." in the FQDN part of the search term would\*(Aqve +\& been automatically escaped for the user. Likewise: +\& +\& pftobyfrom username+foo@example.com /var/log/maillog +\& +\& would have the "+" and "." escaped. If you wanted to find all +\& plussed senders for "username," you\*(Aqd have to do: +\& +\& pftobyfrom \*(Aqusername\e+.+@example\e.com\*(Aq /var/log/maillog +.Ve +.SH "SEE ALSO" +.IX Header "SEE ALSO" +.Vb 1 +\& pflogsumm, pffrombyto +.Ve +.SH NOTES +.IX Header "NOTES" +.Vb 1 +\& All search terms and searched fields are lower\-cased. +\& +\& The pftobyfrom Home Page is at: +\& +\& http://jimsun.LinxNet.com/postfix_contrib.html +.Ve +.SH REQUIREMENTS +.IX Header "REQUIREMENTS" +.Vb 1 +\& Perl +.Ve +.SH LICENSE +.IX Header "LICENSE" +.Vb 4 +\& This program is free software; you can redistribute it and/or +\& modify it under the terms of the GNU General Public License +\& as published by the Free Software Foundation; either version 2 +\& of the License, or (at your option) any later version. +\& +\& This program is distributed in the hope that it will be useful, +\& but WITHOUT ANY WARRANTY; without even the implied warranty of +\& MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +\& GNU General Public License for more details. +\& +\& You may have received a copy of the GNU General Public License +\& along with this program; if not, write to the Free Software +\& Foundation, Inc., 59 Temple Place \- Suite 330, Boston, MA 02111\-1307, +\& USA. +\& +\& An on\-line copy of the GNU General Public License can be found +\& http://www.fsf.org/copyleft/gpl.html. +.Ve -- 2.39.5