Harden plaintext authenticator
[exim.git] / src / src / eximstats.src
index 88eb69fe83b3d849b59d3c965cd5139704340f28..5e1a0847b58fe8424c630c03e5f298da87a89c8e 100644 (file)
@@ -1,7 +1,6 @@
-#!PERL_COMMAND -w
-# $Cambridge: exim/src/src/eximstats.src,v 1.13 2007/01/31 16:52:12 ph10 Exp $
+#!PERL_COMMAND
 
-# Copyright (c) 2001 University of Cambridge.
+# Copyright (c) 2001-2017 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.
@@ -74,7 +73,7 @@
 # 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-01-31  V1.53 Philip Hazel
 #             Allow for [pid] after date in log lines
 #
+# 2007-02-14  V1.54 Daniel Tiefnig
+#             Improved the '($parent) =' pattern match.
+#
+# 2007-03-19  V1.55 Steve Campbell
+#             Differentiate between permanent and temporary rejects.
+#
+# 2007-03-29  V1.56 Jez Hancock
+#             Fixed some broken HTML links and added missing column headers.
+#
+# 2007-03-30  V1.57 Steve Campbell
+#             Fixed Grand Total Summary Domains, Edomains, and Email columns
+#             for Rejects, Temp Rejects, Ham, and Spam rows.
+#
+# 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
@@ -367,7 +390,7 @@ Useful for finding out which of your mailing lists are receiving mail.
 
 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
@@ -510,7 +533,7 @@ about how to create charts from the tables.
 
 =head1 AUTHOR
 
-There is a web site at http://www.exim.org - this contains details of the
+There is a website at https://www.exim.org - this contains details of the
 mailing list exim-users@exim.org.
 
 =head1 TO DO
@@ -518,19 +541,29 @@ mailing list exim-users@exim.org.
 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;
+use File::Basename;
 
 # use Time::Local;  # PH/FANF
 use POSIX;
 
+if (@ARGV and $ARGV[0] eq '--version') {
+    print basename($0) . ": $0\n",
+        "build: EXIM_RELEASE_VERSIONEXIM_VARIANT_VERSION\n",
+        "perl(runtime): $]\n";
+        exit 0;
+}
+
 use vars qw($HAVE_GD_Graph_pie $HAVE_GD_Graph_linespoints $HAVE_Spreadsheet_WriteExcel);
 eval { require GD::Graph::pie; };
 $HAVE_GD_Graph_pie = $@ ? 0 : 1;
@@ -561,7 +594,7 @@ use vars qw($WEEK $DAY $HOUR $MINUTE);
 
 @days_per_month = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
 $gig     = 1024 * 1024 * 1024;
-$VERSION = '1.53';
+$VERSION = '1.61';
 
 # How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
 $COLUMN_WIDTHS = 8;
@@ -575,9 +608,9 @@ $WEEK   =  7 * $DAY;
 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?
@@ -589,8 +622,9 @@ use vars qw($spam_score $spam_score_gigs);
 use vars qw($ham_score  $ham_score_gigs);
 use vars qw(%ham_count_by_ip %spam_count_by_ip);
 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);
@@ -604,7 +638,7 @@ $ntopchart = 5;
 # 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);
@@ -623,6 +657,7 @@ use vars qw(%received_count       %received_data       %received_data_gigs);
 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);
@@ -732,8 +767,8 @@ sub volume_rounded {
   }
   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);
@@ -846,10 +881,10 @@ $p;
 # 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');
