#!/usr/local/bin/perl ############################################################################### ## ## ## File: spamsum.pl ## ## Author: Wolfgang S. Rupprecht ## ## Created: Tue Feb 17 13:47:00 PST 1998 ## ## Contents: summarize the SpamFilter logs ## ## ## ## $Id: spamsum,v 1.43 2004/01/12 21:01:49 wolfgang Exp $ ############################################################################### # Copyright (c) 1998 Wolfgang S. Rupprecht. # # 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. # # NO WARRANTY # # BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO # WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE # LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT # HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT # WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING, BUT # NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND # FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE # QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE # PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY # SERVICING, REPAIR OR CORRECTION. # # IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN # WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY # MODIFY AND/OR REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE # LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, # INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR # INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF # DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU # OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY # OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN # ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. # # You should have received a copy of the GNU General Public License # along with this program; if not, write to the Free Software # Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. # # posftix version of the spamsum program. # # Assumes the log files are located at: # /var/log/maillog /var/log/mailog.0.gz ... /var/log/mailog.N.gz # use strict; our $version = '$Id: spamsum,v 1.43 2004/01/12 21:01:49 wolfgang Exp $ '; # the default list of real user names that we want to check mistaken # spamfilter hits for. our $realusers = "userA\|userB\|userC\|abuse\|postmaster"; # allow these as valid "to" addresses, but don't report them as # bounces to real users. our $realusers_ignore = "root\|postmaster\|abuse\|$realusers"; require 'getopts.pl'; use Time::Local; sub usage (){ die "usage: spamsum -acgwvV -s[0-9] -f[0-9] -a include all logfiles in report. -c check for non-dnsbl hits (useful to see who's mail the rDNS checks block.) -C N Cut-off, number of spams tolerated before an ipf block is created. -d exclude all 'dynamic' client dsl/cable/dialup addresses. (Reguires external regexp module.) -f N use last N logfiles in report. -g grep for the actual rejects and append them to report. -i print ipf filter table. -o F open output file F. -s N start with old logfile number N. -w apply www privacy filters. Used for output posted on web pages. -u U check for non-dnsbl hits for USER (implies -c) -v verbose. Output pages and pages of boring internal data. -t print TLS summary -V Show version and exit. -F display a combined From and Relay tally. (Takes gobs of memory.) -W generate whitelist from all outgoing messages. "; } our $opt_C; our $opt_F; our $opt_V; our $opt_a; our $opt_c; our $opt_d; our $opt_f; our $opt_g; our $opt_i; our $opt_o; our $opt_s; our $opt_t; our $opt_u; our $opt_v; our $opt_w; our $opt_W; if ($#ARGV >= 0) { if (&Getopts("acC:dgitwvVWFf:o:s:u:") != 1) { &usage(); } } if ($opt_V){ print STDERR "$version\n"; exit; } if ($opt_u){ $opt_c = 1; $realusers = "$opt_u"; # append this name onto the ignore regexp. $realusers_ignore = $realusers_ignore . "\|$realusers"; } our $totalmsg = 0; our $totalspammsg = 0; our $totalgoodmsg = 0; our $totalspamhits = 0; our $sumunparsable = 0; our $ipfcutoff = 100; our $totalrealuser = 0; our %listenvto = (); our %listenvfrom = (); our %listrelay = (); our %listnet = (); our %listnetname = (); our %listruleset = (); our %listreject = (); our %listmsg = (); our %listrelaynfrom = (); our %listhelo = (); our %listcn = (); our %listwhite = (); our @listgrep = (); close STDIN; # The lowest number file index to include. our $firstfile = 0; # The highest number file index to include our $lastfile = 0; our $startdate = 0; if ($opt_a) { $lastfile = 1e99; } if ($opt_f) { $lastfile = $opt_f; # -f 1 ==> include maillog.0 } if ($opt_s) { $firstfile = $opt_s; # -s 1 ==> start with maillog.0 } if ($opt_C){ $ipfcutoff = $opt_C; } # for the -d flag, setup the filter filename. if ($opt_d){ our $config='/etc/postfix/client_access_regexp_rej.pm'; if($ENV{'CLIENT_ACCESS_REGEXP_REJ'}) { $config=$ENV{'CLIENT_ACCESS_REGEXP_REJ'}; } if ( -f $config ){ require $config || die "Fatal require $config: $!\n"; } } our %M2N = ( 'Jan' => '01', 'Feb' => '02', 'Mar' => '03', 'Apr' => '04', 'May' => '05', 'Jun' => '06', 'Jul' => '07', 'Aug' => '08', 'Sep' => '09', 'Oct' => '10', 'Nov' => '11', 'Dec' => '12', ); # opendir(DIR, "/var/log") || die "can't opendir $some_dir: $!"; # @filelist1 = grep { /^maillog/ && -f "/var/log/$_" } readdir(DIR); # closedir DIR; # print STDERR "@filelist1 \n"; # @filelist = sort { # ($b =~ /=(\d+)/)[0] <=> ($a =~ /=(\d+)/)[0] # # or print STDERR "same: $a $b\n"; # } @filelist1; # # for $file (@filelist) { # # print STDERR "*** $file\n"; # open (STDIN, "< /var/log/$file") || die "can't open $!\n"; # &readlogs; # close (STDIN); # $curfile += 1; # } our $curfile = $firstfile; our $file; our @filelist; while ($curfile <= $lastfile){ if ($curfile > 0) { $file = "maillog." . ($curfile - 1) . ".gz"; } else { $file = "maillog"; } if (! -f "/var/log/$file") { last; } push (@filelist , $file); $curfile += 1; } @filelist = reverse(@filelist); # print STDERR "@filelist \n"; # die "done\n"; for $file (@filelist) { # print STDERR "*** $file\n"; $_ = $file; if (/\.gz$/){ open (STDIN, "zcat /var/log/$file|") || die "can't open $!\n"; } else { open (STDIN, "< /var/log/$file") || die "can't open $!\n"; } $opt_v && print STDERR "$file\n"; &readlogs; close (STDIN); $curfile += 1; } if ($opt_o){ close STDOUT; open (STDOUT, ">$opt_o") || die "can't open outfile $!.\n"; } #$opt_v && print STDERR "printsum: "; system("date"); if ($opt_W){ &printwhitelist; } elsif ($opt_i){ &printipf; } elsif ($opt_t){ &printtls; } else { &printsum; } sub readlogs () { my $year; my $m; my $msg; my %ignorehost; my $tmp; # the perl in redhat 6.2 requires instead of <>. while () { # Jun 15 14:47:52 capsicum postfix/qmgr[25008]: 76184977F4: # from=, size=5220, nrcpt=1 # (queue active) if (/ postfix\/qmgr\[([0-9]+)\]: /){ $totalgoodmsg += 1; } if (!$startdate){ /^(\w+)\W+(\w+)\W+(\w+):(\w+):(\w+)/ || die "can't grok date '$_'\n"; # /^([a-zA-Z]+[ ]+[0-9]+[ ]+[0-9:]+)/; # Sep 28 01: 15: 54 # 1 2 3 4 5 # print STDERR "$_ date: 1 $1/2 $2/3 $3/4 $4/5 $5\n"; # $year = $localtime->year; # hackjob, intuit year from current year. $year = (localtime())[5]; # localtime->month is 0-11 based. $m = $M2N{$1} - 1; # hackjob #2. If logfile month is larger than current month. # assume the year changed. Decrement the logfile year. # Crap, why don't the logs contain the year!!! -wsr if ($m > (localtime())[4]){ # print STDERR "new year!\n"; $year -= 1; } $startdate = timelocal($5,$4,$3,$2,$m,$year); # print STDERR "startdate: $startdate\n"; } if ($opt_W && / postfix\/smtp\[([0-9]+)\]: /){ # Dec 31 13:07:52 capsicum postfix/smtp[18889]: B60EB66838: # to=, # relay=bar.example.com[10.11.12.13], delay=3, status=sent # (250 2.0.0 hBVL7qF11853 Message accepted for delivery) if (/ status=sent / && / to=<([^>]*)>, /) { my $to=$1; $_=$to; /^MAILER-DAEMON@/ && next; /^(bounce|confirm|sendto|leave|lyris-confirm)-/ && next; /=/ && next; $listwhite{$to} += 1; } } if (!$opt_W && / postfix\/(smtpd|cleanup)\[([0-9]+)\]: /){ # Jun 16 09:21:19 capsicum postfix/smtpd[19585]: reject: RCPT # from unknown[202.41.72.121]: 450 Client host rejected: # cannot find your hostname, [202.41.72.121]; # from= to= my $localfrom=""; my $localrelay=""; # Aug 2 02:00:09 capsicum postfix/smtpd[27264]: Verified: subject_CN=cayenne.wsrcc.com, issuer=WSRCC Root if (/ ([^ ]+erified): subject_CN=(.*)/){ $listcn{"$2, $1"} += 1; } if (!$opt_t && / reject: ([^ ]*) /){ $msg = $_; # now that we ignore 450's we should count bounces. # # don't count bounces for ipf filter purposes. # if ($opt_i && /from=<>/){ # next; # } # Don't count every 4xx error either. # Since a host correctly retries these, the counts are # inflated. if ($opt_i && /: 4[0-9][0-9] # print STDERR "ignoring temp error $_"; next; } $localrelay = 0; if (/ RCPT from ([^] ]+]): /){ $localrelay = $1; } elsif (/ from ([^; ]+); from=/) { $localrelay = $1; } elsif (/ from ([^; ]+)$/) { # reject: ETRN LORD.DOL.RU... from unknown[213.171.58.74] $localrelay = $1; } elsif (/ MAIL from ([^; ]+): /) { # reject: MAIL from msr67.hinet.net[168.95.4.167]: # 552 Message size exceeds fixed limit; proto=ESMTP # helo= $localrelay = $1; } else { printf "** Parser - Can't find client localrelay '$_' ***\n"; } if ($opt_d){ ($_ = $localrelay) =~ s/\[.*\]//; # # grab patterns from: # /etc/postfix/client_access_regexp_rej # client_access_regexp_rej() && next; # # Do a few more exclusions that appear elsewhere. # # from client_access_rej file. No mail should come from # the user's part of the IP space. /\.ipt\.aol\.com$/i && next; # Ignore unknown names. They weren't about to be accepted anyway. /^unknown$/i && next; $_ = $msg; } # in this mode we want to focus on blocks due to # DNS setup errors and SMTP setup error. if ($opt_c) { # ignore blacklist blocks # ignore pipelining blocks # ignore mail to spamtraps if (/ blocked using / || / Improper use of SMTP command pipelining; / || / Mail claims to be from / || ! / to=<($realusers_ignore)(\+[^@>]*)?@/){ if ($localrelay){ $ignorehost{$localrelay} += 1; } next; } # don't bother looking at mail from hosts that are # blacklisted or sent mail to bad users. if ($localrelay && $ignorehost{$localrelay}){ # print STDERR "**** ignoring $_ ****\n"; next; } # if it is something to a non-real user, don't include it. if (!/ to=<($realusers)[^@>]*@/){ next; } # Sven virus spam. Ignore. There is way too much to # peice through. if (/The address has changed\./){ next; } } if ($opt_g) { $listgrep[$totalspammsg] = $msg; } $totalspammsg += 1; # $opt_v && print STDERR "Rejected: $_\n"; $totalspamhits += 1; if (/ to=<($realusers_ignore)[^@>]*@/){ $totalrealuser += 1; } if (/ from=(<[^ >]*>) /){ $listenvfrom{$1} += 1; $localfrom=$1; } $listrelay{$localrelay} += 1; $tmp = $_; $_=$localrelay; if (/\[([0-9]+\.[0-9]+\.[0-9]+)\.[0-9]+\]/){ $listnet{$1} += 1; $listnetname{$1} = $localrelay; # print STDERR "localnet: $1.0/24\n"; } $_ = $tmp; if ($opt_F) { $listrelaynfrom{"$localrelay" . " " . "$localfrom"} += 1; } if (/ to=(<[^ \n>]*>)/){ $listenvto{$1} += 1; } if (/ helo=<([^ \n>]*)>/){ $listhelo{$1} += 1; } # fully process the reject msg now. if (/ reject: ([^]]*]): ([0-9][0-9][0-9]) (.*)/){ $listreject{$2} += 1; $m = $3; $m =~ s/; from=.*//; # not needed now that "blocked using ..." pattern has been added. # $m =~ s/, reason.*//; $m =~ s/( blocked using [^ ,;]+).*/\1/; $m =~ s/\[[0-9.:a-fA-F]+\]/[X.X.X.X]/g; # make sure that the above transformation doesn't # get hit by this one too. $_=$m; if (! /\[X.X.X.X\]/) { $m =~ s/\[[^ \@\[\]]+\]/[XXX.XXX]/g; } # for rhsbl rejects (email addresses in square brackets) $m =~ s/\[[^ \[\]]+\@[^ \[\]]+\]/[XXX\@XXX.XXX]/g; $m =~ s/<[^>]*>//g; # : Helo command rejected: Host not found $m =~ s/<[^>]*>: //; # Client host rejected: Spam from XXX Sun Jun 16 13:36:52 2002 $m =~ s/( Spam from ).*/\1XXX .../; # Try this. Shorten to the first sentence. -wsr 2004/1/11 $m =~ s/\. .*/./; # and get rid of the duplicate trimings. # $m =~ s/( changed\.).*/\1/; # $m =~ s/( instead\.).*/\1/; # $m =~ s/( bounce mail sent to abuse\.).*/\1/; $m =~ s/( Please use your provider's mail server).*/\1/; $listmsg{$m} += 1; } # elsif (/ reject: header (.*)/) { # # $listreject{$2} += 1; # $m = $1; # $m =~ s/\. Microsoft .*/./; # $m =~ s/X-UIDL: .*/X-UIDL: /; # $m =~ s/;.*/;/; # $listmsg{$m} += 1; # } # helo= is for postfix 2.0 # from= is for local bounces (like when we try to send HTML by mistake) elsif (/ reject: (body|header) (.*) (to|helo|from)=<[^>]*>: (.*)/) { # Jul 15 01:54:10 capsicum postfix/cleanup[15360]: # E3DD9979CE: reject: body Content-Type: # multipart/alternative;boundary="----=_NextPart_2"; # from= # to=: HTML not # accepted. my $tag = $1; $m = "$1: $4"; $m =~ s/ [Rr]ejected\. .*/ rejected./; # trim to first sentence. 2004/1/11 $m =~ s/\. */./; # not needed any longer. 2004/1/11 # $m =~ s/\. Microsoft .*/./; # older verbose HTML message only emitted while # we were testing. $m =~ s/.*permit HTML here.*/$tag: HTML rejected./; # $m =~ s/\..*/./; $listmsg{$m} += 1; } elsif (/ reject: (ETRN) /) { $m = $1; $listmsg{$m} += 1; } else { $sumunparsable += 1; chop; print STDERR "*** parser: $_ ***\n"; } } } # elsif (/ newsyslog\[([0-9]+)\]:/){ # /^([a-zA-Z]+[ ]+[0-9]+[ ]+[0-9:]+)/; # if (!$startdate){ # $startdate = $1; # } # next; # } else { # $opt_v && print STDERR "can't get ID for line $_\n"; next; } } } sub printipf () { my $key; my $now = scalar (localtime); printf "# Run at %s\n", $now; my $timediff = (timegm (gmtime) - $startdate) / 3600.0; my $units = "hours"; my $spamtime = $timediff; if ($timediff > 24.0) { $units = "days"; $spamtime /= 24.0; } printf "# timespan %0.1f %s\n", $spamtime, $units; printf "# cuttoff %s hits\n", $ipfcutoff; foreach $key (sort { $listnet{$b} <=> $listnet{$a} or $a cmp $b } (keys %listnet)) { # block in log quick from 65.61.188.0/24 to any if ($listnet{$key} >= $ipfcutoff){ # printf "# hits: %d %s\nblock in log quick from %s.0/24 to any\n", # $listnet{$key}, $listnetname{$key}, $key; # or one-line form with comments on the end. (tends to make long lines.) printf "block return-icmp(filter-prohib) in log quick from %15s.0/24 to any\t# hits: %d %s\n", $key, $listnet{$key}, $listnetname{$key}; } } printf "\n"; } sub printwhitelist () { my $now = scalar (localtime); printf "# Run at %s\n", $now; print "# Log file: @filelist\n"; # printf "from: %0.1f hours\n", (timelocal (localtime)) / 3600.0; # printf "to: %0.1f hours\n", ($startdate) / 3600.0; my $timediff = (timegm (gmtime) - $startdate) / 3600.0; my $units = "hours"; my $spamtime = $timediff; if ($timediff > 24.0) { $units = "days"; $spamtime /= 24.0; } printf "# summary from %s to %s (%0.1f %s)\n", scalar (localtime($startdate)), scalar (localtime), $spamtime, $units; printf "# $version\n\n"; my $key; foreach $key (sort { $a cmp $b or $listwhite{$b} <=> $listwhite{$a}} (keys %listwhite)) { # the db routine doesn't strip the "#" out! # printf "%-40s\tOK # %d\n", $key, $listwhite{$key}; printf "%-40s\tOK\n", $key; } printf "\n# End\n"; } sub printtls () { my $now = scalar (localtime); printf "Run at %s\n", $now; print "Log file: @filelist\n"; # printf "from: %0.1f hours\n", (timelocal (localtime)) / 3600.0; # printf "to: %0.1f hours\n", ($startdate) / 3600.0; my $timediff = (timegm (gmtime) - $startdate) / 3600.0; my $units = "hours"; my $spamtime = $timediff; if ($timediff > 24.0) { $units = "days"; $spamtime /= 24.0; } printf "summary from %s to %s (%0.1f %s)\n", scalar (localtime($startdate)), scalar (localtime), $spamtime, $units; printf "$version\n\n"; my $key; printf "Subject CN:\n"; foreach $key (sort { $listcn{$b} <=> $listcn{$a} or $a cmp $b } (keys %listcn)) { printf "%6d\t%s\n", $listcn{$key}, $key; } printf "\n"; } sub printsum () { $opt_v && print STDERR "starting summaries\n"; my $now = scalar (localtime); printf "Run at %s\n", $now; print "Log file: @filelist\n"; # printf "from: %0.1f hours\n", (timelocal (localtime)) / 3600.0; # printf "to: %0.1f hours\n", ($startdate) / 3600.0; my $timediff = (timegm (gmtime) - $startdate) / 3600.0; my $units = "hours"; my $spamtime = $timediff; if ($timediff > 24.0) { $units = "days"; $spamtime /= 24.0; } printf "summary from %s to %s (%0.1f %s)\n", scalar (localtime($startdate)), scalar (localtime), $spamtime, $units; printf "$version\n\n"; my $cnt = 0; if ($opt_c){ my $ru = "$realusers"; $ru =~ s/\|/ /g; print "*** Summary of non-BLACKLIST spam to: $ru ***\n\n"; } if ($opt_d){ printf "*** Summary for non-customer-regexp hosts ***\n\n"; } my $key; foreach $key (keys %listenvfrom){ $cnt += 1; # print "$key\n"; } printf "total spam froms: %6d\n", $cnt; $cnt = 0; foreach $key (keys %listrelay){ $cnt += 1; } printf "total spam relays: %6d\n", $cnt; printf "total spam msgs: %6d\n", $totalspammsg; if (! $opt_w){ if ($totalspammsg){ printf "total real user spam: %6d (%d%%)\n", $totalrealuser, (100.0 * $totalrealuser) / $totalspammsg; } else { printf "total real user spam: %6d\n", $totalrealuser; } } printf "total good msgs: %6d\n", $totalgoodmsg; printf "total msgs: %6d\n", $totalspammsg + $totalgoodmsg; if ($timediff){ # printf "total spam/hr: %6.1f\n", $totalspammsg / $timediff; printf "total spam/day: %8.1f\n", $totalspammsg * 24.0 / $timediff; printf "total spam/month: %8d\n", $totalspammsg * 24.0 * 30.0/ $timediff; } if ($totalspammsg + $totalgoodmsg) { printf "spam ratio: %6d%%\n", (100.0 * $totalspammsg) / ($totalspammsg + $totalgoodmsg); } # printf "total unparsable: %6d\n", $sumunparsable; printf "\n"; printf "%6s\t%5s\t%s\n\n", "Count", "Percent", "Descr"; printf "Msg:\n"; foreach $key (sort { $listmsg{$b} <=> $listmsg{$a} or $a cmp $b } (keys %listmsg)) { printf "%6d\t%5.2f\t%s\n", $listmsg{$key}, 100.0 * $listmsg{$key}/$totalspammsg, $key; } printf "\n"; if (! $opt_w){ # printf "Ruleset:\n"; # foreach $key (sort { $listruleset{$b} <=> $listruleset{$a} or $a cmp $b } # (keys %listruleset)) { # printf "%6d\t%s\n", $listruleset{$key}, $key; # } # printf "\n"; } printf "Reject:\n"; foreach $key (sort { $listreject{$b} <=> $listreject{$a} or $a cmp $b } (keys %listreject)) { printf "%6d\t%5.2f\t%s\n", $listreject{$key}, 100.0 * $listreject{$key}/$totalspammsg, $key; } printf "\n"; if (! $opt_w && $opt_F){ printf "Relay & From:\n"; foreach $key (sort { $listrelaynfrom{$b} <=> $listrelaynfrom{$a} or $a cmp $b } (keys %listrelaynfrom)) { printf "%6d\t%5.2f\t%s\n", $listrelaynfrom{$key}, 100.0 * $listrelaynfrom{$key}/$totalspammsg, $key; } printf "\n"; } printf "Relays:\n"; if ($opt_d){ foreach $key (sort { $a cmp $b or $listrelay{$b} <=> $listrelay{$a} } (keys %listrelay)) { printf "%6d\t%5.2f\t%s\n", $listrelay{$key}, 100.0 * $listrelay{$key}/$totalspammsg, $key; } } else { foreach $key (sort { $listrelay{$b} <=> $listrelay{$a} or $a cmp $b } (keys %listrelay)) { printf "%6d\t%5.2f\t%s\n", $listrelay{$key}, 100.0 * $listrelay{$key}/$totalspammsg, $key; } } printf "\n"; printf "Helo:\n"; foreach $key (sort { $listhelo{$b} <=> $listhelo{$a} or $a cmp $b } (keys %listhelo)) { printf "%6d\t%5.2f\t%s\n", $listhelo{$key}, 100.0 * $listhelo{$key}/$totalspammsg, $key; } printf "\n"; if (! $opt_w){ printf "From:\n"; foreach $key (sort { $listenvfrom{$b} <=> $listenvfrom{$a} or $a cmp $b } (keys %listenvfrom)) { printf "%6d\t%5.2f\t%s\n", $listenvfrom{$key}, 100.0 * $listenvfrom{$key}/$totalspammsg, $key; } printf "\n"; printf "To:\n"; foreach $key (sort { $listenvto{$b} <=> $listenvto{$a} or $a cmp $b } (keys %listenvto)) { printf "%6d\t%5.2f\t%s\n", $listenvto{$key}, 100.0 * $listenvto{$key}/$totalspammsg, $key; } printf "\n"; my $msg; if ($opt_g){ printf "Log:\n"; foreach $msg (@listgrep) { printf "------\n%s", $msg; } printf "\n"; } } } # # end #