Grammar changes in docs
[exim.git] / src / src / eximstats.src
index c39ecd5e7c7b72c4f91b285f14b1fab3b7ec4659..5e1a0847b58fe8424c630c03e5f298da87a89c8e 100644 (file)
@@ -1,7 +1,6 @@
-#!PERL_COMMAND -w
-# $Cambridge: exim/src/src/eximstats.src,v 1.7 2005/06/03 14:28:50 steve 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.
 # 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
 # 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)'
 #
 #             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.
 #             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.
 #
 # 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
 #             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
 #             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
 #             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.
 #             Allow any combination of xls, txt, and html output.
 #             Fixed display of large numbers with -nvr option
 #             Fixed merging of reports with empty tables.
 #             Added the -include_original_destination flag
 #             Removed tabs and trailing whitespace.
 #
 #             Added the -include_original_destination flag
 #             Removed tabs and trailing whitespace.
 #
+# 2005-06-03  V1.40 Steve Campbell
+#             Whilst parsing the mainlog(s), store information about
+#             the messages in a hash of arrays rather than using
+#             individual hashes. This is a bit cleaner and results in
+#             dramatic memory savings, albeit at a slight CPU cost.
+#
+# 2005-06-15  V1.41 Steve Campbell
+#             Added the -show_rt<list> flag.
+#             Added the -show_dt<list> flag.
+#
+# 2005-06-24  V1.42 Steve Campbell
+#             Added Histograms for user specified patterns.
+#
+# 2005-06-30  V1.43 Steve Campbell
+#             Bug fix for V1.42 with -h0 specified. Spotted by Chris Lear.
+#
+# 2005-07-26  V1.44 Steve Campbell
+#             Use a glob alias rather than an array ref in the generated
+#             parser. This improves both readability and performance.
+#
+# 2005-09-30  V1.45 Marco Gaiarin / Steve Campbell
+#             Collect SpamAssassin and rejection statistics.
+#             Don't display local sender or destination tables unless
+#             there is data to show.
+#             Added average volumes into the top table text output.
+#
+# 2006-02-07  V1.46 Steve Campbell
+#             Collect data on the number of addresses (recipients)
+#             as well as the number of messages.
+#
+# 2006-05-05  V1.47 Steve Campbell
+#             Added 'Message too big' to the list of mail rejection
+#             reasons (thanks to Marco Gaiarin).
+#
+# 2006-06-05  V1.48 Steve Campbell
+#             Mainlog lines which have GMT offsets and are too short to
+#             have a flag are now skipped.
+#
+# 2006-11-10  V1.49 Alain Williams
+#             Added the -emptyok flag.
+#
+# 2006-11-16  V1.50 Steve Campbell
+#             Fixes for obtaining the IP address from reject messages.
+#
+# 2006-11-27  V1.51 Steve Campbell
+#             Another update for obtaining the IP address from reject messages.
+#
+# 2006-11-27  V1.52 Steve Campbell
+#             Tally any reject message containing SpamAssassin.
+#
+# 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
 #
 #
 # For documentation on the logfile format, see
@@ -310,6 +386,30 @@ Include the original destination email addresses rather than just
 using the final ones.
 Useful for finding out which of your mailing lists are receiving mail.
 
 using the final ones.
 Useful for finding out which of your mailing lists are receiving mail.
 
+=item B<-show_dt>I<list>
+
+Show the delivery times (B<DT>)for all the messages.
+
+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
+the number of messages with delivery times under 1 second, 2 seconds, 4 seconds,
+8 seconds, and over 8 seconds.
+
+=item B<-show_rt>I<list>
+
+Show the receipt times for all the messages. The receipt time is
+defined as the Completed hh:mm:ss - queue_time_overall - the Receipt hh:mm:ss.
+These figures will be skewed by pipelined messages so might not be that useful.
+
+Exim must have been configured to use the +queue_time_overall logging option
+for this option to work.
+
+I<list> is an optional list of times. Eg -show_rt1,2,4,8 will show
+the number of messages with receipt times under 1 second, 2 seconds, 4 seconds,
+8 seconds, and over 8 seconds.
+
 =item B<-byhost>
 
 Show results by sending host. This may be combined with
 =item B<-byhost>
 
 Show results by sending host. This may be combined with
@@ -410,6 +510,11 @@ Create the charts in the directory <dir>
 Specify the relative directory for the "img src=" tags from where to include
 the charts
 
 Specify the relative directory for the "img src=" tags from where to include
 the charts
 
+=item B<-emptyok>
+
+Specify that it's OK to not find any valid log lines. Without this
+we will output an error message if we don't find any.
+
 =item B<-d>
 
 Debug flag. This outputs the eval()'d parser onto STDOUT which makes it
 =item B<-d>
 
 Debug flag. This outputs the eval()'d parser onto STDOUT which makes it
@@ -428,7 +533,7 @@ about how to create charts from the tables.
 
 =head1 AUTHOR
 
 
 =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
 mailing list exim-users@exim.org.
 
 =head1 TO DO
@@ -436,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
 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
 
 
 Merging of xls files is not (yet) possible. Be free to implement :)
 
 =cut
 
+use warnings;
 use integer;
 use integer;
+BEGIN { pop @INC if $INC[-1] eq '.' };
 use strict;
 use IO::File;
 use strict;
 use IO::File;
+use File::Basename;
 
 # use Time::Local;  # PH/FANF
 use POSIX;
 
 
 # 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;
 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;
@@ -465,6 +580,7 @@ $HAVE_Spreadsheet_WriteExcel = $@ ? 0 : 1;
 use vars qw(@tab62 @days_per_month $gig);
 use vars qw($VERSION);
 use vars qw($COLUMN_WIDTHS);
 use vars qw(@tab62 @days_per_month $gig);
 use vars qw($VERSION);
 use vars qw($COLUMN_WIDTHS);
+use vars qw($WEEK $DAY $HOUR $MINUTE);
 
 
 @tab62 =
 
 
 @tab62 =
@@ -478,28 +594,41 @@ use vars qw($COLUMN_WIDTHS);
 
 @days_per_month = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
 $gig     = 1024 * 1024 * 1024;
 
 @days_per_month = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
 $gig     = 1024 * 1024 * 1024;
-$VERSION = '1.39';
+$VERSION = '1.61';
 
 # How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
 $COLUMN_WIDTHS = 8;
 
 
 # How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
 $COLUMN_WIDTHS = 8;
 
+$MINUTE = 60;
+$HOUR   = 60 * $MINUTE;
+$DAY    = 24 * $HOUR;
+$WEEK   =  7 * $DAY;
+
 # Declare global variables.
 use vars qw($total_received_data  $total_received_data_gigs  $total_received_count);
 # Declare global variables.
 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_count);
-use vars qw(%arrival_time %size %from_host %from_address);
+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(%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($ntopchart);                        #How many entries should make it into the chart?
 use vars qw($gddirectory);                      #Where to put files from GD::Graph
 use vars qw($localtime_offset);
 use vars qw($i);                                #General loop counter.
 use vars qw($debug);                            #Debug mode?
 use vars qw($ntopchart);                        #How many entries should make it into the chart?
 use vars qw($gddirectory);                      #Where to put files from GD::Graph
-use vars qw($workbook $ws_global $ws_relayed $ws_top50 $ws_errors );   #For use in Speadsheed::WriteExcel
-use vars qw($row $col $row_hist $col_hist $row_league_table);
+
+# SpamAssassin variables
+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 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);
 use vars qw($run_hist);
-use vars qw($f_default $f_header1 $f_header2 $f_headertab $f_percent); #Format Header
+use vars qw($f_default $f_header1 $f_header2 $f_header2_m $f_headertab $f_percent); #Format Header
 
 # Output FileHandles
 use vars qw($txt_fh $htm_fh $xls_fh);
 
 # Output FileHandles
 use vars qw($txt_fh $htm_fh $xls_fh);
@@ -509,9 +638,10 @@ $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);
 # 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($hist_opt $hist_interval $hist_number $volume_rounding);
+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($relay_pattern @queue_times @user_patterns @user_descriptions);
+use vars qw(@rcpt_times @delivery_times);
 use vars qw($include_original_destination);
 use vars qw($txt_fh $htm_fh $xls_fh);
 
 use vars qw($include_original_destination);
 use vars qw($txt_fh $htm_fh $xls_fh);
 
@@ -521,18 +651,35 @@ use vars qw($merge_reports);            #Merge old reports ?
 
 # The following are modified in the parse() routine, and
 # referred to in the print_*() routines.
 
 # The following are modified in the parse() routine, and
 # referred to in the print_*() routines.
-use vars qw($queue_more_than $delayed_count $relayed_unshown $begin $end);
+use vars qw($delayed_count $relayed_unshown $begin $end);
+use vars qw(%messages @message);
 use vars qw(%received_count       %received_data       %received_data_gigs);
 use vars qw(%received_count       %received_data       %received_data_gigs);
