Updated to eximstats version 1.42
[exim.git] / src / src / eximstats.src
1 #!PERL_COMMAND -w
2 # $Cambridge: exim/src/src/eximstats.src,v 1.8 2005/06/29 15:35:09 steve Exp $
3
4 # Copyright (c) 2001 University of Cambridge.
5 # See the file NOTICE for conditions of use and distribution.
6
7 # Perl script to generate statistics from one or more Exim log files.
8
9 # Usage: eximstats [<options>] <log file> <log file> ...
10
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
19 #               information.
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
23 #               line.
24 #             Show total number of errors.
25 #             Add count and percentage of messages with errors to Received
26 #               line.
27 #             Add information about relaying and -nr to suppress it.
28 # 1997-02-03  Merged in some of the things Nigel Metheringham had done:
29 #               Re-worded headings
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;
51 #               ensure it is zero.
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
54 #               bug!
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
59 #               incorrect result.
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
67 #               on large sites.
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.
78 #
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.
82 #
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
88 #               value unset.
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.
95 #
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.
100 #
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.
104 #
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!
109 #
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.
114 #
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;
123 #
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.
127 #
128 # 2002-09-05  V1.22 Steve Campbell
129 #             Fixed a perl 5.005 incompatibility problem ('our' variables).
130 #
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.
134
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.
140 #
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.
147 #
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.
154 #
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.
160 #
161 # 2003-03-13  V1.27 Steve Campbell
162 #             Replaced border attributes with 'border=1', as recommended by
163 #             Bernard Massot.
164 #
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)...
173 #
174 # 2003-11-06  V1.29 Steve Campbell
175 #             Added the '-pattern "Description" "/pattern/"' option.
176 #
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.
180 #
181 # 2004-02-20  V1.31 Andrea Balzi
182 #             Only show the Local Sender/Destination links if the tables exist.
183 #
184 # 2004-07-05  V1.32 Steve Campbell
185 #             Fix '-merge -h0' divide by zero error.
186 #
187 # 2004-07-15  V1.33 Steve Campbell
188 #             Documentation update - I've converted the subroutine
189 #             documentation from POD to comments.
190 #
191 # 2004-12-10  V1.34 Steve Campbell
192 #             Eximstats can now parse syslog lines as well as mainlog lines.
193 #
194 # 2004-12-20  V1.35 Wouter Verhelst
195 #             Pie charts by volume were actually generated by count. Fixed.
196 #
197 # 2005-02-07  V1.36 Gregor Herrmann / Steve Campbell
198 #             Added average sizes to HTML Top tables.
199 #
200 # 2005-04-26  V1.37 Frank Heydlauf
201 #             Added -xls and the ability to specify output files.
202 #
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.
208 #
209 # 2005-05-27  V1.39 Steve Campbell
210 #             Added the -include_original_destination flag
211 #             Removed tabs and trailing whitespace.
212 #
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.
218 #
219 # 2005-06-15  V1.41 Steve Campbell
220 #             Added the -show_rt<list> flag.
221 #             Added the -show_dt<list> flag.
222 #
223 # 2005-06-24  V1.42 Steve Campbell
224 #             Added Histograms for user specified patterns.
225 #
226 #
227 # For documentation on the logfile format, see
228 # http://www.exim.org/exim-html-4.50/doc/html/spec_48.html#IX2793
229
230 =head1 NAME
231
232 eximstats - generates statistics from Exim mainlog or syslog files.
233
234 =head1 SYNOPSIS
235
236  eximstats [Output] [Options] mainlog1 mainlog2 ...
237  eximstats -merge [Options] report.1.txt report.2.txt ... > weekly_report.txt
238
239 =head2 Output:
240
241 =over 4
242
243 =item B<-txt>
244
245 Output the results in plain text to STDOUT.
246
247 =item B<-txt>=I<filename>
248
249 Output the results in plain text. Filename '-' for STDOUT is accepted.
250
251 =item B<-html>
252
253 Output the results in HTML to STDOUT.
254
255 =item B<-html>=I<filename>
256
257 Output the results in HTML. Filename '-' for STDOUT is accepted.
258
259 =item B<-xls>
260
261 Output the results in Excel compatible Format to STDOUT.
262 Requires the Spreadsheet::WriteExcel CPAN module.
263
264 =item B<-xls>=I<filename>
265
266 Output the results in Excel compatible format. Filename '-' for STDOUT is accepted.
267
268
269 =back
270
271 =head2 Options:
272
273 =over 4
274
275 =item B<-h>I<number>
276
277 histogram divisions per hour. The default is 1, and
278 0 suppresses histograms. Valid values are:
279
280 0, 1, 2, 3, 5, 10, 15, 20, 30 or 60.
281
282 =item B<-ne>
283
284 Don't display error information.
285
286 =item B<-nr>
287
288 Don't display relaying information.
289
290 =item B<-nr>I</pattern/>
291
292 Don't display relaying information that matches.
293
294 =item B<-nt>
295
296 Don't display transport information.
297
298 =item B<-nt>I</pattern/>
299
300 Don't display transport information that matches
301
302 =item B<-q>I<list>
303
304 List of times for queuing information single 0 item suppresses.
305
306 =item B<-t>I<number>
307
308 Display top <number> sources/destinations
309 default is 50, 0 suppresses top listing.
310
311 =item B<-tnl>
312
313 Omit local sources/destinations in top listing.
314
315 =item B<-t_remote_users>
316
317 Include remote users in the top source/destination listings.
318
319 =item B<-include_original_destination>
320
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.
324
325 =item B<-show_dt>I<list>
326
327 Show the delivery times (B<DT>)for all the messages.
328
329 Exim must have been configured to use the +delivery_time logging option
330 for this option to work.
331
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.
335
336 =item B<-show_rt>I<list>
337
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.
341
342 Exim must have been configured to use the +queue_time_overall logging option
343 for this option to work.
344
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.
348
349 =item B<-byhost>
350
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.
354
355 =item B<-bydomain>
356
357 Show results by sending domain.
358 May be combined with B<-byhost> and/or B<-byemail> and/or B<-byedomain>.
359
360 =item B<-byemail>
361
362 Show results by sender's email address.
363 May be combined with B<-byhost> and/or B<-bydomain> and/or B<-byedomain>.
364
365 =item B<-byemaildomain> or B<-byedomain>
366
367 Show results by sender's email domain.
368 May be combined with B<-byhost> and/or B<-bydomain> and/or B<-byemail>.
369
370 =item B<-pattern> I<Description> I</Pattern/>
371
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:
374
375  -pattern 'Refused connections' '/refused connection/'
376
377
378 =item B<-merge>
379
380 This option allows eximstats to merge old eximstat reports together. Eg:
381
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
391
392 =over 4
393
394 =item *
395
396 You can merge text or html reports and output the results as text or html.
397
398 =item *
399
400 You can use all the normal eximstat output options, but only data
401 included in the original reports can be shown!
402
403 =item *
404
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.
407
408 =item *
409
410 The order of items in the top I<n> lists may vary when the data volumes
411 round to the same value.
412
413 =back
414
415 =item B<-charts>
416
417 Create graphical charts to be displayed in HTML output.
418 Only valid in combination with I<-html>.
419
420 This requires the following modules which can be obtained
421 from http://www.cpan.org/modules/01modules.index.html
422
423 =over 4
424
425 =item GD
426
427 =item GDTextUtil
428
429 =item GDGraph
430
431 =back
432
433 To install these, download and unpack them, then use the normal perl installation procedure:
434
435  perl Makefile.PL
436  make
437  make test
438  make install
439
440 =item B<-chartdir>I <dir>
441
442 Create the charts in the directory <dir>
443
444 =item B<-chartrel>I <dir>
445
446 Specify the relative directory for the "img src=" tags from where to include
447 the charts
448
449 =item B<-d>
450
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
453 title!
454
455 =back
456
457 =head1 DESCRIPTION
458
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.
464
465 =head1 AUTHOR
466
467 There is a web site at http://www.exim.org - this contains details of the
468 mailing list exim-users@exim.org.
469
470 =head1 TO DO
471
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...
476
477 Merging of xls files is not (yet) possible. Be free to implement :)
478
479 =cut
480
481 use integer;
482 use strict;
483 use IO::File;
484
485 # use Time::Local;  # PH/FANF
486 use POSIX;
487
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;
495
496
497 ##################################################
498 #             Static data                        #
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);
505
506
507 @tab62 =
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
514   59,60,61);                            # x-z
515
516 @days_per_month = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
517 $gig     = 1024 * 1024 * 1024;
518 $VERSION = '1.42';
519
520 # How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
521 $COLUMN_WIDTHS = 8;
522
523 $MINUTE = 60;
524 $HOUR   = 60 * $MINUTE;
525 $DAY    = 24 * $HOUR;
526 $WEEK   =  7 * $DAY;
527
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
544
545 # Output FileHandles
546 use vars qw($txt_fh $htm_fh $xls_fh);
547
548 $ntopchart = 5;
549
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);
559
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 ?
563
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);
580
581 use vars qw(%report_totals);
582
583 # Enumerations
584 use vars qw($SIZE $FROM_HOST $FROM_ADDRESS $ARRIVAL_TIME $REMOTE_DELIVERED $PROTOCOL);
585 use vars qw($DELAYED $HAD_ERROR);
586 $SIZE             = 0;
587 $FROM_HOST        = 1;
588 $FROM_ADDRESS     = 2;
589 $ARRIVAL_TIME     = 3;
590 $REMOTE_DELIVERED = 4;
591 $DELAYED          = 5;
592 $HAD_ERROR        = 6;
593 $PROTOCOL         = 7;
594
595
596
597 ##################################################
598 #                   Subroutines                  #
599 ##################################################
600
601 #######################################################################
602 # get_filehandle($file,\%output_files);
603 # Return a filehandle writing to $file.
604 #
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 #######################################################################
608 sub get_filehandle {
609   my($file,$output_files_href) = @_;
610
611   $file = '-' if ($file eq '');
612
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;
616   }
617
618   if ($file eq '-') {
619     return \*STDOUT;
620   }
621
622   if (-e $file) {
623     unlink $file or die "Failed to rm $file: $!";
624   }
625
626   my $fh = new IO::File $file, O_WRONLY|O_CREAT|O_EXCL;
627   die "new IO::File $file failed: $!" unless (defined $fh);
628   return $fh;
629 }
630
631
632 #######################################################################
633 # volume_rounded();
634 #
635 # $rounded_volume = volume_rounded($bytes,$gigabytes);
636 #
637 # Given a data size in bytes, round it to KB, MB, or GB
638 # as appropriate.
639 #
640 # Eg 12000 => 12KB, 15000000 => 14GB, etc.
641 #
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 #######################################################################
646 sub volume_rounded {
647   my($x,$g) = @_;
648   $x = 0 unless $x;
649   $g = 0 unless $g;
650   my($rounded);
651
652   while ($x > $gig) {
653     $g++;
654     $x -= $gig;
655   }
656
657   if ($volume_rounding) {
658     # Values < 1 GB
659     if ($g <= 0) {
660       if ($x < 10000) {
661         $rounded = sprintf("%6d", $x);
662       }
663       elsif ($x < 10000000) {
664         $rounded = sprintf("%4dKB", ($x + 512)/1024);
665       }
666       else {
667         $rounded = sprintf("%4dMB", ($x + 512*1024)/(1024*1024));
668       }
669     }
670     # Values between 1GB and 10GB are printed in MB
671     elsif ($g < 10) {
672       $rounded = sprintf("%4dMB", ($g * 1024) + ($x + 512*1024)/(1024*1024));
673     }
674     else {
675       # Handle values over 10GB
676       $rounded = sprintf("%4dGB", $g + ($x + $gig/2)/$gig);
677     }
678   }
679   else {
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);
686     no integer;
687     $rounded = sprintf("%.0f", ($g * $gig) + $x);
688   }
689
690   return $rounded;
691 }
692
693
694 #######################################################################
695 # un_round();
696 #
697 #  un_round($rounded_volume,\$bytes,\$gigabytes);
698 #
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.
702 #
703 # Given a data size in bytes, round it to KB, MB, or GB
704 # as appropriate.
705 #
706 # EG: 500 => (500,0), 14GB => (0,14), etc.
707 #######################################################################
708 sub un_round {
709   my($rounded,$bytes_sref,$gigabytes_sref) = @_;
710
711   if ($rounded =~ /(\d+)GB/) {
712     $$gigabytes_sref += $1;
713   }
714   elsif ($rounded =~ /(\d+)MB/) {
715     $$gigabytes_sref +=   $1 / 1024;
716     $$bytes_sref     += (($1 % 1024 ) * 1024 * 1024);
717   }
718   elsif ($rounded =~ /(\d+)KB/) {
719     $$gigabytes_sref +=  $1 / (1024 * 1024);
720     $$bytes_sref     += ($1 % (1024 * 1024) * 1024);
721   }
722   elsif ($rounded =~ /(\d+)/) {
723     # We need to turn off integer in case we are merging an -nvr report.
724     no integer;
725     $$gigabytes_sref += int($1 / $gig);
726     $$bytes_sref     += $1 % $gig;
727   }
728
729   #Now reduce the bytes down to less than 1GB.
730   add_volume($bytes_sref,$gigabytes_sref,0) if ($$bytes_sref > $gig);
731 }
732
733
734 #######################################################################
735 # add_volume();
736 #
737 #   add_volume(\$bytes,\$gigs,$size);
738 #
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 #######################################################################
743 sub add_volume {
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) {
749     $$gigs_ref++;
750     $$bytes_ref -= $gig;
751   }
752 }
753
754
755 #######################################################################
756 # format_time();
757 #
758 #  $formatted_time = format_time($seconds);
759 #
760 # Given a time in seconds, break it down into
761 # weeks, days, hours, minutes, and seconds.
762 #
763 # Eg 12005 => 3h20m5s
764 #######################################################################
765 sub format_time {
766 my($t) = pop @_;
767 my($s) = $t % 60;
768 $t /= 60;
769 my($m) = $t % 60;
770 $t /= 60;
771 my($h) = $t % 24;
772 $t /= 24;
773 my($d) = $t % 7;
774 my($w) = $t/7;
775 my($p) = "";
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 "";
781 $p;
782 }
783
784
785 #######################################################################
786 #  unformat_time();
787 #
788 #  $seconds = unformat_time($formatted_time);
789 #
790 # Given a time in weeks, days, hours, minutes, or seconds, convert it to seconds.
791 #
792 # Eg 3h20m5s => 12005
793 #######################################################################
794 sub unformat_time {
795   my($formated_time) = pop @_;
796   my $time = 0;
797
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');
804   }
805   $time;
806 }
807
808
809 #######################################################################
810 # seconds();
811 #
812 #  $time = seconds($timestamp);
813 #
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
822 # IDs, which is UTC.
823 #
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'.
827 #
828 # We also store the results of the last conversion done, and only
829 # recalculate if the date is different.
830 #
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 #######################################################################
835 sub seconds {
836   my($timestamp) = @_;
837
838   # Is the timestamp the same as the last one?
839   return $last_time if ($last_timestamp eq $timestamp);
840
841   return 0 unless ($timestamp =~ /^((\d{4})\-(\d\d)-(\d\d))\s(\d\d):(\d\d):(\d\d)( ([+-])(\d\d)(\d\d))?/o);
842
843   unless ($last_date eq $1) {
844     $last_date = $1;
845     my(@timestamp) = (0,0,0,$4,$3,$2);
846     $timestamp[5] -= 1900;
847     $timestamp[4]--;
848     $date_seconds = mktime(@timestamp);
849   }
850   my $time = $date_seconds + ($5 * 3600) + ($6 * 60) + $7;
851
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)) {
855     $last_offset = $8;
856     $offset_seconds = ($10 * 60 + $11) * 60;
857     $offset_seconds = -$offset_seconds if ($9 eq '-');
858   }
859
860
861   if (defined $7) {
862     #$time -= $this_offset;
863     $time -= $offset_seconds;
864   } elsif (defined $localtime_offset) {
865     $time -= $localtime_offset;
866   }
867
868   # Store the last timestamp received.
869   $last_timestamp = $timestamp;
870   $last_time      = $time;
871
872   $time;
873 }
874
875
876 #######################################################################
877 #  id_seconds();
878 #
879 #  $time = id_seconds($message_id);
880 #
881 # Given a message ID, convert it into a time() value.
882 #######################################################################
883 sub id_seconds {
884 my($sub_id) = substr((pop @_), 0, 6);
885 my($s) = 0;
886 my(@c) = split(//, $sub_id);
887 while($#c >= 0) { $s = $s * 62 + $tab62[ord(shift @c) - ord('0')] }
888 $s;
889 }
890
891 #######################################################################
892 #  wdhms_seconds();
893 #
894 #  $seconds = wdhms_seconds($string);
895 #
896 # Convert a string in a week/day/hour/minute/second format (eg 4h10s)
897 # into seconds.
898 #######################################################################
899 sub wdhms_seconds {
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));
902   }
903   return undef;
904 }
905
906 #######################################################################
907 #  queue_time();
908 #
909 #  $queued = queue_time($completed_tod, $arrival_time, $id);
910 #
911 # Given the completed time of day and either the arrival time
912 # (preferred), or the message ID, calculate how long the message has
913 # been on the queue.
914 #
915 #######################################################################
916 sub queue_time {
917   my($completed_tod, $arrival_time, $id) = @_;
918
919   # Note: id_seconds() benchmarks as 42% slower than seconds()
920   # and computing the time accounts for a significant portion of
921   # the run time.
922   if (defined $arrival_time) {
923     return(seconds($completed_tod) - seconds($arrival_time));
924   }
925   else {
926     return(seconds($completed_tod) - id_seconds($id));
927   }
928 }
929
930
931 #######################################################################
932 #  calculate_localtime_offset();
933 #
934 #  $localtime_offset = calculate_localtime_offset();
935 #
936 # Calculate the the localtime offset from gmtime in seconds.
937 #
938 #  $localtime = time() + $localtime_offset.
939 #
940 # These are the same semantics as ISO 8601 and RFC 2822 timezone offsets.
941 # (West is negative, East is positive.)
942 #######################################################################
943
944 # $localtime = gmtime() + $localtime_offset.  OLD COMMENT
945 # This subroutine commented out as it's not currently in use.
946
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;
953 #  return $offset;
954 #}
955
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.
960   my $utc = time;
961   # mktime works on local time and gmtime works in UTC
962   my $local = mktime(gmtime($utc));
963   return $local - $utc;
964 }
965
966
967
968 #######################################################################
969 # print_duration_table();
970 #
971 #  print_duration_table($title, $message_type, \@times, \@values, $overflow);
972 #
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
981 #                  time.
982 #######################################################################
983 sub print_duration_table {
984 no integer;
985 my($title, $message_type, $times_aref, $values_aref, $overflow) = @_;
986 my(@chartdatanames);
987 my(@chartdatavals);
988
989 my $printed_one = 0;
990 my $cumulative_percent = 0;
991
992 my $queue_total = $overflow;
993 map {$queue_total += $_} @$values_aref;
994
995 my $temp = "$title: $message_type";
996
997
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";
1000
1001 # write header
1002 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
1003 if ($htm_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";
1009 }
1010 if ($xls_fh)
1011 {
1012
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);
1016 }
1017
1018
1019 for ($i = 0; $i <= $#$times_aref; ++$i) {
1020   if ($$values_aref[$i] > 0)
1021     {
1022     my $percent = ($values_aref->[$i] * 100)/$queue_total;
1023     $cumulative_percent += $percent;
1024
1025     my @content=($printed_one? "     " : "Under",
1026         format_time($times_aref->[$i]),
1027         $values_aref->[$i], $percent, $cumulative_percent);
1028
1029     if ($htm_fh) {
1030       printf $htm_fh ($htm_format, @content);
1031       if (!defined($values_aref->[$i])) {
1032         print $htm_fh "Not defined";
1033       }
1034     }
1035     if ($txt_fh) {
1036       printf $txt_fh ($txt_format, @content);
1037       if (!defined($times_aref->[$i])) {
1038         print $txt_fh "Not defined";
1039       }
1040     }
1041     if ($xls_fh)
1042     {
1043       no integer;
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);
1046
1047       if (!defined($times_aref->[$i])) {
1048         $col=0;
1049         $ws_global->write($row++, $col, "Not defined"  );
1050       }
1051     }
1052
1053     push(@chartdatanames,
1054       ($printed_one? "" : "Under") . format_time($times_aref->[$i]));
1055     push(@chartdatavals, $$values_aref[$i]);
1056     $printed_one = 1;
1057   }
1058 }
1059
1060 if ($overflow && $overflow > 0) {
1061   my $percent = ($overflow * 100)/$queue_total;
1062   $cumulative_percent += $percent;
1063
1064     my @content = ("Over ", format_time($times_aref->[-1]),
1065         $overflow, $percent, $cumulative_percent);
1066
1067     printf $txt_fh ($txt_format, @content) if $txt_fh;
1068     printf $htm_fh ($htm_format, @content) if $htm_fh;
1069     if ($xls_fh)
1070     {
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);
1073     }
1074
1075 }
1076
1077 push(@chartdatanames, "Over " . format_time($times_aref->[-1]));
1078 push(@chartdatavals, $overflow);
1079
1080 #printf("Unknown   %6d\n", $queue_unknown) if $queue_unknown > 0;
1081 if ($htm_fh) {
1082   print $htm_fh "</table>\n";
1083   print $htm_fh "</td><td>\n";
1084
1085   if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0)) {
1086     my @data = (
1087        \@chartdatanames,
1088        \@chartdatavals
1089     );
1090     my $graph = GD::Graph::pie->new(200, 200);
1091     my $pngname = "$title-$message_type.png";
1092     $pngname =~ s/[^\w\-\.]/_/;
1093
1094     my $graph_title = "$title ($message_type)";
1095     $graph->set(title => $graph_title) if (length($graph_title) < 21);
1096
1097     my $gd = $graph->plot(\@data) or warn($graph->error);
1098     if ($gd) {
1099       open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
1100       binmode IMG;
1101       print IMG $gd->png;
1102       close IMG;
1103       print $htm_fh "<img src=\"$chartrel/$pngname\">";
1104     }
1105   }
1106   print $htm_fh "</td></tr></table>\n";
1107 }
1108
1109 if ($xls_fh)
1110 {
1111   $row++;
1112 }
1113 print $txt_fh "\n" if $txt_fh;
1114 print $htm_fh "\n" if $htm_fh;
1115
1116 }
1117
1118
1119 #######################################################################
1120 # print_histogram();
1121 #
1122 #  print_histogram('Deliveries|Messages received|$pattern', $unit, @interval_count);
1123 #
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);
1130 my(@chartdatavals);
1131 my($maxd) = 0;
1132
1133 if (!$run_hist) # save first row of print_histogram for xls output
1134 {
1135   $row_hist = $row;
1136 }
1137 else
1138 {
1139   $row = $row_hist;
1140 }
1141
1142 for ($i = 0; $i < $hist_number; $i++)
1143   { $maxd = $interval_count[$i] if $interval_count[$i] > $maxd; }
1144
1145 my $scale = int(($maxd + 25)/50);
1146 $scale = 1 if $scale == 0;
1147
1148 if ($scale != 1) {
1149   if ($unit !~ s/y$/ies/) {
1150     $unit .= 's';
1151   }
1152 }
1153
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");
1158
1159 my $txt_htm_title = $title . " (each dot is $scale $unit)";
1160
1161 printf $txt_fh ("%s\n%s\n\n", $txt_htm_title, "-" x length($txt_htm_title)) if $txt_fh;
1162
1163 if ($htm_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";
1167 }
1168
1169 if ($xls_fh)
1170 {
1171   $title =~ s/Messages/Msg/ ;
1172   $ws_global->write($row++, $col_hist+1, $title, $f_headertab);
1173 }
1174
1175
1176 my $hour = 0;
1177 my $minutes = 0;
1178 for ($i = 0; $i < $hist_number; $i++)
1179 {
1180   my $c = $interval_count[$i];
1181
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.
1185
1186   my $temp;
1187   if ($hist_opt == 1)
1188   {
1189     $temp = sprintf("%02d-%02d", $hour, $hour + 1);
1190
1191     print $txt_fh $temp if $txt_fh;
1192     print $htm_fh $temp if $htm_fh;
1193
1194     if ($xls_fh)
1195     {
1196       if ($run_hist==0) # only on first run
1197       {
1198         &set_worksheet_line($ws_global, $row, 0, [$temp], $f_default);
1199       }
1200     }
1201
1202     push(@chartdatanames, $temp);
1203     $hour++;
1204   }
1205   else
1206   {
1207     if ($minutes == 0)
1208       { $temp = sprintf("%02d:%02d", $hour, $minutes) }
1209     else
1210       { $temp = sprintf("  :%02d", $minutes) }
1211
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
1215     {
1216       $temp = sprintf("%02d:%02d", $hour, $minutes);
1217       &set_worksheet_line($ws_global, $row, 0, [$temp], $f_default);
1218     }
1219
1220     push(@chartdatanames, $temp);
1221     $minutes += $hist_interval;
1222     if ($minutes >= 60)
1223       {
1224       $minutes = 0;
1225       $hour++;
1226       }
1227   }
1228   push(@chartdatavals, $c);
1229
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;
1232   if ($xls_fh)
1233   {
1234     &set_worksheet_line($ws_global, $row++, $col_hist+1, [$c], $f_default);
1235   }
1236
1237 } #end for
1238
1239 printf $txt_fh "\n" if $txt_fh;
1240 printf $htm_fh "\n" if $htm_fh;
1241
1242 if ($htm_fh)
1243 {
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
1248     my @data = (
1249        \@chartdatanames,
1250        \@chartdatavals
1251     );
1252     my $graph = GD::Graph::linespoints->new(300, 300);
1253     $graph->set(
1254         x_label           => 'Time',
1255         y_label           => 'Amount',
1256         title             => $text,
1257         x_labels_vertical => 1
1258     );
1259     my $pngname = "histogram_$text.png";
1260     $pngname =~ s/[^\w\._]/_/g;
1261
1262     my $gd = $graph->plot(\@data) or warn($graph->error);
1263     if ($gd) {
1264       open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
1265       binmode IMG;
1266       print IMG $gd->png;
1267       close IMG;
1268       print $htm_fh "<img src=\"$chartrel/$pngname\">";
1269     }
1270   }
1271   print $htm_fh "</td></tr></table>\n";
1272 }
1273
1274 $col_hist++; # where to continue next times
1275
1276 $row+=2;     # leave some space after history block
1277 $run_hist=1; # we have done this once or more
1278 }
1279
1280
1281
1282 #######################################################################
1283 # print_league_table();
1284 #
1285 #  print_league_table($league_table_type,\%message_count,\%message_data,\%message_data_gigs);
1286 #
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;
1298
1299 my $htm_format;
1300 my $txt_format = "%7d %10s   %s\n";
1301
1302 # write header
1303 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
1304 if ($htm_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";
1310
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";
1315 }
1316 if ($xls_fh)
1317 {
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 );
1320 }
1321
1322
1323 # write content
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)) {
1326
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});
1333   $data = $gigs = 0;
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);
1338
1339   # write content
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;
1342
1343   if ($htm_fh) {
1344     $htmlkey = $key;
1345     $htmlkey =~ s/>/\&gt\;/g;
1346     $htmlkey =~ s/</\&lt\;/g;
1347     printf $htm_fh ($htm_format, @content, $htmlkey);
1348   }
1349   if ($xls_fh)
1350   {
1351     &set_worksheet_line($ws_top50, $row_league_table++, 0, [@content, $key], $f_default);
1352   }
1353
1354   if (scalar @chartdatanames < $ntopchart)
1355   {
1356     push(@chartdatanames, $key);
1357     push(@chartdatavals, $$m_count{$key});
1358   }
1359   else
1360   {
1361     $chartotherval += $$m_count{$key};
1362   }
1363 }
1364
1365 push(@chartdatanames, "Other");
1366 push(@chartdatavals, $chartotherval);
1367
1368 print $txt_fh "\n" if $txt_fh;
1369 if ($htm_fh)
1370 {
1371   print $htm_fh "</table>\n";
1372   print $htm_fh "</td><td>\n";
1373   if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0))
1374     {
1375     # calculate the graph
1376     my @data = (
1377        \@chartdatanames,
1378        \@chartdatavals
1379     );
1380     my $graph = GD::Graph::pie->new(300, 300);
1381     $graph->set(
1382         x_label           => 'Name',
1383         y_label           => 'Amount',
1384         title             => 'By count',
1385     );
1386     my $gd = $graph->plot(\@data) or warn($graph->error);
1387     if ($gd) {
1388       my $temp = $text;
1389       $temp =~ s/ /_/g;
1390       open(IMG, ">$chartdir/${temp}_count.png") or die "Could not write $chartdir/${temp}_count.png: $!\n";
1391       binmode IMG;
1392       print IMG $gd->png;
1393       close IMG;
1394       print $htm_fh "<img src=\"$chartrel/${temp}_count.png\">";
1395     }
1396   }
1397   print $htm_fh "</td><td>\n";
1398   print $htm_fh "</td></tr></table>\n\n";
1399 }
1400 if ($xls_fh)
1401 {
1402   $row_league_table++;
1403 }
1404
1405
1406 # write header
1407
1408 $temp = "Top $name by volume";
1409
1410 printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
1411 if ($htm_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";
1417 }
1418 if ($xls_fh)
1419 {
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);
1422 }
1423
1424 @chartdatanames = ();
1425 @chartdatavals = ();
1426 $chartotherval = 0;
1427 my $use_gig = 0;
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}) {
1433     $use_gig = 1;
1434   }
1435
1436   $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
1437   $data = $gigs = 0;
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 );
1442
1443   # write content
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;
1446   if ($htm_fh) {
1447     $htmlkey = $key;
1448     $htmlkey =~ s/>/\&gt\;/g;
1449     $htmlkey =~ s/</\&lt\;/g;
1450     printf $htm_fh ($htm_format, @content, $htmlkey);
1451   }
1452   if ($xls_fh)
1453   {
1454     &set_worksheet_line($ws_top50, $row_league_table++, 0, [@content, $key], $f_default);
1455   }
1456
1457
1458   if (scalar @chartdatanames < $ntopchart) {
1459     if ($use_gig) {
1460       if ($$m_data_gigs{$key}) {
1461         push(@chartdatanames, $key);
1462         push(@chartdatavals, $$m_data_gigs{$key});
1463       }
1464     }
1465     else {
1466       push(@chartdatanames, $key);
1467       push(@chartdatavals, $$m_data{$key});
1468     }
1469   }
1470   else {
1471     $chartotherval += ($use_gig) ? $$m_data_gigs{$key} : $$m_data{$key};
1472   }
1473 }
1474 push(@chartdatanames, "Other");
1475 push(@chartdatavals, $chartotherval);
1476
1477 print $txt_fh "\n" if $txt_fh;
1478 if ($htm_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
1483     my @data = (
1484        \@chartdatanames,
1485        \@chartdatavals
1486     );
1487     my $graph = GD::Graph::pie->new(300, 300);
1488     $graph->set(
1489         x_label           => 'Name',
1490         y_label           => 'Volume' ,
1491         title             => 'By Volume',
1492     );
1493     my $gd = $graph->plot(\@data) or warn($graph->error);
1494     if ($gd) {
1495       $temp = $text;
1496       $temp =~ s/ /_/g;
1497       open(IMG, ">$chartdir/${temp}_volume.png") or die "Could not write $chartdir/${temp}_volume.png: $!\n";
1498       binmode IMG;
1499       print IMG $gd->png;
1500       close IMG;
1501       print $htm_fh "<img src=\"$chartrel/${temp}_volume.png\">";
1502     }
1503   }
1504   print $htm_fh "</td><td>\n";
1505   print $htm_fh "</td></tr></table>\n\n";
1506 }
1507 if ($xls_fh)
1508 {
1509   ++$row_league_table;
1510 }
1511
1512 }
1513
1514 #######################################################################
1515 # top_n_sort();
1516 #
1517 #   @sorted_keys = top_n_sort($n,$href1,$href2,$href3);
1518 #
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.
1522 #
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.
1527 #
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.
1531 #
1532 # We assume the values are > 0.
1533 #######################################################################
1534 sub top_n_sort {
1535   my($n,$href1,$href2,$href3) = @_;
1536
1537   # PH's original sort was:
1538   #
1539   # foreach $key (sort
1540   #               {
1541   #               $$m_count{$b}     <=> $$m_count{$a} ||
1542   #               $$m_data_gigs{$b} <=> $$m_data_gigs{$a}  ||
1543   #               $$m_data{$b}      <=> $$m_data{$a}  ||
1544   #               $a cmp $b
1545   #               }
1546   #             keys %{$m_count})
1547   #
1548
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);
1553   my $top_n_key = '';
1554   my $n_minus_1 = $n - 1;
1555   my $n_minus_2 = $n - 2;
1556
1557   # Pick out the top $n keys.
1558   my($key,$value1,$value2,$value3,$i,$comparison,$insert_position);
1559   while (($key,$value1) = each %$href1) {
1560
1561     #print STDERR "key $key ($value1,",$href2->{$key},",",$href3->{$key},") <=> ($minimum_value1,$minimum_value2,$minimum_value3)\n";
1562
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);
1570
1571     # As we will be using these values a few times, extract them into scalars.
1572     $value2 = $href2->{$key};
1573     $value3 = $href3->{$key};
1574
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;
1578
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.
1582     #
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
1588     # on the new key.
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
1596            )
1597          ) {
1598         $insert_position = $i;
1599         last;
1600       }
1601     }
1602
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);
1606
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};
1613     }
1614   }
1615
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));
1619 }
1620
1621
1622 #######################################################################
1623 # html_header();
1624 #
1625 #  $header = html_header($title);
1626 #
1627 # Print our HTML header and start the <body> block.
1628 #######################################################################
1629 sub html_header {
1630   my($title) = @_;
1631   my $text = << "EoText";
1632 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
1633 <html>
1634 <head>
1635 <meta http-equiv="Content-Type" content="text/html; charset=iso-8859-15">
1636 <title>$title</title>
1637 </head>
1638 <body bgcolor="white">
1639 <h1>$title</h1>
1640 EoText
1641   return $text;
1642 }
1643
1644
1645
1646 #######################################################################
1647 # help();
1648 #
1649 #  help();
1650 #
1651 # Display usage instructions and exit.
1652 #######################################################################
1653 sub help {
1654   print << "EoText";
1655
1656 eximstats Version $VERSION
1657
1658 Usage:
1659   eximstats [Output] [Options] mainlog1 mainlog2 ...
1660   eximstats -merge -html [Options] report.1.html ... > weekly_rep.html
1661
1662 Examples:
1663   eximstats -html=eximstats.html mainlog1 mainlog2 ...
1664   eximstats mainlog1 mainlog2 ... > report.txt
1665
1666 Parses exim mainlog or syslog files and generates a statistical analysis
1667 of the messages processed.
1668
1669 Valid output types are:
1670 -txt[=<file>]   plain text (default unless no other type is specified)
1671 -html[=<file>]  HTML
1672 -xls[=<file>]   Excel
1673 With no type and file given, defaults to -txt and STDOUT.
1674
1675 Valid options are:
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.
1693                 Eg -show_rt1,2,4,8.
1694
1695 -include_original_destination   show both the final and original
1696                 destinations in the results rather than just the final ones.
1697
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
1703
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/'
1708
1709 -merge          merge previously generated reports into a new report
1710
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 '.'
1717
1718 -d              Debug mode - dump the eval'ed parser onto STDERR.
1719
1720 EoText
1721
1722   exit 1;
1723 }
1724
1725
1726
1727 #######################################################################
1728 # generate_parser();
1729 #
1730 #  $parser = generate_parser();
1731 #
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.
1735 #
1736 # I've tested using study(), but this does not improve performance.
1737 #
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 {
1743   my $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);
1747   while (<$fh>) {
1748
1749     # Convert syslog lines to mainlog format.
1750     if (! /^\\d{4}/) {
1751       next unless s/^.*? exim\\b.*?: //;
1752     }
1753
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;
1756
1757     ($tod,$m_hour,$m_min) = ($1,$2,$3);
1758
1759     # PH
1760     my($extra) = defined($4)? 6 : 0;
1761     $id   = substr($_, 20 + $extra, 16);
1762     $flag = substr($_, 37 + $extra, 2);
1763 ';
1764
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;
1770   if ($_) {
1771     \$user_pattern_totals[$user_pattern_index]++ if $_;
1772     \$user_pattern_interval_count[$user_pattern_index][(\$m_hour*60 + \$m_min)/$hist_interval]++;
1773   }
1774 EoText
1775     $user_pattern_index++;
1776   }
1777
1778   $parser .= '
1779     next unless ($flag =~ /<=|=>|->|==|\\*\\*|Co/);
1780
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);
1784
1785     $_ = substr($_, 40 + $extra);  # PH
1786
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};
1791
1792
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
1796     # ignored.
1797     #IFDEF ($transport_pattern)
1798     if (/\\sT=(\\S+)/) {
1799        next if ($1 =~ /$transport_pattern/o) ;
1800     }
1801     #ENDIF ($transport_pattern)
1802
1803
1804     $host = "local";          #Host is local unless otherwise specified.
1805     $domain = "localdomain";  #Domain is localdomain unless otherwise specified.
1806
1807
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+)/) {
1813       $host = $1;
1814
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;
1820
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.
1827       }
1828       #ENDIF ($do_sender{Domain})
1829
1830     }
1831
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 : "";
1837       chomp($email);
1838       #ENDIF ($include_original_destination)
1839
1840       #IFNDEF ($include_original_destination)
1841       $email = (/^(\S+)/) ? $1 : "";
1842       #ENDIF ($include_original_destination)
1843     #ENDIF ($do_sender{Email})
1844
1845     #IFDEF ($do_sender{Edomain})
1846       #IFDEF ($include_original_destination)
1847       #$edomain = (/^(\S+) (<\S*?\\@(\S+)>)?/) ? $3 || $1 : "";
1848       $edomain = (/^(\S+ (<\S*?\\@(\S+?)>)?)/) ? $1 : "";
1849       chomp($edomain);
1850       lc($edomain);
1851       #ENDIF ($include_original_destination)
1852
1853       #IFNDEF ($include_original_destination)
1854       $edomain = (/^\S*?\\@(\S+)/) ? lc($1) : "";
1855       #ENDIF ($include_original_destination)
1856     #ENDIF ($do_sender{Edomain})
1857
1858     if ($tod lt $begin) {
1859       $begin = $tod;
1860     }
1861     elsif ($tod gt $end) {
1862       $end   = $tod;
1863     }
1864
1865
1866     if ($flag eq "<=") {
1867       $thissize = (/\\sS=(\\d+)( |$)/) ? $1 : 0;
1868       $message_aref->[$SIZE] = $thissize;
1869       $message_aref->[$PROTOCOL] = (/ P=(\S+)/) ? $1 : undef;
1870
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;
1878       }
1879       #ENDIF ($show_relay)
1880
1881       #IFDEF ($local_league_table || $include_remote_users)
1882         if (/\sU=(\\S+)/) {
1883           my $user = $1;
1884
1885           #IFDEF ($local_league_table && $include_remote_users)
1886           {                         #Store both local and remote users.
1887           #ENDIF ($local_league_table && $include_remote_users)
1888
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)
1892
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)
1896
1897             ++$received_count_user{$user};
1898             add_volume(\\$received_data_user{$user},\\$received_data_gigs_user{$user},$thissize);
1899           }
1900         }
1901       #ENDIF ($local_league_table || $include_remote_users)
1902
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})
1907
1908       #IFDEF ($do_sender{Domain})
1909         if ($domain) {
1910           ++$received_count{Domain}{$domain};
1911           add_volume(\\$received_data{Domain}{$domain},\\$received_data_gigs{Domain}{$domain},$thissize);
1912         }
1913       #ENDIF ($do_sender{Domain})
1914
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})
1919
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})
1924
1925       ++$total_received_count;
1926       add_volume(\\$total_received_data,\\$total_received_data_gigs,$thissize);
1927
1928       #IFDEF ($#queue_times >= 0 || $#rcpt_times >= 0)
1929         $message_aref->[$ARRIVAL_TIME] = $tod;
1930       #ENDIF ($#queue_times >= 0 || $#rcpt_times >= 0)
1931
1932       #IFDEF ($hist_opt > 0)
1933         $received_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
1934       #ENDIF ($hist_opt > 0)
1935     }
1936
1937     elsif ($flag eq "=>") {
1938       $size = $message_aref->[$SIZE] || 0;
1939       if ($host ne "local") {
1940         $message_aref->[$REMOTE_DELIVERED] = 1;
1941
1942
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
1948         # and last.
1949
1950         if (defined $message_aref->[$FROM_HOST]) {
1951           if (/^(\\S+)(?:\\s+\\([^)]\\))?\\s+<([^>]+)>/) {
1952             ($old,$new) = ($1,$2);
1953           }
1954           else {
1955             $old = $new = "";
1956           }
1957
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};
1964               ++$relayed{$key};
1965             }
1966             else {
1967               ++$relayed_unshown;
1968             }
1969           }
1970         }
1971         #ENDIF ($show_relay)
1972
1973       }
1974
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)
1979
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)
1983
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)
1987
1988           if (my($user) = split((/\\s</)? " <" : " ", $_)) {
1989             #IFDEF ($include_original_destination)
1990             {
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;
1997             }
1998             ++$delivered_count_user{$user};
1999             add_volume(\\$delivered_data_user{$user},\\$delivered_data_gigs_user{$user},$size);
2000           }
2001         }
2002       #ENDIF ($local_league_table || $include_remote_users)
2003
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})
2009         if ($domain) {
2010           ++$delivered_count{Domain}{$domain};
2011           add_volume(\\$delivered_data{Domain}{$domain},\\$delivered_data_gigs{Domain}{$domain},$size);
2012         }
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})
2022
2023       ++$total_delivered_count;
2024       add_volume(\\$total_delivered_data,\\$total_delivered_data_gigs,$size);
2025
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)
2031
2032       #IFDEF ($hist_opt > 0)
2033         $delivered_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
2034       #ENDIF ($hist_opt > 0)
2035
2036       #IFDEF ($#delivery_times > 0)
2037         if (/ DT=(\S+)/) {
2038           $seconds = wdhms_seconds($1);
2039           for ($i = 0; $i <= $#delivery_times; $i++) {
2040             if ($seconds < $delivery_times[$i]) {
2041               ++$dt_all_bin[$i];
2042               ++$dt_remote_bin[$i] if $message_aref->[$REMOTE_DELIVERED];
2043               last;
2044             }
2045           }
2046           if ($i > $#delivery_times) {
2047             ++$dt_all_overflow;
2048             ++$dt_remote_overflow if $message_aref->[$REMOTE_DELIVERED];
2049           }
2050         }
2051       #ENDIF ($#delivery_times > 0)
2052
2053     }
2054
2055     elsif ($flag eq "==" && defined($message_aref->[$SIZE]) && !defined($message_aref->[$DELAYED])) {
2056       ++$delayed_count;
2057       $message_aref->[$DELAYED] = 1;
2058     }
2059
2060     elsif ($flag eq "**") {
2061       if (defined ($message_aref->[$SIZE])) {
2062         unless (defined $message_aref->[$HAD_ERROR]) {
2063           ++$message_errors;
2064           $message_aref->[$HAD_ERROR] = 1;
2065         }
2066       }
2067
2068       #IFDEF ($show_errors)
2069         ++$errors_count{$_};
2070       #ENDIF ($show_errors)
2071
2072     }
2073
2074     elsif ($flag eq "Co") {
2075       #Completed?
2076       #IFDEF ($#queue_times >= 0)
2077         $queued = queue_time($tod, $message_aref->[$ARRIVAL_TIME], $id);
2078
2079         for ($i = 0; $i <= $#queue_times; $i++) {
2080           if ($queued < $queue_times[$i]) {
2081             ++$qt_all_bin[$i];
2082             ++$qt_remote_bin[$i] if $message_aref->[$REMOTE_DELIVERED];
2083             last;
2084           }
2085         }
2086         if ($i > $#queue_times) {
2087           ++$qt_all_overflow;
2088           ++$qt_remote_overflow if $message_aref->[$REMOTE_DELIVERED];
2089         }
2090       #ENDIF ($#queue_times >= 0)
2091
2092       #IFDEF ($#rcpt_times >= 0)
2093         if (/ QT=(\S+)/) {
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;
2100           my($protocol);
2101
2102           if (defined $message_aref->[$PROTOCOL]) {
2103             $protocol = $message_aref->[$PROTOCOL];
2104
2105             # Create the bin if its not already defined.
2106             unless (exists $rcpt_times_bin{$protocol}) {
2107               initialise_rcpt_times($protocol);
2108             }
2109           }
2110
2111
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;
2116               last;
2117             }
2118           }
2119
2120           if ($i > $#rcpt_times) {
2121             ++$rcpt_times_overflow{all};
2122             ++$rcpt_times_overflow{$protocol} if defined $protocol;
2123           }
2124         }
2125       #ENDIF ($#rcpt_times >= 0)
2126
2127       delete($messages{$id});
2128     }
2129   }';
2130
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;
2139     }
2140
2141     # Convert constants.
2142     while (/(\$[A-Z][A-Z_]*)\b/) {
2143       my $constant = eval $1;
2144       s/(\$[A-Z][A-Z_]*)\b/$constant/;
2145     }
2146
2147     $processed_parser .= $_."\n" unless $removing_lines;
2148
2149     if (/^\s*#\s*ENDIF\s*\((.*?)\)/i) {
2150       delete $defines_in_operation{$1};
2151       unless (keys %defines_in_operation) {
2152         $removing_lines = 0;
2153       }
2154     }
2155   }
2156   print STDERR "# START OF PARSER:$processed_parser\n# END OF PARSER\n\n" if $debug;
2157
2158   return $processed_parser;
2159 }
2160
2161
2162
2163 #######################################################################
2164 # parse();
2165 #
2166 #  parse($parser,\*FILEHANDLE);
2167 #
2168 # This subroutine accepts a parser and a filehandle from main and parses each
2169 # line. We store the results into global variables.
2170 #######################################################################
2171 sub parse {
2172   my($parser,$fh) = @_;
2173
2174   if ($merge_reports) {
2175     parse_old_eximstat_reports($fh);
2176   }
2177   else {
2178     eval $parser;
2179     die ($@) if $@;
2180   }
2181
2182 }
2183
2184
2185
2186 #######################################################################
2187 # print_header();
2188 #
2189 #  print_header();
2190 #
2191 # Print our headers and contents.
2192 #######################################################################
2193 sub print_header {
2194
2195
2196   my $title = "Exim statistics from $begin to $end";
2197
2198   print $txt_fh "\n$title\n" if $txt_fh;
2199   if ($htm_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;
2205     if ($hist_opt) {
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";
2208     }
2209
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";
2213     }
2214
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";
2218     }
2219
2220     if ($#rcpt_times >= 0) {
2221       print $htm_fh "<li><a href=\"#Receipt times all messages\">Receipt times</a>\n";
2222     }
2223
2224     print $htm_fh "<li><a href=\"#Relayed messages\">Relayed messages</a>\n" if $show_relay;
2225     if ($topcount) {
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";
2230       }
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";
2234       }
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";
2239       }
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";
2243       }
2244     }
2245     print $htm_fh "<li><a href=\"#errors\">List of errors</a>\n" if %errors_count;
2246     print $htm_fh "</ul>\n<hr>\n";
2247   }
2248   if ($xls_fh)
2249   {
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);
2252     $row+=2;
2253   }
2254 }
2255
2256
2257 #######################################################################
2258 # print_grandtotals();
2259 #
2260 #  print_grandtotals();
2261 #
2262 # Print the grand totals.
2263 #######################################################################
2264 sub print_grandtotals {
2265
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);
2271
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"));
2277     }
2278     else {
2279       push(@received_totals,scalar(keys %{$received_data{$_}}));
2280       push(@delivered_totals,scalar(keys %{$delivered_data{$_}}));
2281     }
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";
2286   }
2287
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>";
2292
2293   if ($txt_fh) {
2294     my $sender_spaces = " " x length($sender_txt_header);
2295     print $txt_fh "\n";
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";
2300   }
2301   if ($htm_fh) {
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";
2306   }
2307   if ($xls_fh)
2308   {
2309       $ws_global->write($row++, $col, "Grand total summary", $f_header2);
2310
2311       $row_tablehead = $row+1; # header-row of TOTALS table
2312
2313       &set_worksheet_line($ws_global, $row_tablehead, 0, ['Received', 'Delivered', 'TOTAL'], $f_headertab);
2314
2315       my @content= (
2316         "Volume",
2317         "Messages",
2318         $sender_txt_header,
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)"
2323       );
2324
2325       for (my $i=0; $i < scalar(@content); $i++)
2326       {
2327         $ws_global->write($row_tablehead+$i+1, 2, $content[$i], $f_default);
2328         $row++;
2329       }
2330       $row_max = $row_tablehead+scalar(@content)+2; # continue from this row
2331   }
2332
2333
2334
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');
2341   }
2342   else {
2343     $volume = volume_rounded($total_received_data, $total_received_data_gigs);
2344     $failed_count = $message_errors;
2345   }
2346
2347   {
2348     no integer;
2349
2350     my @content=(
2351         $volume,$total_received_count,
2352         @received_totals,
2353         $delayed_count,
2354         ($total_received_count) ? ($delayed_count*100/$total_received_count) : 0,
2355         $failed_count,
2356         ($total_received_count) ? ($failed_count*100/$total_received_count) : 0
2357     );
2358
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;
2361     if ($xls_fh)
2362     {
2363       $row = $row_tablehead+1;
2364       for (my $i=0; $i < scalar(@content); $i++)
2365       {
2366         if ($i == 4 || $i == 6) {
2367           $ws_global->write($row+$i, 0, $content[$i]/100, $f_percent);
2368         }
2369         else {
2370           $ws_global->write($row+$i, 0, $content[$i], $f_default);
2371         }
2372       }
2373     }
2374   }
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');
2378   }
2379   else {
2380     $volume = volume_rounded($total_delivered_data, $total_delivered_data_gigs);
2381   }
2382
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;
2387   if ($xls_fh)
2388   {
2389
2390       $row = $row_tablehead+1;
2391       for (my $i=0; $i < scalar(@content); $i++)
2392       {
2393         $ws_global->write($row+$i, 1, $content[$i], $f_default);
2394       }
2395       $row = $row_max;
2396   }
2397 }
2398
2399
2400 #######################################################################
2401 # print_user_patterns()
2402 #
2403 #  print_user_patterns();
2404 #
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>";
2410
2411   if ($txt_fh) {
2412     print $txt_fh "User Specified Patterns\n";
2413     print $txt_fh "-----------------------";
2414     print $txt_fh "\n                       Total\n";
2415   }
2416   if ($htm_fh) {
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>&nbsp;</th><th>Total</th>\n";
2422   }
2423   if ($xls_fh) {
2424       $ws_global->write($row++, $col, "User Specified Patterns", $f_header2);
2425       &set_worksheet_line($ws_global, $row++, 1, ["Total"], $f_headertab);
2426   }
2427
2428
2429   my($key);
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;
2436       if ($xls_fh)
2437       {
2438         &set_worksheet_line($ws_global, $row++, 0, [$key,$count], $f_default);
2439       }
2440     }
2441   }
2442   else {
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;
2448       if ($xls_fh)
2449       {
2450         &set_worksheet_line($ws_global, $row++, 0, [$key,$user_pattern_totals[$user_pattern_index]]);
2451       }
2452       $user_pattern_index++;
2453     }
2454   }
2455   print $txt_fh "\n" if $txt_fh;
2456   print $htm_fh "</table>\n\n" if $htm_fh;
2457   if ($xls_fh)
2458   {
2459     ++$row;
2460   }
2461
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++;
2467     }
2468   }
2469 }
2470
2471
2472 #######################################################################
2473 # print_transport();
2474 #
2475 #  print_transport();
2476 #
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.
2484
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>";
2487
2488   if ($txt_fh) {
2489     print $txt_fh "Deliveries by transport\n";
2490     print $txt_fh "-----------------------";
2491     print $txt_fh "\n                      Volume    Messages\n";
2492   }
2493   if ($htm_fh) {
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>&nbsp;</th><th>Volume</th><th>Messages</th>\n";
2499   }
2500   if ($xls_fh) {
2501     $ws_global->write($row++, $col, "Deliveries by transport", $f_header2);
2502     &set_worksheet_line($ws_global, $row++, 1, ["Volume", "Messages"], $f_headertab);
2503   }
2504
2505   my($key);
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;
2517       if ($xls_fh) {
2518         &set_worksheet_line($ws_global, $row++, 0, \@content, $f_default);
2519       }
2520     }
2521   }
2522   else {
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;
2532       if ($xls_fh) {
2533         &set_worksheet_line($ws_global, $row++, 0, \@content);
2534       }
2535     }
2536   }
2537   print $txt_fh "\n" if $txt_fh;
2538   if ($htm_fh) {
2539     print $htm_fh "</table>\n";
2540     print $htm_fh "</td><td>\n";
2541     if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_count > 0))
2542       {
2543       # calculate the graph
2544       my @data = (
2545          \@chartdatanames,
2546          \@chartdatavals_count
2547       );
2548       my $graph = GD::Graph::pie->new(200, 200);
2549       $graph->set(
2550           x_label           => 'Transport',
2551           y_label           => 'Messages',
2552           title             => 'By count',
2553       );
2554       my $gd = $graph->plot(\@data) or warn($graph->error);
2555       if ($gd) {
2556         open(IMG, ">$chartdir/transports_count.png") or die "Could not write $chartdir/transports_count.png: $!\n";
2557         binmode IMG;
2558         print IMG $gd->png;
2559         close IMG;
2560         print $htm_fh "<img src=\"$chartrel/transports_count.png\">";
2561       }
2562     }
2563     print $htm_fh "</td><td>\n";
2564
2565     if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_vol > 0)) {
2566       my @data = (
2567          \@chartdatanames,
2568          \@chartdatavals_vol
2569       );
2570       my $graph = GD::Graph::pie->new(200, 200);
2571       $graph->set(
2572           title             => 'By volume',
2573       );
2574       my $gd = $graph->plot(\@data) or warn($graph->error);
2575       if ($gd) {
2576         open(IMG, ">$chartdir/transports_vol.png") or die "Could not write $chartdir/transports_count.png: $!\n";
2577         binmode IMG;
2578         print IMG $gd->png;
2579         close IMG;
2580         print $htm_fh "<img src=\"$chartrel/transports_vol.png\">";
2581       }
2582     }
2583     print $htm_fh "</td></tr></table>\n\n";
2584   }
2585   if ($xls_fh) {
2586     $row++;
2587   }
2588
2589 }
2590
2591
2592
2593 #######################################################################
2594 # print_relay();
2595 #
2596 #  print_relay();
2597 #
2598 # Print our totals by relay.
2599 #######################################################################
2600 sub print_relay {
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) {
2605     my $shown = 0;
2606     my $spacing = "";
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";
2609
2610     printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
2611     if ($htm_fh) {
2612       print $htm_fh "<table border=1>\n";
2613       print $htm_fh "<tr><th>Count</th><th>From</th><th>To</th>\n";
2614     }
2615     if ($xls_fh) {
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);
2618     }
2619
2620
2621     my($key);
2622     foreach $key (sort keys %relayed) {
2623       my $count = $relayed{$key};
2624       $shown += $count;
2625       $key =~ s/[HA]=//g;
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;
2630       if ($xls_fh)
2631       {
2632         &set_worksheet_line($ws_relayed, $row_print_relay++, 0, \@content);
2633       }
2634       $spacing = "\n";
2635     }
2636
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;
2640     if ($xls_fh)
2641     {
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"]);
2644        $row_print_relay++;
2645     }
2646   }
2647   else {
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;
2650     if ($xls_fh)
2651     {
2652       $row_print_relay++;
2653     }
2654   }
2655 }
2656
2657
2658
2659 #######################################################################
2660 # print_errors();
2661 #
2662 #  print_errors();
2663 #
2664 # Print our errors. In HTML, we display them as a list rather than a table -
2665 # Netscape doesn't like large tables!
2666 #######################################################################
2667 sub print_errors {
2668   my $total_errors = 0;
2669   $row=1;
2670
2671   if (scalar(keys %errors_count) != 0) {
2672     my $temp = "List of errors";
2673     my $htm_format = "<li>%d - %s\n";
2674
2675     printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
2676     if ($htm_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";
2679     }
2680     if ($xls_fh)
2681     {
2682       $ws_errors->write($row++, 0, $temp, $f_header2);
2683       &set_worksheet_line($ws_errors, $row++, 0, ["Count", "Error"], $f_headertab);
2684     }
2685
2686
2687     my($key);
2688     foreach $key (sort keys %errors_count) {
2689       my $text = $key;
2690       chomp($text);
2691       $text =~ s/\s\s+/ /g;   #Convert multiple spaces to a single space.
2692       $total_errors += $errors_count{$key};
2693
2694       if ($txt_fh) {
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+(.+)/;
2699           last if !$first;
2700           printf $txt_fh ("%s\n\t    ", $first);
2701           $text_remaining = $rest;
2702         }
2703         printf $txt_fh ("%s\n\n", $text_remaining);
2704       }
2705
2706       if ($htm_fh) {
2707
2708         #Translate HTML tag characters. Sergey Sholokh.
2709         $text =~ s/\</\&lt\;/g;
2710         $text =~ s/\>/\&gt\;/g;
2711
2712         printf $htm_fh ($htm_format,$errors_count{$key},$text);
2713       }
2714       if ($xls_fh)
2715       {
2716         &set_worksheet_line($ws_errors, $row++, 0, [$errors_count{$key},$text]);
2717       }
2718     }
2719
2720     $temp = "Errors encountered: $total_errors";
2721
2722     if ($txt_fh) {
2723       print $txt_fh $temp, "\n";
2724       print $txt_fh "-" x length($temp),"\n";
2725     }
2726     if ($htm_fh) {
2727       print $htm_fh "</ul>\n<p>\n";
2728       print $htm_fh $temp, "\n";
2729     }
2730     if ($xls_fh)
2731     {
2732         &set_worksheet_line($ws_errors, $row++, 0, [$total_errors, "Sum of Errors encountered"]);
2733     }
2734   }
2735
2736 }
2737
2738
2739 #######################################################################
2740 # parse_old_eximstat_reports();
2741 #
2742 #  parse_old_eximstat_reports($fh);
2743 #
2744 # Parse old eximstat output so we can merge daily stats to weekly stats and weekly to monthly etc.
2745 #
2746 # To test that the merging still works after changes, do something like the following.
2747 # All the diffs should produce no output.
2748 #
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' /<=/"
2753 #
2754 #  ./eximstats $options mainlog > mainlog.txt
2755 #  ./eximstats $options -merge mainlog.txt > mainlog.2.txt
2756 #  diff mainlog.txt mainlog.2.txt
2757 #
2758 #  ./eximstats $options -html mainlog > mainlog.html
2759 #  ./eximstats $options -merge -html mainlog.txt  > mainlog.2.html
2760 #  diff mainlog.html mainlog.2.html
2761 #
2762 #  ./eximstats $options -merge mainlog.html > mainlog.3.txt
2763 #  diff mainlog.txt mainlog.3.txt
2764 #
2765 #  ./eximstats $options -merge -html mainlog.html > mainlog.3.html
2766 #  diff mainlog.html mainlog.3.html
2767 #
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
2771 #
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 {
2776   my($fh) = @_;
2777
2778   my(%league_table_value_entered, %league_table_value_was_zero, %table_order);
2779
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);
2784
2785   while (<$fh>) {
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);
2790     }
2791     elsif (/Grand total summary/) {
2792       # Fill in $report_totals{Received|Delivered}{Volume|Messages|Hosts|Domains|...|Delayed|DelayedPercent|Failed|FailedPercent}
2793       my(@fields);
2794       while (<$fh>) {
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.
2797
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');
2805         }
2806         elsif (/(Received|Delivered)\s+(.*?)\s*$/) {
2807           print STDERR "Parsing $_" if $debug;
2808           add_to_totals($report_totals{$1},\@fields,$2);
2809         }
2810         last if (/Delivered/);   #Last line of this section.
2811       }
2812     }
2813
2814     elsif (/User Specified Patterns/i) {
2815 #User Specified Patterns
2816 #-----------------------
2817 #                       Total
2818 #  Description             85
2819
2820       while (<$fh>) { last if (/Total/); }  #Wait until we get the table headers.
2821       while (<$fh>) {
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);
2827         }
2828         last if (/^\s*$/);              #Finished if we have a blank line.
2829       }
2830     }
2831
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}] );
2835     }
2836
2837
2838     elsif (/Deliveries by transport/i) {
2839 #Deliveries by transport
2840 #-----------------------
2841 #                      Volume    Messages
2842 #  :blackhole:           70KB          51
2843 #  address_pipe         655KB           1
2844 #  smtp                  11MB         151
2845
2846       while (<$fh>) { last if (/Volume/); }  #Wait until we get the table headers.
2847       while (<$fh>) {
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);
2853         }
2854         last if (/^\s*$/);              #Finished if we have a blank line.
2855       }
2856     }
2857     elsif (/Messages received per/) {
2858       parse_histogram($fh, \@received_interval_count);
2859     }
2860     elsif (/Deliveries per/) {
2861       parse_histogram($fh, \@delivered_interval_count);
2862     }
2863
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 #-------------------------------------
2868 #
2869 #Under   1m      217  91.9%   91.9%
2870 #        5m        2   0.8%   92.8%
2871 #        3h        8   3.4%   96.2%
2872 #        6h        7   3.0%   99.2%
2873 #       12h        2   0.8%  100.0%
2874
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;
2884         }
2885         else {
2886           $bin_aref = \@qt_remote_bin;
2887           $overflow_sref = \$qt_remote_overflow;
2888         }
2889       }
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;
2895         }
2896         else {
2897           $bin_aref = \@dt_remote_bin;
2898           $overflow_sref = \$dt_remote_overflow;
2899         }
2900       }
2901       else {
2902         unless (exists $rcpt_times_bin{$3}) {
2903           initialise_rcpt_times($3);
2904         }
2905         $bin_aref = $rcpt_times_bin{$3};
2906         $times_aref = \@rcpt_times;
2907         $overflow_sref = \$rcpt_times_overflow{$3};
2908       }
2909
2910
2911       my $reached_table = 0;
2912       while (<$fh>) {
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');
2924           my($i);
2925           for ($i = 0; $i <= $#$times_aref; $i++) {
2926             if ($time_on_queue < $times_aref->[$i]) {
2927               $$bin_aref[$i] += $count;
2928               last;
2929             }
2930           }
2931           $$overflow_sref += $count if ($i > $#$times_aref);
2932
2933         }
2934         else {
2935           last;                             #Finished the table ?
2936         }
2937       }
2938     }
2939
2940     elsif (/Relayed messages/) {
2941 #Relayed messages
2942 #----------------
2943 #
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
2946 #
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>
2948
2949       my $reached_table = 0;
2950       my($count,$sender);
2951       while (<$fh>) {
2952         unless ($reached_table) {
2953           last if (/No relayed messages/);
2954           $reached_table = 1 if (/^\s*\d/ || />\d+</);
2955           next unless $reached_table;
2956         }
2957         if (/>(\d+)<.td><td>(.*?) ?<.td><td>(.*?)</) {
2958           update_relayed($1,$2,$3);
2959         }
2960         elsif (/^\s*(\d+)\s+(.*?)\s*$/) {
2961           ($count,$sender) = ($1,$2);
2962         }
2963         elsif (/=>\s+(.*?)\s*$/) {
2964           update_relayed($count,$sender,$1);
2965         }
2966         else {
2967           last;                           #Finished the table ?
2968         }
2969       }
2970     }
2971
2972     elsif (/Top (.*?) by (message count|volume)/) {
2973 #Top 50 sending hosts by message count
2974 #-------------------------------------
2975 #
2976 #     48     1468KB   local
2977 # Could also have average values for HTML output.
2978 #     48     1468KB   30KB  local
2979
2980       my($category,$by_count_or_volume) = ($1,$2);
2981
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;
2989         undef %table_order;
2990       }
2991
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;
2999       }
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"}};
3006       }
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;
3011       }
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"}};
3018       }
3019
3020       my $reached_table = 0;
3021       while (<$fh>) {
3022         # Watch out for empty tables.
3023         goto PARSE_OLD_REPORT_LINE if (/<h2>/ or /^[a-zA-Z]/);
3024
3025         $_ = html2txt($_);              #Convert general HTML markup to text.
3026
3027
3028         $reached_table = 1 if (/^\s*\d/);
3029         next unless $reached_table;
3030
3031         # Remove optional 'average value' column.
3032         s/^\s*(\d+)\s+(\S+)\s+(\d+(KB|MB|GB|\b)\s+)/$1 $2 /;
3033
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.
3037
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/);
3040
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;
3048             }
3049
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;
3054           }
3055         }
3056         else {         #Finished the table ?
3057           if ($by_count_or_volume =~ /volume/) {
3058             #Add a few bytes to appropriate entries to preserve the order.
3059
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'};
3067               my(@order,%mark);
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'}});
3071
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;
3079                 }
3080                 $bonus_bytes--;
3081               }
3082             }
3083           }
3084
3085           last;
3086         }
3087       }
3088     }
3089     elsif (/List of errors/) {
3090 #List of errors
3091 #--------------
3092 #
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
3096 #
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>
3098
3099
3100       my $reached_table = 0;
3101       my($count,$error,$blanks);
3102       while (<$fh>) {
3103         $reached_table = 1 if (/^( *|<li>)(\d+)/);
3104         next unless $reached_table;
3105
3106         s/^<li>(\d+) -/$1/;     #Convert an HTML line to a text line.
3107         $_ = html2txt($_);      #Convert general HTML markup to text.
3108
3109         if (/\t\s*(.*)/) {
3110           $error .= ' ' . $1;   #Join a multiline error.
3111         }
3112         elsif (/^\s*(\d+)\s+(.*)/) {
3113           if ($error) {
3114             #Finished with a previous multiline error so save it.
3115             $errors_count{$error} = 0 unless $errors_count{$error};
3116             $errors_count{$error} += $count;
3117           }
3118           ($count,$error) = ($1,$2);
3119         }
3120         elsif (/Errors encountered/) {
3121           if ($error) {
3122             #Finished the section, so save our stored last error.
3123             $errors_count{$error} = 0 unless $errors_count{$error};
3124             $errors_count{$error} += $count;
3125           }
3126           last;
3127         }
3128       }
3129     }
3130
3131   }
3132 }
3133
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) = @_;
3140
3141   #      Messages received per hour (each dot is 2 messages)
3142   #---------------------------------------------------
3143   #
3144   #00-01    106 .....................................................
3145   #01-02    103 ...................................................
3146
3147   my $reached_table = 0;
3148   while (<$fh>) {
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;
3154     }
3155     elsif (/^(\d+)-(\d+)\s+(\d+)/) {        #hh-hh start-end time format ?
3156       $$counters_aref[($1*60)/$hist_interval] += $3 if $hist_opt;
3157     }
3158     else {                                  #Finished the table ?
3159       last;
3160     }
3161   }
3162 }
3163
3164
3165 #######################################################################
3166 # update_relayed();
3167 #
3168 #  update_relayed($count,$sender,$recipient);
3169 #
3170 # Adds an entry into the %relayed hash. Currently only used when
3171 # merging reports.
3172 #######################################################################
3173 sub update_relayed {
3174   my($count,$sender,$recipient) = @_;
3175
3176   #When generating the key, put in the 'H=' and 'A=' which can be used
3177   #in searches.
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;
3183   }
3184   else {
3185     $relayed_unshown += $count;
3186   }
3187 }
3188
3189
3190 #######################################################################
3191 # add_to_totals();
3192 #
3193 #  add_to_totals(\%totals,\@keys,$values);
3194 #
3195 # Given a line of space seperated values, add them into the provided hash using @keys
3196 # as the hash keys.
3197 #
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 #######################################################################
3201 sub add_to_totals {
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.
3205   my($value);
3206   foreach $value (@values) {
3207     my $key = shift(@keys) or next;
3208     if ($value =~ /%/) {
3209       $$totals_href{$key} = $value;
3210     }
3211     else {
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;
3216     }
3217   }
3218 }
3219
3220 #######################################################################
3221 # get_report_total();
3222 #
3223 #  $total = get_report_total(\%hash,$key);
3224 #
3225 # If %hash contains values split into Units and Gigs, we calculate and return
3226 #
3227 #   $hash{$key} + 1024*1024*1024 * $hash{"${key}-gigs"}
3228 #######################################################################
3229 sub get_report_total {
3230   no integer;
3231   my($hash_ref,$key) = @_;
3232   if ($$hash_ref{"${key}-gigs"}) {
3233     return $$hash_ref{$key} + $gig * $$hash_ref{"${key}-gigs"};
3234   }
3235   return $$hash_ref{$key} || 0;
3236 }
3237
3238 #######################################################################
3239 # html2txt();
3240 #
3241 #  $text_line = html2txt($html_line);
3242 #
3243 # Convert a line from html to text. Currently we just convert HTML tags to spaces
3244 # and convert &gt;, &lt;, and &nbsp; tags back.
3245 #######################################################################
3246 sub html2txt {
3247   ($_) = @_;
3248
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;
3254
3255   s/\&lt\;/\</og;             #Convert '&lt;' to '<'.
3256   s/\&gt\;/\>/og;             #Convert '&gt;' to '>'.
3257   s/\&nbsp\;/ /og;            #Convert '&nbsp;' to ' '.
3258   return($_);
3259 }
3260
3261 #######################################################################
3262 # get_next_arg();
3263 #
3264 #  $arg = get_next_arg();
3265 #
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.
3272 #
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
3276 # variables.
3277 #######################################################################
3278 sub get_next_arg {
3279   my $arg = '';
3280   my $matched_pattern = 0;
3281   while ($ARGV[1]) {
3282     $arg .= ' ' if $arg;
3283     $arg .= $ARGV[1]; shift(@ARGV);
3284     if ($arg !~ /^['"]/) {
3285       $matched_pattern = 1;
3286       last;
3287     }
3288     if ($arg =~ s/^(['"])(.*)\1$/$2/) {
3289       $matched_pattern = 1;
3290       last;
3291     }
3292   }
3293   die "Mismatched argument quotes - <$arg>.\n" unless $matched_pattern;
3294   return $arg;
3295 }
3296
3297 #######################################################################
3298 # set_worksheet_line($ws_global, $startrow, $startcol, \@content, $format);
3299 #
3300 # set values to a sequence of cells in a row.
3301 #
3302 #######################################################################
3303 sub set_worksheet_line {
3304   my ($worksheet, $row, $col, $content, $format) = @_;
3305
3306   foreach my $token (@$content)
3307   {
3308      $worksheet->write($row, $col++, $token, $format );
3309   }
3310
3311 }
3312
3313 #######################################################################
3314 # @rcpt_times = parse_time_list($string);
3315 #
3316 # Parse a comma seperated list of time values in seconds given by
3317 # the user and fill an array.
3318 #
3319 # Return a default list if $string is undefined.
3320 # Return () if $string eq '0'.
3321 #######################################################################
3322 sub parse_time_list {
3323   my($string) = @_;
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);
3326   }
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);
3331   return(@times);
3332 }
3333
3334
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 {
3340   my($protocol) = @_;
3341   for (my $i = 0; $i <= $#rcpt_times; ++$i) {
3342     $rcpt_times_bin{$protocol}[$i] = 0;
3343   }
3344   $rcpt_times_overflow{$protocol} = 0;
3345 }
3346
3347
3348 ##################################################
3349 #                 Main Program                   #
3350 ##################################################
3351
3352
3353 $last_timestamp = '';
3354 $last_date = '';
3355 $show_errors = 1;
3356 $show_relay = 1;
3357 $show_transport = 1;
3358 $topcount = 50;
3359 $local_league_table = 1;
3360 $include_remote_users = 0;
3361 $include_original_destination = 0;
3362 $hist_opt = 1;
3363 $volume_rounding = 1;
3364 $localtime_offset = calculate_localtime_offset();    # PH/FANF
3365
3366 $charts = 0;
3367 $charts_option_specified = 0;
3368 $chartrel = ".";
3369 $chartdir = ".";
3370
3371 @queue_times = parse_time_list();
3372 @rcpt_times = ();
3373 @delivery_times = ();
3374
3375 $last_offset = '';
3376 $offset_seconds = 0;
3377
3378 $row=1;
3379 $row_league_table=1;
3380 $col=0;
3381 $col_hist=0;
3382 $run_hist=0;
3383 my(%output_files);     # What output files have been specified?
3384
3385 # Decode options
3386
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 }
3392   }
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$/)
3396     {
3397     if ($1 eq "") { $show_transport = 0 } else { $transport_pattern = $2 }
3398     }
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);
3406     }
3407     else {
3408       warn "WARNING: CPAN Module Spreadsheet::WriteExcel not installed. Obtain from www.cpan.org\n";
3409     }
3410   }
3411   elsif ($ARGV[0] =~ /^-merge$/)    { $merge_reports = 1 }
3412   elsif ($ARGV[0] =~ /^-charts$/)   {
3413     $charts = 1;
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;
3416   }
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$/)
3433     {
3434     push(@user_descriptions,get_next_arg());
3435     push(@user_patterns,get_next_arg());
3436     }
3437   elsif ($ARGV[0] =~ /^-utc$/)
3438     {
3439     # We don't need this value if the log is in UTC.
3440     $localtime_offset = undef;
3441     }
3442   else
3443     {
3444     print STDERR "Eximstats: Unknown or malformed option $ARGV[0]\n";
3445     help();
3446     }
3447   shift;
3448   }
3449
3450   # keep old default behaviour
3451   if (! ($xls_fh or $htm_fh or $txt_fh)) {
3452     $txt_fh = \*STDOUT;
3453   }
3454
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);
3457
3458   # Default to display tables by sending Host.
3459   $do_sender{Host} = 1 unless ($do_sender{Domain} || $do_sender{Email} || $do_sender{Edomain});
3460
3461   # prepare xls Excel Workbook
3462   if (defined $xls_fh)
3463   {
3464
3465     # Create a new Excel workbook
3466     $workbook  = Spreadsheet::WriteExcel->new($xls_fh);
3467
3468     # Add worksheets
3469     $ws_global = $workbook->addworksheet('Exim Statistik');
3470     # show $ws_global as initial sheet
3471     $ws_global->set_first_sheet();
3472     $ws_global->activate();
3473
3474     if ($show_relay) {
3475       $ws_relayed = $workbook->addworksheet('Relayed Messages');
3476       $ws_relayed->set_column(1, 2,  80);
3477     }
3478     if ($topcount) {
3479     $ws_top50 = $workbook->addworksheet('Deliveries');
3480     }
3481     if ($show_errors) {
3482       $ws_errors = $workbook->addworksheet('Errors');
3483     }
3484
3485
3486     # set column widths
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
3490
3491     # Define Formats
3492     $f_default = $workbook->add_format();
3493
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);
3501
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);
3507
3508     $f_percent = $workbook->add_format();
3509     $f_percent->set_num_format('0.0%');
3510
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);
3515
3516   }
3517
3518
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;
3523 }
3524 for (my $i = 0; $i <= $#delivery_times; $i++) {
3525   $dt_all_bin[$i] = 0;
3526   $dt_remote_bin[$i] = 0;
3527 }
3528 initialise_rcpt_times('all');
3529
3530
3531 # Compute the number of slots for the histogram
3532 if ($hist_opt > 0)
3533   {
3534   if ($hist_opt > 60 || 60 % $hist_opt != 0)
3535     {
3536     print STDERR "Eximstats: -h must specify a factor of 60\n";
3537     exit 1;
3538     }
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;
3546   }
3547   @dt_all_bin = (0) x $hist_number;
3548   @dt_remote_bin = (0) x $hist_number;
3549   }
3550
3551 #$queue_unknown = 0;
3552
3553 $total_received_data = 0;
3554 $total_received_data_gigs = 0;
3555 $total_received_count = 0;
3556
3557 $total_delivered_data = 0;
3558 $total_delivered_data_gigs = 0;
3559 $total_delivered_count = 0;
3560
3561 $qt_all_overflow = 0;
3562 $qt_remote_overflow = 0;
3563 $dt_all_overflow = 0;
3564 $dt_remote_overflow = 0;
3565 $delayed_count = 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";
3570 my($section,$type);
3571 foreach $section ('Received','Delivered') {
3572   foreach $type ('Volume','Messages','Delayed','Failed','Hosts','Domains','Emails','Edomains') {
3573     $report_totals{$section}{$type} = 0;
3574   }
3575 }
3576
3577 # Generate our parser.
3578 my $parser = generate_parser();
3579
3580
3581
3582 if (@ARGV) {
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: $!";
3588         next;
3589       }
3590     }
3591     elsif ($file =~ /\.Z/) {
3592       unless (open(FILE,"uncompress -c $file |")) {
3593         print STDERR "Failed to uncompress -c $file: $!";
3594         next;
3595       }
3596     }
3597     else {
3598       unless (open(FILE,$file)) {
3599         print STDERR "Failed to read $file: $!";
3600         next;
3601       }
3602     }
3603     #Now parse the filehandle, updating the global variables.
3604     parse($parser,\*FILE);
3605     close FILE;
3606   }
3607 }
3608 else {
3609   #No files provided. Parse STDIN, updating the global variables.
3610   parse($parser,\*STDIN);
3611 }
3612
3613
3614 if ($begin eq "9999-99-99 99:99:99") {
3615   print STDERR "**** No valid log lines read\n";
3616   exit 1;
3617 }
3618
3619 # Output our results.
3620 print_header();
3621 print_grandtotals();
3622
3623 # Print counts of user specified patterns if required.
3624 print_user_patterns() if @user_patterns;
3625
3626 # Print totals by transport if required.
3627 print_transport() if $show_transport;
3628
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);
3634 }
3635
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);
3640 }
3641
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);
3646 }
3647
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});
3652   }
3653 }
3654
3655 # Print relay information if required.
3656 print_relay() if $show_relay;
3657
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{$_});
3663   }
3664
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{$_});
3670   }
3671   print_league_table("local destination", \%delivered_count_user,
3672     \%delivered_data_user,\%delivered_data_gigs_user) if ($local_league_table || $include_remote_users);
3673 }
3674
3675 # Print the error statistics if required.
3676 print_errors() if $show_errors;
3677
3678 print $htm_fh "</body>\n</html>\n" if $htm_fh;
3679
3680
3681 $txt_fh->close if $txt_fh;
3682 $htm_fh->close if $htm_fh;
3683
3684 if ($xls_fh) {
3685   # close Excel Workbook
3686   $ws_global->set_first_sheet();
3687   # FIXME: whyever - activate does not work :-/
3688   $ws_global->activate();
3689   $workbook->close();
3690 }
3691
3692
3693 # End of eximstats
3694
3695
3696 # FIXME: Doku