Updated for eximstats 1.52
[users/jgh/exim.git] / src / src / eximstats.src
index 32003889b58fce0e778e675916af01a8b65496f6..539c756d7aacb7327852e965fcd977b5095638b0 100644 (file)
@@ -1,5 +1,5 @@
 #!PERL_COMMAND -w
-# $Cambridge: exim/src/src/eximstats.src,v 1.6 2005/02/17 11:58:26 ph10 Exp $
+# $Cambridge: exim/src/src/eximstats.src,v 1.12 2007/01/22 15:14:01 steve Exp $
 
 # Copyright (c) 2001 University of Cambridge.
 # See the file NOTICE for conditions of use and distribution.
 #             Eximstats can now parse syslog lines as well as mainlog lines.
 #
 # 2004-12-20  V1.35 Wouter Verhelst
-#            Pie charts by volume were actually generated by count. Fixed.
+#             Pie charts by volume were actually generated by count. Fixed.
 #
 # 2005-02-07  V1.36 Gregor Herrmann / Steve Campbell
 #             Added average sizes to HTML Top tables.
-
+#
+# 2005-04-26  V1.37 Frank Heydlauf
+#             Added -xls and the ability to specify output files.
+#
+# 2005-04-29  V1.38 Steve Campbell
+#             Use FileHandles for outputing 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.
+#
+# 2005-05-27  V1.39 Steve Campbell
+#             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.
+#
+#
+#
+# For documentation on the logfile format, see
+# http://www.exim.org/exim-html-4.50/doc/html/spec_48.html#IX2793
 
 =head1 NAME
 
@@ -204,11 +271,42 @@ eximstats - generates statistics from Exim mainlog or syslog files.
 
 =head1 SYNOPSIS
 
- eximstats [Options] mainlog1 mainlog2 ... > report.txt
- eximstats -html [Options] mainlog1 mainlog2 ... > report.html
+ eximstats [Output] [Options] mainlog1 mainlog2 ...
  eximstats -merge [Options] report.1.txt report.2.txt ... > weekly_report.txt
 
-Options:
+=head2 Output:
+
+=over 4
+
+=item B<-txt>
+
+Output the results in plain text to STDOUT.
+
+=item B<-txt>=I<filename>
+
+Output the results in plain text. Filename '-' for STDOUT is accepted.
+
+=item B<-html>
+
+Output the results in HTML to STDOUT.
+
+=item B<-html>=I<filename>
+
+Output the results in HTML. Filename '-' for STDOUT is accepted.
+
+=item B<-xls>
+
+Output the results in Excel compatible Format to STDOUT.
+Requires the Spreadsheet::WriteExcel CPAN module.
+
+=item B<-xls>=I<filename>
+
+Output the results in Excel compatible format. Filename '-' for STDOUT is accepted.
+
+
+=back
+
+=head2 Options:
 
 =over 4
 
@@ -256,6 +354,36 @@ Omit local sources/destinations in top listing.
 
 Include remote users in the top source/destination listings.
 
+=item B<-include_original_destination>
+
+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.
+
+=item B<-show_dt>I<list>
+
+Show the delivery times (B<DT>)for all the messages.
+
+Exim must have been configured to use the +delivery_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
@@ -322,13 +450,10 @@ round to the same value.
 
 =back
 
-=item B<-html>
-
-Output the results in HTML.
-
 =item B<-charts>
 
 Create graphical charts to be displayed in HTML output.
+Only valid in combination with I<-html>.
 
 This requires the following modules which can be obtained
 from http://www.cpan.org/modules/01modules.index.html
@@ -359,6 +484,11 @@ Create the charts in the directory <dir>
 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
@@ -371,8 +501,8 @@ title!
 
 Eximstats parses exim mainlog and syslog files to output a statistical
 analysis of the messages processed. By default, a text
-analysis is generated, but you can request an html analysis
-by using the B<-html> flag. See the help (B<-help>) to learn
+analysis is generated, but you can request other output formats
+using flags. See the help (B<-help>) to learn
 about how to create charts from the tables.
 
 =head1 AUTHOR
@@ -387,19 +517,24 @@ 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...
 
+Merging of xls files is not (yet) possible. Be free to implement :)
+
 =cut
 
 use integer;
 use strict;
+use IO::File;
 
 # use Time::Local;  # PH/FANF
 use POSIX;
 
-use vars qw($HAVE_GD_Graph_pie $HAVE_GD_Graph_linespoints);
+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;
 eval { require GD::Graph::linespoints; };
 $HAVE_GD_Graph_linespoints = $@ ? 0 : 1;
+eval { require Spreadsheet::WriteExcel; };
+$HAVE_Spreadsheet_WriteExcel = $@ ? 0 : 1;
 
 
 ##################################################
@@ -409,6 +544,7 @@ $HAVE_GD_Graph_linespoints = $@ ? 0 : 1;
 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 =
@@ -422,24 +558,43 @@ use vars qw($COLUMN_WIDTHS);
 
 @days_per_month = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
 $gig     = 1024 * 1024 * 1024;
-$VERSION = '1.36';
+$VERSION = '1.52';
 
 # 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);
-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(%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($total_delivered_data $total_delivered_data_gigs $total_delivered_messages $total_delivered_addresses);
+use vars qw(%timestamp2time);                   #Hash of timestamp => time.
+use vars qw($last_timestamp $last_time);        #The last time convertion done.
+use vars qw($last_date $date_seconds);          #The last date convertion done.
+use vars qw($last_offset $offset_seconds);      #The last time offset convertion done.
 use vars qw($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($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
+
+# 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);
+
+#For use in Speadsheed::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($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);
 
 $ntopchart = 5;
 
@@ -447,27 +602,46 @@ $ntopchart = 5;
 # 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($relay_pattern @queue_times $html @user_patterns @user_descriptions);
+use vars qw($hist_opt $hist_interval $hist_number $volume_rounding $emptyOK);
+use vars qw($relay_pattern @queue_times @user_patterns @user_descriptions);
+use vars qw(@rcpt_times @delivery_times);
+use vars qw($include_original_destination);
+use vars qw($txt_fh $htm_fh $xls_fh);
 
 use vars qw(%do_sender);                #Do sender by Host, Domain, Email, and/or Edomain tables.
 use vars qw($charts $chartrel $chartdir $charts_option_specified);
-use vars qw($merge_reports);                   #Merge old reports ?
+use vars qw($merge_reports);            #Merge old reports ?
 
 # 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(%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(%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(%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);
 
+# 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;
 
 
 
@@ -475,6 +649,36 @@ use vars qw(%report_totals);
 #                   Subroutines                  #
 ##################################################
 
+#######################################################################
+# get_filehandle($file,\%output_files);
+# Return a filehandle writing to $file.
+#
+# If %output_files is defined, check that $output_files{$file}
+# doesn't exist and die if it does, or set it if it doesn't.
+#######################################################################
+sub get_filehandle {
+  my($file,$output_files_href) = @_;
+
+  $file = '-' if ($file eq '');
+
+  if (defined $output_files_href) {
+    die "You can only output to '$file' once! Use -h for help.\n" if exists $output_files_href->{$file};
+    $output_files_href->{$file} = 1;
+  }
+
+  if ($file eq '-') {
+    return \*STDOUT;
+  }
+
+  if (-e $file) {
+    unlink $file or die "Failed to rm $file: $!";
+  }
+
+  my $fh = new IO::File $file, O_WRONLY|O_CREAT|O_EXCL;
+  die "new IO::File $file failed: $!" unless (defined $fh);
+  return $fh;
+}
+
 
 #######################################################################
 # volume_rounded();
@@ -525,7 +729,13 @@ sub volume_rounded {
   }
   else {
     # We don't want any rounding to be done.
-    $rounded = sprintf("%4d", ($g * $gig) + $x);
+    # 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
+    # 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);
+    no integer;
+    $rounded = sprintf("%.0f", ($g * $gig) + $x);
   }
 
   return $rounded;
@@ -561,7 +771,9 @@ sub un_round {
     $$bytes_sref     += ($1 % (1024 * 1024) * 1024);
   }
   elsif ($rounded =~ /(\d+)/) {
-    $$gigabytes_sref += $1 / $gig;
+    # We need to turn off integer in case we are merging an -nvr report.
+    no integer;
+    $$gigabytes_sref += int($1 / $gig);
     $$bytes_sref     += $1 % $gig;
   }
 
@@ -727,6 +939,44 @@ while($#c >= 0) { $s = $s * 62 + $tab62[ord(shift @c) - ord('0')] }
 $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));
+  }
+}
 
 
 #######################################################################
@@ -765,156 +1015,212 @@ sub calculate_localtime_offset {
 }
 
 
+
 #######################################################################
-# print_queue_times();
-#
-#  $time = print_queue_times($message_type,\@queue_times,$queue_more_than);
-#
-# 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_duration_table();
+#
+#  print_duration_table($title, $message_type, \@times, \@values, $overflow);
+#
+# 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;
-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;
-#$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 = "$title: $message_type";
 
-my $temp = "Time spent on the queue: $string";
 
-my($format);
-if ($html) {
-  print "<hr><a name=\"$string time\"></a><h2>$temp</h2>\n";
-  print "<table border=0 width=\"100%\">\n";
-  print "<tr><td>\n";
-  print "<table border=1>\n";
-  print "<tr><th>Time</th><th>Messages</th><th>Percentage</th><th>Cumulative Percentage</th>\n";
-  $format = "<tr><td align=\"right\">%s %s</td><td align=\"right\">%d</td><td align=\"right\">%5.1f%%</td><td align=\"right\">%5.1f%%</td>\n";
+my $txt_format = "%5s %4s   %6d %5.1f%%  %5.1f%%\n";
+my $htm_format = "<tr><td align=\"right\">%s %s</td><td align=\"right\">%d</td><td align=\"right\">%5.1f%%</td><td align=\"right\">%5.1f%%</td>\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=\"$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";
 }
