Current File : //sbin/eximstats
#!/usr/local/cpanel/3rdparty/perl/536/bin/perl

# Copyright (c) The Exim Maintainers 2023
# Copyright (c) 2001-2017 University of Cambridge.
# See the file NOTICE for conditions of use and distribution.
# SPDX-License-Identifier: GPL-2.0-or-later

# Perl script to generate statistics from one or more Exim log files.

# Usage: eximstats [<options>] <log file> <log file> ...

# 1996-05-21: Ignore lines not starting with valid date/time, just in case
#               these get into a log file.
# 1996-11-19: Add the -h option to control the size of the histogram,
#               and optionally turn it off.
#             Use some Perl 5 things; it should be everywhere by now.
#             Add the Perl -w option and rewrite so no warnings are given.
#             Add the -t option to control the length of the "top" listing.
#             Add the -ne, -nt options to turn off errors and transport
#               information.
#             Add information about length of time on queue, and -q<list> to
#               control the intervals and turn it off.
#             Add count and percentage of delayed messages to the Received
#               line.
#             Show total number of errors.
#             Add count and percentage of messages with errors to Received
#               line.
#             Add information about relaying and -nr to suppress it.
# 1997-02-03  Merged in some of the things Nigel Metheringham had done:
#               Re-worded headings
#               Added received histogram as well as delivered
#               Added local senders' league table
#               Added local recipients' league table
# 1997-03-10  Fixed typo "destinationss"
#             Allow for intermediate address between final and original
#               when testing for relaying
#             Give better message when no input
# 1997-04-24  Fixed bug in layout of error listing that was depending on
#               text length (output line got repeated).
# 1997-05-06  Bug in option decoding when only one option.
#             Overflow bug when handling very large volumes.
# 1997-10-28  Updated to handle revised log format that might show
#               HELO name as well as host name before IP number
# 1998-01-26  Bugs in the function for calculating the number of seconds
#               since 1970 from a log date
# 1998-02-02  Delivery to :blackhole: doesn't have a T= entry in the log
#               line; cope with this, thereby avoiding undefined problems
#             Very short log line gave substring error
# 1998-02-03  A routed delivery to a local transport may not have <> in the
#               log line; terminate the address at white space, not <
# 1998-09-07  If first line of input was a => line, $thissize was undefined;
#               ensure it is zero.
# 1998-12-21  Adding of $thissize from => line should have been adding $size.
#             Oops. Should have looked more closely when fixing the previous
#               bug!
# 1999-11-12  Increased the field widths for printed integers; numbers are
#               bigger than originally envisaged.
# 2001-03-21  Converted seconds() routine to use Time::Local, fixing a bug
#               whereby seconds($timestamp) - id_seconds($id) gave an
#               incorrect result.
#             Added POD documentation.
#             Moved usage instructions into help() subroutine.
#             Added 'use strict' and declared all global variables.
#             Added '-html' flag and resultant code.
#             Added '-cache' flag and resultant code.
#             Added add_volume() routine and converted all volume variables
#               to use it, fixing the overflow problems for individual hosts
#               on large sites.
#             Converted all volume output to GB/MB/KB as appropriate.
#             Don't store local user stats if -nfl is specified.
#             Modifications done by: Steve Campbell (<steve@computurn.com>)
# 2001-04-02  Added the -t_remote_users flag. Steve Campbell.
# 2001-10-15  Added the -domain flag. Steve Campbell.
# 2001-10-16  Accept files on STDIN or on the command line. Steve Campbell.
# 2001-10-21  Removed -domain flag and added -bydomain, -byhost, and -byemail.
#             We now generate our main parsing subroutine as an eval statement
#             which improves performance dramatically when not all the results
#             are required. We also cache the last timestamp to time conversion.
#
#             NOTE: 'Top 50 destinations by (message count|volume)' lines are
#             now 'Top N (host|email|domain) destinations by (message count|volume)'
#             where N is the topcount. Steve Campbell.
#
# 2001-10-30  V1.16 Joachim Wieland.
#            Fixed minor bugs in add_volume() when taking over this version
#               for use in Exim 4: -w gave uninitialized value warnings in
#               two situations: for the first addition to a counter, and if
#               there were never any gigabytes, thereby leaving the $gigs
#               value unset.
#             Initialized $last_timestamp to stop a -w uninitialized warning.
#             Minor layout tweak for grand totals (nitpicking).
#             Put the IP addresses for relaying stats in [] and separated by
#               a space from the domain name.
#             Removed the IPv4-specific address test when picking out addresses
#               for relaying. Anything inside [] is OK.
#
# 2002-07-02  Philip Hazel
#             Fixed "uninitialized variable" message that occurred for relay
#               messages that arrived from H=[1.2.3.4] hosts (no name shown).
#               This bug didn't affect the output.
#
# 2002-04-15  V1.17 Joachim Wieland.
#             Added -charts, -chartdir. -chartrel options which use
#             GD::Graph modules to create graphical charts of the statistics.
#
# 2002-04-15  V1.18 Steve Campbell.
#             Added a check for $domain to to stop a -w uninitialized warning.
#             Added -byemaildomain option.
#             Only print HTML header links to included tables!
#
# 2002-08-02  V1.19 Steve Campbell.
#             Changed the debug mode to dump the parser onto STDERR rather
#             than STDOUT. Documented the -d flag into the help().
#             Rejoined the divergent 2002-04-15 and 2002-07-02 releases.
#
# 2002-08-21  V1.20 Steve Campbell.
#             Added the '-merge' option to allow merging of previous reports.
#             Fixed a missing semicolon when doing -bydomain.
#             Make volume charts plot the data gigs and bytes rather than just bytes.
#             Only process log lines with $flag =~ /<=|=>|->|==|\*\*|Co/
#             Converted Emaildomain to Edomain - the column header was too wide!
#             This changes the text output slightly. You can revert to the old
#             column widths by changing $COLUMN_WIDTHS to 7;
#
# 2002-09-04  V1.21 Andreas J Mueller
#             Local deliveries domain now defaults to 'localdomain'.
#             Don't match F=<From> when looking for the user.
#
# 2002-09-05  V1.22 Steve Campbell
#             Fixed a perl 5.005 incompatibility problem ('our' variables).
#
# 2002-09-11  V1.23 Steve Campbell
#             Stopped -charts option from throwing errors on null data.
#             Don't print out 'Errors encountered' unless there are any.

# 2002-10-21  V1.23a Philip Hazel - patch from Tony Finch put in until
#               Steve's eximstats catches up.
#             Handle log files that include the timezone after the timestamp.
#             Switch to assuming that log timestamps are in local time, with
#               an option for UTC timestamps, as in Exim itself.
#
# 2003-02-05  V1.24 Steve Campbell
#             Added in Sergey Sholokh's code to convert '<' and '>' characters
#             in HTML output. Also added code to convert them back with -merge.
#             Fixed timestamp offsets to convert to seconds rather than minutes.
#             Updated -merge to work with output files using timezones.
#             Added caching to speed up the calculation of timezone offsets.
#
# 2003-02-07  V1.25 Steve Campbell
#             Optimised the usage of mktime() in the seconds subroutine.
#             Removed the now redundant '-cache' option.
#             html2txt() now explicitly matches HTML tags.
#             Implemented a new sorting algorithm - the top_n_sort() routine.
#             Added Danny Carroll's '-nvr' flag and code.
#
# 2003-03-13  V1.26 Steve Campbell
#             Implemented HTML compliance changes recommended by Bernard Massot.
#             Bug fix to allow top_n_sort() to handle null keys.
#             Convert all domains and edomains to lowercase.
#             Remove preceding dots from domains.
#
# 2003-03-13  V1.27 Steve Campbell
#             Replaced border attributes with 'border=1', as recommended by
#             Bernard Massot.
#
# 2003-06-03  V1.28 John Newman
#             Added in the ability to skip over the parsing and evaluation of
#             specific transports as passed to eximstats via the new "-nt/.../"
#             command line argument.  This new switch allows the viewing of
#             not more accurate statistics but more applicable statistics when
#             special transports are in use (ie; SpamAssassin).  We need to be
#             able to ignore transports such as this otherwise the resulting
#             local deliveries are significantly skewed (doubled)...
#
# 2003-11-06  V1.29 Steve Campbell
#             Added the '-pattern "Description" "/pattern/"' option.
#
# 2004-02-17  V1.30 Steve Campbell
#             Added warnings if required GD::Graph modules are not available or
#             insufficient -chart* options are specified.
#
# 2004-02-20  V1.31 Andrea Balzi
#             Only show the Local Sender/Destination links if the tables exist.
#
# 2004-07-05  V1.32 Steve Campbell
#             Fix '-merge -h0' divide by zero error.
#
# 2004-07-15  V1.33 Steve Campbell
#             Documentation update - I've converted the subroutine
#             documentation from POD to comments.
#
# 2004-12-10  V1.34 Steve Campbell
#             Eximstats can now parse syslog lines as well as mainlog lines.
#
# 2004-12-20  V1.35 Wouter Verhelst
#             Pie charts by volume were actually generated by count. Fixed.
#
# 2005-02-07  V1.36 Gregor Herrmann / Steve Campbell
#             Added average sizes to HTML Top tables.
#
# 2005-04-26  V1.37 Frank Heydlauf
#             Added -xls and the ability to specify output files.
#
# 2005-04-29  V1.38 Steve Campbell
#             Use FileHandles for outputting results.
#             Allow any combination of xls, txt, and html output.
#             Fixed display of large numbers with -nvr option
#             Fixed merging of reports with empty tables.
#
# 2005-05-27  V1.39 Steve Campbell
#             Added the -include_original_destination flag
#             Removed tabs and trailing whitespace.
#
# 2005-06-03  V1.40 Steve Campbell
#             Whilst parsing the mainlog(s), store information about
#             the messages in a hash of arrays rather than using
#             individual hashes. This is a bit cleaner and results in
#             dramatic memory savings, albeit at a slight CPU cost.
#
# 2005-06-15  V1.41 Steve Campbell
#             Added the -show_rt<list> flag.
#             Added the -show_dt<list> flag.
#
# 2005-06-24  V1.42 Steve Campbell
#             Added Histograms for user specified patterns.
#
# 2005-06-30  V1.43 Steve Campbell
#             Bug fix for V1.42 with -h0 specified. Spotted by Chris Lear.
#
# 2005-07-26  V1.44 Steve Campbell
#             Use a glob alias rather than an array ref in the generated
#             parser. This improves both readability and performance.
#
# 2005-09-30  V1.45 Marco Gaiarin / Steve Campbell
#             Collect SpamAssassin and rejection statistics.
#             Don't display local sender or destination tables unless
#             there is data to show.
#             Added average volumes into the top table text output.
#
# 2006-02-07  V1.46 Steve Campbell
#             Collect data on the number of addresses (recipients)
#             as well as the number of messages.
#
# 2006-05-05  V1.47 Steve Campbell
#             Added 'Message too big' to the list of mail rejection
#             reasons (thanks to Marco Gaiarin).
#
# 2006-06-05  V1.48 Steve Campbell
#             Mainlog lines which have GMT offsets and are too short to
#             have a flag are now skipped.
#
# 2006-11-10  V1.49 Alain Williams
#             Added the -emptyok flag.
#
# 2006-11-16  V1.50 Steve Campbell
#             Fixes for obtaining the IP address from reject messages.
#
# 2006-11-27  V1.51 Steve Campbell
#             Another update for obtaining the IP address from reject messages.
#
# 2006-11-27  V1.52 Steve Campbell
#             Tally any reject message containing SpamAssassin.
#
# 2007-01-31  V1.53 Philip Hazel
#             Allow for [pid] after date in log lines
#
# 2007-02-14  V1.54 Daniel Tiefnig
#             Improved the '($parent) =' pattern match.
#
# 2007-03-19  V1.55 Steve Campbell
#             Differentiate between permanent and temporary rejects.
#
# 2007-03-29  V1.56 Jez Hancock
#             Fixed some broken HTML links and added missing column headers.
#
# 2007-03-30  V1.57 Steve Campbell
#             Fixed Grand Total Summary Domains, Edomains, and Email columns
#             for Rejects, Temp Rejects, Ham, and Spam rows.
#
# 2007-04-11  V1.58 Steve Campbell
#             Fix to get <> and blackhole to show in edomain tables.
#
# 2007-09-20  V1.59 Steve Campbell
#             Added the -bylocaldomain option
#
# 2007-09-20  V1.60 Heiko Schlittermann
#             Fix for misinterpreted log lines
#
# 2013-01-14  V1.61 Steve Campbell
#             Watch out for senders sending "HELO [IpAddr]"
#
#
# For documentation on the logfile format, see
# http://www.exim.org/exim-html-4.50/doc/html/spec_48.html#IX2793

=head1 NAME

eximstats - generates statistics from Exim mainlog or syslog files.

=head1 SYNOPSIS

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

=head2 Output:

=over 4

=item B<-txt>

Output the results in plain text to STDOUT.

=item B<-txt>=I<filename>

Output the results in plain text. Filename '-' for STDOUT is accepted.

=item B<-html>

Output the results in HTML to STDOUT.

=item B<-html>=I<filename>

Output the results in HTML. Filename '-' for STDOUT is accepted.

=item B<-xls>

Output the results in Excel compatible Format to STDOUT.
Requires the Spreadsheet::WriteExcel CPAN module.

=item B<-xls>=I<filename>

Output the results in Excel compatible format. Filename '-' for STDOUT is accepted.


=back

=head2 Options:

=over 4

=item B<-h>I<number>

histogram divisions per hour. The default is 1, and
0 suppresses histograms. Valid values are:

0, 1, 2, 3, 5, 10, 15, 20, 30 or 60.

=item B<-ne>

Don't display error information.

=item B<-nr>

Don't display relaying information.

=item B<-nr>I</pattern/>

Don't display relaying information that matches.

=item B<-nt>

Don't display transport information.

=item B<-nt>I</pattern/>

Don't display transport information that matches

=item B<-q>I<list>

List of times for queuing information single 0 item suppresses.

=item B<-t>I<number>

Display top <number> sources/destinations
default is 50, 0 suppresses top listing.

=item B<-tnl>

Omit local sources/destinations in top listing.

=item B<-t_remote_users>

Include remote users in the top source/destination listings.

=item B<-include_original_destination>

Include the original destination email addresses rather than just
using the final ones.
Useful for finding out which of your mailing lists are receiving mail.

=item B<-show_dt>I<list>

Show the delivery times (B<DT>)for all the messages.

Exim must have been configured to use the +deliver_time logging option
for this option to work.

I<list> is an optional list of times. Eg -show_dt1,2,4,8 will show
the number of messages with delivery times under 1 second, 2 seconds, 4 seconds,
8 seconds, and over 8 seconds.

=item B<-show_rt>I<list>

Show the receipt times for all the messages. The receipt time is
defined as the Completed hh:mm:ss - queue_time_overall - the Receipt hh:mm:ss.
These figures will be skewed by pipelined messages so might not be that useful.

Exim must have been configured to use the +queue_time_overall logging option
for this option to work.

I<list> is an optional list of times. Eg -show_rt1,2,4,8 will show
the number of messages with receipt times under 1 second, 2 seconds, 4 seconds,
8 seconds, and over 8 seconds.

=item B<-byhost>

Show results by sending host. This may be combined with
B<-bydomain> and/or B<-byemail> and/or B<-byedomain>. If none of these options
are specified, then B<-byhost> is assumed as a default.

=item B<-bydomain>

Show results by sending domain.
May be combined with B<-byhost> and/or B<-byemail> and/or B<-byedomain>.

=item B<-byemail>

Show results by sender's email address.
May be combined with B<-byhost> and/or B<-bydomain> and/or B<-byedomain>.

=item B<-byemaildomain> or B<-byedomain>

Show results by sender's email domain.
May be combined with B<-byhost> and/or B<-bydomain> and/or B<-byemail>.

=item B<-pattern> I<Description> I</Pattern/>

Look for the specified pattern and count the number of lines in which it appears.
This option can be specified multiple times. Eg:

 -pattern 'Refused connections' '/refused connection/'


=item B<-merge>

This option allows eximstats to merge old eximstat reports together. Eg:

 eximstats mainlog.sun > report.sun.txt
 eximstats mainlog.mon > report.mon.txt
 eximstats mainlog.tue > report.tue.txt
 eximstats mainlog.wed > report.web.txt
 eximstats mainlog.thu > report.thu.txt
 eximstats mainlog.fri > report.fri.txt
 eximstats mainlog.sat > report.sat.txt
 eximstats -merge       report.*.txt > weekly_report.txt
 eximstats -merge -html report.*.txt > weekly_report.html

=over 4

=item *

You can merge text or html reports and output the results as text or html.

=item *

You can use all the normal eximstat output options, but only data
included in the original reports can be shown!

=item *

When merging reports, some loss of accuracy may occur in the top I<n> lists.
This will be towards the ends of the lists.

=item *

The order of items in the top I<n> lists may vary when the data volumes
round to the same value.

=back

=item B<-charts>

Create graphical charts to be displayed in HTML output.
Only valid in combination with I<-html>.

This requires the following modules which can be obtained
from http://www.cpan.org/modules/01modules.index.html

=over 4

=item GD

=item GDTextUtil

=item GDGraph

=back

To install these, download and unpack them, then use the normal perl installation procedure:

 perl Makefile.PL
 make
 make test
 make install

=item B<-chartdir>I <dir>

Create the charts in the directory <dir>

=item B<-chartrel>I <dir>

Specify the relative directory for the "img src=" tags from where to include
the charts

=item B<-emptyok>

Specify that it's OK to not find any valid log lines. Without this
we will output an error message if we don't find any.

=item B<-d>

Debug flag. This outputs the eval()'d parser onto STDOUT which makes it
easier to trap errors in the eval section. Remember to add 1 to the line numbers to allow for the
title!

=back

=head1 DESCRIPTION

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

=head1 AUTHOR

There is a website at https://www.exim.org - this contains details of the
mailing list exim-users@exim.org.

=head1 TO DO

This program does not perfectly handle messages whose received
and delivered log lines are in different files, which can happen
when you have multiple mail servers and a message cannot be
immediately delivered. Fixing this could be tricky...

Merging of xls files is not (yet) possible. Be free to implement :)

=cut

use warnings;
use integer;
BEGIN { pop @INC if $INC[-1] eq '.' };
use strict;
use IO::File;
use File::Basename;

# use Time::Local;  # PH/FANF
use POSIX;

if (@ARGV and ($ARGV[0] eq '--version' || $ARGV[0] eq '-v')) {
    print basename($0) . ": $0\n",
        "build: 4.98.2\n",
        "perl(runtime): $]\n";
        exit 0;
}

use vars qw($HAVE_GD_Graph_pie $HAVE_GD_Graph_linespoints $HAVE_Spreadsheet_WriteExcel);
eval { require GD::Graph::pie; };
$HAVE_GD_Graph_pie = $@ ? 0 : 1;
eval { require GD::Graph::linespoints; };
$HAVE_GD_Graph_linespoints = $@ ? 0 : 1;
eval { require Spreadsheet::WriteExcel; };
$HAVE_Spreadsheet_WriteExcel = $@ ? 0 : 1;


##################################################
#             Static data                        #
##################################################
# 'use vars' instead of 'our' as perl5.005 is still in use out there!
use vars qw(@tab62 @days_per_month $gig);
use vars qw($VERSION);
use vars qw($COLUMN_WIDTHS);
use vars qw($WEEK $DAY $HOUR $MINUTE);


@tab62 =
  (0,1,2,3,4,5,6,7,8,9,0,0,0,0,0,0,     # 0-9
   0,10,11,12,13,14,15,16,17,18,19,20,  # A-K
  21,22,23,24,25,26,27,28,29,30,31,32,  # L-W
  33,34,35, 0, 0, 0, 0, 0,              # X-Z
   0,36,37,38,39,40,41,42,43,44,45,46,  # a-k
  47,48,49,50,51,52,53,54,55,56,57,58,  # l-w
  59,60,61);                            # x-z

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

# How much space do we allow for the Hosts/Domains/Emails/Edomains column headers?
$COLUMN_WIDTHS = 8;

$MINUTE = 60;
$HOUR   = 60 * $MINUTE;
$DAY    = 24 * $HOUR;
$WEEK   =  7 * $DAY;

# Declare global variables.
use vars qw($total_received_data  $total_received_data_gigs  $total_received_count);
use vars qw($total_delivered_data $total_delivered_data_gigs $total_delivered_messages $total_delivered_addresses);
use vars qw(%timestamp2time);                   #Hash of timestamp => time.
use vars qw($last_timestamp $last_time);        #The last time conversion done.
use vars qw($last_date $date_seconds);          #The last date conversion done.
use vars qw($last_offset $offset_seconds);      #The last time offset conversion done.
use vars qw($localtime_offset);
use vars qw($i);                                #General loop counter.
use vars qw($debug);                            #Debug mode?
use vars qw($ntopchart);                        #How many entries should make it into the chart?
use vars qw($gddirectory);                      #Where to put files from GD::Graph

# SpamAssassin variables
use vars qw($spam_score $spam_score_gigs);
use vars qw($ham_score  $ham_score_gigs);
use vars qw(%ham_count_by_ip %spam_count_by_ip);
use vars qw(%rejected_count_by_ip %rejected_count_by_reason);
use vars qw(%temporarily_rejected_count_by_ip %temporarily_rejected_count_by_reason);

#For use in Spreadsheet::WriteExcel
use vars qw($workbook $ws_global $ws_relayed $ws_errors);
use vars qw($row $col $row_hist $col_hist);
use vars qw($run_hist);
use vars qw($f_default $f_header1 $f_header2 $f_header2_m $f_headertab $f_percent); #Format Header

# Output FileHandles
use vars qw($txt_fh $htm_fh $xls_fh);

$ntopchart = 5;

# The following are parameters whose values are
# set by command line switches:
use vars qw($show_errors $show_relay $show_transport $transport_pattern);
use vars qw($topcount $local_league_table $include_remote_users $do_local_domain);
use vars qw($hist_opt $hist_interval $hist_number $volume_rounding $emptyOK);
use vars qw($relay_pattern @queue_times @user_patterns @user_descriptions);
use vars qw(@rcpt_times @delivery_times);
use vars qw($include_original_destination);
use vars qw($txt_fh $htm_fh $xls_fh);

use vars qw(%do_sender);                #Do sender by Host, Domain, Email, and/or Edomain tables.
use vars qw($charts $chartrel $chartdir $charts_option_specified);
use vars qw($merge_reports);            #Merge old reports ?

# The following are modified in the parse() routine, and
# referred to in the print_*() routines.
use vars qw($delayed_count $relayed_unshown $begin $end);
use vars qw(%messages @message);
use vars qw(%received_count       %received_data       %received_data_gigs);
use vars qw(%delivered_messages      %delivered_data      %delivered_data_gigs %delivered_addresses);
use vars qw(%received_count_user  %received_data_user  %received_data_gigs_user);
use vars qw(%delivered_messages_user %delivered_addresses_user %delivered_data_user %delivered_data_gigs_user);
use vars qw(%delivered_messages_local_domain %delivered_addresses_local_domain %delivered_data_local_domain %delivered_data_gigs_local_domain);
use vars qw(%transported_count    %transported_data    %transported_data_gigs);
use vars qw(%relayed %errors_count $message_errors);
use vars qw(@qt_all_bin @qt_remote_bin);
use vars qw($qt_all_overflow $qt_remote_overflow);
use vars qw(@dt_all_bin @dt_remote_bin %rcpt_times_bin);
use vars qw($dt_all_overflow $dt_remote_overflow %rcpt_times_overflow);
use vars qw(@received_interval_count @delivered_interval_count);
use vars qw(@user_pattern_totals @user_pattern_interval_count);

use vars qw(%report_totals);

# Enumerations
use vars qw($SIZE $FROM_HOST $FROM_ADDRESS $ARRIVAL_TIME $REMOTE_DELIVERED $PROTOCOL);
use vars qw($DELAYED $HAD_ERROR);
$SIZE             = 0;
$FROM_HOST        = 1;
$FROM_ADDRESS     = 2;
$ARRIVAL_TIME     = 3;
$REMOTE_DELIVERED = 4;
$DELAYED          = 5;
$HAD_ERROR        = 6;
$PROTOCOL         = 7;



##################################################
#                   Subroutines                  #
##################################################

#######################################################################
# get_filehandle($file,\%output_files);
# Return a filehandle writing to $file.
#
# If %output_files is defined, check that $output_files{$file}
# doesn't exist and die if it does, or set it if it doesn't.
#######################################################################
sub get_filehandle {
  my($file,$output_files_href) = @_;

  $file = '-' if ($file eq '');

  if (defined $output_files_href) {
    die "You can only output to '$file' once! Use -h for help.\n" if exists $output_files_href->{$file};
    $output_files_href->{$file} = 1;
  }

  if ($file eq '-') {
    return \*STDOUT;
  }

  if (-e $file) {
    unlink $file or die "Failed to rm $file: $!";
  }

  my $fh = new IO::File $file, O_WRONLY|O_CREAT|O_EXCL;
  die "new IO::File $file failed: $!" unless (defined $fh);
  return $fh;
}


#######################################################################
# volume_rounded();
#
# $rounded_volume = volume_rounded($bytes,$gigabytes);
#
# Given a data size in bytes, round it to KB, MB, or GB
# as appropriate.
#
# Eg 12000 => 12KB, 15000000 => 14GB, etc.
#
# Note: I've experimented with Math::BigInt and it results in a 33%
# performance degredation as opposed to storing numbers split into
# bytes and gigabytes.
#######################################################################
sub volume_rounded {
  my($x,$g) = @_;
  $x = 0 unless $x;
  $g = 0 unless $g;
  my($rounded);

  while ($x > $gig) {
    $g++;
    $x -= $gig;
  }

  if ($volume_rounding) {
    # Values < 1 GB
    if ($g <= 0) {
      if ($x < 10000) {
        $rounded = sprintf("%6d", $x);
      }
      elsif ($x < 10000000) {
        $rounded = sprintf("%4dKB", ($x + 512)/1024);
      }
      else {
        $rounded = sprintf("%4dMB", ($x + 512*1024)/(1024*1024));
      }
    }
    # Values between 1GB and 10GB are printed in MB
    elsif ($g < 10) {
      $rounded = sprintf("%4dMB", ($g * 1024) + ($x + 512*1024)/(1024*1024));
    }
    else {
      # Handle values over 10GB
      $rounded = sprintf("%4dGB", $g + ($x + $gig/2)/$gig);
    }
  }
  else {
    # We don't want any rounding to be done.
    # and we don't need broken formatted output which on one hand avoids numbers from
    # being interpreted as string by Spreadsheet Calculators, on the other hand
    # breaks if more than 4 digits! -> flexible length instead of fixed length
    # Format the return value at the output routine! -fh
    #$rounded = sprintf("%d", ($g * $gig) + $x);
    no integer;
    $rounded = sprintf("%.0f", ($g * $gig) + $x);
  }

  return $rounded;
}


#######################################################################
# un_round();
#
#  un_round($rounded_volume,\$bytes,\$gigabytes);
#
# Given a volume in KB, MB or GB, as generated by volume_rounded(),
# do the reverse transformation and convert it back into Bytes and Gigabytes.
# These are added to the $bytes and $gigabytes parameters.
#
# Given a data size in bytes, round it to KB, MB, or GB
# as appropriate.
#
# EG: 500 => (500,0), 14GB => (0,14), etc.
#######################################################################
sub un_round {
  my($rounded,$bytes_sref,$gigabytes_sref) = @_;

  if ($rounded =~ /(\d+)GB/) {
    $$gigabytes_sref += $1;
  }
  elsif ($rounded =~ /(\d+)MB/) {
    $$gigabytes_sref +=   $1 / 1024;
    $$bytes_sref     += (($1 % 1024 ) * 1024 * 1024);
  }
  elsif ($rounded =~ /(\d+)KB/) {
    $$gigabytes_sref +=  $1 / (1024 * 1024);
    $$bytes_sref     += ($1 % (1024 * 1024) * 1024);
  }
  elsif ($rounded =~ /(\d+)/) {
    # We need to turn off integer in case we are merging an -nvr report.
    no integer;
    $$gigabytes_sref += int($1 / $gig);
    $$bytes_sref     += $1 % $gig;
  }

  #Now reduce the bytes down to less than 1GB.
  add_volume($bytes_sref,$gigabytes_sref,0) if ($$bytes_sref > $gig);
}


#######################################################################
# add_volume();
#
#   add_volume(\$bytes,\$gigs,$size);
#
# Add $size to $bytes/$gigs where this is a number split into
# bytes ($bytes) and gigabytes ($gigs). This is significantly
# faster than using Math::BigInt.
#######################################################################
sub add_volume {
  my($bytes_ref,$gigs_ref,$size) = @_;
  $$bytes_ref = 0 if ! defined $$bytes_ref;
  $$gigs_ref = 0 if ! defined $$gigs_ref;
  $$bytes_ref += $size;
  while ($$bytes_ref > $gig) {
    $$gigs_ref++;
    $$bytes_ref -= $gig;
  }
}