-use vars qw(%delivered_count      %delivered_data      %delivered_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(%received_count_user  %received_data_user  %received_data_gigs_user);
-use vars qw(%delivered_count_user %delivered_data_user %delivered_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(%transported_count    %transported_data    %transported_data_gigs);
-use vars qw(%remote_delivered %relayed %delayed %had_error %errors_count);
-use vars qw(@queue_bin @remote_queue_bin @received_interval_count @delivered_interval_count);
-use vars qw(@user_pattern_totals);
+use vars qw(%relayed %errors_count $message_errors);
+use vars qw(@qt_all_bin @qt_remote_bin);
+use vars qw($qt_all_overflow $qt_remote_overflow);
+use vars qw(@dt_all_bin @dt_remote_bin %rcpt_times_bin);
+use vars qw($dt_all_overflow $dt_remote_overflow %rcpt_times_overflow);
+use vars qw(@received_interval_count @delivered_interval_count);
+use vars qw(@user_pattern_totals @user_pattern_interval_count);
 
 use vars qw(%report_totals);
 
 
 use vars qw(%report_totals);
 
+# Enumerations
+use vars qw($SIZE $FROM_HOST $FROM_ADDRESS $ARRIVAL_TIME $REMOTE_DELIVERED $PROTOCOL);
+use vars qw($DELAYED $HAD_ERROR);
+$SIZE             = 0;
+$FROM_HOST        = 1;
+$FROM_ADDRESS     = 2;
+$ARRIVAL_TIME     = 3;
+$REMOTE_DELIVERED = 4;
+$DELAYED          = 5;
+$HAD_ERROR        = 6;
+$PROTOCOL         = 7;
 
 
 
 
 
 
@@ -620,8 +767,8 @@ sub volume_rounded {
   }
   else {
     # We don't want any rounding to be done.
   }
   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);
     # 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);
@@ -734,10 +881,10 @@ $p;
 # Eg 3h20m5s => 12005
 #######################################################################
 sub unformat_time {
 # Eg 3h20m5s => 12005
 #######################################################################
 sub unformat_time {
-  my($formated_time) = pop @_;
+  my($formatted_time) = pop @_;
   my $time = 0;
 
   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');
     $time +=  $1 if ($2 eq '' || $2 eq 's');
     $time +=  $1 * 60 if ($2 eq 'm');
     $time +=  $1 * 60 * 60 if ($2 eq 'h');
@@ -757,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
 # 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
 # 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
@@ -780,7 +928,7 @@ sub seconds {
   # Is the timestamp the same as the last one?
   return $last_time if ($last_timestamp eq $timestamp);
 
   # 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;
 
   unless ($last_date eq $1) {
     $last_date = $1;
@@ -791,8 +939,8 @@ sub seconds {
   }
   my $time = $date_seconds + ($5 * 3600) + ($6 * 60) + $7;
 
   }
   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 $8 && ($8 ne $last_offset)) {
     $last_offset = $8;
     $offset_seconds = ($10 * 60 + $11) * 60;
@@ -800,7 +948,7 @@ sub seconds {
   }
 
 
   }
 
 
-  if (defined $7) {
+  if (defined $8) {
     #$time -= $this_offset;
     $time -= $offset_seconds;
   } elsif (defined $localtime_offset) {
     #$time -= $this_offset;
     $time -= $offset_seconds;
   } elsif (defined $localtime_offset) {
@@ -830,6 +978,44 @@ while($#c >= 0) { $s = $s * 62 + $tab62[ord(shift @c) - ord('0')] }
 $s;
 }
 
 $s;
 }
 
+#######################################################################
+#  wdhms_seconds();
+#
+#  $seconds = wdhms_seconds($string);
+#
+# Convert a string in a week/day/hour/minute/second format (eg 4h10s)
+# into seconds.
+#######################################################################
+sub wdhms_seconds {
+  if ($_[0] =~ /^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/) {
+    return((($1||0) * $WEEK) + (($2||0) * $DAY) + (($3||0) * $HOUR) + (($4||0) * $MINUTE) + ($5||0));
+  }
+  return undef;
+}
+
+#######################################################################
+#  queue_time();
+#
+#  $queued = queue_time($completed_tod, $arrival_time, $id);
+#
+# Given the completed time of day and either the arrival time
+# (preferred), or the message ID, calculate how long the message has
+# been on the queue.
+#
+#######################################################################
+sub queue_time {
+  my($completed_tod, $arrival_time, $id) = @_;
+
+  # Note: id_seconds() benchmarks as 42% slower than seconds()
+  # and computing the time accounts for a significant portion of
+  # the run time.
+  if (defined $arrival_time) {
+    return(seconds($completed_tod) - seconds($arrival_time));
+  }
+  else {
+    return(seconds($completed_tod) - id_seconds($id));
+  }
+}
 
 
 #######################################################################
 
 
 #######################################################################
@@ -868,29 +1054,35 @@ sub calculate_localtime_offset {
 }
 
 
 }
 
 
+
 #######################################################################
 #######################################################################
-# print_queue_times();
+# print_duration_table();
 #
 #
-#  $time = print_queue_times($message_type,\@queue_times,$queue_more_than);
+#  print_duration_table($title, $message_type, \@times, \@values, $overflow);
 #
 #
-# Given the type of messages being output, the array of message queue times,
-# and the number of messages which exceeded the queue times, print out
-# a table.
+# Print a table showing how long a particular step took for
+# the messages. The parameters are:
+#   $title         Eg "Time spent on the queue"
+#   $message_type  Eg "Remote"
+#   \@times        The maximum time a message took for it to increment
+#                  the corresponding @values counter.
+#   \@values       An array of message counters.
+#   $overflow      The number of messages which exceeded the maximum
+#                  time.
 #######################################################################
 #######################################################################
-sub print_queue_times {
+sub print_duration_table {
 no integer;
 no integer;
-my($string,$array,$queue_more_than) = @_;
+my($title, $message_type, $times_aref, $values_aref, $overflow) = @_;
 my(@chartdatanames);
 my(@chartdatavals);
 
 my $printed_one = 0;
 my $cumulative_percent = 0;
 my(@chartdatanames);
 my(@chartdatavals);
 
 my $printed_one = 0;
 my $cumulative_percent = 0;
-#$queue_unknown += keys %arrival_time;
 
 
-my $queue_total = $queue_more_than;
-for ($i = 0; $i <= $#queue_times; $i++) { $queue_total += $$array[$i] }
+my $queue_total = $overflow;
+map {$queue_total += $_} @$values_aref;
 
 
-my $temp = "Time spent on the queue: $string";
+my $temp = "$title: $message_type";
 
 
 my $txt_format = "%5s %4s   %6d %5.1f%%  %5.1f%%\n";
 
 
 my $txt_format = "%5s %4s   %6d %5.1f%%  %5.1f%%\n";
@@ -899,40 +1091,36 @@ my $htm_format = "<tr><td align=\"right\">%s %s</td><td align=\"right\">%d</td><
 # write header
 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
 if ($htm_fh) {
 # write header
 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
 if ($htm_fh) {
-  print $htm_fh "<hr><a name=\"$string time\"></a><h2>$temp</h2>\n";
-  print $htm_fh "<table border=0 width=\"100%\">\n";
-  print $htm_fh "<tr><td>\n";
-  print $htm_fh "<table border=1>\n";
+  print $htm_fh "<hr><a name=\"$title $message_type\"></a><h2>$temp</h2>\n";
+  print $htm_fh "<table border=0 width=\"100%\"><tr><td><table border=1>\n";
   print $htm_fh "<tr><th>Time</th><th>Messages</th><th>Percentage</th><th>Cumulative Percentage</th>\n";
 }
   print $htm_fh "<tr><th>Time</th><th>Messages</th><th>Percentage</th><th>Cumulative Percentage</th>\n";
 }
-if ($xls_fh)
-{
-
-  $ws_global->write($row++, $col, "Time spent on the queue: ".$string, $f_header2);
+if ($xls_fh) {
+  $ws_global->write($row++, $col, "$title: ".$message_type, $f_header2);
   my @content=("Time", "Messages", "Percentage", "Cumulative Percentage");
   &set_worksheet_line($ws_global, $row++, 1, \@content, $f_headertab);
 }
 
 
   my @content=("Time", "Messages", "Percentage", "Cumulative Percentage");
   &set_worksheet_line($ws_global, $row++, 1, \@content, $f_headertab);
 }
 
 
-for ($i = 0; $i <= $#queue_times; $i++) {
-  if ($$array[$i] > 0)
+for ($i = 0; $i <= $#$times_aref; ++$i) {
+  if ($$values_aref[$i] > 0)
     {
     {
-    my $percent = ($$array[$i] * 100)/$queue_total;
+    my $percent = ($values_aref->[$i] * 100)/$queue_total;
     $cumulative_percent += $percent;
 
     my @content=($printed_one? "     " : "Under",
     $cumulative_percent += $percent;
 
     my @content=($printed_one? "     " : "Under",
-        format_time($queue_times[$i]),
-        $$array[$i], $percent, $cumulative_percent);
+        format_time($times_aref->[$i]),
+        $values_aref->[$i], $percent, $cumulative_percent);
 
     if ($htm_fh) {
       printf $htm_fh ($htm_format, @content);
 
     if ($htm_fh) {
       printf $htm_fh ($htm_format, @content);
-      if (!defined($queue_times[$i])) {
+      if (!defined($values_aref->[$i])) {
         print $htm_fh "Not defined";
       }
     }
     if ($txt_fh) {
       printf $txt_fh ($txt_format, @content);
         print $htm_fh "Not defined";
       }
     }
     if ($txt_fh) {
       printf $txt_fh ($txt_format, @content);
-      if (!defined($queue_times[$i])) {
+      if (!defined($times_aref->[$i])) {
         print $txt_fh "Not defined";
       }
     }
         print $txt_fh "Not defined";
       }
     }
@@ -942,25 +1130,25 @@ for ($i = 0; $i <= $#queue_times; $i++) {
       &set_worksheet_line($ws_global, $row, 0, [@content[0,1,2]], $f_default);
       &set_worksheet_line($ws_global, $row++, 3, [$content[3]/100,$content[4]/100], $f_percent);
 
       &set_worksheet_line($ws_global, $row, 0, [@content[0,1,2]], $f_default);
       &set_worksheet_line($ws_global, $row++, 3, [$content[3]/100,$content[4]/100], $f_percent);
 
-      if (!defined($queue_times[$i])) {
+      if (!defined($times_aref->[$i])) {
         $col=0;
         $ws_global->write($row++, $col, "Not defined"  );
       }
     }
 
     push(@chartdatanames,
         $col=0;
         $ws_global->write($row++, $col, "Not defined"  );
       }
     }
 
     push(@chartdatanames,
-      ($printed_one? "" : "Under") . format_time($queue_times[$i]));
-    push(@chartdatavals, $$array[$i]);
+      ($printed_one? "" : "Under") . format_time($times_aref->[$i]));
+    push(@chartdatavals, $$values_aref[$i]);
     $printed_one = 1;
   }
 }
 
     $printed_one = 1;
   }
 }
 
-if ($queue_more_than > 0) {
-  my $percent = ($queue_more_than * 100)/$queue_total;
+if ($overflow && $overflow > 0) {
+  my $percent = ($overflow * 100)/$queue_total;
   $cumulative_percent += $percent;
 
   $cumulative_percent += $percent;
 
-    my @content = ("Over ", format_time($queue_times[$#queue_times]),
-        $queue_more_than, $percent, $cumulative_percent);
+    my @content = ("Over ", format_time($times_aref->[-1]),
+        $overflow, $percent, $cumulative_percent);
 
     printf $txt_fh ($txt_format, @content) if $txt_fh;
     printf $htm_fh ($htm_format, @content) if $htm_fh;
 
     printf $txt_fh ($txt_format, @content) if $txt_fh;
     printf $htm_fh ($htm_format, @content) if $htm_fh;
@@ -972,27 +1160,25 @@ if ($queue_more_than > 0) {
 
 }
 
 
 }
 
-push(@chartdatanames, "Over " . format_time($queue_times[$#queue_times]));
-push(@chartdatavals, $queue_more_than);
+push(@chartdatanames, "Over " . format_time($times_aref->[-1]));
+push(@chartdatavals, $overflow);
 
 #printf("Unknown   %6d\n", $queue_unknown) if $queue_unknown > 0;
 if ($htm_fh) {
 
 #printf("Unknown   %6d\n", $queue_unknown) if $queue_unknown > 0;
 if ($htm_fh) {
-  print $htm_fh "</table>\n";
-  print $htm_fh "</td><td>\n";
+  print $htm_fh "</table></td><td>";
 
 
-  if ($HAVE_GD_Graph_pie && $charts) {
+  if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0)) {
     my @data = (
        \@chartdatanames,
        \@chartdatavals
     );
     my $graph = GD::Graph::pie->new(200, 200);
     my @data = (
        \@chartdatanames,
        \@chartdatavals
     );
     my $graph = GD::Graph::pie->new(200, 200);
-    my $pngname;
-    my $title;
-    if ($string =~ /all/) { $pngname = "queue_all.png"; $title = "Queue (all)"; }
-    if ($string =~ /remote/) { $pngname = "queue_rem.png"; $title = "Queue (remote)"; }
-    $graph->set(
-        title             => $title,
-    );
+    my $pngname = "$title-$message_type.png";
+    $pngname =~ s/[^\w\-\.]/_/;
+
+    my $graph_title = "$title ($message_type)";
+    $graph->set(title => $graph_title) if (length($graph_title) < 21);
+
     my $gd = $graph->plot(\@data) or warn($graph->error);
     if ($gd) {
       open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
     my $gd = $graph->plot(\@data) or warn($graph->error);
     if ($gd) {
       open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
@@ -1015,28 +1201,25 @@ print $htm_fh "\n" if $htm_fh;
 }
 
 
 }
 
 
-
 #######################################################################
 # print_histogram();
 #
 #######################################################################
 # print_histogram();
 #
-#  print_histogram('Deliverieds|Messages received',@interval_count);
+#  print_histogram('Deliveries|Messages received|$pattern', $unit, @interval_count);
 #
 # Print a histogram of the messages delivered/received per time slot
 # (hour by default).
 #######################################################################
 sub print_histogram {
 #
 # Print a histogram of the messages delivered/received per time slot
 # (hour by default).
 #######################################################################
 sub print_histogram {
-my($text) = shift;
-my(@interval_count) = @_;
+my($text, $unit, @interval_count) = @_;
 my(@chartdatanames);
 my(@chartdatavals);
 my($maxd) = 0;
 
 my(@chartdatanames);
 my(@chartdatavals);
 my($maxd) = 0;
 
-if (!$run_hist) # save first row of print_histogram for xls output
-{
+# save first row of print_histogram for xls output
+if (!$run_hist) {
   $row_hist = $row;
 }
   $row_hist = $row;
 }
-else
-{
+else {
   $row = $row_hist;
 }
 
   $row = $row_hist;
 }
 
@@ -1046,14 +1229,10 @@ for ($i = 0; $i < $hist_number; $i++)
 my $scale = int(($maxd + 25)/50);
 $scale = 1 if $scale == 0;
 
 my $scale = int(($maxd + 25)/50);
 $scale = 1 if $scale == 0;
 
-my($type);
-if ($text eq "Deliveries")
-{
-  $type = ($scale == 1)? "delivery" : "deliveries";
-}
-else
-{
-  $type = ($scale == 1)? "message" : "messages";
+if ($scale != 1) {
+  if ($unit !~ s/y$/ies/) {
+    $unit .= 's';
+  }
 }
 
 # make and output title
 }
 
 # make and output title
@@ -1061,7 +1240,7 @@ my $title = sprintf("$text per %s",
     ($hist_interval == 60)? "hour" :
     ($hist_interval == 1)?  "minute" : "$hist_interval minutes");
 
     ($hist_interval == 60)? "hour" :
     ($hist_interval == 1)?  "minute" : "$hist_interval minutes");
 
-my $txt_htm_title = $title . " (each dot is $scale $type)";
+my $txt_htm_title = $title . " (each dot is $scale $unit)";
 
 printf $txt_fh ("%s\n%s\n\n", $txt_htm_title, "-" x length($txt_htm_title)) if $txt_fh;
 
 
 printf $txt_fh ("%s\n%s\n\n", $txt_htm_title, "-" x length($txt_htm_title)) if $txt_fh;
 
@@ -1071,17 +1250,16 @@ if ($htm_fh) {
   print $htm_fh "<tr><td><pre>\n";
 }
 
   print $htm_fh "<tr><td><pre>\n";
 }
 
-if ($xls_fh)
-{
+if ($xls_fh) {
   $title =~ s/Messages/Msg/ ;
   $title =~ s/Messages/Msg/ ;
+  $row += 2;
   $ws_global->write($row++, $col_hist+1, $title, $f_headertab);
 }
 
 
 my $hour = 0;
 my $minutes = 0;
   $ws_global->write($row++, $col_hist+1, $title, $f_headertab);
 }
 
 
 my $hour = 0;
 my $minutes = 0;
-for ($i = 0; $i < $hist_number; $i++)
-{
+for ($i = 0; $i < $hist_number; $i++) {
   my $c = $interval_count[$i];
 
   # If the interval is an hour (the maximum) print the starting and
   my $c = $interval_count[$i];
 
   # If the interval is an hour (the maximum) print the starting and
@@ -1089,26 +1267,23 @@ for ($i = 0; $i < $hist_number; $i++)
   # minutes, which take up the same space.
 
   my $temp;
   # minutes, which take up the same space.
 
   my $temp;
-  if ($hist_opt == 1)
-  {
+  if ($hist_opt == 1) {
     $temp = sprintf("%02d-%02d", $hour, $hour + 1);
 
     print $txt_fh $temp if $txt_fh;
     print $htm_fh $temp if $htm_fh;
 
     $temp = sprintf("%02d-%02d", $hour, $hour + 1);
 
     print $txt_fh $temp if $txt_fh;
     print $htm_fh $temp if $htm_fh;
 
-    if ($xls_fh)
-    {
-      if ($run_hist==0) # only on first run
-      {
-        &set_worksheet_line($ws_global, $row, 0, [$temp], $f_default);
+    if ($xls_fh) {
+      if ($run_hist==0) {
+        # only on first run
+        $ws_global->write($row, 0, [$temp], $f_default);
       }
     }
 
     push(@chartdatanames, $temp);
     $hour++;
   }
       }
     }
 
     push(@chartdatanames, $temp);
     $hour++;
   }
-  else
-  {
+  else {
     if ($minutes == 0)
       { $temp = sprintf("%02d:%02d", $hour, $minutes) }
     else
     if ($minutes == 0)
       { $temp = sprintf("%02d:%02d", $hour, $minutes) }
     else
@@ -1116,28 +1291,24 @@ for ($i = 0; $i < $hist_number; $i++)
 
     print $txt_fh $temp if $txt_fh;
     print $htm_fh $temp if $htm_fh;
 
     print $txt_fh $temp if $txt_fh;
     print $htm_fh $temp if $htm_fh;
-    if (($xls_fh) and ($run_hist==0)) # only on first run
-    {
+    if (($xls_fh) and ($run_hist==0)) {
+      # only on first run
       $temp = sprintf("%02d:%02d", $hour, $minutes);
       $temp = sprintf("%02d:%02d", $hour, $minutes);
-      &set_worksheet_line($ws_global, $row, 0, [$temp], $f_default);
+      $ws_global->write($row, 0, [$temp], $f_default);
     }
 
     push(@chartdatanames, $temp);
     $minutes += $hist_interval;
     }
 
     push(@chartdatanames, $temp);
     $minutes += $hist_interval;
-    if ($minutes >= 60)
-      {
+    if ($minutes >= 60) {
       $minutes = 0;
       $hour++;
       $minutes = 0;
       $hour++;
-      }
+    }
   }
   push(@chartdatavals, $c);
 
   printf $txt_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $txt_fh;
   printf $htm_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $htm_fh;
   }
   push(@chartdatavals, $c);
 
   printf $txt_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $txt_fh;
   printf $htm_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $htm_fh;
-  if ($xls_fh)
-  {
-    &set_worksheet_line($ws_global, $row++, $col_hist+1, [$c], $f_default);
-  }
+  $ws_global->write($row++, $col_hist+1, [$c], $f_default) if $xls_fh;
 
 } #end for
 
 
 } #end for
 
@@ -1148,7 +1319,7 @@ if ($htm_fh)
 {
   print $htm_fh "</pre>\n";
   print $htm_fh "</td><td>\n";
 {
   print $htm_fh "</pre>\n";
   print $htm_fh "</td><td>\n";
-  if ($HAVE_GD_Graph_linespoints && $charts) {
+  if ($HAVE_GD_Graph_linespoints && $charts && ($#chartdatavals > 0)) {
     # calculate the graph
     my @data = (
        \@chartdatanames,
     # calculate the graph
     my @data = (
        \@chartdatanames,
@@ -1161,9 +1332,9 @@ if ($htm_fh)
         title             => $text,
         x_labels_vertical => 1
     );
         title             => $text,
         x_labels_vertical => 1
     );
-    my($pngname);
-    if ($text =~ /Deliveries/) { $pngname = "histogram_del.png"; }
-    if ($text =~ /Messages/)   { $pngname = "histogram_mes.png"; }
+    my $pngname = "histogram_$text.png";
+    $pngname =~ s/[^\w\._]/_/g;
+
     my $gd = $graph->plot(\@data) or warn($graph->error);
     if ($gd) {
       open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
     my $gd = $graph->plot(\@data) or warn($graph->error);
     if ($gd) {
       open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
@@ -1187,234 +1358,245 @@ $run_hist=1; # we have done this once or more
 #######################################################################
 # print_league_table();
 #
 #######################################################################
 # print_league_table();
 #
-#  print_league_table($league_table_type,\%message_count,\%message_data,\%message_data_gigs);
+#  print_league_table($league_table_type,\%message_count,\%address_count,\%message_data,\%message_data_gigs, $spreadsheet, $row_sref);
 #
 #
-# Given hashes of message count and message data, which are keyed by
-# the table type (eg by the sending host), print a league table
-# showing the top $topcount (defaults to 50).
+# Given hashes of message count, address count, and message data,
+# which are keyed by the table type (eg by the sending host), print a
+# league table showing the top $topcount (defaults to 50).
 #######################################################################
 sub print_league_table {
 #######################################################################
 sub print_league_table {
-my($text,$m_count,$m_data,$m_data_gigs) = @_;
-my($name) = ($topcount == 1)? "$text" : "$topcount ${text}s";
-my($temp) = "Top $name by message count";
-my(@chartdatanames) = ();
-my(@chartdatavals) = ();
-my $chartotherval = 0;
-
-my $htm_format;
-my $txt_format = "%7d %10s   %s\n";
-
-# write header
-printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
-if ($htm_fh) {
-  print $htm_fh "<hr><a name=\"$text count\"></a><h2>$temp</h2>\n";
-  print $htm_fh "<table border=0 width=\"100%\">\n";
-  print $htm_fh "<tr><td>\n";
-  print $htm_fh "<table border=1>\n";
-  print $htm_fh "<tr><th>Messages</th><th>Bytes</th><th>Average</th><th>\u$text</th>\n";
+  my($text,$m_count,$a_count,$m_data,$m_data_gigs,$spreadsheet, $row_sref) = @_;
+  my($name) = ($topcount == 1)? "$text" : "$topcount ${text}s";
+  my($title) = "Top $name by message count";
+  my(@chartdatanames) = ();
+  my(@chartdatavals) = ();
+  my $chartotherval = 0;
+  $text = ucfirst($text);
 
   # Align non-local addresses to the right (so all the .com's line up).
   # Local addresses are aligned on the left as they are userids.
   my $align = ($text !~ /local/i) ? 'right' : 'left';
 
   # Align non-local addresses to the right (so all the .com's line up).
   # Local addresses are aligned on the left as they are userids.
   my $align = ($text !~ /local/i) ? 'right' : 'left';
-  $htm_format = "<tr><td align=\"right\">%d</td><td align=\"right\">%s</td><td align=\"right\">%s</td><td align=\"$align\" nowrap>%s</td>\n";
-}
-if ($xls_fh)
-{
-  $ws_top50->write($row_league_table++, 0, $temp, $f_header2);
-  &set_worksheet_line($ws_top50, $row_league_table++, 0, ["Messages", "Bytes", "Average", $text], $f_headertab );
-}
 
 
 
 
-# write content
-my($key,$htmlkey,$rounded_volume,$rounded_average,$count,$data,$gigs);
-foreach $key (top_n_sort($topcount,$m_count,$m_data_gigs,$m_data)) {
+  ################################################
+  # Generate the printf formats and table headers.
+  ################################################
+  my(@headers) = ('Messages');
+  #push(@headers,'Addresses') if defined $a_count;
+  push(@headers,'Addresses') if defined $a_count && %$a_count;
+  push(@headers,'Bytes','Average') if defined $m_data;
 
 
-  # When displaying the average figures, we calculate the average of
-  # the rounded data, as the user would calculate it. This reduces
-  # the accuracy slightly, but we have to do it this way otherwise
-  # when using -merge to convert results from text to HTML and
-  # vice-versa discrepencies would occur.
-  $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
-  $data = $gigs = 0;
-  un_round($rounded_volume,\$data,\$gigs);
-  $count = $$m_count{$key};
-  $rounded_average = volume_rounded($data/$count,$gigs/$count);
-  my @content=( $count, $rounded_volume, $rounded_average);
+  my $txt_format = "%10s " x @headers . "  %s\n";
+  my $txt_col_headers = sprintf $txt_format, @headers, $text;
+  my $htm_format = "<tr>" . '<td align="right">%s</td>'x@headers . "<td align=\"$align\" nowrap>%s</td></tr>\n";
+  my $htm_col_headers = sprintf $htm_format, @headers, $text;
+  $htm_col_headers =~ s/(<\/?)td/$1th/g;      #Convert <td>'s to <th>'s for the header.
 
 
-  # write content
-  # any reason not to include rounded_average in txt-output? -fh
-  printf $txt_fh ($txt_format, $count, $rounded_volume, $key) if $txt_fh;
+
+  ################################################
+  # Write the table headers
+  ################################################
+  printf $txt_fh ("%s\n%s\n%s", $title, "-" x length($title),$txt_col_headers) if $txt_fh;
 
   if ($htm_fh) {
 
   if ($htm_fh) {
-    $htmlkey = $key;
-    $htmlkey =~ s/>/\&gt\;/g;
-    $htmlkey =~ s/</\&lt\;/g;
-    printf $htm_fh ($htm_format, @content, $htmlkey);
-  }
-  if ($xls_fh)
-  {
-    &set_worksheet_line($ws_top50, $row_league_table++, 0, [@content, $key], $f_default);
+    print $htm_fh <<EoText;
+<hr><a name="$text count"></a><h2>$title</h2>
+<table border=0 width="100%">
+<tr><td>
+<table border=1>
+EoText
+  print $htm_fh $htm_col_headers
   }
 
   }
 
-  if (scalar @chartdatanames < $ntopchart)
-  {
-    push(@chartdatanames, $key);
-    push(@chartdatavals, $$m_count{$key});
-  }
-  else
-  {
-    $chartotherval += $$m_count{$key};
+  if ($xls_fh) {
+    $spreadsheet->write(${$row_sref}++, 0, $title, $f_header2);
+    $spreadsheet->write(${$row_sref}++, 0, [@headers, $text], $f_headertab);
   }
   }
-}
 
 
-push(@chartdatanames, "Other");
-push(@chartdatavals, $chartotherval);
 
 
-print $txt_fh "\n" if $txt_fh;
-if ($htm_fh)
-{
-  print $htm_fh "</table>\n";
-  print $htm_fh "</td><td>\n";
-  if ($HAVE_GD_Graph_pie && $charts)
-    {
-    # calculate the graph
-    my @data = (
-       \@chartdatanames,
-       \@chartdatavals
-    );
-    my $graph = GD::Graph::pie->new(300, 300);
-    $graph->set(
-        x_label           => 'Name',
-        y_label           => 'Amount',
-        title             => 'By count',
-    );
-    my $gd = $graph->plot(\@data) or warn($graph->error);
-    if ($gd) {
-      my $temp = $text;
-      $temp =~ s/ /_/g;
-      open(IMG, ">$chartdir/${temp}_count.png") or die "Could not write $chartdir/${temp}_count.png: $!\n";
-      binmode IMG;
-      print IMG $gd->png;
-      close IMG;
-      print $htm_fh "<img src=\"$chartrel/${temp}_count.png\">";
+  # write content
+  foreach my $key (top_n_sort($topcount,$m_count,$m_data_gigs,$m_data)) {
+
+    # When displaying the average figures, we calculate the average of
+    # the rounded data, as the user would calculate it. This reduces
+    # the accuracy slightly, but we have to do it this way otherwise
+    # when using -merge to convert results from text to HTML and
+    # vice-versa discrepencies would occur.
+    my $messages  = $$m_count{$key};
+    my @content = ($messages);
+    push(@content, $$a_count{$key}) if defined $a_count;
+    if (defined $m_data) {
+      my $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
+      my($data,$gigs) = (0,0);
+      un_round($rounded_volume,\$data,\$gigs);
+      my $rounded_average = volume_rounded($data/$messages,$gigs/$messages);
+      push(@content, $rounded_volume, $rounded_average);
     }
     }
-  }
-  print $htm_fh "</td><td>\n";
-  print $htm_fh "</td></tr></table>\n\n";
-}
-if ($xls_fh)
-{
-  $row_league_table++;
-}
-
 
 
-# write header
+    # write content
+    printf $txt_fh ($txt_format, @content, $key) if $txt_fh;
 
 
-$temp = "Top $name by volume";
+    if ($htm_fh) {
+      my $htmlkey = $key;
+      $htmlkey =~ s/>/\&gt\;/g;
+      $htmlkey =~ s/</\&lt\;/g;
+      printf $htm_fh ($htm_format, @content, $htmlkey);
+    }
+    $spreadsheet->write(${$row_sref}++, 0, [@content, $key], $f_default) if $xls_fh;
 
 
-printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
-if ($htm_fh) {
-  print $htm_fh "<hr><a name=\"$text volume\"></a><h2>$temp</h2>\n";
-  print $htm_fh "<table border=0 width=\"100%\">\n";
-  print $htm_fh "<tr><td>\n";
-  print $htm_fh "<table border=1>\n";
-  print $htm_fh "<tr><th>Messages</th><th>Bytes</th><th>Average</th><th>\u$text</th>\n";
-}
-if ($xls_fh)
-{
-  $ws_top50->write($row_league_table++, 0, $temp, $f_header2);
-  &set_worksheet_line($ws_top50, $row_league_table++, 0, ["Messages", "Bytes", "Average", $text], $f_headertab);
-}
+    if (scalar @chartdatanames < $ntopchart) {
+      push(@chartdatanames, $key);
+      push(@chartdatavals, $$m_count{$key});
+    }
+    else {
+      $chartotherval += $$m_count{$key};
+    }
+  }
 
 
-@chartdatanames = ();
-@chartdatavals = ();
-$chartotherval = 0;
-my $use_gig = 0;
-foreach $key (top_n_sort($topcount,$m_data_gigs,$m_data,$m_count)) {
-  # The largest volume will be the first (top of the list).
-  # If it has at least 1 gig, then just use gigabytes to avoid
-  # risking an integer overflow when generating the pie charts.
-  if ($$m_data_gigs{$key}) {
-    $use_gig = 1;
-  }
-
-  $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
-  $data = $gigs = 0;
-  un_round($rounded_volume,\$data,\$gigs);
-  $count = $$m_count{$key};
-  $rounded_average = volume_rounded($data/$count,$gigs/$count);
-  my @content=($count, $rounded_volume, $rounded_average );
+  push(@chartdatanames, "Other");
+  push(@chartdatavals, $chartotherval);
 
 
-  # write content
-  # any reasons for not including rounded_average in the txt-version?? -fh
-  printf $txt_fh ($txt_format, $count, $rounded_volume, $key) if $txt_fh;
+  print $txt_fh "\n" if $txt_fh;
   if ($htm_fh) {
   if ($htm_fh) {
-    $htmlkey = $key;
-    $htmlkey =~ s/>/\&gt\;/g;
-    $htmlkey =~ s/</\&lt\;/g;
-    printf $htm_fh ($htm_format, @content, $htmlkey);
-  }
-  if ($xls_fh)
-  {
-    &set_worksheet_line($ws_top50, $row_league_table++, 0, [@content, $key], $f_default);
+    print $htm_fh "</table>\n";
+    print $htm_fh "</td><td>\n";
+    if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0))
+      {
+      # calculate the graph
+      my @data = (
+         \@chartdatanames,
+         \@chartdatavals
+      );
+      my $graph = GD::Graph::pie->new(300, 300);
+      $graph->set(
+          x_label           => 'Name',
+          y_label           => 'Amount',
+          title             => 'By count',
+      );
+      my $gd = $graph->plot(\@data) or warn($graph->error);
+      if ($gd) {
+        my $temp = $text;
+        $temp =~ s/ /_/g;
+        open(IMG, ">$chartdir/${temp}_count.png") or die "Could not write $chartdir/${temp}_count.png: $!\n";
+        binmode IMG;
+        print IMG $gd->png;
+        close IMG;
+        print $htm_fh "<img src=\"$chartrel/${temp}_count.png\">";
+      }
+    }
+    print $htm_fh "</td><td>\n";
+    print $htm_fh "</td></tr></table>\n\n";
   }
   }
+  ++${$row_sref} if $xls_fh;
+
+
+  if (defined $m_data) {
+    # write header
+
+    $title = "Top $name by volume";
+
+    printf $txt_fh ("%s\n%s\n%s", $title, "-" x length($title),$txt_col_headers) if $txt_fh;
 
 
+    if ($htm_fh) {
+      print $htm_fh <<EoText;
+<hr><a name="$text volume"></a><h2>$title</h2>
+<table border=0 width="100%">
+<tr><td>
+<table border=1>
+EoText
+    print $htm_fh $htm_col_headers;
+    }
+    if ($xls_fh) {
+      $spreadsheet->write(${$row_sref}++, 0, $title, $f_header2);
+      $spreadsheet->write(${$row_sref}++, 0, [@headers, $text], $f_headertab);
+    }
 
 
-  if (scalar @chartdatanames < $ntopchart) {
-    if ($use_gig) {
+    @chartdatanames = ();
+    @chartdatavals = ();
+    $chartotherval = 0;
+    my $use_gig = 0;
+    foreach my $key (top_n_sort($topcount,$m_data_gigs,$m_data,$m_count)) {
+      # The largest volume will be the first (top of the list).
+      # If it has at least 1 gig, then just use gigabytes to avoid
+      # risking an integer overflow when generating the pie charts.
       if ($$m_data_gigs{$key}) {
       if ($$m_data_gigs{$key}) {
-        push(@chartdatanames, $key);
-        push(@chartdatavals, $$m_data_gigs{$key});
+        $use_gig = 1;
+      }
+
+      my $messages  = $$m_count{$key};
+      my @content = ($messages);
+      push(@content, $$a_count{$key}) if defined $a_count;
+      my $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
+      my($data ,$gigs) = (0,0);
+      un_round($rounded_volume,\$data,\$gigs);
+      my $rounded_average = volume_rounded($data/$messages,$gigs/$messages);
+      push(@content, $rounded_volume, $rounded_average );
+
+      # write content
+      printf $txt_fh ($txt_format, @content, $key) if $txt_fh;
+      if ($htm_fh) {
+        my $htmlkey = $key;
+        $htmlkey =~ s/>/\&gt\;/g;
+        $htmlkey =~ s/</\&lt\;/g;
+        printf $htm_fh ($htm_format, @content, $htmlkey);
+      }
+      $spreadsheet->write(${$row_sref}++, 0, [@content, $key], $f_default) if $xls_fh;
+
+
+      if (scalar @chartdatanames < $ntopchart) {
+        if ($use_gig) {
+          if ($$m_data_gigs{$key}) {
+            push(@chartdatanames, $key);
+            push(@chartdatavals, $$m_data_gigs{$key});
+          }
+        }
+        else {
+          push(@chartdatanames, $key);
+          push(@chartdatavals, $$m_data{$key});
+        }
+      }
+      else {
+        $chartotherval += ($use_gig) ? $$m_data_gigs{$key} : $$m_data{$key};
       }
     }
       }
     }
-    else {
-      push(@chartdatanames, $key);
-      push(@chartdatavals, $$m_data{$key});
-    }
-  }
-  else {
-    $chartotherval += ($use_gig) ? $$m_data_gigs{$key} : $$m_data{$key};
-  }
-}
-push(@chartdatanames, "Other");
-push(@chartdatavals, $chartotherval);
+    push(@chartdatanames, "Other");
+    push(@chartdatavals, $chartotherval);
 
 
-print $txt_fh "\n" if $txt_fh;
-if ($htm_fh) {
-  print $htm_fh "</table>\n";
-  print $htm_fh "</td><td>\n";
-  if ($HAVE_GD_Graph_pie && $charts) {
-    # calculate the graph
-    my @data = (
-       \@chartdatanames,
-       \@chartdatavals
-    );
-    my $graph = GD::Graph::pie->new(300, 300);
-    $graph->set(
-        x_label           => 'Name',
-        y_label           => 'Volume' ,
-        title             => 'By Volume',
-    );
-    my $gd = $graph->plot(\@data) or warn($graph->error);
-    if ($gd) {
-      $temp = $text;
-      $temp =~ s/ /_/g;
-      open(IMG, ">$chartdir/${temp}_volume.png") or die "Could not write $chartdir/${temp}_volume.png: $!\n";
-      binmode IMG;
-      print IMG $gd->png;
-      close IMG;
-      print $htm_fh "<img src=\"$chartrel/${temp}_volume.png\">";
+    print $txt_fh "\n" if $txt_fh;
+    if ($htm_fh) {
+      print $htm_fh "</table>\n";
+      print $htm_fh "</td><td>\n";
+      if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0)) {
+        # calculate the graph
+        my @data = (
+           \@chartdatanames,
+           \@chartdatavals
+        );
+        my $graph = GD::Graph::pie->new(300, 300);
+        $graph->set(
+            x_label           => 'Name',
+            y_label           => 'Volume' ,
+            title             => 'By Volume',
+        );
+        my $gd = $graph->plot(\@data) or warn($graph->error);
+        if ($gd) {
+          my $temp = $text;
+          $temp =~ s/ /_/g;
+          open(IMG, ">$chartdir/${temp}_volume.png") or die "Could not write $chartdir/${temp}_volume.png: $!\n";
+          binmode IMG;
+          print IMG $gd->png;
+          close IMG;
+          print $htm_fh "<img src=\"$chartrel/${temp}_volume.png\">";
+        }
+      }
+      print $htm_fh "</td><td>\n";
+      print $htm_fh "</td></tr></table>\n\n";
     }
     }
+
+    ++${$row_sref} if $xls_fh;
   }
   }
-  print $htm_fh "</td><td>\n";
-  print $htm_fh "</td></tr></table>\n\n";
-}
-if ($xls_fh)
-{
-  ++$row_league_table;
 }
 
 }
 
-}
 
 #######################################################################
 # top_n_sort();
 
 #######################################################################
 # top_n_sort();
@@ -1459,6 +1641,12 @@ sub top_n_sort {
   my $n_minus_1 = $n - 1;
   my $n_minus_2 = $n - 2;
 
   my $n_minus_1 = $n - 1;
   my $n_minus_2 = $n - 2;
 
+  # Create a dummy hash incase the user has not provided us with
+  # tiebreaker hashes.
+  my(%dummy_hash);
+  $href2 = \%dummy_hash unless defined $href2;
+  $href3 = \%dummy_hash unless defined $href3;
+
   # Pick out the top $n keys.
   my($key,$value1,$value2,$value3,$i,$comparison,$insert_position);
   while (($key,$value1) = each %$href1) {
   # Pick out the top $n keys.
   my($key,$value1,$value2,$value3,$i,$comparison,$insert_position);
   while (($key,$value1) = each %$href1) {
@@ -1466,7 +1654,18 @@ sub top_n_sort {
     #print STDERR "key $key ($value1,",$href2->{$key},",",$href3->{$key},") <=> ($minimum_value1,$minimum_value2,$minimum_value3)\n";
 
     # Check to see that the new value is bigger than the lowest of the
     #print STDERR "key $key ($value1,",$href2->{$key},",",$href3->{$key},") <=> ($minimum_value1,$minimum_value2,$minimum_value3)\n";
 
     # Check to see that the new value is bigger than the lowest of the
-    # top n keys that we're keeping.
+    # top n keys that we're keeping. We test the main key first, because
+    # for the majority of cases we can skip creating dummy hash values
+    # should the user have not provided real tie-breaking hashes.
+    next unless $value1 >= $minimum_value1;
+
+    # 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 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};
+
     $comparison = $value1        <=> $minimum_value1 ||
                   $href2->{$key} <=> $minimum_value2 ||
                   $href3->{$key} <=> $minimum_value3 ||
     $comparison = $value1        <=> $minimum_value1 ||
                   $href2->{$key} <=> $minimum_value2 ||
                   $href3->{$key} <=> $minimum_value3 ||
@@ -1524,6 +1723,7 @@ sub top_n_sort {
 }
 
 
 }
 
 
+
 #######################################################################
 # html_header();
 #
 #######################################################################
 # html_header();
 #
@@ -1587,12 +1787,16 @@ Valid options are:
 -nt             don't display transport information
 -nt/pattern/    don't display transport information that matches
 -nvr            don't do volume rounding. Display in bytes, not KB/MB/GB.
 -nt             don't display transport information
 -nt/pattern/    don't display transport information that matches
 -nvr            don't do volume rounding. Display in bytes, not KB/MB/GB.
--q<list>        list of times for queuing information
-                single 0 item suppresses
 -t<number>      display top <number> sources/destinations
                 default is 50, 0 suppresses top listing
 -tnl            omit local sources/destinations in top listing
 -t_remote_users show top user sources/destinations from non-local domains
 -t<number>      display top <number> sources/destinations
                 default is 50, 0 suppresses top listing
 -tnl            omit local sources/destinations in top listing
 -t_remote_users show top user sources/destinations from non-local domains
+-q<list>        list of times for queuing information. -q0 suppresses.
+-show_rt<list>  Show the receipt times for all the messages.
+-show_dt<list>  Show the delivery times for all the messages.
+                <list> is an optional list of times in seconds.
+                Eg -show_rt1,2,4,8.
+
 -include_original_destination   show both the final and original
                 destinations in the results rather than just the final ones.
 
 -include_original_destination   show both the final and original
                 destinations in the results rather than just the final ones.
 
@@ -1601,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
 -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
 
 -pattern "Description" /pattern/
                 Count lines matching specified patterns and show them in
@@ -1616,6 +1821,8 @@ Valid options are:
                 from where to include the charts in the html file
                 -chartdir and -chartrel default to '.'
 
                 from where to include the charts in the html file
                 -chartdir and -chartrel default to '.'
 
+-emptyok        It is OK if there is no valid input, don't print an error.
+
 -d              Debug mode - dump the eval'ed parser onto STDERR.
 
 EoText
 -d              Debug mode - dump the eval'ed parser onto STDERR.
 
 EoText
@@ -1643,7 +1850,9 @@ EoText
 sub generate_parser {
   my $parser = '
   my($ip,$host,$email,$edomain,$domain,$thissize,$size,$old,$new);
 sub generate_parser {
   my $parser = '
   my($ip,$host,$email,$edomain,$domain,$thissize,$size,$old,$new);
-  my($tod,$m_hour,$m_min,$id,$flag);
+  my($tod,$m_hour,$m_min,$id,$flag,$extra,$length);
+  my($seconds,$queued,$rcpt_time,$local_domain);
+  my $rej_id = 0;
   while (<$fh>) {
 
     # Convert syslog lines to mainlog format.
   while (<$fh>) {
 
     # Convert syslog lines to mainlog format.
@@ -1651,34 +1860,83 @@ sub generate_parser {
       next unless s/^.*? exim\\b.*?: //;
     }
 
       next unless s/^.*? exim\\b.*?: //;
     }
 
-    next if length($_) < 38;
-    next unless /^(\\d{4}\\-\\d\\d-\\d\\d\\s(\\d\\d):(\\d\\d):\\d\\d( [-+]\\d\\d\\d\\d)?)/o;
+    $length = length($_);
+    next if ($length < 38);
+    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($5)) {
+      $extra = 6;
+      next if ($length < 44);
+    }
+    else {
+      $extra = 0;
+    }
+
+    # watch for subsecond precision
+    if (defined($4)) {
+      $extra += length($4);
+      next if ($length < 38 + $extra);
+    }
 
 
-    ($tod,$m_hour,$m_min) = ($1,$2,$3);
+    # PH - watch for PID added after the timestamp.
+    if (defined($6)) {
+      $extra += length($6);
+      next if ($length < 38 + $extra);
+    }
 
 
-    # PH
-    my($extra) = defined($4)? 6 : 0;
     $id   = substr($_, 20 + $extra, 16);
     $flag = substr($_, 37 + $extra, 2);
     $id   = substr($_, 20 + $extra, 16);
     $flag = substr($_, 37 + $extra, 2);
+
+    if ($flag !~ /^([<>=*-]+|SA)$/ && /rejected|refused|dropped/) {
+      $flag = "Re";
+      $extra -= 3;
+    }
+
+    # Rejects can have no MSGID...
+    if ($flag eq "Re" && $id !~ /^[-0-9a-zA-Z]+$/) {
+      $id   = "reject:" . ++$rej_id;
+      $extra -= 17;
+    }
 ';
 
   # Watch for user specified patterns.
   my $user_pattern_index = 0;
   foreach (@user_patterns) {
     $user_pattern_totals[$user_pattern_index] = 0;
 ';
 
   # Watch for user specified patterns.
   my $user_pattern_index = 0;
   foreach (@user_patterns) {
     $user_pattern_totals[$user_pattern_index] = 0;
-    $parser .= "  \$user_pattern_totals[$user_pattern_index]++ if $_;\n";
+    $parser .= "    if ($_) {\n";
+    $parser .= "      \$user_pattern_totals[$user_pattern_index]++;\n";
+    $parser .= "      \$user_pattern_interval_count[$user_pattern_index][(\$m_hour*60 + \$m_min)/$hist_interval]++;\n" if ($hist_opt > 0);
+    $parser .= "    }\n";
     $user_pattern_index++;
   }
 
   $parser .= '
     $user_pattern_index++;
   }
 
   $parser .= '
-    next unless ($flag =~ /<=|=>|->|==|\\*\\*|Co/);
-
-    #Strip away the timestamp, ID and flag (which could be "Com" for completed)
-    #This speeds up the later pattern matches.
-    # $_ = substr($_, 40);
+    next unless ($flag =~ /<=|=>|->|==|\\*\\*|Co|SA|Re/);
 
 
+    #Strip away the timestamp, ID and flag to speed up later pattern matches.
+    #The flags include Co (Completed), Re (Rejected), and SA (SpamAssassin).
     $_ = substr($_, 40 + $extra);  # PH
 
     $_ = substr($_, 40 + $extra);  # PH
 
+    # Alias @message to the array of information about the message.
+    # This minimises the number of calls to hash functions.
+    $messages{$id} = [] unless exists $messages{$id};
+    *message = $messages{$id};
+
+
     # JN - Skip over certain transports as specified via the "-nt/.../" command
     # line switch (where ... is a perl style regular expression).  This is
     # required so that transports that skew stats such as SpamAssassin can be
     # JN - Skip over certain transports as specified via the "-nt/.../" command
     # line switch (where ... is a perl style regular expression).  This is
     # required so that transports that skew stats such as SpamAssassin can be
@@ -1690,33 +1948,35 @@ sub generate_parser {
     #ENDIF ($transport_pattern)
 
 
     #ENDIF ($transport_pattern)
 
 
-    $host = "local";          #Host is local unless otherwise specified.
-    $domain = "localdomain";  #Domain is localdomain unless otherwise specified.
-
 
     # Do some pattern matches to get the host and IP address.
     # We expect lines to be of the form "H=[IpAddr]" or "H=Host [IpAddr]" or
     # "H=Host (UnverifiedHost) [IpAddr]" or "H=(UnverifiedHost) [IpAddr]".
     # We do 2 separate matches to keep the matches simple and fast.
 
     # Do some pattern matches to get the host and IP address.
     # We expect lines to be of the form "H=[IpAddr]" or "H=Host [IpAddr]" or
     # "H=Host (UnverifiedHost) [IpAddr]" or "H=(UnverifiedHost) [IpAddr]".
     # We do 2 separate matches to keep the matches simple and fast.
-    if (/\\sH=(\\S+)/) {
-      $host = $1;
+    # Host is local unless otherwise specified.
+    # 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";
 
 
-      ($ip) = /\\sH=.*?(\\s\\[[^]]+\\])/;
-      # If there is only an IP address, it will be in $host and $ip will be
-      # unset. That is OK, because we only use $ip in conjunction with $host
-      # below. But make it empty to avoid warning messages.
-      $ip = "" if !defined $ip;
-
-      #IFDEF ($do_sender{Domain})
-      if ($host !~ /^\\[/ && $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");
-        $domain =~ s/^\\.//;         #Remove preceding dot.
-      }
-      #ENDIF ($do_sender{Domain})
+    $domain = "localdomain";  #Domain is localdomain unless otherwise specified.
 
 
+    #IFDEF ($do_sender{Domain})
+    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");
+      $domain =~ s/^\\.//;         #Remove preceding dot.
+    }
+    #ENDIF ($do_sender{Domain})
 
     #IFDEF ($do_sender{Email})
       #IFDEF ($include_original_destination)
 
     #IFDEF ($do_sender{Email})
       #IFDEF ($include_original_destination)
@@ -1732,16 +1992,25 @@ sub generate_parser {
     #ENDIF ($do_sender{Email})
 
     #IFDEF ($do_sender{Edomain})
     #ENDIF ($do_sender{Email})
 
     #IFDEF ($do_sender{Edomain})
+      if (/^(<>|blackhole)/) {
+        $edomain = $1;
+      }
       #IFDEF ($include_original_destination)
       #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)
       #ENDIF ($include_original_destination)
-
       #IFNDEF ($include_original_destination)
       #IFNDEF ($include_original_destination)
-      $edomain = (/^\S*?\\@(\S+)/) ? lc($1) : "";
+        elsif (/^\S*?\\@(\S+)/) {
+          $edomain = lc($1);
+        }
       #ENDIF ($include_original_destination)
       #ENDIF ($include_original_destination)
+      else {
+        $edomain = "";
+      }
+
     #ENDIF ($do_sender{Edomain})
 
     if ($tod lt $begin) {
     #ENDIF ($do_sender{Edomain})
 
     if ($tod lt $begin) {
@@ -1754,15 +2023,16 @@ sub generate_parser {
 
     if ($flag eq "<=") {
       $thissize = (/\\sS=(\\d+)( |$)/) ? $1 : 0;
 
     if ($flag eq "<=") {
       $thissize = (/\\sS=(\\d+)( |$)/) ? $1 : 0;
-      $size{$id} = $thissize;
+      $message[$SIZE] = $thissize;
+      $message[$PROTOCOL] = (/ P=(\S+)/) ? $1 : undef;
 
       #IFDEF ($show_relay)
       if ($host ne "local") {
         # Save incoming information in case it becomes interesting
         # later, when delivery lines are read.
         my($from) = /^(\\S+)/;
 
       #IFDEF ($show_relay)
       if ($host ne "local") {
         # Save incoming information in case it becomes interesting
         # later, when delivery lines are read.
         my($from) = /^(\\S+)/;
-        $from_host{$id} = "$host$ip";
-        $from_address{$id} = $from;
+        $message[$FROM_HOST]    = "$host$ip";
+        $message[$FROM_ADDRESS] = $from;
       }
       #ENDIF ($show_relay)
 
       }
       #ENDIF ($show_relay)
 
@@ -1782,40 +2052,40 @@ sub generate_parser {
           if ($host ne "local") {   #Store remote users only.
           #ENDIF ($include_remote_users && ! $local_league_table)
 
           if ($host ne "local") {   #Store remote users only.
           #ENDIF ($include_remote_users && ! $local_league_table)
 
-            $received_count_user{$user}++;
+            ++$received_count_user{$user};
             add_volume(\\$received_data_user{$user},\\$received_data_gigs_user{$user},$thissize);
           }
         }
       #ENDIF ($local_league_table || $include_remote_users)
 
       #IFDEF ($do_sender{Host})
             add_volume(\\$received_data_user{$user},\\$received_data_gigs_user{$user},$thissize);
           }
         }
       #ENDIF ($local_league_table || $include_remote_users)
 
       #IFDEF ($do_sender{Host})
-        $received_count{Host}{$host}++;
+        ++$received_count{Host}{$host};
         add_volume(\\$received_data{Host}{$host},\\$received_data_gigs{Host}{$host},$thissize);
       #ENDIF ($do_sender{Host})
 
       #IFDEF ($do_sender{Domain})
         if ($domain) {
         add_volume(\\$received_data{Host}{$host},\\$received_data_gigs{Host}{$host},$thissize);
       #ENDIF ($do_sender{Host})
 
       #IFDEF ($do_sender{Domain})
         if ($domain) {
-          $received_count{Domain}{$domain}++;
+          ++$received_count{Domain}{$domain};
           add_volume(\\$received_data{Domain}{$domain},\\$received_data_gigs{Domain}{$domain},$thissize);
         }
       #ENDIF ($do_sender{Domain})
 
       #IFDEF ($do_sender{Email})
           add_volume(\\$received_data{Domain}{$domain},\\$received_data_gigs{Domain}{$domain},$thissize);
         }
       #ENDIF ($do_sender{Domain})
 
       #IFDEF ($do_sender{Email})
-        $received_count{Email}{$email}++;
+        ++$received_count{Email}{$email};
         add_volume(\\$received_data{Email}{$email},\\$received_data_gigs{Email}{$email},$thissize);
       #ENDIF ($do_sender{Email})
 
       #IFDEF ($do_sender{Edomain})
         add_volume(\\$received_data{Email}{$email},\\$received_data_gigs{Email}{$email},$thissize);
       #ENDIF ($do_sender{Email})
 
       #IFDEF ($do_sender{Edomain})
-        $received_count{Edomain}{$edomain}++;
+        ++$received_count{Edomain}{$edomain};
         add_volume(\\$received_data{Edomain}{$edomain},\\$received_data_gigs{Edomain}{$edomain},$thissize);
       #ENDIF ($do_sender{Edomain})
 
         add_volume(\\$received_data{Edomain}{$edomain},\\$received_data_gigs{Edomain}{$edomain},$thissize);
       #ENDIF ($do_sender{Edomain})
 
-      $total_received_count++;
+      ++$total_received_count;
       add_volume(\\$total_received_data,\\$total_received_data_gigs,$thissize);
 
       add_volume(\\$total_received_data,\\$total_received_data_gigs,$thissize);
 
-      #IFDEF ($#queue_times >= 0)
-        $arrival_time{$id} = $tod;
-      #ENDIF ($#queue_times >= 0)
+      #IFDEF ($#queue_times >= 0 || $#rcpt_times >= 0)
+        $message[$ARRIVAL_TIME] = $tod;
+      #ENDIF ($#queue_times >= 0 || $#rcpt_times >= 0)
 
       #IFDEF ($hist_opt > 0)
         $received_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
 
       #IFDEF ($hist_opt > 0)
         $received_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
@@ -1823,9 +2093,9 @@ sub generate_parser {
     }
 
     elsif ($flag eq "=>") {
     }
 
     elsif ($flag eq "=>") {
-      $size = $size{$id} || 0;
+      $size = $message[$SIZE] || 0;
       if ($host ne "local") {
       if ($host ne "local") {
-        $remote_delivered{$id} = 1;
+        $message[$REMOTE_DELIVERED] = 1;
 
 
         #IFDEF ($show_relay)
 
 
         #IFDEF ($show_relay)
@@ -1835,7 +2105,7 @@ sub generate_parser {
         # addresses, there may be a further address between the first
         # and last.
 
         # addresses, there may be a further address between the first
         # and last.
 
-        if (defined $from_host{$id}) {
+        if (defined $message[$FROM_HOST]) {
           if (/^(\\S+)(?:\\s+\\([^)]\\))?\\s+<([^>]+)>/) {
             ($old,$new) = ($1,$2);
           }
           if (/^(\\S+)(?:\\s+\\([^)]\\))?\\s+<([^>]+)>/) {
             ($old,$new) = ($1,$2);
           }
@@ -1845,14 +2115,14 @@ sub generate_parser {
 
           if ("\\L$new" eq "\\L$old") {
             ($old) = /^(\\S+)/ if $old eq "";
 
           if ("\\L$new" eq "\\L$old") {
             ($old) = /^(\\S+)/ if $old eq "";
-            my $key = "H=\\L$from_host{$id}\\E A=\\L$from_address{$id}\\E => " .
+            my $key = "H=\\L$message[$FROM_HOST]\\E A=\\L$message[$FROM_ADDRESS]\\E => " .
               "H=\\L$host\\E$ip A=\\L$old\\E";
             if (!defined $relay_pattern || $key !~ /$relay_pattern/o) {
               $relayed{$key} = 0 if !defined $relayed{$key};
               "H=\\L$host\\E$ip A=\\L$old\\E";
             if (!defined $relay_pattern || $key !~ /$relay_pattern/o) {
               $relayed{$key} = 0 if !defined $relayed{$key};
-              $relayed{$key}++;
+              ++$relayed{$key};
             }
             else {
             }
             else {
-              $relayed_unshown++
+              ++$relayed_unshown;
             }
           }
         }
             }
           }
         }
@@ -1880,40 +2150,57 @@ sub generate_parser {
             #IFNDEF ($include_original_destination)
             if ($user =~ /^[\\/|]/) {
             #ENDIF ($include_original_destination)
             #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_count_user{$user}++;
+            ++$delivered_messages_user{$user};
+            ++$delivered_addresses_user{$user};
             add_volume(\\$delivered_data_user{$user},\\$delivered_data_gigs_user{$user},$size);
           }
         }
       #ENDIF ($local_league_table || $include_remote_users)
 
       #IFDEF ($do_sender{Host})
             add_volume(\\$delivered_data_user{$user},\\$delivered_data_gigs_user{$user},$size);
           }
         }
       #ENDIF ($local_league_table || $include_remote_users)
 
       #IFDEF ($do_sender{Host})
-        $delivered_count{Host}{$host}++;
+        $delivered_messages{Host}{$host}++;
+        $delivered_addresses{Host}{$host}++;
         add_volume(\\$delivered_data{Host}{$host},\\$delivered_data_gigs{Host}{$host},$size);
       #ENDIF ($do_sender{Host})
       #IFDEF ($do_sender{Domain})
         if ($domain) {
         add_volume(\\$delivered_data{Host}{$host},\\$delivered_data_gigs{Host}{$host},$size);
       #ENDIF ($do_sender{Host})
       #IFDEF ($do_sender{Domain})
         if ($domain) {
-          $delivered_count{Domain}{$domain}++;
+          ++$delivered_messages{Domain}{$domain};
+          ++$delivered_addresses{Domain}{$domain};
           add_volume(\\$delivered_data{Domain}{$domain},\\$delivered_data_gigs{Domain}{$domain},$size);
         }
       #ENDIF ($do_sender{Domain})
       #IFDEF ($do_sender{Email})
           add_volume(\\$delivered_data{Domain}{$domain},\\$delivered_data_gigs{Domain}{$domain},$size);
         }
       #ENDIF ($do_sender{Domain})
       #IFDEF ($do_sender{Email})
-        $delivered_count{Email}{$email}++;
+        ++$delivered_messages{Email}{$email};
+        ++$delivered_addresses{Email}{$email};
         add_volume(\\$delivered_data{Email}{$email},\\$delivered_data_gigs{Email}{$email},$size);
       #ENDIF ($do_sender{Email})
       #IFDEF ($do_sender{Edomain})
         add_volume(\\$delivered_data{Email}{$email},\\$delivered_data_gigs{Email}{$email},$size);
       #ENDIF ($do_sender{Email})
       #IFDEF ($do_sender{Edomain})
-        $delivered_count{Edomain}{$edomain}++;
+        ++$delivered_messages{Edomain}{$edomain};
+        ++$delivered_addresses{Edomain}{$edomain};
         add_volume(\\$delivered_data{Edomain}{$edomain},\\$delivered_data_gigs{Edomain}{$edomain},$size);
       #ENDIF ($do_sender{Edomain})
 
         add_volume(\\$delivered_data{Edomain}{$edomain},\\$delivered_data_gigs{Edomain}{$edomain},$size);
       #ENDIF ($do_sender{Edomain})
 
-      $total_delivered_count++;
+      ++$total_delivered_messages;
+      ++$total_delivered_addresses;
       add_volume(\\$total_delivered_data,\\$total_delivered_data_gigs,$size);
 
       #IFDEF ($show_transport)
         my $transport = (/\\sT=(\\S+)/) ? $1 : ":blackhole:";
       add_volume(\\$total_delivered_data,\\$total_delivered_data_gigs,$size);
 
       #IFDEF ($show_transport)
         my $transport = (/\\sT=(\\S+)/) ? $1 : ":blackhole:";
-        $transported_count{$transport}++;
+        ++$transported_count{$transport};
         add_volume(\\$transported_data{$transport},\\$transported_data_gigs{$transport},$size);
       #ENDIF ($show_transport)
 
         add_volume(\\$transported_data{$transport},\\$transported_data_gigs{$transport},$size);
       #ENDIF ($show_transport)
 
@@ -1921,18 +2208,89 @@ sub generate_parser {
         $delivered_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
       #ENDIF ($hist_opt > 0)
 
         $delivered_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
       #ENDIF ($hist_opt > 0)
 
+      #IFDEF ($#delivery_times > 0)
+        if (/ DT=(\S+)/) {
+          $seconds = wdhms_seconds($1);
+          for ($i = 0; $i <= $#delivery_times; $i++) {
+            if ($seconds < $delivery_times[$i]) {
+              ++$dt_all_bin[$i];
+              ++$dt_remote_bin[$i] if $message[$REMOTE_DELIVERED];
+              last;
+            }
+          }
+          if ($i > $#delivery_times) {
+            ++$dt_all_overflow;
+            ++$dt_remote_overflow if $message[$REMOTE_DELIVERED];
+          }
+        }
+      #ENDIF ($#delivery_times > 0)
+
     }
 
     }
 
-    elsif ($flag eq "==" && defined($size{$id}) && !defined($delayed{$id})) {
-      $delayed_count++;
-      $delayed{$id} = 1;
+    elsif ($flag eq "->") {
+
+      #IFDEF ($local_league_table || $include_remote_users)
+        #IFDEF ($local_league_table && $include_remote_users)
+        {                         #Store both local and remote users.
+        #ENDIF ($local_league_table && $include_remote_users)
+
+        #IFDEF ($local_league_table && ! $include_remote_users)
+        if ($host eq "local") {   #Store local users only.
+        #ENDIF ($local_league_table && ! $include_remote_users)
+
+        #IFDEF ($include_remote_users && ! $local_league_table)
+        if ($host ne "local") {   #Store remote users only.
+        #ENDIF ($include_remote_users && ! $local_league_table)
+
+          if (my($user) = split((/\\s</)? " <" : " ", $_)) {
+            #IFDEF ($include_original_destination)
+            {
+            #ENDIF ($include_original_destination)
+            #IFNDEF ($include_original_destination)
+            if ($user =~ /^[\\/|]/) {
+            #ENDIF ($include_original_destination)
+              #my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
+              my($parent) = $_ =~ / (<.+?>) /;              #DT 1.54
+              $user = "$user $parent" if defined $parent;
+            }
+            ++$delivered_addresses_user{$user};
+          }
+        }
+      #ENDIF ($local_league_table || $include_remote_users)
+
+      #IFDEF ($do_sender{Host})
+        $delivered_addresses{Host}{$host}++;
+      #ENDIF ($do_sender{Host})
+      #IFDEF ($do_sender{Domain})
+        if ($domain) {
+          ++$delivered_addresses{Domain}{$domain};
+        }
+      #ENDIF ($do_sender{Domain})
+      #IFDEF ($do_sender{Email})
+        ++$delivered_addresses{Email}{$email};
+      #ENDIF ($do_sender{Email})
+      #IFDEF ($do_sender{Edomain})
+        ++$delivered_addresses{Edomain}{$edomain};
+      #ENDIF ($do_sender{Edomain})
+
+      ++$total_delivered_addresses;
+    }
+
+    elsif ($flag eq "==" && defined($message[$SIZE]) && !defined($message[$DELAYED])) {
+      ++$delayed_count;
+      $message[$DELAYED] = 1;
     }
 
     elsif ($flag eq "**") {
     }
 
     elsif ($flag eq "**") {
-      $had_error{$id} = 1 if defined ($size{$id});
+      if (defined ($message[$SIZE])) {
+        unless (defined $message[$HAD_ERROR]) {
+          ++$message_errors;
+          $message[$HAD_ERROR] = 1;
+        }
+      }
 
       #IFDEF ($show_errors)
 
       #IFDEF ($show_errors)
-        $errors_count{$_}++;
+        ++$errors_count{$_};
       #ENDIF ($show_errors)
 
     }
       #ENDIF ($show_errors)
 
     }
@@ -1940,32 +2298,177 @@ sub generate_parser {
     elsif ($flag eq "Co") {
       #Completed?
       #IFDEF ($#queue_times >= 0)
     elsif ($flag eq "Co") {
       #Completed?
       #IFDEF ($#queue_times >= 0)
-        #Note: id_seconds() benchmarks as 42% slower than seconds() and computing
-        #the time accounts for a significant portion of the run time.
-        my($queued);
-        if (defined $arrival_time{$id}) {
-          $queued = seconds($tod) - seconds($arrival_time{$id});
-          delete($arrival_time{$id});
-        }
-        else {
-          $queued = seconds($tod) - id_seconds($id);
-        }
+        $queued = queue_time($tod, $message[$ARRIVAL_TIME], $id);
 
         for ($i = 0; $i <= $#queue_times; $i++) {
           if ($queued < $queue_times[$i]) {
 
         for ($i = 0; $i <= $#queue_times; $i++) {
           if ($queued < $queue_times[$i]) {
-            $queue_bin[$i]++;
-            $remote_queue_bin[$i]++ if $remote_delivered{$id};
+            ++$qt_all_bin[$i];
+            ++$qt_remote_bin[$i] if $message[$REMOTE_DELIVERED];
             last;
           }
         }
             last;
           }
         }
-        $queue_more_than++ if $i > $#queue_times;
+        if ($i > $#queue_times) {
+          ++$qt_all_overflow;
+          ++$qt_remote_overflow if $message[$REMOTE_DELIVERED];
+        }
       #ENDIF ($#queue_times >= 0)
 
       #ENDIF ($#queue_times >= 0)
 
-      #IFDEF ($show_relay)
-        delete($from_host{$id});
-        delete($from_address{$id});
-      #ENDIF ($show_relay)
+      #IFDEF ($#rcpt_times >= 0)
+        if (/ QT=(\S+)/) {
+          $seconds = wdhms_seconds($1);
+          #Calculate $queued if not previously calculated above.
+          #IFNDEF ($#queue_times >= 0)
+            $queued = queue_time($tod, $message[$ARRIVAL_TIME], $id);
+          #ENDIF ($#queue_times >= 0)
+          $rcpt_time = $seconds - $queued;
+          my($protocol);
+
+          if (defined $message[$PROTOCOL]) {
+            $protocol = $message[$PROTOCOL];
+
+            # Create the bin if its not already defined.
+            unless (exists $rcpt_times_bin{$protocol}) {
+              initialise_rcpt_times($protocol);
+            }
+          }
+
+
+          for ($i = 0; $i <= $#rcpt_times; ++$i) {
+            if ($rcpt_time < $rcpt_times[$i]) {
+              ++$rcpt_times_bin{all}[$i];
+              ++$rcpt_times_bin{$protocol}[$i] if defined $protocol;
+              last;
+            }
+          }
+
+          if ($i > $#rcpt_times) {
+            ++$rcpt_times_overflow{all};
+            ++$rcpt_times_overflow{$protocol} if defined $protocol;
+          }
+        }
+      #ENDIF ($#rcpt_times >= 0)
 
 
+      delete($messages{$id});
+    }
+    elsif ($flag eq "SA") {
+      $ip = (/From.*?(\\[[^]]+\\])/ || /\\((local)\\)/) ? $1 : "";
+      #SpamAssassin message
+      if (/Action: ((permanently|temporarily) rejected message|flagged as Spam but accepted): score=(\d+\.\d)/) {
+        #add_volume(\\$spam_score,\\$spam_score_gigs,$3);
+        ++$spam_count_by_ip{$ip};
+      } elsif (/Action: scanned but message isn\'t spam: score=(-?\d+\.\d)/) {
+        #add_volume(\\$ham_score,\\$ham_score_gigs,$1);
+        ++$ham_count_by_ip{$ip};
+      } elsif (/(Not running SA because SAEximRunCond expanded to false|check skipped due to message size)/) {
+        ++$ham_count_by_ip{$ip};
+      }
+    }
+
+    # Look for Reject messages or blackholed messages (deliveries
+    # without a transport)
+    if ($flag eq "Re" || ($flag eq "=>" && ! /\\sT=\\S+/)) {
+      # Correct the IP address for rejects:
+      # 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 .*?(\[.+?\]):/);
+      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 [^ ]+)/ ||
+        /(Forged IP detected in HELO)/ ||
+        /(Invalid domain or IP given in HELO\/EHLO)/ ||
+        /(unqualified recipient rejected)/ ||
+        /(closed connection (after|in response) .*?)\s*$/ ||
+        /(sender rejected)/ ||
+        # 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)/ ||
+#        /(sender verify (defer|fail))/i ||
+        /(too many recipients)/ ||
+        /(refused relay.*?) to/ ||
+        /(rejected by non-SMTP ACL: .*)/ ||
+        /(rejected by local_scan.*)/ ||
+        # SMTP call from %s dropped: too many syntax or protocol errors (last command was "%s"
+        # SMTP call from %s dropped: too many nonmail commands
+        /(dropped: too many ((nonmail|unrecognized) commands|syntax or protocol errors))/ ||
+
+        # local_scan() function crashed with signal %d - message temporarily rejected
+        # local_scan() function timed out - message temporarily rejected
+        /(local_scan.. function .* - message temporarily rejected)/ ||
+        # 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)/ ||
+
+        # rejected from <%s>%s%s%s%s: message too big:
+        /(rejected [A-Z]*) .*?(: .*?)(:|\s*$)/ ||
+        # refused connection from %s (host_reject_connection)
+        # refused connection from %s (tcp wrappers)
+        /(refused connection )from.*? (\(.*)/ ||
+
+        # error from remote mailer after RCPT TO:<a@b.c>: host a.b.c [10.0.0.1]: 450 <a@b.c>: Recipient address rejected: Greylisted for 60 seconds
+        # error from remote mailer after MAIL FROM:<> SIZE=3468: host a.b.c [10.0.0.1]: 421 a.b.c has refused your connection because your server did not have a PTR record.
+        /(error from remote mailer after .*?:).*(: .*?)(:|\s*$)/ ||
+
+        # a.b.c F=<a@b.c> rejected after DATA: "@" or "." expected after "Undisclosed-Recipient": failing address in "To" header is: <Undisclosed-Recipient:;>
+        /rejected after DATA: ("." or "." expected).*?(: failing address in .*? header)/ ||
+
+        # connection from %s refused load average = %.2f
+        /(Connection )from.*? (refused: load average)/ ||
+        # connection from %s refused (IP options)
+        # Connection from %s refused: too many connections
+        # connection from %s refused
+        /([Cc]onnection )from.*? (refused.*)/ ||
+        # [10.0.0.1]: connection refused
+        /: (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;
+      }
     }
   }';
 
     }
   }';
 
@@ -1979,6 +2482,12 @@ sub generate_parser {
       $removing_lines = 1;
     }
 
       $removing_lines = 1;
     }
 
+    # Convert constants.
+    while (/(\$[A-Z][A-Z_]*)\b/) {
+      my $constant = eval $1;
+      s/(\$[A-Z][A-Z_]*)\b/$constant/;
+    }
+
     $processed_parser .= $_."\n" unless $removing_lines;
 
     if (/^\s*#\s*ENDIF\s*\((.*?)\)/i) {
     $processed_parser .= $_."\n" unless $removing_lines;
 
     if (/^\s*#\s*ENDIF\s*\((.*?)\)/i) {
@@ -1988,7 +2497,7 @@ sub generate_parser {
       }
     }
   }
       }
     }
   }
-  print STDERR "# START OF PARSER:\n$processed_parser\n# END OF PARSER\n\n" if $debug;
+  print STDERR "# START OF PARSER:$processed_parser\n# END OF PARSER\n\n" if $debug;
 
   return $processed_parser;
 }
 
   return $processed_parser;
 }
@@ -2034,37 +2543,58 @@ sub print_header {
   if ($htm_fh) {
     print $htm_fh html_header($title);
     print $htm_fh "<ul>\n";
   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";
     }
     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";
     }
+
     if ($#queue_times >= 0) {
     if ($#queue_times >= 0) {
-      print $htm_fh "<li><a href=\"#all messages time\">Time spent on the queue: all messages</a>\n";
-      print $htm_fh "<li><a href=\"#messages with at least one remote delivery time\">Time spent on the queue: messages with at least one remote delivery</a>\n";
+      print $htm_fh "<li><a href=\"#Time spent on the queue all messages\">Time spent on the queue: all messages</a>\n";
+      print $htm_fh "<li><a href=\"#Time spent on the queue messages with at least one remote delivery\">Time spent on the queue: messages with at least one remote delivery</a>\n";
+    }
+
+    if ($#delivery_times >= 0) {
+      print $htm_fh "<li><a href=\"#Delivery times all messages\">Delivery times: all messages</a>\n";
+      print $htm_fh "<li><a href=\"#Delivery times messages with at least one remote delivery\">Delivery times: messages with at least one remote delivery</a>\n";
+    }
+
+    if ($#rcpt_times >= 0) {
+      print $htm_fh "<li><a href=\"#Receipt times all messages\">Receipt times</a>\n";
     }
     }
+
     print $htm_fh "<li><a href=\"#Relayed messages\">Relayed messages</a>\n" if $show_relay;
     if ($topcount) {
     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;
       foreach ('Host','Domain','Email','Edomain') {
         next unless $do_sender{$_};
       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) {
-        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";
+      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";
       }
       foreach ('Host','Domain','Email','Edomain') {
         next unless $do_sender{$_};
       }
       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) {
-        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_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";
       }
       }
+      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;
+      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;
     print $htm_fh "</ul>\n<hr>\n";
     }
     print $htm_fh "<li><a href=\"#errors\">List of errors</a>\n" if %errors_count;
     print $htm_fh "</ul>\n<hr>\n";
@@ -2089,9 +2619,10 @@ sub print_grandtotals {
 
   # Get the sender by headings and results. This is complicated as we can have
   # different numbers of columns.
 
   # Get the sender by headings and results. This is complicated as we can have
   # different numbers of columns.
-  my($sender_txt_header,$sender_html_header,$sender_txt_format,$sender_html_format);
+  my($sender_txt_header,$sender_txt_format,$sender_html_format);
   my(@received_totals,@delivered_totals);
   my($row_tablehead, $row_max);
   my(@received_totals,@delivered_totals);
   my($row_tablehead, $row_max);
+  my(@col_headers) = ('TOTAL', 'Volume', 'Messages', 'Addresses');
 
   foreach ('Host','Domain','Email','Edomain') {
     next unless $do_sender{$_};
 
   foreach ('Host','Domain','Email','Edomain') {
     next unless $do_sender{$_};
@@ -2103,15 +2634,15 @@ sub print_grandtotals {
       push(@received_totals,scalar(keys %{$received_data{$_}}));
       push(@delivered_totals,scalar(keys %{$delivered_data{$_}}));
     }
       push(@received_totals,scalar(keys %{$received_data{$_}}));
       push(@delivered_totals,scalar(keys %{$delivered_data{$_}}));
     }
-    $sender_html_header .= "<th>${_}s</th>";
     $sender_txt_header  .= " " x ($COLUMN_WIDTHS - length($_)) . $_ . 's';
     $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");
   }
 
   }
 
-  my $txt_format1 = "  %-16s %9s      %6d $sender_txt_format";
+  my $txt_format1 = "  %-16s %9s     %6d    %6s $sender_txt_format";
   my $txt_format2 = "  %6d %4.1f%% %6d %4.1f%%",
   my $txt_format2 = "  %6d %4.1f%% %6d %4.1f%%",
-  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%s</td>$sender_html_format<td align=\"right\">%d</td>";
+  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%s</td><td align=\"right\">%s</td><td align=\"right\">%s</td>$sender_html_format";
   my $htm_format2 = "<td align=\"right\">%d</td><td align=\"right\">%4.1f%%</td><td align=\"right\">%d</td><td align=\"right\">%4.1f%%</td>";
 
   if ($txt_fh) {
   my $htm_format2 = "<td align=\"right\">%d</td><td align=\"right\">%4.1f%%</td><td align=\"right\">%d</td><td align=\"right\">%4.1f%%</td>";
 
   if ($txt_fh) {
@@ -2119,43 +2650,24 @@ sub print_grandtotals {
     print $txt_fh "\n";
     print $txt_fh "Grand total summary\n";
     print $txt_fh "-------------------\n";
     print $txt_fh "\n";
     print $txt_fh "Grand total summary\n";
     print $txt_fh "-------------------\n";
-    print $txt_fh "                                    $sender_spaces           At least one address\n";
-    print $txt_fh "  TOTAL               Volume    Messages $sender_txt_header      Delayed       Failed\n";
+    print $txt_fh "                                              $sender_spaces           At least one address\n";
+    print $txt_fh "  TOTAL               Volume   Messages Addresses $sender_txt_header      Delayed       Failed\n";
   }
   if ($htm_fh) {
   }
   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 "<h2>Grand total summary</h2>\n";
     print $htm_fh "<table border=1>\n";
-    print $htm_fh "<tr><th>TOTAL</th><th>Volume</th><th>Messages</th>$sender_html_header<th colspan=2>At least one addr<br>Delayed</th><th colspan=2>At least one addr<br>Failed</th>\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";
   }
   }
-  if ($xls_fh)
-  {
-      $ws_global->write($row++, $col, "Grand total summary", $f_header2);
-
-      $row_tablehead = $row+1; # header-row of TOTALS table
-
-      &set_worksheet_line($ws_global, $row_tablehead, 0, ['Received', 'Delivered', 'TOTAL'], $f_headertab);
-
-      my @content= (
-        "Volume",
-        "Messages",
-        $sender_txt_header,
-        "At least one address Delayed (Total)",
-        "At least one address Delayed (Percent)",
-        "At least one address Failed (Total)",
-        "At least one address Failed (Percent)"
-      );
-
-      for (my $i=0; $i < scalar(@content); $i++)
-      {
-        $ws_global->write($row_tablehead+$i+1, 2, $content[$i], $f_default);
-        $row++;
-      }
-      $row_max = $row_tablehead+scalar(@content)+2; # continue from this row
+  if ($xls_fh) {
+    $ws_global->write($row++, 0, "Grand total summary", $f_header2);
+    $ws_global->write($row, 0, \@col_headers, $f_header2);
+    $ws_global->merge_range($row, scalar(@col_headers), $row, scalar(@col_headers)+1, "At least one addr Delayed", $f_header2_m);
+    $ws_global->merge_range($row, scalar(@col_headers)+2, $row, scalar(@col_headers)+3, "At least one addr Failed", $f_header2_m);
+    #$ws_global->write(++$row, scalar(@col_headers), ['Total','Percent','Total','Percent'], $f_header2);
   }
 
 
   }
 
 
-
   my($volume,$failed_count);
   if ($merge_reports) {
     $volume = volume_rounded($report_totals{Received}{Volume}, $report_totals{Received}{'Volume-gigs'});
   my($volume,$failed_count);
   if ($merge_reports) {
     $volume = volume_rounded($report_totals{Received}{Volume}, $report_totals{Received}{'Volume-gigs'});
@@ -2165,14 +2677,14 @@ sub print_grandtotals {
   }
   else {
     $volume = volume_rounded($total_received_data, $total_received_data_gigs);
   }
   else {
     $volume = volume_rounded($total_received_data, $total_received_data_gigs);
-    $failed_count = keys %had_error;
+    $failed_count = $message_errors;
   }
 
   {
     no integer;
 
     my @content=(
   }
 
   {
     no integer;
 
     my @content=(
-        $volume,$total_received_count,
+        $volume,$total_received_count,'',
         @received_totals,
         $delayed_count,
         ($total_received_count) ? ($delayed_count*100/$total_received_count) : 0,
         @received_totals,
         $delayed_count,
         ($total_received_count) ? ($delayed_count*100/$total_received_count) : 0,
@@ -2182,42 +2694,87 @@ sub print_grandtotals {
 
     printf $txt_fh ("$txt_format1$txt_format2\n", 'Received', @content) if $txt_fh;
     printf $htm_fh ("$htm_format1$htm_format2\n", 'Received', @content) if $htm_fh;
 
     printf $txt_fh ("$txt_format1$txt_format2\n", 'Received', @content) if $txt_fh;
     printf $htm_fh ("$htm_format1$htm_format2\n", 'Received', @content) if $htm_fh;
-    if ($xls_fh)
-    {
-      $row = $row_tablehead+1;
-      for (my $i=0; $i < scalar(@content); $i++)
-      {
+    if ($xls_fh) {
+      $ws_global->write(++$row, 0, 'Received', $f_default);
+      for (my $i=0; $i < scalar(@content); $i++) {
         if ($i == 4 || $i == 6) {
         if ($i == 4 || $i == 6) {
-          $ws_global->write($row+$i, 0, $content[$i]/100, $f_percent);
+          $ws_global->write($row, $i+1, $content[$i]/100, $f_percent);
         }
         else {
         }
         else {
-          $ws_global->write($row+$i, 0, $content[$i], $f_default);
+          $ws_global->write($row, $i+1, $content[$i], $f_default);
         }
       }
     }
   }
         }
       }
     }
   }
+
   if ($merge_reports) {
     $volume = volume_rounded($report_totals{Delivered}{Volume}, $report_totals{Delivered}{'Volume-gigs'});
   if ($merge_reports) {
     $volume = volume_rounded($report_totals{Delivered}{Volume}, $report_totals{Delivered}{'Volume-gigs'});
-    $total_delivered_count = get_report_total($report_totals{Delivered},'Messages');
+    $total_delivered_messages = get_report_total($report_totals{Delivered},'Messages');
+    $total_delivered_addresses = get_report_total($report_totals{Delivered},'Addresses');
   }
   else {
     $volume = volume_rounded($total_delivered_data, $total_delivered_data_gigs);
   }
 
   }
   else {
     $volume = volume_rounded($total_delivered_data, $total_delivered_data_gigs);
   }
 
-  my @content=($volume, $total_delivered_count, @delivered_totals);
-  printf $txt_fh ("$txt_format1\n\n", 'Delivered', @content) if $txt_fh;
-  printf $htm_fh ("$htm_format1\n\n", 'Delivered', @content) if $htm_fh;
-  printf $htm_fh "</table>\n" if $htm_fh;
-  if ($xls_fh)
-  {
+  my @content=($volume, $total_delivered_messages, $total_delivered_addresses, @delivered_totals);
+  printf $txt_fh ("$txt_format1\n", 'Delivered', @content) if $txt_fh;
+  printf $htm_fh ("$htm_format1\n", 'Delivered', @content) if $htm_fh;
 
 
-      $row = $row_tablehead+1;
-      for (my $i=0; $i < scalar(@content); $i++)
-      {
-        $ws_global->write($row+$i, 1, $content[$i], $f_default);
+  if ($xls_fh) {
+    $ws_global->write(++$row, 0, 'Delivered', $f_default);
+    for (my $i=0; $i < scalar(@content); $i++) {
+      $ws_global->write($row, $i+1, $content[$i], $f_default);
+    }
+  }
+
+  if ($merge_reports) {
+    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;
+      }
+    }
+  }
+  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;
       }
       }
-      $row = $row_max;
+    }
   }
   }
+
+  printf $txt_fh "\n"         if $txt_fh;
+  printf $htm_fh "</table>\n" if $htm_fh;
+  ++$row;
 }
 
 
 }
 
 
@@ -2238,7 +2795,7 @@ sub print_user_patterns {
     print $txt_fh "\n                       Total\n";
   }
   if ($htm_fh) {
     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";
     print $htm_fh "<table border=0 width=\"100%\">\n";
     print $htm_fh "<tr><td>\n";
     print $htm_fh "<table border=1>\n";
@@ -2269,10 +2826,7 @@ sub print_user_patterns {
     foreach $key (@user_descriptions) {
       printf $txt_fh ("$txt_format1\n",$key,$user_pattern_totals[$user_pattern_index]) if $txt_fh;
       printf $htm_fh ("$htm_format1\n",$key,$user_pattern_totals[$user_pattern_index]) if $htm_fh;
     foreach $key (@user_descriptions) {
       printf $txt_fh ("$txt_format1\n",$key,$user_pattern_totals[$user_pattern_index]) if $txt_fh;
       printf $htm_fh ("$htm_format1\n",$key,$user_pattern_totals[$user_pattern_index]) if $htm_fh;
-      if ($xls_fh)
-      {
-        &set_worksheet_line($ws_global, $row++, 0, [$key,$user_pattern_totals[$user_pattern_index]]);
-      }
+      $ws_global->write($row++, 0, [$key,$user_pattern_totals[$user_pattern_index]]) if $xls_fh;
       $user_pattern_index++;
     }
   }
       $user_pattern_index++;
     }
   }
@@ -2282,9 +2836,90 @@ sub print_user_patterns {
   {
     ++$row;
   }
   {
     ++$row;
   }
+
+  if ($hist_opt > 0) {
+    my $user_pattern_index = 0;
+    foreach $key (@user_descriptions) {
+      print_histogram($key, 'occurence', @{$user_pattern_interval_count[$user_pattern_index]});
+      $user_pattern_index++;
+    }
+  }
+}
+
+#######################################################################
+# print_rejects()
+#
+#  print_rejects();
+#
+# Print statistics about rejected mail.
+#######################################################################
+sub print_rejects {
+  my($format1,$reason);
+
+  my $txt_format1 = "  %-40s  %6d";
+  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%d</td>";
+
+  if ($txt_fh) {
+    print $txt_fh "Rejected mail by reason\n";
+    print $txt_fh "-----------------------";
+    print $txt_fh "\n                                             Total\n";
+  }
+  if ($htm_fh) {
+    print $htm_fh "<hr><a name=\"patterns\"></a><h2>Rejected mail by reason</h2>\n";
+    print $htm_fh "<table border=0 width=\"100%\"><tr><td><table border=1>\n";
+    print $htm_fh "<tr><th>&nbsp;</th><th>Total</th>\n";
+  }
+  if ($xls_fh) {
+    $ws_global->write($row++, $col, "Rejected mail by reason", $f_header2);
+    &set_worksheet_line($ws_global, $row++, 1, ["Total"], $f_headertab);
+  }
+
+
+  my $href = ($merge_reports) ? $report_totals{rejected_mail_by_reason} : \%rejected_count_by_reason;
+  my(@chartdatanames, @chartdatavals_count);
+
+  foreach $reason (top_n_sort($topcount, $href, undef, undef)) {
+    printf $txt_fh ("$txt_format1\n",$reason,$href->{$reason}) if $txt_fh;
+    printf $htm_fh ("$htm_format1\n",$reason,$href->{$reason}) if $htm_fh;
+    set_worksheet_line($ws_global, $row++, 0, [$reason,$href->{$reason}], $f_default) if $xls_fh;
+    push(@chartdatanames, $reason);
+    push(@chartdatavals_count, $href->{$reason});
+  }
+
+  $row++ if $xls_fh;
+  print $txt_fh "\n" if $txt_fh;
+
+  if ($htm_fh) {
+    print $htm_fh "</tr></table></td><td>";
+    if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_count > 0)) {
+      # calculate the graph
+      my @data = (
+         \@chartdatanames,
+         \@chartdatavals_count
+      );
+      my $graph = GD::Graph::pie->new(200, 200);
+      $graph->set(
+          x_label           => 'Rejection Reasons',
+          y_label           => 'Messages',
+          title             => 'By count',
+      );
+      my $gd = $graph->plot(\@data) or warn($graph->error);
+      if ($gd) {
+        open(IMG, ">$chartdir/rejections_count.png") or die "Could not write $chartdir/rejections_count.png: $!\n";
+        binmode IMG;
+        print IMG $gd->png;
+        close IMG;
+        print $htm_fh "<img src=\"$chartrel/rejections_count.png\">";
+      }
+    }
+    print $htm_fh "</td></tr></table>\n\n";
+  }
 }
 
 
 }
 
 
+
+
+
 #######################################################################
 # print_transport();
 #
 #######################################################################
 # print_transport();
 #
@@ -2307,15 +2942,13 @@ sub print_transport {
     print $txt_fh "\n                      Volume    Messages\n";
   }
   if ($htm_fh) {
     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 "<table border=0 width=\"100%\">\n";
-    print $htm_fh "<tr><td>\n";
-    print $htm_fh "<table border=1>\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";
   }
   if ($xls_fh) {
     print $htm_fh "<tr><th>&nbsp;</th><th>Volume</th><th>Messages</th>\n";
   }
   if ($xls_fh) {
-    $ws_global->write($row++, $col, "Deliveries by transport", $f_header2);
-    &set_worksheet_line($ws_global, $row++, 1, ["Volume", "Messages"], $f_headertab);
+    $ws_global->write(++$row, $col, "Deliveries by transport", $f_header2);
+    $ws_global->write(++$row, 1, ["Volume", "Messages"], $f_headertab);
   }
 
   my($key);
   }
 
   my($key);
@@ -2330,9 +2963,7 @@ sub print_transport {
       push(@chartdatavals_vol, $report_totals{transport}{$key}{'Volume-gigs'}*$gig + $report_totals{transport}{$key}{Volume} );
       printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
       printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
       push(@chartdatavals_vol, $report_totals{transport}{$key}{'Volume-gigs'}*$gig + $report_totals{transport}{$key}{Volume} );
       printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
       printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
-      if ($xls_fh) {
-        &set_worksheet_line($ws_global, $row++, 0, \@content, $f_default);
-      }
+      $ws_global->write(++$row, 0, \@content) if $xls_fh;
     }
   }
   else {
     }
   }
   else {
@@ -2345,16 +2976,14 @@ sub print_transport {
       push(@chartdatavals_vol, $transported_data_gigs{$key}*$gig + $transported_data{$key});
       printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
       printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
       push(@chartdatavals_vol, $transported_data_gigs{$key}*$gig + $transported_data{$key});
       printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
       printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
-      if ($xls_fh) {
-        &set_worksheet_line($ws_global, $row++, 0, \@content);
-      }
+      $ws_global->write(++$row, 0, \@content) if $xls_fh;
     }
   }
   print $txt_fh "\n" if $txt_fh;
   if ($htm_fh) {
     }
   }
   print $txt_fh "\n" if $txt_fh;
   if ($htm_fh) {
-    print $htm_fh "</table>\n";
-    print $htm_fh "</td><td>\n";
-    if ($HAVE_GD_Graph_pie && $charts)
+    print $htm_fh "</tr></table></td><td>";
+
+    if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_count > 0))
       {
       # calculate the graph
       my @data = (
       {
       # calculate the graph
       my @data = (
@@ -2376,9 +3005,9 @@ sub print_transport {
         print $htm_fh "<img src=\"$chartrel/transports_count.png\">";
       }
     }
         print $htm_fh "<img src=\"$chartrel/transports_count.png\">";
       }
     }
-    print $htm_fh "</td><td>\n";
+    print $htm_fh "</td><td>";
 
 
-    if ($HAVE_GD_Graph_pie && $charts) {
+    if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_vol > 0)) {
       my @data = (
          \@chartdatanames,
          \@chartdatavals_vol
       my @data = (
          \@chartdatanames,
          \@chartdatavals_vol
@@ -2389,19 +3018,16 @@ sub print_transport {
       );
       my $gd = $graph->plot(\@data) or warn($graph->error);
       if ($gd) {
       );
       my $gd = $graph->plot(\@data) or warn($graph->error);
       if ($gd) {
-        open(IMG, ">$chartdir/transports_vol.png") or die "Could not write $chartdir/transports_count.png: $!\n";
+        open(IMG, ">$chartdir/transports_vol.png") or die "Could not write $chartdir/transports_vol.png: $!\n";
         binmode IMG;
         print IMG $gd->png;
         close IMG;
         print $htm_fh "<img src=\"$chartrel/transports_vol.png\">";
       }
     }
         binmode IMG;
         print IMG $gd->png;
         close IMG;
         print $htm_fh "<img src=\"$chartrel/transports_vol.png\">";
       }
     }
+
     print $htm_fh "</td></tr></table>\n\n";
   }
     print $htm_fh "</td></tr></table>\n\n";
   }
-  if ($xls_fh) {
-    $row++;
-  }
-
 }
 
 
 }
 
 
@@ -2563,6 +3189,7 @@ sub print_errors {
 # All the diffs should produce no output.
 #
 #  options='-bydomain -byemail -byhost -byedomain'
 # All the diffs should produce no output.
 #
 #  options='-bydomain -byemail -byhost -byedomain'
+#  options="$options -show_rt1,2,4 -show_dt 1,2,4"
 #  options="$options -pattern 'Completed Messages' /Completed/"
 #  options="$options -pattern 'Received Messages' /<=/"
 #
 #  options="$options -pattern 'Completed Messages' /Completed/"
 #  options="$options -pattern 'Received Messages' /<=/"
 #
@@ -2592,6 +3219,11 @@ sub parse_old_eximstat_reports {
 
   my(%league_table_value_entered, %league_table_value_was_zero, %table_order);
 
 
   my(%league_table_value_entered, %league_table_value_was_zero, %table_order);
 
+  my(%user_pattern_index);
+  my $user_pattern_index = 0;
+  map {$user_pattern_index{$_} = $user_pattern_index++} @user_descriptions;
+  my $user_pattern_keys = join('|', @user_descriptions);
+
   while (<$fh>) {
     PARSE_OLD_REPORT_LINE:
     if (/Exim statistics from ([\d\-]+ [\d:]+(\s+[\+\-]\d+)?) to ([\d\-]+ [\d:]+(\s+[\+\-]\d+)?)/) {
   while (<$fh>) {
     PARSE_OLD_REPORT_LINE:
     if (/Exim statistics from ([\d\-]+ [\d:]+(\s+[\+\-]\d+)?) to ([\d\-]+ [\d:]+(\s+[\+\-]\d+)?)/) {
@@ -2599,25 +3231,50 @@ sub parse_old_eximstat_reports {
       $end   = $3 if ($3 gt $end);
     }
     elsif (/Grand total summary/) {
       $end   = $3 if ($3 gt $end);
     }
     elsif (/Grand total summary/) {
-      # Fill in $report_totals{Received|Delivered}{Volume|Messages|Hosts|Domains|...|Delayed|DelayedPercent|Failed|FailedPercent}
-      my(@fields);
+      # Fill in $report_totals{Received|Delivered}{Volume|Messages|Addresses|Hosts|Domains|...|Delayed|DelayedPercent|Failed|FailedPercent}
+      my(@fields, @delivered_fields);
+      my $doing_table = 0;
       while (<$fh>) {
         $_ = html2txt($_);       #Convert general HTML markup to text.
         s/At least one addr//g;  #Another part of the HTML output we don't want.
 
       while (<$fh>) {
         $_ = html2txt($_);       #Convert general HTML markup to text.
         s/At least one addr//g;  #Another part of the HTML output we don't want.
 
-#  TOTAL               Volume    Messages    Hosts Domains      Delayed       Failed
-#  Received              26MB         237      177      23       8  3.4%     28 11.8%
-#  Delivered             13MB         233       99      88
+#  TOTAL               Volume    Messages Addresses   Hosts Domains      Delayed       Failed
+#  Received              26MB         237               177      23       8  3.4%     28 11.8%
+#  Delivered             13MB         233       250      99      88
         if (/TOTAL\s+(.*?)\s*$/) {
         if (/TOTAL\s+(.*?)\s*$/) {
-          @fields = split(/\s+/,$1);
+          $doing_table = 1;
+          @delivered_fields = split(/\s+/,$1);
+
           #Delayed and Failed have two columns each, so add the extra field names in.
           #Delayed and Failed have two columns each, so add the extra field names in.
-          splice(@fields,-1,1,'DelayedPercent','Failed','FailedPercent');
+          splice(@delivered_fields,-1,1,'DelayedPercent','Failed','FailedPercent');
+
+          # Addresses only figure in the Delivered row, so remove them from the
+          # normal fields.
+          @fields = grep !/Addresses/, @delivered_fields;
         }
         }
-        elsif (/(Received|Delivered)\s+(.*?)\s*$/) {
+        elsif (/(Received)\s+(.*?)\s*$/) {
           print STDERR "Parsing $_" if $debug;
           add_to_totals($report_totals{$1},\@fields,$2);
         }
           print STDERR "Parsing $_" if $debug;
           add_to_totals($report_totals{$1},\@fields,$2);
         }
-        last if (/Delivered/);   #Last line of this section.
+        elsif (/(Delivered)\s+(.*?)\s*$/) {
+          print STDERR "Parsing $_" if $debug;
+          add_to_totals($report_totals{$1},\@delivered_fields,$2);
+          my $data = $2;
+          # If we're merging an old report which doesn't include addresses,
+          # then use the Messages field instead.
+          unless (grep(/Addresses/, @delivered_fields)) {
+            my %tmp;
+            line_to_hash(\%tmp,\@delivered_fields,$data);
+            add_to_totals($report_totals{Delivered},['Addresses'],$tmp{Messages});
+          }
+        }
+        elsif (/(Temp Rejects|Rejects|Ham|Spam)\s+(.*?)\s*$/) {
+          print STDERR "Parsing $_" if $debug;
+          add_to_totals($report_totals{$1},['Messages','Hosts'],$2);
+        }
+        else {
+          last if $doing_table;
+        }
       }
     }
 
       }
     }
 
@@ -2639,6 +3296,12 @@ sub parse_old_eximstat_reports {
       }
     }
 
       }
     }
 
+    elsif (/(^|<h2>)($user_pattern_keys) per /o) {
+      # Parse User defined pattern histograms if they exist.
+      parse_histogram($fh, $user_pattern_interval_count[$user_pattern_index{$2}] );
+    }
+
+
     elsif (/Deliveries by transport/i) {
 #Deliveries by transport
 #-----------------------
     elsif (/Deliveries by transport/i) {
 #Deliveries by transport
 #-----------------------
@@ -2658,34 +3321,15 @@ sub parse_old_eximstat_reports {
         last if (/^\s*$/);              #Finished if we have a blank line.
       }
     }
         last if (/^\s*$/);              #Finished if we have a blank line.
       }
     }
-    elsif (/(Messages received|Deliveries) per/) {
-#      Messages received per hour (each dot is 2 messages)
-#---------------------------------------------------
-#
-#00-01    106 .....................................................
-#01-02    103 ...................................................
-
-      # Set a pointer to the interval array so we can use the same code
-      # block for both messages received and delivered.
-      my $interval_aref = ($1 eq 'Deliveries') ? \@delivered_interval_count : \@received_interval_count;
-      my $reached_table = 0;
-      while (<$fh>) {
-        $reached_table = 1 if (/^00/);
-        next unless $reached_table;
-        print STDERR "Parsing $_" if $debug;
-        if (/^(\d+):(\d+)\s+(\d+)/) {           #hh:mm start time format ?
-          $$interval_aref[($1*60 + $2)/$hist_interval] += $3 if $hist_opt;
-        }
-        elsif (/^(\d+)-(\d+)\s+(\d+)/) {        #hh-hh start-end time format ?
-          $$interval_aref[($1*60)/$hist_interval] += $3 if $hist_opt;
-        }
-        else {                                  #Finished the table ?
-          last;
-        }
-      }
+    elsif (/Messages received per/) {
+      parse_histogram($fh, \@received_interval_count);
+    }
+    elsif (/Deliveries per/) {
+      parse_histogram($fh, \@delivered_interval_count);
     }
 
     }
 
-    elsif (/Time spent on the queue: (all messages|messages with at least one remote delivery)/) {
+    #elsif (/Time spent on the queue: (all messages|messages with at least one remote delivery)/) {
+    elsif (/(Time spent on the queue|Delivery times|Receipt times): ((\S+) messages|messages with at least one remote delivery)((<[^>]*>)*\s*)$/) {
 #Time spent on the queue: all messages
 #-------------------------------------
 #
 #Time spent on the queue: all messages
 #-------------------------------------
 #
@@ -2697,33 +3341,67 @@ sub parse_old_eximstat_reports {
 
       # Set a pointer to the queue bin so we can use the same code
       # block for both all messages and remote deliveries.
 
       # Set a pointer to the queue bin so we can use the same code
       # block for both all messages and remote deliveries.
-      my $bin_aref = ($1 eq 'all messages') ? \@queue_bin : \@remote_queue_bin;
-      my $reached_table = 0;
+      #my $bin_aref = ($1 eq 'all messages') ? \@qt_all_bin : \@qt_remote_bin;
+      my($bin_aref, $times_aref, $overflow_sref);
+      if ($1 eq 'Time spent on the queue') {
+        $times_aref = \@queue_times;
+        if ($2 eq 'all messages') {
+          $bin_aref = \@qt_all_bin;
+          $overflow_sref = \$qt_all_overflow;
+        }
+        else {
+          $bin_aref = \@qt_remote_bin;
+          $overflow_sref = \$qt_remote_overflow;
+        }
+      }
+      elsif ($1 eq 'Delivery times') {
+        $times_aref = \@delivery_times;
+        if ($2 eq 'all messages') {
+          $bin_aref = \@dt_all_bin;
+          $overflow_sref = \$dt_all_overflow;
+        }
+        else {
+          $bin_aref = \@dt_remote_bin;
+          $overflow_sref = \$dt_remote_overflow;
+        }
+      }
+      else {
+        unless (exists $rcpt_times_bin{$3}) {
+          initialise_rcpt_times($3);
+        }
+        $bin_aref = $rcpt_times_bin{$3};
+        $times_aref = \@rcpt_times;
+        $overflow_sref = \$rcpt_times_overflow{$3};
+      }
+
+
+      my ($blank_lines, $reached_table) = (0,0);
       while (<$fh>) {
         $_ = html2txt($_);              #Convert general HTML markup to text.
       while (<$fh>) {
         $_ = html2txt($_);              #Convert general HTML markup to text.
-        $reached_table = 1 if (/^\s*Under/);
+        # The table is preceded by one blank line, and has one blank line
+        # following it. As the table may be empty, the best way to determine
+        # that we've finished it is to look for the second blank line.
+        ++$blank_lines if /^\s*$/;
+        last if ($blank_lines >=2);     #Finished the table ?
+        $reached_table = 1 if (/\d/);
         next unless $reached_table;
         my $previous_seconds_on_queue = 0;
         if (/^\s*(Under|Over|)\s+(\d+[smhdw])\s+(\d+)/) {
           print STDERR "Parsing $_" if $debug;
         next unless $reached_table;
         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');
           my($i);
           my $time_on_queue = ($seconds + $previous_seconds_on_queue) / 2;
           $previous_seconds_on_queue = $seconds;
           $time_on_queue = $seconds * 2 if ($modifier eq 'Over');
           my($i);
-          for ($i = 0; $i <= $#queue_times; $i++) {
-            if ($time_on_queue < $queue_times[$i]) {
+          for ($i = 0; $i <= $#$times_aref; $i++) {
+            if ($time_on_queue < $times_aref->[$i]) {
               $$bin_aref[$i] += $count;
               last;
             }
           }
               $$bin_aref[$i] += $count;
               last;
             }
           }
-          # There's only one counter for messages going over the queue
-          # times so make sure we only count it once.
-          $queue_more_than += $count if (($bin_aref == \@queue_bin) && ($i > $#queue_times));
-        }
-        else {
-          last;                             #Finished the table ?
+          $$overflow_sref += $count if ($i > $#$times_aref);
+
         }
       }
     }
         }
       }
     }
@@ -2782,73 +3460,88 @@ sub parse_old_eximstat_reports {
 
       #As this section processes multiple different table categories,
       #set up pointers to the hashes to be updated.
 
       #As this section processes multiple different table categories,
       #set up pointers to the hashes to be updated.
-      my($count_href,$data_href,$data_gigs_href);
+      my($messages_href,$addresses_href,$data_href,$data_gigs_href);
       if ($category =~ /local sender/) {
       if ($category =~ /local sender/) {
-        $count_href      = \%received_count_user;
+        $messages_href   = \%received_count_user;
+        $addresses_href  = undef;
         $data_href       = \%received_data_user;
         $data_gigs_href  = \%received_data_gigs_user;
       }
       elsif ($category =~ /sending (\S+?)s?\b/) {
         #Top 50 sending (host|domain|email|edomain)s
         #Top sending (host|domain|email|edomain)
         $data_href       = \%received_data_user;
         $data_gigs_href  = \%received_data_gigs_user;
       }
       elsif ($category =~ /sending (\S+?)s?\b/) {
         #Top 50 sending (host|domain|email|edomain)s
         #Top sending (host|domain|email|edomain)
-        $count_href      = \%{$received_count{"\u$1"}};
+        $messages_href   = \%{$received_count{"\u$1"}};
         $data_href       = \%{$received_data{"\u$1"}};
         $data_gigs_href  = \%{$received_data_gigs{"\u$1"}};
       }
       elsif ($category =~ /local destination/) {
         $data_href       = \%{$received_data{"\u$1"}};
         $data_gigs_href  = \%{$received_data_gigs{"\u$1"}};
       }
       elsif ($category =~ /local destination/) {
-        $count_href      = \%delivered_count_user;
+        $messages_href   = \%delivered_messages_user;
+        $addresses_href  = \%delivered_addresses_user;
         $data_href       = \%delivered_data_user;
         $data_gigs_href  = \%delivered_data_gigs_user;
       }
         $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
       elsif ($category =~ /(\S+) destination/) {
         #Top 50 (host|domain|email|edomain) destinations
         #Top (host|domain|email|edomain) destination
-        $count_href      = \%{$delivered_count{"\u$1"}};
+        $messages_href   = \%{$delivered_messages{"\u$1"}};
+        $addresses_href  = \%{$delivered_addresses{"\u$1"}};
         $data_href       = \%{$delivered_data{"\u$1"}};
         $data_gigs_href  = \%{$delivered_data_gigs{"\u$1"}};
       }
         $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;
+      }
 
       my $reached_table = 0;
 
       my $reached_table = 0;
+      my $row_re;
       while (<$fh>) {
         # Watch out for empty tables.
       while (<$fh>) {
         # Watch out for empty tables.
-        goto PARSE_OLD_REPORT_LINE if (/<h2>/ or /^[a-zA-Z]/);
+        goto PARSE_OLD_REPORT_LINE if (/<h2>/ or (/^\s*[a-zA-Z]/ && !/^\s*Messages/));
 
         $_ = html2txt($_);              #Convert general HTML markup to text.
 
 
         $_ = html2txt($_);              #Convert general HTML markup to text.
 
-
-        $reached_table = 1 if (/^\s*\d/);
+        # Messages      Addresses  Bytes  Average
+        if (/^\s*Messages/) {
+          my $pattern = '^\s*(\d+)';
+          $pattern .= (/Addresses/) ? '\s+(\d+)' : '()';
+          $pattern .= (/Bytes/)     ? '\s+([\dKMGB]+)' : '()';
+          $pattern .= (/Average/)   ? '\s+[\dKMGB]+' : '';
+          $pattern .= '\s+(.*?)\s*$';
+          $row_re = qr/$pattern/;
+          $reached_table = 1;
+          next;
+        }
         next unless $reached_table;
 
         next unless $reached_table;
 
-        # Remove optional 'average value' column.
-        s/^\s*(\d+)\s+(\S+)\s+(\d+(KB|MB|GB|\b)\s+)/$1 $2 /;
-
-        if (/^\s*(\d+)\s+(\S+)\s*(.*?)\s*$/) {
-          my($count,$rounded_volume,$entry) = ($1,$2,$3);
-          #Note: $entry fields can be both null and can contain spaces.
+        my($messages, $addresses, $rounded_volume, $entry);
 
 
-          #Add the entry into the %table_order hash if it has a rounded volume (KB/MB/GB).
-          push(@{$table_order{$rounded_volume}{$by_count_or_volume}},$entry) if ($rounded_volume =~ /\D/);
-
-          unless ($league_table_value_entered{$entry}) {
-            $league_table_value_entered{$entry} = 1;
-            unless ($$count_href{$entry}) {
-              $$count_href{$entry}     = 0;
-              $$data_href{$entry}      = 0;
-              $$data_gigs_href{$entry} = 0;
-              $league_table_value_was_zero{$entry} = 1;
-            }
-
-            $$count_href{$entry} += $count;
-            #Add the rounded value to the data and data_gigs hashes.
-            un_round($rounded_volume,\$$data_href{$entry},\$$data_gigs_href{$entry});
-            print STDERR "$category by $by_count_or_volume: added $count,$rounded_volume to $entry\n" if $debug;
-          }
+        if (/$row_re/) {
+          ($messages, $addresses, $rounded_volume, $entry) = ($1, $2, $3, $4);
         }
         }
-        else {         #Finished the table ?
+        else {
+          #Else we have finished the table and we may need to do some
+          #kludging to retain the order of the entries.
+
           if ($by_count_or_volume =~ /volume/) {
             #Add a few bytes to appropriate entries to preserve the order.
           if ($by_count_or_volume =~ /volume/) {
             #Add a few bytes to appropriate entries to preserve the order.
-
-            my($rounded_volume);
             foreach $rounded_volume (keys %table_order) {
               #For each rounded volume, we want to create a list which has things
               #ordered from the volume table at the front, and additional things
             foreach $rounded_volume (keys %table_order) {
               #For each rounded volume, we want to create a list which has things
               #ordered from the volume table at the front, and additional things
@@ -2872,9 +3565,37 @@ sub parse_old_eximstat_reports {
               }
             }
           }
               }
             }
           }
-
           last;
         }
           last;
         }
+
+        # Store a new table entry.
+
+        # Add the entry into the %table_order hash if it has a rounded
+        # volume (KB/MB/GB).
+        push(@{$table_order{$rounded_volume}{$by_count_or_volume}},$entry) if ($rounded_volume =~ /\D/);
+
+        unless ($league_table_value_entered{$entry}) {
+          $league_table_value_entered{$entry} = 1;
+          unless ($$messages_href{$entry}) {
+            $$messages_href{$entry}  = 0;
+            $$addresses_href{$entry} = 0;
+            $$data_href{$entry}      = 0;
+            $$data_gigs_href{$entry} = 0;
+            $league_table_value_was_zero{$entry} = 1;
+          }
+
+          $$messages_href{$entry} += $messages;
+
+          # When adding the addresses, be aware that we could be merging
+          # an old report which does not include addresses. In this case,
+          # we add the messages instead.
+          $$addresses_href{$entry} += ($addresses) ? $addresses : $messages;
+
+          #Add the rounded value to the data and data_gigs hashes.
+          un_round($rounded_volume,\$$data_href{$entry},\$$data_gigs_href{$entry}) if $rounded_volume;
+          print STDERR "$category by $by_count_or_volume: added $messages,$rounded_volume to $entry\n" if $debug;
+        }
+
       }
     }
     elsif (/List of errors/) {
       }
     }
     elsif (/List of errors/) {
@@ -2922,6 +3643,35 @@ sub parse_old_eximstat_reports {
   }
 }
 
   }
 }
 
+#######################################################################
+# parse_histogram($fh, \@delivered_interval_count);
+# Parse a histogram into the provided array of counters.
+#######################################################################
+sub parse_histogram {
+  my($fh, $counters_aref) = @_;
+
+  #      Messages received per hour (each dot is 2 messages)
+  #---------------------------------------------------
+  #
+  #00-01    106 .....................................................
+  #01-02    103 ...................................................
+
+  my $reached_table = 0;
+  while (<$fh>) {
+    $reached_table = 1 if (/^00/);
+    next unless $reached_table;
+    print STDERR "Parsing $_" if $debug;
+    if (/^(\d+):(\d+)\s+(\d+)/) {           #hh:mm start time format ?
+      $$counters_aref[($1*60 + $2)/$hist_interval] += $3 if $hist_opt;
+    }
+    elsif (/^(\d+)-(\d+)\s+(\d+)/) {        #hh-hh start-end time format ?
+      $$counters_aref[($1*60)/$hist_interval] += $3 if $hist_opt;
+    }
+    else {                                  #Finished the table ?
+      last;
+    }
+  }
+}
 
 
 #######################################################################
 
 
 #######################################################################
@@ -2954,7 +3704,7 @@ sub update_relayed {
 #
 #  add_to_totals(\%totals,\@keys,$values);
 #
 #
 #  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
 # as the hash keys.
 #
 # If the value contains a '%', then the value is set rather than added. Otherwise, we
@@ -2963,22 +3713,39 @@ sub update_relayed {
 sub add_to_totals {
   my($totals_href,$keys_aref,$values) = @_;
   my(@values) = split(/\s+/,$values);
 sub add_to_totals {
   my($totals_href,$keys_aref,$values) = @_;
   my(@values) = split(/\s+/,$values);
-  my(@keys) = @$keys_aref;        #Make a copy as we destroy the one we use.
-  my($value);
-  foreach $value (@values) {
-    my $key = shift(@keys) or next;
-    if ($value =~ /%/) {
-      $$totals_href{$key} = $value;
+
+  for(my $i = 0; $i < @values && $i < @$keys_aref; ++$i) {
+    my $key = $keys_aref->[$i];
+    if ($values[$i] =~ /%/) {
+      $$totals_href{$key} = $values[$i];
     }
     else {
       $$totals_href{$key} = 0 unless ($$totals_href{$key});
       $$totals_href{"$key-gigs"} = 0 unless ($$totals_href{"$key-gigs"});
     }
     else {
       $$totals_href{$key} = 0 unless ($$totals_href{$key});
       $$totals_href{"$key-gigs"} = 0 unless ($$totals_href{"$key-gigs"});
-      un_round($value, \$$totals_href{$key}, \$$totals_href{"$key-gigs"});
-      print STDERR "Added $value to $key - $$totals_href{$key} , " . $$totals_href{"$key-gigs"} . "GB.\n" if $debug;
+      un_round($values[$i], \$$totals_href{$key}, \$$totals_href{"$key-gigs"});
+      print STDERR "Added $values[$i] to $key - $$totals_href{$key} , " . $$totals_href{"$key-gigs"} . "GB.\n" if $debug;
     }
   }
 }
 
     }
   }
 }
 
+
+#######################################################################
+# line_to_hash();
+#
+#  line_to_hash(\%hash,\@keys,$line);
+#
+# Given a line of space separated values, set them into the provided hash
+# using @keys as the hash keys.
+#######################################################################
+sub line_to_hash {
+  my($href,$keys_aref,$values) = @_;
+  my(@values) = split(/\s+/,$values);
+  for(my $i = 0; $i < @values && $i < @$keys_aref; ++$i) {
+    $$href{$keys_aref->[$i]} = $values[$i];
+  }
+}
+
+
 #######################################################################
 # get_report_total();
 #
 #######################################################################
 # get_report_total();
 #
@@ -3012,7 +3779,7 @@ sub html2txt {
   # <Userid@Domain> words, so explicitly specify the HTML tags we will remove
   # (the ones used by this program). If someone is careless enough to have their
   # Userid the same as an HTML tag, there's not much we can do about it.
   # <Userid@Domain> words, so explicitly specify the HTML tags we will remove
   # (the ones used by this program). If someone is careless enough to have their
   # Userid the same as an HTML tag, there's not much we can do about it.
-  s/<\/?(html|head|title|body|h\d|ul|li|a\s+|table|tr|td|th|pre|hr|p|br)\b.*?>/ /og;
+  s/<\/?(html|head|title|body|h\d|ul|li|a\s+|table|tr|td|th|pre|hr|p|br)\b.*?>/ /g;
 
   s/\&lt\;/\</og;             #Convert '&lt;' to '<'.
   s/\&gt\;/\>/og;             #Convert '&gt;' to '>'.
 
   s/\&lt\;/\</og;             #Convert '&lt;' to '<'.
   s/\&gt\;/\>/og;             #Convert '&gt;' to '>'.
@@ -3033,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,
 # 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.
 #######################################################################
 # be preserved precisely, then you get that by not using shell
 # variables.
 #######################################################################
@@ -3072,6 +3839,41 @@ sub set_worksheet_line {
 
 }
 
 
 }
 
+#######################################################################
+# @rcpt_times = parse_time_list($string);
+#
+# 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.
+# Return () if $string eq '0'.
+#######################################################################
+sub parse_time_list {
+  my($string) = @_;
+  if (! defined $string) {
+    return(60, 5*60, 15*60, 30*60, 60*60, 3*60*60, 6*60*60, 12*60*60, 24*60*60);
+  }
+  my(@times) = split(/,/, $string);
+  foreach my $q (@times) { $q = eval($q) + 0 }
+  @times = sort { $a <=> $b } @times;
+  @times = () if ($#times == 0 && $times[0] == 0);
+  return(@times);
+}
+
+
+#######################################################################
+# initialise_rcpt_times($protocol);
+# Initialise an array of rcpt_times to 0 for the specified protocol.
+#######################################################################
+sub initialise_rcpt_times {
+  my($protocol) = @_;
+  for (my $i = 0; $i <= $#rcpt_times; ++$i) {
+    $rcpt_times_bin{$protocol}[$i] = 0;
+  }
+  $rcpt_times_overflow{$protocol} = 0;
+}
+
+
 ##################################################
 #                 Main Program                   #
 ##################################################
 ##################################################
 #                 Main Program                   #
 ##################################################
@@ -3095,14 +3897,14 @@ $charts_option_specified = 0;
 $chartrel = ".";
 $chartdir = ".";
 
 $chartrel = ".";
 $chartdir = ".";
 
-@queue_times = (60, 5*60, 15*60, 30*60, 60*60, 3*60*60, 6*60*60,
-                12*60*60, 24*60*60);
+@queue_times = parse_time_list();
+@rcpt_times = ();
+@delivery_times = ();
 
 $last_offset = '';
 $offset_seconds = 0;
 
 $row=1;
 
 $last_offset = '';
 $offset_seconds = 0;
 
 $row=1;
-$row_league_table=1;
 $col=0;
 $col_hist=0;
 $run_hist=0;
 $col=0;
 $col_hist=0;
 $run_hist=0;
@@ -3110,22 +3912,13 @@ my(%output_files);     # What output files have been specified?
 
 # Decode options
 
 
 # Decode options
 
-while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-')
-  {
+while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-') {
   if    ($ARGV[0] =~ /^\-h(\d+)$/) { $hist_opt = $1 }
   elsif ($ARGV[0] =~ /^\-ne$/)     { $show_errors = 0 }
   if    ($ARGV[0] =~ /^\-h(\d+)$/) { $hist_opt = $1 }
   elsif ($ARGV[0] =~ /^\-ne$/)     { $show_errors = 0 }
-  elsif ($ARGV[0] =~ /^\-nr(.?)(.*)\1$/)
-    {
+  elsif ($ARGV[0] =~ /^\-nr(.?)(.*)\1$/) {
     if ($1 eq "") { $show_relay = 0 } else { $relay_pattern = $2 }
     if ($1 eq "") { $show_relay = 0 } else { $relay_pattern = $2 }
-    }
-  elsif ($ARGV[0] =~ /^\-q([,\d\+\-\*\/]+)$/)
-    {
-    @queue_times = split(/,/, $1);
-    my($q);
-    foreach $q (@queue_times) { $q = eval($q) + 0 }
-    @queue_times = sort { $a <=> $b } @queue_times;
-    @queue_times = () if ($#queue_times == 0 && $queue_times[0] == 0);
-    }
+  }
+  elsif ($ARGV[0] =~ /^\-q([,\d\+\-\*\/]+)$/) { @queue_times = parse_time_list($1) }
   elsif ($ARGV[0] =~ /^-nt$/)       { $show_transport = 0 }
   elsif ($ARGV[0] =~ /^\-nt(.?)(.*)\1$/)
     {
   elsif ($ARGV[0] =~ /^-nt$/)       { $show_transport = 0 }
   elsif ($ARGV[0] =~ /^\-nt(.?)(.*)\1$/)
     {
@@ -3158,7 +3951,11 @@ 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] =~ /^-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] =~ /^-nvr$/)      { $volume_rounding = 0 }
+  elsif ($ARGV[0] =~ /^-show_rt([,\d\+\-\*\/]+)?$/) { @rcpt_times = parse_time_list($1) }
+  elsif ($ARGV[0] =~ /^-show_dt([,\d\+\-\*\/]+)?$/) { @delivery_times = parse_time_list($1) }
   elsif ($ARGV[0] =~ /^-d$/)        { $debug = 1 }
   elsif ($ARGV[0] =~ /^--?h(elp)?$/){ help() }
   elsif ($ARGV[0] =~ /^-t_remote_users$/) { $include_remote_users = 1 }
   elsif ($ARGV[0] =~ /^-d$/)        { $debug = 1 }
   elsif ($ARGV[0] =~ /^--?h(elp)?$/){ help() }
   elsif ($ARGV[0] =~ /^-t_remote_users$/) { $include_remote_users = 1 }
@@ -3192,8 +3989,7 @@ while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-')
   $do_sender{Host} = 1 unless ($do_sender{Domain} || $do_sender{Email} || $do_sender{Edomain});
 
   # prepare xls Excel Workbook
   $do_sender{Host} = 1 unless ($do_sender{Domain} || $do_sender{Email} || $do_sender{Edomain});
 
   # prepare xls Excel Workbook
-  if (defined $xls_fh)
-  {
+  if (defined $xls_fh) {
 
     # Create a new Excel workbook
     $workbook  = Spreadsheet::WriteExcel->new($xls_fh);
 
     # Create a new Excel workbook
     $workbook  = Spreadsheet::WriteExcel->new($xls_fh);
@@ -3208,9 +4004,6 @@ while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-')
       $ws_relayed = $workbook->addworksheet('Relayed Messages');
       $ws_relayed->set_column(1, 2,  80);
     }
       $ws_relayed = $workbook->addworksheet('Relayed Messages');
       $ws_relayed->set_column(1, 2,  80);
     }
-    if ($topcount) {
-    $ws_top50 = $workbook->addworksheet('Deliveries');
-    }
     if ($show_errors) {
       $ws_errors = $workbook->addworksheet('Errors');
     }
     if ($show_errors) {
       $ws_errors = $workbook->addworksheet('Errors');
     }
@@ -3238,6 +4031,13 @@ while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-')
     $f_header2->set_valign();
     # $ws_global->write($row++, 2, "Testing Headers 2", $f_header2);
 
     $f_header2->set_valign();
     # $ws_global->write($row++, 2, "Testing Headers 2", $f_header2);
 
+    # Create another header2 for use in merged cells.
+    $f_header2_m = $workbook->add_format();
+    $f_header2_m->set_bold();
+    $f_header2_m->set_size('8');
+    $f_header2_m->set_valign();
+    $f_header2_m->set_align('center');
+
     $f_percent = $workbook->add_format();
     $f_percent->set_num_format('0.0%');
 
     $f_percent = $workbook->add_format();
     $f_percent->set_num_format('0.0%');
 
@@ -3249,13 +4049,19 @@ while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-')
   }
 
 
   }
 
 
+# Initialise the queue/delivery/rcpt time counters.
 for (my $i = 0; $i <= $#queue_times; $i++) {
 for (my $i = 0; $i <= $#queue_times; $i++) {
-  $queue_bin[$i] = 0;
-  $remote_queue_bin[$i] = 0;
+  $qt_all_bin[$i] = 0;
+  $qt_remote_bin[$i] = 0;
+}
+for (my $i = 0; $i <= $#delivery_times; $i++) {
+  $dt_all_bin[$i] = 0;
+  $dt_remote_bin[$i] = 0;
 }
 }
+initialise_rcpt_times('all');
 
 
-# Compute the number of slots for the histogram
 
 
+# Compute the number of slots for the histogram
 if ($hist_opt > 0)
   {
   if ($hist_opt > 60 || 60 % $hist_opt != 0)
 if ($hist_opt > 0)
   {
   if ($hist_opt > 60 || 60 % $hist_opt != 0)
@@ -3267,7 +4073,13 @@ if ($hist_opt > 0)
   $hist_number = (24*60)/$hist_interval;        #Number of intervals per day.
   @received_interval_count = (0) x $hist_number;
   @delivered_interval_count = (0) x $hist_number;
   $hist_number = (24*60)/$hist_interval;        #Number of intervals per day.
   @received_interval_count = (0) x $hist_number;
   @delivered_interval_count = (0) x $hist_number;
+  my $user_pattern_index = 0;
+  for (my $user_pattern_index = 0; $user_pattern_index <= $#user_patterns; ++$user_pattern_index) {
+    @{$user_pattern_interval_count[$user_pattern_index]} = (0) x $hist_number;
   }
   }
+  @dt_all_bin = (0) x $hist_number;
+  @dt_remote_bin = (0) x $hist_number;
+}
 
 #$queue_unknown = 0;
 
 
 #$queue_unknown = 0;
 
@@ -3277,15 +4089,20 @@ $total_received_count = 0;
 
 $total_delivered_data = 0;
 $total_delivered_data_gigs = 0;
 
 $total_delivered_data = 0;
 $total_delivered_data_gigs = 0;
-$total_delivered_count = 0;
+$total_delivered_messages = 0;
+$total_delivered_addresses = 0;
 
 
-$queue_more_than = 0;
+$qt_all_overflow = 0;
+$qt_remote_overflow = 0;
+$dt_all_overflow = 0;
+$dt_remote_overflow = 0;
 $delayed_count = 0;
 $relayed_unshown = 0;
 $delayed_count = 0;
 $relayed_unshown = 0;
+$message_errors = 0;
 $begin = "9999-99-99 99:99:99";
 $end = "0000-00-00 00:00:00";
 my($section,$type);
 $begin = "9999-99-99 99:99:99";
 $end = "0000-00-00 00:00:00";
 my($section,$type);
-foreach $section ('Received','Delivered') {
+foreach $section ('Received','Delivered','Temp Rejects', 'Rejects','Ham','Spam') {
   foreach $type ('Volume','Messages','Delayed','Failed','Hosts','Domains','Emails','Edomains') {
     $report_totals{$section}{$type} = 0;
   }
   foreach $type ('Volume','Messages','Delayed','Failed','Hosts','Domains','Emails','Edomains') {
     $report_totals{$section}{$type} = 0;
   }
@@ -3328,7 +4145,7 @@ else {
 }
 
 
 }
 
 
-if ($begin eq "9999-99-99 99:99:99") {
+if ($begin eq "9999-99-99 99:99:99" && ! $emptyOK) {
   print STDERR "**** No valid log lines read\n";
   exit 1;
 }
   print STDERR "**** No valid log lines read\n";
   exit 1;
 }
@@ -3340,20 +4157,36 @@ print_grandtotals();
 # Print counts of user specified patterns if required.
 print_user_patterns() if @user_patterns;
 
 # Print counts of user specified patterns if required.
 print_user_patterns() if @user_patterns;
 
+# Print rejection reasons.
+# print_rejects();
+
 # Print totals by transport if required.
 print_transport() if $show_transport;
 
 # Print the deliveries per interval as a histogram, unless configured not to.
 # First find the maximum in one interval and scale accordingly.
 if ($hist_opt > 0) {
 # Print totals by transport if required.
 print_transport() if $show_transport;
 
 # Print the deliveries per interval as a histogram, unless configured not to.
 # First find the maximum in one interval and scale accordingly.
 if ($hist_opt > 0) {
-  print_histogram("Messages received", @received_interval_count);
-  print_histogram("Deliveries", @delivered_interval_count);
+  print_histogram("Messages received", 'message', @received_interval_count);
+  print_histogram("Deliveries", 'delivery', @delivered_interval_count);
 }
 
 # Print times on queue if required.
 if ($#queue_times >= 0) {
 }
 
 # Print times on queue if required.
 if ($#queue_times >= 0) {
-  print_queue_times("all messages", \@queue_bin,$queue_more_than);
-  print_queue_times("messages with at least one remote delivery",\@remote_queue_bin,$queue_more_than);
+  print_duration_table("Time spent on the queue", "all messages", \@queue_times, \@qt_all_bin,$qt_all_overflow);
+  print_duration_table("Time spent on the queue", "messages with at least one remote delivery", \@queue_times, \@qt_remote_bin,$qt_remote_overflow);
+}
+
+# Print delivery times if required.
+if ($#delivery_times >= 0) {
+  print_duration_table("Delivery times", "all messages", \@delivery_times, \@dt_all_bin,$dt_all_overflow);
+  print_duration_table("Delivery times", "messages with at least one remote delivery", \@delivery_times, \@dt_remote_bin,$dt_remote_overflow);
+}
+
+# Print rcpt times if required.
+if ($#rcpt_times >= 0) {
+  foreach my $protocol ('all', grep(!/^all$/, sort keys %rcpt_times_bin)) {
+    print_duration_table("Receipt times", "$protocol messages", \@rcpt_times, $rcpt_times_bin{$protocol}, $rcpt_times_overflow{$protocol});
+  }
 }
 
 # Print relay information if required.
 }
 
 # Print relay information if required.
@@ -3361,19 +4194,35 @@ print_relay() if $show_relay;
 
 # Print the league tables, if topcount isn't zero.
 if ($topcount > 0) {
 
 # Print the league tables, if topcount isn't zero.
 if ($topcount > 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{$_};
   foreach ('Host','Domain','Email','Edomain') {
     next unless $do_sender{$_};
-    print_league_table("sending \l$_", $received_count{$_}, $received_data{$_},$received_data_gigs{$_});
+    print_league_table("sending \l$_", $received_count{$_}, undef, $received_data{$_},$received_data_gigs{$_}, $ws_top50, \$ws_top50_row);
   }
 
   }
 
-  print_league_table("local sender", \%received_count_user,
-    \%received_data_user,\%received_data_gigs_user) if ($local_league_table || $include_remote_users);
+  print_league_table("local sender", \%received_count_user, undef,
+    \%received_data_user,\%received_data_gigs_user, $ws_top50, \$ws_top50_row) if (($local_league_table || $include_remote_users) && %received_count_user);
   foreach ('Host','Domain','Email','Edomain') {
     next unless $do_sender{$_};
   foreach ('Host','Domain','Email','Edomain') {
     next unless $do_sender{$_};
-    print_league_table("\l$_ destination", $delivered_count{$_}, $delivered_data{$_},$delivered_data_gigs{$_});
+    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_count_user,
-    \%delivered_data_user,\%delivered_data_gigs_user) if ($local_league_table || $include_remote_users);
+  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;
+
 }
 
 # Print the error statistics if required.
 }
 
 # Print the error statistics if required.
@@ -3382,7 +4231,7 @@ print_errors() if $show_errors;
 print $htm_fh "</body>\n</html>\n" if $htm_fh;
 
 
 print $htm_fh "</body>\n</html>\n" if $htm_fh;
 
 
-$txt_fh->close if $txt_fh;
+$txt_fh->close if $txt_fh && ref $txt_fh;
 $htm_fh->close if $htm_fh;
 
 if ($xls_fh) {
 $htm_fh->close if $htm_fh;
 
 if ($xls_fh) {
@@ -3395,5 +4244,3 @@ if ($xls_fh) {
 
 
 # End of eximstats
 
 
 # End of eximstats
-
-# FIXME: Doku