@@ -869,6 +904,7 @@ sub unformat_time {
 # 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
@@ -892,7 +928,7 @@ sub seconds {
   # 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;
@@ -903,8 +939,8 @@ sub seconds {
   }
   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;
@@ -912,7 +948,7 @@ sub seconds {
   }
 
 
-  if (defined $7) {
+  if (defined $8) {
     #$time -= $this_offset;
     $time -= $offset_seconds;
   } elsif (defined $localtime_offset) {
@@ -1346,7 +1382,8 @@ sub print_league_table {
   # Generate the printf formats and table headers.
   ################################################
   my(@headers) = ('Messages');
-  push(@headers,'Addresses') if defined $a_count;
+  #push(@headers,'Addresses') if defined $a_count;
+  push(@headers,'Addresses') if defined $a_count && %$a_count;
   push(@headers,'Bytes','Average') if defined $m_data;
 
   my $txt_format = "%10s " x @headers . "  %s\n";
@@ -1368,7 +1405,7 @@ sub print_league_table {
 <tr><td>
 <table border=1>
 EoText
-    print $htm_col_headers;
+  print $htm_fh $htm_col_headers
   }
 
   if ($xls_fh) {
@@ -1467,7 +1504,7 @@ EoText
 <tr><td>
 <table border=1>
 EoText
-    print $htm_col_headers;
+    print $htm_fh $htm_col_headers;
     }
     if ($xls_fh) {
       $spreadsheet->write(${$row_sref}++, 0, $title, $f_header2);
@@ -1624,7 +1661,7 @@ sub top_n_sort {
 
     # 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};
@@ -1768,6 +1805,7 @@ Valid options are:
 -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
@@ -1813,7 +1851,7 @@ sub generate_parser {
   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>) {
 
@@ -1824,12 +1862,23 @@ sub generate_parser {
 
     $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);
     }
@@ -1837,9 +1886,15 @@ sub generate_parser {
       $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);
     }
 
@@ -1899,13 +1954,23 @@ sub generate_parser {
     # "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");
@@ -1927,16 +1992,25 @@ sub generate_parser {
     #ENDIF ($do_sender{Email})
 
     #IFDEF ($do_sender{Edomain})
+      if (/^(<>|blackhole)/) {
+        $edomain = $1;
+      }
       #IFDEF ($include_original_destination)
-      #$edomain = (/^(\S+) (<\S*?\\@(\S+)>)?/) ? $3 || $1 : "";
-      $edomain = (/^(\S+ (<\S*?\\@(\S+?)>)?)/) ? $1 : "";
-      chomp($edomain);
-      lc($edomain);
+        elsif (/^(\S+ (<\S*?\\@(\S+?)>)?)/) {
+          $edomain = $1;
+          chomp($edomain);
+          $edomain =~ s/@(\S+?)>/"@" . lc($1) . ">"/e;
+        }
       #ENDIF ($include_original_destination)
-
       #IFNDEF ($include_original_destination)
-      $edomain = (/^\S*?\\@(\S+)/) ? lc($1) : "";
+        elsif (/^\S*?\\@(\S+)/) {
+          $edomain = lc($1);
+        }
       #ENDIF ($include_original_destination)
+      else {
+        $edomain = "";
+      }
+
     #ENDIF ($do_sender{Edomain})
 
     if ($tod lt $begin) {
@@ -2076,8 +2150,19 @@ sub generate_parser {
             #IFNDEF ($include_original_destination)
             if ($user =~ /^[\\/|]/) {
             #ENDIF ($include_original_destination)
-              my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
-              $user = "$user $parent" if defined $parent;
+              #my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
+              my($parent) = $_ =~ / (<.+?>) /;              #DT 1.54
+              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};
@@ -2164,7 +2249,8 @@ sub generate_parser {
             #IFNDEF ($include_original_destination)
             if ($user =~ /^[\\/|]/) {
             #ENDIF ($include_original_destination)
-              my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
+              #my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
+              my($parent) = $_ =~ / (<.+?>) /;              #DT 1.54
               $user = "$user $parent" if defined $parent;
             }
             ++$delivered_addresses_user{$user};
@@ -2285,9 +2371,21 @@ sub generate_parser {
       # rejected EHLO from my.test.net [10.0.0.5]: syntactically invalid argument(s):
       # rejected EHLO from [10.0.0.6]: syntactically invalid argument(s):
       $ip = $1 if ($ip eq "local" && /^rejected [HE][HE]LO from .*?(\[.+?\]):/);
-      ++$rejected_count_by_ip{$ip};
       if (/SpamAssassin/) {
         ++$rejected_count_by_reason{"Rejected by SpamAssassin"};
+        ++$rejected_count_by_ip{$ip};
+      }
+      elsif (
+        /(temporarily rejected [A-Z]*) .*?(: .*?)(:|\s*$)/
+        ) {
+        ++$temporarily_rejected_count_by_reason{"\u$1$2"};
+        ++$temporarily_rejected_count_by_ip{$ip};
+      }
+      elsif (
+        /(temporarily refused connection)/
+        ) {
+        ++$temporarily_rejected_count_by_reason{"\u$1"};
+        ++$temporarily_rejected_count_by_ip{$ip};
       }
       elsif (
         /(listed at [^ ]+)/ ||
@@ -2299,6 +2397,7 @@ sub generate_parser {
         # 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)/ ||
@@ -2314,18 +2413,20 @@ sub generate_parser {
         # local_scan() function crashed with signal %d - message temporarily rejected
         # local_scan() function timed out - message temporarily rejected
         /(local_scan.. function .* - message temporarily rejected)/ ||
-        /(temporarily refused connection)/ ||
         # SMTP protocol synchronization error (input sent without waiting for greeting): rejected connection from %s
         /(SMTP protocol .*?(error|violation))/ ||
         /(message too big)/
         ) {
         ++$rejected_count_by_reason{"\u$1"};
+        ++$rejected_count_by_ip{$ip};
       }
       elsif (/rejected [HE][HE]LO from [^:]*: syntactically invalid argument/) {
         ++$rejected_count_by_reason{"Rejected HELO/EHLO: syntactically invalid argument"};
+        ++$rejected_count_by_ip{$ip};
       }
       elsif (/response to "RCPT TO.*? was: (.*)/) {
         ++$rejected_count_by_reason{"Response to RCPT TO was: $1"};
+        ++$rejected_count_by_ip{$ip};
       }
       elsif (
         /(lookup of host )\S+ (failed)/ ||
@@ -2353,9 +2454,19 @@ sub generate_parser {
         /: (Connection refused)()/
         ) {
         ++$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 STDERR "Unknown rejection: $_" if $debug;
       }
     }
@@ -2432,9 +2543,9 @@ sub print_header {
   if ($htm_fh) {
     print $htm_fh html_header($title);
     print $htm_fh "<ul>\n";
-    print $htm_fh "<li><a href=\"#grandtotal\">Grand total summary</a>\n";
-    print $htm_fh "<li><a href=\"#patterns\">User Specified Patterns</a>\n" if @user_patterns;
-    print $htm_fh "<li><a href=\"#transport\">Deliveries by Transport</a>\n" if $show_transport;
+    print $htm_fh "<li><a href=\"#Grandtotal\">Grand total summary</a>\n";
+    print $htm_fh "<li><a href=\"#Patterns\">User Specified Patterns</a>\n" if @user_patterns;
+    print $htm_fh "<li><a href=\"#Transport\">Deliveries by Transport</a>\n" if $show_transport;
     if ($hist_opt) {
       print $htm_fh "<li><a href=\"#Messages received\">Messages received per hour</a>\n";
       print $htm_fh "<li><a href=\"#Deliveries\">Deliveries per hour</a>\n";
@@ -2456,28 +2567,33 @@ sub print_header {
 
     print $htm_fh "<li><a href=\"#Relayed messages\">Relayed messages</a>\n" if $show_relay;
     if ($topcount) {
-      print $htm_fh "<li><a href=\"#mail rejection reason count\">Top $topcount mail rejection reasons by message count</a>\n" if %rejected_count_by_reason;
+      print $htm_fh "<li><a href=\"#Mail rejection reason count\">Top $topcount mail rejection reasons by message count</a>\n" if %rejected_count_by_reason;
       foreach ('Host','Domain','Email','Edomain') {
         next unless $do_sender{$_};
-        print $htm_fh "<li><a href=\"#sending \l$_ count\">Top $topcount sending \l${_}s by message count</a>\n";
-        print $htm_fh "<li><a href=\"#sending \l$_ volume\">Top $topcount sending \l${_}s by volume</a>\n";
+        print $htm_fh "<li><a href=\"#Sending \l$_ count\">Top $topcount sending \l${_}s by message count</a>\n";
+        print $htm_fh "<li><a href=\"#Sending \l$_ volume\">Top $topcount sending \l${_}s by volume</a>\n";
       }
       if (($local_league_table || $include_remote_users) && %received_count_user) {
-        print $htm_fh "<li><a href=\"#local sender count\">Top $topcount local senders by message count</a>\n";
-        print $htm_fh "<li><a href=\"#local sender volume\">Top $topcount local senders by volume</a>\n";
+        print $htm_fh "<li><a href=\"#Local sender count\">Top $topcount local senders by message count</a>\n";
+        print $htm_fh "<li><a href=\"#Local sender volume\">Top $topcount local senders by volume</a>\n";
       }
       foreach ('Host','Domain','Email','Edomain') {
         next unless $do_sender{$_};
-        print $htm_fh "<li><a href=\"#\l$_ destination count\">Top $topcount \l$_ destinations by message count</a>\n";
-        print $htm_fh "<li><a href=\"#\l$_ destination volume\">Top $topcount \l$_ destinations by volume</a>\n";
+        print $htm_fh "<li><a href=\"#$_ destination count\">Top $topcount \l$_ destinations by message count</a>\n";
+        print $htm_fh "<li><a href=\"#$_ destination volume\">Top $topcount \l$_ destinations by volume</a>\n";
       }
       if (($local_league_table || $include_remote_users) && %delivered_messages_user) {
-        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";
+        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=\"#non-rejected spamming ip count\">Top $topcount non-rejected spamming ips by message count</a>\n" if %spam_count_by_ip;
+      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;
+      print $htm_fh "<li><a href=\"#Non-rejected spamming ip count\">Top $topcount non-rejected spamming ips by message count</a>\n" if %spam_count_by_ip;
 
     }
     print $htm_fh "<li><a href=\"#errors\">List of errors</a>\n" if %errors_count;
@@ -2519,8 +2635,8 @@ sub print_grandtotals {
       push(@delivered_totals,scalar(keys %{$delivered_data{$_}}));
     }
     $sender_txt_header  .= " " x ($COLUMN_WIDTHS - length($_)) . $_ . 's';
-    $sender_html_format .= "<td align=\"right\">%d</td>";
-    $sender_txt_format  .= " " x ($COLUMN_WIDTHS - 5) . "%6d";
+    $sender_html_format .= "<td align=\"right\">%s</td>";
+    $sender_txt_format  .= " " x ($COLUMN_WIDTHS - 5) . "%6s";
     push(@col_headers,"${_}s");
   }
 
@@ -2538,7 +2654,7 @@ sub print_grandtotals {
     print $txt_fh "  TOTAL               Volume   Messages Addresses $sender_txt_header      Delayed       Failed\n";
   }
   if ($htm_fh) {
-    print $htm_fh "<a name=\"grandtotal\"></a>\n";
+    print $htm_fh "<a name=\"Grandtotal\"></a>\n";
     print $htm_fh "<h2>Grand total summary</h2>\n";
     print $htm_fh "<table border=1>\n";
     print $htm_fh "<tr><th>" . join('</th><th>',@col_headers) . "</th><th colspan=2>At least one addr<br>Delayed</th><th colspan=2>At least one addr<br>Failed</th>\n";
@@ -2612,12 +2728,17 @@ sub print_grandtotals {
   }
 
   if ($merge_reports) {
-    foreach ('Rejects', 'Ham', 'Spam') {
+    foreach ('Rejects', 'Temp Rejects', 'Ham', 'Spam') {
       my $messages = get_report_total($report_totals{$_},'Messages');
       my $addresses = get_report_total($report_totals{$_},'Addresses');
       if ($messages) {
         @content = ($_, '', $messages, '');
         push(@content,get_report_total($report_totals{$_},'Hosts')) if $do_sender{Host};
+        #These rows do not have entries for the following columns (if specified)
+        foreach ('Domain','Email','Edomain') {
+          push(@content,'') if $do_sender{$_};
+        }
+
         printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
         printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
         $ws_global->write(++$row, 0, \@content) if $xls_fh;
@@ -2626,15 +2747,24 @@ sub print_grandtotals {
   }
   else {
     foreach my $total_aref (['Rejects',\%rejected_count_by_ip],
+                            ['Temp Rejects',\%temporarily_rejected_count_by_ip],
                             ['Ham',\%ham_count_by_ip],
                             ['Spam',\%spam_count_by_ip]) {
+      #Count the number of messages of this type.
       my $messages = 0;
       map {$messages += $_} values %{$total_aref->[1]};
 
       if ($messages > 0) {
         @content = ($total_aref->[0], '', $messages, '');
+
+        #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)
+        foreach ('Domain','Email','Edomain') {
+          push(@content,'') if $do_sender{$_};
+        }
+
         printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
         printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
         $ws_global->write(++$row, 0, \@content) if $xls_fh;
@@ -2665,7 +2795,7 @@ sub print_user_patterns {
     print $txt_fh "\n                       Total\n";
   }
   if ($htm_fh) {
-    print $htm_fh "<hr><a name=\"patterns\"></a><h2>User Specified Patterns</h2>\n";
+    print $htm_fh "<hr><a name=\"Patterns\"></a><h2>User Specified Patterns</h2>\n";
     print $htm_fh "<table border=0 width=\"100%\">\n";
     print $htm_fh "<tr><td>\n";
     print $htm_fh "<table border=1>\n";
@@ -2812,7 +2942,7 @@ sub print_transport {
     print $txt_fh "\n                      Volume    Messages\n";
   }
   if ($htm_fh) {
-    print $htm_fh "<hr><a name=\"transport\"></a><h2>Deliveries by Transport</h2>\n";
+    print $htm_fh "<hr><a name=\"Transport\"></a><h2>Deliveries by Transport</h2>\n";
     print $htm_fh "<table border=0 width=\"100%\"><tr><td><table border=1>\n";
     print $htm_fh "<tr><th>&nbsp;</th><th>Volume</th><th>Messages</th>\n";
   }
@@ -3138,7 +3268,7 @@ sub parse_old_eximstat_reports {
             add_to_totals($report_totals{Delivered},['Addresses'],$tmp{Messages});
           }
         }
-        elsif (/(Rejects|Ham|Spam)\s+(.*?)\s*$/) {
+        elsif (/(Temp Rejects|Rejects|Ham|Spam)\s+(.*?)\s*$/) {
           print STDERR "Parsing $_" if $debug;
           add_to_totals($report_totals{$1},['Messages','Hosts'],$2);
         }
@@ -3258,8 +3388,8 @@ sub parse_old_eximstat_reports {
         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');
@@ -3350,6 +3480,12 @@ sub parse_old_eximstat_reports {
         $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
@@ -3358,12 +3494,18 @@ sub parse_old_eximstat_reports {
         $data_href       = \%{$delivered_data{"\u$1"}};
         $data_gigs_href  = \%{$delivered_data_gigs{"\u$1"}};
       }
+      elsif ($category =~ /temporarily rejected ips/) {
+        $messages_href      = \%temporarily_rejected_count_by_ip;
+      }
       elsif ($category =~ /rejected ips/) {
         $messages_href      = \%rejected_count_by_ip;
       }
       elsif ($category =~ /non-rejected spamming ips/) {
         $messages_href      = \%spam_count_by_ip;
       }
+      elsif ($category =~ /mail temporary rejection reasons/) {
+        $messages_href      = \%temporarily_rejected_count_by_reason;
+      }
       elsif ($category =~ /mail rejection reasons/) {
         $messages_href      = \%rejected_count_by_reason;
       }
@@ -3562,7 +3704,7 @@ sub update_relayed {
 #
 #  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
@@ -3592,7 +3734,7 @@ sub add_to_totals {
 #
 #  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 {
@@ -3658,7 +3800,7 @@ sub html2txt {
 # 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.
 #######################################################################
@@ -3700,7 +3842,7 @@ sub set_worksheet_line {
 #######################################################################
 # @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.
@@ -3809,6 +3951,7 @@ while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-') {
   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) }
@@ -3959,7 +4102,7 @@ $message_errors = 0;
 $begin = "9999-99-99 99:99:99";
 $end = "0000-00-00 00:00:00";
 my($section,$type);
-foreach $section ('Received','Delivered','Rejects','Ham','Spam') {
+foreach $section ('Received','Delivered','Temp Rejects', 'Rejects','Ham','Spam') {
   foreach $type ('Volume','Messages','Delayed','Failed','Hosts','Domains','Emails','Edomains') {
     $report_totals{$section}{$type} = 0;
   }
@@ -4051,14 +4194,16 @@ print_relay() if $show_relay;
 
 # Print the league tables, if topcount isn't zero.
 if ($topcount > 0) {
-  my($ws_rej, $ws_top50, $ws_rej_row, $ws_top50_row);
-  $ws_rej_row = $ws_top50_row = 0;
+  my($ws_rej, $ws_top50, $ws_rej_row, $ws_top50_row, $ws_temp_rej, $ws_temp_rej_row);
+  $ws_rej_row = $ws_temp_rej_row = $ws_top50_row = 0;
   if ($xls_fh) {
     $ws_top50 = $workbook->addworksheet('Deliveries');
     $ws_rej = $workbook->addworksheet('Rejections') if (%rejected_count_by_reason || %rejected_count_by_ip || %spam_count_by_ip);
+    $ws_temp_rej = $workbook->addworksheet('Temporary Rejections') if (%temporarily_rejected_count_by_reason || %temporarily_rejected_count_by_ip);
   }
 
   print_league_table("mail rejection reason", \%rejected_count_by_reason, undef, undef, undef, $ws_rej, \$ws_rej_row) if %rejected_count_by_reason;
+  print_league_table("mail temporary rejection reason", \%temporarily_rejected_count_by_reason, undef, undef, undef, $ws_temp_rej, \$ws_temp_rej_row) if %temporarily_rejected_count_by_reason;
 
   foreach ('Host','Domain','Email','Edomain') {
     next unless $do_sender{$_};
@@ -4072,8 +4217,10 @@ if ($topcount > 0) {
     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;
   print_league_table("non-rejected spamming ip", \%spam_count_by_ip, undef, undef, undef, $ws_rej, \$ws_rej_row) if %spam_count_by_ip;
 
 }