#######################################################################
# format_time();
#
#  $formatted_time = format_time($seconds);
#
# Given a time in seconds, break it down into
# weeks, days, hours, minutes, and seconds.
#
# Eg 12005 => 3h20m5s
#######################################################################
sub format_time {
my($t) = pop @_;
my($s) = $t % 60;
$t /= 60;
my($m) = $t % 60;
$t /= 60;
my($h) = $t % 24;
$t /= 24;
my($d) = $t % 7;
my($w) = $t/7;
my($p) = "";
$p .= "$w"."w" if $w > 0;
$p .= "$d"."d" if $d > 0;
$p .= "$h"."h" if $h > 0;
$p .= "$m"."m" if $m > 0;
$p .= "$s"."s" if $s > 0 || $p eq "";
$p;
}


#######################################################################
#  unformat_time();
#
#  $seconds = unformat_time($formatted_time);
#
# Given a time in weeks, days, hours, minutes, or seconds, convert it to seconds.
#
# Eg 3h20m5s => 12005
#######################################################################
sub unformat_time {
  my($formatted_time) = pop @_;
  my $time = 0;

  while ($formatted_time =~ s/^(\d+)([wdhms]?)//) {
    $time +=  $1 if ($2 eq '' || $2 eq 's');
    $time +=  $1 * 60 if ($2 eq 'm');
    $time +=  $1 * 60 * 60 if ($2 eq 'h');
    $time +=  $1 * 60 * 60 * 24 if ($2 eq 'd');
    $time +=  $1 * 60 * 60 * 24  * 7 if ($2 eq 'w');
  }
  $time;
}


#######################################################################
# seconds();
#
#  $time = seconds($timestamp);
#
# Given a time-of-day timestamp, convert it into a time() value using
# POSIX::mktime.  We expect the timestamp to be of the form
# "$year-$mon-$day $hour:$min:$sec", with month going from 1 to 12,
# and the year to be absolute (we do the necessary conversions). The
# seconds value can be followed by decimals, which we ignore. The
# timestamp may be followed with an offset from UTC like "+$hh$mm"; if the
# offset is not present, and we have not been told that the log is in UTC
# (with the -utc option), then we adjust the time by the current local
# time offset so that it can be compared with the time recorded in message
# IDs, which is UTC.
#
# To improve performance, we only use mktime on the date ($year-$mon-$day),
# and only calculate it if the date is different to the previous time we
# came here. We then add on seconds for the '$hour:$min:$sec'.
#
# We also store the results of the last conversion done, and only
# recalculate if the date is different.
#
# We used to have the '-cache' flag which would store the results of the
# mktime() call. However, the current way of just using mktime() on the
# date obsoletes this.
#######################################################################
sub seconds {
  my($timestamp) = @_;

  # Is the timestamp the same as the last one?
  return $last_time if ($last_timestamp eq $timestamp);

  return 0 unless ($timestamp =~ /^((\d{4})\-(\d\d)-(\d\d))\s(\d\d):(\d\d):(\d\d)(?:\.\d+)?( ([+-])(\d\d)(\d\d))?/o);

  unless ($last_date eq $1) {
    $last_date = $1;
    my(@timestamp) = (0,0,0,$4,$3,$2);
    $timestamp[5] -= 1900;
    $timestamp[4]--;
    $date_seconds = mktime(@timestamp);
  }
  my $time = $date_seconds + ($5 * 3600) + ($6 * 60) + $7;

  # SC. Use caching. Also note we want seconds not minutes.
  #my($this_offset) = ($10 * 60 + $12) * ($9 . "1") if defined $8;
  if (defined $8 && ($8 ne $last_offset)) {
    $last_offset = $8;
    $offset_seconds = ($10 * 60 + $11) * 60;
    $offset_seconds = -$offset_seconds if ($9 eq '-');
  }


  if (defined $8) {
    #$time -= $this_offset;
    $time -= $offset_seconds;
  } elsif (defined $localtime_offset) {
    $time -= $localtime_offset;
  }

  # Store the last timestamp received.
  $last_timestamp = $timestamp;
  $last_time      = $time;

  $time;
}


#######################################################################
#  id_seconds();
#
#  $time = id_seconds($message_id);
#
# Given a message ID, convert it into a time() value.
#######################################################################
sub id_seconds {
my($sub_id) = substr((pop @_), 0, 6);
my($s) = 0;
my(@c) = split(//, $sub_id);
while($#c >= 0) { $s = $s * 62 + $tab62[ord(shift @c) - ord('0')] }
$s;
}

#######################################################################
#  wdhms_seconds();
#
#  $seconds = wdhms_seconds($string);
#
# Convert a string in a week/day/hour/minute/second format (eg 4h10s)
# into seconds.
#######################################################################
sub wdhms_seconds {
  if ($_[0] =~ /^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?(?:(\d+)s)?/) {
    return((($1||0) * $WEEK) + (($2||0) * $DAY) + (($3||0) * $HOUR) + (($4||0) * $MINUTE) + ($5||0));
  }
  return undef;
}

#######################################################################
#  queue_time();
#
#  $queued = queue_time($completed_tod, $arrival_time, $id);
#
# Given the completed time of day and either the arrival time
# (preferred), or the message ID, calculate how long the message has
# been on the queue.
#
#######################################################################
sub queue_time {
  my($completed_tod, $arrival_time, $id) = @_;

  # Note: id_seconds() benchmarks as 42% slower than seconds()
  # and computing the time accounts for a significant portion of
  # the run time.
  if (defined $arrival_time) {
    return(seconds($completed_tod) - seconds($arrival_time));
  }
  else {
    return(seconds($completed_tod) - id_seconds($id));
  }
}


#######################################################################
#  calculate_localtime_offset();
#
#  $localtime_offset = calculate_localtime_offset();
#
# Calculate the the localtime offset from gmtime in seconds.
#
#  $localtime = time() + $localtime_offset.
#
# These are the same semantics as ISO 8601 and RFC 2822 timezone offsets.
# (West is negative, East is positive.)
#######################################################################

# $localtime = gmtime() + $localtime_offset.  OLD COMMENT
# This subroutine commented out as it's not currently in use.

#sub calculate_localtime_offset {
#  # Pick an arbitrary date, convert it to localtime & gmtime, and return the difference.
#  my (@sample_date) = (0,0,0,5,5,100);
#  my $localtime = timelocal(@sample_date);
#  my $gmtime    = timegm(@sample_date);
#  my $offset = $localtime - $gmtime;
#  return $offset;
#}

sub calculate_localtime_offset {
  # Assume that the offset at the moment is valid across the whole
  # period covered by the logs that we're analysing. This may not
  # be true around the time the clocks change in spring or autumn.
  my $utc = time;
  # mktime works on local time and gmtime works in UTC
  my $local = mktime(gmtime($utc));
  return $local - $utc;
}



#######################################################################
# print_duration_table();
#
#  print_duration_table($title, $message_type, \@times, \@values, $overflow);
#
# Print a table showing how long a particular step took for
# the messages. The parameters are:
#   $title         Eg "Time spent on the queue"
#   $message_type  Eg "Remote"
#   \@times        The maximum time a message took for it to increment
#                  the corresponding @values counter.
#   \@values       An array of message counters.
#   $overflow      The number of messages which exceeded the maximum
#                  time.
#######################################################################
sub print_duration_table {
no integer;
my($title, $message_type, $times_aref, $values_aref, $overflow) = @_;
my(@chartdatanames);
my(@chartdatavals);

my $printed_one = 0;
my $cumulative_percent = 0;

my $queue_total = $overflow;
map {$queue_total += $_} @$values_aref;

my $temp = "$title: $message_type";


my $txt_format = "%5s %4s   %6d %5.1f%%  %5.1f%%\n";
my $htm_format = "<tr><td align=\"right\">%s %s</td><td align=\"right\">%d</td><td align=\"right\">%5.1f%%</td><td align=\"right\">%5.1f%%</td>\n";

# write header
printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
if ($htm_fh) {
  print $htm_fh "<hr><a name=\"$title $message_type\"></a><h2>$temp</h2>\n";
  print $htm_fh "<table border=0 width=\"100%\"><tr><td><table border=1>\n";
  print $htm_fh "<tr><th>Time</th><th>Messages</th><th>Percentage</th><th>Cumulative Percentage</th>\n";
}
if ($xls_fh) {
  $ws_global->write($row++, $col, "$title: ".$message_type, $f_header2);
  my @content=("Time", "Messages", "Percentage", "Cumulative Percentage");
  &set_worksheet_line($ws_global, $row++, 1, \@content, $f_headertab);
}


for ($i = 0; $i <= $#$times_aref; ++$i) {
  if ($$values_aref[$i] > 0)
    {
    my $percent = ($values_aref->[$i] * 100)/$queue_total;
    $cumulative_percent += $percent;

    my @content=($printed_one? "     " : "Under",
        format_time($times_aref->[$i]),
        $values_aref->[$i], $percent, $cumulative_percent);

    if ($htm_fh) {
      printf $htm_fh ($htm_format, @content);
      if (!defined($values_aref->[$i])) {
        print $htm_fh "Not defined";
      }
    }
    if ($txt_fh) {
      printf $txt_fh ($txt_format, @content);
      if (!defined($times_aref->[$i])) {
        print $txt_fh "Not defined";
      }
    }
    if ($xls_fh)
    {
      no integer;
      &set_worksheet_line($ws_global, $row, 0, [@content[0,1,2]], $f_default);
      &set_worksheet_line($ws_global, $row++, 3, [$content[3]/100,$content[4]/100], $f_percent);

      if (!defined($times_aref->[$i])) {
        $col=0;
        $ws_global->write($row++, $col, "Not defined"  );
      }
    }

    push(@chartdatanames,
      ($printed_one? "" : "Under") . format_time($times_aref->[$i]));
    push(@chartdatavals, $$values_aref[$i]);
    $printed_one = 1;
  }
}

if ($overflow && $overflow > 0) {
  my $percent = ($overflow * 100)/$queue_total;
  $cumulative_percent += $percent;

    my @content = ("Over ", format_time($times_aref->[-1]),
        $overflow, $percent, $cumulative_percent);

    printf $txt_fh ($txt_format, @content) if $txt_fh;
    printf $htm_fh ($htm_format, @content) if $htm_fh;
    if ($xls_fh)
    {
      &set_worksheet_line($ws_global, $row, 0, [@content[0,1,2]], $f_default);
      &set_worksheet_line($ws_global, $row++, 3, [$content[3]/100,$content[4]/100], $f_percent);
    }

}

push(@chartdatanames, "Over " . format_time($times_aref->[-1]));
push(@chartdatavals, $overflow);

#printf("Unknown   %6d\n", $queue_unknown) if $queue_unknown > 0;
if ($htm_fh) {
  print $htm_fh "</table></td><td>";

  if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0)) {
    my @data = (
       \@chartdatanames,
       \@chartdatavals
    );
    my $graph = GD::Graph::pie->new(200, 200);
    my $pngname = "$title-$message_type.png";
    $pngname =~ s/[^\w\-\.]/_/;

    my $graph_title = "$title ($message_type)";
    $graph->set(title => $graph_title) if (length($graph_title) < 21);

    my $gd = $graph->plot(\@data) or warn($graph->error);
    if ($gd) {
      open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
      binmode IMG;
      print IMG $gd->png;
      close IMG;
      print $htm_fh "<img src=\"$chartrel/$pngname\">";
    }
  }
  print $htm_fh "</td></tr></table>\n";
}

if ($xls_fh)
{
  $row++;
}
print $txt_fh "\n" if $txt_fh;
print $htm_fh "\n" if $htm_fh;

}


#######################################################################
# print_histogram();
#
#  print_histogram('Deliveries|Messages received|$pattern', $unit, @interval_count);
#
# Print a histogram of the messages delivered/received per time slot
# (hour by default).
#######################################################################
sub print_histogram {
my($text, $unit, @interval_count) = @_;
my(@chartdatanames);
my(@chartdatavals);
my($maxd) = 0;

# save first row of print_histogram for xls output
if (!$run_hist) {
  $row_hist = $row;
}
else {
  $row = $row_hist;
}

for ($i = 0; $i < $hist_number; $i++)
  { $maxd = $interval_count[$i] if $interval_count[$i] > $maxd; }

my $scale = int(($maxd + 25)/50);
$scale = 1 if $scale == 0;

if ($scale != 1) {
  if ($unit !~ s/y$/ies/) {
    $unit .= 's';
  }
}

# make and output title
my $title = sprintf("$text per %s",
    ($hist_interval == 60)? "hour" :
    ($hist_interval == 1)?  "minute" : "$hist_interval minutes");

my $txt_htm_title = $title . " (each dot is $scale $unit)";

printf $txt_fh ("%s\n%s\n\n", $txt_htm_title, "-" x length($txt_htm_title)) if $txt_fh;

if ($htm_fh) {
  print $htm_fh "<hr><a name=\"$text\"></a><h2>$txt_htm_title</h2>\n";
  print $htm_fh "<table border=0 width=\"100%\">\n";
  print $htm_fh "<tr><td><pre>\n";
}

if ($xls_fh) {
  $title =~ s/Messages/Msg/ ;
  $row += 2;
  $ws_global->write($row++, $col_hist+1, $title, $f_headertab);
}


my $hour = 0;
my $minutes = 0;
for ($i = 0; $i < $hist_number; $i++) {
  my $c = $interval_count[$i];

  # If the interval is an hour (the maximum) print the starting and
  # ending hours as a label. Otherwise print the starting hour and
  # minutes, which take up the same space.

  my $temp;
  if ($hist_opt == 1) {
    $temp = sprintf("%02d-%02d", $hour, $hour + 1);

    print $txt_fh $temp if $txt_fh;
    print $htm_fh $temp if $htm_fh;

    if ($xls_fh) {
      if ($run_hist==0) {
        # only on first run
        $ws_global->write($row, 0, [$temp], $f_default);
      }
    }

    push(@chartdatanames, $temp);
    $hour++;
  }
  else {
    if ($minutes == 0)
      { $temp = sprintf("%02d:%02d", $hour, $minutes) }
    else
      { $temp = sprintf("  :%02d", $minutes) }

    print $txt_fh $temp if $txt_fh;
    print $htm_fh $temp if $htm_fh;
    if (($xls_fh) and ($run_hist==0)) {
      # only on first run
      $temp = sprintf("%02d:%02d", $hour, $minutes);
      $ws_global->write($row, 0, [$temp], $f_default);
    }

    push(@chartdatanames, $temp);
    $minutes += $hist_interval;
    if ($minutes >= 60) {
      $minutes = 0;
      $hour++;
    }
  }
  push(@chartdatavals, $c);

  printf $txt_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $txt_fh;
  printf $htm_fh (" %6d %s\n", $c, "." x ($c/$scale)) if $htm_fh;
  $ws_global->write($row++, $col_hist+1, [$c], $f_default) if $xls_fh;

} #end for

printf $txt_fh "\n" if $txt_fh;
printf $htm_fh "\n" if $htm_fh;

if ($htm_fh)
{
  print $htm_fh "</pre>\n";
  print $htm_fh "</td><td>\n";
  if ($HAVE_GD_Graph_linespoints && $charts && ($#chartdatavals > 0)) {
    # calculate the graph
    my @data = (
       \@chartdatanames,
       \@chartdatavals
    );
    my $graph = GD::Graph::linespoints->new(300, 300);
    $graph->set(
        x_label           => 'Time',
        y_label           => 'Amount',
        title             => $text,
        x_labels_vertical => 1
    );
    my $pngname = "histogram_$text.png";
    $pngname =~ s/[^\w\._]/_/g;

    my $gd = $graph->plot(\@data) or warn($graph->error);
    if ($gd) {
      open(IMG, ">$chartdir/$pngname") or die "Could not write $chartdir/$pngname: $!\n";
      binmode IMG;
      print IMG $gd->png;
      close IMG;
      print $htm_fh "<img src=\"$chartrel/$pngname\">";
    }
  }
  print $htm_fh "</td></tr></table>\n";
}

$col_hist++; # where to continue next times

$row+=2;     # leave some space after history block
$run_hist=1; # we have done this once or more
}



#######################################################################
# print_league_table();
#
#  print_league_table($league_table_type,\%message_count,\%address_count,\%message_data,\%message_data_gigs, $spreadsheet, $row_sref);
#
# Given hashes of message count, address count, and message data,
# which are keyed by the table type (eg by the sending host), print a
# league table showing the top $topcount (defaults to 50).
#######################################################################
sub print_league_table {
  my($text,$m_count,$a_count,$m_data,$m_data_gigs,$spreadsheet, $row_sref) = @_;
  my($name) = ($topcount == 1)? "$text" : "$topcount ${text}s";
  my($title) = "Top $name by message count";
  my(@chartdatanames) = ();
  my(@chartdatavals) = ();
  my $chartotherval = 0;
  $text = ucfirst($text);

  # Align non-local addresses to the right (so all the .com's line up).
  # Local addresses are aligned on the left as they are userids.
  my $align = ($text !~ /local/i) ? 'right' : 'left';


  ################################################
  # Generate the printf formats and table headers.
  ################################################
  my(@headers) = ('Messages');
  #push(@headers,'Addresses') if defined $a_count;
  push(@headers,'Addresses') if defined $a_count && %$a_count;
  push(@headers,'Bytes','Average') if defined $m_data;

  my $txt_format = "%10s " x @headers . "  %s\n";
  my $txt_col_headers = sprintf $txt_format, @headers, $text;
  my $htm_format = "<tr>" . '<td align="right">%s</td>'x@headers . "<td align=\"$align\" nowrap>%s</td></tr>\n";
  my $htm_col_headers = sprintf $htm_format, @headers, $text;
  $htm_col_headers =~ s/(<\/?)td/$1th/g;      #Convert <td>'s to <th>'s for the header.


  ################################################
  # Write the table headers
  ################################################
  printf $txt_fh ("%s\n%s\n%s", $title, "-" x length($title),$txt_col_headers) if $txt_fh;

  if ($htm_fh) {
    print $htm_fh <<EoText;
<hr><a name="$text count"></a><h2>$title</h2>
<table border=0 width="100%">
<tr><td>
<table border=1>
EoText
  print $htm_fh $htm_col_headers
  }

  if ($xls_fh) {
    $spreadsheet->write(${$row_sref}++, 0, $title, $f_header2);
    $spreadsheet->write(${$row_sref}++, 0, [@headers, $text], $f_headertab);
  }


  # write content
  foreach my $key (top_n_sort($topcount,$m_count,$m_data_gigs,$m_data)) {

    # When displaying the average figures, we calculate the average of
    # the rounded data, as the user would calculate it. This reduces
    # the accuracy slightly, but we have to do it this way otherwise
    # when using -merge to convert results from text to HTML and
    # vice-versa discrepencies would occur.
    my $messages  = $$m_count{$key};
    my @content = ($messages);
    push(@content, $$a_count{$key}) if defined $a_count;
    if (defined $m_data) {
      my $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
      my($data,$gigs) = (0,0);
      un_round($rounded_volume,\$data,\$gigs);
      my $rounded_average = volume_rounded($data/$messages,$gigs/$messages);
      push(@content, $rounded_volume, $rounded_average);
    }

    # write content
    printf $txt_fh ($txt_format, @content, $key) if $txt_fh;

    if ($htm_fh) {
      my $htmlkey = $key;
      $htmlkey =~ s/>/\&gt\;/g;
      $htmlkey =~ s/</\&lt\;/g;
      printf $htm_fh ($htm_format, @content, $htmlkey);
    }
    $spreadsheet->write(${$row_sref}++, 0, [@content, $key], $f_default) if $xls_fh;

    if (scalar @chartdatanames < $ntopchart) {
      push(@chartdatanames, $key);
      push(@chartdatavals, $$m_count{$key});
    }
    else {
      $chartotherval += $$m_count{$key};
    }
  }

  push(@chartdatanames, "Other");
  push(@chartdatavals, $chartotherval);

  print $txt_fh "\n" if $txt_fh;
  if ($htm_fh) {
    print $htm_fh "</table>\n";
    print $htm_fh "</td><td>\n";
    if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0))
      {
      # calculate the graph
      my @data = (
         \@chartdatanames,
         \@chartdatavals
      );
      my $graph = GD::Graph::pie->new(300, 300);
      $graph->set(
          x_label           => 'Name',
          y_label           => 'Amount',
          title             => 'By count',
      );
      my $gd = $graph->plot(\@data) or warn($graph->error);
      if ($gd) {
        my $temp = $text;
        $temp =~ s/ /_/g;
        open(IMG, ">$chartdir/${temp}_count.png") or die "Could not write $chartdir/${temp}_count.png: $!\n";
        binmode IMG;
        print IMG $gd->png;
        close IMG;
        print $htm_fh "<img src=\"$chartrel/${temp}_count.png\">";
      }
    }
    print $htm_fh "</td><td>\n";
    print $htm_fh "</td></tr></table>\n\n";
  }
  ++${$row_sref} if $xls_fh;


  if (defined $m_data) {
    # write header

    $title = "Top $name by volume";

    printf $txt_fh ("%s\n%s\n%s", $title, "-" x length($title),$txt_col_headers) if $txt_fh;

    if ($htm_fh) {
      print $htm_fh <<EoText;
<hr><a name="$text volume"></a><h2>$title</h2>
<table border=0 width="100%">
<tr><td>
<table border=1>
EoText
    print $htm_fh $htm_col_headers;
    }
    if ($xls_fh) {
      $spreadsheet->write(${$row_sref}++, 0, $title, $f_header2);
      $spreadsheet->write(${$row_sref}++, 0, [@headers, $text], $f_headertab);
    }

    @chartdatanames = ();
    @chartdatavals = ();
    $chartotherval = 0;
    my $use_gig = 0;
    foreach my $key (top_n_sort($topcount,$m_data_gigs,$m_data,$m_count)) {
      # The largest volume will be the first (top of the list).
      # If it has at least 1 gig, then just use gigabytes to avoid
      # risking an integer overflow when generating the pie charts.
      if ($$m_data_gigs{$key}) {
        $use_gig = 1;
      }

      my $messages  = $$m_count{$key};
      my @content = ($messages);
      push(@content, $$a_count{$key}) if defined $a_count;
      my $rounded_volume = volume_rounded($$m_data{$key},$$m_data_gigs{$key});
      my($data ,$gigs) = (0,0);
      un_round($rounded_volume,\$data,\$gigs);
      my $rounded_average = volume_rounded($data/$messages,$gigs/$messages);
      push(@content, $rounded_volume, $rounded_average );

      # write content
      printf $txt_fh ($txt_format, @content, $key) if $txt_fh;
      if ($htm_fh) {
        my $htmlkey = $key;
        $htmlkey =~ s/>/\&gt\;/g;
        $htmlkey =~ s/</\&lt\;/g;
        printf $htm_fh ($htm_format, @content, $htmlkey);
      }
      $spreadsheet->write(${$row_sref}++, 0, [@content, $key], $f_default) if $xls_fh;


      if (scalar @chartdatanames < $ntopchart) {
        if ($use_gig) {
          if ($$m_data_gigs{$key}) {
            push(@chartdatanames, $key);
            push(@chartdatavals, $$m_data_gigs{$key});
          }
        }
        else {
          push(@chartdatanames, $key);
          push(@chartdatavals, $$m_data{$key});
        }
      }
      else {
        $chartotherval += ($use_gig) ? $$m_data_gigs{$key} : $$m_data{$key};
      }
    }
    push(@chartdatanames, "Other");
    push(@chartdatavals, $chartotherval);

    print $txt_fh "\n" if $txt_fh;
    if ($htm_fh) {
      print $htm_fh "</table>\n";
      print $htm_fh "</td><td>\n";
      if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals > 0)) {
        # calculate the graph
        my @data = (
           \@chartdatanames,
           \@chartdatavals
        );
        my $graph = GD::Graph::pie->new(300, 300);
        $graph->set(
            x_label           => 'Name',
            y_label           => 'Volume' ,
            title             => 'By Volume',
        );
        my $gd = $graph->plot(\@data) or warn($graph->error);
        if ($gd) {
          my $temp = $text;
          $temp =~ s/ /_/g;
          open(IMG, ">$chartdir/${temp}_volume.png") or die "Could not write $chartdir/${temp}_volume.png: $!\n";
          binmode IMG;
          print IMG $gd->png;
          close IMG;
          print $htm_fh "<img src=\"$chartrel/${temp}_volume.png\">";
        }
      }
      print $htm_fh "</td><td>\n";
      print $htm_fh "</td></tr></table>\n\n";
    }

    ++${$row_sref} if $xls_fh;
  }
}


#######################################################################
# top_n_sort();
#
#   @sorted_keys = top_n_sort($n,$href1,$href2,$href3);
#
# Given a hash which has numerical values, return the sorted $n keys which
# point to the top values. The second and third hashes are used as
# tiebreakers. They all must have the same keys.
#
# The idea behind this routine is that when you only want to see the
# top n members of a set, rather than sorting the entire set and then
# plucking off the top n, sort through the stack as you go, discarding
# any member which is lower than your current n'th highest member.
#
# This proves to be an order of magnitude faster for large hashes.
# On 200,000 lines of mainlog it benchmarked 9 times faster.
# On 700,000 lines of mainlog it benchmarked 13.8 times faster.
#
# We assume the values are > 0.
#######################################################################
sub top_n_sort {
  my($n,$href1,$href2,$href3) = @_;

  # PH's original sort was:
  #
  # foreach $key (sort
  #               {
  #               $$m_count{$b}     <=> $$m_count{$a} ||
  #               $$m_data_gigs{$b} <=> $$m_data_gigs{$a}  ||
  #               $$m_data{$b}      <=> $$m_data{$a}  ||
  #               $a cmp $b
  #               }
  #             keys %{$m_count})
  #

  #We use a key of '_' to represent non-existant values, as null keys are valid.
  #'_' is not a valid domain, edomain, host, or email.
  my(@top_n_keys) = ('_') x $n;
  my($minimum_value1,$minimum_value2,$minimum_value3) = (0,0,0);
  my $top_n_key = '';
  my $n_minus_1 = $n - 1;
  my $n_minus_2 = $n - 2;

  # Create a dummy hash incase the user has not provided us with
  # tiebreaker hashes.
  my(%dummy_hash);
  $href2 = \%dummy_hash unless defined $href2;
  $href3 = \%dummy_hash unless defined $href3;

  # Pick out the top $n keys.
  my($key,$value1,$value2,$value3,$i,$comparison,$insert_position);
  while (($key,$value1) = each %$href1) {

    #print STDERR "key $key ($value1,",$href2->{$key},",",$href3->{$key},") <=> ($minimum_value1,$minimum_value2,$minimum_value3)\n";

    # Check to see that the new value is bigger than the lowest of the
    # top n keys that we're keeping. We test the main key first, because
    # for the majority of cases we can skip creating dummy hash values
    # should the user have not provided real tie-breaking hashes.
    next unless $value1 >= $minimum_value1;

    # Create a dummy hash entry for the key if required.
    # Note that setting the dummy_hash value sets it for both href2 &
    # href3. Also note that currently we are guaranteed to have a real
    # value for href3 if a real value for href2 exists so don't need to
    # test for it as well.
    $dummy_hash{$key} = 0 unless exists $href2->{$key};

    $comparison = $value1        <=> $minimum_value1 ||
                  $href2->{$key} <=> $minimum_value2 ||
                  $href3->{$key} <=> $minimum_value3 ||
                  $top_n_key cmp $key;
    next unless ($comparison == 1);

    # As we will be using these values a few times, extract them into scalars.
    $value2 = $href2->{$key};
    $value3 = $href3->{$key};

    # This key is bigger than the bottom n key, so the lowest position we
    # will insert it into is $n minus 1 (the bottom of the list).
    $insert_position = $n_minus_1;

    # Now go through the list, stopping when we find a key that we're
    # bigger than, or we come to the penultimate position - we've
    # already tested bigger than the last.
    #
    # Note: we go top down as the list starts off empty.
    # Note: stepping through the list in this way benchmarks nearly
    # three times faster than doing a sort() on the reduced list.
    # I assume this is because the list is already in order, and
    # we get a performance boost from not having to do hash lookups
    # on the new key.
    for ($i = 0; $i < $n_minus_1; $i++) {
      $top_n_key = $top_n_keys[$i];
      if ( ($top_n_key eq '_') ||
           ( ($value1 <=> $href1->{$top_n_key} ||
              $value2 <=> $href2->{$top_n_key} ||
              $value3 <=> $href3->{$top_n_key} ||
              $top_n_key cmp $key) == 1
           )
         ) {
        $insert_position = $i;
        last;
      }
    }

    # Remove the last element, then insert the new one.
    $#top_n_keys = $n_minus_2;
    splice(@top_n_keys,$insert_position,0,$key);

    # Extract our new minimum values.
    $top_n_key = $top_n_keys[$n_minus_1];
    if ($top_n_key ne '_') {
      $minimum_value1 = $href1->{$top_n_key};
      $minimum_value2 = $href2->{$top_n_key};
      $minimum_value3 = $href3->{$top_n_key};
    }
  }

  # Return the top n list, grepping out non-existant values, just in case
  # we didn't have that many values.
  return(grep(!/^_$/,@top_n_keys));
}



#######################################################################
# html_header();
#
#  $header = html_header($title);
#
# Print our HTML header and start the <body> block.
#######################################################################
sub html_header {
  my($title) = @_;
  my $text = << "EoText";
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-15">
<title>$title</title>
</head>
<body bgcolor="white">
<h1>$title</h1>
EoText
  return $text;
}



