Some systems need an explicit -ldl in order to support EXPAND_DLFUNC
[exim.git] / src / src / eximstats.src
index 533098459b9c04eac4c291dc3a4d2d9587ef287f..c39ecd5e7c7b72c4f91b285f14b1fab3b7ec4659 100644 (file)
@@ -1,5 +1,5 @@
 #!PERL_COMMAND -w
-# $Cambridge: exim/src/src/eximstats.src,v 1.3 2004/12/20 13:12:32 steve Exp $
+# $Cambridge: exim/src/src/eximstats.src,v 1.7 2005/06/03 14:28:50 steve Exp $
 
 # Copyright (c) 2001 University of Cambridge.
 # See the file NOTICE for conditions of use and distribution.
 #             Added warnings if required GD::Graph modules are not available or
 #             insufficient -chart* options are specified.
 #
-# 2004-02-20  V1.31 Andrea Balzi 
+# 2004-02-20  V1.31 Andrea Balzi
 #             Only show the Local Sender/Destination links if the tables exist.
 #
 # 2004-07-05  V1.32 Steve Campbell
 # 2004-12-10  V1.34 Steve Campbell
 #             Eximstats can now parse syslog lines as well as mainlog lines.
 #
-# 2004-12-20  V1.35 Andreas Metzler
-#            Pie charts by volume were actually generated by count. Fixed.
-
+# 2004-12-20  V1.35 Wouter Verhelst
+#             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.
+#
+#
+#
+# For documentation on the logfile format, see
+# http://www.exim.org/exim-html-4.50/doc/html/spec_48.html#IX2793
 
 =head1 NAME
 
@@ -201,11 +221,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
 
@@ -253,6 +304,12 @@ 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<-byhost>
 
 Show results by sending host. This may be combined with
@@ -319,13 +376,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
@@ -368,8 +422,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
@@ -384,19 +438,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;
 
 
 ##################################################
@@ -419,7 +478,7 @@ 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.35';
+$VERSION = '1.39';
 
 # How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
 $COLUMN_WIDTHS = 8;
