2 # $Cambridge: exim/src/src/eximstats.src,v 1.8 2005/06/29 15:35:09 steve Exp $
4 # Copyright (c) 2001 University of Cambridge.
5 # See the file NOTICE for conditions of use and distribution.
7 # Perl script to generate statistics from one or more Exim log files.
9 # Usage: eximstats [<options>] <log file> <log file> ...
11 # 1996-05-21: Ignore lines not starting with valid date/time, just in case
12 # these get into a log file.
13 # 1996-11-19: Add the -h option to control the size of the histogram,
14 # and optionally turn it off.
15 # Use some Perl 5 things; it should be everywhere by now.
16 # Add the Perl -w option and rewrite so no warnings are given.
17 # Add the -t option to control the length of the "top" listing.
18 # Add the -ne, -nt options to turn off errors and transport
20 # Add information about length of time on queue, and -q<list> to
21 # control the intervals and turn it off.
22 # Add count and percentage of delayed messages to the Received
24 # Show total number of errors.
25 # Add count and percentage of messages with errors to Received
27 # Add information about relaying and -nr to suppress it.
28 # 1997-02-03 Merged in some of the things Nigel Metheringham had done:
30 # Added received histogram as well as delivered
31 # Added local senders' league table
32 # Added local recipients' league table
33 # 1997-03-10 Fixed typo "destinationss"
34 # Allow for intermediate address between final and original
35 # when testing for relaying
36 # Give better message when no input
37 # 1997-04-24 Fixed bug in layout of error listing that was depending on
38 # text length (output line got repeated).
39 # 1997-05-06 Bug in option decoding when only one option.
40 # Overflow bug when handling very large volumes.
41 # 1997-10-28 Updated to handle revised log format that might show
42 # HELO name as well as host name before IP number
43 # 1998-01-26 Bugs in the function for calculating the number of seconds
44 # since 1970 from a log date
45 # 1998-02-02 Delivery to :blackhole: doesn't have a T= entry in the log
46 # line; cope with this, thereby avoiding undefined problems
47 # Very short log line gave substring error
48 # 1998-02-03 A routed delivery to a local transport may not have <> in the
49 # log line; terminate the address at white space, not <
50 # 1998-09-07 If first line of input was a => line, $thissize was undefined;
52 # 1998-12-21 Adding of $thissize from => line should have been adding $size.
53 # Oops. Should have looked more closely when fixing the previous
55 # 1999-11-12 Increased the field widths for printed integers; numbers are
56 # bigger than originally envisaged.
57 # 2001-03-21 Converted seconds() routine to use Time::Local, fixing a bug
58 # whereby seconds($timestamp) - id_seconds($id) gave an
60 # Added POD documentation.
61 # Moved usage instructions into help() subroutine.
62 # Added 'use strict' and declared all global variables.
63 # Added '-html' flag and resultant code.
64 # Added '-cache' flag and resultant code.
65 # Added add_volume() routine and converted all volume variables
66 # to use it, fixing the overflow problems for individual hosts
68 # Converted all volume output to GB/MB/KB as appropriate.
69 # Don't store local user stats if -nfl is specified.
70 # Modifications done by: Steve Campbell (<steve@computurn.com>)
71 # 2001-04-02 Added the -t_remote_users flag. Steve Campbell.
72 # 2001-10-15 Added the -domain flag. Steve Campbell.
73 # 2001-10-16 Accept files on STDIN or on the command line. Steve Campbell.
74 # 2001-10-21 Removed -domain flag and added -bydomain, -byhost, and -byemail.
75 # We now generate our main parsing subroutine as an eval statement
76 # which improves performance dramatically when not all the results
77 # are required. We also cache the last timestamp to time convertion.
79 # NOTE: 'Top 50 destinations by (message count|volume)' lines are
80 # now 'Top N (host|email|domain) destinations by (message count|volume)'
81 # where N is the topcount. Steve Campbell.
83 # 2001-10-30 V1.16 Joachim Wieland.
84 # Fixed minor bugs in add_volume() when taking over this version
85 # for use in Exim 4: -w gave uninitialized value warnings in
86 # two situations: for the first addition to a counter, and if
87 # there were never any gigabytes, thereby leaving the $gigs
89 # Initialized $last_timestamp to stop a -w uninitialized warning.
90 # Minor layout tweak for grand totals (nitpicking).
91 # Put the IP addresses for relaying stats in [] and separated by
92 # a space from the domain name.
93 # Removed the IPv4-specific address test when picking out addresses
94 # for relaying. Anything inside [] is OK.
96 # 2002-07-02 Philip Hazel
97 # Fixed "uninitialized variable" message that occurred for relay
98 # messages that arrived from H=[1.2.3.4] hosts (no name shown).
99 # This bug didn't affect the output.
101 # 2002-04-15 V1.17 Joachim Wieland.
102 # Added -charts, -chartdir. -chartrel options which use
103 # GD::Graph modules to create graphical charts of the statistics.
105 # 2002-04-15 V1.18 Steve Campbell.
106 # Added a check for $domain to to stop a -w uninitialized warning.
107 # Added -byemaildomain option.
108 # Only print HTML header links to included tables!
110 # 2002-08-02 V1.19 Steve Campbell.
111 # Changed the debug mode to dump the parser onto STDERR rather
112 # than STDOUT. Documented the -d flag into the help().
113 # Rejoined the divergent 2002-04-15 and 2002-07-02 releases.
115 # 2002-08-21 V1.20 Steve Campbell.
116 # Added the '-merge' option to allow merging of previous reports.
117 # Fixed a missing semicolon when doing -bydomain.
118 # Make volume charts plot the data gigs and bytes rather than just bytes.
119 # Only process log lines with $flag =~ /<=|=>|->|==|\*\*|Co/
120 # Converted Emaildomain to Edomain - the column header was too wide!
121 # This changes the text output slightly. You can revert to the old
122 # column widths by changing $COLUMN_WIDTHS to 7;
124 # 2002-09-04 V1.21 Andreas J Mueller
125 # Local deliveries domain now defaults to 'localdomain'.
126 # Don't match F=<From> when looking for the user.
128 # 2002-09-05 V1.22 Steve Campbell
129 # Fixed a perl 5.005 incompatibility problem ('our' variables).
131 # 2002-09-11 V1.23 Steve Campbell
132 # Stopped -charts option from throwing errors on null data.
133 # Don't print out 'Errors encountered' unless there are any.
135 # 2002-10-21 V1.23a Philip Hazel - patch from Tony Finch put in until
136 # Steve's eximstats catches up.
137 # Handle log files that include the timezone after the timestamp.
138 # Switch to assuming that log timestamps are in local time, with
139 # an option for UTC timestamps, as in Exim itself.
141 # 2003-02-05 V1.24 Steve Campbell
142 # Added in Sergey Sholokh's code to convert '<' and '>' characters
143 # in HTML output. Also added code to convert them back with -merge.
144 # Fixed timestamp offsets to convert to seconds rather than minutes.
145 # Updated -merge to work with output files using timezones.
146 # Added cacheing to speed up the calculation of timezone offsets.
148 # 2003-02-07 V1.25 Steve Campbell
149 # Optimised the usage of mktime() in the seconds subroutine.
150 # Removed the now redundant '-cache' option.
151 # html2txt() now explicitly matches HTML tags.
152 # Implemented a new sorting algorithm - the top_n_sort() routine.
153 # Added Danny Carroll's '-nvr' flag and code.
155 # 2003-03-13 V1.26 Steve Campbell
156 # Implemented HTML compliance changes recommended by Bernard Massot.
157 # Bug fix to allow top_n_sort() to handle null keys.
158 # Convert all domains and edomains to lowercase.
159 # Remove preceding dots from domains.
161 # 2003-03-13 V1.27 Steve Campbell
162 # Replaced border attributes with 'border=1', as recommended by
165 # 2003-06-03 V1.28 John Newman
166 # Added in the ability to skip over the parsing and evaulation of
167 # specific transports as passed to eximstats via the new "-nt/.../"
168 # command line argument. This new switch allows the viewing of
169 # not more accurate statistics but more applicable statistics when
170 # special transports are in use (ie; SpamAssassin). We need to be
171 # able to ignore transports such as this otherwise the resulting
172 # local deliveries are significantly skewed (doubled)...
174 # 2003-11-06 V1.29 Steve Campbell
175 # Added the '-pattern "Description" "/pattern/"' option.
177 # 2004-02-17 V1.30 Steve Campbell
178 # Added warnings if required GD::Graph modules are not available or
179 # insufficient -chart* options are specified.
181 # 2004-02-20 V1.31 Andrea Balzi
182 # Only show the Local Sender/Destination links if the tables exist.
184 # 2004-07-05 V1.32 Steve Campbell
185 # Fix '-merge -h0' divide by zero error.
187 # 2004-07-15 V1.33 Steve Campbell
188 # Documentation update - I've converted the subroutine
189 # documentation from POD to comments.
191 # 2004-12-10 V1.34 Steve Campbell
192 # Eximstats can now parse syslog lines as well as mainlog lines.
194 # 2004-12-20 V1.35 Wouter Verhelst
195 # Pie charts by volume were actually generated by count. Fixed.
197 # 2005-02-07 V1.36 Gregor Herrmann / Steve Campbell
198 # Added average sizes to HTML Top tables.
200 # 2005-04-26 V1.37 Frank Heydlauf
201 # Added -xls and the ability to specify output files.
203 # 2005-04-29 V1.38 Steve Campbell
204 # Use FileHandles for outputing results.
205 # Allow any combination of xls, txt, and html output.
206 # Fixed display of large numbers with -nvr option
207 # Fixed merging of reports with empty tables.
209 # 2005-05-27 V1.39 Steve Campbell
210 # Added the -include_original_destination flag
211 # Removed tabs and trailing whitespace.
213 # 2005-06-03 V1.40 Steve Campbell
214 # Whilst parsing the mainlog(s), store information about
215 # the messages in a hash of arrays rather than using
216 # individual hashes. This is a bit cleaner and results in
217 # dramatic memory savings, albeit at a slight CPU cost.
219 # 2005-06-15 V1.41 Steve Campbell
220 # Added the -show_rt<list> flag.
221 # Added the -show_dt<list> flag.
223 # 2005-06-24 V1.42 Steve Campbell
224 # Added Histograms for user specified patterns.
227 # For documentation on the logfile format, see
228 # http://www.exim.org/exim-html-4.50/doc/html/spec_48.html#IX2793
232 eximstats - generates statistics from Exim mainlog or syslog files.
236 eximstats [Output] [Options] mainlog1 mainlog2 ...
237 eximstats -merge [Options] report.1.txt report.2.txt ... > weekly_report.txt
245 Output the results in plain text to STDOUT.
247 =item B<-txt>=I<filename>
249 Output the results in plain text. Filename '-' for STDOUT is accepted.
253 Output the results in HTML to STDOUT.
255 =item B<-html>=I<filename>
257 Output the results in HTML. Filename '-' for STDOUT is accepted.
261 Output the results in Excel compatible Format to STDOUT.
262 Requires the Spreadsheet::WriteExcel CPAN module.
264 =item B<-xls>=I<filename>
266 Output the results in Excel compatible format. Filename '-' for STDOUT is accepted.
277 histogram divisions per hour. The default is 1, and
278 0 suppresses histograms. Valid values are:
280 0, 1, 2, 3, 5, 10, 15, 20, 30 or 60.
284 Don't display error information.
288 Don't display relaying information.
290 =item B<-nr>I</pattern/>
292 Don't display relaying information that matches.
296 Don't display transport information.
298 =item B<-nt>I</pattern/>
300 Don't display transport information that matches
304 List of times for queuing information single 0 item suppresses.
308 Display top <number> sources/destinations
309 default is 50, 0 suppresses top listing.
313 Omit local sources/destinations in top listing.
315 =item B<-t_remote_users>
317 Include remote users in the top source/destination listings.
319 =item B<-include_original_destination>
321 Include the original destination email addresses rather than just
322 using the final ones.
323 Useful for finding out which of your mailing lists are receiving mail.
325 =item B<-show_dt>I<list>
327 Show the delivery times (B<DT>)for all the messages.
329 Exim must have been configured to use the +delivery_time logging option
330 for this option to work.
332 I<list> is an optional list of times. Eg -show_dt1,2,4,8 will show
333 the number of messages with delivery times under 1 second, 2 seconds, 4 seconds,
334 8 seconds, and over 8 seconds.
336 =item B<-show_rt>I<list>
338 Show the receipt times for all the messages. The receipt time is
339 defined as the Completed hh:mm:ss - queue_time_overall - the Receipt hh:mm:ss.
340 These figures will be skewed by pipelined messages so might not be that useful.
342 Exim must have been configured to use the +queue_time_overall logging option
343 for this option to work.
345 I<list> is an optional list of times. Eg -show_rt1,2,4,8 will show
346 the number of messages with receipt times under 1 second, 2 seconds, 4 seconds,
347 8 seconds, and over 8 seconds.
351 Show results by sending host. This may be combined with
352 B<-bydomain> and/or B<-byemail> and/or B<-byedomain>. If none of these options
353 are specified, then B<-byhost> is assumed as a default.
357 Show results by sending domain.
358 May be combined with B<-byhost> and/or B<-byemail> and/or B<-byedomain>.
362 Show results by sender's email address.
363 May be combined with B<-byhost> and/or B<-bydomain> and/or B<-byedomain>.
365 =item B<-byemaildomain> or B<-byedomain>
367 Show results by sender's email domain.
368 May be combined with B<-byhost> and/or B<-bydomain> and/or B<-byemail>.
370 =item B<-pattern> I<Description> I</Pattern/>
372 Look for the specified pattern and count the number of lines in which it appears.
373 This option can be specified multiple times. Eg:
375 -pattern 'Refused connections' '/refused connection/'
380 This option allows eximstats to merge old eximstat reports together. Eg:
382 eximstats mainlog.sun > report.sun.txt
383 eximstats mainlog.mon > report.mon.txt
384 eximstats mainlog.tue > report.tue.txt
385 eximstats mainlog.wed > report.web.txt
386 eximstats mainlog.thu > report.thu.txt
387 eximstats mainlog.fri > report.fri.txt
388 eximstats mainlog.sat > report.sat.txt
389 eximstats -merge report.*.txt > weekly_report.txt
390 eximstats -merge -html report.*.txt > weekly_report.html
396 You can merge text or html reports and output the results as text or html.
400 You can use all the normal eximstat output options, but only data
401 included in the original reports can be shown!
405 When merging reports, some loss of accuracy may occur in the top I<n> lists.
406 This will be towards the ends of the lists.
410 The order of items in the top I<n> lists may vary when the data volumes
411 round to the same value.
417 Create graphical charts to be displayed in HTML output.
418 Only valid in combination with I<-html>.
420 This requires the following modules which can be obtained
421 from http://www.cpan.org/modules/01modules.index.html
433 To install these, download and unpack them, then use the normal perl installation procedure:
440 =item B<-chartdir>I <dir>
442 Create the charts in the directory <dir>
444 =item B<-chartrel>I <dir>
446 Specify the relative directory for the "img src=" tags from where to include
451 Debug flag. This outputs the eval()'d parser onto STDOUT which makes it
452 easier to trap errors in the eval section. Remember to add 1 to the line numbers to allow for the
459 Eximstats parses exim mainlog and syslog files to output a statistical
460 analysis of the messages processed. By default, a text
461 analysis is generated, but you can request other output formats
462 using flags. See the help (B<-help>) to learn
463 about how to create charts from the tables.
467 There is a web site at http://www.exim.org - this contains details of the
468 mailing list exim-users@exim.org.
472 This program does not perfectly handle messages whose received
473 and delivered log lines are in different files, which can happen
474 when you have multiple mail servers and a message cannot be
475 immeadiately delivered. Fixing this could be tricky...
477 Merging of xls files is not (yet) possible. Be free to implement :)
485 # use Time::Local; # PH/FANF
488 use vars qw($HAVE_GD_Graph_pie $HAVE_GD_Graph_linespoints $HAVE_Spreadsheet_WriteExcel);
489 eval { require GD::Graph::pie; };
490 $HAVE_GD_Graph_pie = $@ ? 0 : 1;
491 eval { require GD::Graph::linespoints; };
492 $HAVE_GD_Graph_linespoints = $@ ? 0 : 1;
493 eval { require Spreadsheet::WriteExcel; };
494 $HAVE_Spreadsheet_WriteExcel = $@ ? 0 : 1;
497 ##################################################
499 ##################################################
500 # 'use vars' instead of 'our' as perl5.005 is still in use out there!
501 use vars qw(@tab62 @days_per_month $gig);
502 use vars qw($VERSION);
503 use vars qw($COLUMN_WIDTHS);
504 use vars qw($WEEK $DAY $HOUR $MINUTE);
508 (0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0, # 0-9
509 0,10,11,12,13,14,15,16,17,18,19,20, # A-K
510 21,22,23,24,25,26,27,28,29,30,31,32, # L-W
511 33,34,35, 0, 0, 0, 0, 0, # X-Z
512 0,36,37,38,39,40,41,42,43,44,45,46, # a-k
513 47,48,49,50,51,52,53,54,55,56,57,58, # l-w
516 @days_per_month = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
517 $gig = 1024 * 1024 * 1024;
520 # How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
524 $HOUR = 60 * $MINUTE;
528 # Declare global variables.
529 use vars qw($total_received_data $total_received_data_gigs $total_received_count);
530 use vars qw($total_delivered_data $total_delivered_data_gigs $total_delivered_count);
531 use vars qw(%timestamp2time); #Hash of timestamp => time.
532 use vars qw($last_timestamp $last_time); #The last time convertion done.
533 use vars qw($last_date $date_seconds); #The last date convertion done.
534 use vars qw($last_offset $offset_seconds); #The last time offset convertion done.
535 use vars qw($localtime_offset);
536 use vars qw($i); #General loop counter.
537 use vars qw($debug); #Debug mode?
538 use vars qw($ntopchart); #How many entries should make it into the chart?
539 use vars qw($gddirectory); #Where to put files from GD::Graph
540 use vars qw($workbook $ws_global $ws_relayed $ws_top50 $ws_errors ); #For use in Speadsheed::WriteExcel
541 use vars qw($row $col $row_hist $col_hist $row_league_table);
542 use vars qw($run_hist);
543 use vars qw($f_default $f_header1 $f_header2 $f_headertab $f_percent); #Format Header
546 use vars qw($txt_fh $htm_fh $xls_fh);
550 # The following are parameters whose values are
551 # set by command line switches:
552 use vars qw($show_errors $show_relay $show_transport $transport_pattern);
553 use vars qw($topcount $local_league_table $include_remote_users);
554 use vars qw($hist_opt $hist_interval $hist_number $volume_rounding);
555 use vars qw($relay_pattern @queue_times @user_patterns @user_descriptions);
556 use vars qw(@rcpt_times @delivery_times);
557 use vars qw($include_original_destination);
558 use vars qw($txt_fh $htm_fh $xls_fh);
560 use vars qw(%do_sender); #Do sender by Host, Domain, Email, and/or Edomain tables.
561 use vars qw($charts $chartrel $chartdir $charts_option_specified);
562 use vars qw($merge_reports); #Merge old reports ?
564 # The following are modified in the parse() routine, and
565 # referred to in the print_*() routines.
566 use vars qw($delayed_count $relayed_unshown $begin $end);
567 use vars qw(%messages $message_aref);
568 use vars qw(%received_count %received_data %received_data_gigs);
569 use vars qw(%delivered_count %delivered_data %delivered_data_gigs);
570 use vars qw(%received_count_user %received_data_user %received_data_gigs_user);
571 use vars qw(%delivered_count_user %delivered_data_user %delivered_data_gigs_user);
572 use vars qw(%transported_count %transported_data %transported_data_gigs);
573 use vars qw(%relayed %errors_count $message_errors);
574 use vars qw(@qt_all_bin @qt_remote_bin);
575 use vars qw($qt_all_overflow $qt_remote_overflow);
576 use vars qw(@dt_all_bin @dt_remote_bin %rcpt_times_bin);
577 use vars qw($dt_all_overflow $dt_remote_overflow %rcpt_times_overflow);
578 use vars qw(@received_interval_count @delivered_interval_count);
579 use vars qw(@user_pattern_totals @user_pattern_interval_count);
581 use vars qw(%report_totals);
584 use vars qw($SIZE $FROM_HOST $FROM_ADDRESS $ARRIVAL_TIME $REMOTE_DELIVERED $PROTOCOL);
585 use vars qw($DELAYED $HAD_ERROR);
590 $REMOTE_DELIVERED = 4;
597 ##################################################
599 ##################################################
601 #######################################################################
602 # get_filehandle($file,\%output_files);
603 # Return a filehandle writing to $file.
605 # If %output_files is defined, check that $output_files{$file}
606 # doesn't exist and die if it does, or set it if it doesn't.
607 #######################################################################
609 my($file,$output_files_href) = @_;
611 $file = '-' if ($file eq '');
613 if (defined $output_files_href) {
614 die "You can only output to '$file' once! Use -h for help.\n" if exists $output_files_href->{$file};
615 $output_files_href->{$file} = 1;
623 unlink $file or die "Failed to rm $file: $!";
626 my $fh = new IO::File $file, O_WRONLY|O_CREAT|O_EXCL;
627 die "new IO::File $file failed: $!" unless (defined $fh);
632 #######################################################################
635 # $rounded_volume = volume_rounded($bytes,$gigabytes);
637 # Given a data size in bytes, round it to KB, MB, or GB
640 # Eg 12000 => 12KB, 15000000 => 14GB, etc.
642 # Note: I've experimented with Math::BigInt and it results in a 33%
643 # performance degredation as opposed to storing numbers split into
644 # bytes and gigabytes.
645 #######################################################################
657 if ($volume_rounding) {
661 $rounded = sprintf("%6d", $x);
663 elsif ($x < 10000000) {
664 $rounded = sprintf("%4dKB", ($x + 512)/1024);
667 $rounded = sprintf("%4dMB", ($x + 512*1024)/(1024*1024));
670 # Values between 1GB and 10GB are printed in MB
672 $rounded = sprintf("%4dMB", ($g * 1024) + ($x + 512*1024)/(1024*1024));
675 # Handle values over 10GB
676 $rounded = sprintf("%4dGB", $g + ($x + $gig/2)/$gig);
680 # We don't want any rounding to be done.
681 # and we don't need broken formated output which on one hand avoids numbers from
682 # being interpreted as string by Spreadsheed Calculators, on the other hand
683 # breaks if more than 4 digits! -> flexible length instead of fixed length
684 # Format the return value at the output routine! -fh
685 #$rounded = sprintf("%d", ($g * $gig) + $x);
687 $rounded = sprintf("%.0f", ($g * $gig) + $x);
694 #######################################################################
697 # un_round($rounded_volume,\$bytes,\$gigabytes);
699 # Given a volume in KB, MB or GB, as generated by volume_rounded(),
700 # do the reverse transformation and convert it back into Bytes and Gigabytes.
701 # These are added to the $bytes and $gigabytes parameters.
703 # Given a data size in bytes, round it to KB, MB, or GB
706 # EG: 500 => (500,0), 14GB => (0,14), etc.
707 #######################################################################
709 my($rounded,$bytes_sref,$gigabytes_sref) = @_;
711 if ($rounded =~ /(\d+)GB/) {
712 $$gigabytes_sref += $1;
714 elsif ($rounded =~ /(\d+)MB/) {
715 $$gigabytes_sref += $1 / 1024;
716 $$bytes_sref += (($1 % 1024 ) * 1024 * 1024);
718 elsif ($rounded =~ /(\d+)KB/) {
719 $$gigabytes_sref += $1 / (1024 * 1024);
720 $$bytes_sref += ($1 % (1024 * 1024) * 1024);
722 elsif ($rounded =~ /(\d+)/) {
723 # We need to turn off integer in case we are merging an -nvr report.
725 $$gigabytes_sref += int($1 / $gig);
726 $$bytes_sref += $1 % $gig;
729 #Now reduce the bytes down to less than 1GB.
730 add_volume($bytes_sref,$gigabytes_sref,0) if ($$bytes_sref > $gig);
734 #######################################################################
737 # add_volume(\$bytes,\$gigs,$size);
739 # Add $size to $bytes/$gigs where this is a number split into
740 # bytes ($bytes) and gigabytes ($gigs). This is significantly
741 # faster than using Math::BigInt.
742 #######################################################################
744 my($bytes_ref,$gigs_ref,$size) = @_;
745 $$bytes_ref = 0 if ! defined $$bytes_ref;
746 $$gigs_ref = 0 if ! defined $$gigs_ref;
747 $$bytes_ref += $size;
748 while ($$bytes_ref > $gig) {
755 #######################################################################
758 # $formatted_time = format_time($seconds);
760 # Given a time in seconds, break it down into
761 # weeks, days, hours, minutes, and seconds.
763 # Eg 12005 => 3h20m5s
764 #######################################################################
776 $p .= "$w"."w" if $w > 0;
777 $p .= "$d"."d" if $d > 0;
778 $p .= "$h"."h" if $h > 0;
779 $p .= "$m"."m" if $m > 0;
780 $p .= "$s"."s" if $s > 0 || $p eq "";
785 #######################################################################
788 # $seconds = unformat_time($formatted_time);
790 # Given a time in weeks, days, hours, minutes, or seconds, convert it to seconds.
792 # Eg 3h20m5s => 12005
793 #######################################################################
795 my($formated_time) = pop @_;
798 while ($formated_time =~ s/^(\d+)([wdhms]?)//) {
799 $time += $1 if ($2 eq '' || $2 eq 's');
800 $time += $1 * 60 if ($2 eq 'm');
801 $time += $1 * 60 * 60 if ($2 eq 'h');
802 $time += $1 * 60 * 60 * 24 if ($2 eq 'd');
803 $time += $1 * 60 * 60 * 24 * 7 if ($2 eq 'w');
809 #######################################################################
812 # $time = seconds($timestamp);
814 # Given a time-of-day timestamp, convert it into a time() value using
815 # POSIX::mktime. We expect the timestamp to be of the form
816 # "$year-$mon-$day $hour:$min:$sec", with month going from 1 to 12,
817 # and the year to be absolute (we do the necessary conversions). The
818 # timestamp may be followed with an offset from UTC like "+$hh$mm"; if the
819 # offset is not present, and we have not been told that the log is in UTC
820 # (with the -utc option), then we adjust the time by the current local
821 # time offset so that it can be compared with the time recorded in message
824 # To improve performance, we only use mktime on the date ($year-$mon-$day),
825 # and only calculate it if the date is different to the previous time we
826 # came here. We then add on seconds for the '$hour:$min:$sec'.
828 # We also store the results of the last conversion done, and only
829 # recalculate if the date is different.
831 # We used to have the '-cache' flag which would store the results of the
832 # mktime() call. However, the current way of just using mktime() on the
833 # date obsoletes this.
834 #######################################################################
838 # Is the timestamp the same as the last one?
839 return $last_time if ($last_timestamp eq $timestamp);
841 return 0 unless ($timestamp =~ /^((\d{4})\-(\d\d)-(\d\d))\s(\d\d):(\d\d):(\d\d)( ([+-])(\d\d)(\d\d))?/o);
843 unless ($last_date eq $1) {
845 my(@timestamp) = (0,0,0,$4,$3,$2);
846 $timestamp[5] -= 1900;
848 $date_seconds = mktime(@timestamp);
850 my $time = $date_seconds + ($5 * 3600) + ($6 * 60) + $7;
852 # SC. Use cacheing. Also note we want seconds not minutes.
853 #my($this_offset) = ($10 * 60 + $11) * ($9 . "1") if defined $8;
854 if (defined $8 && ($8 ne $last_offset)) {
856 $offset_seconds = ($10 * 60 + $11) * 60;
857 $offset_seconds = -$offset_seconds if ($9 eq '-');
862 #$time -= $this_offset;
863 $time -= $offset_seconds;
864 } elsif (defined $localtime_offset) {
865 $time -= $localtime_offset;
868 # Store the last timestamp received.
869 $last_timestamp = $timestamp;
876 #######################################################################
879 # $time = id_seconds($message_id);
881 # Given a message ID, convert it into a time() value.
882 #######################################################################
884 my($sub_id) = substr((pop @_), 0, 6);
886 my(@c) = split(//, $sub_id);
887 while($#c >= 0) { $s = $s * 62 + $tab62[ord(shift @c) - ord('0')] }
891 #######################################################################
894 # $seconds = wdhms_seconds($string);
896 # Convert a string in a week/day/hour/minute/second format (eg 4h10s)
898 #######################################################################
900 if ($_[0] =~ /^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/) {
901 return((($1||0) * $WEEK) + (($2||0) * $DAY) + (($3||0) * $HOUR) + (($4||0) * $MINUTE) + ($5||0));
906 #######################################################################
909 # $queued = queue_time($completed_tod, $arrival_time, $id);
911 # Given the completed time of day and either the arrival time
912 # (preferred), or the message ID, calculate how long the message has
915 #######################################################################
917 my($completed_tod, $arrival_time, $id) = @_;
919 # Note: id_seconds() benchmarks as 42% slower than seconds()
920 # and computing the time accounts for a significant portion of
922 if (defined $arrival_time) {
923 return(seconds($completed_tod) - seconds($arrival_time));
926 return(seconds($completed_tod) - id_seconds($id));
931 #######################################################################
932 # calculate_localtime_offset();
934 # $localtime_offset = calculate_localtime_offset();
936 # Calculate the the localtime offset from gmtime in seconds.
938 # $localtime = time() + $localtime_offset.
940 # These are the same semantics as ISO 8601 and RFC 2822 timezone offsets.
941 # (West is negative, East is positive.)
942 #######################################################################
944 # $localtime = gmtime() + $localtime_offset. OLD COMMENT
945 # This subroutine commented out as it's not currently in use.
947 #sub calculate_localtime_offset {
948 # # Pick an arbitrary date, convert it to localtime & gmtime, and return the difference.
949 # my (@sample_date) = (0,0,0,5,5,100);
950 # my $localtime = timelocal(@sample_date);
951 # my $gmtime = timegm(@sample_date);
952 # my $offset = $localtime - $gmtime;
956 sub calculate_localtime_offset {
957 # Assume that the offset at the moment is valid across the whole
958 # period covered by the logs that we're analysing. This may not
959 # be true around the time the clocks change in spring or autumn.
961 # mktime works on local time and gmtime works in UTC
962 my $local = mktime(gmtime($utc));
963 return $local - $utc;
968 #######################################################################
969 # print_duration_table();
971 # print_duration_table($title, $message_type, \@times, \@values, $overflow);
973 # Print a table showing how long a particular step took for
974 # the messages. The parameters are:
975 # $title Eg "Time spent on the queue"
976 # $message_type Eg "Remote"
977 # \@times The maximum time a message took for it to increment
978 # the corresponding @values counter.
979 # \@values An array of message counters.
980 # $overflow The number of messages which exceeded the maximum
982 #######################################################################
983 sub print_duration_table {
985 my($title, $message_type, $times_aref, $values_aref, $overflow) = @_;
990 my $cumulative_percent = 0;
992 my $queue_total = $overflow;
993 map {$queue_total += $_} @$values_aref;
995 my $temp = "$title: $message_type";
998 my $txt_format = "%5s %4s %6d %5.1f%% %5.1f%%\n";
999 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";
1002 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
1004 print $htm_fh "<hr><a name=\"$title $message_type\"></a><h2>$temp</h2>\n";
1005 print $htm_fh "<table border=0 width=\"100%\">\n";
1006 print $htm_fh "<tr><td>\n";
1007 print $htm_fh "<table border=1>\n";
1008 print $htm_fh "<tr><th>Time</th><th>Messages</th><th>Percentage</th><th>Cumulative Percentage</th>\n";
1013 $ws_global->write($row++, $col, "$title: ".$message_type, $f_header2);
1014 my @content=("Time", "Messages", "Percentage", "Cumulative Percentage");
1015 &set_worksheet_line($ws_global, $row++, 1, \@content, $f_headertab);
1019 for ($i = 0; $i <= $#$times_aref; ++$i) {
1020 if ($$values_aref[$i] > 0)
1022 my $percent = ($values_aref->[$i] * 100)/$queue_total;
1023 $cumulative_percent += $percent;
1025 my @content=($printed_one? " " : "Under",
1026 format_time($times_aref->[$i]),
1027 $values_aref->[$i], $percent, $cumulative_percent);
1030 printf $htm_fh ($htm_format, @content);
1031 if (!defined($values_aref->[$i])) {
1032 print $htm_fh "Not defined";
1036 printf $txt_fh ($txt_format, @content);
1037 if (!defined($times_aref->[$i])) {
1038 print $txt_fh "Not defined";
1044 &set_worksheet_line($ws_global, $row, 0, [@content[0,1,2]], $f_default);
1045 &set_worksheet_line($ws_global, $row++, 3, [$content[3]/100,$content[4]/100], $f_percent);
1047 if (!defined($times_aref->[$i])) {
1049 $ws_global->write($row++, $col, "Not defined" );
1053 push(@chartdatanames,
1054 ($printed_one? "" : "Under") . format_time($times_aref->[$i]));
1055 push(@chartdatavals, $$values_aref[$i]);
1060 if ($overflow && $overflow > 0) {
1061 my $percent = ($overflow * 100)/$queue_total;
1062 $cumulative_percent += $percent;
1064 my @content = ("Over ", format_time($times_aref->[-1]),
1065 $overflow, $percent, $cumulative_percent);
1067 printf $txt_fh ($txt_format, @content) if $txt_fh;
1068 printf $htm_fh ($htm_format, @content) if $htm_fh;
1071 &set_worksheet_line($ws_global, $row, 0, [@content[0,1,2]], $f_default);
1072 &set_worksheet_line($ws_global, $row++, 3, [$content[3]/100,$content[4]/100], $f_percent);
1077 push(@chartdatanames, "Over " . format_time($times_aref->[-1]));
1078 push(@chartdatavals, $overflow);
1080 #printf("Unknown %6d\n", $queue_unknown) if $queue_unknown > 0;
1082 print $htm_fh "</table>\n";
1083 print $htm_fh "</td><td>\n";
1085 if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0)) {
1090 my $graph = GD::Graph::pie->new(200, 200);
1091 my $pngname = "$title-$message_type.png";
1092 $pngname =~ s/[^\w\-\.]/_/;
1094 my $graph_title = "$title ($message_type)";
1095 $graph->set(title => $graph_title) if (length($graph_title) < 21);
1097 my $gd = $graph->plot(\@data) or warn($graph->error);
1099 open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
1103 print $htm_fh "<img src=\"$chartrel/$pngname\">";
1106 print $htm_fh "</td></tr></table>\n";
1113 print $txt_fh "\n" if $txt_fh;
1114 print $htm_fh "\n" if $htm_fh;
1119 #######################################################################
1120 # print_histogram();
1122 # print_histogram('Deliveries|Messages received|$pattern', $unit, @interval_count);
1124 # Print a histogram of the messages delivered/received per time slot
1125 # (hour by default).
1126 #######################################################################
1127 sub print_histogram {
1128 my($text, $unit, @interval_count) = @_;
1129 my(@chartdatanames);
1133 if (!$run_hist) # save first row of print_histogram for xls output
1142 for ($i = 0; $i < $hist_number; $i++)
1143 { $maxd = $interval_count[$i] if $interval_count[$i] > $maxd; }
1145 my $scale = int(($maxd + 25)/50);
1146 $scale = 1 if $scale == 0;
1149 if ($unit !~ s/y$/ies/) {
1154 # make and output title
1155 my $title = sprintf("$text per %s",
1156 ($hist_interval == 60)? "hour" :
1157 ($hist_interval == 1)? "minute" : "$hist_interval minutes");
1159 my $txt_htm_title = $title . " (each dot is $scale $unit)";
1161 printf $txt_fh ("%s\n%s\n\n", $txt_htm_title, "-" x length($txt_htm_title)) if $txt_fh;
1164 print $htm_fh "<hr><a name=\"$text\"></a><h2>$txt_htm_title</h2>\n";
1165 print $htm_fh "<table border=0 width=\"100%\">\n";
1166 print $htm_fh "<tr><td><pre>\n";
1171 $title =~ s/Messages/Msg/ ;
1172 $ws_global->write($row++, $col_hist+1, $title, $f_headertab);
1178 for ($i = 0; $i < $hist_number; $i++)
1180 my $c = $interval_count[$i];
1182 # If the interval is an hour (the maximum) print the starting and
1183 # ending hours as a label. Otherwise print the starting hour and
1184 # minutes, which take up the same space.
1189 $temp = sprintf("%02d-%02d", $hour, $hour + 1);
1191 print $txt_fh $temp if $txt_fh;
1192 print $htm_fh $temp if $htm_fh;
1196 if ($run_hist==0) # only on first run
1198 &set_worksheet_line($ws_global, $row, 0, [$temp], $f_default);
1202 push(@chartdatanames, $temp);
1208 { $temp = sprintf("%02d:%02d", $hour, $minutes) }
1210 { $temp = sprintf(" :%02d", $minutes) }
1212 print $txt_fh $temp if $txt_fh;
1213 print $htm_fh $temp if $htm_fh;
1214 if (($xls_fh) and ($run_hist==0)) # only on first run
1216 $temp = sprintf("%02d:%02d", $hour, $minutes);
1217 &set_worksheet_line($ws_global, $row, 0, [$temp], $f_default);
1220 push(@chartdatanames, $temp);
1221 $minutes += $hist_interval;
1228 push(@chartdatavals, $c);
1230 printf $txt_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $txt_fh;
1231 printf $htm_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $htm_fh;
1234 &set_worksheet_line($ws_global, $row++, $col_hist+1, [$c], $f_default);
1239 printf $txt_fh "\n" if $txt_fh;
1240 printf $htm_fh "\n" if $htm_fh;
1244 print $htm_fh "</pre>\n";
1245 print $htm_fh "</td><td>\n";
1246 if ($HAVE_GD_Graph_linespoints && $charts && ($#chartdatavals > 0)) {
1247 # calculate the graph
1252 my $graph = GD::Graph::linespoints->new(300, 300);
1255 y_label => 'Amount',
1257 x_labels_vertical => 1
1259 my $pngname = "histogram_$text.png";
1260 $pngname =~ s/[^\w\._]/_/g;
1262 my $gd = $graph->plot(\@data) or warn($graph->error);
1264 open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
1268 print $htm_fh "<img src=\"$chartrel/$pngname\">";
1271 print $htm_fh "</td></tr></table>\n";
1274 $col_hist++; # where to continue next times
1276 $row+=2; # leave some space after history block
1277 $run_hist=1; # we have done this once or more
1282 #######################################################################
1283 # print_league_table();
1285 # print_league_table($league_table_type,\%message_count,\%message_data,\%message_data_gigs);
1287 # Given hashes of message count and message data, which are keyed by
1288 # the table type (eg by the sending host), print a league table
1289 # showing the top $topcount (defaults to 50).
1290 #######################################################################
1291 sub print_league_table {
1292 my($text,$m_count,$m_data,$m_data_gigs) = @_;
1293 my($name) = ($topcount == 1)? "$text" : "$topcount ${text}s";
1294 my($temp) = "Top $name by message count";
1295 my(@chartdatanames) = ();
1296 my(@chartdatavals) = ();
1297 my $chartotherval = 0;
1300 my $txt_format = "%7d %10s %s\n";
1303 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
1305 print $htm_fh "<hr><a name=\"$text count\"></a><h2>$temp</h2>\n";
1306 print $htm_fh "<table border=0 width=\"100%\">\n";
1307 print $htm_fh "<tr><td>\n";
1308 print $htm_fh "<table border=1>\n";
1309 print $htm_fh "<tr><th>Messages</th><th>Bytes</th><th>Average</th><th>\u$text</th>\n";
1311 # Align non-local addresses to the right (so all the .com's line up).
1312 # Local addresses are aligned on the left as they are userids.
1313 my $align = ($text !~ /local/i) ? 'right' : 'left';
1314 $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";
1318 $ws_top50->write($row_league_table++, 0, $temp, $f_header2);
1319 &set_worksheet_line($ws_top50, $row_league_table++, 0, ["Messages", "Bytes", "Average", $text], $f_headertab );
1324 my($key,$htmlkey,$rounded_volume,$rounded_average,$count,$data,$gigs);
1325 foreach $key (top_n_sort($topcount,$m_count,$m_data_gigs,$m_data)) {
1327 # When displaying the average figures, we calculate the average of
1328 # the rounded data, as the user would calculate it. This reduces
1329 # the accuracy slightly, but we have to do it this way otherwise
1330 # when using -merge to convert results from text to HTML and
1331 # vice-versa discrepencies would occur.
1332 $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
1334 un_round($rounded_volume,\$data,\$gigs);
1335 $count = $$m_count{$key};
1336 $rounded_average = volume_rounded($data/$count,$gigs/$count);
1337 my @content=( $count, $rounded_volume, $rounded_average);
1340 # any reason not to include rounded_average in txt-output? -fh
1341 printf $txt_fh ($txt_format, $count, $rounded_volume, $key) if $txt_fh;
1345 $htmlkey =~ s/>/\>\;/g;
1346 $htmlkey =~ s/</\<\;/g;
1347 printf $htm_fh ($htm_format, @content, $htmlkey);
1351 &set_worksheet_line($ws_top50, $row_league_table++, 0, [@content, $key], $f_default);
1354 if (scalar @chartdatanames < $ntopchart)
1356 push(@chartdatanames, $key);
1357 push(@chartdatavals, $$m_count{$key});
1361 $chartotherval += $$m_count{$key};
1365 push(@chartdatanames, "Other");
1366 push(@chartdatavals, $chartotherval);
1368 print $txt_fh "\n" if $txt_fh;
1371 print $htm_fh "</table>\n";
1372 print $htm_fh "</td><td>\n";
1373 if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0))
1375 # calculate the graph
1380 my $graph = GD::Graph::pie->new(300, 300);
1383 y_label => 'Amount',
1384 title => 'By count',
1386 my $gd = $graph->plot(\@data) or warn($graph->error);
1390 open(IMG, ">$chartdir/${temp}_count.png") or die "Could not write $chartdir/${temp}_count.png: $!\n";
1394 print $htm_fh "<img src=\"$chartrel/${temp}_count.png\">";
1397 print $htm_fh "</td><td>\n";
1398 print $htm_fh "</td></tr></table>\n\n";
1402 $row_league_table++;
1408 $temp = "Top $name by volume";
1410 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
1412 print $htm_fh "<hr><a name=\"$text volume\"></a><h2>$temp</h2>\n";
1413 print $htm_fh "<table border=0 width=\"100%\">\n";
1414 print $htm_fh "<tr><td>\n";
1415 print $htm_fh "<table border=1>\n";
1416 print $htm_fh "<tr><th>Messages</th><th>Bytes</th><th>Average</th><th>\u$text</th>\n";
1420 $ws_top50->write($row_league_table++, 0, $temp, $f_header2);
1421 &set_worksheet_line($ws_top50, $row_league_table++, 0, ["Messages", "Bytes", "Average", $text], $f_headertab);
1424 @chartdatanames = ();
1425 @chartdatavals = ();
1428 foreach $key (top_n_sort($topcount,$m_data_gigs,$m_data,$m_count)) {
1429 # The largest volume will be the first (top of the list).
1430 # If it has at least 1 gig, then just use gigabytes to avoid
1431 # risking an integer overflow when generating the pie charts.
1432 if ($$m_data_gigs{$key}) {
1436 $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
1438 un_round($rounded_volume,\$data,\$gigs);
1439 $count = $$m_count{$key};
1440 $rounded_average = volume_rounded($data/$count,$gigs/$count);
1441 my @content=($count, $rounded_volume, $rounded_average );
1444 # any reasons for not including rounded_average in the txt-version?? -fh
1445 printf $txt_fh ($txt_format, $count, $rounded_volume, $key) if $txt_fh;
1448 $htmlkey =~ s/>/\>\;/g;
1449 $htmlkey =~ s/</\<\;/g;
1450 printf $htm_fh ($htm_format, @content, $htmlkey);
1454 &set_worksheet_line($ws_top50, $row_league_table++, 0, [@content, $key], $f_default);
1458 if (scalar @chartdatanames < $ntopchart) {
1460 if ($$m_data_gigs{$key}) {
1461 push(@chartdatanames, $key);
1462 push(@chartdatavals, $$m_data_gigs{$key});
1466 push(@chartdatanames, $key);
1467 push(@chartdatavals, $$m_data{$key});
1471 $chartotherval += ($use_gig) ? $$m_data_gigs{$key} : $$m_data{$key};
1474 push(@chartdatanames, "Other");
1475 push(@chartdatavals, $chartotherval);
1477 print $txt_fh "\n" if $txt_fh;
1479 print $htm_fh "</table>\n";
1480 print $htm_fh "</td><td>\n";
1481 if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0)) {
1482 # calculate the graph
1487 my $graph = GD::Graph::pie->new(300, 300);
1490 y_label => 'Volume' ,
1491 title => 'By Volume',
1493 my $gd = $graph->plot(\@data) or warn($graph->error);
1497 open(IMG, ">$chartdir/${temp}_volume.png") or die "Could not write $chartdir/${temp}_volume.png: $!\n";
1501 print $htm_fh "<img src=\"$chartrel/${temp}_volume.png\">";
1504 print $htm_fh "</td><td>\n";
1505 print $htm_fh "</td></tr></table>\n\n";
1509 ++$row_league_table;
1514 #######################################################################
1517 # @sorted_keys = top_n_sort($n,$href1,$href2,$href3);
1519 # Given a hash which has numerical values, return the sorted $n keys which
1520 # point to the top values. The second and third hashes are used as
1521 # tiebreakers. They all must have the same keys.
1523 # The idea behind this routine is that when you only want to see the
1524 # top n members of a set, rather than sorting the entire set and then
1525 # plucking off the top n, sort through the stack as you go, discarding
1526 # any member which is lower than your current n'th highest member.
1528 # This proves to be an order of magnitude faster for large hashes.
1529 # On 200,000 lines of mainlog it benchmarked 9 times faster.
1530 # On 700,000 lines of mainlog it benchmarked 13.8 times faster.
1532 # We assume the values are > 0.
1533 #######################################################################
1535 my($n,$href1,$href2,$href3) = @_;
1537 # PH's original sort was:
1539 # foreach $key (sort
1541 # $$m_count{$b} <=> $$m_count{$a} ||
1542 # $$m_data_gigs{$b} <=> $$m_data_gigs{$a} ||
1543 # $$m_data{$b} <=> $$m_data{$a} ||
1549 #We use a key of '_' to represent non-existant values, as null keys are valid.
1550 #'_' is not a valid domain, edomain, host, or email.
1551 my(@top_n_keys) = ('_') x $n;
1552 my($minimum_value1,$minimum_value2,$minimum_value3) = (0,0,0);
1554 my $n_minus_1 = $n - 1;
1555 my $n_minus_2 = $n - 2;
1557 # Pick out the top $n keys.
1558 my($key,$value1,$value2,$value3,$i,$comparison,$insert_position);
1559 while (($key,$value1) = each %$href1) {
1561 #print STDERR "key $key ($value1,",$href2->{$key},",",$href3->{$key},") <=> ($minimum_value1,$minimum_value2,$minimum_value3)\n";
1563 # Check to see that the new value is bigger than the lowest of the
1564 # top n keys that we're keeping.
1565 $comparison = $value1 <=> $minimum_value1 ||
1566 $href2->{$key} <=> $minimum_value2 ||
1567 $href3->{$key} <=> $minimum_value3 ||
1568 $top_n_key cmp $key;
1569 next unless ($comparison == 1);
1571 # As we will be using these values a few times, extract them into scalars.
1572 $value2 = $href2->{$key};
1573 $value3 = $href3->{$key};
1575 # This key is bigger than the bottom n key, so the lowest position we
1576 # will insert it into is $n minus 1 (the bottom of the list).
1577 $insert_position = $n_minus_1;
1579 # Now go through the list, stopping when we find a key that we're
1580 # bigger than, or we come to the penultimate position - we've
1581 # already tested bigger than the last.
1583 # Note: we go top down as the list starts off empty.
1584 # Note: stepping through the list in this way benchmarks nearly
1585 # three times faster than doing a sort() on the reduced list.
1586 # I assume this is because the list is already in order, and
1587 # we get a performance boost from not having to do hash lookups
1589 for ($i = 0; $i < $n_minus_1; $i++) {
1590 $top_n_key = $top_n_keys[$i];
1591 if ( ($top_n_key eq '_') ||
1592 ( ($value1 <=> $href1->{$top_n_key} ||
1593 $value2 <=> $href2->{$top_n_key} ||
1594 $value3 <=> $href3->{$top_n_key} ||
1595 $top_n_key cmp $key) == 1
1598 $insert_position = $i;
1603 # Remove the last element, then insert the new one.
1604 $#top_n_keys = $n_minus_2;
1605 splice(@top_n_keys,$insert_position,0,$key);
1607 # Extract our new minimum values.
1608 $top_n_key = $top_n_keys[$n_minus_1];
1609 if ($top_n_key ne '_') {
1610 $minimum_value1 = $href1->{$top_n_key};
1611 $minimum_value2 = $href2->{$top_n_key};
1612 $minimum_value3 = $href3->{$top_n_key};
1616 # Return the top n list, grepping out non-existant values, just in case
1617 # we didn't have that many values.
1618 return(grep(!/^_$/,@top_n_keys));
1622 #######################################################################
1625 # $header = html_header($title);
1627 # Print our HTML header and start the <body> block.
1628 #######################################################################
1631 my $text = << "EoText";
1632 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
1635 <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-15">
1636 <title>$title</title>
1638 <body bgcolor="white">
1646 #######################################################################
1651 # Display usage instructions and exit.
1652 #######################################################################
1656 eximstats Version $VERSION
1659 eximstats [Output] [Options] mainlog1 mainlog2 ...
1660 eximstats -merge -html [Options] report.1.html ... > weekly_rep.html
1663 eximstats -html=eximstats.html mainlog1 mainlog2 ...
1664 eximstats mainlog1 mainlog2 ... > report.txt
1666 Parses exim mainlog or syslog files and generates a statistical analysis
1667 of the messages processed.
1669 Valid output types are:
1670 -txt[=<file>] plain text (default unless no other type is specified)
1673 With no type and file given, defaults to -txt and STDOUT.
1676 -h<number> histogram divisions per hour. The default is 1, and
1677 0 suppresses histograms. Other valid values are:
1678 2, 3, 5, 10, 15, 20, 30 or 60.
1679 -ne don't display error information
1680 -nr don't display relaying information
1681 -nr/pattern/ don't display relaying information that matches
1682 -nt don't display transport information
1683 -nt/pattern/ don't display transport information that matches
1684 -nvr don't do volume rounding. Display in bytes, not KB/MB/GB.
1685 -t<number> display top <number> sources/destinations
1686 default is 50, 0 suppresses top listing
1687 -tnl omit local sources/destinations in top listing
1688 -t_remote_users show top user sources/destinations from non-local domains
1689 -q<list> list of times for queuing information. -q0 suppresses.
1690 -show_rt<list> Show the receipt times for all the messages.
1691 -show_dt<list> Show the delivery times for all the messages.
1692 <list> is an optional list of times in seconds.
1695 -include_original_destination show both the final and original
1696 destinations in the results rather than just the final ones.
1698 -byhost show results by sending host (default unless bydomain or
1699 byemail is specified)
1700 -bydomain show results by sending domain.
1701 -byemail show results by sender's email address
1702 -byedomain show results by sender's email domain
1704 -pattern "Description" /pattern/
1705 Count lines matching specified patterns and show them in
1706 the results. It can be specified multiple times. Eg:
1707 -pattern 'Refused connections' '/refused connection/'
1709 -merge merge previously generated reports into a new report
1711 -charts Create charts (this requires the GD::Graph modules).
1712 Only valid with -html.
1713 -chartdir <dir> Create the charts' png files in the directory <dir>
1714 -chartrel <dir> Specify the relative directory for the "img src=" tags
1715 from where to include the charts in the html file
1716 -chartdir and -chartrel default to '.'
1718 -d Debug mode - dump the eval'ed parser onto STDERR.
1727 #######################################################################
1728 # generate_parser();
1730 # $parser = generate_parser();
1732 # This subroutine generates the parsing routine which will be
1733 # used to parse the mainlog. We take the base operation, and remove bits not in use.
1734 # This improves performance depending on what bits you take out or add.
1736 # I've tested using study(), but this does not improve performance.
1738 # We store our parsing routing in a variable, and process it looking for #IFDEF (Expression)
1739 # or #IFNDEF (Expression) statements and corresponding #ENDIF (Expression) statements. If
1740 # the expression evaluates to true, then it is included/excluded accordingly.
1741 #######################################################################
1742 sub generate_parser {
1744 my($ip,$host,$email,$edomain,$domain,$thissize,$size,$old,$new);
1745 my($tod,$m_hour,$m_min,$id,$flag);
1746 my($seconds,$queued,$rcpt_time);
1749 # Convert syslog lines to mainlog format.
1751 next unless s/^.*? exim\\b.*?: //;
1754 next if length($_) < 38;
1755 next unless /^(\\d{4}\\-\\d\\d-\\d\\d\\s(\\d\\d):(\\d\\d):\\d\\d( [-+]\\d\\d\\d\\d)?)/o;
1757 ($tod,$m_hour,$m_min) = ($1,$2,$3);
1760 my($extra) = defined($4)? 6 : 0;
1761 $id = substr($_, 20 + $extra, 16);
1762 $flag = substr($_, 37 + $extra, 2);
1765 # Watch for user specified patterns.
1766 my $user_pattern_index = 0;
1767 foreach (@user_patterns) {
1768 $user_pattern_totals[$user_pattern_index] = 0;
1769 $parser .= <<EoText;
1771 \$user_pattern_totals[$user_pattern_index]++ if $_;
1772 \$user_pattern_interval_count[$user_pattern_index][(\$m_hour*60 + \$m_min)/$hist_interval]++;
1775 $user_pattern_index++;
1779 next unless ($flag =~ /<=|=>|->|==|\\*\\*|Co/);
1781 #Strip away the timestamp, ID and flag (which could be "Com" for completed)
1782 #This speeds up the later pattern matches.
1783 # $_ = substr($_, 40);
1785 $_ = substr($_, 40 + $extra); # PH
1787 # Get a pointer to an array of information about the message.
1788 # This minimises the number of calls to hash functions.
1789 $messages{$id} = [] unless exists $messages{$id};
1790 $message_aref = $messages{$id};
1793 # JN - Skip over certain transports as specified via the "-nt/.../" command
1794 # line switch (where ... is a perl style regular expression). This is
1795 # required so that transports that skew stats such as SpamAssassin can be
1797 #IFDEF ($transport_pattern)
1798 if (/\\sT=(\\S+)/) {
1799 next if ($1 =~ /$transport_pattern/o) ;
1801 #ENDIF ($transport_pattern)
1804 $host = "local"; #Host is local unless otherwise specified.
1805 $domain = "localdomain"; #Domain is localdomain unless otherwise specified.
1808 # Do some pattern matches to get the host and IP address.
1809 # We expect lines to be of the form "H=[IpAddr]" or "H=Host [IpAddr]" or
1810 # "H=Host (UnverifiedHost) [IpAddr]" or "H=(UnverifiedHost) [IpAddr]".
1811 # We do 2 separate matches to keep the matches simple and fast.
1812 if (/\\sH=(\\S+)/) {
1815 ($ip) = /\\sH=.*?(\\s\\[[^]]+\\])/;
1816 # If there is only an IP address, it will be in $host and $ip will be
1817 # unset. That is OK, because we only use $ip in conjunction with $host
1818 # below. But make it empty to avoid warning messages.
1819 $ip = "" if !defined $ip;
1821 #IFDEF ($do_sender{Domain})
1822 if ($host !~ /^\\[/ && $host =~ /^(\\(?)[^\\.]+\\.([^\\.]+\\..*)/) {
1823 # Remove the host portion from the DNS name. We ensure that we end up
1824 # with at least xxx.yyy. $host can be "(x.y.z)" or "x.y.z".
1825 $domain = lc("$1.$2");
1826 $domain =~ s/^\\.//; #Remove preceding dot.
1828 #ENDIF ($do_sender{Domain})
1832 #IFDEF ($do_sender{Email})
1833 #IFDEF ($include_original_destination)
1834 # Catch both "a@b.com <c@d.com>" and "e@f.com"
1835 #$email = (/^(\S+) (<(\S*?)>)?/) ? $3 || $1 : "";
1836 $email = (/^(\S+ (<[^@>]+@?[^>]*>)?)/) ? $1 : "";
1838 #ENDIF ($include_original_destination)
1840 #IFNDEF ($include_original_destination)
1841 $email = (/^(\S+)/) ? $1 : "";
1842 #ENDIF ($include_original_destination)
1843 #ENDIF ($do_sender{Email})
1845 #IFDEF ($do_sender{Edomain})
1846 #IFDEF ($include_original_destination)
1847 #$edomain = (/^(\S+) (<\S*?\\@(\S+)>)?/) ? $3 || $1 : "";
1848 $edomain = (/^(\S+ (<\S*?\\@(\S+?)>)?)/) ? $1 : "";
1851 #ENDIF ($include_original_destination)
1853 #IFNDEF ($include_original_destination)
1854 $edomain = (/^\S*?\\@(\S+)/) ? lc($1) : "";
1855 #ENDIF ($include_original_destination)
1856 #ENDIF ($do_sender{Edomain})
1858 if ($tod lt $begin) {
1861 elsif ($tod gt $end) {
1866 if ($flag eq "<=") {
1867 $thissize = (/\\sS=(\\d+)( |$)/) ? $1 : 0;
1868 $message_aref->[$SIZE] = $thissize;
1869 $message_aref->[$PROTOCOL] = (/ P=(\S+)/) ? $1 : undef;
1871 #IFDEF ($show_relay)
1872 if ($host ne "local") {
1873 # Save incoming information in case it becomes interesting
1874 # later, when delivery lines are read.
1875 my($from) = /^(\\S+)/;
1876 $message_aref->[$FROM_HOST] = "$host$ip";
1877 $message_aref->[$FROM_ADDRESS] = $from;
1879 #ENDIF ($show_relay)
1881 #IFDEF ($local_league_table || $include_remote_users)
1885 #IFDEF ($local_league_table && $include_remote_users)
1886 { #Store both local and remote users.
1887 #ENDIF ($local_league_table && $include_remote_users)
1889 #IFDEF ($local_league_table && ! $include_remote_users)
1890 if ($host eq "local") { #Store local users only.
1891 #ENDIF ($local_league_table && ! $include_remote_users)
1893 #IFDEF ($include_remote_users && ! $local_league_table)
1894 if ($host ne "local") { #Store remote users only.
1895 #ENDIF ($include_remote_users && ! $local_league_table)
1897 ++$received_count_user{$user};
1898 add_volume(\\$received_data_user{$user},\\$received_data_gigs_user{$user},$thissize);
1901 #ENDIF ($local_league_table || $include_remote_users)
1903 #IFDEF ($do_sender{Host})
1904 ++$received_count{Host}{$host};
1905 add_volume(\\$received_data{Host}{$host},\\$received_data_gigs{Host}{$host},$thissize);
1906 #ENDIF ($do_sender{Host})
1908 #IFDEF ($do_sender{Domain})
1910 ++$received_count{Domain}{$domain};
1911 add_volume(\\$received_data{Domain}{$domain},\\$received_data_gigs{Domain}{$domain},$thissize);
1913 #ENDIF ($do_sender{Domain})
1915 #IFDEF ($do_sender{Email})
1916 ++$received_count{Email}{$email};
1917 add_volume(\\$received_data{Email}{$email},\\$received_data_gigs{Email}{$email},$thissize);
1918 #ENDIF ($do_sender{Email})
1920 #IFDEF ($do_sender{Edomain})
1921 ++$received_count{Edomain}{$edomain};
1922 add_volume(\\$received_data{Edomain}{$edomain},\\$received_data_gigs{Edomain}{$edomain},$thissize);
1923 #ENDIF ($do_sender{Edomain})
1925 ++$total_received_count;
1926 add_volume(\\$total_received_data,\\$total_received_data_gigs,$thissize);
1928 #IFDEF ($#queue_times >= 0 || $#rcpt_times >= 0)
1929 $message_aref->[$ARRIVAL_TIME] = $tod;
1930 #ENDIF ($#queue_times >= 0 || $#rcpt_times >= 0)
1932 #IFDEF ($hist_opt > 0)
1933 $received_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
1934 #ENDIF ($hist_opt > 0)
1937 elsif ($flag eq "=>") {
1938 $size = $message_aref->[$SIZE] || 0;
1939 if ($host ne "local") {
1940 $message_aref->[$REMOTE_DELIVERED] = 1;
1943 #IFDEF ($show_relay)
1944 # Determine relaying address if either only one address listed,
1945 # or two the same. If they are different, it implies a forwarding
1946 # or aliasing, which is not relaying. Note that for multi-aliased
1947 # addresses, there may be a further address between the first
1950 if (defined $message_aref->[$FROM_HOST]) {
1951 if (/^(\\S+)(?:\\s+\\([^)]\\))?\\s+<([^>]+)>/) {
1952 ($old,$new) = ($1,$2);
1958 if ("\\L$new" eq "\\L$old") {
1959 ($old) = /^(\\S+)/ if $old eq "";
1960 my $key = "H=\\L$message_aref->[$FROM_HOST]\\E A=\\L$message_aref->[$FROM_ADDRESS]\\E => " .
1961 "H=\\L$host\\E$ip A=\\L$old\\E";
1962 if (!defined $relay_pattern || $key !~ /$relay_pattern/o) {
1963 $relayed{$key} = 0 if !defined $relayed{$key};
1971 #ENDIF ($show_relay)
1975 #IFDEF ($local_league_table || $include_remote_users)
1976 #IFDEF ($local_league_table && $include_remote_users)
1977 { #Store both local and remote users.
1978 #ENDIF ($local_league_table && $include_remote_users)
1980 #IFDEF ($local_league_table && ! $include_remote_users)
1981 if ($host eq "local") { #Store local users only.
1982 #ENDIF ($local_league_table && ! $include_remote_users)
1984 #IFDEF ($include_remote_users && ! $local_league_table)
1985 if ($host ne "local") { #Store remote users only.
1986 #ENDIF ($include_remote_users && ! $local_league_table)
1988 if (my($user) = split((/\\s</)? " <" : " ", $_)) {
1989 #IFDEF ($include_original_destination)
1991 #ENDIF ($include_original_destination)
1992 #IFNDEF ($include_original_destination)
1993 if ($user =~ /^[\\/|]/) {
1994 #ENDIF ($include_original_destination)
1995 my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
1996 $user = "$user $parent" if defined $parent;
1998 ++$delivered_count_user{$user};
1999 add_volume(\\$delivered_data_user{$user},\\$delivered_data_gigs_user{$user},$size);
2002 #ENDIF ($local_league_table || $include_remote_users)
2004 #IFDEF ($do_sender{Host})
2005 $delivered_count{Host}{$host}++;
2006 add_volume(\\$delivered_data{Host}{$host},\\$delivered_data_gigs{Host}{$host},$size);
2007 #ENDIF ($do_sender{Host})
2008 #IFDEF ($do_sender{Domain})
2010 ++$delivered_count{Domain}{$domain};
2011 add_volume(\\$delivered_data{Domain}{$domain},\\$delivered_data_gigs{Domain}{$domain},$size);
2013 #ENDIF ($do_sender{Domain})
2014 #IFDEF ($do_sender{Email})
2015 ++$delivered_count{Email}{$email};
2016 add_volume(\\$delivered_data{Email}{$email},\\$delivered_data_gigs{Email}{$email},$size);
2017 #ENDIF ($do_sender{Email})
2018 #IFDEF ($do_sender{Edomain})
2019 ++$delivered_count{Edomain}{$edomain};
2020 add_volume(\\$delivered_data{Edomain}{$edomain},\\$delivered_data_gigs{Edomain}{$edomain},$size);
2021 #ENDIF ($do_sender{Edomain})
2023 ++$total_delivered_count;
2024 add_volume(\\$total_delivered_data,\\$total_delivered_data_gigs,$size);
2026 #IFDEF ($show_transport)
2027 my $transport = (/\\sT=(\\S+)/) ? $1 : ":blackhole:";
2028 ++$transported_count{$transport};
2029 add_volume(\\$transported_data{$transport},\\$transported_data_gigs{$transport},$size);
2030 #ENDIF ($show_transport)
2032 #IFDEF ($hist_opt > 0)
2033 $delivered_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
2034 #ENDIF ($hist_opt > 0)
2036 #IFDEF ($#delivery_times > 0)
2038 $seconds = wdhms_seconds($1);
2039 for ($i = 0; $i <= $#delivery_times; $i++) {
2040 if ($seconds < $delivery_times[$i]) {
2042 ++$dt_remote_bin[$i] if $message_aref->[$REMOTE_DELIVERED];
2046 if ($i > $#delivery_times) {
2048 ++$dt_remote_overflow if $message_aref->[$REMOTE_DELIVERED];
2051 #ENDIF ($#delivery_times > 0)
2055 elsif ($flag eq "==" && defined($message_aref->[$SIZE]) && !defined($message_aref->[$DELAYED])) {
2057 $message_aref->[$DELAYED] = 1;
2060 elsif ($flag eq "**") {
2061 if (defined ($message_aref->[$SIZE])) {
2062 unless (defined $message_aref->[$HAD_ERROR]) {
2064 $message_aref->[$HAD_ERROR] = 1;
2068 #IFDEF ($show_errors)
2069 ++$errors_count{$_};
2070 #ENDIF ($show_errors)
2074 elsif ($flag eq "Co") {
2076 #IFDEF ($#queue_times >= 0)
2077 $queued = queue_time($tod, $message_aref->[$ARRIVAL_TIME], $id);
2079 for ($i = 0; $i <= $#queue_times; $i++) {
2080 if ($queued < $queue_times[$i]) {
2082 ++$qt_remote_bin[$i] if $message_aref->[$REMOTE_DELIVERED];
2086 if ($i > $#queue_times) {
2088 ++$qt_remote_overflow if $message_aref->[$REMOTE_DELIVERED];
2090 #ENDIF ($#queue_times >= 0)
2092 #IFDEF ($#rcpt_times >= 0)
2094 $seconds = wdhms_seconds($1);
2095 #Calculate $queued if not previously calculated above.
2096 #IFNDEF ($#queue_times >= 0)
2097 $queued = queue_time($tod, $message_aref->[$ARRIVAL_TIME], $id);
2098 #ENDIF ($#queue_times >= 0)
2099 $rcpt_time = $seconds - $queued;
2102 if (defined $message_aref->[$PROTOCOL]) {
2103 $protocol = $message_aref->[$PROTOCOL];
2105 # Create the bin if its not already defined.
2106 unless (exists $rcpt_times_bin{$protocol}) {
2107 initialise_rcpt_times($protocol);
2112 for ($i = 0; $i <= $#rcpt_times; ++$i) {
2113 if ($rcpt_time < $rcpt_times[$i]) {
2114 ++$rcpt_times_bin{all}[$i];
2115 ++$rcpt_times_bin{$protocol}[$i] if defined $protocol;
2120 if ($i > $#rcpt_times) {
2121 ++$rcpt_times_overflow{all};
2122 ++$rcpt_times_overflow{$protocol} if defined $protocol;
2125 #ENDIF ($#rcpt_times >= 0)
2127 delete($messages{$id});
2131 # We now do a 'C preprocessor style operation on our parser
2132 # to remove bits not in use.
2133 my(%defines_in_operation,$removing_lines,$processed_parser);
2134 foreach (split (/\n/,$parser)) {
2135 if ((/^\s*#\s*IFDEF\s*\((.*?)\)/i && ! eval $1) ||
2136 (/^\s*#\s*IFNDEF\s*\((.*?)\)/i && eval $1) ) {
2137 $defines_in_operation{$1} = 1;
2138 $removing_lines = 1;
2141 # Convert constants.
2142 while (/(\$[A-Z][A-Z_]*)\b/) {
2143 my $constant = eval $1;
2144 s/(\$[A-Z][A-Z_]*)\b/$constant/;
2147 $processed_parser .= $_."\n" unless $removing_lines;
2149 if (/^\s*#\s*ENDIF\s*\((.*?)\)/i) {
2150 delete $defines_in_operation{$1};
2151 unless (keys %defines_in_operation) {
2152 $removing_lines = 0;
2156 print STDERR "# START OF PARSER:$processed_parser\n# END OF PARSER\n\n" if $debug;
2158 return $processed_parser;
2163 #######################################################################
2166 # parse($parser,\*FILEHANDLE);
2168 # This subroutine accepts a parser and a filehandle from main and parses each
2169 # line. We store the results into global variables.
2170 #######################################################################
2172 my($parser,$fh) = @_;
2174 if ($merge_reports) {
2175 parse_old_eximstat_reports($fh);
2186 #######################################################################
2191 # Print our headers and contents.
2192 #######################################################################
2196 my $title = "Exim statistics from $begin to $end";
2198 print $txt_fh "\n$title\n" if $txt_fh;
2200 print $htm_fh html_header($title);
2201 print $htm_fh "<ul>\n";
2202 print $htm_fh "<li><a href=\"#grandtotal\">Grand total summary</a>\n";
2203 print $htm_fh "<li><a href=\"#patterns\">User Specified Patterns</a>\n" if @user_patterns;
2204 print $htm_fh "<li><a href=\"#transport\">Deliveries by Transport</a>\n" if $show_transport;
2206 print $htm_fh "<li><a href=\"#Messages received\">Messages received per hour</a>\n";
2207 print $htm_fh "<li><a href=\"#Deliveries\">Deliveries per hour</a>\n";
2210 if ($#queue_times >= 0) {
2211 print $htm_fh "<li><a href=\"#Time spent on the queue all messages\">Time spent on the queue: all messages</a>\n";
2212 print $htm_fh "<li><a href=\"#Time spent on the queue messages with at least one remote delivery\">Time spent on the queue: messages with at least one remote delivery</a>\n";
2215 if ($#delivery_times >= 0) {
2216 print $htm_fh "<li><a href=\"#Delivery times all messages\">Delivery times: all messages</a>\n";
2217 print $htm_fh "<li><a href=\"#Delivery times messages with at least one remote delivery\">Delivery times: messages with at least one remote delivery</a>\n";
2220 if ($#rcpt_times >= 0) {
2221 print $htm_fh "<li><a href=\"#Receipt times all messages\">Receipt times</a>\n";
2224 print $htm_fh "<li><a href=\"#Relayed messages\">Relayed messages</a>\n" if $show_relay;
2226 foreach ('Host','Domain','Email','Edomain') {
2227 next unless $do_sender{$_};
2228 print $htm_fh "<li><a href=\"#sending \l$_ count\">Top $topcount sending \l${_}s by message count</a>\n";
2229 print $htm_fh "<li><a href=\"#sending \l$_ volume\">Top $topcount sending \l${_}s by volume</a>\n";
2231 if ($local_league_table || $include_remote_users) {
2232 print $htm_fh "<li><a href=\"#local sender count\">Top $topcount local senders by message count</a>\n";
2233 print $htm_fh "<li><a href=\"#local sender volume\">Top $topcount local senders by volume</a>\n";
2235 foreach ('Host','Domain','Email','Edomain') {
2236 next unless $do_sender{$_};
2237 print $htm_fh "<li><a href=\"#\l$_ destination count\">Top $topcount \l$_ destinations by message count</a>\n";
2238 print $htm_fh "<li><a href=\"#\l$_ destination volume\">Top $topcount \l$_ destinations by volume</a>\n";
2240 if ($local_league_table || $include_remote_users) {
2241 print $htm_fh "<li><a href=\"#local destination count\">Top $topcount local destinations by message count</a>\n";
2242 print $htm_fh "<li><a href=\"#local destination volume\">Top $topcount local destinations by volume</a>\n";
2245 print $htm_fh "<li><a href=\"#errors\">List of errors</a>\n" if %errors_count;
2246 print $htm_fh "</ul>\n<hr>\n";
2250 $ws_global->write($row++, $col+0, "Exim Statistics", $f_header1);
2251 &set_worksheet_line($ws_global, $row, $col, ["from:", $begin, "to:", $end], $f_default);
2257 #######################################################################
2258 # print_grandtotals();
2260 # print_grandtotals();
2262 # Print the grand totals.
2263 #######################################################################
2264 sub print_grandtotals {
2266 # Get the sender by headings and results. This is complicated as we can have
2267 # different numbers of columns.
2268 my($sender_txt_header,$sender_html_header,$sender_txt_format,$sender_html_format);
2269 my(@received_totals,@delivered_totals);
2270 my($row_tablehead, $row_max);
2272 foreach ('Host','Domain','Email','Edomain') {
2273 next unless $do_sender{$_};
2274 if ($merge_reports) {
2275 push(@received_totals, get_report_total($report_totals{Received},"${_}s"));
2276 push(@delivered_totals,get_report_total($report_totals{Delivered},"${_}s"));
2279 push(@received_totals,scalar(keys %{$received_data{$_}}));
2280 push(@delivered_totals,scalar(keys %{$delivered_data{$_}}));
2282 $sender_html_header .= "<th>${_}s</th>";
2283 $sender_txt_header .= " " x ($COLUMN_WIDTHS - length($_)) . $_ . 's';
2284 $sender_html_format .= "<td align=\"right\">%d</td>";
2285 $sender_txt_format .= " " x ($COLUMN_WIDTHS - 5) . "%6d";
2288 my $txt_format1 = " %-16s %9s %6d $sender_txt_format";
2289 my $txt_format2 = " %6d %4.1f%% %6d %4.1f%%",
2290 my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%s</td>$sender_html_format<td align=\"right\">%d</td>";
2291 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>";
2294 my $sender_spaces = " " x length($sender_txt_header);
2296 print $txt_fh "Grand total summary\n";
2297 print $txt_fh "-------------------\n";
2298 print $txt_fh " $sender_spaces At least one address\n";
2299 print $txt_fh " TOTAL Volume Messages $sender_txt_header Delayed Failed\n";
2302 print $htm_fh "<a name=\"grandtotal\"></a>\n";
2303 print $htm_fh "<h2>Grand total summary</h2>\n";
2304 print $htm_fh "<table border=1>\n";
2305 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";
2309 $ws_global->write($row++, $col, "Grand total summary", $f_header2);
2311 $row_tablehead = $row+1; # header-row of TOTALS table
2313 &set_worksheet_line($ws_global, $row_tablehead, 0, ['Received', 'Delivered', 'TOTAL'], $f_headertab);
2319 "At least one address Delayed (Total)",
2320 "At least one address Delayed (Percent)",
2321 "At least one address Failed (Total)",
2322 "At least one address Failed (Percent)"
2325 for (my $i=0; $i < scalar(@content); $i++)
2327 $ws_global->write($row_tablehead+$i+1, 2, $content[$i], $f_default);
2330 $row_max = $row_tablehead+scalar(@content)+2; # continue from this row
2335 my($volume,$failed_count);
2336 if ($merge_reports) {
2337 $volume = volume_rounded($report_totals{Received}{Volume}, $report_totals{Received}{'Volume-gigs'});
2338 $total_received_count = get_report_total($report_totals{Received},'Messages');
2339 $failed_count = get_report_total($report_totals{Received},'Failed');
2340 $delayed_count = get_report_total($report_totals{Received},'Delayed');
2343 $volume = volume_rounded($total_received_data, $total_received_data_gigs);
2344 $failed_count = $message_errors;
2351 $volume,$total_received_count,
2354 ($total_received_count) ? ($delayed_count*100/$total_received_count) : 0,
2356 ($total_received_count) ? ($failed_count*100/$total_received_count) : 0
2359 printf $txt_fh ("$txt_format1$txt_format2\n", 'Received', @content) if $txt_fh;
2360 printf $htm_fh ("$htm_format1$htm_format2\n", 'Received', @content) if $htm_fh;
2363 $row = $row_tablehead+1;
2364 for (my $i=0; $i < scalar(@content); $i++)
2366 if ($i == 4 || $i == 6) {
2367 $ws_global->write($row+$i, 0, $content[$i]/100, $f_percent);
2370 $ws_global->write($row+$i, 0, $content[$i], $f_default);
2375 if ($merge_reports) {
2376 $volume = volume_rounded($report_totals{Delivered}{Volume}, $report_totals{Delivered}{'Volume-gigs'});
2377 $total_delivered_count = get_report_total($report_totals{Delivered},'Messages');
2380 $volume = volume_rounded($total_delivered_data, $total_delivered_data_gigs);
2383 my @content=($volume, $total_delivered_count, @delivered_totals);
2384 printf $txt_fh ("$txt_format1\n\n", 'Delivered', @content) if $txt_fh;
2385 printf $htm_fh ("$htm_format1\n\n", 'Delivered', @content) if $htm_fh;
2386 printf $htm_fh "</table>\n" if $htm_fh;
2390 $row = $row_tablehead+1;
2391 for (my $i=0; $i < scalar(@content); $i++)
2393 $ws_global->write($row+$i, 1, $content[$i], $f_default);
2400 #######################################################################
2401 # print_user_patterns()
2403 # print_user_patterns();
2405 # Print the counts of user specified patterns.
2406 #######################################################################
2407 sub print_user_patterns {
2408 my $txt_format1 = " %-18s %6d";
2409 my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%d</td>";
2412 print $txt_fh "User Specified Patterns\n";
2413 print $txt_fh "-----------------------";
2414 print $txt_fh "\n Total\n";
2417 print $htm_fh "<hr><a name=\"patterns\"></a><h2>User Specified Patterns</h2>\n";
2418 print $htm_fh "<table border=0 width=\"100%\">\n";
2419 print $htm_fh "<tr><td>\n";
2420 print $htm_fh "<table border=1>\n";
2421 print $htm_fh "<tr><th> </th><th>Total</th>\n";
2424 $ws_global->write($row++, $col, "User Specified Patterns", $f_header2);
2425 &set_worksheet_line($ws_global, $row++, 1, ["Total"], $f_headertab);
2430 if ($merge_reports) {
2431 # We are getting our data from previous reports.
2432 foreach $key (@user_descriptions) {
2433 my $count = get_report_total($report_totals{patterns}{$key},'Total');
2434 printf $txt_fh ("$txt_format1\n",$key,$count) if $txt_fh;
2435 printf $htm_fh ("$htm_format1\n",$key,$count) if $htm_fh;
2438 &set_worksheet_line($ws_global, $row++, 0, [$key,$count], $f_default);
2443 # We are getting our data from mainlog files.
2444 my $user_pattern_index = 0;
2445 foreach $key (@user_descriptions) {
2446 printf $txt_fh ("$txt_format1\n",$key,$user_pattern_totals[$user_pattern_index]) if $txt_fh;
2447 printf $htm_fh ("$htm_format1\n",$key,$user_pattern_totals[$user_pattern_index]) if $htm_fh;
2450 &set_worksheet_line($ws_global, $row++, 0, [$key,$user_pattern_totals[$user_pattern_index]]);
2452 $user_pattern_index++;
2455 print $txt_fh "\n" if $txt_fh;
2456 print $htm_fh "</table>\n\n" if $htm_fh;
2462 if ($hist_opt > 0) {
2463 my $user_pattern_index = 0;
2464 foreach $key (@user_descriptions) {
2465 print_histogram($key, 'occurence', @{$user_pattern_interval_count[$user_pattern_index]});
2466 $user_pattern_index++;
2472 #######################################################################
2473 # print_transport();
2475 # print_transport();
2477 # Print totals by transport.
2478 #######################################################################
2479 sub print_transport {
2480 my(@chartdatanames);
2481 my(@chartdatavals_count);
2482 my(@chartdatavals_vol);
2483 no integer; #Lose this for charting the data.
2485 my $txt_format1 = " %-18s %6s %6d";
2486 my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%s</td><td align=\"right\">%d</td>";
2489 print $txt_fh "Deliveries by transport\n";
2490 print $txt_fh "-----------------------";
2491 print $txt_fh "\n Volume Messages\n";
2494 print $htm_fh "<hr><a name=\"transport\"></a><h2>Deliveries by Transport</h2>\n";
2495 print $htm_fh "<table border=0 width=\"100%\">\n";
2496 print $htm_fh "<tr><td>\n";
2497 print $htm_fh "<table border=1>\n";
2498 print $htm_fh "<tr><th> </th><th>Volume</th><th>Messages</th>\n";
2501 $ws_global->write($row++, $col, "Deliveries by transport", $f_header2);
2502 &set_worksheet_line($ws_global, $row++, 1, ["Volume", "Messages"], $f_headertab);
2506 if ($merge_reports) {
2507 # We are getting our data from previous reports.
2508 foreach $key (sort keys %{$report_totals{transport}}) {
2509 my $count = get_report_total($report_totals{transport}{$key},'Messages');
2510 my @content=($key, volume_rounded($report_totals{transport}{$key}{Volume},
2511 $report_totals{transport}{$key}{'Volume-gigs'}), $count);
2512 push(@chartdatanames, $key);
2513 push(@chartdatavals_count, $count);
2514 push(@chartdatavals_vol, $report_totals{transport}{$key}{'Volume-gigs'}*$gig + $report_totals{transport}{$key}{Volume} );
2515 printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
2516 printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
2518 &set_worksheet_line($ws_global, $row++, 0, \@content, $f_default);
2523 # We are getting our data from mainlog files.
2524 foreach $key (sort keys %transported_data) {
2525 my @content=($key, volume_rounded($transported_data{$key},$transported_data_gigs{$key}),
2526 $transported_count{$key});
2527 push(@chartdatanames, $key);
2528 push(@chartdatavals_count, $transported_count{$key});
2529 push(@chartdatavals_vol, $transported_data_gigs{$key}*$gig + $transported_data{$key});
2530 printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
2531 printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
2533 &set_worksheet_line($ws_global, $row++, 0, \@content);
2537 print $txt_fh "\n" if $txt_fh;
2539 print $htm_fh "</table>\n";
2540 print $htm_fh "</td><td>\n";
2541 if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_count > 0))
2543 # calculate the graph
2546 \@chartdatavals_count
2548 my $graph = GD::Graph::pie->new(200, 200);
2550 x_label => 'Transport',
2551 y_label => 'Messages',
2552 title => 'By count',
2554 my $gd = $graph->plot(\@data) or warn($graph->error);
2556 open(IMG, ">$chartdir/transports_count.png") or die "Could not write $chartdir/transports_count.png: $!\n";
2560 print $htm_fh "<img src=\"$chartrel/transports_count.png\">";
2563 print $htm_fh "</td><td>\n";
2565 if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_vol > 0)) {
2570 my $graph = GD::Graph::pie->new(200, 200);
2572 title => 'By volume',
2574 my $gd = $graph->plot(\@data) or warn($graph->error);
2576 open(IMG, ">$chartdir/transports_vol.png") or die "Could not write $chartdir/transports_count.png: $!\n";
2580 print $htm_fh "<img src=\"$chartrel/transports_vol.png\">";
2583 print $htm_fh "</td></tr></table>\n\n";
2593 #######################################################################
2598 # Print our totals by relay.
2599 #######################################################################
2601 my $row_print_relay=1;
2602 my $temp = "Relayed messages";
2603 print $htm_fh "<hr><a name=\"$temp\"></a><h2>$temp</h2>\n" if $htm_fh;
2604 if (scalar(keys %relayed) > 0 || $relayed_unshown > 0) {
2607 my $txt_format = "%7d %s\n => %s\n";
2608 my $htm_format = "<tr><td align=\"right\">%d</td><td>%s</td><td>%s</td>\n";
2610 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
2612 print $htm_fh "<table border=1>\n";
2613 print $htm_fh "<tr><th>Count</th><th>From</th><th>To</th>\n";
2616 $ws_relayed->write($row_print_relay++, $col, $temp, $f_header2);
2617 &set_worksheet_line($ws_relayed, $row_print_relay++, 0, ["Count", "From", "To"], $f_headertab);
2622 foreach $key (sort keys %relayed) {
2623 my $count = $relayed{$key};
2626 my($one,$two) = split(/=> /, $key);
2627 my @content=($count, $one, $two);
2628 printf $txt_fh ($txt_format, @content) if $txt_fh;
2629 printf $htm_fh ($htm_format, @content) if $htm_fh;
2632 &set_worksheet_line($ws_relayed, $row_print_relay++, 0, \@content);
2637 print $htm_fh "</table>\n<p>\n" if $htm_fh;
2638 print $txt_fh "${spacing}Total: $shown (plus $relayed_unshown unshown)\n\n" if $txt_fh;
2639 print $htm_fh "${spacing}Total: $shown (plus $relayed_unshown unshown)\n\n" if $htm_fh;
2642 &set_worksheet_line($ws_relayed, $row_print_relay++, 0, [$shown, "Sum of shown" ]);
2643 &set_worksheet_line($ws_relayed, $row_print_relay++, 0, [$relayed_unshown, "unshown"]);
2648 print $txt_fh "No relayed messages\n-------------------\n\n" if $txt_fh;
2649 print $htm_fh "No relayed messages\n\n" if $htm_fh;
2659 #######################################################################
2664 # Print our errors. In HTML, we display them as a list rather than a table -
2665 # Netscape doesn't like large tables!
2666 #######################################################################
2668 my $total_errors = 0;
2671 if (scalar(keys %errors_count) != 0) {
2672 my $temp = "List of errors";
2673 my $htm_format = "<li>%d - %s\n";
2675 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
2677 print $htm_fh "<hr><a name=\"errors\"></a><h2>$temp</h2>\n";
2678 print $htm_fh "<ul><li><b>Count - Error</b>\n";
2682 $ws_errors->write($row++, 0, $temp, $f_header2);
2683 &set_worksheet_line($ws_errors, $row++, 0, ["Count", "Error"], $f_headertab);
2688 foreach $key (sort keys %errors_count) {
2691 $text =~ s/\s\s+/ /g; #Convert multiple spaces to a single space.
2692 $total_errors += $errors_count{$key};
2695 printf $txt_fh ("%5d ", $errors_count{$key});
2696 my $text_remaining = $text;
2697 while (length($text_remaining) > 65) {
2698 my($first,$rest) = $text_remaining =~ /(.{50}\S*)\s+(.+)/;
2700 printf $txt_fh ("%s\n\t ", $first);
2701 $text_remaining = $rest;
2703 printf $txt_fh ("%s\n\n", $text_remaining);
2708 #Translate HTML tag characters. Sergey Sholokh.
2709 $text =~ s/\</\<\;/g;
2710 $text =~ s/\>/\>\;/g;
2712 printf $htm_fh ($htm_format,$errors_count{$key},$text);
2716 &set_worksheet_line($ws_errors, $row++, 0, [$errors_count{$key},$text]);
2720 $temp = "Errors encountered: $total_errors";
2723 print $txt_fh $temp, "\n";
2724 print $txt_fh "-" x length($temp),"\n";
2727 print $htm_fh "</ul>\n<p>\n";
2728 print $htm_fh $temp, "\n";
2732 &set_worksheet_line($ws_errors, $row++, 0, [$total_errors, "Sum of Errors encountered"]);
2739 #######################################################################
2740 # parse_old_eximstat_reports();
2742 # parse_old_eximstat_reports($fh);
2744 # Parse old eximstat output so we can merge daily stats to weekly stats and weekly to monthly etc.
2746 # To test that the merging still works after changes, do something like the following.
2747 # All the diffs should produce no output.
2749 # options='-bydomain -byemail -byhost -byedomain'
2750 # options="$options -show_rt1,2,4 -show_dt 1,2,4"
2751 # options="$options -pattern 'Completed Messages' /Completed/"
2752 # options="$options -pattern 'Received Messages' /<=/"
2754 # ./eximstats $options mainlog > mainlog.txt
2755 # ./eximstats $options -merge mainlog.txt > mainlog.2.txt
2756 # diff mainlog.txt mainlog.2.txt
2758 # ./eximstats $options -html mainlog > mainlog.html
2759 # ./eximstats $options -merge -html mainlog.txt > mainlog.2.html
2760 # diff mainlog.html mainlog.2.html
2762 # ./eximstats $options -merge mainlog.html > mainlog.3.txt
2763 # diff mainlog.txt mainlog.3.txt
2765 # ./eximstats $options -merge -html mainlog.html > mainlog.3.html
2766 # diff mainlog.html mainlog.3.html
2768 # ./eximstats $options -nvr mainlog > mainlog.nvr.txt
2769 # ./eximstats $options -merge mainlog.nvr.txt > mainlog.4.txt
2770 # diff mainlog.txt mainlog.4.txt
2772 # # double_mainlog.txt should have twice the values that mainlog.txt has.
2773 # ./eximstats $options mainlog mainlog > double_mainlog.txt
2774 #######################################################################
2775 sub parse_old_eximstat_reports {
2778 my(%league_table_value_entered, %league_table_value_was_zero, %table_order);
2780 my(%user_pattern_index);
2781 my $user_pattern_index = 0;
2782 map {$user_pattern_index{$_} = $user_pattern_index++} @user_descriptions;
2783 my $user_pattern_keys = join('|', @user_descriptions);
2786 PARSE_OLD_REPORT_LINE:
2787 if (/Exim statistics from ([\d\-]+ [\d:]+(\s+[\+\-]\d+)?) to ([\d\-]+ [\d:]+(\s+[\+\-]\d+)?)/) {
2788 $begin = $1 if ($1 lt $begin);
2789 $end = $3 if ($3 gt $end);
2791 elsif (/Grand total summary/) {
2792 # Fill in $report_totals{Received|Delivered}{Volume|Messages|Hosts|Domains|...|Delayed|DelayedPercent|Failed|FailedPercent}
2795 $_ = html2txt($_); #Convert general HTML markup to text.
2796 s/At least one addr//g; #Another part of the HTML output we don't want.
2798 # TOTAL Volume Messages Hosts Domains Delayed Failed
2799 # Received 26MB 237 177 23 8 3.4% 28 11.8%
2800 # Delivered 13MB 233 99 88
2801 if (/TOTAL\s+(.*?)\s*$/) {
2802 @fields = split(/\s+/,$1);
2803 #Delayed and Failed have two columns each, so add the extra field names in.
2804 splice(@fields,-1,1,'DelayedPercent','Failed','FailedPercent');
2806 elsif (/(Received|Delivered)\s+(.*?)\s*$/) {
2807 print STDERR "Parsing $_" if $debug;
2808 add_to_totals($report_totals{$1},\@fields,$2);
2810 last if (/Delivered/); #Last line of this section.
2814 elsif (/User Specified Patterns/i) {
2815 #User Specified Patterns
2816 #-----------------------
2820 while (<$fh>) { last if (/Total/); } #Wait until we get the table headers.
2822 print STDERR "Parsing $_" if $debug;
2823 $_ = html2txt($_); #Convert general HTML markup to text.
2824 if (/^\s*(.*?)\s+(\d+)\s*$/) {
2825 $report_totals{patterns}{$1} = {} unless (defined $report_totals{patterns}{$1});
2826 add_to_totals($report_totals{patterns}{$1},['Total'],$2);
2828 last if (/^\s*$/); #Finished if we have a blank line.
2832 elsif (/(^|<h2>)($user_pattern_keys) per /o) {
2833 # Parse User defined pattern histograms if they exist.
2834 parse_histogram($fh, $user_pattern_interval_count[$user_pattern_index{$2}] );
2838 elsif (/Deliveries by transport/i) {
2839 #Deliveries by transport
2840 #-----------------------
2842 # :blackhole: 70KB 51
2843 # address_pipe 655KB 1
2846 while (<$fh>) { last if (/Volume/); } #Wait until we get the table headers.
2848 print STDERR "Parsing $_" if $debug;
2849 $_ = html2txt($_); #Convert general HTML markup to text.
2850 if (/(\S+)\s+(\d+\S*\s+\d+)/) {
2851 $report_totals{transport}{$1} = {} unless (defined $report_totals{transport}{$1});
2852 add_to_totals($report_totals{transport}{$1},['Volume','Messages'],$2);
2854 last if (/^\s*$/); #Finished if we have a blank line.
2857 elsif (/Messages received per/) {
2858 parse_histogram($fh, \@received_interval_count);
2860 elsif (/Deliveries per/) {
2861 parse_histogram($fh, \@delivered_interval_count);
2864 #elsif (/Time spent on the queue: (all messages|messages with at least one remote delivery)/) {
2865 elsif (/(Time spent on the queue|Delivery times|Receipt times): ((\S+) messages|messages with at least one remote delivery)((<[^>]*>)*\s*)$/) {
2866 #Time spent on the queue: all messages
2867 #-------------------------------------
2869 #Under 1m 217 91.9% 91.9%
2875 # Set a pointer to the queue bin so we can use the same code
2876 # block for both all messages and remote deliveries.
2877 #my $bin_aref = ($1 eq 'all messages') ? \@qt_all_bin : \@qt_remote_bin;
2878 my($bin_aref, $times_aref, $overflow_sref);
2879 if ($1 eq 'Time spent on the queue') {
2880 $times_aref = \@queue_times;
2881 if ($2 eq 'all messages') {
2882 $bin_aref = \@qt_all_bin;
2883 $overflow_sref = \$qt_all_overflow;
2886 $bin_aref = \@qt_remote_bin;
2887 $overflow_sref = \$qt_remote_overflow;
2890 elsif ($1 eq 'Delivery times') {
2891 $times_aref = \@delivery_times;
2892 if ($2 eq 'all messages') {
2893 $bin_aref = \@dt_all_bin;
2894 $overflow_sref = \$dt_all_overflow;
2897 $bin_aref = \@dt_remote_bin;
2898 $overflow_sref = \$dt_remote_overflow;
2902 unless (exists $rcpt_times_bin{$3}) {
2903 initialise_rcpt_times($3);
2905 $bin_aref = $rcpt_times_bin{$3};
2906 $times_aref = \@rcpt_times;
2907 $overflow_sref = \$rcpt_times_overflow{$3};
2911 my $reached_table = 0;
2913 $_ = html2txt($_); #Convert general HTML markup to text.
2914 $reached_table = 1 if (/^\s*Under/);
2915 next unless $reached_table;
2916 my $previous_seconds_on_queue = 0;
2917 if (/^\s*(Under|Over|)\s+(\d+[smhdw])\s+(\d+)/) {
2918 print STDERR "Parsing $_" if $debug;
2919 my($modifier,$formated_time,$count) = ($1,$2,$3);
2920 my $seconds = unformat_time($formated_time);
2921 my $time_on_queue = ($seconds + $previous_seconds_on_queue) / 2;
2922 $previous_seconds_on_queue = $seconds;
2923 $time_on_queue = $seconds * 2 if ($modifier eq 'Over');
2925 for ($i = 0; $i <= $#$times_aref; $i++) {
2926 if ($time_on_queue < $times_aref->[$i]) {
2927 $$bin_aref[$i] += $count;
2931 $$overflow_sref += $count if ($i > $#$times_aref);
2935 last; #Finished the table ?
2940 elsif (/Relayed messages/) {
2944 # 1 addr.domain.com [1.2.3.4] a.user@domain.com
2945 # => addr2.domain2.com [5.6.7.8] a2.user2@domain2.com
2947 #<tr><td align="right">1</td><td>addr.domain.com [1.2.3.4] a.user@domain.com </td><td>addr2.domain2.com [5.6.7.8] a2.user2@domain2.com</td>
2949 my $reached_table = 0;
2952 unless ($reached_table) {
2953 last if (/No relayed messages/);
2954 $reached_table = 1 if (/^\s*\d/ || />\d+</);
2955 next unless $reached_table;
2957 if (/>(\d+)<.td><td>(.*?) ?<.td><td>(.*?)</) {
2958 update_relayed($1,$2,$3);
2960 elsif (/^\s*(\d+)\s+(.*?)\s*$/) {
2961 ($count,$sender) = ($1,$2);
2963 elsif (/=>\s+(.*?)\s*$/) {
2964 update_relayed($count,$sender,$1);
2967 last; #Finished the table ?
2972 elsif (/Top (.*?) by (message count|volume)/) {
2973 #Top 50 sending hosts by message count
2974 #-------------------------------------
2977 # Could also have average values for HTML output.
2978 # 48 1468KB 30KB local
2980 my($category,$by_count_or_volume) = ($1,$2);
2982 #As we show 2 views of each table (by count and by volume),
2983 #most (but not all) entries will appear in both tables.
2984 #Set up a hash to record which entries we have already seen
2985 #and one to record which ones we are seeing for the first time.
2986 if ($by_count_or_volume =~ /count/) {
2987 undef %league_table_value_entered;
2988 undef %league_table_value_was_zero;
2992 #As this section processes multiple different table categories,
2993 #set up pointers to the hashes to be updated.
2994 my($count_href,$data_href,$data_gigs_href);
2995 if ($category =~ /local sender/) {
2996 $count_href = \%received_count_user;
2997 $data_href = \%received_data_user;
2998 $data_gigs_href = \%received_data_gigs_user;
3000 elsif ($category =~ /sending (\S+?)s?\b/) {
3001 #Top 50 sending (host|domain|email|edomain)s
3002 #Top sending (host|domain|email|edomain)
3003 $count_href = \%{$received_count{"\u$1"}};
3004 $data_href = \%{$received_data{"\u$1"}};
3005 $data_gigs_href = \%{$received_data_gigs{"\u$1"}};
3007 elsif ($category =~ /local destination/) {
3008 $count_href = \%delivered_count_user;
3009 $data_href = \%delivered_data_user;
3010 $data_gigs_href = \%delivered_data_gigs_user;
3012 elsif ($category =~ /(\S+) destination/) {
3013 #Top 50 (host|domain|email|edomain) destinations
3014 #Top (host|domain|email|edomain) destination
3015 $count_href = \%{$delivered_count{"\u$1"}};
3016 $data_href = \%{$delivered_data{"\u$1"}};
3017 $data_gigs_href = \%{$delivered_data_gigs{"\u$1"}};
3020 my $reached_table = 0;
3022 # Watch out for empty tables.
3023 goto PARSE_OLD_REPORT_LINE if (/<h2>/ or /^[a-zA-Z]/);
3025 $_ = html2txt($_); #Convert general HTML markup to text.
3028 $reached_table = 1 if (/^\s*\d/);
3029 next unless $reached_table;
3031 # Remove optional 'average value' column.
3032 s/^\s*(\d+)\s+(\S+)\s+(\d+(KB|MB|GB|\b)\s+)/$1 $2 /;
3034 if (/^\s*(\d+)\s+(\S+)\s*(.*?)\s*$/) {
3035 my($count,$rounded_volume,$entry) = ($1,$2,$3);
3036 #Note: $entry fields can be both null and can contain spaces.
3038 #Add the entry into the %table_order hash if it has a rounded volume (KB/MB/GB).
3039 push(@{$table_order{$rounded_volume}{$by_count_or_volume}},$entry) if ($rounded_volume =~ /\D/);
3041 unless ($league_table_value_entered{$entry}) {
3042 $league_table_value_entered{$entry} = 1;
3043 unless ($$count_href{$entry}) {
3044 $$count_href{$entry} = 0;
3045 $$data_href{$entry} = 0;
3046 $$data_gigs_href{$entry} = 0;
3047 $league_table_value_was_zero{$entry} = 1;
3050 $$count_href{$entry} += $count;
3051 #Add the rounded value to the data and data_gigs hashes.
3052 un_round($rounded_volume,\$$data_href{$entry},\$$data_gigs_href{$entry});
3053 print STDERR "$category by $by_count_or_volume: added $count,$rounded_volume to $entry\n" if $debug;
3056 else { #Finished the table ?
3057 if ($by_count_or_volume =~ /volume/) {
3058 #Add a few bytes to appropriate entries to preserve the order.
3060 my($rounded_volume);
3061 foreach $rounded_volume (keys %table_order) {
3062 #For each rounded volume, we want to create a list which has things
3063 #ordered from the volume table at the front, and additional things
3064 #from the count table ordered at the back.
3065 @{$table_order{$rounded_volume}{volume}} = () unless defined $table_order{$rounded_volume}{volume};
3066 @{$table_order{$rounded_volume}{'message count'}} = () unless defined $table_order{$rounded_volume}{'message count'};
3068 map {$mark{$_} = 1} @{$table_order{$rounded_volume}{volume}};
3069 @order = @{$table_order{$rounded_volume}{volume}};
3070 map {push(@order,$_)} grep(!$mark{$_},@{$table_order{$rounded_volume}{'message count'}});
3072 my $bonus_bytes = $#order;
3073 $bonus_bytes = 511 if ($bonus_bytes > 511); #Don't go over the half-K boundary!
3074 while (@order and ($bonus_bytes > 0)) {
3075 my $entry = shift(@order);
3076 if ($league_table_value_was_zero{$entry}) {
3077 $$data_href{$entry} += $bonus_bytes;
3078 print STDERR "$category by $by_count_or_volume: added $bonus_bytes bonus bytes to $entry\n" if $debug;
3089 elsif (/List of errors/) {
3093 # 1 07904931641@one2one.net R=external T=smtp: SMTP error
3094 # from remote mailer after RCPT TO:<07904931641@one2one.net>:
3095 # host mail.one2one.net [193.133.192.24]: 550 User unknown
3097 #<li>1 - ally.dufc@dunbar.org.uk R=external T=smtp: SMTP error from remote mailer after RCPT TO:<ally.dufc@dunbar.org.uk>: host mail.dunbar.org.uk [216.167.89.88]: 550 Unknown local part ally.dufc in <ally.dufc@dunbar.org.uk>
3100 my $reached_table = 0;
3101 my($count,$error,$blanks);
3103 $reached_table = 1 if (/^( *|<li>)(\d+)/);
3104 next unless $reached_table;
3106 s/^<li>(\d+) -/$1/; #Convert an HTML line to a text line.
3107 $_ = html2txt($_); #Convert general HTML markup to text.
3110 $error .= ' ' . $1; #Join a multiline error.
3112 elsif (/^\s*(\d+)\s+(.*)/) {
3114 #Finished with a previous multiline error so save it.
3115 $errors_count{$error} = 0 unless $errors_count{$error};
3116 $errors_count{$error} += $count;
3118 ($count,$error) = ($1,$2);
3120 elsif (/Errors encountered/) {
3122 #Finished the section, so save our stored last error.
3123 $errors_count{$error} = 0 unless $errors_count{$error};
3124 $errors_count{$error} += $count;
3134 #######################################################################
3135 # parse_histogram($fh, \@delivered_interval_count);
3136 # Parse a histogram into the provided array of counters.
3137 #######################################################################
3138 sub parse_histogram {
3139 my($fh, $counters_aref) = @_;
3141 # Messages received per hour (each dot is 2 messages)
3142 #---------------------------------------------------
3144 #00-01 106 .....................................................
3145 #01-02 103 ...................................................
3147 my $reached_table = 0;
3149 $reached_table = 1 if (/^00/);
3150 next unless $reached_table;
3151 print STDERR "Parsing $_" if $debug;
3152 if (/^(\d+):(\d+)\s+(\d+)/) { #hh:mm start time format ?
3153 $$counters_aref[($1*60 + $2)/$hist_interval] += $3 if $hist_opt;
3155 elsif (/^(\d+)-(\d+)\s+(\d+)/) { #hh-hh start-end time format ?
3156 $$counters_aref[($1*60)/$hist_interval] += $3 if $hist_opt;
3158 else { #Finished the table ?
3165 #######################################################################
3168 # update_relayed($count,$sender,$recipient);
3170 # Adds an entry into the %relayed hash. Currently only used when
3172 #######################################################################
3173 sub update_relayed {
3174 my($count,$sender,$recipient) = @_;
3176 #When generating the key, put in the 'H=' and 'A=' which can be used
3178 my $key = "H=$sender => H=$recipient";
3179 $key =~ s/ ([^=\s]+\@\S+|<>)/ A=$1/g;
3180 if (!defined $relay_pattern || $key !~ /$relay_pattern/o) {
3181 $relayed{$key} = 0 if !defined $relayed{$key};
3182 $relayed{$key} += $count;
3185 $relayed_unshown += $count;
3190 #######################################################################
3193 # add_to_totals(\%totals,\@keys,$values);
3195 # Given a line of space seperated values, add them into the provided hash using @keys
3198 # If the value contains a '%', then the value is set rather than added. Otherwise, we
3199 # convert the value to bytes and gigs. The gigs get added to I<Key>-gigs.
3200 #######################################################################
3202 my($totals_href,$keys_aref,$values) = @_;
3203 my(@values) = split(/\s+/,$values);
3204 my(@keys) = @$keys_aref; #Make a copy as we destroy the one we use.
3206 foreach $value (@values) {
3207 my $key = shift(@keys) or next;
3208 if ($value =~ /%/) {
3209 $$totals_href{$key} = $value;
3212 $$totals_href{$key} = 0 unless ($$totals_href{$key});
3213 $$totals_href{"$key-gigs"} = 0 unless ($$totals_href{"$key-gigs"});
3214 un_round($value, \$$totals_href{$key}, \$$totals_href{"$key-gigs"});
3215 print STDERR "Added $value to $key - $$totals_href{$key} , " . $$totals_href{"$key-gigs"} . "GB.\n" if $debug;
3220 #######################################################################
3221 # get_report_total();
3223 # $total = get_report_total(\%hash,$key);
3225 # If %hash contains values split into Units and Gigs, we calculate and return
3227 # $hash{$key} + 1024*1024*1024 * $hash{"${key}-gigs"}
3228 #######################################################################
3229 sub get_report_total {
3231 my($hash_ref,$key) = @_;
3232 if ($$hash_ref{"${key}-gigs"}) {
3233 return $$hash_ref{$key} + $gig * $$hash_ref{"${key}-gigs"};
3235 return $$hash_ref{$key} || 0;
3238 #######################################################################
3241 # $text_line = html2txt($html_line);
3243 # Convert a line from html to text. Currently we just convert HTML tags to spaces
3244 # and convert >, <, and tags back.
3245 #######################################################################
3249 # Convert HTML tags to spacing. Note that the reports may contain <Userid> and
3250 # <Userid@Domain> words, so explicitly specify the HTML tags we will remove
3251 # (the ones used by this program). If someone is careless enough to have their
3252 # Userid the same as an HTML tag, there's not much we can do about it.
3253 s/<\/?(html|head|title|body|h\d|ul|li|a\s+|table|tr|td|th|pre|hr|p|br)\b.*?>/ /g;
3255 s/\<\;/\</og; #Convert '<' to '<'.
3256 s/\>\;/\>/og; #Convert '>' to '>'.
3257 s/\ \;/ /og; #Convert ' ' to ' '.
3261 #######################################################################
3264 # $arg = get_next_arg();
3266 # Because eximstats arguments are often passed as variables,
3267 # we can't rely on shell parsing to deal with quotes. This
3268 # subroutine returns $ARGV[1] and does a shift. If $ARGV[1]
3269 # starts with a quote (' or "), and doesn't end in one, then
3270 # we append the next argument to it and shift again. We repeat
3271 # until we've got all of the argument.
3273 # This isn't perfect as all white space gets reduced to one space,
3274 # but it's as good as we can get! If it's esential that spacing
3275 # be preserved precisely, then you get that by not using shell
3277 #######################################################################
3280 my $matched_pattern = 0;
3282 $arg .= ' ' if $arg;
3283 $arg .= $ARGV[1]; shift(@ARGV);
3284 if ($arg !~ /^['"]/) {
3285 $matched_pattern = 1;
3288 if ($arg =~ s/^(['"])(.*)\1$/$2/) {
3289 $matched_pattern = 1;
3293 die "Mismatched argument quotes - <$arg>.\n" unless $matched_pattern;
3297 #######################################################################
3298 # set_worksheet_line($ws_global, $startrow, $startcol, \@content, $format);
3300 # set values to a sequence of cells in a row.
3302 #######################################################################
3303 sub set_worksheet_line {
3304 my ($worksheet, $row, $col, $content, $format) = @_;
3306 foreach my $token (@$content)
3308 $worksheet->write($row, $col++, $token, $format );
3313 #######################################################################
3314 # @rcpt_times = parse_time_list($string);
3316 # Parse a comma seperated list of time values in seconds given by
3317 # the user and fill an array.
3319 # Return a default list if $string is undefined.
3320 # Return () if $string eq '0'.
3321 #######################################################################
3322 sub parse_time_list {
3324 if (! defined $string) {
3325 return(60, 5*60, 15*60, 30*60, 60*60, 3*60*60, 6*60*60, 12*60*60, 24*60*60);
3327 my(@times) = split(/,/, $string);
3328 foreach my $q (@times) { $q = eval($q) + 0 }
3329 @times = sort { $a <=> $b } @times;
3330 @times = () if ($#times == 0 && $times[0] == 0);
3335 #######################################################################
3336 # initialise_rcpt_times($protocol);
3337 # Initialise an array of rcpt_times to 0 for the specified protocol.
3338 #######################################################################
3339 sub initialise_rcpt_times {
3341 for (my $i = 0; $i <= $#rcpt_times; ++$i) {
3342 $rcpt_times_bin{$protocol}[$i] = 0;
3344 $rcpt_times_overflow{$protocol} = 0;
3348 ##################################################
3350 ##################################################
3353 $last_timestamp = '';
3357 $show_transport = 1;
3359 $local_league_table = 1;
3360 $include_remote_users = 0;
3361 $include_original_destination = 0;
3363 $volume_rounding = 1;
3364 $localtime_offset = calculate_localtime_offset(); # PH/FANF
3367 $charts_option_specified = 0;
3371 @queue_times = parse_time_list();
3373 @delivery_times = ();
3376 $offset_seconds = 0;
3379 $row_league_table=1;
3383 my(%output_files); # What output files have been specified?
3387 while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-') {
3388 if ($ARGV[0] =~ /^\-h(\d+)$/) { $hist_opt = $1 }
3389 elsif ($ARGV[0] =~ /^\-ne$/) { $show_errors = 0 }
3390 elsif ($ARGV[0] =~ /^\-nr(.?)(.*)\1$/) {
3391 if ($1 eq "") { $show_relay = 0 } else { $relay_pattern = $2 }
3393 elsif ($ARGV[0] =~ /^\-q([,\d\+\-\*\/]+)$/) { @queue_times = parse_time_list($1) }
3394 elsif ($ARGV[0] =~ /^-nt$/) { $show_transport = 0 }
3395 elsif ($ARGV[0] =~ /^\-nt(.?)(.*)\1$/)
3397 if ($1 eq "") { $show_transport = 0 } else { $transport_pattern = $2 }
3399 elsif ($ARGV[0] =~ /^-t(\d+)$/) { $topcount = $1 }
3400 elsif ($ARGV[0] =~ /^-tnl$/) { $local_league_table = 0 }
3401 elsif ($ARGV[0] =~ /^-txt=?(\S*)$/) { $txt_fh = get_filehandle($1,\%output_files) }
3402 elsif ($ARGV[0] =~ /^-html=?(\S*)$/) { $htm_fh = get_filehandle($1,\%output_files) }
3403 elsif ($ARGV[0] =~ /^-xls=?(\S*)$/) {
3404 if ($HAVE_Spreadsheet_WriteExcel) {
3405 $xls_fh = get_filehandle($1,\%output_files);
3408 warn "WARNING: CPAN Module Spreadsheet::WriteExcel not installed. Obtain from www.cpan.org\n";
3411 elsif ($ARGV[0] =~ /^-merge$/) { $merge_reports = 1 }
3412 elsif ($ARGV[0] =~ /^-charts$/) {
3414 warn "WARNING: CPAN Module GD::Graph::pie not installed. Obtain from www.cpan.org\n" unless $HAVE_GD_Graph_pie;
3415 warn "WARNING: CPAN Module GD::Graph::linespoints not installed. Obtain from www.cpan.org\n" unless $HAVE_GD_Graph_linespoints;
3417 elsif ($ARGV[0] =~ /^-chartdir$/) { $chartdir = $ARGV[1]; shift; $charts_option_specified = 1; }
3418 elsif ($ARGV[0] =~ /^-chartrel$/) { $chartrel = $ARGV[1]; shift; $charts_option_specified = 1; }
3419 elsif ($ARGV[0] =~ /^-include_original_destination$/) { $include_original_destination = 1 }
3420 elsif ($ARGV[0] =~ /^-cache$/) { } #Not currently used.
3421 elsif ($ARGV[0] =~ /^-byhost$/) { $do_sender{Host} = 1 }
3422 elsif ($ARGV[0] =~ /^-bydomain$/) { $do_sender{Domain} = 1 }
3423 elsif ($ARGV[0] =~ /^-byemail$/) { $do_sender{Email} = 1 }
3424 elsif ($ARGV[0] =~ /^-byemaildomain$/) { $do_sender{Edomain} = 1 }
3425 elsif ($ARGV[0] =~ /^-byedomain$/) { $do_sender{Edomain} = 1 }
3426 elsif ($ARGV[0] =~ /^-nvr$/) { $volume_rounding = 0 }
3427 elsif ($ARGV[0] =~ /^-show_rt([,\d\+\-\*\/]+)?$/) { @rcpt_times = parse_time_list($1) }
3428 elsif ($ARGV[0] =~ /^-show_dt([,\d\+\-\*\/]+)?$/) { @delivery_times = parse_time_list($1) }
3429 elsif ($ARGV[0] =~ /^-d$/) { $debug = 1 }
3430 elsif ($ARGV[0] =~ /^--?h(elp)?$/){ help() }
3431 elsif ($ARGV[0] =~ /^-t_remote_users$/) { $include_remote_users = 1 }
3432 elsif ($ARGV[0] =~ /^-pattern$/)
3434 push(@user_descriptions,get_next_arg());
3435 push(@user_patterns,get_next_arg());
3437 elsif ($ARGV[0] =~ /^-utc$/)
3439 # We don't need this value if the log is in UTC.
3440 $localtime_offset = undef;
3444 print STDERR "Eximstats: Unknown or malformed option $ARGV[0]\n";
3450 # keep old default behaviour
3451 if (! ($xls_fh or $htm_fh or $txt_fh)) {
3455 # Check that all the charts options are specified.
3456 warn "-charts option not specified. Use -help for help.\n" if ($charts_option_specified && ! $charts);
3458 # Default to display tables by sending Host.
3459 $do_sender{Host} = 1 unless ($do_sender{Domain} || $do_sender{Email} || $do_sender{Edomain});
3461 # prepare xls Excel Workbook
3462 if (defined $xls_fh)
3465 # Create a new Excel workbook
3466 $workbook = Spreadsheet::WriteExcel->new($xls_fh);
3469 $ws_global = $workbook->addworksheet('Exim Statistik');
3470 # show $ws_global as initial sheet
3471 $ws_global->set_first_sheet();
3472 $ws_global->activate();
3475 $ws_relayed = $workbook->addworksheet('Relayed Messages');
3476 $ws_relayed->set_column(1, 2, 80);
3479 $ws_top50 = $workbook->addworksheet('Deliveries');
3482 $ws_errors = $workbook->addworksheet('Errors');
3487 $ws_global->set_column(0, 2, 20); # Columns B-D width set to 30
3488 $ws_global->set_column(3, 3, 15); # Columns B-D width set to 30
3489 $ws_global->set_column(4, 4, 25); # Columns B-D width set to 30
3492 $f_default = $workbook->add_format();
3494 $f_header1 = $workbook->add_format();
3495 $f_header1->set_bold();
3496 #$f_header1->set_color('red');
3497 $f_header1->set_size('15');
3498 $f_header1->set_valign();
3499 # $f_header1->set_align('center');
3500 # $ws_global->write($row++, 2, "Testing Headers 1", $f_header1);
3502 $f_header2 = $workbook->add_format();
3503 $f_header2->set_bold();
3504 $f_header2->set_size('12');
3505 $f_header2->set_valign();
3506 # $ws_global->write($row++, 2, "Testing Headers 2", $f_header2);
3508 $f_percent = $workbook->add_format();
3509 $f_percent->set_num_format('0.0%');
3511 $f_headertab = $workbook->add_format();
3512 $f_headertab->set_bold();
3513 $f_headertab->set_valign();
3514 # $ws_global->write($row++, 2, "Testing Headers tab", $f_headertab);
3519 # Initialise the queue/delivery/rcpt time counters.
3520 for (my $i = 0; $i <= $#queue_times; $i++) {
3521 $qt_all_bin[$i] = 0;
3522 $qt_remote_bin[$i] = 0;
3524 for (my $i = 0; $i <= $#delivery_times; $i++) {
3525 $dt_all_bin[$i] = 0;
3526 $dt_remote_bin[$i] = 0;
3528 initialise_rcpt_times('all');
3531 # Compute the number of slots for the histogram
3534 if ($hist_opt > 60 || 60 % $hist_opt != 0)
3536 print STDERR "Eximstats: -h must specify a factor of 60\n";
3539 $hist_interval = 60/$hist_opt; #Interval in minutes.
3540 $hist_number = (24*60)/$hist_interval; #Number of intervals per day.
3541 @received_interval_count = (0) x $hist_number;
3542 @delivered_interval_count = (0) x $hist_number;
3543 my $user_pattern_index = 0;
3544 for (my $user_pattern_index = 0; $user_pattern_index <= $#user_patterns; ++$user_pattern_index) {
3545 @{$user_pattern_interval_count[$user_pattern_index]} = (0) x $hist_number;
3547 @dt_all_bin = (0) x $hist_number;
3548 @dt_remote_bin = (0) x $hist_number;
3551 #$queue_unknown = 0;
3553 $total_received_data = 0;
3554 $total_received_data_gigs = 0;
3555 $total_received_count = 0;
3557 $total_delivered_data = 0;
3558 $total_delivered_data_gigs = 0;
3559 $total_delivered_count = 0;
3561 $qt_all_overflow = 0;
3562 $qt_remote_overflow = 0;
3563 $dt_all_overflow = 0;
3564 $dt_remote_overflow = 0;
3566 $relayed_unshown = 0;
3567 $message_errors = 0;
3568 $begin = "9999-99-99 99:99:99";
3569 $end = "0000-00-00 00:00:00";
3571 foreach $section ('Received','Delivered') {
3572 foreach $type ('Volume','Messages','Delayed','Failed','Hosts','Domains','Emails','Edomains') {
3573 $report_totals{$section}{$type} = 0;
3577 # Generate our parser.
3578 my $parser = generate_parser();
3583 # Scan the input files and collect the data
3584 foreach my $file (@ARGV) {
3585 if ($file =~ /\.gz/) {
3586 unless (open(FILE,"gunzip -c $file |")) {
3587 print STDERR "Failed to gunzip -c $file: $!";
3591 elsif ($file =~ /\.Z/) {
3592 unless (open(FILE,"uncompress -c $file |")) {
3593 print STDERR "Failed to uncompress -c $file: $!";
3598 unless (open(FILE,$file)) {
3599 print STDERR "Failed to read $file: $!";
3603 #Now parse the filehandle, updating the global variables.
3604 parse($parser,\*FILE);
3609 #No files provided. Parse STDIN, updating the global variables.
3610 parse($parser,\*STDIN);
3614 if ($begin eq "9999-99-99 99:99:99") {
3615 print STDERR "**** No valid log lines read\n";
3619 # Output our results.
3621 print_grandtotals();
3623 # Print counts of user specified patterns if required.
3624 print_user_patterns() if @user_patterns;
3626 # Print totals by transport if required.
3627 print_transport() if $show_transport;
3629 # Print the deliveries per interval as a histogram, unless configured not to.
3630 # First find the maximum in one interval and scale accordingly.
3631 if ($hist_opt > 0) {
3632 print_histogram("Messages received", 'message', @received_interval_count);
3633 print_histogram("Deliveries", 'delivery', @delivered_interval_count);
3636 # Print times on queue if required.
3637 if ($#queue_times >= 0) {
3638 print_duration_table("Time spent on the queue", "all messages", \@queue_times, \@qt_all_bin,$qt_all_overflow);
3639 print_duration_table("Time spent on the queue", "messages with at least one remote delivery", \@queue_times, \@qt_remote_bin,$qt_remote_overflow);
3642 # Print delivery times if required.
3643 if ($#delivery_times >= 0) {
3644 print_duration_table("Delivery times", "all messages", \@delivery_times, \@dt_all_bin,$dt_all_overflow);
3645 print_duration_table("Delivery times", "messages with at least one remote delivery", \@delivery_times, \@dt_remote_bin,$dt_remote_overflow);
3648 # Print rcpt times if required.
3649 if ($#rcpt_times >= 0) {
3650 foreach my $protocol ('all', grep(!/^all$/, sort keys %rcpt_times_bin)) {
3651 print_duration_table("Receipt times", "$protocol messages", \@rcpt_times, $rcpt_times_bin{$protocol}, $rcpt_times_overflow{$protocol});
3655 # Print relay information if required.
3656 print_relay() if $show_relay;
3658 # Print the league tables, if topcount isn't zero.
3659 if ($topcount > 0) {
3660 foreach ('Host','Domain','Email','Edomain') {
3661 next unless $do_sender{$_};
3662 print_league_table("sending \l$_", $received_count{$_}, $received_data{$_},$received_data_gigs{$_});
3665 print_league_table("local sender", \%received_count_user,
3666 \%received_data_user,\%received_data_gigs_user) if ($local_league_table || $include_remote_users);
3667 foreach ('Host','Domain','Email','Edomain') {
3668 next unless $do_sender{$_};
3669 print_league_table("\l$_ destination", $delivered_count{$_}, $delivered_data{$_},$delivered_data_gigs{$_});
3671 print_league_table("local destination", \%delivered_count_user,
3672 \%delivered_data_user,\%delivered_data_gigs_user) if ($local_league_table || $include_remote_users);
3675 # Print the error statistics if required.
3676 print_errors() if $show_errors;
3678 print $htm_fh "</body>\n</html>\n" if $htm_fh;
3681 $txt_fh->close if $txt_fh;
3682 $htm_fh->close if $htm_fh;
3685 # close Excel Workbook
3686 $ws_global->set_first_sheet();
3687 # FIXME: whyever - activate does not work :-/
3688 $ws_global->activate();