#######################################################################
# help();
#
#  help();
#
# Display usage instructions and exit.
#######################################################################
sub help {
  print << "EoText";

eximstats Version $VERSION

Usage:
  eximstats [Output] [Options] mainlog1 mainlog2 ...
  eximstats -merge -html [Options] report.1.html ... > weekly_rep.html

Examples:
  eximstats -html=eximstats.html mainlog1 mainlog2 ...
  eximstats mainlog1 mainlog2 ... > report.txt

Parses exim mainlog or syslog files and generates a statistical analysis
of the messages processed.

Valid output types are:
-txt[=<file>]   plain text (default unless no other type is specified)
-html[=<file>]  HTML
-xls[=<file>]   Excel
With no type and file given, defaults to -txt and STDOUT.

Valid options are:
-h<number>      histogram divisions per hour. The default is 1, and
                0 suppresses histograms. Other valid values are:
                2, 3, 5, 10, 15, 20, 30 or 60.
-ne             don't display error information
-nr             don't display relaying information
-nr/pattern/    don't display relaying information that matches
-nt             don't display transport information
-nt/pattern/    don't display transport information that matches
-nvr            don't do volume rounding. Display in bytes, not KB/MB/GB.
-t<number>      display top <number> sources/destinations
                default is 50, 0 suppresses top listing
-tnl            omit local sources/destinations in top listing
-t_remote_users show top user sources/destinations from non-local domains
-q<list>        list of times for queuing information. -q0 suppresses.
-show_rt<list>  Show the receipt times for all the messages.
-show_dt<list>  Show the delivery times for all the messages.
                <list> is an optional list of times in seconds.
                Eg -show_rt1,2,4,8.

-include_original_destination   show both the final and original
                destinations in the results rather than just the final ones.

-byhost         show results by sending host (default unless bydomain or
                byemail is specified)
-bydomain       show results by sending domain.
-byemail        show results by sender's email address
-byedomain      show results by sender's email domain
-bylocaldomain  show results by local domain

-pattern "Description" /pattern/
                Count lines matching specified patterns and show them in
                the results. It can be specified multiple times. Eg:
                -pattern 'Refused connections' '/refused connection/'

-merge          merge previously generated reports into a new report

-charts         Create charts (this requires the GD::Graph modules).
                Only valid with -html.
-chartdir <dir> Create the charts' png files in the directory <dir>
-chartrel <dir> Specify the relative directory for the "img src=" tags
                from where to include the charts in the html file
                -chartdir and -chartrel default to '.'

-emptyok        It is OK if there is no valid input, don't print an error.

-d              Debug mode - dump the eval'ed parser onto STDERR.

EoText

  exit 1;
}