-else
-{
-  printf("%s\n%s\n\n", $temp, "-" x length($temp));
-  $format = "%5s %4s   %6d %5.1f%%  %5.1f%%\n";
+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);
 }
 
-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;
-    printf($format,
-      $printed_one? "     " : "Under",
-      format_time($queue_times[$i]),
-      $$array[$i], $percent, $cumulative_percent);
-    if (!defined($queue_times[$i])) {
-      print "Not defined";
+
+    my @content=($printed_one? "     " : "Under",
+        format_time($times_aref->[$i]),
+        $values_aref->[$i], $percent, $cumulative_percent);
+
+    if ($htm_fh) {
+      printf $htm_fh ($htm_format, @content);
+      if (!defined($values_aref->[$i])) {
+        print $htm_fh "Not defined";
+      }
+    }
+    if ($txt_fh) {
+      printf $txt_fh ($txt_format, @content);
+      if (!defined($times_aref->[$i])) {
+        print $txt_fh "Not defined";
+      }
+    }
+    if ($xls_fh)
+    {
+      no integer;
+      &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($times_aref->[$i])) {
+        $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;
   }
 }
 
-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;
-  printf($format,
-    "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;
+    if ($xls_fh)
+    {
+      &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);
+    }
+
 }
-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 ($html) {
-  print "</table>\n";
-  print "</td><td>\n";
+if ($htm_fh) {
+  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 $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 $!;
+      open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
       binmode IMG;
       print IMG $gd->png;
       close IMG;
-      print "<img src=\"$chartrel/$pngname\">";
+      print $htm_fh "<img src=\"$chartrel/$pngname\">";
     }
   }
-  print "</td></tr></table>\n";
+  print $htm_fh "</td></tr></table>\n";
 }
-print "\n";
+
+if ($xls_fh)
+{
+  $row++;
 }
+print $txt_fh "\n" if $txt_fh;
+print $htm_fh "\n" if $htm_fh;
 
+}
 
 
 #######################################################################
 # 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 {
-my($text) = shift;
-my(@interval_count) = @_;
+my($text, $unit, @interval_count) = @_;
 my(@chartdatanames);
 my(@chartdatavals);
 my($maxd) = 0;
+
+# save first row of print_histogram for xls output
+if (!$run_hist) {
+  $row_hist = $row;
+}
+else {
+  $row = $row_hist;
+}
+
 for ($i = 0; $i < $hist_number; $i++)
   { $maxd = $interval_count[$i] if $interval_count[$i] > $maxd; }
 
 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
+my $title = sprintf("$text per %s",
+    ($hist_interval == 60)? "hour" :
+    ($hist_interval == 1)?  "minute" : "$hist_interval minutes");
 
-my($title) = sprintf("$text per %s (each dot is $scale $type)",
-  ($hist_interval == 60)? "hour" :
-  ($hist_interval == 1)?  "minute" : "$hist_interval minutes");
+my $txt_htm_title = $title . " (each dot is $scale $unit)";
 
-if ($html) {
-  print "<hr><a name=\"$text\"></a><h2>$title</h2>\n";
-  print "<table border=0 width=\"100%\">\n";
-  print "<tr><td><pre>\n";
+printf $txt_fh ("%s\n%s\n\n", $txt_htm_title, "-" x length($txt_htm_title)) if $txt_fh;
+
+if ($htm_fh) {
+  print $htm_fh "<hr><a name=\"$text\"></a><h2>$txt_htm_title</h2>\n";
+  print $htm_fh "<table border=0 width=\"100%\">\n";
+  print $htm_fh "<tr><td><pre>\n";
 }
-else {
-  printf("%s\n%s\n\n", $title, "-" x length($title));
+
+if ($xls_fh) {
+  $title =~ s/Messages/Msg/ ;
+  $row += 2;
+  $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
@@ -922,37 +1228,59 @@ for ($i = 0; $i < $hist_number; $i++)
   # 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 $temp;
+
+    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
+        $ws_global->write($row, 0, [$temp], $f_default);
+      }
+    }
+
     push(@chartdatanames, $temp);
     $hour++;
-    }
-  else
-    {
+  }
+  else {
     if ($minutes == 0)
       { $temp = sprintf("%02d:%02d", $hour, $minutes) }
     else
       { $temp = sprintf("  :%02d", $minutes) }
-    print $temp;
+
+    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
+      $temp = sprintf("%02d:%02d", $hour, $minutes);
+      $ws_global->write($row, 0, [$temp], $f_default);
+    }
+
     push(@chartdatanames, $temp);
     $minutes += $hist_interval;
-    if ($minutes >= 60)
-      {
+    if ($minutes >= 60) {
       $minutes = 0;
       $hour++;
-      }
     }
-  push(@chartdatavals, $c);
-  printf(" %6d %s\n", $c, "." x ($c/$scale));
   }
-print "\n";
-if ($html)
-  {
-  print "</pre>\n";
-  print "</td><td>\n";
-  if ($HAVE_GD_Graph_linespoints && $charts) {
+  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;
+  $ws_global->write($row++, $col_hist+1, [$c], $f_default) if $xls_fh;
+
+} #end for
+
+printf $txt_fh "\n" if $txt_fh;
+printf $htm_fh "\n" if $htm_fh;
+
+if ($htm_fh)
+{
+  print $htm_fh "</pre>\n";
+  print $htm_fh "</td><td>\n";
+  if ($HAVE_GD_Graph_linespoints && $charts && ($#chartdatavals > 0)) {
     # calculate the graph
     my @data = (
        \@chartdatanames,
@@ -965,20 +1293,25 @@ if ($html)
         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 $!;
+      open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
       binmode IMG;
       print IMG $gd->png;
       close IMG;
-      print "<img src=\"$chartrel/$pngname\">";
+      print $htm_fh "<img src=\"$chartrel/$pngname\">";
     }
   }
-  print "</td></tr></table>\n";
+  print $htm_fh "</td></tr></table>\n";
 }
+
+$col_hist++; # where to continue next times
+
+$row+=2;     # leave some space after history block
+$run_hist=1; # we have done this once or more
 }
 
 
@@ -986,202 +1319,249 @@ if ($html)
 #######################################################################
 # 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 {
-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($format);
-if ($html) {
-  print "<hr><a name=\"$text count\"></a><h2>$temp</h2>\n";
-  print "<table border=0 width=\"100%\">\n";
-  print "<tr><td>\n";
-  print "<table border=1>\n";
-  print "<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';
-  $format = "<tr><td align=\"right\">%d</td><td align=\"right\">%s</td><td align=\"right\">%s</td><td align=\"$align\" nowrap>%s</td>\n";
-}
-else {
-  printf("%s\n%s\n\n", $temp, "-" x length($temp));
-  $format = "%7d %10s   %s\n";
-}
 
-my($key,$htmlkey,$rounded_volume,$rounded_average,$count,$data,$gigs);
-foreach $key (top_n_sort($topcount,$m_count,$m_data_gigs,$m_data)) {
-  if ($html) {
-    $htmlkey = $key;
-    $htmlkey =~ s/>/\&gt\;/g;
-    $htmlkey =~ s/</\&lt\;/g;
+
+  ################################################
+  # Generate the printf formats and table headers.
+  ################################################
+  my(@headers) = ('Messages');
+  push(@headers,'Addresses') if defined $a_count;
+  push(@headers,'Bytes','Average') if defined $m_data;
+
+  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 the table headers
+  ################################################
+  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 count"></a><h2>$title</h2>
+<table border=0 width="100%">
+<tr><td>
+<table border=1>
+EoText
+    print $htm_col_headers;
+  }
+
+  if ($xls_fh) {
+    $spreadsheet->write(${$row_sref}++, 0, $title, $f_header2);
+    $spreadsheet->write(${$row_sref}++, 0, [@headers, $text], $f_headertab);
+  }
+
+
+  # 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.
-    $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);
-    printf($format, $count, $rounded_volume, $rounded_average, $htmlkey);
-  }
-  else {
-    printf($format, $$m_count{$key}, volume_rounded($$m_data{$key},$$m_data_gigs{$key}), $key);
-  }
-  if (scalar @chartdatanames < $ntopchart)
-    {
-    push(@chartdatanames, $key);
-    push(@chartdatavals, $$m_count{$key});
+    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);
     }
-  else
-    {
-    $chartotherval += $$m_count{$key};
+
+    # 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);
     }
-  }
-push(@chartdatanames, "Other");
-push(@chartdatavals, $chartotherval);
+    $spreadsheet->write(${$row_sref}++, 0, [@content, $key], $f_default) if $xls_fh;
 
-if ($html)
-  {
-  print "</table>\n";
-  print "</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 $!;
-      binmode IMG;
-      print IMG $gd->png;
-      close IMG;
-      print "<img src=\"$chartrel/${temp}_count.png\">";
+    if (scalar @chartdatanames < $ntopchart) {
+      push(@chartdatanames, $key);
+      push(@chartdatavals, $$m_count{$key});
+    }
+    else {
+      $chartotherval += $$m_count{$key};
     }
   }
-  print "</td><td>\n";
-  print "</td></tr></table>\n";
-}
-print "\n";
-
-$temp = "Top $name by volume";
-if ($html) {
-  print "<hr><a name=\"$text volume\"></a><h2>$temp</h2>\n";
-  print "<table border=0 width=\"100%\">\n";
-  print "<tr><td>\n";
-  print "<table border=1>\n";
-  print "<tr><th>Messages</th><th>Bytes</th><th>Average</th><th>\u$text</th>\n";
-}
-else {
-  printf("%s\n%s\n\n", $temp, "-" x length($temp));
-}
 
-@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;
-  }
-  if ($html) {
-    $htmlkey = $key;
-    $htmlkey =~ s/>/\&gt\;/g;
-    $htmlkey =~ s/</\&lt\;/g;
-
-    $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);
-    printf($format, $count, $rounded_volume, $rounded_average, $htmlkey);
-  }
-  else {
-    printf($format, $$m_count{$key}, volume_rounded($$m_data{$key},$$m_data_gigs{$key}), $key);
-  }
+  push(@chartdatanames, "Other");
+  push(@chartdatavals, $chartotherval);
 
