[exim-cvs] cvs commit: exim/exim-src/src eximstats.src

Startseite
Nachricht löschen
Nachricht beantworten
Autor: Steve Campbell
Datum:  
To: exim-cvs
Betreff: [exim-cvs] cvs commit: exim/exim-src/src eximstats.src
steve 2005/06/03 15:28:50 BST

  Modified files:
    exim-src/src         eximstats.src 
  Log:
  Added -xls and the ability to specify output files (Frank Heydlauf).
  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.
  Added the -include_original_destination flag
  Removed tabs and trailing whitespace.


  Revision  Changes     Path
  1.7       +1103 -583  exim/exim-src/src/eximstats.src


  Index: eximstats.src
  ===================================================================
  RCS file: /home/cvs/exim/exim-src/src/eximstats.src,v
  retrieving revision 1.6
  retrieving revision 1.7
  diff -u -r1.6 -r1.7
  --- eximstats.src    17 Feb 2005 11:58:26 -0000    1.6
  +++ eximstats.src    3 Jun 2005 14:28:50 -0000    1.7
  @@ -1,5 +1,5 @@
   #!PERL_COMMAND -w
  -# $Cambridge: exim/exim-src/src/eximstats.src,v 1.6 2005/02/17 11:58:26 ph10 Exp $
  +# $Cambridge: exim/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.
  @@ -192,11 +192,28 @@
   #             Eximstats can now parse syslog lines as well as mainlog lines.
   #
   # 2004-12-20  V1.35 Wouter Verhelst
  -#          Pie charts by volume were actually generated by count. Fixed.
  +#             Pie charts by volume were actually generated by count. Fixed.
   #
   # 2005-02-07  V1.36 Gregor Herrmann / Steve Campbell
   #             Added average sizes to HTML Top tables.
  -
  +#
  +# 2005-04-26  V1.37 Frank Heydlauf
  +#             Added -xls and the ability to specify output files.
  +#
  +# 2005-04-29  V1.38 Steve Campbell
  +#             Use FileHandles for outputing results.
  +#             Allow any combination of xls, txt, and html output.
  +#             Fixed display of large numbers with -nvr option
  +#             Fixed merging of reports with empty tables.
  +#
  +# 2005-05-27  V1.39 Steve Campbell
  +#             Added the -include_original_destination flag
  +#             Removed tabs and trailing whitespace.
  +#
  +#
  +#
  +# For documentation on the logfile format, see
  +# http://www.exim.org/exim-html-4.50/doc/html/spec_48.html#IX2793


=head1 NAME

@@ -204,11 +221,42 @@

=head1 SYNOPSIS

  - eximstats [Options] mainlog1 mainlog2 ... > report.txt
  - eximstats -html [Options] mainlog1 mainlog2 ... > report.html
  + eximstats [Output] [Options] mainlog1 mainlog2 ...
    eximstats -merge [Options] report.1.txt report.2.txt ... > weekly_report.txt


-Options:
+=head2 Output:
+
+=over 4
+
+=item B<-txt>
+
+Output the results in plain text to STDOUT.
+
+=item B<-txt>=I<filename>
+
+Output the results in plain text. Filename '-' for STDOUT is accepted.
+
+=item B<-html>
+
+Output the results in HTML to STDOUT.
+
+=item B<-html>=I<filename>
+
+Output the results in HTML. Filename '-' for STDOUT is accepted.
+
+=item B<-xls>
+
+Output the results in Excel compatible Format to STDOUT.
+Requires the Spreadsheet::WriteExcel CPAN module.
+
+=item B<-xls>=I<filename>
+
+Output the results in Excel compatible format. Filename '-' for STDOUT is accepted.
+
+
+=back
+
+=head2 Options:

=over 4

@@ -256,6 +304,12 @@

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
@@ -322,13 +376,10 @@

=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
@@ -371,8 +422,8 @@

Eximstats parses exim mainlog and syslog files to output a statistical
analysis of the messages processed. By default, a text
-analysis is generated, but you can request an html analysis
-by using the B<-html> flag. See the help (B<-help>) to learn
+analysis is generated, but you can request other output formats
+using flags. See the help (B<-help>) to learn
about how to create charts from the tables.