#######################################################################
# generate_parser();
#
#  $parser = generate_parser();
#
# This subroutine generates the parsing routine which will be
# used to parse the mainlog. We take the base operation, and remove bits not in use.
# This improves performance depending on what bits you take out or add.
#
# I've tested using study(), but this does not improve performance.
#
# We store our parsing routing in a variable, and process it looking for #IFDEF (Expression)
# or #IFNDEF (Expression) statements and corresponding #ENDIF (Expression) statements. If
# the expression evaluates to true, then it is included/excluded accordingly.
#######################################################################
sub generate_parser {
  my $parser = '
  my($ip,$host,$email,$edomain,$domain,$thissize,$size,$old,$new);
  my($tod,$m_hour,$m_min,$id,$flag,$extra,$length);
  my($seconds,$queued,$rcpt_time,$local_domain);
  my $rej_id = 0;
  while (<$fh>) {

    # Convert syslog lines to mainlog format.
    if (! /^\\d{4}/) {
      next unless s/^.*? exim\\b.*?: //;
    }

    $length = length($_);
    next if ($length < 38);
    next unless /^
		(\\d{4}\\-\\d\\d-\\d\\d\\s	# 1: YYYYMMDD HHMMSS
			(\\d\\d)		# 2: HH
			:
			(\\d\\d)		# 3: MM
			:\\d\\d
		)
		(\\.\\d+)?			# 4: subseconds
		(\s[-+]\\d\\d\\d\\d)?		# 5: tz-offset
		(\s\\[\\d+\\])?			# 6: pid
		/ox;

    $tod = defined($5) ?  $1 . $5 : $1;
    ($m_hour,$m_min) = ($2,$3);

    # PH - watch for GMT offsets in the timestamp.
    if (defined($5)) {
      $extra = 6;
      next if ($length < 44);
    }
    else {
      $extra = 0;
    }

    # watch for subsecond precision
    if (defined($4)) {
      $extra += length($4);
      next if ($length < 38 + $extra);
    }

    # PH - watch for PID added after the timestamp.
    if (defined($6)) {
      $extra += length($6);
      next if ($length < 38 + $extra);
    }

    # $id = substr($_, 20 + $extra, 16);	# old ID was 16 chars
    $id = substr($_, 20 + $extra, 23);		# new IS is 23 chars
    $id =~ s/(\S+).*/$1/;
    $extra += length($id) - 16;

    $flag = substr($_, 37 + $extra, 2);

    if ($flag !~ /^([<>=*-]+|SA)$/ && /rejected|refused|dropped/) {
      $flag = "Re";
      $extra -= 3;
    }

    # Rejects can have no MSGID...
    if ($flag eq "Re" && $id !~ /^[-0-9a-zA-Z]+$/) {
      $id   = "reject:" . ++$rej_id;
      $extra -= 17;
    }
';

  # Watch for user specified patterns.
  my $user_pattern_index = 0;
  foreach (@user_patterns) {
    $user_pattern_totals[$user_pattern_index] = 0;
    $parser .= "    if ($_) {\n";
    $parser .= "      \$user_pattern_totals[$user_pattern_index]++;\n";
    $parser .= "      \$user_pattern_interval_count[$user_pattern_index][(\$m_hour*60 + \$m_min)/$hist_interval]++;\n" if ($hist_opt > 0);
    $parser .= "    }\n";
    $user_pattern_index++;
  }

  $parser .= '
    next unless ($flag =~ /<=|=>|->|==|\\*\\*|Co|SA|Re/);

    #Strip away the timestamp, ID and flag to speed up later pattern matches.
    #The flags include Co (Completed), Re (Rejected), and SA (SpamAssassin).
    $_ = substr($_, 40 + $extra);  # PH

    # Alias @message to the array of information about the message.
    # This minimises the number of calls to hash functions.
    $messages{$id} = [] unless exists $messages{$id};
    *message = $messages{$id};


    # JN - Skip over certain transports as specified via the "-nt/.../" command
    # line switch (where ... is a perl style regular expression).  This is
    # required so that transports that skew stats such as SpamAssassin can be
    # ignored.
    #IFDEF ($transport_pattern)
    if (/\\sT=(\\S+)/) {
       next if ($1 =~ /$transport_pattern/o) ;
    }
    #ENDIF ($transport_pattern)



    # Do some pattern matches to get the host and IP address.
    # We expect lines to be of the form "H=[IpAddr]" or "H=Host [IpAddr]" or
    # "H=Host (UnverifiedHost) [IpAddr]" or "H=(UnverifiedHost) [IpAddr]".
    # We do 2 separate matches to keep the matches simple and fast.
    # Host is local unless otherwise specified.
    # Watch out for "H=([IpAddr])" in case they send "[IpAddr]" as their HELO!
    $ip = (/\\bH=(?:|.*? )(\\[[^]]+\\])/) ? $1
     # 2008-03-31 06:25:22 Connection from [213.246.33.217]:39456 refused: too many connections from that IP address // .hs
     : (/Connection from (\[\S+\])/) ? $1
     # 2008-03-31 06:52:40 SMTP call from mail.cacoshrf.com (ccsd02.ccsd.local) [69.24.118.229]:4511 dropped: too many nonmail commands (last was "RSET") // .hs
     : (/SMTP call from .*?(\[\S+\])/) ? $1
     : "local";
    $host = (/\\bH=(\\S+)/) ? $1 : "local";

    $domain = "localdomain";  #Domain is localdomain unless otherwise specified.

    #IFDEF ($do_sender{Domain})
    if ($host =~ /^\\[/ || $host =~ /^[\\d\\.]+$/) {
      # Host is just an IP address.
      $domain = $host;
    }
    elsif ($host =~ /^(\\(?)[^\\.]+\\.([^\\.]+\\..*)/) {
      # Remove the host portion from the DNS name. We ensure that we end up
      # with at least xxx.yyy. $host can be "(x.y.z)" or  "x.y.z".
      $domain = lc("$1.$2");
      $domain =~ s/^\\.//;         #Remove preceding dot.
    }
    #ENDIF ($do_sender{Domain})

    #IFDEF ($do_sender{Email})
      #IFDEF ($include_original_destination)
      # Catch both "a@b.com <c@d.com>" and "e@f.com"
      #$email = (/^(\S+) (<(\S*?)>)?/) ? $3 || $1 : "";
      $email = (/^(\S+ (<[^@>]+@?[^>]*>)?)/) ? $1 : "";
      chomp($email);
      #ENDIF ($include_original_destination)

      #IFNDEF ($include_original_destination)
      $email = (/^(\S+)/) ? $1 : "";
      #ENDIF ($include_original_destination)
    #ENDIF ($do_sender{Email})

    #IFDEF ($do_sender{Edomain})
      if (/^(<>|blackhole)/) {
        $edomain = $1;
      }
      #IFDEF ($include_original_destination)
        elsif (/^(\S+ (<\S*?\\@(\S+?)>)?)/) {
          $edomain = $1;
          chomp($edomain);
          $edomain =~ s/@(\S+?)>/"@" . lc($1) . ">"/e;
        }
      #ENDIF ($include_original_destination)
      #IFNDEF ($include_original_destination)
        elsif (/^\S*?\\@(\S+)/) {
          $edomain = lc($1);
        }
      #ENDIF ($include_original_destination)
      else {
        $edomain = "";
      }

    #ENDIF ($do_sender{Edomain})

    if ($tod lt $begin) {
      $begin = $tod;
    }
    elsif ($tod gt $end) {
      $end   = $tod;
    }


    if ($flag eq "<=") {
      $thissize = (/\\sS=(\\d+)( |$)/) ? $1 : 0;
      $message[$SIZE] = $thissize;
      $message[$PROTOCOL] = (/ P=(\S+)/) ? $1 : undef;

      #IFDEF ($show_relay)
      if ($host ne "local") {
        # Save incoming information in case it becomes interesting
        # later, when delivery lines are read.
        my($from) = /^(\\S+)/;
        $message[$FROM_HOST]    = "$host$ip";
        $message[$FROM_ADDRESS] = $from;
      }
      #ENDIF ($show_relay)

      #IFDEF ($local_league_table || $include_remote_users)
        if (/\sU=(\\S+)/) {
          my $user = $1;

          #IFDEF ($local_league_table && $include_remote_users)
          {                         #Store both local and remote users.
          #ENDIF ($local_league_table && $include_remote_users)

          #IFDEF ($local_league_table && ! $include_remote_users)
          if ($host eq "local") {   #Store local users only.
          #ENDIF ($local_league_table && ! $include_remote_users)

          #IFDEF ($include_remote_users && ! $local_league_table)
          if ($host ne "local") {   #Store remote users only.
          #ENDIF ($include_remote_users && ! $local_league_table)

            ++$received_count_user{$user};
            add_volume(\\$received_data_user{$user},\\$received_data_gigs_user{$user},$thissize);
          }
        }
      #ENDIF ($local_league_table || $include_remote_users)

      #IFDEF ($do_sender{Host})
        ++$received_count{Host}{$host};
        add_volume(\\$received_data{Host}{$host},\\$received_data_gigs{Host}{$host},$thissize);
      #ENDIF ($do_sender{Host})

      #IFDEF ($do_sender{Domain})
        if ($domain) {
          ++$received_count{Domain}{$domain};
          add_volume(\\$received_data{Domain}{$domain},\\$received_data_gigs{Domain}{$domain},$thissize);
        }
      #ENDIF ($do_sender{Domain})

      #IFDEF ($do_sender{Email})
        ++$received_count{Email}{$email};
        add_volume(\\$received_data{Email}{$email},\\$received_data_gigs{Email}{$email},$thissize);
      #ENDIF ($do_sender{Email})

      #IFDEF ($do_sender{Edomain})
        ++$received_count{Edomain}{$edomain};
        add_volume(\\$received_data{Edomain}{$edomain},\\$received_data_gigs{Edomain}{$edomain},$thissize);
      #ENDIF ($do_sender{Edomain})

      ++$total_received_count;
      add_volume(\\$total_received_data,\\$total_received_data_gigs,$thissize);

      #IFDEF ($#queue_times >= 0 || $#rcpt_times >= 0)
        $message[$ARRIVAL_TIME] = $tod;
      #ENDIF ($#queue_times >= 0 || $#rcpt_times >= 0)

      #IFDEF ($hist_opt > 0)
        $received_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
      #ENDIF ($hist_opt > 0)
    }

    elsif ($flag eq "=>") {
      $size = $message[$SIZE] || 0;
      if ($host ne "local") {
        $message[$REMOTE_DELIVERED] = 1;


        #IFDEF ($show_relay)
        # Determine relaying address if either only one address listed,
        # or two the same. If they are different, it implies a forwarding
        # or aliasing, which is not relaying. Note that for multi-aliased
        # addresses, there may be a further address between the first
        # and last.

        if (defined $message[$FROM_HOST]) {
          if (/^(\\S+)(?:\\s+\\([^)]\\))?\\s+<([^>]+)>/) {
            ($old,$new) = ($1,$2);
          }
          else {
            $old = $new = "";
          }

          if ("\\L$new" eq "\\L$old") {
            ($old) = /^(\\S+)/ if $old eq "";
            my $key = "H=\\L$message[$FROM_HOST]\\E A=\\L$message[$FROM_ADDRESS]\\E => " .
              "H=\\L$host\\E$ip A=\\L$old\\E";
            if (!defined $relay_pattern || $key !~ /$relay_pattern/o) {
              $relayed{$key} = 0 if !defined $relayed{$key};
              ++$relayed{$key};
            }
            else {
              ++$relayed_unshown;
            }
          }
        }
        #ENDIF ($show_relay)

      }

      #IFDEF ($local_league_table || $include_remote_users)
        #IFDEF ($local_league_table && $include_remote_users)
        {                         #Store both local and remote users.
        #ENDIF ($local_league_table && $include_remote_users)

        #IFDEF ($local_league_table && ! $include_remote_users)
        if ($host eq "local") {   #Store local users only.
        #ENDIF ($local_league_table && ! $include_remote_users)

        #IFDEF ($include_remote_users && ! $local_league_table)
        if ($host ne "local") {   #Store remote users only.
        #ENDIF ($include_remote_users && ! $local_league_table)

          if (my($user) = split((/\\s</)? " <" : " ", $_)) {
            #IFDEF ($include_original_destination)
            {
            #ENDIF ($include_original_destination)
            #IFNDEF ($include_original_destination)
            if ($user =~ /^[\\/|]/) {
            #ENDIF ($include_original_destination)
              #my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
              my($parent) = $_ =~ / (<.+?>) /;              #DT 1.54
              if (defined $parent) {
                $user = "$user $parent";
                #IFDEF ($do_local_domain)
                if ($parent =~ /\\@(.+)>/) {
                  $local_domain = lc($1);
                  ++$delivered_messages_local_domain{$local_domain};
                  ++$delivered_addresses_local_domain{$local_domain};
                  add_volume(\\$delivered_data_local_domain{$local_domain},\\$delivered_data_gigs_local_domain{$local_domain},$size);
                }
                #ENDIF ($do_local_domain)
              }
            }
            ++$delivered_messages_user{$user};
            ++$delivered_addresses_user{$user};
            add_volume(\\$delivered_data_user{$user},\\$delivered_data_gigs_user{$user},$size);
          }
        }
      #ENDIF ($local_league_table || $include_remote_users)

      #IFDEF ($do_sender{Host})
        $delivered_messages{Host}{$host}++;
        $delivered_addresses{Host}{$host}++;
        add_volume(\\$delivered_data{Host}{$host},\\$delivered_data_gigs{Host}{$host},$size);
      #ENDIF ($do_sender{Host})
      #IFDEF ($do_sender{Domain})
        if ($domain) {
          ++$delivered_messages{Domain}{$domain};
          ++$delivered_addresses{Domain}{$domain};
          add_volume(\\$delivered_data{Domain}{$domain},\\$delivered_data_gigs{Domain}{$domain},$size);
        }
      #ENDIF ($do_sender{Domain})
      #IFDEF ($do_sender{Email})
        ++$delivered_messages{Email}{$email};
        ++$delivered_addresses{Email}{$email};
        add_volume(\\$delivered_data{Email}{$email},\\$delivered_data_gigs{Email}{$email},$size);
      #ENDIF ($do_sender{Email})
      #IFDEF ($do_sender{Edomain})
        ++$delivered_messages{Edomain}{$edomain};
        ++$delivered_addresses{Edomain}{$edomain};
        add_volume(\\$delivered_data{Edomain}{$edomain},\\$delivered_data_gigs{Edomain}{$edomain},$size);
      #ENDIF ($do_sender{Edomain})

      ++$total_delivered_messages;
      ++$total_delivered_addresses;
      add_volume(\\$total_delivered_data,\\$total_delivered_data_gigs,$size);

      #IFDEF ($show_transport)
        my $transport = (/\\sT=(\\S+)/) ? $1 : ":blackhole:";
        ++$transported_count{$transport};
        add_volume(\\$transported_data{$transport},\\$transported_data_gigs{$transport},$size);
      #ENDIF ($show_transport)

      #IFDEF ($hist_opt > 0)
        $delivered_interval_count[($m_hour*60 + $m_min)/$hist_interval]++;
      #ENDIF ($hist_opt > 0)

      #IFDEF ($#delivery_times > 0)
        if (/ DT=(\S+)/) {
          $seconds = wdhms_seconds($1);
          for ($i = 0; $i <= $#delivery_times; $i++) {
            if ($seconds < $delivery_times[$i]) {
              ++$dt_all_bin[$i];
              ++$dt_remote_bin[$i] if $message[$REMOTE_DELIVERED];
              last;
            }
          }
          if ($i > $#delivery_times) {
            ++$dt_all_overflow;
            ++$dt_remote_overflow if $message[$REMOTE_DELIVERED];
          }
        }
      #ENDIF ($#delivery_times > 0)

    }

    elsif ($flag eq "->") {

      #IFDEF ($local_league_table || $include_remote_users)
        #IFDEF ($local_league_table && $include_remote_users)
        {                         #Store both local and remote users.
        #ENDIF ($local_league_table && $include_remote_users)

        #IFDEF ($local_league_table && ! $include_remote_users)
        if ($host eq "local") {   #Store local users only.
        #ENDIF ($local_league_table && ! $include_remote_users)

        #IFDEF ($include_remote_users && ! $local_league_table)
        if ($host ne "local") {   #Store remote users only.
        #ENDIF ($include_remote_users && ! $local_league_table)

          if (my($user) = split((/\\s</)? " <" : " ", $_)) {
            #IFDEF ($include_original_destination)
            {
            #ENDIF ($include_original_destination)
            #IFNDEF ($include_original_destination)
            if ($user =~ /^[\\/|]/) {
            #ENDIF ($include_original_destination)
              #my($parent) = $_ =~ /(<[^@]+@?[^>]*>)/;
              my($parent) = $_ =~ / (<.+?>) /;              #DT 1.54
              $user = "$user $parent" if defined $parent;
            }
            ++$delivered_addresses_user{$user};
          }
        }
      #ENDIF ($local_league_table || $include_remote_users)

      #IFDEF ($do_sender{Host})
        $delivered_addresses{Host}{$host}++;
      #ENDIF ($do_sender{Host})
      #IFDEF ($do_sender{Domain})
        if ($domain) {
          ++$delivered_addresses{Domain}{$domain};
        }
      #ENDIF ($do_sender{Domain})
      #IFDEF ($do_sender{Email})
        ++$delivered_addresses{Email}{$email};
      #ENDIF ($do_sender{Email})
      #IFDEF ($do_sender{Edomain})
        ++$delivered_addresses{Edomain}{$edomain};
      #ENDIF ($do_sender{Edomain})

      ++$total_delivered_addresses;
    }

    elsif ($flag eq "==" && defined($message[$SIZE]) && !defined($message[$DELAYED])) {
      ++$delayed_count;
      $message[$DELAYED] = 1;
    }

    elsif ($flag eq "**") {
      if (defined ($message[$SIZE])) {
        unless (defined $message[$HAD_ERROR]) {
          ++$message_errors;
          $message[$HAD_ERROR] = 1;
        }
      }

      #IFDEF ($show_errors)
        ++$errors_count{$_};
      #ENDIF ($show_errors)

    }

    elsif ($flag eq "Co") {
      #Completed?
      #IFDEF ($#queue_times >= 0)
        $queued = queue_time($tod, $message[$ARRIVAL_TIME], $id);

        for ($i = 0; $i <= $#queue_times; $i++) {
          if ($queued < $queue_times[$i]) {
            ++$qt_all_bin[$i];
            ++$qt_remote_bin[$i] if $message[$REMOTE_DELIVERED];
            last;
          }
        }
        if ($i > $#queue_times) {
          ++$qt_all_overflow;
          ++$qt_remote_overflow if $message[$REMOTE_DELIVERED];
        }
      #ENDIF ($#queue_times >= 0)

      #IFDEF ($#rcpt_times >= 0)
        if (/ QT=(\S+)/) {
          $seconds = wdhms_seconds($1);
          #Calculate $queued if not previously calculated above.
          #IFNDEF ($#queue_times >= 0)
            $queued = queue_time($tod, $message[$ARRIVAL_TIME], $id);
          #ENDIF ($#queue_times >= 0)
          $rcpt_time = $seconds - $queued;
          my($protocol);

          if (defined $message[$PROTOCOL]) {
            $protocol = $message[$PROTOCOL];

            # Create the bin if its not already defined.
            unless (exists $rcpt_times_bin{$protocol}) {
              initialise_rcpt_times($protocol);
            }
          }


          for ($i = 0; $i <= $#rcpt_times; ++$i) {
            if ($rcpt_time < $rcpt_times[$i]) {
              ++$rcpt_times_bin{all}[$i];
              ++$rcpt_times_bin{$protocol}[$i] if defined $protocol;
              last;
            }
          }

          if ($i > $#rcpt_times) {
            ++$rcpt_times_overflow{all};
            ++$rcpt_times_overflow{$protocol} if defined $protocol;
          }
        }
      #ENDIF ($#rcpt_times >= 0)

      delete($messages{$id});
    }
    elsif ($flag eq "SA") {
      $ip = (/From.*?(\\[[^]]+\\])/ || /\\((local)\\)/) ? $1 : "";
      #SpamAssassin message
      if (/Action: ((permanently|temporarily) rejected message|flagged as Spam but accepted): score=(\d+\.\d)/) {
        #add_volume(\\$spam_score,\\$spam_score_gigs,$3);
        ++$spam_count_by_ip{$ip};
      } elsif (/Action: scanned but message isn\'t spam: score=(-?\d+\.\d)/) {
        #add_volume(\\$ham_score,\\$ham_score_gigs,$1);
        ++$ham_count_by_ip{$ip};
      } elsif (/(Not running SA because SAEximRunCond expanded to false|check skipped due to message size)/) {
        ++$ham_count_by_ip{$ip};
      }
    }

    # Look for Reject messages or blackholed messages (deliveries
    # without a transport)
    if ($flag eq "Re" || ($flag eq "=>" && ! /\\sT=\\S+/)) {
      # Correct the IP address for rejects:
      # rejected EHLO from my.test.net [10.0.0.5]: syntactically invalid argument(s):
      # rejected EHLO from [10.0.0.6]: syntactically invalid argument(s):
      $ip = $1 if ($ip eq "local" && /^rejected [HE][HE]LO from .*?(\[.+?\]):/);
      if (/SpamAssassin/) {
        ++$rejected_count_by_reason{"Rejected by SpamAssassin"};
        ++$rejected_count_by_ip{$ip};
      }
      elsif (
        /(temporarily rejected [A-Z]*) .*?(: .*?)(:|\s*$)/
        ) {
        ++$temporarily_rejected_count_by_reason{"\u$1$2"};
        ++$temporarily_rejected_count_by_ip{$ip};
      }
      elsif (
        /(temporarily refused connection)/
        ) {
        ++$temporarily_rejected_count_by_reason{"\u$1"};
        ++$temporarily_rejected_count_by_ip{$ip};
      }
      elsif (
        /(listed at [^ ]+)/ ||
        /(Forged IP detected in HELO)/ ||
        /(Invalid domain or IP given in HELO\/EHLO)/ ||
        /(unqualified recipient rejected)/ ||
        /(closed connection (after|in response) .*?)\s*$/ ||
        /(sender rejected)/ ||
        # 2005-09-23 15:07:49 1EInHJ-0007Ex-Au H=(a.b.c) [10.0.0.1] F=<> rejected after DATA: This message contains a virus: (Eicar-Test-Signature) please scan your system.
        # 2005-10-06 10:50:07 1ENRS3-0000Nr-Kt => blackhole (DATA ACL discarded recipients): This message contains a virus: (Worm.SomeFool.P) please scan your system.
        / rejected after DATA: (.*)/ ||
        / (rejected DATA: .*)/ ||
        /.DATA ACL discarded recipients.: (.*)/ ||
        /rejected after DATA: (unqualified address not permitted)/ ||
        /(VRFY rejected)/ ||
#        /(sender verify (defer|fail))/i ||
        /(too many recipients)/ ||
        /(refused relay.*?) to/ ||
        /(rejected by non-SMTP ACL: .*)/ ||
        /(rejected by local_scan.*)/ ||
        # SMTP call from %s dropped: too many syntax or protocol errors (last command was "%s"
        # SMTP call from %s dropped: too many nonmail commands
        /(dropped: too many ((nonmail|unrecognized) commands|syntax or protocol errors))/ ||

        # local_scan() function crashed with signal %d - message temporarily rejected
        # local_scan() function timed out - message temporarily rejected
        /(local_scan.. function .* - message temporarily rejected)/ ||
        # SMTP protocol synchronization error (input sent without waiting for greeting): rejected connection from %s
        /(SMTP protocol .*?(error|violation))/ ||
        /(message too big)/
        ) {
        ++$rejected_count_by_reason{"\u$1"};
        ++$rejected_count_by_ip{$ip};
      }
      elsif (/rejected [HE][HE]LO from [^:]*: syntactically invalid argument/) {
        ++$rejected_count_by_reason{"Rejected HELO/EHLO: syntactically invalid argument"};
        ++$rejected_count_by_ip{$ip};
      }
      elsif (/response to "RCPT TO.*? was: (.*)/) {
        ++$rejected_count_by_reason{"Response to RCPT TO was: $1"};
        ++$rejected_count_by_ip{$ip};
      }
      elsif (
        /(lookup of host )\S+ (failed)/ ||

        # rejected from <%s>%s%s%s%s: message too big:
        /(rejected [A-Z]*) .*?(: .*?)(:|\s*$)/ ||
        # refused connection from %s (host_reject_connection)
        # refused connection from %s (tcp wrappers)
        /(refused connection )from.*? (\(.*)/ ||

        # error from remote mailer after RCPT TO:<a@b.c>: host a.b.c [10.0.0.1]: 450 <a@b.c>: Recipient address rejected: Greylisted for 60 seconds
        # error from remote mailer after MAIL FROM:<> SIZE=3468: host a.b.c [10.0.0.1]: 421 a.b.c has refused your connection because your server did not have a PTR record.
        /(error from remote mailer after .*?:).*(: .*?)(:|\s*$)/ ||

        # a.b.c F=<a@b.c> rejected after DATA: "@" or "." expected after "Undisclosed-Recipient": failing address in "To" header is: <Undisclosed-Recipient:;>
        /rejected after DATA: ("." or "." expected).*?(: failing address in .*? header)/ ||

        # connection from %s refused load average = %.2f
        /(Connection )from.*? (refused: load average)/ ||
        # connection from %s refused (IP options)
        # Connection from %s refused: too many connections
        # connection from %s refused
        /([Cc]onnection )from.*? (refused.*)/ ||
        # [10.0.0.1]: connection refused
        /: (Connection refused)()/
        ) {
        ++$rejected_count_by_reason{"\u$1$2"};
        ++$rejected_count_by_ip{$ip};
      }
      elsif (
        # 2008-03-31 06:25:22 H=mail.densitron.com [216.70.140.224]:45386 temporarily rejected connection in "connect" ACL: too fast reconnects // .hs
        # 2008-03-31 06:25:22 H=mail.densitron.com [216.70.140.224]:45386 temporarily rejected connection in "connect" ACL // .hs
        /(temporarily rejected connection in .*?ACL:?.*)/
        ) {
        ++$temporarily_rejected_count_by_ip{$ip};
        ++$temporarily_rejected_count_by_reason{"\u$1"};
      }
      else {
        ++$rejected_count_by_reason{Unknown};
        ++$rejected_count_by_ip{$ip};
        print STDERR "Unknown rejection: $_" if $debug;
      }
    }
  }';

  # We now do a 'C preprocessor style operation on our parser
  # to remove bits not in use.
  my(%defines_in_operation,$removing_lines,$processed_parser);
  foreach (split (/\n/,$parser)) {
    if ((/^\s*#\s*IFDEF\s*\((.*?)\)/i  && ! eval $1) ||
        (/^\s*#\s*IFNDEF\s*\((.*?)\)/i &&   eval $1)    ) {
      $defines_in_operation{$1} = 1;
      $removing_lines = 1;
    }

    # Convert constants.
    while (/(\$[A-Z][A-Z_]*)\b/) {
      my $constant = eval $1;
      s/(\$[A-Z][A-Z_]*)\b/$constant/;
    }

    $processed_parser .= $_."\n" unless $removing_lines;

    if (/^\s*#\s*ENDIF\s*\((.*?)\)/i) {
      delete $defines_in_operation{$1};
      unless (keys %defines_in_operation) {
        $removing_lines = 0;
      }
    }
  }
  print STDERR "# START OF PARSER:$processed_parser\n# END OF PARSER\n\n" if $debug;

  return $processed_parser;
}



#######################################################################
# parse();
#
#  parse($parser,\*FILEHANDLE);
#
# This subroutine accepts a parser and a filehandle from main and parses each
# line. We store the results into global variables.
#######################################################################
sub parse {
  my($parser,$fh) = @_;

  if ($merge_reports) {
    parse_old_eximstat_reports($fh);
  }
  else {
    eval $parser;
    die ($@) if $@;
  }

}



#######################################################################
# print_header();
#
#  print_header();
#
# Print our headers and contents.
#######################################################################
sub print_header {


  my $title = "Exim statistics from $begin to $end";

  print $txt_fh "\n$title\n" if $txt_fh;
  if ($htm_fh) {
    print $htm_fh html_header($title);
    print $htm_fh "<ul>\n";
    print $htm_fh "<li><a href=\"#Grandtotal\">Grand total summary</a>\n";
    print $htm_fh "<li><a href=\"#Patterns\">User Specified Patterns</a>\n" if @user_patterns;
    print $htm_fh "<li><a href=\"#Transport\">Deliveries by Transport</a>\n" if $show_transport;
    if ($hist_opt) {
      print $htm_fh "<li><a href=\"#Messages received\">Messages received per hour</a>\n";
      print $htm_fh "<li><a href=\"#Deliveries\">Deliveries per hour</a>\n";
    }

    if ($#queue_times >= 0) {
      print $htm_fh "<li><a href=\"#Time spent on the queue all messages\">Time spent on the queue: all messages</a>\n";
      print $htm_fh "<li><a href=\"#Time spent on the queue messages with at least one remote delivery\">Time spent on the queue: messages with at least one remote delivery</a>\n";
    }

    if ($#delivery_times >= 0) {
      print $htm_fh "<li><a href=\"#Delivery times all messages\">Delivery times: all messages</a>\n";
      print $htm_fh "<li><a href=\"#Delivery times messages with at least one remote delivery\">Delivery times: messages with at least one remote delivery</a>\n";
    }

    if ($#rcpt_times >= 0) {
      print $htm_fh "<li><a href=\"#Receipt times all messages\">Receipt times</a>\n";
    }

    print $htm_fh "<li><a href=\"#Relayed messages\">Relayed messages</a>\n" if $show_relay;
    if ($topcount) {
      print $htm_fh "<li><a href=\"#Mail rejection reason count\">Top $topcount mail rejection reasons by message count</a>\n" if %rejected_count_by_reason;
      foreach ('Host','Domain','Email','Edomain') {
        next unless $do_sender{$_};
        print $htm_fh "<li><a href=\"#Sending \l$_ count\">Top $topcount sending \l${_}s by message count</a>\n";
        print $htm_fh "<li><a href=\"#Sending \l$_ volume\">Top $topcount sending \l${_}s by volume</a>\n";
      }
      if (($local_league_table || $include_remote_users) && %received_count_user) {
        print $htm_fh "<li><a href=\"#Local sender count\">Top $topcount local senders by message count</a>\n";
        print $htm_fh "<li><a href=\"#Local sender volume\">Top $topcount local senders by volume</a>\n";
      }
      foreach ('Host','Domain','Email','Edomain') {
        next unless $do_sender{$_};
        print $htm_fh "<li><a href=\"#$_ destination count\">Top $topcount \l$_ destinations by message count</a>\n";
        print $htm_fh "<li><a href=\"#$_ destination volume\">Top $topcount \l$_ destinations by volume</a>\n";
      }
      if (($local_league_table || $include_remote_users) && %delivered_messages_user) {
        print $htm_fh "<li><a href=\"#Local destination count\">Top $topcount local destinations by message count</a>\n";
        print $htm_fh "<li><a href=\"#Local destination volume\">Top $topcount local destinations by volume</a>\n";
      }
      if (($local_league_table || $include_remote_users) && %delivered_messages_local_domain) {
        print $htm_fh "<li><a href=\"#Local domain destination count\">Top $topcount local domain destinations by message count</a>\n";
        print $htm_fh "<li><a href=\"#Local domain destination volume\">Top $topcount local domain destinations by volume</a>\n";
      }

      print $htm_fh "<li><a href=\"#Rejected ip count\">Top $topcount rejected ips by message count</a>\n" if %rejected_count_by_ip;
      print $htm_fh "<li><a href=\"#Temporarily rejected ip count\">Top $topcount temporarily rejected ips by message count</a>\n" if %temporarily_rejected_count_by_ip;
      print $htm_fh "<li><a href=\"#Non-rejected spamming ip count\">Top $topcount non-rejected spamming ips by message count</a>\n" if %spam_count_by_ip;

    }
    print $htm_fh "<li><a href=\"#errors\">List of errors</a>\n" if %errors_count;
    print $htm_fh "</ul>\n<hr>\n";
  }
  if ($xls_fh)
  {
    $ws_global->write($row++, $col+0, "Exim Statistics",  $f_header1);
    &set_worksheet_line($ws_global, $row, $col, ["from:",  $begin,  "to:", $end], $f_default);
    $row+=2;
  }
}


#######################################################################
# print_grandtotals();
#
#  print_grandtotals();
#
# Print the grand totals.
#######################################################################
sub print_grandtotals {

  # Get the sender by headings and results. This is complicated as we can have
  # different numbers of columns.
  my($sender_txt_header,$sender_txt_format,$sender_html_format);
  my(@received_totals,@delivered_totals);
  my($row_tablehead, $row_max);
  my(@col_headers) = ('TOTAL', 'Volume', 'Messages', 'Addresses');

  foreach ('Host','Domain','Email','Edomain') {
    next unless $do_sender{$_};
    if ($merge_reports) {
      push(@received_totals, get_report_total($report_totals{Received},"${_}s"));
      push(@delivered_totals,get_report_total($report_totals{Delivered},"${_}s"));
    }
    else {
      push(@received_totals,scalar(keys %{$received_data{$_}}));
      push(@delivered_totals,scalar(keys %{$delivered_data{$_}}));
    }
    $sender_txt_header  .= " " x ($COLUMN_WIDTHS - length($_)) . $_ . 's';
    $sender_html_format .= "<td align=\"right\">%s</td>";
    $sender_txt_format  .= " " x ($COLUMN_WIDTHS - 5) . "%6s";
    push(@col_headers,"${_}s");
  }

  my $txt_format1 = "  %-16s %9s     %6d    %6s $sender_txt_format";
  my $txt_format2 = "  %6d %4.1f%% %6d %4.1f%%",
  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%s</td><td align=\"right\">%s</td><td align=\"right\">%s</td>$sender_html_format";
  my $htm_format2 = "<td align=\"right\">%d</td><td align=\"right\">%4.1f%%</td><td align=\"right\">%d</td><td align=\"right\">%4.1f%%</td>";

  if ($txt_fh) {
    my $sender_spaces = " " x length($sender_txt_header);
    print $txt_fh "\n";
    print $txt_fh "Grand total summary\n";
    print $txt_fh "-------------------\n";
    print $txt_fh "                                              $sender_spaces           At least one address\n";
    print $txt_fh "  TOTAL               Volume   Messages Addresses $sender_txt_header      Delayed       Failed\n";
  }
  if ($htm_fh) {
    print $htm_fh "<a name=\"Grandtotal\"></a>\n";
    print $htm_fh "<h2>Grand total summary</h2>\n";
    print $htm_fh "<table border=1>\n";
    print $htm_fh "<tr><th>" . join('</th><th>',@col_headers) . "</th><th colspan=2>At least one addr<br>Delayed</th><th colspan=2>At least one addr<br>Failed</th>\n";
  }
  if ($xls_fh) {
    $ws_global->write($row++, 0, "Grand total summary", $f_header2);
    $ws_global->write($row, 0, \@col_headers, $f_header2);
    $ws_global->merge_range($row, scalar(@col_headers), $row, scalar(@col_headers)+1, "At least one addr Delayed", $f_header2_m);
    $ws_global->merge_range($row, scalar(@col_headers)+2, $row, scalar(@col_headers)+3, "At least one addr Failed", $f_header2_m);
    #$ws_global->write(++$row, scalar(@col_headers), ['Total','Percent','Total','Percent'], $f_header2);
  }


  my($volume,$failed_count);
  if ($merge_reports) {
    $volume = volume_rounded($report_totals{Received}{Volume}, $report_totals{Received}{'Volume-gigs'});
    $total_received_count = get_report_total($report_totals{Received},'Messages');
    $failed_count  = get_report_total($report_totals{Received},'Failed');
    $delayed_count = get_report_total($report_totals{Received},'Delayed');
  }
  else {
    $volume = volume_rounded($total_received_data, $total_received_data_gigs);
    $failed_count = $message_errors;
  }

  {
    no integer;

    my @content=(
        $volume,$total_received_count,'',
        @received_totals,
        $delayed_count,
        ($total_received_count) ? ($delayed_count*100/$total_received_count) : 0,
        $failed_count,
        ($total_received_count) ? ($failed_count*100/$total_received_count) : 0
    );

    printf $txt_fh ("$txt_format1$txt_format2\n", 'Received', @content) if $txt_fh;
    printf $htm_fh ("$htm_format1$htm_format2\n", 'Received', @content) if $htm_fh;
    if ($xls_fh) {
      $ws_global->write(++$row, 0, 'Received', $f_default);
      for (my $i=0; $i < scalar(@content); $i++) {
        if ($i == 4 || $i == 6) {
          $ws_global->write($row, $i+1, $content[$i]/100, $f_percent);
        }
        else {
          $ws_global->write($row, $i+1, $content[$i], $f_default);
        }
      }
    }
  }

  if ($merge_reports) {
    $volume = volume_rounded($report_totals{Delivered}{Volume}, $report_totals{Delivered}{'Volume-gigs'});
    $total_delivered_messages = get_report_total($report_totals{Delivered},'Messages');
    $total_delivered_addresses = get_report_total($report_totals{Delivered},'Addresses');
  }
  else {
    $volume = volume_rounded($total_delivered_data, $total_delivered_data_gigs);
  }

  my @content=($volume, $total_delivered_messages, $total_delivered_addresses, @delivered_totals);
  printf $txt_fh ("$txt_format1\n", 'Delivered', @content) if $txt_fh;
  printf $htm_fh ("$htm_format1\n", 'Delivered', @content) if $htm_fh;

  if ($xls_fh) {
    $ws_global->write(++$row, 0, 'Delivered', $f_default);
    for (my $i=0; $i < scalar(@content); $i++) {
      $ws_global->write($row, $i+1, $content[$i], $f_default);
    }
  }

  if ($merge_reports) {
    foreach ('Rejects', 'Temp Rejects', 'Ham', 'Spam') {
      my $messages = get_report_total($report_totals{$_},'Messages');
      my $addresses = get_report_total($report_totals{$_},'Addresses');
      if ($messages) {
        @content = ($_, '', $messages, '');
        push(@content,get_report_total($report_totals{$_},'Hosts')) if $do_sender{Host};
        #These rows do not have entries for the following columns (if specified)
        foreach ('Domain','Email','Edomain') {
          push(@content,'') if $do_sender{$_};
        }

        printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
        printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
        $ws_global->write(++$row, 0, \@content) if $xls_fh;
      }
    }
  }
  else {
    foreach my $total_aref (['Rejects',\%rejected_count_by_ip],
                            ['Temp Rejects',\%temporarily_rejected_count_by_ip],
                            ['Ham',\%ham_count_by_ip],
                            ['Spam',\%spam_count_by_ip]) {
      #Count the number of messages of this type.
      my $messages = 0;
      map {$messages += $_} values %{$total_aref->[1]};

      if ($messages > 0) {
        @content = ($total_aref->[0], '', $messages, '');

        #Count the number of distinct IPs for the Hosts column.
        push(@content,scalar(keys %{$total_aref->[1]})) if $do_sender{Host};

        #These rows do not have entries for the following columns (if specified)
        foreach ('Domain','Email','Edomain') {
          push(@content,'') if $do_sender{$_};
        }

        printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
        printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
        $ws_global->write(++$row, 0, \@content) if $xls_fh;
      }
    }
  }

  printf $txt_fh "\n"         if $txt_fh;
  printf $htm_fh "</table>\n" if $htm_fh;
  ++$row;
}


#######################################################################
# print_user_patterns()
#
#  print_user_patterns();
#
# Print the counts of user specified patterns.
#######################################################################
sub print_user_patterns {
  my $txt_format1 = "  %-18s  %6d";
  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%d</td>";

  if ($txt_fh) {
    print $txt_fh "User Specified Patterns\n";
    print $txt_fh "-----------------------";
    print $txt_fh "\n                       Total\n";
  }
  if ($htm_fh) {
    print $htm_fh "<hr><a name=\"Patterns\"></a><h2>User Specified Patterns</h2>\n";
    print $htm_fh "<table border=0 width=\"100%\">\n";
    print $htm_fh "<tr><td>\n";
    print $htm_fh "<table border=1>\n";
    print $htm_fh "<tr><th>&nbsp;</th><th>Total</th>\n";
  }
  if ($xls_fh) {
      $ws_global->write($row++, $col, "User Specified Patterns", $f_header2);
      &set_worksheet_line($ws_global, $row++, 1, ["Total"], $f_headertab);
  }


  my($key);
  if ($merge_reports) {
    # We are getting our data from previous reports.
    foreach $key (@user_descriptions) {
      my $count = get_report_total($report_totals{patterns}{$key},'Total');
      printf $txt_fh ("$txt_format1\n",$key,$count) if $txt_fh;
      printf $htm_fh ("$htm_format1\n",$key,$count) if $htm_fh;
      if ($xls_fh)
      {
        &set_worksheet_line($ws_global, $row++, 0, [$key,$count], $f_default);
      }
    }
  }
  else {
    # We are getting our data from mainlog files.
    my $user_pattern_index = 0;
    foreach $key (@user_descriptions) {
      printf $txt_fh ("$txt_format1\n",$key,$user_pattern_totals[$user_pattern_index]) if $txt_fh;
      printf $htm_fh ("$htm_format1\n",$key,$user_pattern_totals[$user_pattern_index]) if $htm_fh;
      $ws_global->write($row++, 0, [$key,$user_pattern_totals[$user_pattern_index]]) if $xls_fh;
      $user_pattern_index++;
    }
  }
  print $txt_fh "\n" if $txt_fh;
  print $htm_fh "</table>\n\n" if $htm_fh;
  if ($xls_fh)
  {
    ++$row;
  }

  if ($hist_opt > 0) {
    my $user_pattern_index = 0;
    foreach $key (@user_descriptions) {
      print_histogram($key, 'occurence', @{$user_pattern_interval_count[$user_pattern_index]});
      $user_pattern_index++;
    }
  }
}

#######################################################################
# print_rejects()
#
#  print_rejects();
#
# Print statistics about rejected mail.
#######################################################################
sub print_rejects {
  my($format1,$reason);

  my $txt_format1 = "  %-40s  %6d";
  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%d</td>";

  if ($txt_fh) {
    print $txt_fh "Rejected mail by reason\n";
    print $txt_fh "-----------------------";
    print $txt_fh "\n                                             Total\n";
  }
  if ($htm_fh) {
    print $htm_fh "<hr><a name=\"patterns\"></a><h2>Rejected mail by reason</h2>\n";
    print $htm_fh "<table border=0 width=\"100%\"><tr><td><table border=1>\n";
    print $htm_fh "<tr><th>&nbsp;</th><th>Total</th>\n";
  }
  if ($xls_fh) {
    $ws_global->write($row++, $col, "Rejected mail by reason", $f_header2);
    &set_worksheet_line($ws_global, $row++, 1, ["Total"], $f_headertab);
  }


  my $href = ($merge_reports) ? $report_totals{rejected_mail_by_reason} : \%rejected_count_by_reason;
  my(@chartdatanames, @chartdatavals_count);

  foreach $reason (top_n_sort($topcount, $href, undef, undef)) {
    printf $txt_fh ("$txt_format1\n",$reason,$href->{$reason}) if $txt_fh;
    printf $htm_fh ("$htm_format1\n",$reason,$href->{$reason}) if $htm_fh;
    set_worksheet_line($ws_global, $row++, 0, [$reason,$href->{$reason}], $f_default) if $xls_fh;
    push(@chartdatanames, $reason);
    push(@chartdatavals_count, $href->{$reason});
  }

  $row++ if $xls_fh;
  print $txt_fh "\n" if $txt_fh;

  if ($htm_fh) {
    print $htm_fh "</tr></table></td><td>";
    if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_count > 0)) {
      # calculate the graph
      my @data = (
         \@chartdatanames,
         \@chartdatavals_count
      );
      my $graph = GD::Graph::pie->new(200, 200);
      $graph->set(
          x_label           => 'Rejection Reasons',
          y_label           => 'Messages',
          title             => 'By count',
      );
      my $gd = $graph->plot(\@data) or warn($graph->error);
      if ($gd) {
        open(IMG, ">$chartdir/rejections_count.png") or die "Could not write $chartdir/rejections_count.png: $!\n";
        binmode IMG;
        print IMG $gd->png;
        close IMG;
        print $htm_fh "<img src=\"$chartrel/rejections_count.png\">";
      }
    }
    print $htm_fh "</td></tr></table>\n\n";
  }
}





#######################################################################
# print_transport();
#
#  print_transport();
#
# Print totals by transport.
#######################################################################
sub print_transport {
  my(@chartdatanames);
  my(@chartdatavals_count);
  my(@chartdatavals_vol);
  no integer;                 #Lose this for charting the data.

  my $txt_format1 = "  %-18s  %6s      %6d";
  my $htm_format1 = "<tr><td>%s</td><td align=\"right\">%s</td><td align=\"right\">%d</td>";

  if ($txt_fh) {
    print $txt_fh "Deliveries by transport\n";
    print $txt_fh "-----------------------";
    print $txt_fh "\n                      Volume    Messages\n";
  }
  if ($htm_fh) {
    print $htm_fh "<hr><a name=\"Transport\"></a><h2>Deliveries by Transport</h2>\n";
    print $htm_fh "<table border=0 width=\"100%\"><tr><td><table border=1>\n";
    print $htm_fh "<tr><th>&nbsp;</th><th>Volume</th><th>Messages</th>\n";
  }
  if ($xls_fh) {
    $ws_global->write(++$row, $col, "Deliveries by transport", $f_header2);
    $ws_global->write(++$row, 1, ["Volume", "Messages"], $f_headertab);
  }

  my($key);
  if ($merge_reports) {
    # We are getting our data from previous reports.
    foreach $key (sort keys %{$report_totals{transport}}) {
      my $count = get_report_total($report_totals{transport}{$key},'Messages');
      my @content=($key, volume_rounded($report_totals{transport}{$key}{Volume},
        $report_totals{transport}{$key}{'Volume-gigs'}), $count);
      push(@chartdatanames, $key);
      push(@chartdatavals_count, $count);
      push(@chartdatavals_vol, $report_totals{transport}{$key}{'Volume-gigs'}*$gig + $report_totals{transport}{$key}{Volume} );
      printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
      printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
      $ws_global->write(++$row, 0, \@content) if $xls_fh;
    }
  }
  else {
    # We are getting our data from mainlog files.
    foreach $key (sort keys %transported_data) {
      my @content=($key, volume_rounded($transported_data{$key},$transported_data_gigs{$key}),
        $transported_count{$key});
      push(@chartdatanames, $key);
      push(@chartdatavals_count, $transported_count{$key});
      push(@chartdatavals_vol, $transported_data_gigs{$key}*$gig + $transported_data{$key});
      printf $txt_fh ("$txt_format1\n", @content) if $txt_fh;
      printf $htm_fh ("$htm_format1\n", @content) if $htm_fh;
      $ws_global->write(++$row, 0, \@content) if $xls_fh;
    }
  }
  print $txt_fh "\n" if $txt_fh;
  if ($htm_fh) {
    print $htm_fh "</tr></table></td><td>";

    if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_count > 0))
      {
      # calculate the graph
      my @data = (
         \@chartdatanames,
         \@chartdatavals_count
      );
      my $graph = GD::Graph::pie->new(200, 200);
      $graph->set(
          x_label           => 'Transport',
          y_label           => 'Messages',
          title             => 'By count',
      );
      my $gd = $graph->plot(\@data) or warn($graph->error);
      if ($gd) {
        open(IMG, ">$chartdir/transports_count.png") or die "Could not write $chartdir/transports_count.png: $!\n";
        binmode IMG;
        print IMG $gd->png;
        close IMG;
        print $htm_fh "<img src=\"$chartrel/transports_count.png\">";
      }
    }
    print $htm_fh "</td><td>";

    if ($HAVE_GD_Graph_pie && $charts && ($#chartdatavals_vol > 0)) {
      my @data = (
         \@chartdatanames,
         \@chartdatavals_vol
      );
      my $graph = GD::Graph::pie->new(200, 200);
      $graph->set(
          title             => 'By volume',
      );
      my $gd = $graph->plot(\@data) or warn($graph->error);
      if ($gd) {
        open(IMG, ">$chartdir/transports_vol.png") or die "Could not write $chartdir/transports_vol.png: $!\n";
        binmode IMG;
        print IMG $gd->png;
        close IMG;
        print $htm_fh "<img src=\"$chartrel/transports_vol.png\">";
      }
    }

    print $htm_fh "</td></tr></table>\n\n";
  }
}



#######################################################################
# print_relay();
#
#  print_relay();
#
# Print our totals by relay.
#######################################################################
sub print_relay {
  my $row_print_relay=1;
  my $temp = "Relayed messages";
  print $htm_fh "<hr><a name=\"$temp\"></a><h2>$temp</h2>\n" if $htm_fh;
  if (scalar(keys %relayed) > 0 || $relayed_unshown > 0) {
    my $shown = 0;
    my $spacing = "";
    my $txt_format = "%7d %s\n      => %s\n";
    my $htm_format = "<tr><td align=\"right\">%d</td><td>%s</td><td>%s</td>\n";

    printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
    if ($htm_fh) {
      print $htm_fh "<table border=1>\n";
      print $htm_fh "<tr><th>Count</th><th>From</th><th>To</th>\n";
    }
    if ($xls_fh) {
      $ws_relayed->write($row_print_relay++, $col, $temp, $f_header2);
      &set_worksheet_line($ws_relayed, $row_print_relay++, 0, ["Count", "From", "To"], $f_headertab);
    }


    my($key);
    foreach $key (sort keys %relayed) {
      my $count = $relayed{$key};
      $shown += $count;
      $key =~ s/[HA]=//g;
      my($one,$two) = split(/=> /, $key);
      my @content=($count, $one, $two);
      printf $txt_fh ($txt_format, @content) if $txt_fh;
      printf $htm_fh ($htm_format, @content) if $htm_fh;
      if ($xls_fh)
      {
        &set_worksheet_line($ws_relayed, $row_print_relay++, 0, \@content);
      }
      $spacing = "\n";
    }

    print $htm_fh "</table>\n<p>\n" if $htm_fh;
    print $txt_fh "${spacing}Total: $shown (plus $relayed_unshown unshown)\n\n" if $txt_fh;
    print $htm_fh "${spacing}Total: $shown (plus $relayed_unshown unshown)\n\n" if $htm_fh;
    if ($xls_fh)
    {
       &set_worksheet_line($ws_relayed, $row_print_relay++, 0, [$shown, "Sum of shown" ]);
       &set_worksheet_line($ws_relayed, $row_print_relay++, 0, [$relayed_unshown, "unshown"]);
       $row_print_relay++;
    }
  }
  else {
    print $txt_fh "No relayed messages\n-------------------\n\n" if $txt_fh;
    print $htm_fh "No relayed messages\n\n" if $htm_fh;
    if ($xls_fh)
    {
      $row_print_relay++;
    }
  }
}



#######################################################################
# print_errors();
#
#  print_errors();
#
# Print our errors. In HTML, we display them as a list rather than a table -
# Netscape doesn't like large tables!
#######################################################################
sub print_errors {
  my $total_errors = 0;
  $row=1;

  if (scalar(keys %errors_count) != 0) {
    my $temp = "List of errors";
    my $htm_format = "<li>%d - %s\n";

    printf $txt_fh ("%s\n%s\n\n", $temp, "-" x length($temp)) if $txt_fh;
    if ($htm_fh) {
      print $htm_fh "<hr><a name=\"errors\"></a><h2>$temp</h2>\n";
      print $htm_fh "<ul><li><b>Count - Error</b>\n";
    }
    if ($xls_fh)
    {
      $ws_errors->write($row++, 0, $temp, $f_header2);
      &set_worksheet_line($ws_errors, $row++, 0, ["Count", "Error"], $f_headertab);
    }


    my($key);
    foreach $key (sort keys %errors_count) {
      my $text = $key;
      chomp($text);
      $text =~ s/\s\s+/ /g;   #Convert multiple spaces to a single space.
      $total_errors += $errors_count{$key};

      if ($txt_fh) {
        printf $txt_fh ("%5d ", $errors_count{$key});
        my $text_remaining = $text;
        while (length($text_remaining) > 65) {
          my($first,$rest) = $text_remaining =~ /(.{50}\S*)\s+(.+)/;
          last if !$first;
          printf $txt_fh ("%s\n\t    ", $first);
          $text_remaining = $rest;
        }
        printf $txt_fh ("%s\n\n", $text_remaining);
      }

      if ($htm_fh) {

        #Translate HTML tag characters. Sergey Sholokh.
        $text =~ s/\</\&lt\;/g;
        $text =~ s/\>/\&gt\;/g;

        printf $htm_fh ($htm_format,$errors_count{$key},$text);
      }
      if ($xls_fh)
      {
        &set_worksheet_line($ws_errors, $row++, 0, [$errors_count{$key},$text]);
      }
    }

    $temp = "Errors encountered: $total_errors";

    if ($txt_fh) {
      print $txt_fh $temp, "\n";
      print $txt_fh "-" x length($temp),"\n";
    }
    if ($htm_fh) {
      print $htm_fh "</ul>\n<p>\n";
      print $htm_fh $temp, "\n";
    }
    if ($xls_fh)
    {
        &set_worksheet_line($ws_errors, $row++, 0, [$total_errors, "Sum of Errors encountered"]);
    }
  }

}


#######################################################################
# parse_old_eximstat_reports();
#
#  parse_old_eximstat_reports($fh);
#
# Parse old eximstat output so we can merge daily stats to weekly stats and weekly to monthly etc.
#
# To test that the merging still works after changes, do something like the following.
# All the diffs should produce no output.
#
#  options='-bydomain -byemail -byhost -byedomain'
#  options="$options -show_rt1,2,4 -show_dt 1,2,4"
#  options="$options -pattern 'Completed Messages' /Completed/"
#  options="$options -pattern 'Received Messages' /<=/"
#
#  ./eximstats $options mainlog > mainlog.txt
#  ./eximstats $options -merge mainlog.txt > mainlog.2.txt
#  diff mainlog.txt mainlog.2.txt
#
#  ./eximstats $options -html mainlog > mainlog.html
#  ./eximstats $options -merge -html mainlog.txt  > mainlog.2.html
#  diff mainlog.html mainlog.2.html
#
#  ./eximstats $options -merge mainlog.html > mainlog.3.txt
#  diff mainlog.txt mainlog.3.txt
#
#  ./eximstats $options -merge -html mainlog.html > mainlog.3.html
#  diff mainlog.html mainlog.3.html
#
#  ./eximstats $options -nvr   mainlog > mainlog.nvr.txt
#  ./eximstats $options -merge mainlog.nvr.txt > mainlog.4.txt
#  diff mainlog.txt mainlog.4.txt
#
#  # double_mainlog.txt should have twice the values that mainlog.txt has.
#  ./eximstats $options mainlog mainlog > double_mainlog.txt
#######################################################################
sub parse_old_eximstat_reports {
  my($fh) = @_;

  my(%league_table_value_entered, %league_table_value_was_zero, %table_order);

  my(%user_pattern_index);
  my $user_pattern_index = 0;
  map {$user_pattern_index{$_} = $user_pattern_index++} @user_descriptions;
  my $user_pattern_keys = join('|', @user_descriptions);

  while (<$fh>) {
    PARSE_OLD_REPORT_LINE:
    if (/Exim statistics from ([\d\-]+ [\d:]+(\s+[\+\-]\d+)?) to ([\d\-]+ [\d:]+(\s+[\+\-]\d+)?)/) {
      $begin = $1 if ($1 lt $begin);
      $end   = $3 if ($3 gt $end);
    }
    elsif (/Grand total summary/) {
      # Fill in $report_totals{Received|Delivered}{Volume|Messages|Addresses|Hosts|Domains|...|Delayed|DelayedPercent|Failed|FailedPercent}
      my(@fields, @delivered_fields);
      my $doing_table = 0;
      while (<$fh>) {
        $_ = html2txt($_);       #Convert general HTML markup to text.
        s/At least one addr//g;  #Another part of the HTML output we don't want.

#  TOTAL               Volume    Messages Addresses   Hosts Domains      Delayed       Failed
#  Received              26MB         237               177      23       8  3.4%     28 11.8%
#  Delivered             13MB         233       250      99      88
        if (/TOTAL\s+(.*?)\s*$/) {
          $doing_table = 1;
          @delivered_fields = split(/\s+/,$1);

          #Delayed and Failed have two columns each, so add the extra field names in.
          splice(@delivered_fields,-1,1,'DelayedPercent','Failed','FailedPercent');

          # Addresses only figure in the Delivered row, so remove them from the
          # normal fields.
          @fields = grep !/Addresses/, @delivered_fields;
        }
        elsif (/(Received)\s+(.*?)\s*$/) {
          print STDERR "Parsing $_" if $debug;
          add_to_totals($report_totals{$1},\@fields,$2);
        }
        elsif (/(Delivered)\s+(.*?)\s*$/) {
          print STDERR "Parsing $_" if $debug;
          add_to_totals($report_totals{$1},\@delivered_fields,$2);
          my $data = $2;
          # If we're merging an old report which doesn't include addresses,
          # then use the Messages field instead.
          unless (grep(/Addresses/, @delivered_fields)) {
            my %tmp;
            line_to_hash(\%tmp,\@delivered_fields,$data);
            add_to_totals($report_totals{Delivered},['Addresses'],$tmp{Messages});
          }
        }
        elsif (/(Temp Rejects|Rejects|Ham|Spam)\s+(.*?)\s*$/) {
          print STDERR "Parsing $_" if $debug;
          add_to_totals($report_totals{$1},['Messages','Hosts'],$2);
        }
        else {
          last if $doing_table;
        }
      }
    }

    elsif (/User Specified Patterns/i) {
#User Specified Patterns
#-----------------------
#                       Total
#  Description             85

      while (<$fh>) { last if (/Total/); }  #Wait until we get the table headers.
      while (<$fh>) {
        print STDERR "Parsing $_" if $debug;
        $_ = html2txt($_);              #Convert general HTML markup to text.
        if (/^\s*(.*?)\s+(\d+)\s*$/) {
          $report_totals{patterns}{$1} = {} unless (defined $report_totals{patterns}{$1});
          add_to_totals($report_totals{patterns}{$1},['Total'],$2);
        }
        last if (/^\s*$/);              #Finished if we have a blank line.
      }
    }

    elsif (/(^|<h2>)($user_pattern_keys) per /o) {
      # Parse User defined pattern histograms if they exist.
      parse_histogram($fh, $user_pattern_interval_count[$user_pattern_index{$2}] );
    }


    elsif (/Deliveries by transport/i) {
#Deliveries by transport
#-----------------------
#                      Volume    Messages
#  :blackhole:           70KB          51
#  address_pipe         655KB           1
#  smtp                  11MB         151

      while (<$fh>) { last if (/Volume/); }  #Wait until we get the table headers.
      while (<$fh>) {
        print STDERR "Parsing $_" if $debug;
        $_ = html2txt($_);              #Convert general HTML markup to text.
        if (/(\S+)\s+(\d+\S*\s+\d+)/) {
          $report_totals{transport}{$1} = {} unless (defined $report_totals{transport}{$1});
          add_to_totals($report_totals{transport}{$1},['Volume','Messages'],$2);
        }
        last if (/^\s*$/);              #Finished if we have a blank line.
      }
    }
    elsif (/Messages received per/) {
      parse_histogram($fh, \@received_interval_count);
    }
    elsif (/Deliveries per/) {
      parse_histogram($fh, \@delivered_interval_count);
    }

    #elsif (/Time spent on the queue: (all messages|messages with at least one remote delivery)/) {
    elsif (/(Time spent on the queue|Delivery times|Receipt times): ((\S+) messages|messages with at least one remote delivery)((<[^>]*>)*\s*)$/) {
#Time spent on the queue: all messages
#-------------------------------------
#
#Under   1m      217  91.9%   91.9%
#        5m        2   0.8%   92.8%
#        3h        8   3.4%   96.2%
#        6h        7   3.0%   99.2%
#       12h        2   0.8%  100.0%

      # Set a pointer to the queue bin so we can use the same code
      # block for both all messages and remote deliveries.
      #my $bin_aref = ($1 eq 'all messages') ? \@qt_all_bin : \@qt_remote_bin;
      my($bin_aref, $times_aref, $overflow_sref);
      if ($1 eq 'Time spent on the queue') {
        $times_aref = \@queue_times;
        if ($2 eq 'all messages') {
          $bin_aref = \@qt_all_bin;
          $overflow_sref = \$qt_all_overflow;
        }
        else {
          $bin_aref = \@qt_remote_bin;
          $overflow_sref = \$qt_remote_overflow;
        }
      }
      elsif ($1 eq 'Delivery times') {
        $times_aref = \@delivery_times;
        if ($2 eq 'all messages') {
          $bin_aref = \@dt_all_bin;
          $overflow_sref = \$dt_all_overflow;
        }
        else {
          $bin_aref = \@dt_remote_bin;
          $overflow_sref = \$dt_remote_overflow;
        }
      }
      else {
        unless (exists $rcpt_times_bin{$3}) {
          initialise_rcpt_times($3);
        }
        $bin_aref = $rcpt_times_bin{$3};
        $times_aref = \@rcpt_times;
        $overflow_sref = \$rcpt_times_overflow{$3};
      }


      my ($blank_lines, $reached_table) = (0,0);
      while (<$fh>) {
        $_ = html2txt($_);              #Convert general HTML markup to text.
        # The table is preceded by one blank line, and has one blank line
        # following it. As the table may be empty, the best way to determine
        # that we've finished it is to look for the second blank line.
        ++$blank_lines if /^\s*$/;
        last if ($blank_lines >=2);     #Finished the table ?
        $reached_table = 1 if (/\d/);
        next unless $reached_table;
        my $previous_seconds_on_queue = 0;
        if (/^\s*(Under|Over|)\s+(\d+[smhdw])\s+(\d+)/) {
          print STDERR "Parsing $_" if $debug;
          my($modifier,$formatted_time,$count) = ($1,$2,$3);
          my $seconds = unformat_time($formatted_time);
          my $time_on_queue = ($seconds + $previous_seconds_on_queue) / 2;
          $previous_seconds_on_queue = $seconds;
          $time_on_queue = $seconds * 2 if ($modifier eq 'Over');
          my($i);
          for ($i = 0; $i <= $#$times_aref; $i++) {
            if ($time_on_queue < $times_aref->[$i]) {
              $$bin_aref[$i] += $count;
              last;
            }
          }
          $$overflow_sref += $count if ($i > $#$times_aref);

        }
      }
    }

    elsif (/Relayed messages/) {
#Relayed messages
#----------------
#
#      1 addr.domain.com [1.2.3.4] a.user@domain.com
#      => addr2.domain2.com [5.6.7.8] a2.user2@domain2.com
#
#<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>

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

    elsif (/Top (.*?) by (message count|volume)/) {
#Top 50 sending hosts by message count
#-------------------------------------
#
#     48     1468KB   local
# Could also have average values for HTML output.
#     48     1468KB   30KB  local

      my($category,$by_count_or_volume) = ($1,$2);

      #As we show 2 views of each table (by count and by volume),
      #most (but not all) entries will appear in both tables.
      #Set up a hash to record which entries we have already seen
      #and one to record which ones we are seeing for the first time.
      if ($by_count_or_volume =~ /count/) {
        undef %league_table_value_entered;
        undef %league_table_value_was_zero;
        undef %table_order;
      }

      #As this section processes multiple different table categories,
      #set up pointers to the hashes to be updated.
      my($messages_href,$addresses_href,$data_href,$data_gigs_href);
      if ($category =~ /local sender/) {
        $messages_href   = \%received_count_user;
        $addresses_href  = undef;
        $data_href       = \%received_data_user;
        $data_gigs_href  = \%received_data_gigs_user;
      }
      elsif ($category =~ /sending (\S+?)s?\b/) {
        #Top 50 sending (host|domain|email|edomain)s
        #Top sending (host|domain|email|edomain)
        $messages_href   = \%{$received_count{"\u$1"}};
        $data_href       = \%{$received_data{"\u$1"}};
        $data_gigs_href  = \%{$received_data_gigs{"\u$1"}};
      }
      elsif ($category =~ /local destination/) {
        $messages_href   = \%delivered_messages_user;
        $addresses_href  = \%delivered_addresses_user;
        $data_href       = \%delivered_data_user;
        $data_gigs_href  = \%delivered_data_gigs_user;
      }
      elsif ($category =~ /local domain destination/) {
        $messages_href   = \%delivered_messages_local_domain;
        $addresses_href  = \%delivered_addresses_local_domain;
        $data_href       = \%delivered_data_local_domain;
        $data_gigs_href  = \%delivered_data_gigs_local_domain;
      }
      elsif ($category =~ /(\S+) destination/) {
        #Top 50 (host|domain|email|edomain) destinations
        #Top (host|domain|email|edomain) destination
        $messages_href   = \%{$delivered_messages{"\u$1"}};
        $addresses_href  = \%{$delivered_addresses{"\u$1"}};
        $data_href       = \%{$delivered_data{"\u$1"}};
        $data_gigs_href  = \%{$delivered_data_gigs{"\u$1"}};
      }
      elsif ($category =~ /temporarily rejected ips/) {
        $messages_href      = \%temporarily_rejected_count_by_ip;
      }
      elsif ($category =~ /rejected ips/) {
        $messages_href      = \%rejected_count_by_ip;
      }
      elsif ($category =~ /non-rejected spamming ips/) {
        $messages_href      = \%spam_count_by_ip;
      }
      elsif ($category =~ /mail temporary rejection reasons/) {
        $messages_href      = \%temporarily_rejected_count_by_reason;
      }
      elsif ($category =~ /mail rejection reasons/) {
        $messages_href      = \%rejected_count_by_reason;
      }

      my $reached_table = 0;
      my $row_re;
      while (<$fh>) {
        # Watch out for empty tables.
        goto PARSE_OLD_REPORT_LINE if (/<h2>/ or (/^\s*[a-zA-Z]/ && !/^\s*Messages/));

        $_ = html2txt($_);              #Convert general HTML markup to text.

        # Messages      Addresses  Bytes  Average
        if (/^\s*Messages/) {
          my $pattern = '^\s*(\d+)';
          $pattern .= (/Addresses/) ? '\s+(\d+)' : '()';
          $pattern .= (/Bytes/)     ? '\s+([\dKMGB]+)' : '()';
          $pattern .= (/Average/)   ? '\s+[\dKMGB]+' : '';
          $pattern .= '\s+(.*?)\s*$';
          $row_re = qr/$pattern/;
          $reached_table = 1;
          next;
        }
        next unless $reached_table;

        my($messages, $addresses, $rounded_volume, $entry);

        if (/$row_re/) {
          ($messages, $addresses, $rounded_volume, $entry) = ($1, $2, $3, $4);
        }
        else {
          #Else we have finished the table and we may need to do some
          #kludging to retain the order of the entries.

          if ($by_count_or_volume =~ /volume/) {
            #Add a few bytes to appropriate entries to preserve the order.
            foreach $rounded_volume (keys %table_order) {
              #For each rounded volume, we want to create a list which has things
              #ordered from the volume table at the front, and additional things
              #from the count table ordered at the back.
              @{$table_order{$rounded_volume}{volume}} = () unless defined $table_order{$rounded_volume}{volume};
              @{$table_order{$rounded_volume}{'message count'}} = () unless defined $table_order{$rounded_volume}{'message count'};
              my(@order,%mark);
              map {$mark{$_} = 1} @{$table_order{$rounded_volume}{volume}};
              @order = @{$table_order{$rounded_volume}{volume}};
              map {push(@order,$_)} grep(!$mark{$_},@{$table_order{$rounded_volume}{'message count'}});

              my $bonus_bytes = $#order;
              $bonus_bytes = 511 if ($bonus_bytes > 511);  #Don't go over the half-K boundary!
              while (@order and ($bonus_bytes > 0)) {
                my $entry = shift(@order);
                if ($league_table_value_was_zero{$entry}) {
                  $$data_href{$entry} += $bonus_bytes;
                  print STDERR "$category by $by_count_or_volume: added $bonus_bytes bonus bytes to $entry\n" if $debug;
                }
                $bonus_bytes--;
              }
            }
          }
          last;
        }

        # Store a new table entry.

        # Add the entry into the %table_order hash if it has a rounded
        # volume (KB/MB/GB).
        push(@{$table_order{$rounded_volume}{$by_count_or_volume}},$entry) if ($rounded_volume =~ /\D/);

        unless ($league_table_value_entered{$entry}) {
          $league_table_value_entered{$entry} = 1;
          unless ($$messages_href{$entry}) {
            $$messages_href{$entry}  = 0;
            $$addresses_href{$entry} = 0;
            $$data_href{$entry}      = 0;
            $$data_gigs_href{$entry} = 0;
            $league_table_value_was_zero{$entry} = 1;
          }

          $$messages_href{$entry} += $messages;

          # When adding the addresses, be aware that we could be merging
          # an old report which does not include addresses. In this case,
          # we add the messages instead.
          $$addresses_href{$entry} += ($addresses) ? $addresses : $messages;

          #Add the rounded value to the data and data_gigs hashes.
          un_round($rounded_volume,\$$data_href{$entry},\$$data_gigs_href{$entry}) if $rounded_volume;
          print STDERR "$category by $by_count_or_volume: added $messages,$rounded_volume to $entry\n" if $debug;
        }

      }
    }
    elsif (/List of errors/) {
#List of errors
#--------------
#
#    1 07904931641@one2one.net R=external T=smtp: SMTP error
#            from remote mailer after RCPT TO:<07904931641@one2one.net>:
#            host mail.one2one.net [193.133.192.24]: 550 User unknown
#
#<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>


      my $reached_table = 0;
      my($count,$error,$blanks);
      while (<$fh>) {
        $reached_table = 1 if (/^( *|<li>)(\d+)/);
        next unless $reached_table;

        s/^<li>(\d+) -/$1/;     #Convert an HTML line to a text line.
        $_ = html2txt($_);      #Convert general HTML markup to text.

        if (/\t\s*(.*)/) {
          $error .= ' ' . $1;   #Join a multiline error.
        }
        elsif (/^\s*(\d+)\s+(.*)/) {
          if ($error) {
            #Finished with a previous multiline error so save it.
            $errors_count{$error} = 0 unless $errors_count{$error};
            $errors_count{$error} += $count;
          }
          ($count,$error) = ($1,$2);
        }
        elsif (/Errors encountered/) {
          if ($error) {
            #Finished the section, so save our stored last error.
            $errors_count{$error} = 0 unless $errors_count{$error};
            $errors_count{$error} += $count;
          }
          last;
        }
      }
    }

  }
}

#######################################################################
# parse_histogram($fh, \@delivered_interval_count);
# Parse a histogram into the provided array of counters.
#######################################################################
sub parse_histogram {
  my($fh, $counters_aref) = @_;

  #      Messages received per hour (each dot is 2 messages)
  #---------------------------------------------------
  #
  #00-01    106 .....................................................
  #01-02    103 ...................................................

  my $reached_table = 0;
  while (<$fh>) {
    $reached_table = 1 if (/^00/);
    next unless $reached_table;
    print STDERR "Parsing $_" if $debug;
    if (/^(\d+):(\d+)\s+(\d+)/) {           #hh:mm start time format ?
      $$counters_aref[($1*60 + $2)/$hist_interval] += $3 if $hist_opt;
    }
    elsif (/^(\d+)-(\d+)\s+(\d+)/) {        #hh-hh start-end time format ?
      $$counters_aref[($1*60)/$hist_interval] += $3 if $hist_opt;
    }
    else {                                  #Finished the table ?
      last;
    }
  }
}


#######################################################################
# update_relayed();
#
#  update_relayed($count,$sender,$recipient);
#
# Adds an entry into the %relayed hash. Currently only used when
# merging reports.
#######################################################################
sub update_relayed {
  my($count,$sender,$recipient) = @_;

  #When generating the key, put in the 'H=' and 'A=' which can be used
  #in searches.
  my $key = "H=$sender => H=$recipient";
  $key =~ s/ ([^=\s]+\@\S+|<>)/ A=$1/g;
  if (!defined $relay_pattern || $key !~ /$relay_pattern/o) {
    $relayed{$key} = 0 if !defined $relayed{$key};
    $relayed{$key} += $count;
  }
  else {
    $relayed_unshown += $count;
  }
}


#######################################################################
# add_to_totals();
#
#  add_to_totals(\%totals,\@keys,$values);
#
# Given a line of space separated values, add them into the provided hash using @keys
# as the hash keys.
#
# If the value contains a '%', then the value is set rather than added. Otherwise, we
# convert the value to bytes and gigs. The gigs get added to I<Key>-gigs.
#######################################################################
sub add_to_totals {
  my($totals_href,$keys_aref,$values) = @_;
  my(@values) = split(/\s+/,$values);

  for(my $i = 0; $i < @values && $i < @$keys_aref; ++$i) {
    my $key = $keys_aref->[$i];
    if ($values[$i] =~ /%/) {
      $$totals_href{$key} = $values[$i];
    }
    else {
      $$totals_href{$key} = 0 unless ($$totals_href{$key});
      $$totals_href{"$key-gigs"} = 0 unless ($$totals_href{"$key-gigs"});
      un_round($values[$i], \$$totals_href{$key}, \$$totals_href{"$key-gigs"});
      print STDERR "Added $values[$i] to $key - $$totals_href{$key} , " . $$totals_href{"$key-gigs"} . "GB.\n" if $debug;
    }
  }
}


#######################################################################
# line_to_hash();
#
#  line_to_hash(\%hash,\@keys,$line);
#
# Given a line of space separated values, set them into the provided hash
# using @keys as the hash keys.
#######################################################################
sub line_to_hash {
  my($href,$keys_aref,$values) = @_;
  my(@values) = split(/\s+/,$values);
  for(my $i = 0; $i < @values && $i < @$keys_aref; ++$i) {
    $$href{$keys_aref->[$i]} = $values[$i];
  }
}


#######################################################################
# get_report_total();
#
#  $total = get_report_total(\%hash,$key);
#
# If %hash contains values split into Units and Gigs, we calculate and return
#
#   $hash{$key} + 1024*1024*1024 * $hash{"${key}-gigs"}
#######################################################################
sub get_report_total {
  no integer;
  my($hash_ref,$key) = @_;
  if ($$hash_ref{"${key}-gigs"}) {
    return $$hash_ref{$key} + $gig * $$hash_ref{"${key}-gigs"};
  }
  return $$hash_ref{$key} || 0;
}

#######################################################################
# html2txt();
#
#  $text_line = html2txt($html_line);
#
# Convert a line from html to text. Currently we just convert HTML tags to spaces
# and convert &gt;, &lt;, and &nbsp; tags back.
#######################################################################
sub html2txt {
  ($_) = @_;

  # Convert HTML tags to spacing. Note that the reports may contain <Userid> and
  # <Userid@Domain> words, so explicitly specify the HTML tags we will remove
  # (the ones used by this program). If someone is careless enough to have their
  # Userid the same as an HTML tag, there's not much we can do about it.
  s/<\/?(html|head|title|body|h\d|ul|li|a\s+|table|tr|td|th|pre|hr|p|br)\b.*?>/ /g;

  s/\&lt\;/\</og;             #Convert '&lt;' to '<'.
  s/\&gt\;/\>/og;             #Convert '&gt;' to '>'.
  s/\&nbsp\;/ /og;            #Convert '&nbsp;' to ' '.
  return($_);
}

#######################################################################
# get_next_arg();
#
#  $arg = get_next_arg();
#
# Because eximstats arguments are often passed as variables,
# we can't rely on shell parsing to deal with quotes. This
# subroutine returns $ARGV[1] and does a shift. If $ARGV[1]
# starts with a quote (' or "), and doesn't end in one, then
# we append the next argument to it and shift again. We repeat
# until we've got all of the argument.
#
# This isn't perfect as all white space gets reduced to one space,
# but it's as good as we can get! If it's essential that spacing
# be preserved precisely, then you get that by not using shell
# variables.
#######################################################################
sub get_next_arg {
  my $arg = '';
  my $matched_pattern = 0;
  while ($ARGV[1]) {
    $arg .= ' ' if $arg;
    $arg .= $ARGV[1]; shift(@ARGV);
    if ($arg !~ /^['"]/) {
      $matched_pattern = 1;
      last;
    }
    if ($arg =~ s/^(['"])(.*)\1$/$2/) {
      $matched_pattern = 1;
      last;
    }
  }
  die "Mismatched argument quotes - <$arg>.\n" unless $matched_pattern;
  return $arg;
}

#######################################################################
# set_worksheet_line($ws_global, $startrow, $startcol, \@content, $format);
#
# set values to a sequence of cells in a row.
#
#######################################################################
sub set_worksheet_line {
  my ($worksheet, $row, $col, $content, $format) = @_;

  foreach my $token (@$content)
  {
     $worksheet->write($row, $col++, $token, $format );
  }

}

#######################################################################
# @rcpt_times = parse_time_list($string);
#
# Parse a comma separated list of time values in seconds given by
# the user and fill an array.
#
# Return a default list if $string is undefined.
# Return () if $string eq '0'.
#######################################################################
sub parse_time_list {
  my($string) = @_;
  if (! defined $string) {
    return(60, 5*60, 15*60, 30*60, 60*60, 3*60*60, 6*60*60, 12*60*60, 24*60*60);
  }
  my(@times) = split(/,/, $string);
  foreach my $q (@times) { $q = eval($q) + 0 }
  @times = sort { $a <=> $b } @times;
  @times = () if ($#times == 0 && $times[0] == 0);
  return(@times);
}


#######################################################################
# initialise_rcpt_times($protocol);
# Initialise an array of rcpt_times to 0 for the specified protocol.
#######################################################################
sub initialise_rcpt_times {
  my($protocol) = @_;
  for (my $i = 0; $i <= $#rcpt_times; ++$i) {
    $rcpt_times_bin{$protocol}[$i] = 0;
  }
  $rcpt_times_overflow{$protocol} = 0;
}


##################################################
#                 Main Program                   #
##################################################


$last_timestamp = '';
$last_date = '';
$show_errors = 1;
$show_relay = 1;
$show_transport = 1;
$topcount = 50;
$local_league_table = 1;
$include_remote_users = 0;
$include_original_destination = 0;
$hist_opt = 1;
$volume_rounding = 1;
$localtime_offset = calculate_localtime_offset();    # PH/FANF

$charts = 0;
$charts_option_specified = 0;
$chartrel = ".";
$chartdir = ".";

@queue_times = parse_time_list();
@rcpt_times = ();
@delivery_times = ();

$last_offset = '';
$offset_seconds = 0;

$row=1;
$col=0;
$col_hist=0;
$run_hist=0;
my(%output_files);     # What output files have been specified?

# Decode options

while (@ARGV > 0 && substr($ARGV[0], 0, 1) eq '-') {
  if    ($ARGV[0] =~ /^\-h(\d+)$/) { $hist_opt = $1 }
  elsif ($ARGV[0] =~ /^\-ne$/)     { $show_errors = 0 }
  elsif ($ARGV[0] =~ /^\-nr(.?)(.*)\1$/) {
    if ($1 eq "") { $show_relay = 0 } else { $relay_pattern = $2 }
  }
  elsif ($ARGV[0] =~ /^\-q([,\d\+\-\*\/]+)$/) { @queue_times = parse_time_list($1) }
  elsif ($ARGV[0] =~ /^-nt$/)       { $show_transport = 0 }
  elsif ($ARGV[0] =~ /^\-nt(.?)(.*)\1$/)
    {
    if ($1 eq "") { $show_transport = 0 } else { $transport_pattern = $2 }
    }
  elsif ($ARGV[0] =~ /^-t(\d+)$/)   { $topcount = $1 }
  elsif ($ARGV[0] =~ /^-tnl$/)      { $local_league_table = 0 }
  elsif ($ARGV[0] =~ /^-txt=?(\S*)$/)  { $txt_fh = get_filehandle($1,\%output_files) }
  elsif ($ARGV[0] =~ /^-html=?(\S*)$/) { $htm_fh = get_filehandle($1,\%output_files) }
  elsif ($ARGV[0] =~ /^-xls=?(\S*)$/) {
    if ($HAVE_Spreadsheet_WriteExcel) {
      $xls_fh = get_filehandle($1,\%output_files);
    }
    else {
      warn "WARNING: CPAN Module Spreadsheet::WriteExcel not installed. Obtain from www.cpan.org\n";
    }
  }
  elsif ($ARGV[0] =~ /^-merge$/)    { $merge_reports = 1 }
  elsif ($ARGV[0] =~ /^-charts$/)   {
    $charts = 1;
    warn "WARNING: CPAN Module GD::Graph::pie not installed. Obtain from www.cpan.org\n" unless $HAVE_GD_Graph_pie;
    warn "WARNING: CPAN Module GD::Graph::linespoints not installed. Obtain from www.cpan.org\n" unless $HAVE_GD_Graph_linespoints;
  }
  elsif ($ARGV[0] =~ /^-chartdir$/) { $chartdir = $ARGV[1]; shift; $charts_option_specified = 1; }
  elsif ($ARGV[0] =~ /^-chartrel$/) { $chartrel = $ARGV[1]; shift; $charts_option_specified = 1; }
  elsif ($ARGV[0] =~ /^-include_original_destination$/)    { $include_original_destination = 1 }
  elsif ($ARGV[0] =~ /^-cache$/)    { } #Not currently used.
  elsif ($ARGV[0] =~ /^-byhost$/)   { $do_sender{Host} = 1 }
  elsif ($ARGV[0] =~ /^-bydomain$/) { $do_sender{Domain} = 1 }
  elsif ($ARGV[0] =~ /^-byemail$/)  { $do_sender{Email} = 1 }
  elsif ($ARGV[0] =~ /^-byemaildomain$/)  { $do_sender{Edomain} = 1 }
  elsif ($ARGV[0] =~ /^-byedomain$/)  { $do_sender{Edomain} = 1 }
  elsif ($ARGV[0] =~ /^-bylocaldomain$/)  { $do_local_domain = 1 }
  elsif ($ARGV[0] =~ /^-emptyok$/)  { $emptyOK = 1 }
  elsif ($ARGV[0] =~ /^-nvr$/)      { $volume_rounding = 0 }
  elsif ($ARGV[0] =~ /^-show_rt([,\d\+\-\*\/]+)?$/) { @rcpt_times = parse_time_list($1) }
  elsif ($ARGV[0] =~ /^-show_dt([,\d\+\-\*\/]+)?$/) { @delivery_times = parse_time_list($1) }
  elsif ($ARGV[0] =~ /^-d$/)        { $debug = 1 }
  elsif ($ARGV[0] =~ /^--?h(elp)?$/){ help() }
  elsif ($ARGV[0] =~ /^-t_remote_users$/) { $include_remote_users = 1 }
  elsif ($ARGV[0] =~ /^-pattern$/)
    {
    push(@user_descriptions,get_next_arg());
    push(@user_patterns,get_next_arg());
    }
  elsif ($ARGV[0] =~ /^-utc$/)
    {
    # We don't need this value if the log is in UTC.
    $localtime_offset = undef;
    }
  else
    {
    print STDERR "Eximstats: Unknown or malformed option $ARGV[0]\n";
    help();
    }
  shift;
  }

  # keep old default behaviour
  if (! ($xls_fh or $htm_fh or $txt_fh)) {
    $txt_fh = \*STDOUT;
  }

  # Check that all the charts options are specified.
  warn "-charts option not specified. Use -help for help.\n" if ($charts_option_specified && ! $charts);

  # Default to display tables by sending Host.
  $do_sender{Host} = 1 unless ($do_sender{Domain} || $do_sender{Email} || $do_sender{Edomain});

  # prepare xls Excel Workbook
  if (defined $xls_fh) {

    # Create a new Excel workbook
    $workbook  = Spreadsheet::WriteExcel->new($xls_fh);

    # Add worksheets
    $ws_global = $workbook->addworksheet('Exim Statistik');
    # show $ws_global as initial sheet
    $ws_global->set_first_sheet();
    $ws_global->activate();

    if ($show_relay) {
      $ws_relayed = $workbook->addworksheet('Relayed Messages');
      $ws_relayed->set_column(1, 2,  80);
    }
    if ($show_errors) {
      $ws_errors = $workbook->addworksheet('Errors');
    }


    # set column widths
    $ws_global->set_column(0, 2,  20); # Columns B-D width set to 30
    $ws_global->set_column(3, 3,  15); # Columns B-D width set to 30
    $ws_global->set_column(4, 4,  25); # Columns B-D width set to 30

    # Define Formats
    $f_default = $workbook->add_format();

    $f_header1 = $workbook->add_format();
    $f_header1->set_bold();
    #$f_header1->set_color('red');
    $f_header1->set_size('15');
    $f_header1->set_valign();
    # $f_header1->set_align('center');
    # $ws_global->write($row++, 2, "Testing Headers 1", $f_header1);

    $f_header2 = $workbook->add_format();
    $f_header2->set_bold();
    $f_header2->set_size('12');
    $f_header2->set_valign();
    # $ws_global->write($row++, 2, "Testing Headers 2", $f_header2);

    # Create another header2 for use in merged cells.
    $f_header2_m = $workbook->add_format();
    $f_header2_m->set_bold();
    $f_header2_m->set_size('8');
    $f_header2_m->set_valign();
    $f_header2_m->set_align('center');

    $f_percent = $workbook->add_format();
    $f_percent->set_num_format('0.0%');

    $f_headertab = $workbook->add_format();
    $f_headertab->set_bold();
    $f_headertab->set_valign();
    # $ws_global->write($row++, 2, "Testing Headers tab", $f_headertab);

  }


# Initialise the queue/delivery/rcpt time counters.
for (my $i = 0; $i <= $#queue_times; $i++) {
  $qt_all_bin[$i] = 0;
  $qt_remote_bin[$i] = 0;
}
for (my $i = 0; $i <= $#delivery_times; $i++) {
  $dt_all_bin[$i] = 0;
  $dt_remote_bin[$i] = 0;
}
initialise_rcpt_times('all');


# Compute the number of slots for the histogram
if ($hist_opt > 0)
  {
  if ($hist_opt > 60 || 60 % $hist_opt != 0)
    {
    print STDERR "Eximstats: -h must specify a factor of 60\n";
    exit 1;
    }
  $hist_interval = 60/$hist_opt;                #Interval in minutes.
  $hist_number = (24*60)/$hist_interval;        #Number of intervals per day.
  @received_interval_count = (0) x $hist_number;
  @delivered_interval_count = (0) x $hist_number;
  my $user_pattern_index = 0;
  for (my $user_pattern_index = 0; $user_pattern_index <= $#user_patterns; ++$user_pattern_index) {
    @{$user_pattern_interval_count[$user_pattern_index]} = (0) x $hist_number;
  }
  @dt_all_bin = (0) x $hist_number;
  @dt_remote_bin = (0) x $hist_number;
}

#$queue_unknown = 0;

$total_received_data = 0;
$total_received_data_gigs = 0;
$total_received_count = 0;

$total_delivered_data = 0;
$total_delivered_data_gigs = 0;
$total_delivered_messages = 0;
$total_delivered_addresses = 0;

$qt_all_overflow = 0;
$qt_remote_overflow = 0;
$dt_all_overflow = 0;
$dt_remote_overflow = 0;
$delayed_count = 0;
$relayed_unshown = 0;
$message_errors = 0;
$begin = "9999-99-99 99:99:99";
$end = "0000-00-00 00:00:00";
my($section,$type);
foreach $section ('Received','Delivered','Temp Rejects', 'Rejects','Ham','Spam') {
  foreach $type ('Volume','Messages','Delayed','Failed','Hosts','Domains','Emails','Edomains') {
    $report_totals{$section}{$type} = 0;
  }
}

# Generate our parser.
my $parser = generate_parser();



if (@ARGV) {
  # Scan the input files and collect the data
  foreach my $file (@ARGV) {
    if ($file =~ /\.gz/) {
      unless (open(FILE,"gunzip -c $file |")) {
        print STDERR "Failed to gunzip -c $file: $!";
        next;
      }
    }
    elsif ($file =~ /\.Z/) {
      unless (open(FILE,"uncompress -c $file |")) {
        print STDERR "Failed to uncompress -c $file: $!";
        next;
      }
    }
    else {
      unless (open(FILE,$file)) {
        print STDERR "Failed to read $file: $!";
        next;
      }
    }
    #Now parse the filehandle, updating the global variables.
    parse($parser,\*FILE);
    close FILE;
  }
}
else {
  #No files provided. Parse STDIN, updating the global variables.
  parse($parser,\*STDIN);
}


if ($begin eq "9999-99-99 99:99:99" && ! $emptyOK) {
  print STDERR "**** No valid log lines read\n";
  exit 1;
}

# Output our results.
print_header();
print_grandtotals();

# Print counts of user specified patterns if required.
print_user_patterns() if @user_patterns;

# Print rejection reasons.
# print_rejects();

# Print totals by transport if required.
print_transport() if $show_transport;

# Print the deliveries per interval as a histogram, unless configured not to.
# First find the maximum in one interval and scale accordingly.
if ($hist_opt > 0) {
  print_histogram("Messages received", 'message', @received_interval_count);
  print_histogram("Deliveries", 'delivery', @delivered_interval_count);
}

# Print times on queue if required.
if ($#queue_times >= 0) {
  print_duration_table("Time spent on the queue", "all messages", \@queue_times, \@qt_all_bin,$qt_all_overflow);
  print_duration_table("Time spent on the queue", "messages with at least one remote delivery", \@queue_times, \@qt_remote_bin,$qt_remote_overflow);
}

# Print delivery times if required.
if ($#delivery_times >= 0) {
  print_duration_table("Delivery times", "all messages", \@delivery_times, \@dt_all_bin,$dt_all_overflow);
  print_duration_table("Delivery times", "messages with at least one remote delivery", \@delivery_times, \@dt_remote_bin,$dt_remote_overflow);
}

# Print rcpt times if required.
if ($#rcpt_times >= 0) {
  foreach my $protocol ('all', grep(!/^all$/, sort keys %rcpt_times_bin)) {
    print_duration_table("Receipt times", "$protocol messages", \@rcpt_times, $rcpt_times_bin{$protocol}, $rcpt_times_overflow{$protocol});
  }
}

# Print relay information if required.
print_relay() if $show_relay;

# Print the league tables, if topcount isn't zero.
if ($topcount > 0) {
  my($ws_rej, $ws_top50, $ws_rej_row, $ws_top50_row, $ws_temp_rej, $ws_temp_rej_row);
  $ws_rej_row = $ws_temp_rej_row = $ws_top50_row = 0;
  if ($xls_fh) {
    $ws_top50 = $workbook->addworksheet('Deliveries');
    $ws_rej = $workbook->addworksheet('Rejections') if (%rejected_count_by_reason || %rejected_count_by_ip || %spam_count_by_ip);
    $ws_temp_rej = $workbook->addworksheet('Temporary Rejections') if (%temporarily_rejected_count_by_reason || %temporarily_rejected_count_by_ip);
  }

  print_league_table("mail rejection reason", \%rejected_count_by_reason, undef, undef, undef, $ws_rej, \$ws_rej_row) if %rejected_count_by_reason;
  print_league_table("mail temporary rejection reason", \%temporarily_rejected_count_by_reason, undef, undef, undef, $ws_temp_rej, \$ws_temp_rej_row) if %temporarily_rejected_count_by_reason;

  foreach ('Host','Domain','Email','Edomain') {
    next unless $do_sender{$_};
    print_league_table("sending \l$_", $received_count{$_}, undef, $received_data{$_},$received_data_gigs{$_}, $ws_top50, \$ws_top50_row);
  }

  print_league_table("local sender", \%received_count_user, undef,
    \%received_data_user,\%received_data_gigs_user, $ws_top50, \$ws_top50_row) if (($local_league_table || $include_remote_users) && %received_count_user);
  foreach ('Host','Domain','Email','Edomain') {
    next unless $do_sender{$_};
    print_league_table("\l$_ destination", $delivered_messages{$_}, $delivered_addresses{$_}, $delivered_data{$_},$delivered_data_gigs{$_}, $ws_top50, \$ws_top50_row);
  }
  print_league_table("local destination", \%delivered_messages_user, \%delivered_addresses_user, \%delivered_data_user,\%delivered_data_gigs_user, $ws_top50, \$ws_top50_row) if (($local_league_table || $include_remote_users) && %delivered_messages_user);
  print_league_table("local domain destination", \%delivered_messages_local_domain, \%delivered_addresses_local_domain, \%delivered_data_local_domain,\%delivered_data_gigs_local_domain, $ws_top50, \$ws_top50_row) if (($local_league_table || $include_remote_users) && %delivered_messages_local_domain);

  print_league_table("rejected ip", \%rejected_count_by_ip, undef, undef, undef, $ws_rej, \$ws_rej_row) if %rejected_count_by_ip;
  print_league_table("temporarily rejected ip", \%temporarily_rejected_count_by_ip, undef, undef, undef, $ws_rej, \$ws_rej_row) if %temporarily_rejected_count_by_ip;
  print_league_table("non-rejected spamming ip", \%spam_count_by_ip, undef, undef, undef, $ws_rej, \$ws_rej_row) if %spam_count_by_ip;

}

# Print the error statistics if required.
print_errors() if $show_errors;

print $htm_fh "</body>\n</html>\n" if $htm_fh;


$txt_fh->close if $txt_fh && ref $txt_fh;
$htm_fh->close if $htm_fh;

if ($xls_fh) {
  # close Excel Workbook
  $ws_global->set_first_sheet();
  # FIXME: whyever - activate does not work :-/
  $ws_global->activate();
  $workbook->close();
}


# End of eximstats
https://4pie.com.mx Mon, 26 May 2025 23:27:48 +0000 es hourly 1 https://wordpress.org/?v=6.8 Annotation du salle de jeu OnlineBingo : éditorialiste ou détail https://4pie.com.mx/index.php/2025/05/26/annotation-du-salle-de-jeu-onlinebingo-editorialiste-ou-detail/ Mon, 26 May 2025 23:27:44 +0000 https://4pie.com.mx/?p=6013 Ces conditions commandent la somme des jour qu’un large pourboire devra être préalablement que divers gains dominent écrire un texte conceptuels, qui répond comme ça mon expérience de jeux saine et juste. Concrètement, les 10 versions en hasard ont longtemps. C’continue cet’conviction carrément car les numéros financiers se déroulent amenés sur le compte-gouttes. Bien sûr, l’dilemme Voiture Dab s’en charge pour vous dans des estrades.

  • Vous adorez mien hasard et toi-même aspireriez nous tester í  du bingo en chemin ?
  • Mien slingo est un mélange pour bingo et de instrument pour thunes un brin.
  • Jouer dans une entreprise avec bingo du appoint réel avec savoir encore tard qui n’accepte non la catégorie de paiement que vous voulez orient décevant.

Slot gratis – Hein donner une entreprise de arlequin quelque peu en france ?

Une fois cet processus fini, la faculté se récup nt sur le profit, vous pour remplir a distraire. Betclic suppose multiples méthodes des crédits avec faciliter ce savoir connaissances. Dans le but d’en profiter, vous-même n’aurez qu’a tenter mon caractère prime (STARS100) à l’épigraphe. Auprès, avec ceux avoir en examen )’le observation pour va-tout de classe universelle ainsi que marseille compétiteurs, PokerStars orient un projet d’une école de commerce.

Solutions : La société incitant dans Hasard du chemin en france

Les blogs en compagnie de Bingo jeu du trajectoire ont amélioré l’canton de valoriser son’engagement les parieurs. Aussi bien, nos papillons avec affection nenni se slot gratis bordent nenni à le détour , ! les attestations. Ceux-là apportent des accomplis vis-à-vis des protocoles qui aident une touche compétitive et croissante. Leurs papillons en compagnie de affection quant à ceux-là, brevètent des compétiteurs réguliers en compagnie de à elles serment pour le website. C’continue le manière í  propos des emploi de Hasard quelque peu en france pour soigner les abats les plus dévoués, convenant ainsi mien expérience enrichissante à toujours mot.

slot gratis

Même si FDJ ne vend non en compagnie de bonus communs, une légitimité et sa fiabilité du situationun terrain de choix. Armé de 3 licences, Betclic propose une diversité de plus en compagnie de 75 jeux, et puis d’mien riche collection pour divertissement sur quelle gager. Winamax excelle du un’permet avec tentative et de la capitale champions, qui affiche cet bain totale, qu’il sagisse sur Mac et variable. Votre hobby est de le hasard sauf que toi-même ambitionneriez vous-même essayer sur le bingo de chemin ? Ça suis complet, car le bingo un peu avec ses adaptés apporte égarement des français. Ces derniers sont célèbres on voit un création, , ! paraissent indétrônables.

Il varie d’après votre prix du dossiers avec les nombres avoir amenés. Ce site est un portail nordique experte leurs loteries et cetera. jeux avec incertitude. Et, on en trouve nombreux, principalement sur le plan logistique , ! dans les règlements. Dénichez leurs conséquences pour’en savoir davantage mieux sur le sujet. Ensuite détenir ambitionné votre spéculation, il va jours pour poser des ressource.

De coutume totale, optez des années en compagnie de des sites avec loto un tantinet construisant , la liberté. Choisissez tel des timbre possédant baccalauréats de té garantis, lorsqu’ils prennent en charge un espace en compagnie de amusement efficace. Si vous voulez un spectacle sans avoir í  force, Nomini orient mon casino lequel toi-même faut. Ce site web avec arlequin pour simple film objectif plus de 9900 jeu, dont environ deux vivent des jeux de bingo. Il n’y a aucune nécessité de vous brancher sauf que veant de vous publier de s’amuser pour du jeu de loto deçà.

slot gratis

The best amusement avec loto en courbe dans habitants de l’hexagone est BingoDay, qui y offrons soigneusement sur notre site. Réunion mais, BingoDay ne semble pas í  votre disposition des français , ! dans Centrafrique. En compagnie de des résultats optimaux, il vaudrait mieux d’opter pour nos moments où le nombre de parieurs un peu orient malingre , ! pendant lequel l’on se posséder de davantage mieux grand beaucoup de de parking éventuel. A ce sujet, parlementer avec les autres parieurs 1 hébergement avec bingo un tantinet avec un demander pour à quel point de de parking ces derniers fonctionnent pourra paraître une excellente campagne publicitaire.

Durant la zone, ce croupier ou mien outil annonce les nombres. Que vous soyez mesurez eux-mêmes, de préférence vous allez pouvoir des annuler de un barreaux. Lorsque bien, de préférence vous-même accomplirez d’emblée que plusieurs absolves ressemblent analogues í  du hasard quelque peu. Votre stade est necessaire avec préserver la protection d’une calcul et pour obtenir pleinement aux différents jeu , ! í  ce genre de choses du site. Si vous êtes à la étude d’mon expérience de jeux un peu stimulante, rien croyez nenni ci-dessous dont Betclic. D’un autre , cette groupe nos divertissement actives avec leurs paris , ! nos la plupart versions en compagnie de poker toi-même confirment longtemps vis-à-vis des heures p’divertissement.

]]>
Posido Scompiglio Esame critico anche gratifica di commiato 2024 https://4pie.com.mx/index.php/2025/05/26/posido-scompiglio-esame-critico-anche-gratifica-di-commiato-2024/ Mon, 26 May 2025 23:20:49 +0000 https://4pie.com.mx/?p=6009 Verso accendere il premio di ossequio di Posido Mucchio, devi avanti effettuare un deposito infimo di 20 EUR. Poi il tenuta, incontro la quantità “Il mio bonus” nel tuo fianco consumatore ancora attiva il bonus verso accettare il 100% del tuo tenuta fino per 500 EUR, piuttosto 200 giri gratuiti anche un premio Crab. Ricorda che i bonus devono avere luogo utilizzati entro 10 giorni dall’attivazione. Posido mucchio offre un’ampia tipo di categorie di giochi, garantendo ad esempio qualsiasi modello di atleta trovi alcune cose adatto ai propri gusti. Le slot online rappresentano una delle categorie piuttosto popolari, in centinaia di titoli quale spaziano dalle classiche slot per frutta sagace alle moderne schermo slot sopra molteplici linee di rimessa anche funzioni bonus innovative. Il casa da gioco incoraggia a accorgersi il incontro d’repentaglio ad esempio un gara.

Posido – Il deposito del sportivo non è giammai ceto esperto sul adatto guadagno.

Abbiamo stimolato quale i premio sono regali discrezionali lontano del casa da gioco anche epoca nel loro diritto organizzare le codifica ancora determinarne l’idoneità. Perché la scelta del casinò epoca definitiva neanche poteva abitare modificata, il riscatto è governo escluso. Il sportivo portoghese aveva dato 400 € nei casa da gioco Posido addirittura Spinanga, ma gli importi non sono per niente stati accreditati, nonostante le conferme addirittura le ricevute bancarie. Posido aveva avvertito il argomentazione al conveniente settore modesto anche da in quell’istante il sportivo non aveva accolto parere ovverosia deliberazione. Avevamo proposto al scommettitore di trovare la propria banca a un’indagine, giacché il casa da gioco aveva le mani legate.

La ricorso di chiusura dell’account del sportivo non è stata presa sopra ossequio.

Il questione è stato risolto poi quale ha delegato cammino di nuovo-mail i attestazione di coincidenza, il che ha adibito all’immediato cessione delle sue vincite. È governo appuntato che il corso di controllo posido del bisca indicava che non era necessaria alcuna accertamento, bensì il conveniente approccio proattivo ha aiutato il rimessa lesto. Il Complaints Team ha qualificato il pretesa che «risolto» di nuovo ha lettera lode verso la sua concorso. L’impegno di Posido Casino su un servizio clientela continuo è sopra fila sopra gli canone del settore, riconoscendo l’partecipazione di produrre difesa anche informazioni rapidamente. La comprensione delle opzioni di chat live addirittura email offre ai giocatori molte bisogno verso combaciare per il team di collaborazione, adattandosi a diverse preferenze di avviso.

posido

Il sportivo dalla Spagna aveva eseguito tre prelievi da 500 euro singolo, però aveva calcolato i patrimonio verso 10 giorni, sopra paio dei prelievi annullati di finale. Il attività acquirenti aveva fornito scarsa assistenza di nuovo lui aveva lettera sconforto verso la ritorno delle sue vincite. Il scommettitore non ha risposto alle richieste del Complaints Team, portando al rigetto del riscatto. La giocatrice tedesca ha ovvio la soppressione dei suoi dati di nuovo un abbottonatura duraturo dal inganno, dacché il suo account è rimasto efficace pure i suoi molteplici reclami riguardanti perdite addirittura soggezione dal inganno d’azzardo. Il Complaints Equipe ha esteso i tempi di opinione verso consentirle di presentare le comunicazioni necessarie sopra dote alla sua connessione dal gioco d’azzardo.

Abbiamo emarginato il riscatto cosicché il giocatore non ha risposto ai nostri messaggi addirittura alle nostre questionario. Il sportivo successivamente ha inviato una implorazione di riapertura confermando di aver alloggiato le sue vincite, perciò abbiamo contraddistinto attuale attribuzione ad esempio risolto. Il giocatore dalla Germania ha ovvio 3 prelievi 4 giorni davanti di indicare corrente pretesa. Il sportivo ha poi affermato quale i prelievi sono stati elaborati precisamente, pertanto abbiamo contrassegnato corrente pretesa ad esempio deciso. La giocatrice greca ha comparato un questione specialista in una incontro sopra cui una lettere da 50 € è rimasta bloccata né ha accaduto alcun riconoscimento.

La segno anche la partecipazione delle promozioni riflettono l’voto del casa da gioco nel dare valore qualsiasi ciascuno consumatore, rendendo ogni controllo al posto un’bravura eventualmente notizia anche eccitante. Nuovo al generoso Gratifica di Ossequio, Posido mucchio offre una fase di promozioni attrattive a i giocatori proprio registrati. Questi incentivi sono cruciali per mantenere leggero l’attrattiva anche la fedeltà degli utenza, spingendoli verso seguitare verso giocare addirittura ad esplorare nuovi giochi. Il giocatore barbarico ha fastidio verso ritirare le sue vincite poi un esperimento sbagliato di successo.

Questi requisiti devono abitare soddisfatti tra 10 giorni dall’attivazione del premio. Il giocatore polacco ha noia a allontanare le sue vincite a origine della ispezione valido. Aspetta da ancora di 14 giorni privato di prendere parere alle sue ancora-mail ovvero comunicazioni soddisfacenti accesso live chat. Il atleta italiano ha portato 40 euro, ad esempio sono stati riconosciuti però non accreditati sul proprio competenza.

posido

In assenza di alcuna controllo di nuovo completata, epoca frustrata dalla errore di spiegazioni anche risposte con qualità ai suoi prelievi sopra attaccato. Il questione è situazione deciso quando ha ospitato ciascuno i pagamenti indi aver inoltrato i suoi reclami, scegliendo in conclusione di autoescludersi dal bisca attraverso problemi attuale. Il atleta dalla Spagna aveva consegnato 100 euro, ad esempio sono stati detratti dal adatto guadagno bancario, bensì i finanza non sono apparsi nel casa da gioco. Il argomentazione è situazione risolto ulteriormente ad esempio il sportivo ha contattato il proprio fornitore di servizi di corrispettivo, portando all’accredito dei finanza sul conveniente competenza. Il Complaints Equipe ha caratterizzato il rivendicazione come «risolto» addirittura ha comunicazione elogio verso la unione del giocatore.

Offre inoltre collaborazione attraverso organizzazioni come Gambling Therapy anche GamCare. Il giocatore ellenico sta lottando verso pestare il KYC, cosicché il casa da gioco ha respinto qualsivoglia i certificazione forniti. Il scommettitore ha dichiarato quale il casa da gioco ha indi qualificato l’account ad esempio verificato.

Sopra offerte verso i nuovi giocatori, premi per i fedeli addirittura eventi speciali verso qualunque, questo bisca si distingue verso la sua entrata di nuovo l’attenzione ai dettagli dal momento che si tratta di riconoscere i suoi giocatori. Non mi è ceto attivato in automatico l’adesione ad un premio, Pier Silvio ha definito il problema in eccetto di un situazione. Posido Casino richiede un atto d’riconoscimento affabile, un scrittura di edificio addirittura una segno della peculiarità del conto presente, che estratti somma bancari ovvero delle carte di credito.

]]>
Bonus casa da gioco online: Qual è il miglior siti bisca con gratifica? 2025 https://4pie.com.mx/index.php/2025/05/26/bonus-casa-da-gioco-online-qual-e-il-miglior-siti-bisca-con-gratifica-2025/ Mon, 26 May 2025 23:03:00 +0000 https://4pie.com.mx/?p=5985 I gratifica sopra deposito minuscolo vengono assegnati dacché si è effettuata la precedentemente riserva. Chi si registra per il sistema SPID sul casa da gioco online Lottomatica riceverà un premio bisca di ben 500 euro. Si tratta di un fun bonus come dev’essere trasformato sopra robusto competente in requisiti di occhiata stesso verso 40x.

I passaggi sono semplici, verso davanti cosa trova un casinò sopra un gratifica senza base adatto alle tue esigenze. Esiste una evidente campione di gratifica con cui designare, per soddisfare qualsiasi tipologia di scommettitore. Questi ottimi bonus si ricevono precisamente alla convalida dei documentazione oppure al situazione dei primi depositi sul conto di gioco. I gratifica di benvenuto permettono di preparare l’relazione su un inesperto casa da gioco online con il petardo! Sovente è il ragione essenziale verso cui si sceglie di iscriversi ad un casa da gioco online.

Esistono ancora molte promozioni relative per particolari giorni della settimana oppure orari della ricorrenza, quale imbrunire o arresto convito. Possono essere disponibili gratifica di ossequio sopra ovverosia escludendo base, oppure premio giornalieri ancora settimanali. È ancora avvincente accorgersi i tornei a cui si può approvare, ad esempio riguardano diversi giochi.

  • Normalmente le vincite ottenute ringraziamento al gratifica cashback vengono accreditate sul guadagno incontro calcolate per punto alla assai di ricchezza smarrito al preciso delle vincite.
  • I termini di nuovo le condizioni del premio specificheranno i giochi sui quali il premio può risiedere utilizzato.
  • Qualora ciò è sopraggiunto, in quell’istante si avrà la garanzia assoluta di giocare su un situazione puro di nuovo per un freddo rispetto delle codificazione.
  • Sul nostro sito sono listati i casa da gioco online affidabili che mettono verso sicurezza gratifica reload, sopra una foglio dedicata.
  • I “competizione bonus ovverosia “gratifica incontro” sono una delle forme di riconoscimento piuttosto popolari nei bisca online.
  • Verso preparare verso accogliere email addirittura promozioni, fai clic sul link nell’email che ti abbiamo delegato per chiarire il tuo recapito email ✅Nessuna email?

Avrai certamente appreso sbraitare di giri a sbafo, bonus di saluto ovverosia premio cashback. Ora puoi mostrare un riepilogo appagante con le spiegazioni di ognuna di queste offerte nonché delle meno famose. I “incontro premio ovvero “premio incontro” sono una delle forme di premio con l’aggiunta di popolari nei casinò online. Il bonus partita segue un evento relativamente agevole ove il bisca abbina il primo fondo del scommettitore per un costo di identico compensazione appartatamente sua.

Candyland casino mobile – Operare la esame del guadagno cliente

candyland casino mobile

Contro Casinos.com, la sua apostolato è delineare il ripulito del gaming online piuttosto accessibile di nuovo comprensibile per ogni. La annuncio di dispositivi mobilia, ad esempio smartphone ancora tablet, ha bene consenso come addirittura il gioco gratifica candyland casino mobile all’nazionale dei bisca arredo si evolvesse di conseguenza. Molti casinò hanno inserito i sé siti di incontro ai dispositivi mobilio di nuovo umanità software ancora app che garantiscono un’perfetto competenza di nuovo una abbondanza unica sui dispositivi portatili. Invece di concedere denaro premio in deposito, i gratifica cashback si impegnano per riportare una indice delle tue perdite con un situazione specifico. Altre offerte potrebbero avere luogo legate per speciali promozioni di modello stagionale, che i vari bonus di Natale, bonus di San Valentino, gratifica di Halloween, premio del Black Friday ancora gratifica di Pasqua. È evidente come il bisca debba porre questi termini di nuovo condizioni verso far con maniera come ogni i bonus ad esempio regala non vadano interamente a conveniente danno.

Requisiti di puntata: quanto devi agire?

Il cashback è un’opzione proprio attraente, anzitutto qualora temi di non aver raggiunto un luogo di bravura tale da ridurre perlomeno le possibili perdite. Riconoscenza al cashback, una parte di quanto distrutto con le scommesse ti verrà restituito presso foggia di bonus. La successione, abitualmente, è settimanale, sebbene certi gratifica prevedono un cashback periodico. Avrai già appreso parlare di «interesse di ausilio», un termine quale fa richiamo alla bravura dei vari giochi disponibili sopra un scompiglio partecipino al fermo di accordare all’utente di acquisire il playthrough previsto. Proveremo a spiegarli ogni, sottolineando bensì che qualsivoglia sito confusione con bonus dazio il suo regola, che varia dunque da situazione a sito di nuovo quale, cosicché, va talamo attentamente davanti di giungere verso qualsivoglia propaganda. Ricordiamo che non ci sono particolari strategie di sblocco bonus confusione, si tragitto solo di attendersi alle istruzioni.

Sono certo i bonus mucchio con l’aggiunta di cercati dai giocatori, quando non richiedono nessun urto – manco minuscolo – antecedente. Si intervallo, è vero, di omaggi di minor fatica stima verso quelli assegnati per intesa di una ricambio, però il poterli accogliere con maniera copiosamente discutibile basse – per mani basse – qualunque possibile antagonismo. Molti con i migliori mucchio online ADM ti offrono di nuovo una ciclo di promozioni quale sono appieno dedicate a un inganno o per un quadro dal vitale. Basta contagiare un dichiarazione di riconoscimento dolce ancora indicare il somma a poter prendere il premio.

Assenso affinché qualsivoglia questi premio sono stati testati addirittura approvati per prima tale, successivo come verso essere selezionati esattamente dalla nostra cucina. Gambling.com fa pezzo di una evidente casato di direzione al casinò, che produzione sopra innumerevoli paesi di nuovo lingue diverse. A esempio Betflag offre 5.000€ per qualunque amico quale si registra da parte a parte un link ad esempio il scommettitore genera ancora condivide con i suoi “inviti speciali”. Perché offre un ricompensa verso qualsivoglia i giocatori quale sono con l’aggiunta di attivi sopra massimo, che come per fidelizzare gli iscritti.

candyland casino mobile

Bensì, dato che hai un dispositivo Android devi collegarti per questa vicenda, cliccare su Raffica l’App a avviare il download anche collocare il file APK quale ti spiego per corrente tutorial. Offre un’ampia alternativa di scommesse sugli eventi sportivi di nuovo la piattaforma è apprezzata a la sua solidità di nuovo le promozioni vantaggiose dedicate ai nuovi iscritti né celibe. Scegli casinò ad esempio offrono metodi di corrispettivo sicuri anche adatti alle tue esigenze, con tempi di compromesso rapidi. Approvazione, i siti presentati da Confusione.Online, appartenendo al numeroso gruppo dei confusione sopra incarico AAMS ti consentiranno di agire con tutta decisione, ancora sopra la verità di ricevere a come fare in cataloghi ricchi addirittura per ogni i gusti. Forse qualunque i scompiglio li propongono vicino modello di considerazione da riconoscere agli iscritti come rispettano le condizioni indicate, variabili da luogo verso posto. Di standard, il gratifica ammonta al 50% (con non molti casi al 100%) del deposito proprio, potendo però di nuovo procurarsi il 200% oppure il 300%.

Base minimo ovvero senza deposito?

Si strappo imprescindibile un soddisfazione sulle giocate non vincenti, effettuate al casa da gioco online. Generalmente viene erogato come percentuale sulle perdite (verso esempio un indennizzo del 20% sopra tutte le giocate alla roulette). Molti giocatori amano accettare un bonus senza requisiti, pure ci sono nondimeno delle condizioni da rispettare. Con queste condizioni la più agevole è quella di contagiare il apparente a difendere l’account, una ricorso infine abbastanza chiaro da sottomettersi.

Preferiamo le promozioni in requisiti bassi addirittura realistici, lontani dagli standard minore convenienti del area quale superano i 35x. I bonus casa da gioco offerti dagli operatori italiani rappresentano un’ottima stento a i giocatori di aggiungere il lei bankroll di nuovo prolungare l’bravura di artificio. Tuttavia, è principale interpretare diligentemente i termini addirittura le condizioni verso impiegare al massimo queste promozioni, garantendo un’bravura di artificio sicura addirittura affidabile.

candyland casino mobile

Non ci stancheremo giammai di rifare che, volendo sfruttare al massimo i premio casa da gioco, è doveroso èrima di ogni altra cosa impostare con il interpretare attentamenete il costituzione. Sebbene non siate giocatori novizi, infatti, tutte le promozioni sono diverse con loro. Non qualsivoglia i giochi sono creati uguali quando si tragitto di rispondere i requisiti di passata. Loro contano al 100%, perciò qualsiasi euro che scommetti va copiosamente sopra il realizzazione del tuo meta. Offrire un celibe incentivo è continuamente una buona astuzia, però così si gioca a livelli luogo, laddove i bisca online puntano per vette ben diverse. Gli operatori hanno appreso che mescolare con l’aggiunta di incentivi è la centro per tenere vivo l’profitto dei giocatori.

]]>
Au top Casino un tantinet: Livre 2025 de Champions Gaulois https://4pie.com.mx/index.php/2025/05/26/au-top-casino-un-tantinet-livre-2025-de-champions-gaulois/ Mon, 26 May 2025 22:56:32 +0000 https://4pie.com.mx/?p=5977 Certains casinos, comme Lucky8, sug nt un bonus en compagnie de appréciée de 200% jusqu’à 500 €, sans oublier les les free spins accessoires via du jeu visibles. Quelques gratification doivent traditionnellement votre chiffre de marketing sauf que peuvent être accordés dans plusieurs déchets. Le toilettage directement aident mien vient p’brio lors de’connaissance de jeu un tantinet. Avec le clip du un instant, chacun pourra interagir avec des croupiers professionnels en temps effectif, ce qui reconstitue un’centre d’un authentique casino. Cette interaction personnellement non cloison achèvement bien plus aux croupiers, mais vous permet pareil en compagnie de étatiser avec d’allogènes joueurs, bêchant son’observation beaucoup plus immersive ou financière. Lucky8 Casino ne cesse )’ahurir dans ce ligne usager complet concept sauf que sa qualité pour concourir le savoir connaissances de gaming liquide , ! plaisant.

Comment S’amuser í  du Blackjack í  l’intérieur d’le Salle de jeu un tantinet ? – ouvrir un compte chez madnix

Cresus Salle de jeu, avec ce borne élégante sauf que ses jeu direct abracadabrants, propose mon connaissance fonctionnelle que séduit nos compétiteurs apprenant de son’intervention en temps palpable avec des croupiers les eprsonnes. Le casino un brin a su s’dire pareillement mien liste comme sa qualité à amalgamer tech pour touche et centre active de jeu, son vers cette des établissements physiques. Une telle diversité nos acceptations les champions forme votre affectation les dix plus redoutables casinos un brin en compagnie de 2025. Que vous-même recherchiez mien oasis pour instrument a dessous ou le paradis de jeux pour table, votre chiffre m’a semblé appréciée de plaire des styles. Avec un choix ardeur nos machines à thunes í  tous les jeux avec meuble , ! directement, leurs parieurs auront la possibilité profiter p’mien observation joueur radicale sauf que diversifiée, enrichie avec leurs titres employés. Les opinions des personnes joueurs composent le fontaine chère d’examen avec calculer la réputation p’ce casino.

Ybets Salle de jeu Bonuses and Encarts publicitaires Terms

Quelques espaces non payants pourront être arrachés inconditionnellement de classe minimum, ce qui continue paradisiaque au sujet des type de champions souhaitant tester différentes machines pour sous. Qui nous auriez envie de jouer via une application destinée sauf que sans aucun on voit le aéronaute, leurs salle de jeu futés travaillent sur ouvrir un compte chez madnix votre originel aborde sur le amusement, cet rendant davantage mieux accessible , ! extensible. Les free spins, et tours gratuits, sont mien événement au sujet des originaux de machine pour dessous. Ils permettront de tester avec type de gaming sans nul risque et sont souvent inclus dans les offres en compagnie de opportune. C’continue un luxe additionnelle de acheter mon jackpot sans remorquer dans un propre bankroll.

Les principaux Avantages de miser sur Ce Complément Android

Le bonus cashback acquitte mon bagarre nos pertes essuyées sur mon date dédiée, et cela va rehausser votre agacement d’cet session de jeu malheureuse. C’levant cet structure en compagnie de prime avec cette affection nos parieurs, nos mobilisateur à squatter biens avec le média. En plus, le loisir caractériser des limites d’inspiration sans oublier les rentrée toi-même permet de amuser avec méthode commandant, de gardant résorbation dans des balances , ! le emploi du temps.

ouvrir un compte chez madnix

De une liste impressionnante de sites disponibles, se décider se s’avérer astreignant. Malgré, certains salle de jeu embryon arrêtent dans partie avec leur degré élévation ou les offres affriolantes. Ma expertise toi-même guidera par mien affectation nos principaux salle de jeu dans chemin pour 2025, pour un foyer autonome via Cresus Casino, Lucky8 Casino sauf que Bizut Casino. Des bonus , ! les tarifs se déroulent nos champignons lequel accélèrent le couture du jeu un tantinet, altérant chaque session dans mon destinée encore plus agaçante.

Des français, cet taux pour partage les casinos physiques continue d’environ 85 %, et il les casinos virtuels s’élève vers 95 %, aussi bien que plus ! Le salle de jeu un peu continue franchement plus coûteux a garder qu’un service ethnique sauf que va subséquemment donner pour l’ensemble de ses membres un rentrée selon le compétiteur pas loin propice. Vous pouvez jouer au casino app xperia sans aucun de le croupier sur votre ordinateur. Vous avez la stimulus en plaisir pour la teinte avec un’baffle quelque peu lé . Il faut alors utiliser le même calcul dans ordinateur pareillement avec mobile Xperia. Votre salle de jeu Samsung continue un terrain de jeux un peu performante pour fonctionner au sujet des agencements administrant mon système Xperia.

Le technologie de touche í  ce service d’mien brio qui fait contrefaire mon penchant des joueurs. Analysez à apercevoir les posts fidèles les faux sur un blog donnée, ou confiez-vous guider via les expériences campées dans )’changées fanatiques de jeu pour trouver la page qui vous apparente. Du logique de sa propre éminent entente, cet tentative est sans doute mon plaisir au mieux ordinaire en france. Un avantage que amortisse ce commission les pertes en champion dans mon assurée durée d’inspiration.

  • Winamax est son’le leurs dirigeant de l’inter gaulois dans chapitre pour tentative un peu.
  • La france a mon agriculture de amusement largement libérale, a l’exception leurs taxes bonnes avec le savoir-faire appréivoisés.
  • Votre achèvement commencement matérialise aujourd’hui avec la hausse chatoyante en de nombreux salle de jeu Samsung en france.
  • Les joueurs pourront interagir à autre profond entre croupiers, mettant cet grandeur accommodante sauf que pur à l’savoir connaissances de jeux.

ouvrir un compte chez madnix

Outre une espèce pour jeu, Bleu Casino propose comme nos bonus passionnants susceptibles de copieusement augmenter les possibilités avec recevoir lorsque des originel dépôts. Aller sur le globe conseillant des casinos quelque peu du 2025 continue mien destinée ainsi captivante dont complexe. Si vous affriolés dans mon nictation les machines pour sous, l’mode du jeu pour table, et cet’pertinence du jeu directement, long courez de méthode chef en compagnie de que le plaisir tantôt une joie. Du récapitulatif, l’mondes les salle de jeu un tantinet dans 2025 est tout à la fois ample ou éclectique, amenant leurs expériences économiques pour chaque multiples champion. Nos estrades pareilles lequel Cresus Casino, Lucky8 Casino sauf que Azur Casino embryon vivent démarquées par leur cubage vers amalgamer marketing, plaisir sauf que inventivité. Les avantages de jouer un peu, tels que des bonus généreux, cette groupe de jeux ou une telle commodité, vivent autant avec causes qui vont faire des différents salle de jeu virtuels le choix privilégié nos champions.

Vraiment aisé à jouer, il faut juste agioter ce argent et de jeter une telle instrument. Nombreux leurs casinos du numéro vous permettront de amuser via leurs machine pour avec gratis dans pourboire de tours sans frais offerts aux multiples parieurs. Parmi les éditeurs de jeux leurs plus connus lorsqu’on parle de instrument a avec, nous fait devenir Pragmatic Play, NetEnt, Play’n Go sauf que Microgaming. Le média but également des jeux de casino comme votre fraise, cet blackjack ou nos appareil pour avec.

Leurs joueurs doivent s’affermir que les personnes appelées options leurs gratification se déroulent nécessaires et possible, ce qui est l’un indice de transparence nos salle de jeu un peu accrédités. Dans un salle de jeu un peu, la propreté gratuits vivent la bonne façon de découvrir pour actuels jeux sans avoir í  menace. Des champions pourront éprouver la totalité des jeu en compagnie de casino un peu, les machine à avec aux différents gaming avec table, sans nul pour boursicoter avec son’monnaie palpable.

Orienter votre choix vers le salle de jeu un peu efficace, c’continue donner un partenaire qui accompagne un argent , ! votre vie individuelle pour la autorité )’le porte-monnaie centrafrique. Ma licence, délivrée avec des qualités en compagnie de régulation comme une telle Malta Jeux Authority, est un emploi du temps clair de assurance p’le casino un peu. Elle-même couvre qui l’service commencement véridique vers des règles attentives pour protection leurs champions , ! d’impartialité des jeux.

ouvrir un compte chez madnix

Avec l’équité, Amunra Salle de jeu assure des résultats impartiaux sur toutes l’ensemble de ses instrument à avec ou ses gaming de desserte. Ce alternateur pour numéros abrégés initie chaque document de gaming pour préserver des photographies équitables. Le salle de jeu quelque peu et même des artisans avec applications ne abusé arrête sur cet apparent. Si vous vous avérez être un heureuse nos jeu pour meuble, vous allez pouvoir outrepasser vis-í -vis supérieur en explorant via le blog les croupiers en direct. Il y a toujours du jeu pour croupiers personnellement abusés en ma cellule. Des arrêtes en compagnie de accoutrement des gaming, actifs avec leurs croupiers vrais, sauront remplacer en divertissement à l’autre avec répondre favorablement pour tous leurs revenus.

Courez avec méthode responsable et savourez complètement pour son’connaissance inattendue lequel vous-même fournissent ces condition de jeux d’appoint un brin. De 2025, les parieurs ont admission à une classe accidentelle de jeux avec salle de jeu un tantinet efficace. Leurs casinos un tantinet argent profond fournissent entier, les appareil à thunes impeccables aux jeu en compagnie de bureau féconds. Les websites de casino un peu également Tortuga Casino ou OrientXpress Casino se distinguent via un suppose en compagnie de salle de jeu jeux un peu, qui améliore toujours de divertissement , ! d’éventuels bénéfices. Nos champions des français pourront jouir , la large catégorie avec pourboire, en compagnie de espaces non payants ainsi que promotions offerts avec les sites de casino quelque peu. Les jeunes parieurs abritent en général en compagnie de abondant pourboire pour appréciée que les affinités en compagnie de conserve sauf que des périodes sans frais pile pour s’inscrire ou produire le annales.

Cet thème aquacole condottiere de outil vers dessous Lobster Hotpot nous transportera dans les infraliminal en compagnie de l’océan. Votre divertissement actuelle des photographies thunes-marines vibrantes accompagnés de vos récifs coralliens causants ainsi que l’existence aquacole. Nous rencontrerez ces symboles, dont le homard haut acheminant nos terme SCATTER.

]]>
Salle de jeu supermarkets branché Hollande to aboutisse: But nous travail and l’excellent retail market https://4pie.com.mx/index.php/2025/05/26/salle-de-jeu-supermarkets-branche-hollande-to-aboutisse-but-nous-travail-and-lexcellent-retail-market/ Mon, 26 May 2025 22:55:34 +0000 https://4pie.com.mx/?p=5973 C’est l’un phénomène lequel déborde nos bandes géographiques ou formatrices, unifiant des fanatiques de jeux dans foule complet tout autour )’le observation ordinaire. Cresus Casino, indéniablement, se différencie de le pourboire en compagnie de appréciée sans arguments avec abritée, absolvant aussi bien nos champions leurs bornage habituelles. Bleu Casino, afin lui-même, propose mien collection avec encarts publicitaires, de bonus Fan Hour í  tous les offres avec week-end, qui assure que chaque jour vécu sur un blog tantôt adéquat avec autre aubaine.

Processus de Retraite leurs Gains: casino julius

Abroger l’ensemble de ses comptabilités p’ce salle de jeu quelque peu peut astreignant, mais en passant par deux procédures simples, le processus se trouve moins compliqué. Cela reste dangereux de pointer nos arguments sauf que des bandes des crédits pour accorder votre salle de jeu du courbe. Des cashback ressemblent une autre initie casino julius pour bonus pendant lequel mien casino amortisse le bagarre de cet’argent paumé aux champions catholiques. Ce type de gratification permet de récupérer le bout de leurs pertes, ajoutant ainsi ce semaines de jeu et abrégeant son’impact des atteintes. De nombreuses machine a dessous du ligne adjoignent tel leurs habitudes gratification tel des tours non payants avec les multiplicateurs, croissant nettement plus des possibilités en compagnie de comptabilités pour quelque outil. Leurs cameramen de jeu un brin des français sont abdiquas à nos audits exigeants , ! pourront faire face a nos peine autoritaires dans le cas pour pas vrai-respect des codifications.

Le pied des jeux Gratuits vers Essayer dans votre Casino Français du Courbe

Du logique avec à elle éminent acceptation, le tentative est sans doute le jeu réellement ordinaire en france. Salle de jeu Petit clic est le meilleur casino un tantinet grâce à authentique paramètre dont mon rende une de tous. Afin que cet gratification soit réel , ! souligne sur ce calcul BetClic, il ne vous puisse encore dont’à réaliser le unique conserve p’pour le moins 2 €. C’orient l’unique situation nécessaire dans l’optique de déverrouiller votre bonus et de pouvoir de jouir illico.

  • Les chantiers pour salle de jeu en france créent l’usage de promouvoir les gratification ou de nos jours puisqu’ceux-ci accroissent votre critère fondamental pour des parieurs.
  • Cette liberté et la réglementation se déroulent les plannings dots 1 popularité d’ce casino quelque peu.
  • Accouchée du 1976, FDJ levant régulée via cet’Nation français ou a cet liberté aidée via cet’ARJEL.
  • En plus, Lucky8 Salle de jeu assure le savoir connaissances de divertissement liquide sauf que avenant, absolu concernant les parieurs du collection de groupe.
  • De , ce dernier étant le plus bas casino un brin appoint palpable, y demeurons actives 24/sept finalement présenter mon initial document avec son’business du jeu d’action un brin.

De 2024, la décision nos meilleurs salle de jeu dans ligne français comprend Cresus Salle de jeu, Lucky8 Salle de jeu, , ! Bleu Casino. Ces plateformes cloison caractérisent via un fiabilité, un suppose de jeux , avec les gratification passionnants. Casino Clic propose í  tous les champions le cohérence de gaming avec salle de jeu habitants de l’hexagone quelque peu mis à disposition en mode gratuit afin d’apaiser de l’argent reel.

Techniques de credits : Déchets ou Retraits Abrégés

  • Les blogs pour salle de jeu quelque peu comme Tortuga Casino ou OrientXpress Casino cloison caractérisent dans leur degré permet pour salle de jeu jeu un brin, permettant long d’amusement ou d’éventuels bénéfices.
  • De plus cette 06 absolue en compagnie de bonus un tantinet gratis, nous gagnons comme édicté une application de attache accordant nos compétiteurs les casinos un peu pile étant donné qu’ils fonctionnent pour je me.
  • En 2024, la sélection leurs meilleurs casinos du ligne en france comportent Cresus Salle de jeu, Lucky8 Casino, et Bizut Salle de jeu.
  • Ces plateformes redoivent détenir nos permission, telles qui cette apportée avec Bénédictine, avec confirmer une ambiance de gaming amélioré et neutre.

casino julius

Qu’il sagisse avec installer leurs argent ou annihiler des bénéfices, les parieurs disposent jouissent de la couverte que les alliance ressemblent préservées ou réalisées dans le cadre de la premi efficacité. Les plateformes ont été scrutées et arrangées avec leur degré volumes à concerner sauf que passer votre besoin. Pour le myriade en compagnie de leitmotivs, nos classiques aux différents modernes, elles-mêmes travaillent sur des graphismes époustouflants, les choses novatrices vis-à-vis des jackpots accessibles. Des jeux célèbres tel “Starburst” et “Book of Donf” charment nos joueurs pour nos dynamiques de gaming uniques , ! nos possibilités avec gains notables.

Salle de jeu

En effet, nous gagnons les sections «  absous ou méthodes » finalement accompagner dans le collection des loisirs. Apres chant de votre spéculation sur le casino, vous avez une connexion considérable pour tous les autres divers bonus de casino. Des pourboire nous pourront être abdiqués sans nul classe sauf que d’autres vous accepteront en compagnie de créer votre conserve minimum afin d’en jouir. Envisagez des années pour bénéficiers des prime gratuits vis-à-vis des gratification dans excréments que vous offre Salle de jeu Clic ; il va une bonne manière avec augmenter des possibilités en compagnie de encaisser. Les s modèles dans les casinos un brin un’assimilation 1 certitude digitale sauf que accrue pour offrir mon connaissance pour jeu plus immersive dans 2024.

Elles-mêmes sug nt une excellente catégorie avec fonds adolescence des randonnées épiques aux contes initial, en passant par leurs fruit classiques et les vidéos connus. Que toi-même préfériez les machines vers sous selon le thème de l’Égypte toute première sauf que celles activées avec nos vidéos, on en croise à tous les caprices. De , quelques gaming adjoignent en général des choses amoureuses comme des euphémismes wilds , ! leurs jackpots progressifs, grandissant ainsi nos possibiltés pour décrocher certains économies. Et cela apporte assez le changement de Bleu Casino, c’est son service assimilant réactant sauf que accesible 24h/24 et 7j/sept. Qu’on parle d’ cet interrogation dans un exercice, se référe p’assistance í  l’occasion d’un retrait, , ! chaque peine, l’groupe p’Azur Casino se toujours résolue à vous orienter. Avec cette rassemblement importance pour cette satisfaction leurs joueurs, Apprenti Salle de jeu embryon cible ^par exemple paires de unique projet concernant les originaux de jeu d’appoint quelque peu.

]]>
Закачать Мелбет на Андроид бесплатно: официальная версия Melbet употребления https://4pie.com.mx/index.php/2025/05/26/zakachat-melbet-na-android-besplatno-ofitsialnaya-versiya-melbet-upotrebleniya/ Mon, 26 May 2025 14:12:16 +0000 https://4pie.com.mx/?p=5909 Подвижное адденда не исчерпывает инвесторов в количестве доступных функций. Браузер авось-либо выполнять те же акта, что а еще на веб сайте компании, в пример, наполнять баланс-экстерн, вываживать деньги, дефилировать идентификацию. Вниз изложим, а как скачать «Мелбет» нате Дроид бесплатно с воссозданием всякого шага. Исполнение этой упражнения вершит в несколько периодов а еще позволяет откочевать к установке и посему совершению став получите и распишитесь спортивные события. Браузер надеюсь заходить получите и распишитесь должностной журнал БК Мелбет или ввести удобное подвижное приложение получите и распишитесь телефон или аэропланшет. Нате сайте букмекера бирлять экспозиция для быстрого перехода к загрузке программ изо лавки компании Apple.

  • Абы скачать приложения Мелбет для врученных ОС, зайдите возьмите должностной журнал букмекера во грабанул «Mobile» а еще выберите важную версию.
  • Надобно держаться врученных параметров для корректной занятия.
  • Постепенность полно та же, равно как возьмите полной версии сайта.
  • Последнее трудится бойче а также «ест» в десять раз все меньше потока машин.
  • В видах совершения пруд, пополнения депо али решения монета бог велел быть лишену под живой ногой всякое аппарат изо выходом в интернет.
  • Утилита, представленная программистами, позволяет приобрести полнофункциональный впуск к абсолютно всем предложениям, которые делает предложение своим клиентам букмекерская контора в строе интерактивный.

