-#!PERL_COMMAND -w
-# $Cambridge: exim/src/src/eximstats.src,v 1.16 2007/04/11 15:05:03 steve Exp $
+#!PERL_COMMAND
-# Copyright (c) 2001 University of Cambridge.
+# Copyright (c) 2001-2016 University of Cambridge.
# See the file NOTICE for conditions of use and distribution.
# Perl script to generate statistics from one or more Exim log files.
# 2001-10-21 Removed -domain flag and added -bydomain, -byhost, and -byemail.
# We now generate our main parsing subroutine as an eval statement
# which improves performance dramatically when not all the results
-# are required. We also cache the last timestamp to time convertion.
+# are required. We also cache the last timestamp to time conversion.
#
# NOTE: 'Top 50 destinations by (message count|volume)' lines are
# now 'Top N (host|email|domain) destinations by (message count|volume)'
# in HTML output. Also added code to convert them back with -merge.
# Fixed timestamp offsets to convert to seconds rather than minutes.
# Updated -merge to work with output files using timezones.
-# Added cacheing to speed up the calculation of timezone offsets.
+# Added caching to speed up the calculation of timezone offsets.
#
# 2003-02-07 V1.25 Steve Campbell
# Optimised the usage of mktime() in the seconds subroutine.
# Bernard Massot.
#
# 2003-06-03 V1.28 John Newman
-# Added in the ability to skip over the parsing and evaulation of
+# Added in the ability to skip over the parsing and evaluation of
# specific transports as passed to eximstats via the new "-nt/.../"
# command line argument. This new switch allows the viewing of
# not more accurate statistics but more applicable statistics when
# Added -xls and the ability to specify output files.
#
# 2005-04-29 V1.38 Steve Campbell
-# Use FileHandles for outputing results.
+# Use FileHandles for outputting results.
# Allow any combination of xls, txt, and html output.
# Fixed display of large numbers with -nvr option
# Fixed merging of reports with empty tables.
# 2007-04-11 V1.58 Steve Campbell
# Fix to get <> and blackhole to show in edomain tables.
#
+# 2007-09-20 V1.59 Steve Campbell
+# Added the -bylocaldomain option
+#
+# 2007-09-20 V1.60 Heiko Schlittermann
+# Fix for misinterpreted log lines
+#
+# 2013-01-14 V1.61 Steve Campbell
+# Watch out for senders sending "HELO [IpAddr]"
#
#
# For documentation on the logfile format, see
Show the delivery times (B<DT>)for all the messages.
-Exim must have been configured to use the +delivery_time logging option
+Exim must have been configured to use the +deliver_time logging option
for this option to work.
I<list> is an optional list of times. Eg -show_dt1,2,4,8 will show
This program does not perfectly handle messages whose received
and delivered log lines are in different files, which can happen
when you have multiple mail servers and a message cannot be
-immeadiately delivered. Fixing this could be tricky...
+immediately delivered. Fixing this could be tricky...
Merging of xls files is not (yet) possible. Be free to implement :)
=cut
+use warnings;
use integer;
+BEGIN { pop @INC if $INC[-1] eq '.' };
use strict;
use IO::File;
@days_per_month = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
$gig = 1024 * 1024 * 1024;
-$VERSION = '1.58';
+$VERSION = '1.61';
# How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
$COLUMN_WIDTHS = 8;
use vars qw($total_received_data $total_received_data_gigs $total_received_count);
use vars qw($total_delivered_data $total_delivered_data_gigs $total_delivered_messages $total_delivered_addresses);
use vars qw(%timestamp2time); #Hash of timestamp => time.
-use vars qw($last_timestamp $last_time); #The last time convertion done.
-use vars qw($last_date $date_seconds); #The last date convertion done.
-use vars qw($last_offset $offset_seconds); #The last time offset convertion done.
+use vars qw($last_timestamp $last_time); #The last time conversion done.
+use vars qw($last_date $date_seconds); #The last date conversion done.
+use vars qw($last_offset $offset_seconds); #The last time offset conversion done.
use vars qw($localtime_offset);
use vars qw($i); #General loop counter.
use vars qw($debug); #Debug mode?
use vars qw(%rejected_count_by_ip %rejected_count_by_reason);
use vars qw(%temporarily_rejected_count_by_ip %temporarily_rejected_count_by_reason);
-#For use in Speadsheed::WriteExcel
+#For use in Spreadsheet::WriteExcel
use vars qw($workbook $ws_global $ws_relayed $ws_errors);
use vars qw($row $col $row_hist $col_hist);
use vars qw($run_hist);
# The following are parameters whose values are
# set by command line switches:
use vars qw($show_errors $show_relay $show_transport $transport_pattern);
-use vars qw($topcount $local_league_table $include_remote_users);
+use vars qw($topcount $local_league_table $include_remote_users $do_local_domain);
use vars qw($hist_opt $hist_interval $hist_number $volume_rounding $emptyOK);
use vars qw($relay_pattern @queue_times @user_patterns @user_descriptions);
use vars qw(@rcpt_times @delivery_times);
use vars qw(%delivered_messages %delivered_data %delivered_data_gigs %delivered_addresses);
use vars qw(%received_count_user %received_data_user %received_data_gigs_user);
use vars qw(%delivered_messages_user %delivered_addresses_user %delivered_data_user %delivered_data_gigs_user);
+use vars qw(%delivered_messages_local_domain %delivered_addresses_local_domain %delivered_data_local_domain %delivered_data_gigs_local_domain);
use vars qw(%transported_count %transported_data %transported_data_gigs);
use vars qw(%relayed %errors_count $message_errors);
use vars qw(@qt_all_bin @qt_remote_bin);
}
else {
# We don't want any rounding to be done.
- # and we don't need broken formated output which on one hand avoids numbers from
- # being interpreted as string by Spreadsheed Calculators, on the other hand
+ # and we don't need broken formatted output which on one hand avoids numbers from
+ # being interpreted as string by Spreadsheet Calculators, on the other hand
# breaks if more than 4 digits! -> flexible length instead of fixed length
# Format the return value at the output routine! -fh
#$rounded = sprintf("%d", ($g * $gig) + $x);
# Eg 3h20m5s => 12005
#######################################################################
sub unformat_time {
- my($formated_time) = pop @_;
+ my($formatted_time) = pop @_;
my $time = 0;
- while ($formated_time =~ s/^(\d+)([wdhms]?)//) {
+ while ($formatted_time =~ s/^(\d+)([wdhms]?)//) {
$time += $1 if ($2 eq '' || $2 eq 's');
$time += $1 * 60 if ($2 eq 'm');
$time += $1 * 60 * 60 if ($2 eq 'h');
# POSIX::mktime. We expect the timestamp to be of the form
# "$year-$mon-$day $hour:$min:$sec", with month going from 1 to 12,
# and the year to be absolute (we do the necessary conversions). The
+# seconds value can be followed by decimals, which we ignore. The
# timestamp may be followed with an offset from UTC like "+$hh$mm"; if the
# offset is not present, and we have not been told that the log is in UTC
# (with the -utc option), then we adjust the time by the current local
# Is the timestamp the same as the last one?
return $last_time if ($last_timestamp eq $timestamp);
- return 0 unless ($timestamp =~ /^((\d{4})\-(\d\d)-(\d\d))\s(\d\d):(\d\d):(\d\d)( ([+-])(\d\d)(\d\d))?/o);
+ return 0 unless ($timestamp =~ /^((\d{4})\-(\d\d)-(\d\d))\s(\d\d):(\d\d):(\d\d)(?:\.\d+)?( ([+-])(\d\d)(\d\d))?/o);
unless ($last_date eq $1) {
$last_date = $1;
}
my $time = $date_seconds + ($5 * 3600) + ($6 * 60) + $7;
- # SC. Use cacheing. Also note we want seconds not minutes.
- #my($this_offset) = ($10 * 60 + $11) * ($9 . "1") if defined $8;
+ # SC. Use caching. Also note we want seconds not minutes.
+ #my($this_offset) = ($10 * 60 + $12) * ($9 . "1") if defined $8;
if (defined $8 && ($8 ne $last_offset)) {
$last_offset = $8;
$offset_seconds = ($10 * 60 + $11) * 60;
}
- if (defined $7) {
+ if (defined $8) {
#$time -= $this_offset;
$time -= $offset_seconds;
} elsif (defined $localtime_offset) {
# Create a dummy hash entry for the key if required.
# Note that setting the dummy_hash value sets it for both href2 &
- # href3. Also note that currently we are guarenteed to have a real
+ # href3. Also note that currently we are guaranteed to have a real
# value for href3 if a real value for href2 exists so don't need to
# test for it as well.
$dummy_hash{$key} = 0 unless exists $href2->{$key};
-bydomain show results by sending domain.
-byemail show results by sender's email address
-byedomain show results by sender's email domain
+-bylocaldomain show results by local domain
-pattern "Description" /pattern/
Count lines matching specified patterns and show them in
my $parser = '
my($ip,$host,$email,$edomain,$domain,$thissize,$size,$old,$new);
my($tod,$m_hour,$m_min,$id,$flag,$extra,$length);
- my($seconds,$queued,$rcpt_time);
+ my($seconds,$queued,$rcpt_time,$local_domain);
my $rej_id = 0;
while (<$fh>) {
$length = length($_);
next if ($length < 38);
- next unless /^(\\d{4}\\-\\d\\d-\\d\\d\\s(\\d\\d):(\\d\\d):\\d\\d( [-+]\\d\\d\\d\\d)?)( \\[\\d+\\])?/o;
-
- ($tod,$m_hour,$m_min) = ($1,$2,$3);
+ next unless /^
+ (\\d{4}\\-\\d\\d-\\d\\d\\s # 1: YYYYMMDD HHMMSS
+ (\\d\\d) # 2: HH
+ :
+ (\\d\\d) # 3: MM
+ :\\d\\d
+ )
+ (\\.\\d+)? # 4: subseconds
+ (\s[-+]\\d\\d\\d\\d)? # 5: tz-offset
+ (\s\\[\\d+\\])? # 6: pid
+ /ox;
+
+ $tod = defined($5) ? $1 . $5 : $1;
+ ($m_hour,$m_min) = ($2,$3);
# PH - watch for GMT offsets in the timestamp.
- if (defined($4)) {
+ if (defined($5)) {
$extra = 6;
next if ($length < 44);
}
$extra = 0;
}
+ # watch for subsecond precision
+ if (defined($4)) {
+ $extra += length($4);
+ next if ($length < 38 + $extra);
+ }
+
# PH - watch for PID added after the timestamp.
- if (defined($5)) {
- $extra += length($5);
+ if (defined($6)) {
+ $extra += length($6);
next if ($length < 38 + $extra);
}
# "H=Host (UnverifiedHost) [IpAddr]" or "H=(UnverifiedHost) [IpAddr]".
# We do 2 separate matches to keep the matches simple and fast.
# Host is local unless otherwise specified.
- $ip = (/\\bH=.*?(\\[[^]]+\\])/) ? $1 : "local";
+ # Watch out for "H=([IpAddr])" in case they send "[IpAddr]" as their HELO!
+ $ip = (/\\bH=(?:|.*? )(\\[[^]]+\\])/) ? $1
+ # 2008-03-31 06:25:22 Connection from [213.246.33.217]:39456 refused: too many connections from that IP address // .hs
+ : (/Connection from (\[\S+\])/) ? $1
+ # 2008-03-31 06:52:40 SMTP call from mail.cacoshrf.com (ccsd02.ccsd.local) [69.24.118.229]:4511 dropped: too many nonmail commands (last was "RSET") // .hs
+ : (/SMTP call from .*?(\[\S+\])/) ? $1
+ : "local";
$host = (/\\bH=(\\S+)/) ? $1 : "local";
$domain = "localdomain"; #Domain is localdomain unless otherwise specified.
#IFDEF ($do_sender{Domain})
- if ($host !~ /^\\[/ && $host =~ /^(\\(?)[^\\.]+\\.([^\\.]+\\..*)/) {
+ if ($host =~ /^\\[/ || $host =~ /^[\\d\\.]+$/) {
+ # Host is just an IP address.
+ $domain = $host;
+ }
+ elsif ($host =~ /^(\\(?)[^\\.]+\\.([^\\.]+\\..*)/) {
# Remove the host portion from the DNS name. We ensure that we end up
# with at least xxx.yyy. $host can be "(x.y.z)" or "x.y.z".
$domain = lc("$1.$2");
#ENDIF ($include_original_destination)
#my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
my($parent) = $_ =~ / (<.+?>) /; #DT 1.54
- $user = "$user $parent" if defined $parent;
+ if (defined $parent) {
+ $user = "$user $parent";
+ #IFDEF ($do_local_domain)
+ if ($parent =~ /\\@(.+)>/) {
+ $local_domain = lc($1);
+ ++$delivered_messages_local_domain{$local_domain};
+ ++$delivered_addresses_local_domain{$local_domain};
+ add_volume(\\$delivered_data_local_domain{$local_domain},\\$delivered_data_gigs_local_domain{$local_domain},$size);
+ }
+ #ENDIF ($do_local_domain)
+ }
}
++$delivered_messages_user{$user};
++$delivered_addresses_user{$user};
# 2005-09-23 15:07:49 1EInHJ-0007Ex-Au H=(a.b.c) [10.0.0.1] F=<> rejected after DATA: This message contains a virus: (Eicar-Test-Signature) please scan your system.
# 2005-10-06 10:50:07 1ENRS3-0000Nr-Kt => blackhole (DATA ACL discarded recipients): This message contains a virus: (Worm.SomeFool.P) please scan your system.
/ rejected after DATA: (.*)/ ||
+ / (rejected DATA: .*)/ ||
/.DATA ACL discarded recipients.: (.*)/ ||
/rejected after DATA: (unqualified address not permitted)/ ||
/(VRFY rejected)/ ||
++$rejected_count_by_reason{"\u$1$2"};
++$rejected_count_by_ip{$ip};
}
+ elsif (
+ # 2008-03-31 06:25:22 H=mail.densitron.com [216.70.140.224]:45386 temporarily rejected connection in "connect" ACL: too fast reconnects // .hs
+ # 2008-03-31 06:25:22 H=mail.densitron.com [216.70.140.224]:45386 temporarily rejected connection in "connect" ACL // .hs
+ /(temporarily rejected connection in .*?ACL:?.*)/
+ ) {
+ ++$temporarily_rejected_count_by_ip{$ip};
+ ++$temporarily_rejected_count_by_reason{"\u$1"};
+ }
else {
++$rejected_count_by_reason{Unknown};
++$rejected_count_by_ip{$ip};
print $htm_fh "<li><a href=\"#Local destination count\">Top $topcount local destinations by message count</a>\n";
print $htm_fh "<li><a href=\"#Local destination volume\">Top $topcount local destinations by volume</a>\n";
}
+ if (($local_league_table || $include_remote_users) && %delivered_messages_local_domain) {
+ print $htm_fh "<li><a href=\"#Local domain destination count\">Top $topcount local domain destinations by message count</a>\n";
+ print $htm_fh "<li><a href=\"#Local domain destination volume\">Top $topcount local domain destinations by volume</a>\n";
+ }
print $htm_fh "<li><a href=\"#Rejected ip count\">Top $topcount rejected ips by message count</a>\n" if %rejected_count_by_ip;
print $htm_fh "<li><a href=\"#Temporarily rejected ip count\">Top $topcount temporarily rejected ips by message count</a>\n" if %temporarily_rejected_count_by_ip;
if ($messages > 0) {
@content = ($total_aref->[0], '', $messages, '');
- #Count the number of distict IPs for the Hosts column.
+ #Count the number of distinct IPs for the Hosts column.
push(@content,scalar(keys %{$total_aref->[1]})) if $do_sender{Host};
#These rows do not have entries for the following columns (if specified)
my $previous_seconds_on_queue = 0;
if (/^\s*(Under|Over|)\s+(\d+[smhdw])\s+(\d+)/) {
print STDERR "Parsing $_" if $debug;
- my($modifier,$formated_time,$count) = ($1,$2,$3);
- my $seconds = unformat_time($formated_time);
+ my($modifier,$formatted_time,$count) = ($1,$2,$3);
+ my $seconds = unformat_time($formatted_time);
my $time_on_queue = ($seconds + $previous_seconds_on_queue) / 2;
$previous_seconds_on_queue = $seconds;
$time_on_queue = $seconds * 2 if ($modifier eq 'Over');
$data_href = \%delivered_data_user;
$data_gigs_href = \%delivered_data_gigs_user;
}
+ elsif ($category =~ /local domain destination/) {
+ $messages_href = \%delivered_messages_local_domain;
+ $addresses_href = \%delivered_addresses_local_domain;
+ $data_href = \%delivered_data_local_domain;
+ $data_gigs_href = \%delivered_data_gigs_local_domain;
+ }
elsif ($category =~ /(\S+) destination/) {
#Top 50 (host|domain|email|edomain) destinations
#Top (host|domain|email|edomain) destination
#
# add_to_totals(\%totals,\@keys,$values);
#
-# Given a line of space seperated values, add them into the provided hash using @keys
+# Given a line of space separated values, add them into the provided hash using @keys
# as the hash keys.
#
# If the value contains a '%', then the value is set rather than added. Otherwise, we
#
# line_to_hash(\%hash,\@keys,$line);
#
-# Given a line of space seperated values, set them into the provided hash
+# Given a line of space separated values, set them into the provided hash
# using @keys as the hash keys.
#######################################################################
sub line_to_hash {
# until we've got all of the argument.
#
# This isn't perfect as all white space gets reduced to one space,
-# but it's as good as we can get! If it's esential that spacing
+# but it's as good as we can get! If it's essential that spacing
# be preserved precisely, then you get that by not using shell
# variables.
#######################################################################
#######################################################################
# @rcpt_times = parse_time_list($string);
#
-# Parse a comma seperated list of time values in seconds given by
+# Parse a comma separated list of time values in seconds given by
# the user and fill an array.
#
# Return a default list if $string is undefined.
elsif ($ARGV[0] =~ /^-byemail$/) { $do_sender{Email} = 1 }
elsif ($ARGV[0] =~ /^-byemaildomain$/) { $do_sender{Edomain} = 1 }
elsif ($ARGV[0] =~ /^-byedomain$/) { $do_sender{Edomain} = 1 }
+ elsif ($ARGV[0] =~ /^-bylocaldomain$/) { $do_local_domain = 1 }
elsif ($ARGV[0] =~ /^-emptyok$/) { $emptyOK = 1 }
elsif ($ARGV[0] =~ /^-nvr$/) { $volume_rounding = 0 }
elsif ($ARGV[0] =~ /^-show_rt([,\d\+\-\*\/]+)?$/) { @rcpt_times = parse_time_list($1) }
print_league_table("\l$_ destination", $delivered_messages{$_}, $delivered_addresses{$_}, $delivered_data{$_},$delivered_data_gigs{$_}, $ws_top50, \$ws_top50_row);
}
print_league_table("local destination", \%delivered_messages_user, \%delivered_addresses_user, \%delivered_data_user,\%delivered_data_gigs_user, $ws_top50, \$ws_top50_row) if (($local_league_table || $include_remote_users) && %delivered_messages_user);
+ print_league_table("local domain destination", \%delivered_messages_local_domain, \%delivered_addresses_local_domain, \%delivered_data_local_domain,\%delivered_data_gigs_local_domain, $ws_top50, \$ws_top50_row) if (($local_league_table || $include_remote_users) && %delivered_messages_local_domain);
print_league_table("rejected ip", \%rejected_count_by_ip, undef, undef, undef, $ws_rej, \$ws_rej_row) if %rejected_count_by_ip;
print_league_table("temporarily rejected ip", \%temporarily_rejected_count_by_ip, undef, undef, undef, $ws_rej, \$ws_rej_row) if %temporarily_rejected_count_by_ip;