=head1 AUTHOR
@@ -387,19 +438,24 @@
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;


##################################################
@@ -422,7 +478,7 @@

   @days_per_month = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
   $gig     = 1024 * 1024 * 1024;
  -$VERSION = '1.36';
  +$VERSION = '1.39';


   # How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
   $COLUMN_WIDTHS = 8;
  @@ -431,15 +487,22 @@
   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;

@@ -448,11 +511,13 @@
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.
  @@ -475,6 +540,36 @@
   #                   Subroutines                  #
   ##################################################


  +#######################################################################
  +# get_filehandle($file,\%output_files);
  +# Return a filehandle writing to $file.
  +#
  +# If %output_files is defined, check that $output_files{$file}
  +# doesn't exist and die if it does, or set it if it doesn't.
  +#######################################################################
  +sub get_filehandle {
  +  my($file,$output_files_href) = @_;
  +
  +  $file = '-' if ($file eq '');
  +
  +  if (defined $output_files_href) {
  +    die "You can only output to '$file' once! Use -h for help.\n" if exists $output_files_href->{$file};
  +    $output_files_href->{$file} = 1;
  +  }
  +
  +  if ($file eq '-') {
  +    return \*STDOUT;
  +  }
  +
  +  if (-e $file) {
  +    unlink $file or die "Failed to rm $file: $!";
  +  }
  +
  +  my $fh = new IO::File $file, O_WRONLY|O_CREAT|O_EXCL;
  +  die "new IO::File $file failed: $!" unless (defined $fh);
  +  return $fh;
  +}
  +


   #######################################################################
   # volume_rounded();
  @@ -525,7 +620,13 @@
     }
     else {
       # We don't want any rounding to be done.
  -    $rounded = sprintf("%4d", ($g * $gig) + $x);
  +    # and we don't need broken formated output which on one hand avoids numbers from
  +    # being interpreted as string by Spreadsheed Calculators, on the other hand
  +    # breaks if more than 4 digits! -> flexible length instead of fixed length
  +    # Format the return value at the output routine! -fh
  +    #$rounded = sprintf("%d", ($g * $gig) + $x);
  +    no integer;
  +    $rounded = sprintf("%.0f", ($g * $gig) + $x);
     }


     return $rounded;
  @@ -561,7 +662,9 @@
       $$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;
     }


@@ -789,33 +892,62 @@

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]);
  @@ -826,18 +958,27 @@
   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 = (
  @@ -854,16 +995,23 @@
       );
       my $gd = $graph->plot(\@data) or warn($graph->error);
       if ($gd) {
  -      open(IMG, ">$chartdir/$pngname") or die $!;
  +      open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
         binmode IMG;
         print IMG $gd->png;
         close IMG;
  -      print "<img src=\"$chartrel/$pngname\">";
  +      print $htm_fh "<img src=\"$chartrel/$pngname\">";
       }
     }
  -  print "</td></tr></table>\n";
  +  print $htm_fh "</td></tr></table>\n";
   }
  -print "\n";
  +
  +if ($xls_fh)
  +{
  +  $row++;
  +}
  +print $txt_fh "\n" if $txt_fh;
  +print $htm_fh "\n" if $htm_fh;
  +
   }



  @@ -882,6 +1030,16 @@
   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; }