-  if (scalar @chartdatanames < $ntopchart) {
-    if ($use_gig) {
-      if ($$m_data_gigs{$key}) {
-       push(@chartdatanames, $key);
-       push(@chartdatavals, $$m_data_gigs{$key});
+  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           => '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\">";
       }
     }
-    else {
-      push(@chartdatanames, $key);
-      push(@chartdatavals, $$m_data{$key});
-    }
+    print $htm_fh "</td><td>\n";
+    print $htm_fh "</td></tr></table>\n\n";
   }
-  else {
-    $chartotherval += ($use_gig) ? $$m_data_gigs{$key} : $$m_data{$key};
-  }
-}
-push(@chartdatanames, "Other");
-push(@chartdatavals, $chartotherval);
+  ++${$row_sref} if $xls_fh;
 
-if ($html) {
-  print "</table>\n";
-  print "</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) {
-      my $temp = $text;
-      $temp =~ s/ /_/g;
-      open(IMG, ">$chartdir/${temp}_volume.png") or die $!;
-      binmode IMG;
-      print IMG $gd->png;
-      close IMG;
-      print "<img src=\"$chartrel/${temp}_volume.png\">";
+
+  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_col_headers;
+    }
+    if ($xls_fh) {
+      $spreadsheet->write(${$row_sref}++, 0, $title, $f_header2);
+      $spreadsheet->write(${$row_sref}++, 0, [@headers, $text], $f_headertab);
     }
-  }
-  print "</td><td>\n";
-  print "</td></tr></table>\n";
-}
 
-print "\n";
-}
+    @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}) {
+        $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;
 
-#######################################################################
-# top_n_sort();
-#
-#   @sorted_keys = top_n_sort($n,$href1,$href2,$href3);
+
+      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};
+      }
+    }
+    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 && ($#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;
+  }
+}
+
+
+#######################################################################
+# top_n_sort();
+#
+#   @sorted_keys = top_n_sort($n,$href1,$href2,$href3);
 #
 # Given a hash which has numerical values, return the sorted $n keys which
 # point to the top values. The second and third hashes are used as
@@ -1221,6 +1601,12 @@ sub top_n_sort {
   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) {
@@ -1228,11 +1614,22 @@ 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
-    # 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 guarenteed 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 ||
-                 $top_n_key cmp $key;
+                  $href2->{$key} <=> $minimum_value2 ||
+                  $href3->{$key} <=> $minimum_value3 ||
+                  $top_n_key cmp $key;
     next unless ($comparison == 1);
 
     # As we will be using these values a few times, extract them into scalars.
@@ -1256,14 +1653,14 @@ sub top_n_sort {
     for ($i = 0; $i < $n_minus_1; $i++) {
       $top_n_key = $top_n_keys[$i];
       if ( ($top_n_key eq '_') ||
-          ( ($value1 <=> $href1->{$top_n_key} ||
+           ( ($value1 <=> $href1->{$top_n_key} ||
               $value2 <=> $href2->{$top_n_key} ||
-             $value3 <=> $href3->{$top_n_key} ||
-             $top_n_key cmp $key) == 1
-          )
-        ) {
-       $insert_position = $i;
-       last;
+              $value3 <=> $href3->{$top_n_key} ||
+              $top_n_key cmp $key) == 1
+           )
+         ) {
+        $insert_position = $i;
+        last;
       }
     }
 
@@ -1286,6 +1683,7 @@ sub top_n_sort {
 }
 
 
+
 #######################################################################
 # html_header();
 #
@@ -1322,51 +1720,69 @@ sub help {
 
 eximstats Version $VERSION
 
-Usage: eximstats [Options] mainlog1 mainlog2 ... > report.txt
-       eximstats -html  [Options] mainlog1 mainlog2 ... > report.html
-       eximstats -merge [Options] report.1.txt report.2.txt ... > weekly_rep.txt
-       eximstats -merge -html [Options] report.1.html ... > weekly_rep.html
+Usage:
+  eximstats [Output] [Options] mainlog1 mainlog2 ...
+  eximstats -merge -html [Options] report.1.html ... > weekly_rep.html
+
+Examples:
+  eximstats -html=eximstats.html mainlog1 mainlog2 ...
+  eximstats mainlog1 mainlog2 ... > report.txt
 
 Parses exim mainlog or syslog files and generates a statistical analysis
-of the messages processed. Valid options are:
+of the messages processed.
 
+Valid output types are:
+-txt[=<file>]   plain text (default unless no other type is specified)
+-html[=<file>]  HTML
+-xls[=<file>]   Excel
+With no type and file given, defaults to -txt and STDOUT.
+
+Valid options are:
 -h<number>      histogram divisions per hour. The default is 1, and
                 0 suppresses histograms. Other valid values are:
-               2, 3, 5, 10, 15, 20, 30 or 60.
+                2, 3, 5, 10, 15, 20, 30 or 60.
 -ne             don't display error information
 -nr             don't display relaying information
 -nr/pattern/    don't display relaying information that matches
 -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
+-nvr            don't do volume rounding. Display in bytes, not KB/MB/GB.
 -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.
 
--byhost                show results by sending host (default unless bydomain or
+-byhost         show results by sending host (default unless bydomain or
                 byemail is specified)
--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
 
 -pattern "Description" /pattern/
                 Count lines matching specified patterns and show them in
-               the results. It can be specified multiple times. Eg:
-               -pattern 'Refused connections' '/refused connection/'
+                the results. It can be specified multiple times. Eg:
+                -pattern 'Refused connections' '/refused connection/'
 
 -merge          merge previously generated reports into a new report
 
--html           output the results in HTML
--charts         Create charts (this requires the GD::Graph modules)
+-charts         Create charts (this requires the GD::Graph modules).
+                Only valid with -html.
 -chartdir <dir> Create the charts' png files in the directory <dir>
 -chartrel <dir> Specify the relative directory for the "img src=" tags
                 from where to include the charts in the html file
-               -chartdir and -chartrel default to '.'
+                -chartdir and -chartrel default to '.'
 
--d             Debug mode - dump the eval'ed parser onto STDERR.
+-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
 
@@ -1393,7 +1809,9 @@ EoText
 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);
+  my $rej_id = 0;
   while (<$fh>) {
 
     # Convert syslog lines to mainlog format.
@@ -1401,34 +1819,60 @@ sub generate_parser {
       next unless s/^.*? exim\\b.*?: //;
     }
 
-    next if length($_) < 38;
+    $length = length($_);
+    next if ($length < 38);
     next unless /^(\\d{4}\\-\\d\\d-\\d\\d\\s(\\d\\d):(\\d\\d):\\d\\d( [-+]\\d\\d\\d\\d)?)/o;
 
     ($tod,$m_hour,$m_min) = ($1,$2,$3);
 
-    # PH
-    my($extra) = defined($4)? 6 : 0;
+    # PH - watch for GMT offsets in the timestamp.
+    if (defined($4)) {
+      $extra = 6;
+      next if ($length < 44);
+    }
+    else {
+      $extra = 0;
+    }
+
     $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;
-    $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 .= '
-    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
 
+    # 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
@@ -1440,40 +1884,50 @@ sub generate_parser {
     #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.
-    if (/\\sH=(\\S+)/) {
-      $host = $1;
-
-      ($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;
+    # Host is local unless otherwise specified.
+    $ip = (/\\bH=.*?(\\[[^]]+\\])/) ? $1 : "local";
+    $host = (/\\bH=(\\S+)/) ? $1 : "local";
 
-      #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 =~ /^(\\(?)[^\\.]+\\.([^\\.]+\\..*)/) {
+      # 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})
-    $email = (/^(\S+)/) ? $1 : "";
+      #IFDEF ($include_original_destination)
+      # Catch both "a@b.com <c@d.com>" and "e@f.com"
+      #$email = (/^(\S+) (<(\S*?)>)?/) ? $3 || $1 : "";
+      $email = (/^(\S+ (<[^@>]+@?[^>]*>)?)/) ? $1 : "";
+      chomp($email);
+      #ENDIF ($include_original_destination)
+
+      #IFNDEF ($include_original_destination)
+      $email = (/^(\S+)/) ? $1 : "";
+      #ENDIF ($include_original_destination)
     #ENDIF ($do_sender{Email})
 
     #IFDEF ($do_sender{Edomain})
-    $edomain = (/^\S*?\\@(\S+)/) ? lc($1) : "";
+      #IFDEF ($include_original_destination)
+      #$edomain = (/^(\S+) (<\S*?\\@(\S+)>)?/) ? $3 || $1 : "";
+      $edomain = (/^(\S+ (<\S*?\\@(\S+?)>)?)/) ? $1 : "";
+      chomp($edomain);
+      lc($edomain);
+      #ENDIF ($include_original_destination)
+
+      #IFNDEF ($include_original_destination)
+      $edomain = (/^\S*?\\@(\S+)/) ? lc($1) : "";
+      #ENDIF ($include_original_destination)
     #ENDIF ($do_sender{Edomain})
 
     if ($tod lt $begin) {
@@ -1486,78 +1940,79 @@ sub generate_parser {
 
     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+)/;
-       $from_host{$id} = "$host$ip";
-       $from_address{$id} = $from;
+        # Save incoming information in case it becomes interesting
+        # later, when delivery lines are read.
+        my($from) = /^(\\S+)/;
+        $message[$FROM_HOST]    = "$host$ip";
+        $message[$FROM_ADDRESS] = $from;
       }
       #ENDIF ($show_relay)
 
       #IFDEF ($local_league_table || $include_remote_users)
-       if (/\sU=(\\S+)/) {
-         my $user = $1;
+        if (/\sU=(\\S+)/) {
+          my $user = $1;
 
-         #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)
+          {                         #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 ($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)
+          #IFDEF ($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}++;
-           add_volume(\\$received_data_user{$user},\\$received_data_gigs_user{$user},$thissize);
+            ++$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})
-       $received_count{Host}{$host}++;
-       add_volume(\\$received_data{Host}{$host},\\$received_data_gigs{Host}{$host},$thissize);
+        ++$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) {
-         $received_count{Domain}{$domain}++;
-         add_volume(\\$received_data{Domain}{$domain},\\$received_data_gigs{Domain}{$domain},$thissize);
-       }
+          ++$received_count{Domain}{$domain};
+          add_volume(\\$received_data{Domain}{$domain},\\$received_data_gigs{Domain}{$domain},$thissize);
+        }
       #ENDIF ($do_sender{Domain})
 
       #IFDEF ($do_sender{Email})
-       $received_count{Email}{$email}++;
-       add_volume(\\$received_data{Email}{$email},\\$received_data_gigs{Email}{$email},$thissize);
+        ++$received_count{Email}{$email};
+        add_volume(\\$received_data{Email}{$email},\\$received_data_gigs{Email}{$email},$thissize);
       #ENDIF ($do_sender{Email})
 
       #IFDEF ($do_sender{Edomain})
-       $received_count{Edomain}{$edomain}++;
-       add_volume(\\$received_data{Edomain}{$edomain},\\$received_data_gigs{Edomain}{$edomain},$thissize);
+        ++$received_count{Edomain}{$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);
 
-      #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]++;
+        $received_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
       #ENDIF ($hist_opt > 0)
     }
 
     elsif ($flag eq "=>") {
-      $size = $size{$id} || 0;
+      $size = $message[$SIZE] || 0;
       if ($host ne "local") {
-        $remote_delivered{$id} = 1;
+        $message[$REMOTE_DELIVERED] = 1;
 
 
         #IFDEF ($show_relay)
@@ -1567,24 +2022,24 @@ sub generate_parser {
         # 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);
-         }
+          }
           else {
-           $old = $new = "";
-         }
+            $old = $new = "";
+          }
 
           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};
-              $relayed{$key}++;
-           }
+              ++$relayed{$key};
+            }
             else {
-             $relayed_unshown++
+              ++$relayed_unshown;
             }
           }
         }