@@ -428,15 +487,22 @@ $COLUMN_WIDTHS = 8;
 use vars qw($total_received_data  $total_received_data_gigs  $total_received_count);
 use vars qw($total_delivered_data $total_delivered_data_gigs $total_delivered_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(%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
+use vars qw($workbook $ws_global $ws_relayed $ws_top50 $ws_errors );   #For use in Speadsheed::WriteExcel
+use vars qw($row $col $row_hist $col_hist $row_league_table);
+use vars qw($run_hist);
+use vars qw($f_default $f_header1 $f_header2 $f_headertab $f_percent); #Format Header
+
+# Output FileHandles
+use vars qw($txt_fh $htm_fh $xls_fh);
 
 $ntopchart = 5;
 
@@ -445,11 +511,13 @@ $ntopchart = 5;
 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($relay_pattern @queue_times @user_patterns @user_descriptions);
+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.
@@ -472,6 +540,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();
@@ -522,7 +620,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;
@@ -531,16 +635,16 @@ sub volume_rounded {
 
 #######################################################################
 # un_round();
-# 
+#
 #  un_round($rounded_volume,\$bytes,\$gigabytes);
-# 
+#
 # Given a volume in KB, MB or GB, as generated by volume_rounded(),
 # do the reverse transformation and convert it back into Bytes and Gigabytes.
 # These are added to the $bytes and $gigabytes parameters.
-# 
+#
 # Given a data size in bytes, round it to KB, MB, or GB
 # as appropriate.
-# 
+#
 # EG: 500 => (500,0), 14GB => (0,14), etc.
 #######################################################################
 sub un_round {
@@ -558,7 +662,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;
   }
 
@@ -569,9 +675,9 @@ sub un_round {
 
 #######################################################################
 # add_volume();
-# 
+#
 #   add_volume(\$bytes,\$gigs,$size);
-# 
+#
 # Add $size to $bytes/$gigs where this is a number split into
 # bytes ($bytes) and gigabytes ($gigs). This is significantly
 # faster than using Math::BigInt.
@@ -590,12 +696,12 @@ sub add_volume {
 
 #######################################################################
 # format_time();
-# 
+#
 #  $formatted_time = format_time($seconds);
-# 
+#
 # Given a time in seconds, break it down into
 # weeks, days, hours, minutes, and seconds.
-# 
+#
 # Eg 12005 => 3h20m5s
 #######################################################################
 sub format_time {
@@ -620,11 +726,11 @@ $p;
 
 #######################################################################
 #  unformat_time();
-# 
+#
 #  $seconds = unformat_time($formatted_time);
-# 
+#
 # Given a time in weeks, days, hours, minutes, or seconds, convert it to seconds.
-# 
+#
 # Eg 3h20m5s => 12005
 #######################################################################
 sub unformat_time {
@@ -644,9 +750,9 @@ sub unformat_time {
 
 #######################################################################
 # seconds();
-# 
+#
 #  $time = seconds($timestamp);
-# 
+#
 # Given a time-of-day timestamp, convert it into a time() value using
 # POSIX::mktime.  We expect the timestamp to be of the form
 # "$year-$mon-$day $hour:$min:$sec", with month going from 1 to 12,
@@ -656,14 +762,14 @@ sub unformat_time {
 # (with the -utc option), then we adjust the time by the current local
 # time offset so that it can be compared with the time recorded in message
 # IDs, which is UTC.
-# 
+#
 # To improve performance, we only use mktime on the date ($year-$mon-$day),
 # and only calculate it if the date is different to the previous time we
 # came here. We then add on seconds for the '$hour:$min:$sec'.
-# 
+#
 # We also store the results of the last conversion done, and only
 # recalculate if the date is different.
-# 
+#
 # We used to have the '-cache' flag which would store the results of the
 # mktime() call. However, the current way of just using mktime() on the
 # date obsoletes this.
@@ -711,9 +817,9 @@ sub seconds {
 
 #######################################################################
 #  id_seconds();
-# 
+#
 #  $time = id_seconds($message_id);
-# 
+#
 # Given a message ID, convert it into a time() value.
 #######################################################################
 sub id_seconds {
@@ -728,13 +834,13 @@ $s;
 
 #######################################################################
 #  calculate_localtime_offset();
-# 
+#
 #  $localtime_offset = calculate_localtime_offset();
-# 
+#
 # Calculate the the localtime offset from gmtime in seconds.
-# 
+#
 #  $localtime = time() + $localtime_offset.
-# 
+#
 # These are the same semantics as ISO 8601 and RFC 2822 timezone offsets.
 # (West is negative, East is positive.)
 #######################################################################
@@ -764,9 +870,9 @@ 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.
@@ -786,33 +892,62 @@ for ($i = 0; $i <= $#queue_times; $i++) { $queue_total += $$array[$i] }
 
 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=\"$string time\"></a><h2>$temp</h2>\n";
+  print $htm_fh "<table border=0 width=\"100%\">\n";
+  print $htm_fh "<tr><td>\n";
+  print $htm_fh "<table border=1>\n";
+  print $htm_fh "<tr><th>Time</th><th>Messages</th><th>Percentage</th><th>Cumulative Percentage</th>\n";
 }
-else
+if ($xls_fh)
 {
-  printf("%s\n%s\n\n", $temp, "-" x length($temp));
-  $format = "%5s %4s   %6d %5.1f%%  %5.1f%%\n";
+
+  $ws_global->write($row++, $col, "Time spent on the queue: ".$string, $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)
     {
     my $percent = ($$array[$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($queue_times[$i]),
+        $$array[$i], $percent, $cumulative_percent);
+
+    if ($htm_fh) {
+      printf $htm_fh ($htm_format, @content);
+      if (!defined($queue_times[$i])) {
+        print $htm_fh "Not defined";
+      }
+    }
+    if ($txt_fh) {
+      printf $txt_fh ($txt_format, @content);
+      if (!defined($queue_times[$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($queue_times[$i])) {
+        $col=0;
+        $ws_global->write($row++, $col, "Not defined"  );
+      }
+    }
+
     push(@chartdatanames,
       ($printed_one? "" : "Under") . format_time($queue_times[$i]));
     push(@chartdatavals, $$array[$i]);
@@ -823,18 +958,27 @@ for ($i = 0; $i <= $#queue_times; $i++) {
 if ($queue_more_than > 0) {
   my $percent = ($queue_more_than * 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($queue_times[$#queue_times]),
+        $queue_more_than, $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);
 
 #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>\n";
+  print $htm_fh "</td><td>\n";
 
   if ($HAVE_GD_Graph_pie && $charts) {
     my @data = (
@@ -851,25 +995,32 @@ if ($html) {
     );
     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";
+}
+
+if ($xls_fh)
+{
+  $row++;
 }
-print "\n";
+print $txt_fh "\n" if $txt_fh;
+print $htm_fh "\n" if $htm_fh;
+
 }
 
 
 
 #######################################################################
 # print_histogram();
-# 
+#
 #  print_histogram('Deliverieds|Messages received',@interval_count);
-# 
+#
 # Print a histogram of the messages delivered/received per time slot
 # (hour by default).
 #######################################################################
@@ -879,6 +1030,16 @@ my(@interval_count) = @_;
 my(@chartdatanames);
 my(@chartdatavals);
 my($maxd) = 0;
+
+if (!$run_hist) # save first row of print_histogram for xls output
+{
+  $row_hist = $row;
+}
+else
+{
+  $row = $row_hist;
+}
+
 for ($i = 0; $i < $hist_number; $i++)
   { $maxd = $interval_count[$i] if $interval_count[$i] > $maxd; }
 
@@ -887,31 +1048,40 @@ $scale = 1 if $scale == 0;
 
 my($type);
 if ($text eq "Deliveries")
-  {
+{
   $type = ($scale == 1)? "delivery" : "deliveries";
-  }
+}
 else
-  {
+{
   $type = ($scale == 1)? "message" : "messages";
-  }
+}
+
+# make and output title
+my $title = sprintf("$text per %s",
+    ($hist_interval == 60)? "hour" :
+    ($hist_interval == 1)?  "minute" : "$hist_interval minutes");
+
+my $txt_htm_title = $title . " (each dot is $scale $type)";
 
-my($title) = sprintf("$text per %s (each dot is $scale $type)",
-  ($hist_interval == 60)? "hour" :
-  ($hist_interval == 1)?  "minute" : "$hist_interval minutes");
+printf $txt_fh ("%s\n%s\n\n", $txt_htm_title, "-" x length($txt_htm_title)) if $txt_fh;
 
-if ($html) {
-  print "<hr><a name=\"$text\"></a><h2>$title</h2>\n";
-  print "<table border=0 width=\"100%\">\n";
-  print "<tr><td><pre>\n";
+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/ ;
+  $ws_global->write($row++, $col_hist+1, $title, $f_headertab);
 }
 
+
 my $hour = 0;
 my $minutes = 0;
 for ($i = 0; $i < $hist_number; $i++)
-  {
+{
   my $c = $interval_count[$i];
 
   # If the interval is an hour (the maximum) print the starting and
@@ -920,19 +1090,38 @@ for ($i = 0; $i < $hist_number; $i++)
 
   my $temp;
   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
+      {
+        &set_worksheet_line($ws_global, $row, 0, [$temp], $f_default);
+      }
+    }
+
     push(@chartdatanames, $temp);
     $hour++;
-    }
+  }
   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);
+      &set_worksheet_line($ws_global, $row, 0, [$temp], $f_default);
+    }
+
     push(@chartdatanames, $temp);
     $minutes += $hist_interval;
     if ($minutes >= 60)
@@ -940,15 +1129,25 @@ for ($i = 0; $i < $hist_number; $i++)
       $minutes = 0;
       $hour++;
       }
-    }
-  push(@chartdatavals, $c);
-  printf(" %6d %s\n", $c, "." x ($c/$scale));
   }
-print "\n";
-if ($html)
+  push(@chartdatavals, $c);
+
+  printf $txt_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $txt_fh;
+  printf $htm_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $htm_fh;
+  if ($xls_fh)
   {
-  print "</pre>\n";
-  print "</td><td>\n";
+    &set_worksheet_line($ws_global, $row++, $col_hist+1, [$c], $f_default);
+  }
+
+} #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) {
     # calculate the graph
     my @data = (
@@ -967,24 +1166,29 @@ if ($html)
     if ($text =~ /Messages/)   { $pngname = "histogram_mes.png"; }
     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
 }
 
 
 
 #######################################################################
 # print_league_table();
-# 
+#
 #  print_league_table($league_table_type,\%message_count,\%message_data,\%message_data_gigs);
-# 
+#
 # 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).
@@ -997,52 +1201,80 @@ 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>\u$text</th>\n";
+my $htm_format;
+my $txt_format = "%7d %10s   %s\n";
+
+# write header
+printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
+if ($htm_fh) {
+  print $htm_fh "<hr><a name=\"$text count\"></a><h2>$temp</h2>\n";
+  print $htm_fh "<table border=0 width=\"100%\">\n";
+  print $htm_fh "<tr><td>\n";
+  print $htm_fh "<table border=1>\n";
+  print $htm_fh "<tr><th>Messages</th><th>Bytes</th><th>Average</th><th>\u$text</th>\n";
 
   # 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=\"$align\" nowrap>%s</td>\n";
+  $htm_format = "<tr><td align=\"right\">%d</td><td align=\"right\">%s</td><td align=\"right\">%s</td><td align=\"$align\" nowrap>%s</td>\n";
 }
-else {
-  printf("%s\n%s\n\n", $temp, "-" x length($temp));
-  $format = "%7d %10s   %s\n";
+if ($xls_fh)
+{
+  $ws_top50->write($row_league_table++, 0, $temp, $f_header2);
+  &set_worksheet_line($ws_top50, $row_league_table++, 0, ["Messages", "Bytes", "Average", $text], $f_headertab );
 }
 
-my($key,$htmlkey);
+
+# write content
+my($key,$htmlkey,$rounded_volume,$rounded_average,$count,$data,$gigs);
 foreach $key (top_n_sort($topcount,$m_count,$m_data_gigs,$m_data)) {
-  if ($html) {
+
+  # When displaying the average figures, we calculate the average of
+  # the rounded data, as the user would calculate it. This reduces
+  # the accuracy slightly, but we have to do it this way otherwise
+  # when using -merge to convert results from text to HTML and
+  # vice-versa discrepencies would occur.
+  $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
+  $data = $gigs = 0;
+  un_round($rounded_volume,\$data,\$gigs);
+  $count = $$m_count{$key};
+  $rounded_average = volume_rounded($data/$count,$gigs/$count);
+  my @content=( $count, $rounded_volume, $rounded_average);
+
+  # write content
+  # any reason not to include rounded_average in txt-output? -fh
+  printf $txt_fh ($txt_format, $count, $rounded_volume, $key) if $txt_fh;
+
+  if ($htm_fh) {
     $htmlkey = $key;
     $htmlkey =~ s/>/\&gt\;/g;
     $htmlkey =~ s/</\&lt\;/g;
-    printf($format, $$m_count{$key}, volume_rounded($$m_data{$key},$$m_data_gigs{$key}), $htmlkey);
+    printf $htm_fh ($htm_format, @content, $htmlkey);
   }
-  else {
-    printf($format, $$m_count{$key}, volume_rounded($$m_data{$key},$$m_data_gigs{$key}), $key);
+  if ($xls_fh)
+  {
+    &set_worksheet_line($ws_top50, $row_league_table++, 0, [@content, $key], $f_default);
   }
+
   if (scalar @chartdatanames < $ntopchart)
-    {
+  {
     push(@chartdatanames, $key);
     push(@chartdatavals, $$m_count{$key});
-    }
+  }
   else
-    {
+  {
     $chartotherval += $$m_count{$key};
-    }
   }
+}
+
 push(@chartdatanames, "Other");
 push(@chartdatavals, $chartotherval);
 
-if ($html)
-  {
-  print "</table>\n";
-  print "</td><td>\n";
+print $txt_fh "\n" if $txt_fh;
+if ($htm_fh)
+{
+  print $htm_fh "</table>\n";
+  print $htm_fh "</td><td>\n";
   if ($HAVE_GD_Graph_pie && $charts)
     {
     # calculate the graph
@@ -1060,28 +1292,38 @@ if ($html)
     if ($gd) {
       my $temp = $text;
       $temp =~ s/ /_/g;
-      open(IMG, ">$chartdir/${temp}_count.png") or die $!;
+      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 "<img src=\"$chartrel/${temp}_count.png\">";
+      print $htm_fh "<img src=\"$chartrel/${temp}_count.png\">";
     }
   }
-  print "</td><td>\n";
-  print "</td></tr></table>\n";
+  print $htm_fh "</td><td>\n";
+  print $htm_fh "</td></tr></table>\n\n";
+}
+if ($xls_fh)
+{
+  $row_league_table++;
 }
-print "\n";
+
+
+# write header
 
 $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>\u$text</th>\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=\"$text volume\"></a><h2>$temp</h2>\n";
+  print $htm_fh "<table border=0 width=\"100%\">\n";
+  print $htm_fh "<tr><td>\n";
+  print $htm_fh "<table border=1>\n";
+  print $htm_fh "<tr><th>Messages</th><th>Bytes</th><th>Average</th><th>\u$text</th>\n";
 }
-else {
-  printf("%s\n%s\n\n", $temp, "-" x length($temp));
+if ($xls_fh)
+{
+  $ws_top50->write($row_league_table++, 0, $temp, $f_header2);
+  &set_worksheet_line($ws_top50, $row_league_table++, 0, ["Messages", "Bytes", "Average", $text], $f_headertab);
 }
 
 @chartdatanames = ();
@@ -1089,24 +1331,40 @@ else {
 $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) {
+
+  $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
+  $data = $gigs = 0;
+  un_round($rounded_volume,\$data,\$gigs);
+  $count = $$m_count{$key};
+  $rounded_average = volume_rounded($data/$count,$gigs/$count);
+  my @content=($count, $rounded_volume, $rounded_average );
+
+  # write content
+  # any reasons for not including rounded_average in the txt-version?? -fh
+  printf $txt_fh ($txt_format, $count, $rounded_volume, $key) if $txt_fh;
+  if ($htm_fh) {
     $htmlkey = $key;
     $htmlkey =~ s/>/\&gt\;/g;
     $htmlkey =~ s/</\&lt\;/g;
-    printf($format, $$m_count{$key}, volume_rounded($$m_data{$key},$$m_data_gigs{$key}), $htmlkey);
+    printf $htm_fh ($htm_format, @content, $htmlkey);
   }
-  else {
-    printf($format, $$m_count{$key}, volume_rounded($$m_data{$key},$$m_data_gigs{$key}), $key);
+  if ($xls_fh)
+  {
+    &set_worksheet_line($ws_top50, $row_league_table++, 0, [@content, $key], $f_default);
   }
 
+
   if (scalar @chartdatanames < $ntopchart) {
     if ($use_gig) {
       if ($$m_data_gigs{$key}) {
-       push(@chartdatanames, $key);
-       push(@chartdatavals, $$m_data_gigs{$key});
+        push(@chartdatanames, $key);
+        push(@chartdatavals, $$m_data_gigs{$key});
       }
     }
     else {
@@ -1121,9 +1379,10 @@ foreach $key (top_n_sort($topcount,$m_data_gigs,$m_data,$m_count)) {
 push(@chartdatanames, "Other");
 push(@chartdatavals, $chartotherval);
 
-if ($html) {
-  print "</table>\n";
-  print "</td><td>\n";
+print $txt_fh "\n" if $txt_fh;
+if ($htm_fh) {
+  print $htm_fh "</table>\n";
+  print $htm_fh "</td><td>\n";
   if ($HAVE_GD_Graph_pie && $charts) {
     # calculate the graph
     my @data = (
@@ -1138,41 +1397,43 @@ if ($html) {
     );
     my $gd = $graph->plot(\@data) or warn($graph->error);
     if ($gd) {
-      my $temp = $text;
+      $temp = $text;
       $temp =~ s/ /_/g;
-      open(IMG, ">$chartdir/${temp}_volume.png") or die $!;
+      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 "<img src=\"$chartrel/${temp}_volume.png\">";
+      print $htm_fh "<img src=\"$chartrel/${temp}_volume.png\">";
     }
   }
-  print "</td><td>\n";
-  print "</td></tr></table>\n";
+  print $htm_fh "</td><td>\n";
+  print $htm_fh "</td></tr></table>\n\n";
 }
-
-print "\n";
+if ($xls_fh)
+{
+  ++$row_league_table;
 }
 
+}
 
 #######################################################################
 # 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
 # tiebreakers. They all must have the same keys.
-# 
+#
 # The idea behind this routine is that when you only want to see the
 # top n members of a set, rather than sorting the entire set and then
 # plucking off the top n, sort through the stack as you go, discarding
 # any member which is lower than your current n'th highest member.
-# 
+#
 # This proves to be an order of magnitude faster for large hashes.
 # On 200,000 lines of mainlog it benchmarked 9 times faster.
 # On 700,000 lines of mainlog it benchmarked 13.8 times faster.
-# 
+#
 # We assume the values are > 0.
 #######################################################################
 sub top_n_sort {
@@ -1198,18 +1459,18 @@ sub top_n_sort {
   my $n_minus_1 = $n - 1;
   my $n_minus_2 = $n - 2;
 
-  # Pick out the top $n keys. 
+  # Pick out the top $n keys.
   my($key,$value1,$value2,$value3,$i,$comparison,$insert_position);
   while (($key,$value1) = each %$href1) {
 
     #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.
-    $comparison = $value1        <=> $minimum_value1 || 
-                 $href2->{$key} <=> $minimum_value2 ||
-                 $href3->{$key} <=> $minimum_value3 ||
-                 $top_n_key cmp $key;
+    $comparison = $value1        <=> $minimum_value1 ||
+                  $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.
@@ -1233,14 +1494,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;
       }
     }
 
@@ -1265,9 +1526,9 @@ sub top_n_sort {
 
 #######################################################################
 # html_header();
-# 
+#
 #  $header = html_header($title);
-# 
+#
 # Print our HTML header and start the <body> block.
 #######################################################################
 sub html_header {
@@ -1289,9 +1550,9 @@ EoText
 
 #######################################################################
 # help();
-# 
+#
 #  help();
-# 
+#
 # Display usage instructions and exit.
 #######################################################################
 sub help {
@@ -1299,51 +1560,63 @@ 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.
+-nvr            don't do volume rounding. Display in bytes, not KB/MB/GB.
 -q<list>        list of times for queuing information
                 single 0 item suppresses
 -t<number>      display top <number> sources/destinations
                 default is 50, 0 suppresses top listing
 -tnl            omit local sources/destinations in top listing
 -t_remote_users show top user sources/destinations from non-local domains
+-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.
+-d              Debug mode - dump the eval'ed parser onto STDERR.
 
 EoText
 
@@ -1354,15 +1627,15 @@ EoText
 
 #######################################################################
 # generate_parser();
-# 
+#
 #  $parser = generate_parser();
-# 
+#
 # This subroutine generates the parsing routine which will be
 # used to parse the mainlog. We take the base operation, and remove bits not in use.
 # This improves performance depending on what bits you take out or add.
-# 
+#
 # I've tested using study(), but this does not improve performance.
-# 
+#
 # We store our parsing routing in a variable, and process it looking for #IFDEF (Expression)
 # or #IFNDEF (Expression) statements and corresponding #ENDIF (Expression) statements. If
 # the expression evaluates to true, then it is included/excluded accordingly.
@@ -1417,8 +1690,8 @@ sub generate_parser {
     #ENDIF ($transport_pattern)
 
 
-    $host = "local";           #Host is local unless otherwise specified.
-    $domain = "localdomain";   #Domain is localdomain unless otherwise specified.
+    $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.
@@ -1436,21 +1709,39 @@ sub generate_parser {
 
       #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.
+        # 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) {
@@ -1467,67 +1758,67 @@ sub generate_parser {
 
       #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+)/;
+        $from_host{$id} = "$host$ip";
+        $from_address{$id} = $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++;
       add_volume(\\$total_received_data,\\$total_received_data_gigs,$thissize);
 
       #IFDEF ($#queue_times >= 0)
-       $arrival_time{$id} = $tod;
+        $arrival_time{$id} = $tod;
       #ENDIF ($#queue_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)
     }
 
@@ -1547,10 +1838,10 @@ sub generate_parser {
         if (defined $from_host{$id}) {
           if (/^(\\S+)(?:\\s+\\([^)]\\))?\\s+<([^>]+)>/) {
             ($old,$new) = ($1,$2);
-         }
+          }
           else {
-           $old = $new = "";
-         }
+            $old = $new = "";
+          }
 
           if ("\\L$new" eq "\\L$old") {
             ($old) = /^(\\S+)/ if $old eq "";
@@ -1559,9 +1850,9 @@ sub generate_parser {
             if (!defined $relay_pattern || $key !~ /$relay_pattern/o) {
               $relayed{$key} = 0 if !defined $relayed{$key};
               $relayed{$key}++;
-           }
+            }
             else {
-             $relayed_unshown++
+              $relayed_unshown++
             }
           }
         }
@@ -1570,46 +1861,51 @@ 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_count_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_count{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_count{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_count{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_count{Edomain}{$edomain}++;
+        add_volume(\\$delivered_data{Edomain}{$edomain},\\$delivered_data_gigs{Edomain}{$edomain},$size);
       #ENDIF ($do_sender{Edomain})
 
       $total_delivered_count++;
@@ -1649,10 +1945,10 @@ sub generate_parser {
         my($queued);
         if (defined $arrival_time{$id}) {
           $queued = seconds($tod) - seconds($arrival_time{$id});
-         delete($arrival_time{$id});
+          delete($arrival_time{$id});
         }
         else {
-         $queued = seconds($tod) - id_seconds($id);
+          $queued = seconds($tod) - id_seconds($id);
         }
 
         for ($i = 0; $i <= $#queue_times; $i++) {
@@ -1660,8 +1956,8 @@ sub generate_parser {
             $queue_bin[$i]++;
             $remote_queue_bin[$i]++ if $remote_delivered{$id};
             last;
-         }
-       }
+          }
+        }
         $queue_more_than++ if $i > $#queue_times;
       #ENDIF ($#queue_times >= 0)
 
@@ -1678,7 +1974,7 @@ 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;
     }
@@ -1688,7 +1984,7 @@ sub generate_parser {
     if (/^\s*#\s*ENDIF\s*\((.*?)\)/i) {
       delete $defines_in_operation{$1};
       unless (keys %defines_in_operation) {
-       $removing_lines = 0;
+        $removing_lines = 0;
       }
     }
   }
@@ -1701,9 +1997,9 @@ sub generate_parser {
 
 #######################################################################
 # parse();
-# 
+#
 #  parse($parser,\*FILEHANDLE);
-# 
+#
 # This subroutine accepts a parser and a filehandle from main and parses each
 # line. We store the results into global variables.
 #######################################################################
@@ -1724,65 +2020,69 @@ sub parse {
 
 #######################################################################
 # print_header();
-# 
+#
 #  print_header();
-# 
+#
 # Print our headers and contents.
 #######################################################################
 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=\"#all messages time\">Time spent on the queue: all messages</a>\n";
+      print $htm_fh "<li><a href=\"#messages with at least one remote delivery time\">Time spent on the queue: messages with at least one remote delivery</a>\n";
     }
-    print "<li><a href=\"#Relayed messages\">Relayed messages</a>\n" if $show_relay;
+    print $htm_fh "<li><a href=\"#Relayed messages\">Relayed messages</a>\n" if $show_relay;
     if ($topcount) {
       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";
+        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";
+        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=\"#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;
   }
 }
 
 
 #######################################################################
 # print_grandtotals();
-# 
+#
 #  print_grandtotals();
-# 
+#
 # Print the grand totals.
 #######################################################################
 sub print_grandtotals {
@@ -1791,6 +2091,8 @@ sub print_grandtotals {
   # different numbers of columns.
   my($sender_txt_header,$sender_html_header,$sender_txt_format,$sender_html_format);
   my(@received_totals,@delivered_totals);
+  my($row_tablehead, $row_max);
+
   foreach ('Host','Domain','Email','Edomain') {
     next unless $do_sender{$_};
     if ($merge_reports) {
@@ -1807,31 +2109,53 @@ sub print_grandtotals {
     $sender_txt_format  .= " " x ($COLUMN_WIDTHS - 5) . "%6d";
   }
 
-  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 $sender_txt_format";
+  my $txt_format2 = "  %6d %4.1f%% %6d %4.1f%%",
+  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%s</td>$sender_html_format<td align=\"right\">%d</td>";
+  my $htm_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";
+    print $txt_fh "\n";
+    print $txt_fh "Grand total summary\n";
+    print $txt_fh "-------------------\n";
+    print $txt_fh "                                    $sender_spaces           At least one address\n";
+    print $txt_fh "  TOTAL               Volume    Messages $sender_txt_header      Delayed       Failed\n";
+  }
+  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>TOTAL</th><th>Volume</th><th>Messages</th>$sender_html_header<th colspan=2>At least one addr<br>Delayed</th><th colspan=2>At least one addr<br>Failed</th>\n";
+  }
+  if ($xls_fh)
+  {
+      $ws_global->write($row++, $col, "Grand total summary", $f_header2);
 
-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%%",
+      $row_tablehead = $row+1; # header-row of TOTALS table
+
+      &set_worksheet_line($ws_global, $row_tablehead, 0, ['Received', 'Delivered', 'TOTAL'], $f_headertab);
+
+      my @content= (
+        "Volume",
+        "Messages",
+        $sender_txt_header,
+        "At least one address Delayed (Total)",
+        "At least one address Delayed (Percent)",
+        "At least one address Failed (Total)",
+        "At least one address Failed (Percent)"
+      );
+
+      for (my $i=0; $i < scalar(@content); $i++)
+      {
+        $ws_global->write($row_tablehead+$i+1, 2, $content[$i], $f_default);
+        $row++;
+      }
+      $row_max = $row_tablehead+scalar(@content)+2; # continue from this row
   }
 
+
+
   my($volume,$failed_count);
   if ($merge_reports) {
     $volume = volume_rounded($report_totals{Received}{Volume}, $report_totals{Received}{'Volume-gigs'});
@@ -1846,13 +2170,32 @@ EoText
 
   {
     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)
+    {
+      $row = $row_tablehead+1;
+      for (my $i=0; $i < scalar(@content); $i++)
+      {
+        if ($i == 4 || $i == 6) {
+          $ws_global->write($row+$i, 0, $content[$i]/100, $f_percent);
+        }
+        else {
+          $ws_global->write($row+$i, 0, $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');
@@ -1860,86 +2203,119 @@ EoText
   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_count, @delivered_totals);
+  printf $txt_fh ("$txt_format1\n\n", 'Delivered', @content) if $txt_fh;
+  printf $htm_fh ("$htm_format1\n\n", 'Delivered', @content) if $htm_fh;
+  printf $htm_fh "</table>\n" if $htm_fh;
+  if ($xls_fh)
+  {
+
+      $row = $row_tablehead+1;
+      for (my $i=0; $i < scalar(@content); $i++)
+      {
+        $ws_global->write($row+$i, 1, $content[$i], $f_default);
+      }
+      $row = $row_max;
+  }
 }
 
 
 #######################################################################
 # print_user_patterns()
-# 
+#
 #  print_user_patterns();
-# 
+#
 # 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;
+      if ($xls_fh)
+      {
+        &set_worksheet_line($ws_global, $row++, 0, [$key,$user_pattern_totals[$user_pattern_index]]);
+      }
       $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;
   }
-  print "\n";
 }
 
 
 #######################################################################
 # print_transport();
-# 
+#
 #  print_transport();
-# 
+#
 # 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.
 
-  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>";
+  my $txt_format1 = "  %-18s  %6s      %6d";
+  my $htm_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%\">\n";
+    print $htm_fh "<tr><td>\n";
+    print $htm_fh "<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);
+    &set_worksheet_line($ws_global, $row++, 1, ["Volume", "Messages"], $f_headertab);
   }
 
   my($key);
@@ -1947,28 +2323,37 @@ 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;
+      if ($xls_fh) {
+        &set_worksheet_line($ws_global, $row++, 0, \@content, $f_default);
+      }
     }
   }
   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;
+      if ($xls_fh) {
+        &set_worksheet_line($ws_global, $row++, 0, \@content);
+      }
     }
   }
-  if ($html) {
-    print "</table>\n";
-    print "</td><td>\n";
+  print $txt_fh "\n" if $txt_fh;
+  if ($htm_fh) {
+    print $htm_fh "</table>\n";
+    print $htm_fh "</td><td>\n";
     if ($HAVE_GD_Graph_pie && $charts)
       {
       # calculate the graph
@@ -1984,14 +2369,14 @@ 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>\n";
 
     if ($HAVE_GD_Graph_pie && $charts) {
       my @data = (
@@ -2004,119 +2389,164 @@ 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_count.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";
+  }
+  if ($xls_fh) {
+    $row++;
   }
-  print "\n";
+
 }
 
 
 
 #######################################################################
 # print_relay();
-# 
+#
 #  print_relay();
-# 
+#
 # 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";
 }
 
 
 
 #######################################################################
 # print_errors();
-# 
+#
 #  print_errors();
-# 
+#
 # Print our errors. In HTML, we display them as a list rather than a table -
 # Netscape doesn't like large tables!
 #######################################################################
 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"]);
+    }
   }
 
 }
@@ -2124,36 +2554,36 @@ sub print_errors {
 
 #######################################################################
 # parse_old_eximstat_reports();
-# 
+#
 #  parse_old_eximstat_reports($fh);
-# 
+#
 # Parse old eximstat output so we can merge daily stats to weekly stats and weekly to monthly etc.
-# 
+#
 # To test that the merging still works after changes, do something like the following.
 # All the diffs should produce no output.
-# 
+#
 #  options='-bydomain -byemail -byhost -byedomain'
 #  options="$options -pattern 'Completed Messages' /Completed/"
 #  options="$options -pattern 'Received Messages' /<=/"
-# 
+#
 #  ./eximstats $options mainlog > mainlog.txt
 #  ./eximstats $options -merge mainlog.txt > mainlog.2.txt
 #  diff mainlog.txt mainlog.2.txt
-# 
+#
 #  ./eximstats $options -html mainlog > mainlog.html
 #  ./eximstats $options -merge -html mainlog.txt  > mainlog.2.html
 #  diff mainlog.html mainlog.2.html
-# 
+#
 #  ./eximstats $options -merge mainlog.html > mainlog.3.txt
 #  diff mainlog.txt mainlog.3.txt
-# 
+#
 #  ./eximstats $options -merge -html mainlog.html > mainlog.3.html
 #  diff mainlog.html mainlog.3.html
-# 
+#
 #  ./eximstats $options -nvr   mainlog > mainlog.nvr.txt
 #  ./eximstats $options -merge mainlog.nvr.txt > mainlog.4.txt
 #  diff mainlog.txt mainlog.4.txt
-# 
+#
 #  # double_mainlog.txt should have twice the values that mainlog.txt has.
 #  ./eximstats $options mainlog mainlog > double_mainlog.txt
 #######################################################################
@@ -2163,6 +2593,7 @@ sub parse_old_eximstat_reports {
   my(%league_table_value_entered, %league_table_value_was_zero, %table_order);
 
   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);
@@ -2171,22 +2602,22 @@ sub parse_old_eximstat_reports {
       # Fill in $report_totals{Received|Delivered}{Volume|Messages|Hosts|Domains|...|Delayed|DelayedPercent|Failed|FailedPercent}
       my(@fields);
       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.
+        $_ = 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);
+        if (/TOTAL\s+(.*?)\s*$/) {
+          @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(@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.
       }
     }
 
@@ -2196,15 +2627,15 @@ 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.
       }
     }
 
@@ -2216,15 +2647,15 @@ 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/) {
@@ -2239,18 +2670,18 @@ sub parse_old_eximstat_reports {
       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;
-       }
+        $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;
+        }
       }
     }
 
@@ -2269,31 +2700,31 @@ sub parse_old_eximstat_reports {
       my $bin_aref = ($1 eq 'all messages') ? \@queue_bin : \@remote_queue_bin;
       my $reached_table = 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;
-           }
-         }
+        $_ = 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 ?
-       }
+        }
+        else {
+          last;                             #Finished the table ?
+        }
       }
     }
 
@@ -2309,23 +2740,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 ?
+        }
       }
     }
 
@@ -2334,6 +2765,9 @@ sub parse_old_eximstat_reports {
 #-------------------------------------
 #
 #     48     1468KB   local
+# Could also have average values for HTML output.
+#     48     1468KB   30KB  local
+
       my($category,$by_count_or_volume) = ($1,$2);
 
       #As we show 2 views of each table (by count and by volume),
@@ -2341,9 +2775,9 @@ 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,
@@ -2351,87 +2785,96 @@ sub parse_old_eximstat_reports {
       my($count_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;
+        $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"}};
+        $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;
+        $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"}};
+        $data_href       = \%{$delivered_data{"\u$1"}};
+        $data_gigs_href  = \%{$delivered_data_gigs{"\u$1"}};
       }
 
       my $reached_table = 0;
       while (<$fh>) {
-       $_ = html2txt($_);              #Convert general HTML markup to text.
-       $reached_table = 1 if (/^\s*\d/);
-       next unless $reached_table;
-       if (/^\s*(\d+)\s+(\S+)\s*(.*?)\s*$/) {
-         my($count,$rounded_volume,$entry) = ($1,$2,$3);
+        # Watch out for empty tables.
+        goto PARSE_OLD_REPORT_LINE if (/<h2>/ or /^[a-zA-Z]/);
+
+        $_ = 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/);
+          #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;
+            $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;
-       }
+            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;
+        }
       }
     }
     elsif (/List of errors/) {
@@ -2448,31 +2891,31 @@ 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;
+        }
       }
     }
 
@@ -2483,9 +2926,9 @@ sub parse_old_eximstat_reports {
 
 #######################################################################
 # update_relayed();
-# 
+#
 #  update_relayed($count,$sender,$recipient);
-# 
+#
 # Adds an entry into the %relayed hash. Currently only used when
 # merging reports.
 #######################################################################
@@ -2508,19 +2951,19 @@ sub update_relayed {
 
 #######################################################################
 # add_to_totals();
-# 
+#
 #  add_to_totals(\%totals,\@keys,$values);
-# 
+#
 # Given a line of space seperated values, add them into the provided hash using @keys
 # as the hash keys.
-# 
+#
 # If the value contains a '%', then the value is set rather than added. Otherwise, we
 # convert the value to bytes and gigs. The gigs get added to I<Key>-gigs.
 #######################################################################
 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(@keys) = @$keys_aref;        #Make a copy as we destroy the one we use.
   my($value);
   foreach $value (@values) {
     my $key = shift(@keys) or next;
@@ -2538,11 +2981,11 @@ sub add_to_totals {
 
 #######################################################################
 # get_report_total();
-# 
+#
 #  $total = get_report_total(\%hash,$key);
-# 
+#
 # If %hash contains values split into Units and Gigs, we calculate and return
-# 
+#
 #   $hash{$key} + 1024*1024*1024 * $hash{"${key}-gigs"}
 #######################################################################
 sub get_report_total {
@@ -2556,9 +2999,9 @@ sub get_report_total {
 
 #######################################################################
 # html2txt();
-# 
+#
 #  $text_line = html2txt($html_line);
-# 
+#
 # Convert a line from html to text. Currently we just convert HTML tags to spaces
 # and convert &gt;, &lt;, and &nbsp; tags back.
 #######################################################################
@@ -2579,16 +3022,16 @@ sub html2txt {
 
 #######################################################################
 # get_next_arg();
-# 
+#
 #  $arg = get_next_arg();
-# 
+#
 # Because eximstats arguments are often passed as variables,
 # we can't rely on shell parsing to deal with quotes. This
 # subroutine returns $ARGV[1] and does a shift. If $ARGV[1]
 # starts with a quote (' or "), and doesn't end in one, then
 # we append the next argument to it and shift again. We repeat
 # until we've got all of the argument.
-# 
+#
 # This isn't perfect as all white space gets reduced to one space,
 # but it's as good as we can get! If it's esential that spacing
 # be preserved precisely, then you get that by not using shell
@@ -2613,7 +3056,21 @@ 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 );
+  }
 
+}
 
 ##################################################
 #                 Main Program                   #
@@ -2628,6 +3085,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
@@ -2643,6 +3101,13 @@ $chartdir = ".";
 $last_offset = '';
 $offset_seconds = 0;
 
+$row=1;
+$row_league_table=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 '-')
@@ -2668,7 +3133,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;
@@ -2677,7 +3151,8 @@ 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 }
@@ -2705,12 +3180,74 @@ 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 ($topcount) {
+    $ws_top50 = $workbook->addworksheet('Deliveries');
+    }
+    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);
+
+    $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);
+
+  }
+
 
 for (my $i = 0; $i <= $#queue_times; $i++) {
   $queue_bin[$i] = 0;
@@ -2723,11 +3260,11 @@ 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;
   }
@@ -2764,20 +3301,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.
@@ -2792,7 +3329,7 @@ else {
 
 
 if ($begin eq "9999-99-99 99:99:99") {
-  print "**** No valid log lines read\n";
+  print STDERR "**** No valid log lines read\n";
   exit 1;
 }
 
@@ -2842,8 +3379,21 @@ if ($topcount > 0) {
 # 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;
+$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
+
+# FIXME: Doku