@@ -890,31 +1048,40 @@

   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($title) = sprintf("$text per %s (each dot is $scale $type)",
- ($hist_interval == 60)? "hour" :
- ($hist_interval == 1)? "minute" : "$hist_interval minutes");
-
-if ($html) {
- print "<hr><a name=\"$text\"></a><h2>$title</h2>\n";
- print "<table border=0 width=\"100%\">\n";
- print "<tr><td><pre>\n";
+my $txt_htm_title = $title . " (each dot is $scale $type)";
+
+printf $txt_fh ("%s\n%s\n\n", $txt_htm_title, "-" x length($txt_htm_title)) if $txt_fh;
+
+if ($htm_fh) {
+ print $htm_fh "<hr><a name=\"$text\"></a><h2>$txt_htm_title</h2>\n";
+ print $htm_fh "<table border=0 width=\"100%\">\n";
+ print $htm_fh "<tr><td><pre>\n";
}
-else {
- printf("%s\n%s\n\n", $title, "-" x length($title));
+
+if ($xls_fh)
+{
+ $title =~ s/Messages/Msg/ ;
+ $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
  @@ -923,19 +1090,38 @@


     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)
  @@ -943,15 +1129,25 @@
         $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 = (
  @@ -970,15 +1166,20 @@
       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
   }



@@ -1000,63 +1201,80 @@
my(@chartdatavals) = ();
my $chartotherval = 0;

-my($format);
-if ($html) {
- print "<hr><a name=\"$text count\"></a><h2>$temp</h2>\n";
- print "<table border=0 width=\"100%\">\n";
- print "<tr><td>\n";
- print "<table border=1>\n";
- print "<tr><th>Messages</th><th>Bytes</th><th>Average</th><th>\u$text</th>\n";
+my $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=\"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 );
   }


  +
  +# 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;
  -
  -    # When displaying the average figures, we calculate the average of
  -    # the rounded data, as the user would calculate it. This reduces
  -    # the accuracy slightly, but we have to do it this way otherwise
  -    # when using -merge to convert results from text to HTML and
  -    # vice-versa discrepencies would occur.
  -    $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
  -    $data = $gigs = 0;
  -    un_round($rounded_volume,\$data,\$gigs);
  -    $count = $$m_count{$key};
  -    $rounded_average = volume_rounded($data/$count,$gigs/$count);
  -    printf($format, $count, $rounded_volume, $rounded_average, $htmlkey);
  +    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
  @@ -1074,28 +1292,38 @@
       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>Average</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 = ();
  @@ -1109,27 +1337,34 @@
     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;
  -
  -    $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
  -    $data = $gigs = 0;
  -    un_round($rounded_volume,\$data,\$gigs);
  -    $count = $$m_count{$key};
  -    $rounded_average = volume_rounded($data/$count,$gigs/$count);
  -    printf($format, $count, $rounded_volume, $rounded_average, $htmlkey);
  +    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 {
  @@ -1144,9 +1379,10 @@
   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 = (
  @@ -1161,22 +1397,24 @@
       );
       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();
  @@ -1230,9 +1468,9 @@
       # 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;
  +                  $href2->{$key} <=> $minimum_value2 ||
  +                  $href3->{$key} <=> $minimum_value3 ||
  +                  $top_n_key cmp $key;
       next unless ($comparison == 1);


       # As we will be using these values a few times, extract them into scalars.
  @@ -1256,14 +1494,14 @@
       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;
         }
       }


@@ -1322,51 +1560,63 @@

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

  @@ -1440,8 +1690,8 @@
       #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.
  @@ -1459,21 +1709,39 @@


         #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@??? <c@???>" and "e@???"
  +      #$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) {
  @@ -1490,67 +1758,67 @@


         #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)
  -      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 ($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)


  -        $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)
       }


  @@ -1570,10 +1838,10 @@
           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 "";
  @@ -1582,9 +1850,9 @@
               if (!defined $relay_pattern || $key !~ /$relay_pattern/o) {
                 $relayed{$key} = 0 if !defined $relayed{$key};
                 $relayed{$key}++;
  -        }
  +            }
               else {
  -          $relayed_unshown++
  +              $relayed_unshown++
               }
             }
           }
  @@ -1593,46 +1861,51 @@
         }


         #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++;
  @@ -1672,10 +1945,10 @@
           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++) {
  @@ -1683,8 +1956,8 @@
               $queue_bin[$i]++;
               $remote_queue_bin[$i]++ if $remote_delivered{$id};
               last;
  -      }
  -    }
  +          }
  +        }
           $queue_more_than++ if $i > $#queue_times;
         #ENDIF ($#queue_times >= 0)


  @@ -1701,7 +1974,7 @@
     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;
       }
  @@ -1711,7 +1984,7 @@
       if (/^\s*#\s*ENDIF\s*\((.*?)\)/i) {
         delete $defines_in_operation{$1};
         unless (keys %defines_in_operation) {
  -    $removing_lines = 0;
  +        $removing_lines = 0;
         }
       }
     }
  @@ -1754,49 +2027,53 @@
   #######################################################################
   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;
     }
   }


  @@ -1814,6 +2091,8 @@
     # 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) {
  @@ -1830,31 +2109,53 @@
       $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'});
  @@ -1869,13 +2170,32 @@


     {
       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');
  @@ -1883,8 +2203,21 @@
     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;
  +  }
   }