@@ -1593,54 +2048,65 @@ sub generate_parser {
       }
 
       #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</)? " <" : " ", $_)) {
-           if ($user =~ /^[\\/|]/) {
-             my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
-             $user = "$user $parent" if defined $parent;
-           }
-           $delivered_count_user{$user}++;
-           add_volume(\\$delivered_data_user{$user},\\$delivered_data_gigs_user{$user},$size);
-         }
-       }
+        #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) = $_ =~ /(<[^@]+@?[^>]*>)/;
+              $user = "$user $parent" if defined $parent;
+            }
+            ++$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})
-       $delivered_count{Host}{$host}++;
-       add_volume(\\$delivered_data{Host}{$host},\\$delivered_data_gigs{Host}{$host},$size);
+        $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) {
-         $delivered_count{Domain}{$domain}++;
-         add_volume(\\$delivered_data{Domain}{$domain},\\$delivered_data_gigs{Domain}{$domain},$size);
-       }
+          ++$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})
-       $delivered_count{Email}{$email}++;
-       add_volume(\\$delivered_data{Email}{$email},\\$delivered_data_gigs{Email}{$email},$size);
+        ++$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})
-       $delivered_count{Edomain}{$edomain}++;
-       add_volume(\\$delivered_data{Edomain}{$edomain},\\$delivered_data_gigs{Edomain}{$edomain},$size);
+        ++$delivered_messages{Edomain}{$edomain};
+        ++$delivered_addresses{Edomain}{$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:";
-        $transported_count{$transport}++;
+        ++$transported_count{$transport};
         add_volume(\\$transported_data{$transport},\\$transported_data_gigs{$transport},$size);
       #ENDIF ($show_transport)
 
@@ -1648,18 +2114,88 @@ sub generate_parser {
         $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) = $_ =~ /(<[^@]+@?[^>]*>)/;
+              $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 "**") {
-      $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)
-        $errors_count{$_}++;
+        ++$errors_count{$_};
       #ENDIF ($show_errors)
 
     }
@@ -1667,32 +2203,152 @@ sub generate_parser {
     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]) {
-            $queue_bin[$i]++;
-            $remote_queue_bin[$i]++ if $remote_delivered{$id};
+            ++$qt_all_bin[$i];
+            ++$qt_remote_bin[$i] if $message[$REMOTE_DELIVERED];
             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)
 
-      #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 .*?(\[.+?\]):/);
+      ++$rejected_count_by_ip{$ip};
+      if (/SpamAssassin/) {
+        ++$rejected_count_by_reason{"Rejected by SpamAssassin"};
+      }
+      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: (.*)/ ||
+        /.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)/ ||
+        /(temporarily refused connection)/ ||
+        # SMTP protocol synchronization error (input sent without waiting for greeting): rejected connection from %s
+        /(SMTP protocol .*?(error|violation))/ ||
+        /(message too big)/
+        ) {
+        ++$rejected_count_by_reason{"\u$1"};
+      }
+      elsif (/rejected [HE][HE]LO from [^:]*: syntactically invalid argument/) {
+        ++$rejected_count_by_reason{"Rejected HELO/EHLO: syntactically invalid argument"};
+      }
+      elsif (/response to "RCPT TO.*? was: (.*)/) {
+        ++$rejected_count_by_reason{"Response to RCPT TO was: $1"};
+      }
+      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"};
+      }
+      else {
+        ++$rejected_count_by_reason{Unknown};
+        print STDERR "Unknown rejection: $_" if $debug;
+      }
     }
   }';
 