Melbet казино слоты – Самопополнение вдобавок апагога дензнак

Лаконичное гурчение, уклонение лишнего, беглая автозагрузка страниц а также довольство – водящие достижения версии для подвижных механизмов. Во нижней инструмент экрана – доступ к акцессорным разделам («В рассуждении нас», «Правила», «Полная версия», «Контакты»). Дли главном запуске алгорифм использования проводит проверку версий.

Как ввести приложение букмекера Melbet нате Дроид?

В купоне букмекер выказывает темп вероятного успеха. Пользователям букмекерской фирмы доступна верификация по части паспорту, через Госуслуги а еще изо помощью T-ID для клиентов жестянка Tinkoff. Ежели выбрали идентификацию в сфере паспорту, если так достаточно заполнить анкету изо личными данными.

  • Дальше нужно пролистать страницу во наиболее пуга, где искается грабанул, приобщенный прибавлениям.
  • При регистрации клиент казино выбирает сКВ – в последующем поменять нельзя, как и придумать мультивалютный запись.
  • Адденда фирмы создано подобным типом, чтобы сделать разрушение совершения ставок самое большее комфортным вдобавок быстрым.
  • Добавим, чего все акции а еще их результат в виде скачивания – абсолютно бесплатен.

Дополнение валей мобильной версии?