@@ -1896,43 +2229,59 @@
# 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>";
  -  }
  -  else {
  -    print "User Specified Patterns\n";
  -    print "-----------------------";
  -    print "\n                       Total\n";
  -    $format1 = "  %-18s  %6d";
  +  if ($txt_fh) {
  +    print $txt_fh "User Specified Patterns\n";
  +    print $txt_fh "-----------------------";
  +    print $txt_fh "\n                       Total\n";
  +  }
  +  if ($htm_fh) {
  +    print $htm_fh "<hr><a name=\"patterns\"></a><h2>User Specified Patterns</h2>\n";
  +    print $htm_fh "<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";
   }



  @@ -1944,25 +2293,29 @@
   # 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>";
  -  }
  -  else {
  -    print "Deliveries by transport\n";
  -    print "-----------------------";
  -    print "\n                      Volume    Messages\n";
  -    $format1 = "  %-18s  %6s      %6d";
  +  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";
  +  }
  +  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);
  @@ -1970,28 +2323,37 @@
       # 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
  @@ -2007,14 +2369,14 @@
         );
         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 = (
  @@ -2027,16 +2389,19 @@
         );
         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";
  +
   }



  @@ -2049,40 +2414,60 @@
   # 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";
  -    }
  -    else {
  -      printf("%s\n%s\n\n", $temp, "-" x length($temp));
  -      $format = "%7d %s\n      => %s\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";
  +    }
  +    if ($xls_fh) {
  +      $ws_relayed->write($row_print_relay++, $col, $temp, $f_header2);
  +      &set_worksheet_line($ws_relayed, $row_print_relay++, 0, ["Count", "From", "To"], $f_headertab);
       }


  +
       my($key);
       foreach $key (sort keys %relayed) {
         my $count = $relayed{$key};
         $shown += $count;
         $key =~ s/[HA]=//g;
         my($one,$two) = split(/=> /, $key);
  -      printf($format, $count, $one, $two);
  +      my @content=($count, $one, $two);
  +      printf $txt_fh ($txt_format, @content) if $txt_fh;
  +      printf $htm_fh ($htm_format, @content) if $htm_fh;
  +      if ($xls_fh)
  +      {
  +        &set_worksheet_line($ws_relayed, $row_print_relay++, 0, \@content);
  +      }
         $spacing = "\n";
       }
  -    print "</table>\n<p>\n" if $html;
  -    print "${spacing}Total: $shown (plus $relayed_unshown unshown)\n";
  +
  +    print $htm_fh "</table>\n<p>\n" if $htm_fh;
  +    print $txt_fh "${spacing}Total: $shown (plus $relayed_unshown unshown)\n\n" if $txt_fh;
  +    print $htm_fh "${spacing}Total: $shown (plus $relayed_unshown unshown)\n\n" if $htm_fh;
  +    if ($xls_fh)
  +    {
  +       &set_worksheet_line($ws_relayed, $row_print_relay++, 0, [$shown, "Sum of shown" ]);
  +       &set_worksheet_line($ws_relayed, $row_print_relay++, 0, [$relayed_unshown, "unshown"]);
  +       $row_print_relay++;
  +    }
     }
     else {
  -    print "No relayed messages\n";
  -    print "-------------------\n" unless $html;
  +    print $txt_fh "No relayed messages\n-------------------\n\n" if $txt_fh;
  +    print $htm_fh "No relayed messages\n\n" if $htm_fh;
  +    if ($xls_fh)
  +    {
  +      $row_print_relay++;
  +    }
     }
  -  print "\n";
   }



  @@ -2097,49 +2482,71 @@
   #######################################################################
   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"]);
  +    }
     }


   }
  @@ -2186,6 +2593,7 @@
     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);
  @@ -2194,22 +2602,22 @@
         # 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.
         }
       }


  @@ -2219,15 +2627,15 @@
   #                       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.
         }
       }


  @@ -2239,15 +2647,15 @@
   #  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/) {
  @@ -2262,18 +2670,18 @@
         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;
  +        }
         }
       }


  @@ -2292,31 +2700,31 @@
         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 ?
  +        }
         }
       }


  @@ -2332,23 +2740,23 @@
         my $reached_table = 0;
         my($count,$sender);
         while (<$fh>) {
  -    unless ($reached_table) {
  -      last if (/No relayed messages/);
  -      $reached_table = 1 if (/^\s*\d/ || />\d+</);
  -      next unless $reached_table;
  -    }
  -    if (/>(\d+)<.td><td>(.*?) ?<.td><td>(.*?)</) {
  -      update_relayed($1,$2,$3);
  -    }
  -    elsif (/^\s*(\d+)\s+(.*?)\s*$/) {
  -      ($count,$sender) = ($1,$2);
  -    }
  -    elsif (/=>\s+(.*?)\s*$/) {
  -      update_relayed($count,$sender,$1);
  -    }
  -    else {
  -      last;                    #Finished the table ?
  -    }
  +        unless ($reached_table) {
  +          last if (/No relayed messages/);
  +          $reached_table = 1 if (/^\s*\d/ || />\d+</);
  +          next unless $reached_table;
  +        }
  +        if (/>(\d+)<.td><td>(.*?) ?<.td><td>(.*?)</) {
  +          update_relayed($1,$2,$3);
  +        }
  +        elsif (/^\s*(\d+)\s+(.*?)\s*$/) {
  +          ($count,$sender) = ($1,$2);
  +        }
  +        elsif (/=>\s+(.*?)\s*$/) {
  +          update_relayed($count,$sender,$1);
  +        }
  +        else {
  +          last;                           #Finished the table ?
  +        }
         }
       }


  @@ -2367,9 +2775,9 @@
         #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,
  @@ -2377,91 +2785,96 @@
         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;
  +        # 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 /;
  +        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);
  +        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;
  -        }
  +            $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;
  +            $$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--;
  -          }
  -        }
  -      }
  +            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;
  -    }
  +          last;
  +        }
         }
       }
       elsif (/List of errors/) {
  @@ -2478,31 +2891,31 @@
         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;
  +        }
         }
       }


  @@ -2550,7 +2963,7 @@
   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;
  @@ -2643,7 +3056,21 @@
     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                   #
  @@ -2658,6 +3085,7 @@
   $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
  @@ -2673,6 +3101,13 @@
   $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 '-')
  @@ -2698,7 +3133,16 @@
       }
     elsif ($ARGV[0] =~ /^-t(\d+)$/)   { $topcount = $1 }
     elsif ($ARGV[0] =~ /^-tnl$/)      { $local_league_table = 0 }
  -  elsif ($ARGV[0] =~ /^-html$/)     { $html = 1 }
  +  elsif ($ARGV[0] =~ /^-txt=?(\S*)$/)  { $txt_fh = get_filehandle($1,\%output_files) }
  +  elsif ($ARGV[0] =~ /^-html=?(\S*)$/) { $htm_fh = get_filehandle($1,\%output_files) }
  +  elsif ($ARGV[0] =~ /^-xls=?(\S*)$/) {
  +    if ($HAVE_Spreadsheet_WriteExcel) {
  +      $xls_fh = get_filehandle($1,\%output_files);
  +    }
  +    else {
  +      warn "WARNING: CPAN Module Spreadsheet::WriteExcel not installed. Obtain from www.cpan.org\n";
  +    }
  +  }
     elsif ($ARGV[0] =~ /^-merge$/)    { $merge_reports = 1 }
     elsif ($ARGV[0] =~ /^-charts$/)   {
       $charts = 1;
  @@ -2707,7 +3151,8 @@
     }
     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 }
  @@ -2735,12 +3180,74 @@
     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;
  @@ -2753,11 +3260,11 @@
     {
     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;
     }
  @@ -2794,20 +3301,20 @@
     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.
  @@ -2822,7 +3329,7 @@



   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;
   }


@@ -2872,8 +3379,21 @@
# 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