@@ -1701,21 +2357,27 @@ sub generate_parser {
   my(%defines_in_operation,$removing_lines,$processed_parser);
   foreach (split (/\n/,$parser)) {
     if ((/^\s*#\s*IFDEF\s*\((.*?)\)/i  && ! eval $1) ||
-       (/^\s*#\s*IFNDEF\s*\((.*?)\)/i &&   eval $1)    ) {
+        (/^\s*#\s*IFNDEF\s*\((.*?)\)/i &&   eval $1)    ) {
       $defines_in_operation{$1} = 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) {
       delete $defines_in_operation{$1};
       unless (keys %defines_in_operation) {
-       $removing_lines = 0;
+        $removing_lines = 0;
       }
     }
   }
-  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;
 }
@@ -1754,49 +2416,69 @@ sub parse {
 #######################################################################
 sub print_header {
 
+
   my $title = "Exim statistics from $begin to $end";
 
-  if ($html) {
-    print html_header($title);
-    print "<ul>\n";
-    print "<li><a href=\"#grandtotal\">Grand total summary</a>\n";
-    print "<li><a href=\"#patterns\">User Specified Patterns</a>\n" if @user_patterns;
-    print "<li><a href=\"#transport\">Deliveries by Transport</a>\n" if $show_transport;
+  print $txt_fh "\n$title\n" if $txt_fh;
+  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;
     if ($hist_opt) {
-      print "<li><a href=\"#Messages received\">Messages received per hour</a>\n";
-      print "<li><a href=\"#Deliveries\">Deliveries per hour</a>\n";
+      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) {
-      print "<li><a href=\"#all messages time\">Time spent on the queue: all messages</a>\n";
-      print "<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";
     }
-    print "<li><a href=\"#Relayed messages\">Relayed messages</a>\n" if $show_relay;
+
+    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=\"#mail rejection reason count\">Top $topcount mail rejection reasons by message count</a>\n" if %rejected_count_by_reason;
       foreach ('Host','Domain','Email','Edomain') {
-       next unless $do_sender{$_};
-       print "<li><a href=\"#sending \l$_ count\">Top $topcount sending \l${_}s by message count</a>\n";
-       print "<li><a href=\"#sending \l$_ volume\">Top $topcount sending \l${_}s by volume</a>\n";
+        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";
       }
-      if ($local_league_table || $include_remote_users) {
-       print "<li><a href=\"#local sender count\">Top $topcount local senders by message count</a>\n";
-       print "<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{$_};
-       print "<li><a href=\"#\l$_ destination count\">Top $topcount \l$_ destinations by message count</a>\n";
-       print "<li><a href=\"#\l$_ destination volume\">Top $topcount \l$_ destinations by volume</a>\n";
+        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";
       }
-      if ($local_league_table || $include_remote_users) {
-       print "<li><a href=\"#local destination count\">Top $topcount local destinations by message count</a>\n";
-       print "<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";
       }
-    }
-    print "<li><a href=\"#errors\">List of errors</a>\n" if %errors_count;
-    print "</ul>\n<hr>\n";
 
+      print $htm_fh "<li><a href=\"#rejected ip count\">Top $topcount rejected ips by message count</a>\n" if %rejected_count_by_ip;
+      print $htm_fh "<li><a href=\"#non-rejected spamming ip count\">Top $topcount non-rejected spamming ips by message count</a>\n" if %spam_count_by_ip;
+
+    }
+    print $htm_fh "<li><a href=\"#errors\">List of errors</a>\n" if %errors_count;
+    print $htm_fh "</ul>\n<hr>\n";
   }
-  else {
-    print "\n$title\n";
+  if ($xls_fh)
+  {
+    $ws_global->write($row++, $col+0, "Exim Statistics",  $f_header1);
+    &set_worksheet_line($ws_global, $row, $col, ["from:",  $begin,  "to:", $end], $f_default);
+    $row+=2;
   }
 }
 
@@ -1812,8 +2494,11 @@ sub print_grandtotals {
 
   # 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(@col_headers) = ('TOTAL', 'Volume', 'Messages', 'Addresses');
+
   foreach ('Host','Domain','Email','Edomain') {
     next unless $do_sender{$_};
     if ($merge_reports) {
@@ -1824,37 +2509,40 @@ sub print_grandtotals {
       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_html_format .= "<td align=\"right\">%d</td>";
     $sender_txt_format  .= " " x ($COLUMN_WIDTHS - 5) . "%6d";
+    push(@col_headers,"${_}s");
   }
 
-  my($format1,$format2);
-  if ($html) {
-    print << "EoText";
-<a name="grandtotal"></a>
-<h2>Grand total summary</h2>
-<table border=1>
-<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>
-EoText
+  my $txt_format1 = "  %-16s %9s     %6d    %6s $sender_txt_format";
+  my $txt_format2 = "  %6d %4.1f%% %6d %4.1f%%",
+  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>";
 
-    $format1 = "<tr><td>%s</td><td align=\"right\">%s</td>$sender_html_format<td align=\"right\">%d</td>";
-    $format2 = "<td align=\"right\">%d</td><td align=\"right\">%4.1f%%</td><td align=\"right\">%d</td><td align=\"right\">%4.1f%%</td>";
-  }
-  else {
+  if ($txt_fh) {
     my $sender_spaces = " " x length($sender_txt_header);
-    print << "EoText";
-
-Grand total summary
--------------------
-                                    $sender_spaces           At least one address
-  TOTAL               Volume    Messages $sender_txt_header      Delayed       Failed
-EoText
-    $format1 = "  %-16s %9s      %6d $sender_txt_format";
-    $format2 = "  %6d %4.1f%% %6d %4.1f%%",
+    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 Addresses $sender_txt_header      Delayed       Failed\n";
+  }
+  if ($htm_fh) {
+    print $htm_fh "<a name=\"grandtotal\"></a>\n";
+    print $htm_fh "<h2>Grand total summary</h2>\n";
+    print $htm_fh "<table border=1>\n";
+    print $htm_fh "<tr><th>" . join('</th><th>',@col_headers) . "</th><th colspan=2>At least one addr<br>Delayed</th><th colspan=2>At least one addr<br>Failed</th>\n";
+  }
+  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'});
@@ -1864,27 +2552,90 @@ EoText
   }
   else {
     $volume = volume_rounded($total_received_data, $total_received_data_gigs);
-    $failed_count = keys %had_error;
+    $failed_count = $message_errors;
   }
 
   {
     no integer;
-    printf("$format1$format2\n",'Received',$volume,$total_received_count,
-      @received_totals,$delayed_count,
-      ($total_received_count) ? ($delayed_count*100/$total_received_count) : 0,
-      $failed_count,
-      ($total_received_count) ? ($failed_count*100/$total_received_count) : 0);
+
+    my @content=(
+        $volume,$total_received_count,'',
+        @received_totals,
+        $delayed_count,
+        ($total_received_count) ? ($delayed_count*100/$total_received_count) : 0,
+        $failed_count,
+        ($total_received_count) ? ($failed_count*100/$total_received_count) : 0
+    );
+
+    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) {
+      $ws_global->write(++$row, 0, 'Received', $f_default);
+      for (my $i=0; $i < scalar(@content); $i++) {
+        if ($i == 4 || $i == 6) {
+          $ws_global->write($row, $i+1, $content[$i]/100, $f_percent);
+        }
+        else {
+          $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'});
-    $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);
   }
-  printf("$format1\n\n",'Delivered',$volume,$total_delivered_count,@delivered_totals);
-  print "</table>\n" if $html;
+
+  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;
+
+  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', '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};
+        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],
+                            ['Ham',\%ham_count_by_ip],
+                            ['Spam',\%spam_count_by_ip]) {
+      my $messages = 0;
+      map {$messages += $_} values %{$total_aref->[1]};
+
+      if ($messages > 0) {
+        @content = ($total_aref->[0], '', $messages, '');
+        push(@content,scalar(keys %{$total_aref->[1]})) if $do_sender{Host};
+
+        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;
+      }
+    }
+  }
+
+  printf $txt_fh "\n"         if $txt_fh;
+  printf $htm_fh "</table>\n" if $htm_fh;
+  ++$row;
 }
 
 
@@ -1896,46 +2647,140 @@ EoText
 # Print the counts of user specified patterns.
 #######################################################################
 sub print_user_patterns {
-  my($format1);
+  my $txt_format1 = "  %-18s  %6d";
+  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%d</td>";
 
-  if ($html) {
-    print "<hr><a name=\"patterns\"></a><h2>User Specified Patterns</h2>\n";
-    print "<table border=0 width=\"100%\">\n";
-    print "<tr><td>\n";
-    print "<table border=1>\n";
-    print "<tr><th>&nbsp;</th><th>Total</th>\n";
-    $format1 = "<tr><td>%s</td><td align=\"right\">%d</td>";
+  if ($txt_fh) {
+    print $txt_fh "User Specified Patterns\n";
+    print $txt_fh "-----------------------";
+    print $txt_fh "\n                       Total\n";
   }
-  else {
-    print "User Specified Patterns\n";
-    print "-----------------------";
-    print "\n                       Total\n";
-    $format1 = "  %-18s  %6d";
+  if ($htm_fh) {
+    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 "<tr><th>&nbsp;</th><th>Total</th>\n";
+  }
+  if ($xls_fh) {
+      $ws_global->write($row++, $col, "User Specified Patterns", $f_header2);
+      &set_worksheet_line($ws_global, $row++, 1, ["Total"], $f_headertab);
   }
 
+
   my($key);
   if ($merge_reports) {
     # We are getting our data from previous reports.
     foreach $key (@user_descriptions) {
       my $count = get_report_total($report_totals{patterns}{$key},'Total');
-      printf("$format1\n",$key,$count);
+      printf $txt_fh ("$txt_format1\n",$key,$count) if $txt_fh;
+      printf $htm_fh ("$htm_format1\n",$key,$count) if $htm_fh;
+      if ($xls_fh)
+      {
+        &set_worksheet_line($ws_global, $row++, 0, [$key,$count], $f_default);
+      }
     }
   }
   else {
     # We are getting our data from mainlog files.
     my $user_pattern_index = 0;
     foreach $key (@user_descriptions) {
-      printf("$format1\n",$key,$user_pattern_totals[$user_pattern_index]);
+      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;
+      $ws_global->write($row++, 0, [$key,$user_pattern_totals[$user_pattern_index]]) if $xls_fh;
       $user_pattern_index++;
     }
   }
-  if ($html) {
-    print "</table>\n";
+  print $txt_fh "\n" if $txt_fh;
+  print $htm_fh "</table>\n\n" if $htm_fh;
+  if ($xls_fh)
+  {
+    ++$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 "\n";
 }
 
 
+
+
+
 #######################################################################
 # print_transport();
 #
@@ -1944,25 +2789,27 @@ sub print_user_patterns {
 # Print totals by transport.
 #######################################################################
 sub print_transport {
-  my($format1);
   my(@chartdatanames);
   my(@chartdatavals_count);
   my(@chartdatavals_vol);
-  no integer;          #Lose this for charting the data.
+  no integer;                 #Lose this for charting the data.
+
+  my $txt_format1 = "  %-18s  %6s      %6d";
+  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%s</td><td align=\"right\">%d</td>";
 
-  if ($html) {
-    print "<hr><a name=\"transport\"></a><h2>Deliveries by Transport</h2>\n";
-    print "<table border=0 width=\"100%\">\n";
-    print "<tr><td>\n";
-    print "<table border=1>\n";
-    print "<tr><th>&nbsp;</th><th>Volume</th><th>Messages</th>\n";
-    $format1 = "<tr><td>%s</td><td align=\"right\">%s</td><td align=\"right\">%d</td>";
+  if ($txt_fh) {
+    print $txt_fh "Deliveries by transport\n";
+    print $txt_fh "-----------------------";
+    print $txt_fh "\n                      Volume    Messages\n";
   }
-  else {
-    print "Deliveries by transport\n";
-    print "-----------------------";
-    print "\n                      Volume    Messages\n";
-    $format1 = "  %-18s  %6s      %6d";
+  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%\"><tr><td><table border=1>\n";
+    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);
+    $ws_global->write(++$row, 1, ["Volume", "Messages"], $f_headertab);
   }
 
   my($key);
@@ -1970,29 +2817,34 @@ sub print_transport {
     # We are getting our data from previous reports.
     foreach $key (sort keys %{$report_totals{transport}}) {
       my $count = get_report_total($report_totals{transport}{$key},'Messages');
-      printf("$format1\n",$key,
-       volume_rounded($report_totals{transport}{$key}{Volume},$report_totals{transport}{$key}{'Volume-gigs'}),
-       $count);
+      my @content=($key, volume_rounded($report_totals{transport}{$key}{Volume},
+        $report_totals{transport}{$key}{'Volume-gigs'}), $count);
       push(@chartdatanames, $key);
       push(@chartdatavals_count, $count);
       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;
+      $ws_global->write(++$row, 0, \@content) if $xls_fh;
     }
   }
   else {
     # We are getting our data from mainlog files.
     foreach $key (sort keys %transported_data) {
-      printf("$format1\n",$key,
-       volume_rounded($transported_data{$key},$transported_data_gigs{$key}),
-       $transported_count{$key});
+      my @content=($key, volume_rounded($transported_data{$key},$transported_data_gigs{$key}),
+        $transported_count{$key});
       push(@chartdatanames, $key);
       push(@chartdatavals_count, $transported_count{$key});
       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;
+      $ws_global->write(++$row, 0, \@content) if $xls_fh;
     }
   }
-  if ($html) {
-    print "</table>\n";
-    print "</td><td>\n";
-    if ($HAVE_GD_Graph_pie && $charts)
+  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 = (
@@ -2007,16 +2859,16 @@ sub print_transport {
       );
       my $gd = $graph->plot(\@data) or warn($graph->error);
       if ($gd) {
-       open(IMG, ">$chartdir/transports_count.png") or die $!;
-       binmode IMG;
-       print IMG $gd->png;
-       close IMG;
-       print "<img src=\"$chartrel/transports_count.png\">";
+        open(IMG, ">$chartdir/transports_count.png") or die "Could not write $chartdir/transports_count.png: $!\n";
+        binmode IMG;
+        print IMG $gd->png;
+        close IMG;
+        print $htm_fh "<img src=\"$chartrel/transports_count.png\">";
       }
     }
-    print "</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
@@ -2027,16 +2879,16 @@ sub print_transport {
       );
       my $gd = $graph->plot(\@data) or warn($graph->error);
       if ($gd) {
-       open(IMG, ">$chartdir/transports_vol.png") or die $!;
-       binmode IMG;
-       print IMG $gd->png;
-       close IMG;
-       print "<img src=\"$chartrel/transports_vol.png\">";
+        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\">";
       }
     }
-    print "</td></tr></table>\n";
+
+    print $htm_fh "</td></tr></table>\n\n";
   }
-  print "\n";
 }
 
 
@@ -2049,40 +2901,60 @@ sub print_transport {
 # Print our totals by relay.
 #######################################################################
 sub print_relay {
+  my $row_print_relay=1;
   my $temp = "Relayed messages";
-  print "<hr><a name=\"$temp\"></a><h2>$temp</h2>\n" if $html;
+  print $htm_fh "<hr><a name=\"$temp\"></a><h2>$temp</h2>\n" if $htm_fh;
   if (scalar(keys %relayed) > 0 || $relayed_unshown > 0) {
     my $shown = 0;
     my $spacing = "";
-    my($format);
+    my $txt_format = "%7d %s\n      => %s\n";
+    my $htm_format = "<tr><td align=\"right\">%d</td><td>%s</td><td>%s</td>\n";
 
-    if ($html) {
-      print "<table border=1>\n";
-      print "<tr><th>Count</th><th>From</th><th>To</th>\n";
-      $format = "<tr><td align=\"right\">%d</td><td>%s</td><td>%s</td>\n";
+    printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
+    if ($htm_fh) {
+      print $htm_fh "<table border=1>\n";
+      print $htm_fh "<tr><th>Count</th><th>From</th><th>To</th>\n";
     }
-    else {
-      printf("%s\n%s\n\n", $temp, "-" x length($temp));
-      $format = "%7d %s\n      => %s\n";
+    if ($xls_fh) {
+      $ws_relayed->write($row_print_relay++, $col, $temp, $f_header2);
+      &set_worksheet_line($ws_relayed, $row_print_relay++, 0, ["Count", "From", "To"], $f_headertab);
     }
 
+
     my($key);
     foreach $key (sort keys %relayed) {
       my $count = $relayed{$key};
       $shown += $count;
       $key =~ s/[HA]=//g;
       my($one,$two) = split(/=> /, $key);
-      printf($format, $count, $one, $two);
+      my @content=($count, $one, $two);
+      printf $txt_fh ($txt_format, @content) if $txt_fh;
+      printf $htm_fh ($htm_format, @content) if $htm_fh;
+      if ($xls_fh)
+      {
+        &set_worksheet_line($ws_relayed, $row_print_relay++, 0, \@content);
+      }
       $spacing = "\n";
     }
-    print "</table>\n<p>\n" if $html;
-    print "${spacing}Total: $shown (plus $relayed_unshown unshown)\n";
+
+    print $htm_fh "</table>\n<p>\n" if $htm_fh;
+    print $txt_fh "${spacing}Total: $shown (plus $relayed_unshown unshown)\n\n" if $txt_fh;
+    print $htm_fh "${spacing}Total: $shown (plus $relayed_unshown unshown)\n\n" if $htm_fh;
+    if ($xls_fh)
+    {
+       &set_worksheet_line($ws_relayed, $row_print_relay++, 0, [$shown, "Sum of shown" ]);
+       &set_worksheet_line($ws_relayed, $row_print_relay++, 0, [$relayed_unshown, "unshown"]);
+       $row_print_relay++;
+    }
   }
   else {
-    print "No relayed messages\n";
-    print "-------------------\n" unless $html;
+    print $txt_fh "No relayed messages\n-------------------\n\n" if $txt_fh;
+    print $htm_fh "No relayed messages\n\n" if $htm_fh;
+    if ($xls_fh)
+    {
+      $row_print_relay++;
+    }
   }
-  print "\n";
 }
 
 
@@ -2097,49 +2969,71 @@ sub print_relay {
 #######################################################################
 sub print_errors {
   my $total_errors = 0;
+  $row=1;
 
   if (scalar(keys %errors_count) != 0) {
     my $temp = "List of errors";
-    my($format);
-    if ($html) {
-      print "<hr><a name=\"errors\"></a><h2>$temp</h2>\n";
-      print "<ul><li><b>Count - Error</b>\n";
-      $format = "<li>%d - %s\n";
+    my $htm_format = "<li>%d - %s\n";
+
+    printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
+    if ($htm_fh) {
+      print $htm_fh "<hr><a name=\"errors\"></a><h2>$temp</h2>\n";
+      print $htm_fh "<ul><li><b>Count - Error</b>\n";
     }
-    else {
-      printf("%s\n%s\n\n", $temp, "-" x length($temp));
+    if ($xls_fh)
+    {
+      $ws_errors->write($row++, 0, $temp, $f_header2);
+      &set_worksheet_line($ws_errors, $row++, 0, ["Count", "Error"], $f_headertab);
     }
 
+
     my($key);
     foreach $key (sort keys %errors_count) {
       my $text = $key;
       chomp($text);
-      $text =~ s/\s\s+/ /g;    #Convert multiple spaces to a single space.
+      $text =~ s/\s\s+/ /g;   #Convert multiple spaces to a single space.
       $total_errors += $errors_count{$key};
-      if ($html) {
+
+      if ($txt_fh) {
+        printf $txt_fh ("%5d ", $errors_count{$key});
+        my $text_remaining = $text;
+        while (length($text_remaining) > 65) {
+          my($first,$rest) = $text_remaining =~ /(.{50}\S*)\s+(.+)/;
+          last if !$first;
+          printf $txt_fh ("%s\n\t    ", $first);
+          $text_remaining = $rest;
+        }
+        printf $txt_fh ("%s\n\n", $text_remaining);
+      }
+
+      if ($htm_fh) {
 
         #Translate HTML tag characters. Sergey Sholokh.
         $text =~ s/\</\&lt\;/g;
         $text =~ s/\>/\&gt\;/g;
 
-       printf($format,$errors_count{$key},$text);
+        printf $htm_fh ($htm_format,$errors_count{$key},$text);
       }
-      else {
-       printf("%5d ", $errors_count{$key});
-       while (length($text) > 65) {
-         my($first,$rest) = $text =~ /(.{50}\S*)\s+(.+)/;
-         last if !$first;
-         printf("%s\n      ", $first);
-         $text = $rest;
-       }
-       printf("%s\n\n", $text);
+      if ($xls_fh)
+      {
+        &set_worksheet_line($ws_errors, $row++, 0, [$errors_count{$key},$text]);
       }
     }
-    print "</ul>\n<p>\n" if $html;
 
     $temp = "Errors encountered: $total_errors";
-    print $temp,"\n";
-    print "-" x length($temp),"\n" unless $html;
+
+    if ($txt_fh) {
+      print $txt_fh $temp, "\n";
+      print $txt_fh "-" x length($temp),"\n";
+    }
+    if ($htm_fh) {
+      print $htm_fh "</ul>\n<p>\n";
+      print $htm_fh $temp, "\n";
+    }
+    if ($xls_fh)
+    {
+        &set_worksheet_line($ws_errors, $row++, 0, [$total_errors, "Sum of Errors encountered"]);
+    }
   }
 
 }
@@ -2156,6 +3050,7 @@ sub print_errors {
 # 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' /<=/"
 #
@@ -2185,31 +3080,62 @@ sub parse_old_eximstat_reports {
 
   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+)?)/) {
       $begin = $1 if ($1 lt $begin);
       $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.
-
-#  TOTAL               Volume    Messages    Hosts Domains      Delayed       Failed
-#  Received              26MB         237      177      23       8  3.4%     28 11.8%
-#  Delivered             13MB         233       99      88
-       if (/TOTAL\s+(.*?)\s*$/) {
-         @fields = split(/\s+/,$1);
+        $_ = 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 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*$/) {
+          $doing_table = 1;
+          @delivered_fields = split(/\s+/,$1);
+
           #Delayed and Failed have two columns each, so add the extra field names in.
-         splice(@fields,-1,1,'DelayedPercent','Failed','FailedPercent');
-       }
-       elsif (/(Received|Delivered)\s+(.*?)\s*$/) {
-         print STDERR "Parsing $_" if $debug;
-         add_to_totals($report_totals{$1},\@fields,$2);
-       }
-       last if (/Delivered/);          #Last line of this section.
+          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)\s+(.*?)\s*$/) {
+          print STDERR "Parsing $_" if $debug;
+          add_to_totals($report_totals{$1},\@fields,$2);
+        }
+        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 (/(Rejects|Ham|Spam)\s+(.*?)\s*$/) {
+          print STDERR "Parsing $_" if $debug;
+          add_to_totals($report_totals{$1},['Messages','Hosts'],$2);
+        }
+        else {
+          last if $doing_table;
+        }
       }
     }
 
@@ -2219,18 +3145,24 @@ sub parse_old_eximstat_reports {
 #                       Total
 #  Description             85
 
-      while (<$fh>) { last if (/Total/); }     #Wait until we get the table headers.
+      while (<$fh>) { last if (/Total/); }  #Wait until we get the table headers.
       while (<$fh>) {
-       print STDERR "Parsing $_" if $debug;
-       $_ = html2txt($_);              #Convert general HTML markup to text.
-       if (/^\s*(.*?)\s+(\d+)\s*$/) {
-         $report_totals{patterns}{$1} = {} unless (defined $report_totals{patterns}{$1});
-         add_to_totals($report_totals{patterns}{$1},['Total'],$2);
-       }
-       last if (/^\s*$/);                      #Finished if we have a blank line.
+        print STDERR "Parsing $_" if $debug;
+        $_ = html2txt($_);              #Convert general HTML markup to text.
+        if (/^\s*(.*?)\s+(\d+)\s*$/) {
+          $report_totals{patterns}{$1} = {} unless (defined $report_totals{patterns}{$1});
+          add_to_totals($report_totals{patterns}{$1},['Total'],$2);
+        }
+        last if (/^\s*$/);              #Finished if we have a blank line.
       }
     }
 
+    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
 #-----------------------
@@ -2239,45 +3171,26 @@ sub parse_old_eximstat_reports {
 #  address_pipe         655KB           1
 #  smtp                  11MB         151
 
-      while (<$fh>) { last if (/Volume/); }    #Wait until we get the table headers.
+      while (<$fh>) { last if (/Volume/); }  #Wait until we get the table headers.
       while (<$fh>) {
-       print STDERR "Parsing $_" if $debug;
-       $_ = html2txt($_);              #Convert general HTML markup to text.
-       if (/(\S+)\s+(\d+\S*\s+\d+)/) {
-         $report_totals{transport}{$1} = {} unless (defined $report_totals{transport}{$1});
-         add_to_totals($report_totals{transport}{$1},['Volume','Messages'],$2);
-       }
-       last if (/^\s*$/);                      #Finished if we have a blank line.
+        print STDERR "Parsing $_" if $debug;
+        $_ = html2txt($_);              #Convert general HTML markup to text.
+        if (/(\S+)\s+(\d+\S*\s+\d+)/) {
+          $report_totals{transport}{$1} = {} unless (defined $report_totals{transport}{$1});
+          add_to_totals($report_totals{transport}{$1},['Volume','Messages'],$2);
+        }
+        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
 #-------------------------------------
 #
@@ -2289,34 +3202,68 @@ 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.
-      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.
-       $reached_table = 1 if (/^\s*Under/);
-       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 $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]) {
-             $$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 ?
-       }
+        $_ = html2txt($_);              #Convert general HTML markup to text.
+        # 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;
+          my($modifier,$formated_time,$count) = ($1,$2,$3);
+          my $seconds = unformat_time($formated_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);
+          for ($i = 0; $i <= $#$times_aref; $i++) {
+            if ($time_on_queue < $times_aref->[$i]) {
+              $$bin_aref[$i] += $count;
+              last;
+            }
+          }
+          $$overflow_sref += $count if ($i > $#$times_aref);
+
+        }
       }
     }
 
@@ -2332,23 +3279,23 @@ sub parse_old_eximstat_reports {
       my $reached_table = 0;
       my($count,$sender);
       while (<$fh>) {
-       unless ($reached_table) {
-         last if (/No relayed messages/);
-         $reached_table = 1 if (/^\s*\d/ || />\d+</);
-         next unless $reached_table;
-       }
-       if (/>(\d+)<.td><td>(.*?) ?<.td><td>(.*?)</) {
-         update_relayed($1,$2,$3);
-       }
-       elsif (/^\s*(\d+)\s+(.*?)\s*$/) {
-         ($count,$sender) = ($1,$2);
-       }
-       elsif (/=>\s+(.*?)\s*$/) {
-         update_relayed($count,$sender,$1);
-       }
-       else {
-         last;                                 #Finished the table ?
-       }
+        unless ($reached_table) {
+          last if (/No relayed messages/);
+          $reached_table = 1 if (/^\s*\d/ || />\d+</);
+          next unless $reached_table;
+        }
+        if (/>(\d+)<.td><td>(.*?) ?<.td><td>(.*?)</) {
+          update_relayed($1,$2,$3);
+        }
+        elsif (/^\s*(\d+)\s+(.*?)\s*$/) {
+          ($count,$sender) = ($1,$2);
+        }
+        elsif (/=>\s+(.*?)\s*$/) {
+          update_relayed($count,$sender,$1);
+        }
+        else {
+          last;                           #Finished the table ?
+        }
       }
     }
 
@@ -2367,101 +3314,137 @@ sub parse_old_eximstat_reports {
       #Set up a hash to record which entries we have already seen
       #and one to record which ones we are seeing for the first time.
       if ($by_count_or_volume =~ /count/) {
-       undef %league_table_value_entered;
-       undef %league_table_value_was_zero;
-       undef %table_order;
+        undef %league_table_value_entered;
+        undef %league_table_value_was_zero;
+        undef %table_order;
       }
 
       #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/) {
-        $count_href      = \%received_count_user;
-       $data_href       = \%received_data_user;
-       $data_gigs_href  = \%received_data_gigs_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)
-        $count_href      = \%{$received_count{"\u$1"}};
-       $data_href       = \%{$received_data{"\u$1"}};
-       $data_gigs_href  = \%{$received_data_gigs{"\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/) {
-        $count_href      = \%delivered_count_user;
-       $data_href       = \%delivered_data_user;
-       $data_gigs_href  = \%delivered_data_gigs_user;
+        $messages_href   = \%delivered_messages_user;
+        $addresses_href  = \%delivered_addresses_user;
+        $data_href       = \%delivered_data_user;
+        $data_gigs_href  = \%delivered_data_gigs_user;
       }
       elsif ($category =~ /(\S+) destination/) {
         #Top 50 (host|domain|email|edomain) destinations
         #Top (host|domain|email|edomain) destination
-        $count_href      = \%{$delivered_count{"\u$1"}};
-       $data_href       = \%{$delivered_data{"\u$1"}};
-       $data_gigs_href  = \%{$delivered_data_gigs{"\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"}};
+      }
+      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 rejection reasons/) {
+        $messages_href      = \%rejected_count_by_reason;
       }
 
       my $reached_table = 0;
+      my $row_re;
       while (<$fh>) {
-       $_ = html2txt($_);              #Convert general HTML markup to text.
-       $reached_table = 1 if (/^\s*\d/);
-       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.
-
-         #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;
-         }
-       }
-       else {          #Finished the table ?
-         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
-             #from the count table ordered at the back.
-             @{$table_order{$rounded_volume}{volume}} = () unless defined $table_order{$rounded_volume}{volume};
-             @{$table_order{$rounded_volume}{'message count'}} = () unless defined $table_order{$rounded_volume}{'message count'};
-             my(@order,%mark);
-             map {$mark{$_} = 1} @{$table_order{$rounded_volume}{volume}};
-             @order = @{$table_order{$rounded_volume}{volume}};
-             map {push(@order,$_)} grep(!$mark{$_},@{$table_order{$rounded_volume}{'message count'}});
-
-             my $bonus_bytes = $#order;
-             $bonus_bytes = 511 if ($bonus_bytes > 511);       #Don't go over the half-K boundary!
-             while (@order and ($bonus_bytes > 0)) {
-               my $entry = shift(@order);
-               if ($league_table_value_was_zero{$entry}) {
-                 $$data_href{$entry} += $bonus_bytes;
-                 print STDERR "$category by $by_count_or_volume: added $bonus_bytes bonus bytes to $entry\n" if $debug;
-               }
-               $bonus_bytes--;
-             }
-           }
-         }
-
-         last;
-       }
+        # Watch out for empty tables.
+        goto PARSE_OLD_REPORT_LINE if (/<h2>/ or (/^\s*[a-zA-Z]/ && !/^\s*Messages/));
+
+        $_ = html2txt($_);              #Convert general HTML markup to text.
+
+        # 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;
+
+        my($messages, $addresses, $rounded_volume, $entry);
+
+        if (/$row_re/) {
+          ($messages, $addresses, $rounded_volume, $entry) = ($1, $2, $3, $4);
+        }
+        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.
+            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
+              #from the count table ordered at the back.
+              @{$table_order{$rounded_volume}{volume}} = () unless defined $table_order{$rounded_volume}{volume};
+              @{$table_order{$rounded_volume}{'message count'}} = () unless defined $table_order{$rounded_volume}{'message count'};
+              my(@order,%mark);
+              map {$mark{$_} = 1} @{$table_order{$rounded_volume}{volume}};
+              @order = @{$table_order{$rounded_volume}{volume}};
+              map {push(@order,$_)} grep(!$mark{$_},@{$table_order{$rounded_volume}{'message count'}});
+
+              my $bonus_bytes = $#order;
+              $bonus_bytes = 511 if ($bonus_bytes > 511);  #Don't go over the half-K boundary!
+              while (@order and ($bonus_bytes > 0)) {
+                my $entry = shift(@order);
+                if ($league_table_value_was_zero{$entry}) {
+                  $$data_href{$entry} += $bonus_bytes;
+                  print STDERR "$category by $by_count_or_volume: added $bonus_bytes bonus bytes to $entry\n" if $debug;
+                }
+                $bonus_bytes--;
+              }
+            }
+          }
+          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/) {
@@ -2478,37 +3461,66 @@ sub parse_old_eximstat_reports {
       my $reached_table = 0;
       my($count,$error,$blanks);
       while (<$fh>) {
-       $reached_table = 1 if (/^( *|<li>)(\d+)/);
-       next unless $reached_table;
+        $reached_table = 1 if (/^( *|<li>)(\d+)/);
+        next unless $reached_table;
 
-       s/^<li>(\d+) -/$1/;     #Convert an HTML line to a text line.
-       $_ = html2txt($_);      #Convert general HTML markup to text.
+        s/^<li>(\d+) -/$1/;     #Convert an HTML line to a text line.
+        $_ = html2txt($_);      #Convert general HTML markup to text.
 
-       if (/\t\s*(.*)/) {
-         $error .= ' ' . $1;   #Join a multiline error.
-       }
-       elsif (/^\s*(\d+)\s+(.*)/) {
-         if ($error) {
+        if (/\t\s*(.*)/) {
+          $error .= ' ' . $1;   #Join a multiline error.
+        }
+        elsif (/^\s*(\d+)\s+(.*)/) {
+          if ($error) {
             #Finished with a previous multiline error so save it.
-           $errors_count{$error} = 0 unless $errors_count{$error};
-           $errors_count{$error} += $count;
-         }
-         ($count,$error) = ($1,$2);
-       }
-       elsif (/Errors encountered/) {
-         if ($error) {
+            $errors_count{$error} = 0 unless $errors_count{$error};
+            $errors_count{$error} += $count;
+          }
+          ($count,$error) = ($1,$2);
+        }
+        elsif (/Errors encountered/) {
+          if ($error) {
             #Finished the section, so save our stored last error.
-           $errors_count{$error} = 0 unless $errors_count{$error};
-           $errors_count{$error} += $count;
-         }
-         last;
-       }
+            $errors_count{$error} = 0 unless $errors_count{$error};
+            $errors_count{$error} += $count;
+          }
+          last;
+        }
       }
     }
 
   }
 }
 
+#######################################################################
+# 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;
+    }
+  }
+}
 
 
 #######################################################################