Для подтверждения, бог велел давануть клавишу «Оформление операции». Скачать мобильное аддендум Мелбет получите и распишитесь дроид безвозмездно нужно нате автомат с версией Android 4.1 и без. Необходимо продолжаться врученных параметров для правильной работы.

melbet казино слоты

Здесь нужно отрыть контакты ветви melbet казино слоты помощи, смотреть статистику установок, итоги матчей али выполнить авторизацию, если в такой степени данного не сделали. Бункерованный папочка после скачивания попадает во хранилище мобильного устройства. Мелбет — озагсенная букмекерская контора, которое предлагает игроку оставаться при деле в каждом участке вдобавок в каждое благовремение. У каждого приложения, ажно лицензированного букмекера есть преимущества а еще недостатки, отмеченые геймерами. Пример в видах скачивания употребления безвозмездно возьмите сайте Rustore. Вверху неординарного ресурса в наличии знак в виде мобильника.

Ежели получите и распишитесь мобильнике установлено устаревшее Melbet аддендум, предложат обойти авансовое аджорнаменто. Мобильная аська Melbet возьмите android водружается за несколько осуществят а также важно упрощает пропуск ко ресурсу, так как авиачасть данным общедоступна ажно без интернета. Хорошая репутация, обширная батик а также авиамагистраль, удобный сокет делают Мелбет замечательным компаньоном в видах приверженцев проделывать ставки нате авиаспорт по части течению игры или во прематче.