@@ -2550,22 +3562,39 @@ sub update_relayed {
 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"});
-      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 seperated 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();
 #
@@ -2599,7 +3628,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.
-  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 '>'.
@@ -2643,6 +3672,55 @@ sub get_next_arg {
   return $arg;
 }
 
+#######################################################################
+# set_worksheet_line($ws_global, $startrow, $startcol, \@content, $format);
+#
+# set values to a sequence of cells in a row.
+#
+#######################################################################
+sub set_worksheet_line {
+  my ($worksheet, $row, $col, $content, $format) = @_;
+
+  foreach my $token (@$content)
+  {
+     $worksheet->write($row, $col++, $token, $format );
+  }
+
+}
+
+#######################################################################
+# @rcpt_times = parse_time_list($string);
+#
+# Parse a comma seperated 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;
+}
 
 
 ##################################################
@@ -2658,6 +3736,7 @@ $show_transport = 1;
 $topcount = 50;
 $local_league_table = 1;
 $include_remote_users = 0;
+$include_original_destination = 0;
 $hist_opt = 1;
 $volume_rounding = 1;
 $localtime_offset = calculate_localtime_offset();    # PH/FANF
@@ -2667,30 +3746,28 @@ $charts_option_specified = 0;
 $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;
+$col=0;
+$col_hist=0;
+$run_hist=0;
+my(%output_files);     # What output files have been specified?
+
 # 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 }
-  elsif ($ARGV[0] =~ /^\-nr(.?)(.*)\1$/)
-    {
+  elsif ($ARGV[0] =~ /^\-nr(.?)(.*)\1$/) {
     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$/)
     {
@@ -2698,7 +3775,16 @@ while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-')
     }
   elsif ($ARGV[0] =~ /^-t(\d+)$/)   { $topcount = $1 }
   elsif ($ARGV[0] =~ /^-tnl$/)      { $local_league_table = 0 }
-  elsif ($ARGV[0] =~ /^-html$/)     { $html = 1 }
+  elsif ($ARGV[0] =~ /^-txt=?(\S*)$/)  { $txt_fh = get_filehandle($1,\%output_files) }
+  elsif ($ARGV[0] =~ /^-html=?(\S*)$/) { $htm_fh = get_filehandle($1,\%output_files) }
+  elsif ($ARGV[0] =~ /^-xls=?(\S*)$/) {
+    if ($HAVE_Spreadsheet_WriteExcel) {
+      $xls_fh = get_filehandle($1,\%output_files);
+    }
+    else {
+      warn "WARNING: CPAN Module Spreadsheet::WriteExcel not installed. Obtain from www.cpan.org\n";
+    }
+  }
   elsif ($ARGV[0] =~ /^-merge$/)    { $merge_reports = 1 }
   elsif ($ARGV[0] =~ /^-charts$/)   {
     $charts = 1;
@@ -2707,13 +3793,17 @@ while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-')
   }
   elsif ($ARGV[0] =~ /^-chartdir$/) { $chartdir = $ARGV[1]; shift; $charts_option_specified = 1; }
   elsif ($ARGV[0] =~ /^-chartrel$/) { $chartrel = $ARGV[1]; shift; $charts_option_specified = 1; }
-  elsif ($ARGV[0] =~ /^-cache$/)    { }        #Not currently used.
+  elsif ($ARGV[0] =~ /^-include_original_destination$/)    { $include_original_destination = 1 }
+  elsif ($ARGV[0] =~ /^-cache$/)    { } #Not currently used.
   elsif ($ARGV[0] =~ /^-byhost$/)   { $do_sender{Host} = 1 }
   elsif ($ARGV[0] =~ /^-bydomain$/) { $do_sender{Domain} = 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] =~ /^-emptyok$/)  { $emptyOK = 1 }
   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 }