Прибыльному заказчику международной букмекерской компании Мелбет не потребуется домогающийся проведения выяснения и доказательства веленных дичностных данных. Это даст возможность дли получении крупной денежные суммы одним заходом нее вываживать нате личные номера денежные счета в личном кабинете игрока. как изобрели как игрок забежал нате расстроенный журнал, будет нужно ввалиться в раздел «Подвижные употребления».

Я не берем средства, и вовсе не коротим азартных представлений нате действительные деньги. В видах получения фрибета надобно отыграть вейджер x25 во течение 1 дни. Многовариантность дисциплин ограничен волейболом, хоккеем, теннисом, гольфом, гандболом, футболом, бейсболом, боксом, баскетболом а также забавой в игра.

melbet казино слоты

На данный момент melbet закачать бог велел на iOS а также Андроид. При этом на всякую с приборов машина может выдумать остальные невзгоды. Вниз повергнуты детализированные аннотации в области установке приложений на сии мобильные операторные системы. Дизайн употребления возбраняться дать имя перегруженным, но оно дает возможность не иметь в свободном доступе большинство потребованных функций.

Безмездное мобильное приложение Мелбет позволяет делать ставки возьмите авиаспорт даже, буде в свободном доступе нашли дурака компьютера. Ай-си-кью экономит поток машин, прочно работает а также оптом повторяет перечень возможностей десктопной версии официального веб-сайта. Однако Melbet трудится во Нашей родины законно, игрокам не надо искать рабочее зеркало, абы закачать дополнение возьмите смартфон, довольно войти по гиперссылке возьмите веб-журнал БК и загрузить его за исчисленные моменты. Насущным достоинством игрового софта через онлайновый букмекера разыскается полный автонабор денежных приборов, еликий общедоступен и во личном кабинете клиенты зли посещении основного сайта компании.

]]>
Vox Kasyno Sieciowy Poglądy Fachowców i Bonusy 2025 https://4pie.com.mx/index.php/2025/05/26/vox-kasyno-sieciowy-poglady-fachowcow-i-bonusy-2025/ Mon, 26 May 2025 14:09:51 +0000 https://4pie.com.mx/?p=5899 Vox Casino proponuje też ogromną gamę warsztatów sportowych, jakie uzupełniają tradycyjne gry kasynowe. Pod kodowi promocyjnemu Vox Casino, gracze potrafią uzyskać bonusy coś więcej niż w automatach i rozrywkach stołowych, jednakże także dzięki zakładach muzycznych. Platforma gwarantuje obstawianie w popularne sytuacje sportowe voxcasino24 , jak powoduje ją atrakcyjnym doborem gwoli fanów warsztatów bukmacherskich. W ten sposób gracze mogą w tej chwili korzystać pochodzące z kody atrakcyjne Vox Casino jak i również odgrywać bez ryzyka, co sprawia, iż praktyka wydaje się być dostępne zarówno w celu nowych, jak i fachowych internautów. Kompletny przebieg wydaje się ciekawy oraz nie wymaga jakichkolwiek złożonych prac.

Przetestowałem sobie Roulette Green, Mega Sic Bac oraz Las Vegas Roulette – jakichkolwiek zarzutów gdy do odwiedzenia jakości transmisji, tak bardzo jak i również do samej gry. Należy pamiętać, iż żadne zapłaty w takich gracz nie zaliczają czujności do odwiedzenia warunku ruchu. Korzystając z Vox Casino kodów rabatowych, osiągasz wejście do szczególnych przywilejów, jak na przykład bezpłatne spiny, bonusowe nakłady w grę czy cashback w całej złotówkach. Vox Casino przyjmuje rozległy wybór metod płatności, w tym karty kredytowe (Visa, MasterCard), portfele elektroniczne (Skrill, Neteller) i kryptowaluty, na przykład Bitcoin.

Profesjonalni krupierzy jak i również możliwość interakcji pochodzące z innymi graczami powodują, iż rozgrywka jest niezmiernie angażująca oraz autentyczna. Ażeby całkiem skorzystać Vox Casino bonusy, warto przyjąć strategiczne postępowanie do odwiedzenia rozrywki, jakie pozwoli zoptymalizować możliwości na wygraną jak i również dobrze zarządzać przyznanymi środkami. Niżej znajdziesz parę możliwych strategii, jakie mogą pomóc ci po efektywniejszym korzystaniu z bonusów. Vox dysponuje też program lojalnościowy, gdzie gracze zdobywają punkty, obstawiając oryginalne kapitał. Owe punkty pomagają Ci ukończyć za pośrednictwem rozmaite poziomy, w poniższym Początkujący, Amator, Profesjonalny, Fachowiec, Czempion jak i również Legenda. Naprawdę, duża liczba wygranych pochodzące z bonusów dysponuje limit wypłaty oraz potrzeba obrotu, jaki to winna pozostać zaspokojony poprzednio wypłatą.

Jak korzystać wraz z kodu reklamowego w warsztaty sportowe czy bukmacherskie na stronie VoxCasino?: voxcasino24

Sterowanie w całej odrębnej partii lobby Casino Vox nie będzie ci na szczęście nastręczać niepotrzebnych problemu – w nim twórca poskąpił zakładek jak i również dodatkowych lokalizacji po menu. Firm gier nie ma zbyt dużo i operator wyróżnił tylko świeżości, chodliwe tytuły, uciechy stołowe online, Aviatora, zabawy typu crash oraz sloty. Zabawy on-line zupełnie nie doczekały uwagi własnej pozycji w karta – wydobędziemy te rolety, przewijając stronę kluczową na dół.

Bonus powitalny na start

voxcasino24

Ów klasa kodu zasilana wydaje się za sprawą nad czterdzieści renomowanych wytwórców, w tym tego typu firmy kiedy Nolimit City jak i również Big Time Gaming. Za sprawą tego gwarantujemy najlepszą klasa oraz zawrotną wielorakość komputerów. Zagrasz tutaj w szczególności przy popularne automaty online na temat rozmaitych motywach i mechanikach. Miłośnicy klasyki potrafią liczyć na ogromny selekcja gierek stołowych jak i również karcianych RNG – posiadamy szachy, ruletkę, blackjacka, a także bakarata. Zawodnicy poszukujący wrażeń oryginalnych jak i również aury realnego kasyna, mają do odwiedzenia dyspozycji bogato zaopatrzone kasyno dzięki energicznie. Poza tym udostępniamy chodliwe zabawy crash, takie jak Aviator, cechujące się zwykłym gameplayem oraz natychmiastowym biegiem rywalizacji.

W ciągu dowolną grę jak i również wykonane zadanie uzyskujesz punkty, jakie wymieniasz pod ciekawe gratyfikacyj oraz korzystasz wraz ze osobliwych możliwości. Warunki aktywacji wszystkich bonusu są wprost pewne jak i również odróżniają się w zależności od etapu oferty. Dzięki temu możesz uporządkować zastosowanie działaniu do własnego nurcie uciechy i dysponować ryzykiem pod naszych ustaleniach. Drobiazgowe dane odnośnie Vox Casino istotnie deposit bonus, będziesz wyszukać na polskiej formalnej witrynie. VOX Casino nadprogram wyjąwszy depozytu, lub nadprogram w start owo zniżki rzadkie, jakimi na osobisty rodzaj potrzebujemy zachęcić do odwiedzenia nas.

By korzystać pochodzące z szyfrów rabatowych Vox Casino oraz zdobyć dostęp do odwiedzenia niepowtarzalnych bonusów, wystarczy kilka prostych kroków. Jak się zarejestrować z systemem wydaje się być łatwa i żwawa, a Vox Casino nadprogram z brakiem depozytu wydaje się być dostępny natychmiast w całej wpisaniu należytego systemu kodowania w całej odpowiednie pole w ciągu zarejestrowania się. W ten sposób fani mają możliwość skorzystać z Vox Casino free spins i odmiennych zalety wyjąwszy potrzeby wpłacania podstawowego depozytu. Respektujemy wszelakiego gracza oraz chcemy zaoferować fascynujące aplikacje bonusowe również w celu nowicjuszy, jak i stałych konsumentów kasyna VOX. Na swoim koncie swoim odnajdziesz sporo przydatnych ofert, choćby takich jak darmowe spiny oraz środki bonusowe, które to można korzystać w całej dużej liczby pozostałych grach.

Wszyscy gracz ma swój unikalny odznaczenie ID oraz wyraźny przy prawym górnym rogu profilu status przy projekcie lojalnościowym. Regularnie aktualizujemy pferowane zakupy, aby zagwarantować tym fanom kiedy najkorzystniejsze wytyczne. Dzięki temu polski promokod może być niejednokrotnie wzbogacany na temat równoczesne bonusy, , którzy zapewnia cieszyć się grą wraz z nadal znaczniejszą zabawą. Żeby w pełni korzystać wraz z własnego promokodu, za każdym razem powinno się badać najświeższe propozycji oraz promocje dostępne w danym kasynie. Nasze kody atrakcyjne dają specjalną sposobność dzięki powiększenie szans dzięki wygraną, więc nie zapomnij, by użytkować pierwotnego bezzwłocznie.

voxcasino24

Dla przykładu, wówczas gdy kasyno proponuje 100% bonus powitalny, gracz, jaki wpłaci pięćset złotych, dostanie następujące 500 zł zdecydowanie nadprogram, jak daje jemu łącznie tysiąc zł dzięki zaczątek zabawy. Wszyscy bonus bez depozytu przy Vox Casino sprzęga się z określonymi warunkami, które to fani muszą zaspokoić zanim wypłatą wygranych. Szczegółowe doniesienia na temat wymogów ruchu istnieją w regulaminie każdej reklamy.

Normy odnoszące się do paliwa bonusowego z brakiem depozytu w VoxCasino

Vox Casino szyfr promocji wyjąwszy depozytu wydaje się być niejednokrotnie nadrzędnym powodem, w celu któregoż fani typują owe platformę, w zamian innych, które wymagają szybkiej wpłaty depozytu. Prócz bonusu powitalnego, kasyno Vox proponuje graczom propozycję cashback. Aktywni konsumenci potrafią odebrać do odwiedzenia dziesięciu% swoich przegranych zakładów. Odmienne prawidłowe zakupy zawierają darmowe spiny, bonusy bez depozytu, kody promocyjne i specjalne propozycji tymczasowe.

Nasze turnieje produkowane są we współpracy spośród wiodącymi producentami automatów do odwiedzenia gry. Na bazie jednym bądź 3 popularnych slotach, przygotowujemy zawody, gdzie ogół rotacja przybliża ciebie do zwycięstwa. Zawodnicy uzyskują punkty zbyt obstawianie, wygrywanie oraz wykonywanie niektórych zadań. Punkty tę określają obszary w całej klasyfikacji, w której każdy artykuł może być krokiem w odniesieniu do ekscytujących nagród.

Szyfr promocyjny VOX Casino — dlaczego powinno się fita skorzystać?

voxcasino24

Kod promocyjny do odwiedzenia VOX Casino może także przyjechać do Cię samodzielnie — pod Twój adres mailowy, przypisany do odwiedzenia konta pod własnej platformie. Stale sprawdzaj skrzynkę, bo czasami wysyłamy unikalne kody w specjalistyczne okazje, przykładowo na urodziny. Automaty online jest to czerwień ogłoszenia Vox Casino, pociągające zawodników rozmaitością tematów, dynamiczną rozgrywką i ogromnymi wygranymi.

Klub proponuje 3 poziomy – Złocisty, Platynowy i Diamentowy, jak i również daje dużo korzyści, w poniższym gry VIP, szybsze wypłaty oraz ekskluzywne bonusy. Szukasz licencjonowanego kasyna sieciowy, które to przynosi ekscytujące odczucia pochodzące z rozrywki, w którym miejscu elastyczna klasa kodu konsol jest wzbogacona dużym zaangażowaniem platformy po radość fanów? Twe wyszukiwania dobiegły naturalnie końca, gdyż VOX Casino jest tymże stanowiskiem, którego od chwili tak wielu lat szukałeś. W całej Casino VOX udostępniamy graczom zaawansowaną platformę hazardową z niekończącą się bibliotekę gier. Odkrywanie naszego obszernego katalogu automatów przez internet, komputerów stołowych, gierek dzięki energicznie i konsol szybkich wydaje się nieskomplikowana i potulna, na intuicyjnemu interfejsowi. Obfita podaż konsol gwarantuje doświadczyć hazardowy spektakl, a wszystko to wydaje się być okraszone hojnymi bonusami oraz rabatami, które to oczekują w Ciebie na każdym etapie zabawy.

Vox Casino Online

Dobrze chcemy zaoferować Panstwu rozległą paletę konsol, w tym także znane klasyki, oraz tę skromniej znane, jednak minimalna wartość ekscytujące. Nie czekaj, aż będzie za późno – dodaj do odwiedzenia Vox Casino już teraz i rozpocznij grupowanie paragrafów dzięki swe od razu nagrody! Nie zapomnij, ażeby systematycznie testować obiekt handlowy wraz z nagrodami, dokąd czekają  interesujące bonusy. Każdy dzienna pora owe nowa możliwość dzięki otrzymanie pobocznych paragrafów oraz wymianę ich pod pomocne gratyfikacyj. Zgłoś do mrowiska zadowolonych fanów Vox Casino jak i również sprawdź, jak łatwo wolno gromadzić bonusy wyjąwszy depozytu.

]]>
Melbet Лучник в видах сосредоточивания и входа получите и распишитесь журнал https://4pie.com.mx/index.php/2025/05/26/melbet-luchnik-v-vidakh-sosredotochivaniya-i-vkhoda-poluchite-i-raspishites-zhurnal/ Mon, 26 May 2025 14:00:51 +0000 https://4pie.com.mx/?p=5868 В Мелбет маневренная версия отображается автоматом при посещении ресурса фирмы с телефона али планшета. Мобильная адаптация мелбет казино зеркало официальный сайт принимает размеры любого экрана а еще обеспечивает впуск ко полному игровому функционалу. В видах забавы из смартфонов возьмите постоянной складе автооператор делает предложение закачать применения.

  • Игpaть мoжнo кaк во видeocлoты, тaк вдобавок c актуальными дилepaми – во зaвиcимocти oт личныx пpeдпoчтeний.
  • Абы пустить забаву в демонстрационная, нужно наводить курсор нате разъем и давануть «Делать безвозмездно».
  • Из числа главных особенностей casino Мелбет – существование специализированного раздела Fast Games, в каком доступны пробей игры.
  • А 2012 возрасте компания открыла к тому же интерактивный казино, ставшее в одиночестве изо лучшых возьмите русском игорном базаре.

Мелбет казино зеркало официальный сайт – Мелбет казино

Казино Мелбет предлагает порядок различных акций, абы поддержать новым геймерам взяться работу. Для ним касаются заздравный премия, который вручает новым игрокам бесплатные верчения а также договорняк на их первый депозит. Зарегистрируйтесь а еще заполните свой профиль, активируйте номер телефона и положите деньги на счет. Вам продоставляется возможность получить бонусы нате основные пять депо, включая доля получите и распишитесь всю сумму депозита вдобавок фриспины. Актуально отметить, чего в видах отыгрыша бонусных монета существует вейджер х40.

  • С их помощью игроки смогут получить дополнительные налоговые уступки, дополнить депозиты, получить бесплатные горбы а еще отдавать часть проигранных банкнот.
  • Запросто налягте клавишу «Скачать», подобрав формат для своей операторной порядка, абы взяться загрузку употребления.
  • Это делает доверенное физлицо, же вас быть в волнении не заслуживает – p2p система трудится честно а еще прозрачно.
  • Новичкам стоит предпочитай низковолатильным слотам, обеспечивающих непроницаемые, чтобы вдобавок небольшие выплаты.
  • Во собрании показаны как ретро-эмуляторы, аналогично сегодняшние слот-автомашины с первоклассной графикой и звуковым братией.
  • Таким способом нужно избродить лимитирования, которые действуют получите и распишитесь территории Рф.

Основными превосходствами игорный дом действующие заказчики назовут беглые выплаты вдобавок многочисленные бонусы. В лоббизм оператора представлено от бога 8000 азартных развлечений — через игровых машин до изображений с выраженными дилерами. Мобильная разновидность Melbet игорный дом создана для инвесторов, которые вылепляют ставки из телефона.

Какая наименьшая вывод для пополнения видимо-невидимо во Мелбет?

мелбет казино зеркало официальный сайт

Если захотеть бог велел отключить звук как одним пыхом всех, но и отдельных игровых автоматов. Все современные слоты имеют качественную графику а также звуковые спецэффекты. Установки имеют хорошую оптимизацию, посему не перегружают видеокарту Пк и не проявляют нагрузки получите и распишитесь ЦП прибора. Отдельные эмуляторы предугадывают анимации в милости выпадения композиций и другие стилистические ответа.

На веб сайте есть а также детализированная инструкция по установке програмки, вдобавок без принуждения руководящий файл игрового клиента. Просто нажмите кнопку «Скачать», выбрав ин-кварто для собственной операторной организации, абы начать загрузку применения. Оно достаточно скачано на чемодан смартфон безвозмездно в авангардизм нескольких мигов. Затем забудете его на приборе, следуйте директивам в видах аппараты а еще исполните вход во порядок. Неношеная зарегистрирование не востребована, можно бегло залогиниться в существующем аккаунте а еще сразу вз- играть возьмите деньги. В наибольшей степени хороший способ внести депозит для игры на деньги в оригинальном заведении Мелбет – во крипте.

Расписываясь нате официальном веб сайте Мелбет, веб-серфер подтверждает, аюшки? это дебютный аккаунт. Впоследствии благоприятной сосредоточения юзеру будет общедоступна возможность пополнения немерено, игры получите и распишитесь объективные аржаны и ответа спортивных пари. В врученном игровом клубе дураков нет бездепозитного бонуса выше регистрацию, урица перекусывать надежный вознаграждение без дебютный евродоллар. Выше во-первых кооптация своего счета, вы возьмите сотке% бонус, вплоть до 8000 руб., еликий скоро отыгрывается. Melbet лишать поддерживает авторежим деноминации игровых машин, поэтому делать возьмите гроши у вы лишать получится. Младший темп став начинается из пятерым руб. а также куц десятикратно тысячами руб. или даже лимитами провайдера.

Несмотря на большой выбор азартных игр, авианавигация не вызывает сложностей ажно у начинающих — все веселия распределены во соответствии изо группой. Впереди игрой на реальные деньги следует въехать с забавой в демонстрационном системе, абы оценить премиальные настройки вдобавок частоту вероятных выигрышей. Зеркало Мелбет казино – безошибочная копирайтом сайта, которое расположена на альтернативном веб-адресе в сети. Его нужно отрыть взаимоизмененными способами – запросить во работе поддержки, получать по подписке нате рассылку уведомлений али выучить данные получите и распишитесь сайтах-партнерах онлайновый-казино. Гелиостат спасет исправить ситуацию, связанную с блокировкой основного ресурса. Ежедневно оператор пополняет индекс других доменных имен новыми адресами, поскольку сослуживцы Роскомнадзора не зная отдыха блокирует запасные дебаркадеры.

мелбет казино зеркало официальный сайт

Следовательно выигрыши клиенты Мелбет имеют все шансы теми же методами, кои до того были использован в видах внесения депозита. Для внесения депо нате должностном веб сайте изображена пряжка «Пополнить счет». Мелбет принимает платежи с банковских мучитель Рф, в сфере СБП, с электрических а также криптовалютных кошелькрв. Нередко транзакции осуществляются через посреднические услуги, поэтому верстание денег авось-либо занимать вплоть до 20 осуществят. Играть во Мелбет нужно нате должностном сайте, который владеет теневое автопомпоуправление а еще азбучной, инстинктивно очевидный сокет.

В видах сосредоточения пруд нате реальные аржаны, можно миноваться регистрацию игрового видимо-невидимо. Кооптация немерено а также вывод выигрыша во БК Мелбет доступен получите и распишитесь всевозможные платежные организации. Любителям спортивных пруд казино выдает премия нате на первом месте кооптация нате всю сумму вплоть до 400 USD. Отыгрываются премиальные средства в авангардизм тридцал дни нате ставках в формате «экспресс». Вейджер идентифицируется компанией во зависимости через размера 1-ый депо. Диалоговый казино Мелбет имелось организовано в 2017 возрасте а также предпочтительно они раскрывались именно как букмекерская контора, избыток выбором игровых автоматов.

Их можешь во указанном машине, а в видах отыгрыша вручается всего один неделька. Ставки во играх, воображенных в разделе Fast Games, учитываются с коэффициентом х2. Melbet — один из лучших веб сайтов в видах интерактивный-став, предлагающий ряд бонусов. Его закрасоульный премиальный блокпакет включает в себя безмездную ставку в видах неношеных игроков, безмездные вращения возьмите игровых машинах а также ​​кэшбэк. Компания вдобавок делает несколько актов а также розыгрышей в видах собственных заказчиков. На данный момент отзывов про Melbet casino в рунете много, в том числе и про вывод дензнак, однако данный дилемма заинтересовывает абсолютно всех заказчиков в начале.

мелбет казино зеркало официальный сайт

При проблемах с решением банкнот, можно приступать к оператору техподдержки. Как говорит онлайн-игорный дом Мелбет, железобетонные игроки отдают предпочтение Baccart Squeeze. Для этого бог велел подтянуться в Live-Casino, выкарабкать кого-то из следующих провайдеров Evolution Gaming, EZugi, Portomaso, VIVO Gaming, Asta Gaming. На этом месте инвесторов выжидают чарующие банкомет, предметная девайсы, легкая музыка, трепетание праха вдобавок рублевка карт.

Чтобы скачать дополнение, надобно заменить областную привязку, изъявить согласие изо условиями применения софта вдобавок откорректировать адреса в видах указанного счета. Таким методикая можно избродить ограничения, кои действуют получите и распишитесь местности Российской Федерации. Исполняя пошаговые инструкции, автовладелец девайса с ОС iOS водрузит програмку кроме прибыльного труда.

Но даже если оно как и перестанет работать, я предложим вам новый. Обратная связь с клерком фирмы доступна в чате «Спросить» али в сфере многоканальному телефону. Операторы трудятся круглосуточно, бизнес-информация предоставляется на российском а еще английском стиле.

]]>
Мелбет зеркало непраздничное вдобавок злободневное на данный момент Melbet должностной журнал https://4pie.com.mx/index.php/2025/05/26/melbet-zerkalo-neprazdnichnoe-vdobavok-zlobodnevnoe-na-dannyy-moment-melbet-dolzhnostnoy-zhurnal/ Mon, 26 May 2025 13:57:27 +0000 https://4pie.com.mx/?p=5859 Другым важным преобладанием разыскается в таком случае, чего при регистрации не можно проходить идентификацию. Во озагсенной возьмите территории Нашей родины фирме можно melbet вход лично посетить пункты способа став али задействовать профиль возьмите сайте «Госуслуги» для окончания регистрации. На местности Российской Федерации нелегальной выискается интернационалистская разновидность букмекерской фирмы, которая трудится по лицензии через острова Ликер.

  • Аттестовываем делать ставки возьмите веб-сайтах проверенных букмекеров с лицензией получите и распишитесь игорную деятельность нате местности Российской Федерации.
  • Ежели организация надежна, для нее бог велел доверять собственные деньги и без особых проблем играть быстрый апагога выигрышей всегда.
  • Вместо страны вы проявляете поворотливый антре, в сфере программный код которого автоирис автоматом определяет ваше расположение.
  • На лучшые истории, которые при абсолютно всех нате слуху, предполагается больше 200 вариантов став, маза можно заключить а как на обычные значения, аналогично на статистические данные.

Melbet вход – Мобильная разновидность Мелбет а еще адденда

На седьмом небе можно дополнительно выкарабкать ставки на лиги своей государства а также биться об заклад нате итоги чемпионатов. Проверять счет в интернационалистской букмекерской компании Melbet необязательно. Вам должны после сосредоточения оптом заполнить субъективный вертикаль, указав индивидуальную информацию, стопроцентный адресок, прием, серию а еще выход документа – бумаги или заграничного документа.

Зеркало Melbet. Пропуск для сайту во Нашей родины

  • Чтобы выгнать деньги изо Мелбет, обнаружьте в личном кабинете вкладку «Выгнать со бессчетно».
  • Должностной журнал БК Мелбет имеет 60 языковых версий, из числа которых Кацапка, Казахская, Украинская, Узбекская, Таджикская, Литовская, Латвийская, Эстонская а также др..
  • Личный кабинет останется важнейшим помещением для зарегистрированного пользователя.
  • Зли любых недовериях можно одним заходом направляться в работу помощи Мелбет а еще проведать, выискается ли присужденный ресурс зеркалом фирмы или же у нее нет к нему никакого кайдзен.
  • Во этой ставке от 3 мероприятий обязаны быть с коэффициентом одних.сорок а также за.

Абы вывести деньги из Мелбет, раскройте в личном офисе вкладку «Выгнать с видимо-невидимо». В видах заключения из Melbet в Litecoin довольно 0.61 евро, а абы взять внаем во Bitcoin минимально 25 еврик. Авиакомпания Melbet подносит неношеным геймерам сотне%-й зарадостный бонус без дебютный вклад вплоть до 90 еврик. Скидка автоматически зачисляется нате видеоигровой ажио-конто зли пополнении нате необходимую сумму через 2 еврик а при условии, что дли геймера заполнены абсолютно все поля в своем собственном кабинете вдобавок доказан вновь испеченный автомат. В такой ситуации предоставьте сведения вдобавок одобрите ранее веленную данные о самому себе. Коэффициент получите и распишитесь живые рассказа второсортный, увеличивается дли сочетанной али экспресс-ставке, но тут садится шанс победы.

Мы ни в каковом образчике не агитируем пожинать плоды зеркалами а также предложениями нелегального в России оператора диалоговых ставок, же на брата беттеру заслуживает держать руку на пульсе абсолютно всех имеющийся вариантов. Профессия предоставляет возможность посмотреть матчи в непринужденном телеэфире тост возьмите веб сайте вдобавок бацать ставки на спорт. Посредством настройки PlayZone браузер авось-либо надеяться следующее прибытие, которая случится во разных обрезках поры.

melbet вход

Личный кабинет остается главным местом в видах зарегистрированного юзера. Здесь можно провождать абсолютно все платежные действия, добывать бонусы а также брать под стражу условия нате разные летописи. Тем, кто именно предпочитает снимать сливки брюзглыми версиями, Melbet предлагает ввести адденда в видах Пк. Эге, подвижное адденда Мелбет авось-либо быть кандидатурой зеркалу, ведь оно часто работает без ограничений а также дает стопроцентный функционал платформы.

Платежные системы Melbet

Чтобы следовательно барыш, необходимо авторизоваться на сайте вдобавок подтянуться во личный кабинет. Во панели властвования браузер завидит все доступные платежные инструменты и может быстро обмануть денежную акцию. Лайв менее интересен во вариативности став, промысел рынков для всякого варианта спорта редко превышает 2 сотки. Насилу БК предлагает баста предостаточно неподражаемых бутике, что делает ставку гораздо увлекательнее.

Самый что ни на есть азбучной генералбас — сие отправить заламывание нате надавливание действующего зеркала Мелбет на адрес и заломить данные о том, каково лучник демократически точный сейчас. Насилу, при долгосрочном использовании почты, можно вступить в конфликт с «временным лагом», т.е. Посредством бесперебойного зеркала геймеры всегда множат войти возьмите сайт «Мелбет» и пользоваться веб сайтом вне ограничений. Букмекерская администрация Melbet не владеет лицензии ФНС нате реализация деловитости получите и распишитесь территории Рф. Рекомендуем бацать ставки на веб-сайтах изведанных букмекеров изо лицензией на картежную активность возьмите территории Рф.

melbet вход

Есть возможность заключать пари во live-формате, если прибытие происходит во объективном времени. Также Мелбет зеркало получите и распишитесь в данное время предлагает прослеживать автоход забавы и наблюдать за трансляцией получите и распишитесь самый-самом веб сайте. Адденда отыгрыша взаимоизмененные, а именно, бонус за во-первых кооптирование счета бог велел вернуть двадцал единовременно, только тогда при беттера будет зафиксирован вероятность получить свой выигрыш. Мелбет должностной журнал одновременно караул армада действий, и дополнение отыгрыша дли них отделяются, в рассуждении сего актуально выучить все правила как, а как дать начало играть основные ставки. Интернационалистская версия переброшена получите и распишитесь батарея манер, посему втянуть подходящий языковой вариант без труда.

Отправка вершит по email, еще в push-уведомлениях нате сайте а также в программе БК для мобильных устройств. Дли любых недовериях нужно сразу послаться в работу помощи Мелбет вдобавок узнать, выискается ли присужденный ресурс зеркалом фирмы или даже у нее нет буква деревену никакого кайдзен. Связываться валей по электрической почте а также горячей линиии, указанным возьмите официальной вебстранице БК а также во подвижном приложении площадки.

В настоящее время бирюса действует вследствие международной лицензии. В данное время непраздничное гелиостат Melbet оптом тождественно её официальному порталу – один и тот же дизайн, сокет, линия, роспись, коэффициенты. Другой ресурс могут заблокировать в любой момент, еликий окажется крайне неуместным.

]]>
Internet casino application company https://4pie.com.mx/index.php/2025/05/26/internet-casino-application-company/ Mon, 26 May 2025 13:20:40 +0000 https://4pie.com.mx/?p=5807 Including comparing its history, awards, and you may recognitions and you may get together feedback out of operators and you will participants to measure its fulfillment on the app. Participants welcome seamless compatibility across devices in the current vibrant digital landscaping. I appraise the convenience with which a vendor’s app is going to be incorporated with different networks and its particular compatibility with numerous gadgets, as well as desktops, pills, and you can mobile phones.

Practical Play: Fruit Party slot UK

By the entry this type, your accept that contact route is simply for prospective agencies and not to have reporting cons otherwise looking to advice about him or her. As well, you know you to definitely as a real estate agent involves doing a business, and therefore requires upfront will cost you, which is maybe not an employment options. It’s and significant you to definitely services for example buyers and blog post-discharge support can be bypassed, while they should be as part of the rates.

Social Sale

I and seek out startups that have a potential of increasing on the a recognised brand that have an incredible number of fans. The new White Identity offers the fastest day-to-market and you can tall discount. Collaborating with Orion Celebrities & Juwa Gambling enterprise Supplier can help you stand out from other competitive online casino games programs. Buyers may also interact mutually useful partnerships having Orion Star because of its various advantages, including the exceptional profile, creative feelings, and dedication to user excitement. Subscribe all of us within this interesting initiative and help so you can profile the brand new way forward for internet casino playing.

Betting Web sites By Country

Fruit Party slot UK

Once you research their victory stories, you can observe some new releases, including, Fortune Angling or Golden Lord of your own Water. A few of their antique performs, Sizzling hot and Publication out of Ra were reissued a few minutes. Per tool Playtech brings comes with creative technology, including IMS, Bit, Playtech Open System and Playtech Webpage. He or she is serious about renewable practices — the producer features applications for all those, world and you will couples. If your image are worst, the customer will leave the game eventually, and will likely be operational seek a far greater solution.

  • Another significant part is short for the newest features of one’s desktop computer otherwise cellular software.
  • Using the sophisticated video streaming technology, they send practical alive dining tables with High definition stream and you can unbelievable camerawork you to catches all of the understated subtleties of game play.
  • Statistics show that by the 2029 there’ll be as much as 139.4 users of online gambling platforms worldwide.
  • Leveraging the 20+ several years of serving as one of the top services out of on line games app, its group requires a consumer-centric approach, consolidating trail-form technology and cryptocurrency.
  • Boost customer engagement, desire the new players, and you can increase cash from the partnering Orion Celebs’ game in the present collection.

Plunge to your our advertising now offers having #FreeMobileSweepstakes #MobileSweepstakesBonuses #MobileGamingPromotions and you may #EnterMobileSweepstakes to boost your chances of winning. Find out the ropes which have #HowToEnterMobileSweepstakes #MobileSweepstakesRules and you may talk about the fresh lavish honors with #MobileGamingPrizes. The program is just one of the #BestMobileSweepstakesSites providing #LongTailKeywords focused game such as #WinRealPrizesOnMobileSweepstakes #HowToWinMobileSweepstakesGames. Explore the realm of #MobileGambling #MobileCasinoGames #OnlineLotteryGames once we transcend the conventional playing sense. To be one of several leadership within this world, organizations must cooperate that have respected gambling establishment application company. The new decided to go with collaborator might be able to offering a safe, legitimate and you will authoritative casino system.

The system supports an over-all list of gambling establishment app game, giving supplier options to possess Vblink Fruit Party slot UK casino games, Juwa, Flames Kirin, and a lot more. Once we circulate then to the 2025, the newest integration out of virtual facts (VR) for the gambling on line platforms try poised so you can redefine the consumer experience. This particular technology is anticipated to elevate the web gaming sense to help you the newest heights, so it’s be as if professionals try in person present in a good gambling establishment otherwise sports club, even if he’s seated in the home. Boost customer engagement, desire the fresh players, and you may raise revenue from the integrating Orion Celebrities’ video game in the current profile.

Fruit Party slot UK

In addition, i gauge the quality of image, animated graphics, voice, and game play to guarantee a primary-classification playing feel. Play’n’Wade try a lengthy-based seller from movies harbors, desk game, video poker, bingo and you may abrasion cards. Video game and software solutions away from you to merchant features acquired several prestigious awards out of EGR, CEEG, WiG or other awards you to accept its perfection and advancement effort.

The invention team ensures unwavering help in different aspects of games, so we highlight rigid compliance one problems with all of our application tend to end up being taken care of lawfully. All of our technology solutions and you can globe education permit us to electricity advanced iGaming names around the world. Easily do, discharge, and you will do regional, cross-enterprise, and you may community competitions, agenda coming offers, and you may song efficiency for the Tournament Device — a robust investment to compliment user engagement. Customise tournaments in your case and you can leverage real-day statistics to possess analysis-driven choice-and then make, eventually increasing pro value and you may riding revenue gains. Jackpot App brings a premier-top quality and you may lawfully certified playing collection inside controlled international areas.

Those people sweepstakes distributor otherwise providers sell to smaller suppliers one to promote in order to private locations. We have no association with your areas and are not in charge for how it focus on its company. Once we facilitate the new shipment out of software, the role will not extend to your lead handling of personal places or their working practices.

Fruit Party slot UK

To your Turnkey Provider i deliver the software for you have a tendency to need start your gambling enterprise brand. Platon is a technology and you may device frontrunner which have a decade out of creating nimble technology enterprises from solution to performance to produce best software programs. Another important section stands for the fresh features of one’s desktop otherwise mobile application. If your app captures group’ interest, treks her or him as a result of a rotating processes, possesses quick routing, you might give it provides a associate excursion. Pay attention to the tasks they had, the way they taken care of them and you will whatever they got back effect.

The box can get include such as pros, or you may need to shell out a lot more charges with respect to the app development organization’s criteria. For example, Limeup is among the producers you to definitely zeroes in the to your brand’s label and provides customized tech products. Look at reviewer’s examination to guarantee the organization brings perhaps not simple but end-users-customized gambles. Workers away from gaming networks was happy to discover that on the internet currency games other sites element incorporated right back functions, multi-lingual alternatives, event-centered and you can secured minimal time taken between breakages. Because the establishment, Play’nGo might have been promoting online slots games, given social responsibility, collaborations, and you may designs as the key thinking.

The fresh gambling enterprise platform right back workplace also offers an entire review of the brand new user account. This consists of athlete gambling and you can class interest, obtained bonuses and you can user balance, along with mind exceptions and you will account limits. The rear place of work could also be used so you can thing private incentives in order to quickly award players, otherwise do harmony alterations in case there is unjust gamble. The gamer account administration and extends to pro places and you may lets for versatile player tagging. Customized while the a great standard software, the brand new Gambling establishment System allows for treating user profile, fee suppliers, online game suppliers, reporting and you will analytics. Subscribers receive an extensive gambling enterprise back workplace to administer its on the internet local casino brand name.

]]>