@@ -2735,32 +3825,109 @@ while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-')
   shift;
   }
 
+  # keep old default behaviour
+  if (! ($xls_fh or $htm_fh or $txt_fh)) {
+    $txt_fh = \*STDOUT;
+  }
+
   # Check that all the charts options are specified.
   warn "-charts option not specified. Use -help for help.\n" if ($charts_option_specified && ! $charts);
 
   # Default to display tables by sending Host.
   $do_sender{Host} = 1 unless ($do_sender{Domain} || $do_sender{Email} || $do_sender{Edomain});
 
+  # prepare xls Excel Workbook
+  if (defined $xls_fh) {
+
+    # Create a new Excel workbook
+    $workbook  = Spreadsheet::WriteExcel->new($xls_fh);
+
+    # Add worksheets
+    $ws_global = $workbook->addworksheet('Exim Statistik');
+    # show $ws_global as initial sheet
+    $ws_global->set_first_sheet();
+    $ws_global->activate();
+
+    if ($show_relay) {
+      $ws_relayed = $workbook->addworksheet('Relayed Messages');
+      $ws_relayed->set_column(1, 2,  80);
+    }
+    if ($show_errors) {
+      $ws_errors = $workbook->addworksheet('Errors');
+    }
+
+
+    # set column widths
+    $ws_global->set_column(0, 2,  20); # Columns B-D width set to 30
+    $ws_global->set_column(3, 3,  15); # Columns B-D width set to 30
+    $ws_global->set_column(4, 4,  25); # Columns B-D width set to 30
+
+    # Define Formats
+    $f_default = $workbook->add_format();
+
+    $f_header1 = $workbook->add_format();
+    $f_header1->set_bold();
+    #$f_header1->set_color('red');
+    $f_header1->set_size('15');
+    $f_header1->set_valign();
+    # $f_header1->set_align('center');
+    # $ws_global->write($row++, 2, "Testing Headers 1", $f_header1);
+
+    $f_header2 = $workbook->add_format();
+    $f_header2->set_bold();
+    $f_header2->set_size('12');
+    $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_headertab = $workbook->add_format();
+    $f_headertab->set_bold();
+    $f_headertab->set_valign();
+    # $ws_global->write($row++, 2, "Testing Headers tab", $f_headertab);
+
+  }
+
+
+# Initialise the queue/delivery/rcpt time counters.
 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)
     {
-    print "Eximstats: -h must specify a factor of 60\n";
+    print STDERR "Eximstats: -h must specify a factor of 60\n";
     exit 1;
     }
-  $hist_interval = 60/$hist_opt;               #Interval in minutes.
-  $hist_number = (24*60)/$hist_interval;       #Number of intervals per day.
+  $hist_interval = 60/$hist_opt;                #Interval in minutes.
+  $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;
 
@@ -2770,15 +3937,20 @@ $total_received_count = 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;
+$message_errors = 0;
 $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','Rejects','Ham','Spam') {
   foreach $type ('Volume','Messages','Delayed','Failed','Hosts','Domains','Emails','Edomains') {
     $report_totals{$section}{$type} = 0;
   }
@@ -2794,20 +3966,20 @@ if (@ARGV) {
   foreach my $file (@ARGV) {
     if ($file =~ /\.gz/) {
       unless (open(FILE,"gunzip -c $file |")) {
-       print STDERR "Failed to gunzip -c $file: $!";
-       next;
+        print STDERR "Failed to gunzip -c $file: $!";
+        next;
       }
     }
     elsif ($file =~ /\.Z/) {
       unless (open(FILE,"uncompress -c $file |")) {
-       print STDERR "Failed to uncompress -c $file: $!";
-       next;
+        print STDERR "Failed to uncompress -c $file: $!";
+        next;
       }
     }
     else {
       unless (open(FILE,$file)) {
-       print STDERR "Failed to read $file: $!";
-       next;
+        print STDERR "Failed to read $file: $!";
+        next;
       }
     }
     #Now parse the filehandle, updating the global variables.
@@ -2821,8 +3993,8 @@ else {
 }
 
 
-if ($begin eq "9999-99-99 99:99:99") {
-  print "**** No valid log lines read\n";
+if ($begin eq "9999-99-99 99:99:99" && ! $emptyOK) {
+  print STDERR "**** No valid log lines read\n";
   exit 1;
 }
 
@@ -2833,20 +4005,36 @@ print_grandtotals();
 # 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_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_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.
@@ -2854,26 +4042,49 @@ print_relay() if $show_relay;
 
 # Print the league tables, if topcount isn't zero.
 if ($topcount > 0) {
+  my($ws_rej, $ws_top50, $ws_rej_row, $ws_top50_row);
+  $ws_rej_row = $ws_top50_row = 0;
+  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);
+  }
+
+  print_league_table("mail rejection reason", \%rejected_count_by_reason, undef, undef, undef, $ws_rej, \$ws_rej_row) if %rejected_count_by_reason;
+
   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{$_};
-    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("rejected ip", \%rejected_count_by_ip, undef, undef, undef, $ws_rej, \$ws_rej_row) if %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_errors() if $show_errors;
 
-if ($html) {
-  print "</body>\n</html>\n"
+print $htm_fh "</body>\n</html>\n" if $htm_fh;
+
+
+$txt_fh->close if $txt_fh && ref $txt_fh;
+$htm_fh->close if $htm_fh;
+
+if ($xls_fh) {
+  # close Excel Workbook
+  $ws_global->set_first_sheet();
+  # FIXME: whyever - activate does not work :-/
+  $ws_global->activate();
+  $workbook->close();
 }
 
+
 # End of eximstats