
##########################################################################
# $Id: postfix,v 1.32 2007/02/17 18:41:22 mrc Exp $
##########################################################################
# $Log: postfix,v $
# Revision 1.32  2007/02/17 18:41:22  mrc
#   - Ensure no output occurs when nothing is captured
#
# Revision 1.31  2007/02/16 06:18:45  mrc
#   - Make reject and warn_if_reject distinct sections
#   - Track Qids to properly report messages and bytes sent / accepted
#   - Track both messages deferred and total deferrals
#   - Place recipients and senders in their own keys, instead of combined
#   - Consider reject VRFY a reject and accumulate in reject totals
#   - Move header/body and 'MAIL from' reject code into main reject code block
#   - Remove unused variable
#   - Print row heading seperator lines only when appropriate
#   - Move printing of report headings into printReports
#   - Change Sent header to Sent via SMTP (orthogonal to Sent via LMTP)
#
# Revision 1.30  2007/02/09 22:26:39  mrc
#   - Added new postfix.conf configuration variable "postfix_Recipient_Delimiter",
#     which can be set to match the postfix variable "recipient_delimiter".
#     Allows Delivered and Sent reports to be grouped by email addresses
#     minus their address extension.
#   - Added new postfix.conf configuration variable "postfix_Max_Report_Width",
#     to control maximum report width in postfix.conf
#   - All line widths in report now obey max report width
#   - Added VFRY reject section
#   - Added RFC 3463 DSN code messages to bounce/deferred sections
#   - Created a SASL authenticated relayed messages which hits with
#     sasl_sender is present in smtpd messages
#   - Split orig_to email addresses into a subkey.  Allows reports
#     to be grouped by primary "to" address, instead of various aliases.
#   - Split Host & HostIP into their own keys for Bounce Remote section
#   - For Pix Workaround section, format Host & HostIP similar to others
#   - Add 'o' option where missing in REs
#   - Parse and canonicalize various "host...said" remote server messages for
#     few report sections
#   - Add Sent via LMTP section, removing lmtp-delivered mail out of Delivered.
#     Allows users with lmtp-based content filters to avoid double counting
#     (but only for message counts; byte counts are still about 50% too large).
#     Configuration variable postfix_MsgsSentLmtp controls display
#   - Added pre-queue content-filter overload section
#   - Reworked Bounce (local/remote) and Deferred sections
#   - Fixed several reversed captures of Host and HostIP
#   - Format Host and HostIP in Numeric hostname section
#   - Changed all From -> To lines to To <- From.  From address
#     is often bogus and To is more interesting to see.
#   - Made Reason the primary key in Deliverable/Undeliverable sendmail -bv tests
#   - Modified re_DSN RE to capture DSNs missing 3 number response code
#   - Enhanced SASL authenticated messages to capture missing log lines
#   - Added milter-reject section
#   - Updated 'Reject server configuration error' RE for older postfix versions
#   - Fix copy/paste error which caused use of incorrect variable in bad size limit
#   - Add Concurrency limit reached section
#   - Add "maildrop" to the list of relay's considered local in Bounce section
#   - Thanks: Jorey Bump, Mike Horwath,
#
# Revision 1.29  2007/01/27 20:21:46  mrc
# Rewrite by Mike Cappella (MrC)
#   - Provide more useful information and summaries
#   - Provide increasing detail as requested via --detail
#   - Provide ability to configure per section maximum detail
#   - Optimize and combine the numerous REs
#   - Capture and summarize many more log lines
#   - Pin important errors to top of report
#   - Sort by hits, IP, and lexically
#   - Handle IPv6 addresses
#   - Generalize log line capturing and reporting
#   - Eliminate excessive copy/paste reporting code
#   - Requires updated postfix.conf file
#   - Thanks: Eray Aslan, Jorey Bump, Harald Geiger, Bill Hudacek,
#     Frederic Jacquet, Geert Janssens, Leon Kolchinsky, Rob Myroon
#
# Revision 1.28  2006/12/15 06:24:49  bjorn
# Filtering "sender non-delivery notification", by Ivana Varekova.
#
# Revision 1.27  2006/12/15 05:00:41  bjorn
# Filter all held message logs, by Hugo van der Kooij.
#
# Revision 1.26  2006/10/20 16:51:50  bjorn
# Additional matching of sasl messages, by Willi Mann.
#
# Revision 1.25  2006/08/13 21:25:55  bjorn
# Updates to work with the Postfix 2.3.x series (due to log format changes),
# by Mike Cappella.
#
# Revision 1.24  2006/03/22 17:43:46  bjorn
# Changes by Harald Geiger:
# - ignore additional statistics: messages (Postfix 2.2)
# - replaced several 5xx Codes by [0-9]+
#   (main reason is to make them match on 4xx if in soft_bounce=yes mode)
# - a more generic "Client host rejected" reporting
# - changed "Messages rejected:" to "Messages rejected from sender:"
#
# Revision 1.23  2005/12/19 15:47:47  bjorn
# Updates from Mike Cappella:
#   - Catches some of the Unknown Users messages from newer versions of postfix
#   - Consolidates a couple of REs
#   - Adds a cumulative total to each of the Unknown users and Header content rejection headers
#   - Adds a Body content rejection section
#
# Revision 1.22  2005/11/22 18:30:47  bjorn
# Detecting 'virtual alias table', by Kevin Old.
#
# Revision 1.21  2005/08/23 23:54:38  mike
# Fixed typo propably from Roland Hermans -mgt
#
# Revision 1.20  2005/07/25 22:26:28  bjorn
# Added "Sender address" to "554 Service unavailable" regexp, by Who Knows
#
# Revision 1.19  2005/04/22 13:48:28  bjorn
# This patch catches (un)deliverable messages and many more, which were
# missing until now on mu new postfix-2.1.*, from Paweł Gołaszewski
#
# Revision 1.18  2005/04/17 23:12:28  bjorn
# Patches from Peter Bieringer and Willi Mann: ignoring more lines and
# some blank spaces
#
# Revision 1.17  2005/02/24 17:08:05  kirk
# Applying consolidated patches from Mike Tremaine
#
# Revision 1.7  2005/02/16 00:43:28  mgt
# Added #vi tag to everything, updated ignore.conf with comments, added emerge and netopia to the tree from Laurent -mgt
#
# Revision 1.6  2005/02/13 23:50:42  mgt
# Tons of patches from Pawel and PLD Linux folks...Thanks! -mgt
#
# Revision 1.5  2004/10/06 21:42:53  mgt
# patches from Pawel quien-sabe -mgt
#
# Revision 1.4  2004/07/29 19:33:29  mgt
# Chmod and removed perl call -mgt
#
# Revision 1.3  2004/07/10 01:54:35  mgt
# sync with kirk -mgt
#
# Revision 1.13  2004/06/23 15:01:17  kirk
# - Added more patches from blues@ds.pg.gda.pl
#
# Revision 1.12  2004/06/21 14:59:05  kirk
# Added tons of patches from Pawe? Go?aszewski" <blues@ds.pg.gda.pl>
#
# Thanks, as always!
#
# Revision 1.11  2004/06/21 13:42:02  kirk
# From: Matthew Wise <matt@oatsystems.com>
# This is more of a suggestion than a true patch submission. On a busy
# postfix server the messages sent by section is really long and not
# helpful. This patch finds and lists the top 10 senders by bumber of
# messages.
#
# Revision 1.10  2004/06/21 13:41:04  kirk
# Patch from rod@nayfield.com
#
# Revision 1.9.1 2004/02/22 16:44:01 rod
# Added patch from rod@nayfield.com
#
# Revision 1.9  2004/02/03 03:25:02  kirk
# Added patch from quien-sabe@metaorg.com
#
# Revision 1.8  2004/02/03 02:45:26  kirk
# Tons of patches, and new 'oidentd' and 'shaperd' filters from
# Pawe? Go?aszewski" <blues@ds.pg.gda.pl>
#
# Revision 1.7  2003/12/15 18:35:03  kirk
# Tons of patches from blues@ds.pg.gda.pl
#
# Revision 1.6  2003/12/15 18:09:23  kirk
# Added standard vi formatting commands at the bottom of all files.
# Applied many patches from blues@ds.pg.gda.pl
#
# Revision 1.5  2003/12/15 17:45:09  kirk
# Added clamAV update log filter from lars@spinn.dk
#
# Revision 1.4  2003/11/26 14:36:30  kirk
# Applied patch from blues@ds.pg.gda.pl
#
# Revision 1.3  2003/11/18 14:04:05  kirk
# More patches from blues@ds.pg.gda.pl
#
# Revision 1.2  2003/11/18 04:02:21  kirk
# Patch from blues@ds.pg.gda.pl
#
# Revision 1.1  2003/11/03 04:49:18  kirk
# Added postfix filter from Sven Conrad <sconrad@receptec.net>
#
# Revision 1.1  2002/03/29 15:32:14  kirk
# Added some filters found in RH's release
#
# Revision ???  2000/07/12 Simon Liddington <sjl@zepler.org>
# converted from sendmail to postfix Sven Conrad <scon@gmx.net>
# added unknown users, relay denials
#
# Revision 1.1  2003/03/21 21:10  sven
# Initial revision
#
# filters all postfix/<process> messages
#
##########################################################################

########################################################
# Major rewrite by:
#    Mike "MrC" Cappella <lists-logwatch@cappella.us>
#
# Please send all comments, suggestions, bug reports to the logwatch
# mailing list (logwatch@logwatch.org), or to the email address above.
# I will respond as timely as possible. [MrC]
#
# This file was originally written by:
#    Kenneth Porter
#
########################################################
#
# Test data included via inline comments starting with "#TD" and optionally
# followed by an integer indicating replication count.
#
# Generate test data via the command:
#
#    perl -e  'while (<>) { print "$2\n" x ($1 ? $1:1)    if /^\s*#TD(\d+)? (.*)$/}' postfix | sed "s#^#`date +"%b %d %H:%M:%S"` `hostname` postfix/smtp[12345]: #"
#

use warnings;
no warnings "uninitialized";
use strict;

my $Detail = $ENV{'LOGWATCH_DETAIL_LEVEL'} || 0;
my $Debug = $ENV{'LOGWATCH_DEBUG'} || 0;

my $debug = 0;
my (%Totals, %Counts, %MaxLevel, %UnmatchedList, %DeferredByQid, %Qids);
my ($Recipient_Delimiter);

use vars qw (
   $ThisLine $ThisLine2 $ThisLine3 $ThisLine4 $Reason $Host $HostIP $From $Source
   $Warning $Queue $QID $Process $Status $Domain $To $Reason2 $Header
   $Message $Type $Site $Helo $Command $Addr $Service $Map $Key $When
   $Size $Recip $Remainder $Table $Bytes $NumRecips $OrigTo $DDD
   $Relay $HorB $Rejected $Trigger $Filter $Count $Cert $RejectAction
);

sub get_env_vals;
sub buildTree(\% $ $);
sub printTree($);
sub formathost($ $);
sub commify($);
sub printReports ($ \@);
sub cleanhostreply($ $ $ $);


# Notes:
#
#   In REs, sender addresses may be empty "<>" - capture using *, not + ( eg. from=<[^>]*> )
#   IN REs, always use /o option at end of RE when RE uses interpolated vars

# IPv4 only
#my $re_IP      = '(?:\d{1,3}\.){3}(?:\d{1,3})';
# IPv4 and IPv6 - not well tested
my $re_IP      = '(?:(?:::(?:ffff:|FFFF:)?)?(?:\d{1,3}\.){3}\d{1,3}|(?:[\da-fA-F]{0,4}:){2}(?:[\da-fA-F]{0,4}:){0,5}[\da-fA-F]{0,4})';
my $re_DSN     = '(?:(?:\d{3})?(?: ?\d\.\d\.\d)?)';
my $re_QID     = '[A-Z\d]+';
my $re_DDD     = '(?:(?:conn_use=\d+ )?delay=-?[\d.]+(?:, delays=[\d\/.]+)?(?:, dsn=[\d.]+)?)';

# Maximum report width.
# Configurable via the postfix.conf variable postfix_Max_Report_Width
my $Max_Report_Width = 100;

my $ipaddrwidth = 15;
#my $ipaddrwidth = 40;     # for formathost w/IPv6 addresses

# References to these are used in the Formats table below; we'll predeclare them.
$Totals{'TotalRejects'} = 0;
$Totals{'TotalRejectWarns'} = 0;
$Totals{'TotalAcceptPlusReject'} = 0;

#
# The Formats table drives reports.  For each entry in the table, a summary line and/or
# detailed report section is a candidate for output, depending upon logwatch Detail
# level, and .conf configuration variables.  Each entry below has four fields:
#
#   1: Key to %Counts and %Totals accumulator hashes
#   2: Numeric output format specifier
#   3: Summary and Section Title
#   4: A hash to a divisor used to calculate the percentage of a total for that key
#
# Alternatively, when field 1 contains a single character, this character will
# cause a line filled with that character to be output, but only if there was
# output for that section.
# The special name '__SECTION' is used to indicate the beginning of a new section.
# This ensures the printReports routine does not print needless horizontal lines.
#
my @Formats = (
   # Place configuration and critical errors appear first

   [ '__SECTION' ],
   [ 'PanicError',                  "d", "*Panic:   General panic" ],
   [ 'FatalFileTooBig',             "d", "*Fatal:   Message file too big" ],
   [ 'FatalConfigError',            "d", "*Fatal:   Configuration error" ],
   [ 'FatalErrors',                 "d", "*Fatal:   General fatal" ],
   [ 'WarnFileTooBig',              "d", "*Warning: Queue file size limit exceeded" ],
   [ 'WarnInsufficientSpace',       "d", "*Warning: Insufficient system storage error" ],
   [ 'WarnConfigError',             "d", "*Warning: Server configuration error" ],
   [ 'QueueWriteError',             "d", "*Warning: Error writing queue file" ],
   [ 'MessageWriteError',           "d", "*Warning: Error writing message file" ],
   [ 'DatabaseGeneration',          "d", "*Warning: Database file needs update" ],
   [ 'MailerLoop',                  "d", "*Warning: Mailer loop" ],
   [ 'StartupError',                "d", "*Warning: Startup error" ],
   [ 'MapProblem',                  "d", "*Warning: Map lookup problem" ],
   [ 'PrematureEOI',                "d", "*Warning: Premature end of input" ],
   [ 'ConcurrencyLimit',            "d", "*Warning: Connection concurrency limit reached" ],
   [ 'ConnectionLostOverload',      "d", "*Warning: Pre-queue content-filter connection overload" ],
   [ 'ProcessExit',                 "d", "Process exited" ],
   [ 'Hold',                        "d", "Placed on hold" ],
   [ 'CommunicationError',          "d", "Postfix communications error" ],
   [ 'SaslAuthFail',                "d", "SASL authentication failed" ],
   [ 'LdapError',                   "d", "LDAP error" ],
   [ 'WarningsOther',               "d", "Miscellaneous warnings" ],
   [ 'TotalRejectWarns',            "d", "Reject warnings (warn_if_reject)" ],
   [ '\n' ],

   [ '__SECTION' ],
   [ 'BytesAccepted',               "Z", "Bytes accepted " ],          # Z means print scaled as in 1k, 1m, etc.
   [ 'BytesDelivered',              "Z", "Bytes delivered" ],
   [ '='  ],
   [ '\n' ],

   [ '__SECTION' ],
   [ 'MsgsAccepted',                "d", "Accepted",                          \$Totals{'TotalAcceptPlusReject'} ],
   [ 'TotalRejects',                "d", "Rejected",                          \$Totals{'TotalAcceptPlusReject'} ],
   [ '-',                           "",  "",                                  \$Totals{'TotalAcceptPlusReject'} ],
   [ 'TotalAcceptPlusReject',       "d", "Total",                             \$Totals{'TotalAcceptPlusReject'} ],
   [ '=', ],
   [ '\n' ],

   [ '__SECTION' ],
   [ 'RejectRelay',                 "d", "Reject relay denied",               \$Totals{'TotalRejects'} ],
   [ 'RejectHelo',                  "d", "Reject HELO/EHLO",                  \$Totals{'TotalRejects'} ],
   [ 'RejectUnknownUser',           "d", "Reject unknown user",               \$Totals{'TotalRejects'} ],
   [ 'RejectRecip',                 "d", "Reject recipient address",          \$Totals{'TotalRejects'} ],
   [ 'RejectSender',                "d", "Reject sender address",             \$Totals{'TotalRejects'} ],
   [ 'RejectClient',                "d", "Reject client host",                \$Totals{'TotalRejects'} ],
   [ 'RejectUnknownClient',         "d", "Reject unknown client host",        \$Totals{'TotalRejects'} ],
   [ 'RejectRBL',                   "d", "Reject RBL",                        \$Totals{'TotalRejects'} ],
   [ 'RejectHeader',                "d", "Reject header",                     \$Totals{'TotalRejects'} ],
   [ 'RejectBody',                  "d", "Reject body",                       \$Totals{'TotalRejects'} ],
   [ 'RejectSize',                  "d", "Reject message size",               \$Totals{'TotalRejects'} ],
   [ 'RejectMilter',                "d", "Reject milter",                     \$Totals{'TotalRejects'} ],
   [ 'RejectInsufficientSpace',     "d", "Reject insufficient space",         \$Totals{'TotalRejects'} ],
   [ 'RejectConfigError',           "d", "Reject server configuration error", \$Totals{'TotalRejects'} ],
   [ 'RejectVerify',                "d", "Reject VRFY",                       \$Totals{'TotalRejects'} ],
   [ '-', ],
   [ 'TotalRejects',                "d", "Total Rejects",                     \$Totals{'TotalRejects'} ],
   [ '=', ],
   [ '\n' ],

   [ '__SECTION' ],
   [ 'RejectWarnRelay',             "d", "Reject warning relay denied", ],
   [ 'RejectWarnHelo',              "d", "Reject warning HELO/EHLO" ],
   [ 'RejectWarnUnknownUser',       "d", "Reject warning unknown user" ],
   [ 'RejectWarnRecip',             "d", "Reject warning recipient address" ],
   [ 'RejectWarnSender',            "d", "Reject warning sender address" ],
   [ 'RejectWarnClient',            "d", "Reject warning client host" ],
   [ 'RejectWarnUnknownClient',     "d", "Reject warning unknown client host" ],
   [ 'RejectWarnRBL',               "d", "Reject warning via RBL" ],
   [ 'RejectWarnInsufficientSpace', "d", "Reject warning insufficient space" ],
   [ 'RejectWarnConfigError',       "d", "Reject warning server configuration error" ],
   [ 'RejectWarnVerify',            "d", "Reject warning VRFY" ],
   [ '-', ],
   [ 'TotalRejectWarns',            "d", "Total Reject Warnings" ],
   [ '=', ],
   [ '\n' ],

   [ '__SECTION' ],
   [ 'ConnectionInbound',           "d", "Connections made" ],
   [ 'ConnectionLost',              "d", "Connections lost" ],
   [ 'Disconnection',               "d", "Disconnections" ],
   [ 'RemovedFromQueue',            "d", "Removed from queue" ],
   [ 'MsgsDelivered',               "d", "Delivered" ],
   [ 'MsgsSent',                    "d", "Sent via SMTP" ],
   [ 'MsgsSentLmtp',                "d", "Sent via LMTP" ],
   [ 'MsgsForwarded',               "d", "Forwarded" ],
   [ 'MsgsResent',                  "d", "Resent" ],
   [ 'MsgsDeferred',                "d", "Deferred" ],
   [ 'Deferrals',                   "d", "Deferrals" ],
   [ 'BounceLocal',                 "d", "Bounce (local)" ],
   [ 'BounceRemote',                "d", "Bounce (remote)" ],
   [ 'Filtered',                    "d", "Filtered" ],
   [ 'Discarded',                   "d", "Discarded" ],
   [ 'Requeued',                    "d", "Requeued messages" ],
   [ 'ReturnedToSender',            "d", "Expired and returned to sender" ],
   [ 'SenderDelayNotification',     "d", "Sender delay notification" ],
   [ 'DSNDelivered',                "d", "DSNs delivered" ],
   [ 'DSNUndelivered',              "d", "DSNs undeliverable" ],
   [ '\n' ],

   [ '__SECTION' ],
   [ 'ConnectToFailure',            "d", "Connection failure (outbound)" ],
   [ 'TimeoutInbound',              "d", "Timeout (inbound)" ],
   [ 'HeloError',                   "d", "HELO/EHLO conversations errors" ],
   [ 'IllegalAddrSyntax',           "d", "Illegal address syntax in SMTP command" ],
   [ 'WarningHeader',               "d", "Header warning" ],
   [ 'ReleasedFromHold',            "d", "Released from hold" ],
   [ 'RBLError',                    "d", "RBL lookup error" ],
   [ 'MxError',                     "d", "MX error" ],
   [ 'NumericHostname',             "d", "Numeric hostname" ],
   [ 'SmtpConversationError',       "d", "SMTP commands dialog error" ],
   [ 'TooManyErrors',               "d", "Excessive errors in SMTP commands dialog" ],
   [ 'HostnameVerification',        "d", "Hostname verification errors" ],
   [ 'HostnameValidationError',     "d", "Hostname validation error" ],
   [ 'Deliverable',                 "d", "Address is deliverable (sendmail -bv)" ],
   [ 'Undeliverable',               "d", "Address is undeliverable (sendmail -bv)" ],
   [ 'TableChanged',                "d", "Restarts due to lookup table change" ],
   [ 'PixWorkaround',               "d", "Enabled PIX <CRLF>.<CRLF> workaround" ],
   [ 'TlsConnectFrom',              "d", "TLS connections (inbound)" ],
   [ 'TlsConnectTo',                "d", "TLS connections (outbound)" ],
   [ 'SaslAuth',                    "d", "SASL authenticated messages" ],
   [ 'SaslAuthRelay',               "d", "SASL authenticated relayed messages" ],
   [ 'TlsUnverified',               "d", "TLS certificate unverified" ],
);

# Get the postfix.conf settings passed via environment variables 
get_env_vals;

#-------------------------------------------------
# RFC 3463 DSN Codes
# http://www.faqs.org/rfcs/rfc3463.html
#
# Class.Subject.Detail
#
# Class
my %dsn_codes = (
    class => {
	"2" => "Success",
	"4" => "Persistent Transient Failure",
	"5" => "Permanent Failure",
    },
    
    subject => {
	"0" => "Other or Undefined Status",
	"1" => "Addressing Status",
	"2" => "Mailbox Status",
	"3" => "Mail System Status",
	"4" => "Network & Routing Status",
	"5" => "Mail Delivery Protocol Status",
	"6" => "Message Content or Media Status",
	"7" => "Security or Policy Status",
    },

    detail => {
	"0.0" => "Other undefined status",
	"1.0" => "Other address status",
	"1.1" => "Bad destination mailbox address",
	"1.2" => "Bad destination system address",
	"1.3" => "Bad destination mailbox address syntax",
	"1.4" => "Destination mailbox address ambiguous",
	"1.5" => "Destination mailbox address valid",
	"1.6" => "Mailbox has moved",
	"1.7" => "Bad sender's mailbox address syntax",
	"1.8" => "Bad sender's system address",

	"2.0" => "Other or undefined mailbox status",
	"2.1" => "Mailbox disabled, not accepting messages",
	"2.2" => "Mailbox full",
	"2.3" => "Message length exceeds administrative limit.",
	"2.4" => "Mailing list expansion problem",

	"3.0" => "Other or undefined mail system status",
	"3.1" => "Mail system full",
	"3.2" => "System not accepting network messages",
	"3.3" => "System not capable of selected features",
	"3.4" => "Message too big for system",

	"4.0" => "Other or undefined network or routing status",
	"4.1" => "No answer from host",
	"4.2" => "Bad connection",
	"4.3" => "Routing server failure",
	"4.4" => "Unable to route",
	"4.5" => "Network congestion",
	"4.6" => "Routing loop detected",
	"4.7" => "Delivery time expired",

	"5.0" => "Other or undefined protocol status",
	"5.1" => "Invalid command",
	"5.2" => "Syntax error",
	"5.3" => "Too many recipients",
	"5.4" => "Invalid command arguments",
	"5.5" => "Wrong protocol version",

	"6.0" => "Other or undefined media error",
	"6.1" => "Media not supported",
	"6.2" => "Conversion required & prohibited",
	"6.3" => "Conversion required but not supported",
	"6.4" => "Conversion with loss performed",
	"6.5" => "Conversion failed",

	"7.0" => "Other or undefined security status",
	"7.1" => "Delivery not authorized, message refused",
	"7.2" => "Mailing list expansion prohibited",
	"7.3" => "Security conversion required but not possible",
	"7.4" => "Security features not supported",
	"7.5" => "Cryptographic failure",
	"7.6" => "Cryptographic algorithm not supported",
	"7.7" => "Message integrity failure",
    },
);


# Main processing loop
#
while (defined($ThisLine = <STDIN>)) {

   my ($PostfixService);
   #print "ThisLine: \"$ThisLine\"\n";

   ($PostfixService, $ThisLine) = ( $ThisLine =~ /^... .. ..:..:.. [^ ]* postfix\/([^[]+)\[\d+\]: (.*)$/ );
   $ThisLine =~ s/\s+$//;

   # We don't care about these, but see also less frequent log entries at the of the while loop
   next if ( 
         ( $ThisLine =~ /^Deleted: \d message$/ )
      or ( $ThisLine =~ /: replace: header / )
      or ( $ThisLine =~ /: Greylisted for / )                           # Greylisting has it's own statistics tool
      #XXX Perhaps the following are candidates for extended statistics
      or ( $ThisLine =~ /certificate verification failed for/o )     
      or ( $ThisLine =~ /Server certificate could not be verified/o )
      or ( $ThisLine =~ /certificate peer name verification failed/o )
   );

   # common log entries up front
   if ($ThisLine =~ /^connect from/) {
      #TD25 connect from sample.net[10.0.0.1]
      #TD connect from mail.example.com[2001:dead:beef::1]
      #TD connect from localhost.localdomain[127.0.0.1]
      $Totals{'ConnectionInbound'}++;
   }
   elsif ($ThisLine =~ /^disconnect from/) {
      #TD25 disconnect from sample.net[10.0.0.1]
      #TD disconnect from mail.example.com[2001:dead:beef::1]
      $Totals{'Disconnection'}++;
   }
   elsif (my ($Host,$HostIP,$Reason) = ($ThisLine =~ /^connect to ([^[]*)\[($re_IP)\]: (.*)$/o)) {
      # all "connect to" messages indicate a problem with the connection
      #TD connect to example.org[10.0.0.1]: Connection refused (port 25)
      #TD connect to mail.sample.com[10.0.0.1]: No route to host (port 25)
      #TD connect to sample.net[192.168.0.1]: read timeout (port 25)
      #TD connect to mail.example.com[10.0.0.1]: server dropped connection without sending the initial SMTP greeting (port 25)
      #TD connect to mail.example.com[192.168.0.1]: server dropped connection without sending the initial SMTP greeting (port 25)
      #TD connect to ipv6-1.example.com[2001:dead:beef::1]: Connection refused (port 25)
      #TD connect to ipv6-2.example.com[FEDC:BA98:7654:3210:FEDC:BA98:7654:3210]: Connection refused (port 25)
      #TD connect to ipv6-3.example.com[1080:0:0:0:8:800:200C:4171]: Connection refused (port 25)
      #TD connect to ipv6-4.example.com[3ffe:2a00:100:7031::1]: Connection refused (port 25)
      #TD connect to ipv6-5.example.com[1080::8:800:200C:417A]: Connection refused (port 25)
      #TD connect to ipv6-6.example.com[::192.9.5.5]: Connection refused (port 25)
      #TD connect to ipv6-7.example.com[::FFFF:129.144.52.38]: Connection refused (port 25)
      #TD connect to ipv6-8.example.com[2010:836B:4179::836B:4179]: Connection refused (port 25)
      $Totals{'ConnectToFailure'}++;
      $Counts{'ConnectToFailure'}{$Reason}{formathost($HostIP,$Host)}++;
   }

   # ^fatal: ...
   elsif ( ($Reason) = ($ThisLine =~ /^fatal: (.*)$/)) {

      if ($Reason =~ /^[^ ]*\(\d+\): Message file too big$/) {
         #TD fatal: root(0): Message file too big
         $Totals{'FatalFileTooBig'}++;

      # XXX its not clear this is at all useful - consider falling through to last case
      } elsif ( $Reason =~ /^config variable ([^ ]*): (.*)$/ ) {
         #TD fatal: config variable inet_interfaces: host not found: 10.0.0.1:2525
         #TD fatal: config variable inet_interfaces: host not found: all:2525
         $Totals{'FatalConfigError'}++;
         $Counts{'FatalConfigError'}{$Reason}++;
      }
      else {
         #TD fatal: watchdog timeout
         #TD fatal: bad boolean configuration: smtpd_use_tls =
         $Totals{'FatalError'}++;
         $Counts{'FatalError'}{"\u$Reason"}++;
      }
   }
   elsif ( ($Reason) = ($ThisLine =~ /^panic: (.*)$/)) {
         #TD panic: myfree: corrupt or unallocated memory block
         $Totals{'PanicError'}++;
         $Counts{'PanicError'}{"\u$Reason"}++;
   }

   # ^warning: ...
   elsif (($Warning) = ($ThisLine =~ /^warning: (.*)$/ )) {
      # Skip these
      next if ( $Warning =~ /$re_QID: skipping further client input$/o  );
      next if ( $Warning =~ /^Mail system is down -- accessing queue directly$/ );
      next if ( $Warning =~ /^SASL authentication failure: (?:Password verification failed|no secret in database)$/ );
      next if ( $Warning =~ /^no MX host for .* has a valid A record$/ );
      next if ( $Warning =~ /^uid=\d: Broken pipe$/ );

     #TD warning: connect to 127.0.0.1:12525: Connection refused
     #TD warning: problem talking to server 127.0.0.1:12525: Connection refused
     #TD warning: valid_ipv4_hostaddr: invalid octet count:


      if ( ($HostIP,$Host,$Reason) = ($Warning =~ /^(?:smtpd_peer_init: )?($re_IP): hostname ([^ ]+) verification failed: (.*)$/o ) or
           ($HostIP,$Reason,$Host) = ($Warning =~ /^(?:smtpd_peer_init: )?($re_IP): (address not listed for hostname) (.*)$/o )) {
         #TD warning: 10.0.0.1: hostname sample.com verification failed: Host not found 
         #TD warning: smtpd_peer_init: 192.168.0.1: hostname example.com verification failed: Name or service not known 
         #TD warning: 192.168.0.1: address not listed for hostname sample.net
         $Totals{'HostnameVerification'}++;
         $Counts{'HostnameVerification'}{"\u$Reason"}{formathost($HostIP, $Host)}++;

      } elsif ( ($Warning =~ /^$re_QID: queue file size limit exceeded$/o ) or
                ($Warning =~ /^uid=\d+: File too large$/)) {
         $Totals{'WarnFileTooBig'}++;

      } elsif ( ($Source) = ($Warning =~ /^database (?:[^ ]*) is older than source file ([\w\/]+)$/)) {
         #TD warning: database /etc/postfix/client_checks.db is older than source file /etc/postfix/client_checks 
         $Totals{'DatabaseGeneration'}++;
         $Counts{'DatabaseGeneration'}{$Source}++;

      } elsif ( ($Reason) = ($Warning =~ /^$re_QID: write queue file: (.*)$/o )) {
         #TD warning: E669DE52: write queue file: No such file or directory
         $Totals{'QueueWriteError'}++;
         $Counts{'QueueWriteError'}{$Reason}++;

      } elsif ( ($Reason) = ($Warning =~ /^open active $re_QID: (.*)$/o )) {
         #TD warning: open active BDB9B1309F7: No such file or directory
         $Totals{'QueueWriteError'}++;
         $Counts{'QueueWriteError'}{"open active: $Reason"}++;

      } elsif ( ($Reason) = ($Warning =~ /^qmgr_active_corrupt: save corrupt file queue active id $re_QID: (.*)$/o )) {
         #TD warning: qmgr_active_corrupt: save corrupt file queue active id 4F4272F342: No such file or directory
         $Totals{'QueueWriteError'}++;
         $Counts{'QueueWriteError'}{"active corrupt: $Reason"}++;

      } elsif ( ($Reason) = ($Warning =~ /^qmgr_active_done_3_generic: remove $re_QID from active: (.*)$/o )) {
         #TD warning: qmgr_active_done_3_generic: remove AF0F223FC05 from active: No such file or directory 
         $Totals{'QueueWriteError'}++;
         $Counts{'QueueWriteError'}{"remove active: $Reason"}++;

      } elsif ( ($Queue,$QID) = ($Warning =~ /^([^\/]*)\/($re_QID): Error writing message file$/o )) {
         #TD warning: maildrop/C9E66ADF: Error writing message file 
         $Totals{'MessageWriteError'}++;
         $Counts{'MessageWriteError'}{$Queue}{$QID}++;

      } elsif ( ($Process,$Status) = ($Warning =~ /^process ([^ ]*) pid \d+ exit status (\d+)$/)) {
         #TD warning: process /usr/lib/postfix/smtp pid 9724 exit status 1
         $Totals{'ProcessExit'}++;
         $Counts{'ProcessExit'}{"$Process: exit status $Status"}++;

      } elsif ( ($Reason) = ($Warning =~ /^mailer loop: (.*)$/)) {
         #TD warning: mailer loop: best MX host for example.com is local
         $Totals{'MailerLoop'}++;
         $Counts{'MailerLoop'}{$Reason}++;

      } elsif ( ($Reason,$Domain) = ($Warning =~ /^(malformed domain name in resource data of MX record) for (.*):$/)) {
         #TD warning: malformed domain name in resource data of MX record for mail.example.com:
         $Totals{'MxError'}++;
         $Counts{'MxError'}{"\u$Reason"}{$Domain}{""}++;

      } elsif ( ($Reason,$Host,$Reason2) = ($Warning =~ /^(Unable to look up MX host) for ([^:]*): (.*)$/)) {
         #TD warning: Unable to look up MX host for example.com: Host not found
         $Totals{'MxError'}++;
         $Counts{'MxError'}{"\u$Reason: $Reason2"}{$Host}{""}++;

      } elsif ( ($Reason,$Host,$To,$Reason2) = ($Warning =~ /^(Unable to look up MX host) (.*) for Sender address ([^:]*): (.*)$/)) {
         #TD warning: Unable to look up MX host mail.example.com for Sender address from@example.com: hostname nor servname provided, or not known
         my ($name, $domain) = split ('@', "\L$To");
         $Totals{'MxError'}++;
         $Counts{'MxError'}{"\u$Reason: $Reason2"}{$Host}{$name}++;

      } elsif ( ($Host,$HostIP,$Header) = ($Warning =~ /^([^[]+)\[($re_IP)\] sent \w+ header instead of SMTP command: (.*)$/o )  or
                ($Host,$HostIP,$Header) = ($Warning =~ /^non-SMTP command from ([^[]+)\[($re_IP)\]: (.*)$/o )) {
         # ancient
         #TD warning: example.com[192.168.0.1] sent message header instead of SMTP command: From: "Someone" <40245426501example.com>
         # current
         #TD warning: non-SMTP command from sample.net[10.0.0.1]: Received: from 192.168.0.1 (HELO bogus.sample.com)

         $Totals{'SmtpConversationError'}++;
         $Counts{'SmtpConversationError'}{formathost($HostIP,$Host)}{$Header}++;

      } elsif ( ($Message) = ($Warning =~ /^valid_hostname: (.*)$/)) {
         #TD warning: valid_hostname: empty hostname 
         $Totals{'HostnameValidationError'}++;
         $Counts{'HostnameValidationError'}{$Message}++;

      } elsif ( ($Host,$HostIP,$Type) = ($Warning =~ /^([^[]+)\[($re_IP)\]: SASL (.*) authentication failed/o )) {
         #TD warning: example.com[192.168.0.1]: SASL DIGEST-MD5 authentication failed 
         $Totals{'SaslAuthFail'}++;
         $Counts{'SaslAuthFail'}{formathost($HostIP,$Host)}++;

      } elsif ( ($Host,$Site,$Reason) = ($Warning =~ /^([^:]*): RBL lookup error:.* Name service error for (?:name=)?$re_IP\.([^:]*): (.*)$/o )) {
         #TD warning: 192.168.0.1.sbl.spamhaus.org: RBL lookup error: Host or domain name not found. Name service error for name=192.168.0.1.sbl.spamhaus.org type=A: Host not found, try again

         #TD warning: 10.0.0.1.relays.osirusoft.com: RBL lookup error: Name service error for 10.0.0.1.relays.osirusoft.com: Host not found, try again 
         $Totals{'RBLError'}++;
         $Counts{'RBLError'}{$Site}{$Reason}{$Host}++;

      } elsif (
            ($Host,$HostIP,$Reason,$Helo) = ($Warning =~ /^host ([^[]+)\[($re_IP)\] (greeted me with my own hostname) ([^ ]*)$/o ) or
            ($Host,$HostIP,$Reason,$Helo) = ($Warning =~ /^host ([^[]+)\[($re_IP)\] (replied to HELO\/EHLO with my own hostname) ([^ ]*)$/o )) {
         #TD warning: host example.com[192.168.0.1] greeted me with my own hostname example.com 
         #TD warning: host example.com[192.168.0.1] replied to HELO/EHLO with my own hostname example.com
         $Totals{'HeloError'}++;
         $Counts{'HeloError'}{"\u$Reason"}{formathost($HostIP,$Host)}++;

      } elsif ( ($Host,$HostIP,$Command,$Addr) = ($Warning =~ /^Illegal address syntax from ([^[]+)\[($re_IP)\] in ([^ ]*) command: (.*)/o )) {
         #TD warning: Illegal address syntax from example.com[192.168.0.1] in MAIL command: user@sample.net
         $Addr =~ s/[<>]//g;
         $Totals{'IllegalAddrSyntax'}++;
         $Counts{'IllegalAddrSyntax'}{$Command}{$Addr}{formathost($HostIP,$Host)}++;

      } elsif ( ($Reason, $Host) = ($Warning =~ /^numeric (hostname): ($re_IP)$/o ) or
                ($Reason, $Host) = ($Warning =~ /^numeric domain name in (resource data of MX record) for (.*)$/ )) {
         #TD warning: numeric hostname: 192.168.0.1
         #TD warning: numeric domain name in resource data of MX record for sample.com: 192.168.0.1

         if (($Host,$HostIP) = ($Host =~ /([^:]+): ($re_IP)/o)) {
            $Host = formathost($HostIP, $Host);
         }
         $Totals{'NumericHostname'}++;
         $Counts{'NumericHostname'}{"\u$Reason"}{$Host}++;

      } elsif (($Service,$Reason) = ($Warning =~ /^(.*): (bad command startup -- throttling)/o )) {
         #TD warning: /usr/libexec/postfix/trivial-rewrite: bad command startup -- throttling
         $Totals{'StartupError'}++;
         $Counts{'StartupError'}{"Service: $Service"}{$Reason}++;

      } elsif (($Service,$Reason) = ($Warning =~ /(problem talking to service [^:]*): (.*)$/o )) {
         #TD warning: problem talking to service rewrite: Connection reset by peer
         #TD warning: problem talking to service rewrite: Success
         $Totals{'CommunicationError'}++;
         $Counts{'CommunicationError'}{"\u$Service"}{$Reason}++;

      } elsif (($Map,$Key) = ($Warning =~ /^$re_QID: ([^ ]*) map lookup problem for (.*)$/o )) {
         #TD warning: 6F74F74431: virtual_alias_maps map lookup problem for root@example.com
         $Totals{'MapProblem'}++;
         $Counts{'MapProblem'}{"$Map"}{$Key}++;

      } elsif (($Map,$Reason) = ($Warning =~ /pcre map ([^,]+), (.*)$/ )) {
         #TD warning: pcre map /etc/postfix/body_checks, line 92: unknown regexp option "F": skipping this rule
         $Totals{'MapProblem'}++;
         $Counts{'MapProblem'}{$Map}{$Reason}++;

      } elsif (($Reason) = ($Warning =~ /dict_ldap_lookup: (.*)$/ )) {
         #TD warning: dict_ldap_lookup: Search error 80: Internal (implementation specific) error
         $Totals{'LdapError'}++;
         $Counts{'LdapError'}{$Reason}++;

      } elsif (($Service,$When) = ($Warning =~ /premature end-of-input on ([^ ]+) (.*)$/ )) {
         #TD warning: premature end-of-input on private/anvil while reading input attribute name
         $Totals{'PrematureEOI'}++;
         $Counts{'PrematureEOI'}{$Service}{$When}++;

      } elsif ( ($Size,$Host,$HostIP) = ($Warning =~ /^bad size limit "([^"]+)" in EHLO reply from ([^[]+)\[($re_IP)\]$/o )) {
         #TD warning: bad size limit "-679215104" in EHLO reply from example.com[192.168.0.1] 
         $Totals{'HeloError'}++;
         $Counts{'HeloError'}{"Bad size limit in EHLO reply"}{formathost($HostIP, $Host)}{"$Size"}++;

      } elsif ( ($Size,$Host,$HostIP,$Service) = ($Warning =~ /^Connection concurrency limit exceeded: (\d+) from ([^[]+)\[($re_IP)\] for service (.*)/o )) {
         #TD warning: Connection concurrency limit exceeded: 51 from example.com[192.168.0.1] for service smtp
         $Totals{'ConcurrencyLimit'}++;
         $Counts{'ConcurrencyLimit'}{$Service}{formathost($HostIP, $Host)}{$Size}++;

      } else {
         #TD warning: No server certs available. TLS won't be enabled
         #TD warning: smtp_connect_addr: bind <localip>: Address already in use 
         $Totals{'WarningsOther'}++;
         $Counts{'WarningsOther'}{$Warning}++;
      }
   }
   # end of warnings section


   # ^$re_QID: ...
   elsif ( ($QID, $ThisLine2) = ($ThisLine =~ /^($re_QID): (.*)$/o ) ) {
      next if ( $ThisLine2 =~ /^client=(?:[^ ]*\[[^ ]*\])\s*$/o );
      next if ( $ThisLine2 =~ /^skipped, still being delivered/o );
      next if ( $ThisLine2 =~ /^host [^ ]*\[[^ ]*\] said: 4[0-9][0-9]/o );
      next if ( $ThisLine2 =~ /^host [^ ]*\[[^ ]*\] refused to talk to me: 4[0-9][0-9]/o );
      next if ( $ThisLine2 =~ /^released from hold$/o );
      next if ( $ThisLine2 =~ /^requeued$/o );
      #TD DA080C2E0B: client=example.com[192.168.0.1]
      #TD NOQUEUE: client=mail.example.com[2001:dead:beef::1]
      #TD F0EC9BBE2: client=mail.example.com[2001:dead:beef::1]
      #TD F0EC9BBE2: message-id=<C1BEA2A0.188572%from@example.com>

      next if ( $ThisLine2 =~ /^message-id=/ );
      # XXX probably don't care about message-id; for now, useful debug aid
      #if (($Remainder) = ($ThisLine2 =~ /^message-id=<(.*)>$/ )) {
      #   if (exists $Qids{$QID}) {
      #      print "Error: Duplicate QID: $QID, $Remainder\n";
      #   }
      #   $Qids{$QID}{'message-id'} = $Remainder;
      #}

      # $re_QID: reject: ...
      # $re_QID: reject_warning: ...
      if (($RejectAction,$ThisLine3) = ($ThisLine2 =~ /^(reject(?:_warning)?): (.*)$/ )) {
         $RejectAction =~ s/^r/R/; $RejectAction =~ s/_warning$/Warn/;

         # $re_QID: reject: RCPT from ...
         if (($ThisLine4) = ($ThisLine3 =~ /^RCPT from (.*)$/o )) {
            #print "ThisLine4: $ThisLine4\n";
         
            # Recipient address rejected: Unknown users and via check_recipient_access
            if ( ($Host,$HostIP,$Recip,$Reason,$Remainder) = ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN <(.*)>: Recipient address rejected: ([^;]*);(.*)$/o )) {

               # Unknown users; local mailbox, alias, virtual, relay user, unspecified
               if (($Reason) =~ s/^User unknown *//o) {
                  ($Table) = ($Reason =~ /^in ((?:\w+ )+table)/o);
                  ($From) = ($Remainder =~ /^ from=<([^>]*)>/o );

                  $Table = "Address table unavailable"	if ($Table =~ /^$/);     # when show_user_unknown_table_name=no
                  $From = "<>"		if ($From =~ /^$/);

                  #TD NOQUEUE: reject: RCPT from sample.net[192.168.0.1]: 550 <to@example.com>: Recipient address rejected: User unknown in local recipient table; from=<> to=<to@example.com> proto=SMTP helo=<sample.net>
                  #TD NOQUEUE: reject_warning: RCPT from sample.net[192.168.0.1]: 550 <to@example.com>: Recipient address rejected: User unknown in local recipient table; from=<> to=<to@example.com> proto=SMTP helo=<sample.net>
                  #TD NOQUEUE: reject: RCPT from localhost[127.0.0.1]: 550 5.1.1 <to@example.com>: Recipient address rejected: User unknown in virtual address table; from=<from@sample.net> to=<to@example.com> proto=ESMTP helo=<localhost>
                  #TD NOQUEUE: reject: RCPT from example.com[10.0.0.1]: 450 4.1.1 <to@sample.net>: Recipient address rejected: User unknown in virtual mailbox table; from=<from@example.com> to=<to@sample.net> proto=ESMTP helo=<example.com>
                  #TD NOQUEUE: reject: RCPT from sample.net[10.0.0.1]: 550 5.5.0 <to1@example.com>: Recipient address rejected: User unknown; from=<from1@sample.net> to=<to@example.com> proto=ESMTP helo=<[10.0.0.1]>
                  #TD NOQUEUE: reject: RCPT from example.com[2001:dead:beef::1]: 450 <to@example.net>: Recipient address rejected: Greylisted; from=<from@example.com> to=<to@example.net> proto=ESMTP helo=<example.com>
                  #print "User: $User, Table: $Table\n";

                  $Totals{"${RejectAction}UnknownUser"}++;
                  $Counts{"${RejectAction}UnknownUser"}{"\u$Table"}{$Recip}{$From}++;

               # check_recipient_access
               } else {
                  #TD NOQUEUE: reject: RCPT from example.com[10.0.0.1]: 454 4.7.1 <to@sample.net>: Recipient address rejected: Access denied; from=<from@example.com> to=<to@sample.net> proto=SMTP helo=<example.com>
                  #TD NOQUEUE: reject_warning: RCPT from example.com[10.0.0.1]: 454 4.7.1 <to@sample.net>: Recipient address rejected: Access denied; from=<from@example.com> to=<to@sample.net> proto=SMTP helo=<example.com>
                  #TD NOQUEUE: reject: RCPT from example.com[10.0.0.1]: 450 4.1.2 <to@example.com>: Recipient address rejected: Domain not found; from=<from@sample.net> to=<to@example.com> proto=ESMTP helo=<sample.net>
                  #TD NOQUEUE: reject: RCPT from example.com[10.0.0.1]: 554 <to@example.net>: Recipient address rejected: Please see http://www.openspf.org/why.html?sender=from%40example.net&ip=10.0.0.1&receiver=mx.example.net; from=<from@example.net> to=<to@example.net> proto=ESMTP helo=<to@example.com>
                  #TD NOQUEUE: reject: RCPT from mail.example.com[10.0.0.1]: 550 <unknown@example.net>: Recipient address rejected: undeliverable address: host mail.example.net[192.168.0.1] said: 550 <unknown@example.net>: User unknown in virtual alias table (in reply to RCPT TO command); from=<from@example.com> to=<unknown@example.net> proto=SMTP helo=<mail.example.com>
                  if ($Reason =~ m{^Please see http://www.openspf.org/why.html}o) {
                     $Reason = 'SPF reject';
                  } elsif ($Reason =~ /^undeliverable address: host ([^[]+)\[($re_IP)\] said:/o) {
                     $Reason = 'undeliverable address: remote host rejected recipient';
                  }

                  $Totals{"${RejectAction}Recip"}++;
                  $Counts{"${RejectAction}Recip"}{"\u$Reason"}{$Recip}{formathost($HostIP,$Host)}++;
               }

            } elsif ( ($Host,$HostIP,$To) = ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN <([^ ]*)>.* Relay access denied.* to=([^ ]*)/o ) ) {
               #TD NOQUEUE: reject: RCPT from example.com[192.168.0.1]: 554 <to@sample.net>: Relay access denied; from=<from@example.com> to=<to@sample.net> proto=SMTP helo=<example.com>
               #TD NOQUEUE: reject_warning: RCPT from example.com[192.168.0.1]: 554 <to@sample.net>: Relay access denied; from=<from@example.com> to=<to@sample.net> proto=SMTP helo=<example.com>
               # print "Host: \"$Host\", HostIP: \"$HostIP\", To: \"$To\"\n";

               $Totals{"${RejectAction}Relay"}++;
               $Counts{"${RejectAction}Relay"}{formathost($HostIP,$Host)}{$To}++;

            } elsif ( ($Host,$HostIP,$From,$Reason) =  ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN <(.*)>: Sender address rejected: (.*);/o )) {
               #TD NOQUEUE: reject: RCPT from sample.net[10.0.0.1]: 450 4.1.8 <from@sample.net>: Sender address rejected: Domain not found; from=<from@sample.com> to=<to@example.com> proto=ESMTP helo=<sample.net>
               #TD NOQUEUE: reject_warning: RCPT from sample.net[10.0.0.1]: 450 4.1.8 <from@sample.net>: Sender address rejected: Domain not found; from=<from@sample.com> to=<to@example.com> proto=ESMTP helo=<sample.net>
               #TD NOQUEUE: reject: RCPT from mail.example.com[10.0.0.1]: 550 <unknown@example.net>: Sender address rejected: undeliverable address: host mail.example.net[192.168.0.1] said: 550 <unknown@example.net>: User unknown in virtual alias table (in reply to RCPT TO command); from=<unknown@example.net> to=<user@example.net> proto=SMTP helo=<mail.example.com>
               # print "Host: \"$Host\", HostIP: \"$HostIP\", From: \"$From\", Reason: \"$Reason\"\n";
               $From = "<>"		if ($From =~ /^$/);
               if ($Reason =~ /^undeliverable address: host ([^[]+)\[($re_IP)\] said:/o) {
                  $Reason = 'undeliverable address: remote host rejected sender';
               }
               $Totals{"${RejectAction}Sender"}++;
               $Counts{"${RejectAction}Sender"}{"\u$Reason"}{formathost($HostIP,$Host)}{$From}++;

            } elsif ( ($Host,$HostIP,$Reason,$From,$Recip) =   ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN <[^[]+\[$re_IP\]>: Client host rejected: (.*); from=<(.*)> to=<(.*)> proto=/o )) {
               #TD NOQUEUE: reject: RCPT from sample.net[10.0.0.1]: 554 <sample.net[10.0.0.1]>: Client host rejected: Access denied; from=<from@sample.net> to=<to@example.com> proto=SMTP helo=<friend> 
               #TD NOQUEUE: reject_warning: RCPT from sample.net[10.0.0.1]: 554 <sample.net[10.0.0.1]>: Client host rejected: Access denied; from=<from@sample.net> to=<to@example.com> proto=SMTP helo=<friend> 
               #TD NOQUEUE: reject: RCPT from sample.net[10.0.0.1]: 450 Client host rejected: cannot find your hostname, [10.0.0.1]; from=<from@sample.net> to=<to@example.com> proto=ESMTP helo=<sample.net>
               $From = "<>"		if ($From =~ /^$/);
               $Totals{"${RejectAction}Client"}++;
               $Counts{"${RejectAction}Client"}{"\u$Reason"}{formathost($HostIP,$Host)}{$Recip}{$From}++;

            } elsif ( ($Host,$From,$Recip,$Helo) = ($ThisLine4 =~ /[^[]+\[($re_IP)\]: $re_DSN Client host rejected: cannot find your hostname, \[$re_IP\]; from=<(.*?)> to=<(.*?)> proto=\S+ helo=<(.*)>/o )) {
               #TD NOQUEUE: reject: RCPT from unknown[10.0.0.1]: 450 Client host rejected: cannot find your hostname, [10.0.0.1]; from=<from@example.com> to=<to@sample.net> proto=ESMTP helo=<example.com> 
               #TD NOQUEUE: reject_warning: RCPT from unknown[10.0.0.1]: 450 Client host rejected: cannot find your hostname, [10.0.0.1]; from=<from@example.com> to=<to@sample.net> proto=ESMTP helo=<example.com> 
               $From = "<>"		if ($From =~ /^$/);
               $Totals{"${RejectAction}UnknownClient"}++;
               $Counts{"${RejectAction}UnknownClient"}{$Host}{$Helo}{$From}{$Recip}++;


            } elsif ( ($Host,$HostIP,$Site,$Reason)  = ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN Service unavailable; (?:Client host |Sender address )?\[[^ ]*\] blocked using ([^ ]*)(, reason: .*)?;/o )) {
               # Note: similar code below: search RejectRBL
               #TD NOQUEUE: reject: RCPT from example.com[10.0.0.1]: 554 5.7.1 Service unavailable; Client host [10.0.0.1] blocked using sbl-xbl.spamhaus.org; http://www.spamhaus.org/query/bl?ip=10.0.0.1; from=<from@example.com> to=<to@sample.net> proto=ESMTP helo=<friend>
               #TD NOQUEUE: reject_warning: RCPT from example.com[10.0.0.1]: 554 5.7.1 Service unavailable; Client host [10.0.0.1] blocked using sbl-xbl.spamhaus.org; http://www.spamhaus.org/query/bl?ip=10.0.0.1; from=<from@example.com> to=<to@sample.net> proto=ESMTP helo=<friend>

               $Totals{"${RejectAction}RBL"}++;
               if ($Reason =~ /^$/) {
                  $Counts{"${RejectAction}RBL"}{$Site}{formathost($HostIP,$Host)}++;
               } else {
                  $Counts{"${RejectAction}RBL"}{$Site}{formathost($HostIP,$Host)}{$Reason}++;
               }

            } elsif ( ($Host,$HostIP,$Reason,$Helo) = ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN <.*>: Helo command rejected: (.*);.* helo=<(.*)>$/o )) {
               #TD NOQUEUE: reject: RCPT from sample.net[10.0.0.1]: 454 4.7.1 <localhost>: Helo command rejected: Access denied; from=<from@sample.net> to=<to@example.com> proto=SMTP helo=<localhost>
               #TD NOQUEUE: reject_warning: RCPT from sample.net[10.0.0.1]: 454 4.7.1 <localhost>: Helo command rejected: Access denied; from=<from@sample.net> to=<to@example.com> proto=SMTP helo=<localhost>
               $Totals{"${RejectAction}Helo"}++;
               $Counts{"${RejectAction}Helo"}{$Reason}{formathost($HostIP, $Host)}{"$Helo"}++;

            } elsif ( ($Host,$HostIP,$From,$To) = ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN Insufficient system storage; from=<([^>]*)> to=<([^>]+)>/o )) {
               #TD NOQUEUE: reject: RCPT from example.com[192.168.0.1]: 452 Insufficient system storage; from=<from@example.com> to=<to@sample.net> 
               #TD NOQUEUE: reject_warning: RCPT from example.com[192.168.0.1]: 452 Insufficient system storage; from=<from@example.com> to=<to@sample.net> 
               $From = "<>"		if ($From =~ /^$/);
               $Totals{"${RejectAction}InsufficientSpace"}++;
               $Counts{"${RejectAction}InsufficientSpace"}{formathost($HostIP,$Host)}{$To}{$From}++;

               $Totals{'WarnInsufficientSpace'}++;    # to show in Warnings section

            } elsif ( ($Host,$HostIP,$From,$To) = ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN Server configuration (?:error|problem); from=<([^>]*)> to=<([^>]+)>/o )) {
               #TD NOQUEUE: reject: RCPT from example.com[10.0.0.1]: 451 4.3.5 Server configuration error; from=<from@example.com> to=<user@sample.net> proto=ESMTP helo=<example.com>
               #TD NOQUEUE: reject_warning: RCPT from example.com[10.0.0.1]: 451 4.3.5 Server configuration error; from=<from@example.com> to=<user@sample.net> proto=ESMTP helo=<example.com>
               #TD NOQUEUE: reject: RCPT from sample.net[192.168.0.1]: 450 Server configuration problem; from=<from@sample.net> to=<to@example.com> proto=ESMTP helo=<sample.net>
               $From = "<>"		if ($From =~ /^$/);
               $Totals{"${RejectAction}ConfigError"}++;
               $Counts{"${RejectAction}ConfigError"}{formathost($HostIP,$Host)}{$To}{$From}++;

               $Totals{'WarnConfigError'}++;          # to show in Warnings section

            # This would capture all other rejects, but I think it might be more useful to add
            # additional capture sections based on user reports of uncapture lines.
            #
            #} elsif ( ($Reason) = ($ThisLine4 =~ /^[^[]+\[$re_IP\]: $re_DSN ([^;]+);/o)) {
            #  $Totals{"${RejectAction}Other"}++;
            #  $Counts{"${RejectAction}Other"}{$Reason}++;

            } else {
               chomp($ThisLine);
               $UnmatchedList{$ThisLine}++;
            }
         }
         # end of $re_QID: reject: RCPT from ...

         # $re_QID: reject: body ...
         # $re_QID: reject: header ...
         elsif ( ($HorB,$Rejected,$Host,$HostIP,$To,$Reason) = ($ThisLine3 =~ /^(header|body) (.*) from ([^[]+)\[($re_IP)\]; from=<(?:[^ ]*)> to=<([^ ]*)>(?: proto=[^ ]* helo=<[^ ]*>)?: (.*)$/o )) {
            #TD 9804DB31C2: reject: header To: <user@example.com> from sample.net[192.168.0.1]; from=<bogus@anywhere.com> to=<user@example.com> proto=ESMTP helo=<anywhere.com>: Any Reason
            #TD 831C2C2E0D: reject: body Quality Replica watches!!! from example.com[192.168.0.1]; from=<user@example.com> to=<recip@sample.net> proto=SMTP helo=<example.com>: 5.7.1 Spam: Watches
            # Note: reject_warning does not seem to occur

            $Rejected =~ s/\s+/ /g;
            if ($HorB =~ /body/) {
               $Totals{'RejectBody'}++;
               $Counts{'RejectBody'}{$Reason}{$To}{formathost($HostIP,$Host)}{"$Rejected"}++;
            }
            else {
               #print "Rejected: \"$Rejected\", Host: \"$Host\", HostIP: \"$HostIP\", To: \"$To\", Reason: \"$Reason\"\n";
               $Totals{'RejectHeader'}++;
               $Counts{'RejectHeader'}{$Reason}{$To}{formathost($HostIP,$Host)}{"$Rejected"}++;
            }
         }

         # $re_QID: reject: MAIL from ...
         elsif ( ($Host,$HostIP) = ($ThisLine3 =~ /^MAIL from ([^[]+)\[($re_IP)\]: $re_DSN Message size exceeds fixed limit; proto=[^ ]* helo=<[^>]+>$/o )) {
            # Postfix responds with this message after a MAIL FROM:<...> SIZE=nnn  command, where postfix consider's nnn excessive
            # Note: similar code below: search RejectSize
            # Note: reject_warning does not seem to occur
            #TD NOQUEUE: reject: MAIL from localhost[127.0.0.2]: 552 Message size exceeds fixed limit; proto=ESMTP helo=<localhost> 
            #TD NOQUEUE: reject: MAIL from example.com[192.168.0.2]: 452 4.3.4 Message size exceeds fixed limit; proto=ESMTP helo=<example.com>
            $Totals{'RejectSize'}++;
            $Counts{'RejectSize'}{formathost($HostIP,$Host)}{'unknown'}++;
         }

         # $re_QID: reject: CONNECT from ...
         elsif (($ThisLine4) = ($ThisLine3 =~ /^CONNECT from (.*)$/o )) {

            if ( ($Host,$HostIP,$Reason,$Remainder) = ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN <.*>: Client host rejected: ([^;]*);(.*)$/o )) {
               #TD NOQUEUE: reject: CONNECT from unknown[192.168.0.1]: 503 5.5.0 <unknown[192.168.0.1]>: Client host rejected: Improper use of SMTP command pipelining; proto=SMTP
               $Totals{"${RejectAction}Client"}++;
               $Counts{"${RejectAction}Client"}{"\u$Reason"}{formathost($HostIP,$Host)}{""}++;    # XXX currently need to keep same key depth - add CONNECT key to do so
            } else {
               chomp($ThisLine);
               $UnmatchedList{$ThisLine}++;
            }
         }

         # $re_QID: reject: VRFY from ...
         elsif (($ThisLine4) = ($ThisLine3 =~ /^VRFY from (.*)$/o )) {
            #TD NOQUEUE: reject: VRFY from example.com[10.0.0.1]: 550 5.1.1 <:>: Recipient address rejected: User unknown in local recipient table; to=<:> proto=SMTP helo=<192.168.0.1>
            #TD NOQUEUE: reject_warning: VRFY from example.com[10.0.0.1]: 450 4.1.2 <<D0-1C7-1F41F6@BS>>: Recipient address rejected: Domain not found; to=<<D0-1C7-1F41F6@BS>> proto=SMTP helo=<friend>
            #TD NOQUEUE: reject: VRFY from example.com[10.0.0.1]: 450 4.1.8 <to@example.com>: Sender address rejected: Domain not found; from=<to@example.com> to=<to> proto=SMTP 
            #TD NOQUEUE: reject: VRFY from example.com[10.0.0.1]: 554 5.7.1 Service unavailable; Client host [10.0.0.1] blocked using zen.spamhaus.org; http://www.spamhaus.org/query/bl?ip=10.0.0.1; to=<u> proto=SMTP

            if ( ($Host,$HostIP,$Reason,$Remainder) = ($ThisLine4 =~ /([^[]+)\[($re_IP)\]: $re_DSN (?:<.*>: )?([^;]*);(.*)$/o )) {
               $Totals{"${RejectAction}Verify"}++;
               $Counts{"${RejectAction}Verify"}{"\u$Reason"}{formathost($HostIP,$Host)}++;

            } else {
               chomp($ThisLine);
               $UnmatchedList{$ThisLine}++;
            }
         }
         else {
            chomp($ThisLine);
            $UnmatchedList{$ThisLine}++;
         }
      }

      # ^$re_QID: ...  (not rejects)
      elsif ( ($Bytes,$NumRecips) = ($ThisLine2 =~ /^from=<[^>]*>, size=(\d+), nrcpt=(\d+).*$/o ) ) {
         #TD 4AEFAF569C11: from=<FROM: SOME USER@example.com>, size=4051, nrcpt=1 (queue active)
         #TD12 2A535C2E01: from=<anyone@example.com>, size=25302, nrcpt=2 (queue active)
         #TD F0EC9BBE2: from=<from@example.com>, size=5529, nrcpt=1 (queue active)

         # Distinguish bytes accepted vs. bytes delivered due to multiple recips

         #if (!exists $Qids{$QID}) {
         #   print "ERROR: no Qids{$QID} found\n";
         #}
         if (!exists $Qids{$QID} and !exists $Qids{$QID}{'nrcpt'}) {
            $Qids{$QID}{'nrcpt'} = $NumRecips;
            $Qids{$QID}{'size'} = $Bytes;
            $Totals{'MsgsAccepted'}++;
            $Totals{'BytesAccepted'} += $Bytes;
         }
         #else {
         #   Occurs for each deferral   
         #   print "DEBUG: RETRY($Qid) $ThisLine2\n";
         #}
      }

      ### sent, forwarded, bounced, softbounce, deferred, (un)deliverable
      elsif ( ($To,$OrigTo,$Relay,$DDD,$Status,$Reason) = ($ThisLine2 =~ /^to=<([^>]*)>,(?: orig_to=\<([^>]*)>,)? relay=([^ ]*).*, ($re_DDD), status=([^ ]+) (.*)$/o  )) {
         #TD 552B6C20E: to=<to@sample.com>, relay=mail.example.net[10.0.0.1]:25, delay=1021, delays=1020/0.04/0.56/0.78, dsn=2.0.0, status=sent (250 Ok: queued as 6EAC4719EB)
         #TD DD925BBE2: to=<to@example.net>, orig_to=<to-ext@example.net>, relay=mail.example.net[2001:dead:beef::1], delay=2, status=sent (250 Ok: queued as 5221227246)
         my ($Dsn);

         $Reason =~ s/\((.*)\)/$1/;    # Makes capturing nested parens easier
         $To     = lc $To;
         $OrigTo = lc $OrigTo;
         my ($Localpart, $Domain) = split ('@', $To);

         # If recipient_delimiter is set, break localpart into user + extension
         # and save localpart in OrigTo if OrigTo is empty
         #
         if ($Recipient_Delimiter and $Localpart =~ /$Recipient_Delimiter/o) {
            my ($user,$extension) = split (/$Recipient_Delimiter/o, $Localpart, 2);
            $OrigTo = $Localpart    if ($OrigTo =~ /^$/);
            $Localpart = $user;
         }

         unless (($Dsn) = ($DDD =~ /dsn=(\d\.\d\.\d)/)) {
            #$Dsn = "X.X.X (DSN unavailable)";
            $Dsn = "";
         }

         ### sent
         if ($Status =~ /^sent$/) {
            if ($Reason =~ /forwarded as /) {
               $Totals{'MsgsForwarded'}++;
               $Counts{'MsgsForwarded'}{$Domain}{$Localpart}{$OrigTo}++;
            }
            else {
               if ($PostfixService =~ /^lmtp$/) {
                  $Totals{'MsgsSentLmtp'}++;
                  $Counts{'MsgsSentLmtp'}{$Domain}{$Localpart}{$OrigTo}++;
               }
               elsif ($PostfixService =~ /^smtp$/) {
                  $Totals{'MsgsSent'}++;
                  $Counts{'MsgsSent'}{$Domain}{$Localpart}{$OrigTo}++;
               }
               # virtual, command, ...
               else {
                  $Totals{'MsgsDelivered'}++;
                  $Counts{'MsgsDelivered'}{$Domain}{$Localpart}{$OrigTo}++;
               }
            }
            if (exists $Qids{$QID} and exists $Qids{$QID}{'size'}) {
               $Totals{'BytesDelivered'} += $Qids{$QID}{'size'};
            }
         }

         ### bounced
         elsif ($Status =~ /^(?:bounced|SOFTBOUNCE)$/) {
            #TD 76EB0D13: to=<user@example.com>, relay=none, delay=1, status=bounced (mail for mail.example.com loops back to myself)
            #TD C8103B94: to=<user@example.com>, relay=none, delay=0, status=bounced (Host or domain name not found. Name service error for name=unknown.com type=A: Host not found)
            #TD C76431E2: to=<login@sample.net>, relay=local, delay=2, status=SOFTBOUNCE (host sample.net[192.168.0.1] said: 450 <login@sample.com>: User unknown in local recipient table (in reply to RCPT TO command))
            #TD EB0B8770: to=<to@example.com>, orig_to=<postmaster>, relay=none, delay=1, status=bounced (User unknown in virtual alias table) 
            #TD EB0B8770: to=<to@example.com>, orig_to=<postmaster>, relay=sample.net[192.168.0.1], delay=1.1, status=bounced (User unknown in relay recipient table) 
            #TD D8962E54: to=<anyone@example.com>, relay=local, conn_use=2 delay=0.21, delays=0.05/0.02/0/0.14, dsn=4.1.1, status=SOFTBOUNCE (unknown user: "to")
            #TD F031C832: to=<to@sample.net>, orig_to=<alias@sample.net>, relay=local, delay=0.17, delays=0.13/0.01/0/0.03, dsn=5.1.1, status=bounced (unknown user: "to")
            #TD 04B0702E: to=<anyone@example.com>, relay=example.com[10.0.0.1]:25, delay=12, delays=6.5/0.01/0.03/5.1, dsn=5.1.1, status=bounced (host example.com[10.0.0.1] said: 550 5.1.1 User unknown (in reply to RCPT TO command))
            #TD 9DAC8B2D: to=<to@example.com>, relay=example.com[10.0.0.1]:25, delay=1.4, delays=0.04/0/0.27/1.1, dsn=5.0.0, status=bounced (host example.com[10.0.0.1] said: 511 sorry, no mailbox here by that name (#5.1.1 - chkuser) (in reply to RCPT TO command))
            #TD 79CB702D: to=<to@example.com>, relay=example.com[10.0.0.1]:25, delay=0.3, delays=0.04/0/0.61/0.8, dsn=5.0.0, status=bounced (host example.com[10.0.0.1] said: 550 <to@example.com>, Recipient unknown (in reply to RCPT TO command))
            #TD 88B7A079: to=<to@example.com>, relay=example.com[10.0.0.1]:25, delay=45, delays=0.03/0/5.1/40, dsn=5.0.0, status=bounced (host example.com[10.0.0.1] said: 550-"The recipient cannot be verified.  Please check all recipients of this 550 message to verify they are valid." (in reply to RCPT TO command))
            #TD 47B7B074: to=<to@example.com>, relay=example.com[10.0.0.1]:25, delay=6.6, delays=6.5/0/0/0.11, dsn=5.1.1, status=bounced (host example.com[10.0.0.1] said: 550 5.1.1 <to@example.com> User unknown; rejecting (in reply to RCPT TO command))

            # print "bounce message from " . $To . " msg : " . $Relay . "\n";

            ### local bounce
            # XXX local v. remote bounce seems iffy:  local seems relative
            if ($Relay =~ /^(?:none|local|avcheck|maildrop|127\.0\.0\.1)/) {
               $Totals{'BounceLocal'}++;
               $Counts{'BounceLocal'}{get_dsn_msg($Dsn)}{$To}{"\u$Reason"}++;

            ### remote bounce
            } else {
               my ($reply,$fmtdhost) = cleanhostreply($Reason,$Relay,$To,$Domain);

               $Totals{'BounceRemote'}++;
               $Counts{'BounceRemote'}{get_dsn_msg($Dsn)}{$Domain}{$Localpart}{$fmtdhost}{$reply}++;
            }
         }

         elsif ($Status =~ /deferred/) {

            #TD DD4F2AC4D3: to=<to@example.com>, relay=none, delay=27077, delays=27077/0/0.57/0, dsn=4.4.3, status=deferred (Host or domain name not found. Name service error for name=example.com type=MX: Host not found, try again)
            #TD E52A1F1B52: to=<to@example.com>, relay=none, delay=141602, status=deferred (connect to mx1.example.com[10.0.0.1]: Connection refused)
            #TD E52A1F1B52: to=<to@example.com>, relay=none, delay=141602, status=deferred (delivery temporarily suspended: connect to example.com[192.168.0.1]: Connection refused)
            #TD DB775D7035: to=<to@example.com>, relay=none, delay=306142, delays=306142/0.04/0.18/0, dsn=4.4.1, status=deferred (connect to example.com[10.0.0.1]: Connection refused)
            #TD EEDC1F1AA6: to=<to@example.org>, relay=example.org[10.0.0.1], delay=48779, status=deferred (lost connection with mail.example.org[10.0.0.1] while sending MAIL FROM)
            #TD 8E7A0575C3: to=<to@sample.net>, relay=sample.net, delay=26541, status=deferred (conversation with mail.example.com timed out while sending end of data -- message may be sent more than once) 
            #TD 7CF61B7030: to=<to@sample.net>, relay=sample.net[10.0.0.1]:25, delay=322, delays=0.04/0/322/0, dsn=4.4.2, status=deferred (conversation with example.com[10.0.0.01] timed out while receiving the initial server greeting)
            #TD B8BF0AE331: to=<to@localhost>, orig_to=<toalias@localhost>, relay=none, delay=238024, status=deferred (delivery temporarily suspended: transport is unavailable) 

            # XXX postfix reports dsn=5.0.0, host's reply may contain its own dsn's such as 511 and #5.1.1
            # XXX should these be used instead?
            #TD 232EAC2E55: to=<to@sample.net>, relay=sample.net[10.0.0.1]:25, delay=5.7, delays=0.05/0.02/5.3/0.3, dsn=4.7.1, status=deferred (host sample.net[10.0.0.1] said: 450 4.7.1 <to@sample.net>: Recipient address rejected: Greylisted (in reply to RCPT TO command))
            #TD 11677B700D: to=<to@example.com>, relay=example.com[10.0.0.1]:25, delay=79799, delays=79797/0.02/0.4/1.3, dsn=4.0.0, status=deferred (host example.com[10.0.0.1] said: 450 <to@example.com>: User unknown in local recipient table (in reply to RCPT TO command))
            #TD 0DA72B7035: to=<to@example.com>, relay=example.com[10.0.0.1]:25, delay=97, delays=0.03/0/87/10, dsn=4.0.0, status=deferred (host example.com[10.0.0.1] said: 450 <to@example.com>: Recipient address rejected: undeliverable address: User unknown in virtual alias table (in reply to RCPT TO command))

            my ($reply,$fmtdhost) = cleanhostreply($Reason,$Relay,$To,$Domain);

            if ($DeferredByQid{$QID}++ == 0) {
               $Totals{'MsgsDeferred'}++;
            }
            $Totals{'Deferrals'}++;
            $Counts{'Deferrals'}{get_dsn_msg($Dsn)}{$reply}{$Domain}{$Localpart}{$fmtdhost}++;
         }

         elsif ($Status =~ /^undeliverable$/) {
            #TD B54D220BFC: to=<u@example.com>, relay=sample.com[10.0.0.1], delay=0, dsn=5.0.0, status=undeliverable (host sample.com[10.0.0.1] refused to talk to me: 554 5.7.1 example.com Connection not authorized) 
            #TD 8F699C2EA6: to=<u@example.com>, relay=virtual, delay=0.14, delays=0.06/0/0/0.08, dsn=5.1.1, status=undeliverable (unknown user: "u@example.com")
            $Totals{'Undeliverable'}++;
            $Counts{'Undeliverable'}{$Reason}{$OrigTo ? "$To ($OrigTo)" : "$To"}++;
         }

         elsif ($Status =~ /^deliverable$/) {
            # sendmail -bv style deliverable reports
            #TD ED862C2EA6: to=<u@example.com>, relay=virtual, delay=0.09, delays=0.03/0/0/0.06, dsn=2.0.0, status=deliverable (delivers to maildir)
            $Totals{'Deliverable'}++;
            $Counts{'Deliverable'}{$Reason}{$OrigTo ? "$To ($OrigTo)" : "$To"}++;
         }

         else {
            # keep this as the last condition in this else clause
            chomp($ThisLine);
            $UnmatchedList{$ThisLine}++;
         }
      } # end of sent, forwarded, bounced, softbounce, deferred, (un)deliverable

      # XXX don't care about this anymore; MsgsAccepted are counted with from= lines
      elsif ( $ThisLine2 =~ /^uid=(?:[^ ]*) from=<(?:[^>]*)>/o ) {
         #TD2 1DFE2C2E18: uid=0 from=<root>
         #$Totals{'MsgsAccepted'}++;
      }

      elsif ( ($From) = ($ThisLine2 =~ /^from=<([^>]*)>, status=expired, returned to sender$/o )) {
         #TD 9294C8866: from=<from@example.com>, status=expired, returned to sender
         $From = "<>"		if ($From =~ /^$/);
         $Totals{'ReturnedToSender'}++;
         $Counts{'ReturnedToSender'}{$From}++;

      } elsif ( ($ThisLine2 =~ /^resent-message-id=<?(?:[^>]*)>?$/o  )) {
         #TD 52A49200E1: resent-message-id=4739073.1
         #TD DB2E3C2E0E: resent-message-id=<ARF+DXZwLECdxm@mail.example.com>
         $Totals{'MsgsResent'}++;

      # see also ConnectionLost elsewhere
      } elsif (($Host,$HostIP,$Reason) = ($ThisLine2 =~ /^lost connection with ([^[]*)\[($re_IP)\] (while .*)$/o )) {
         #TD EB7D4341F0: lost connection with sample.net[10.0.0.1] while sending MAIL FROM
         #TD 5F6C7C2E0F: lost connection with sample.net[10.0.0.2] while receiving the initial server greeting
         $Totals{'ConnectionLost'}++;
         $Counts{'ConnectionLost'}{"\u$Reason"}{formathost($HostIP,$Host)}++;
      }

      # see also TimeoutInbound elsewhere
      elsif (($Host,$HostIP,$Reason) = ($ThisLine2 =~ /^conversation with ([^[]*)\[($re_IP)\] timed out (while .*)$/o )) {
         #TD C20574341F3: conversation with sample.net[10.0.0.1] timed out while receiving the initial SMTP greeting 
         $Totals{'TimeoutInbound'}++;
         $Counts{'TimeoutInbound'}{"\u$Reason"}{formathost($HostIP,$Host)}++;
      }

      elsif ($ThisLine2 =~ /^sender delay notification: $re_QID$/o) {
         #TD 8DB93C2FF2: sender delay notification: AA61EC2F9A 
         $Totals{'SenderDelayNotification'}++;

      } elsif ( ($Warning,$Host,$HostIP,$To,$Reason) = ($ThisLine2 =~ /^warning: header (.*) from ([^[]+)\[($re_IP)\]; from=<(?:[^ ]*)> to=<([^ ]*)>(?: proto=[^ ]* helo=<[^ ]*>)?(?:: (.*))?$/o )) {
         $Reason = 'Unknown Reason'    if ($Reason =~ /^$/);
         $Totals{'WarningHeader'}++;
         $Counts{'WarningHeader'}{$Reason}{formathost($HostIP,$Host)}{$To}{$Warning}++;

      ### filter messages
      } elsif ( ($Host,$HostIP,$Trigger,$Reason,$Filter,$From,$To) = ($ThisLine2 =~ /^filter: RCPT from ([^[]+)\[($re_IP)\]: <([^>]*)>: (.*) triggers FILTER ([^;]+); from=<([^>]*)> to=<([^>]+)> proto=\S+ helo=<[^>]+>$/o )) {
         $From = "<>"		if ($From =~ /^$/);
         #TD NOQUEUE: filter: RCPT from example.com[10.0.0.1]: <>: Sender address triggers FILTER filter:somefilter; from=<> to=<to@sample.net> proto=SMTP helo=<example.com>
         #TD NOQUEUE: filter: RCPT from example.com[192.168.0.1]: <to@exmple.com>: Recipient address triggers FILTER smtp-amavis:[127.0.0.1]:10024; from=<from@sample.net> to=<to@example.com> proto=SMTP helo=<sample.net>
         $Totals{'Filtered'}++;
         $Counts{'Filtered'}{$Reason}{$Filter}{formathost($HostIP,$Host)}{$Trigger}{$To}{$From}++;

      ### Hold messages
      } elsif ( ($Reason,$Host,$HostIP,$To) = ($ThisLine2 =~ /^hold: (?:header|body) (.*) from ([^[]+)\[($re_IP)\]; from=<(?:[^ ]*)> to=<([^ ]*)>(?: proto=[^ ]* helo=<[^ ]*>)?(?:: (.*))?$/o )) {
         #TD E9E0CC2E22: hold: header Message-ID: <user@example.com> from localhost[127.0.0.1]; from=<test@sample.net> to=<user@example.com> proto=ESMTP helo=<sample.net>: Log message here
         #TD 76561D30BF: hold: header Received: from sample.net (sample.net[192.168.0.1])??by example.com (Postfix) with ESMTP id 676530BF??for <X>; Thu, 20 Oct 2006 13:27: from sample.net[192.168.0.2]; from=<user@sample.net> to=<touser@example.com> proto=ESMTP helo=<sample.net>
         $Reason = 'Unknown Reason'    if ($Reason =~ /^$/);
         $Totals{'Hold'}++;
         $Counts{'Hold'}{$Reason}{formathost($HostIP,$Host)}{$To}++;

      } elsif (($Reason,$Host,$To) = ($ThisLine2 =~ /^hold: (?:header|body) (.*) from (local); from=<(?:[^ ]*)> to=<([^ ]*)>/o )) {
         #TD 64215C2E55: hold: header Subject: Hold Test from local; from=<test@sample.net> to=<user@example.com>: testing hold messages
         $Reason = 'Unknown Reason'    if ($Reason =~ /^$/);
         $Totals{'Hold'}++;
         $Counts{'Hold'}{$Reason}{"$Host"}{$To}++;


      } elsif ( $ThisLine2 =~ /^removed\s*$/o ) {
         # 52CBDC2E0F: removed
         if (exists $Qids{$QID}) {
            delete $Qids{$QID};
         }
         #else {
         #   happens when log lines are outside of logwatch's range
         #   or a log rotation occurred.
         #   print "Debug: Qids{$QID} nonexistent\n";
         #}
         $Totals{'RemovedFromQueue'}++;

      } elsif ( ($Host, $HostIP) = ($ThisLine2 =~ /^enabling PIX <CRLF>\.<CRLF> workaround for ([^[]+)\[($re_IP)\]/o  )) {
         #TD 6DE182FC0B: enabling PIX <CRLF>.<CRLF> workaround for example.com[192.168.0.1]
         #TD 272D0C2E55: enabling PIX <CRLF>.<CRLF> workaround for mail.sample.net[10.0.0.1]:25
         $Totals{'PixWorkaround'}++;
         $Counts{'PixWorkaround'}{formathost($HostIP, $Host)}++;

      } elsif ( ($Host,$HostIP,$Remainder) = ($ThisLine2 =~ /^client=([^[]+)\[($re_IP)\],( sasl_(?:method|username|sender)=.*)$/o )) {
         #TD 6C8F93041B: client=localhost[127.0.0.1], sasl_sender=someone@example.com 
         #TD 150B9837E4: client=example.com[192.168.0.1], sasl_method=PLAIN, sasl_username=anyone@sample.net
         #TD EFC962C4C1: client=example.com[192.168.0.1], sasl_method=LOGIN, sasl_username=user@example.com, sasl_sender=<id352ib@sample.net>
         my ($Method,$User,$Sender) = ($Remainder =~ /^(?: sasl_method=([^,]+),?)?(?: sasl_username=([^,]+),?)?(?: sasl_sender=<([^>]*)>)?$/o );

         $User = 'Unknown'       if ($User =~ /^$/);
         $Method = 'Unknown'     if ($Method =~ /^$/);

         # sasl_sender occurs when AUTH verb is present in MAIL FROM, typically used for relaying
         # the username (eg. sasl_username) of authenticated users.
         if ($Sender) {
            $Totals{'SaslAuthRelay'}++;
            $Counts{'SaslAuthRelay'}{"$Sender ($User)"}{$Method}{formathost($HostIP,$Host)}++;
         }
         else {
            $Totals{'SaslAuth'}++;
            $Counts{'SaslAuth'}{$User}{$Method}{formathost($HostIP,$Host)}{$Sender}++;
         }

      } elsif ( $ThisLine2 =~ /^sender non-delivery notification/ ) {
         #TD 5426ACC81: sender non-delivery notification: 7446BCD68
         $Totals{'DSNUndelivered'}++;

      } elsif ( $ThisLine2 =~ /^sender delivery status notification/ ) {
         #TD 5426ACC81: sender delivery status notification: 7446BCD68
         $Totals{'DSNDelivered'}++;

      } elsif ( ($Host,$HostIP,$Site,$Reason) = ($ThisLine2 =~ /^discard: RCPT from ([^[]+)\[($re_IP)\]: ([^:]*): ([^;]*);/o)) {
         #TD NOQUEUE: discard: RCPT from sample.net[192.168.0.1]: <sender@example.com>: Sender address - test; from=<sender@example.com> to=<To@sample.net> proto=ESMTP helo=<example.com>
         $Totals{'Discarded'}++;
         $Counts{'Discarded'}{formathost($HostIP,$Host)}{$Site}{$Reason}++;

      } elsif ( ($When,$Host,$HostIP,$Reason,$Remainder) = ($ThisLine2 =~ /^milter-reject: (\S+) from ([^[]+)\[($re_IP)\]: $re_DSN ([^;]+); (.*)$/o )) {

         #TD NOQUEUE: milter-reject: MAIL from example.com[192.168.0.1]: 553 5.1.7 address incomplete; proto=ESMTP helo=<example.com>
         #TD NOQUEUE: milter-reject: CONNECT from sample.net[10.0.0.1]: 451 4.7.1 Service unavailable - try again later; proto=SMTP
         #TD C569C12: milter-reject: END-OF-MESSAGE from sample.net[10.0.0.1]: 5.7.1 black listed URL host sample.com by .black.uribl.com; from=<from@sample.net> to=<to@example.com> proto=ESMTP helo=<sample.net>
         # Note: reject_warning does not seem to occur

         $Totals{'RejectMilter'}++;
         #$Counts{'RejectMilter'}{$When}{formathost($HostIP,$Host)}{$Reason}{$Remainder}++;
         $Counts{'RejectMilter'}{$When}{formathost($HostIP,$Host)}{$Reason}++;

      } else {
         # keep this as the last condition in this else clause
         chomp($ThisLine);
         $UnmatchedList{$ThisLine}++;
      }
   }
   # end of $re_QID section


   # see also ConnectionLost in $re_QID section
   elsif ( ($Reason,$Host,$HostIP) = ($ThisLine =~ /lost connection (after [^ ]*) from ([^[]*)\[($re_IP|unknown)\]$/o )) {
      unless ($HostIP =~ /unknown/) {
         #TD lost connection after CONNECT from mail.example.com[192.168.0.1] 
         $Totals{'ConnectionLost'}++;
         $Counts{'ConnectionLost'}{"\u$Reason"}{formathost($HostIP,$Host)}++;
      } else {
         # According to Wietse, this is "a symptom of doing too much before-queue processing. When
         # Postfix falls behind, established connections accumulate in the kernel, and clients
         # disconnect after timeout while waiting for the SMTP server to respond."
         # So, we'll call this out as a warning

         $Totals{'ConnectionLostOverload'}++;
         $Counts{'ConnectionLostOverload'}{"\u$Reason"}{formathost($HostIP,$Host)}++;
      }
   }

   # see also TimeoutInbound in $re_QID section
   elsif ( ($Reason,$Host,$HostIP) = ($ThisLine =~ /^timeout (after [^ ]*) from ([^[]*)\[($re_IP)\]$/o)) {
      #TD timeout after RSET from example.com[192.168.0.1]
      $Totals{'TimeoutInbound'}++;
      $Counts{'TimeoutInbound'}{"\u$Reason"}{formathost($HostIP,$Host)}++;
   }

   elsif ( ($RejectAction,$Host,$HostIP,$Site,$Reason)  = ($ThisLine =~ /^(reject(?:_warning)?): RCPT from ([^[]+)\[($re_IP)\]: $re_DSN Service unavailable; (?:Client host |Sender address )?\[[^ ]*\] blocked using ([^ ]*)(?:, reason: (.*))?;/o )) {
      $RejectAction =~ s/^r/R/; $RejectAction =~ s/_warning/Warn/;
      # Note: similar code above: search RejectRBL
      # postfix doesn't always log QID.  Also, "reason:" was probably always present in this case, but I'm not certain
      #TD reject: RCPT from example.com[10.0.0.1]: 554 Service unavailable; [10.0.0.1] blocked using orbz.org, reason: Open relay. Please see http://orbz.org/?10.0.0.1; from=<from@example.com> to=<to@sample.net> 
      #TD reject_warning: RCPT from example.com[10.0.0.1]: 554 Service unavailable; [10.0.0.1] blocked using orbz.org, reason: Open relay. Please see http://orbz.org/?10.0.0.1; from=<from@example.com> to=<to@sample.net> 

      $Totals{"${RejectAction}RBL"}++;
      if ($Reason =~ /^$/) {
         $Counts{"${RejectAction}RBL"}{$Site}{formathost($HostIP,$Host)}++;
      } else {
         $Counts{"${RejectAction}RBL"}{$Site}{formathost($HostIP,$Host)}{$Reason}++;
      }
   }

   ### postsuper release from hold
   elsif ( ($Count) = ($ThisLine =~ /^Released from hold: (\d+) messages?$/)) {
      #TD Released from hold: 1 message
      $Totals{'ReleasedFromHold'} += $Count;

   ### postsupoer requeued
   } elsif ( ($Count) = ($ThisLine =~ /^Requeued: (\d+) messages?$/)) {
      #TD Requeued: 1 message
      $Totals{'Requeued'} += $Count;

   } elsif ( ($Host,$HostIP,$Type) = ($ThisLine =~ /^TLS connection established from ([^[]+)\[($re_IP)\]: (.*)$/o )) {
      #TD TLS connection established from example.com[192.168.0.1]: TLSv1 with cipher DHE-RSA-AES256-SHA (256/256 bits) 
      $Totals{'TlsConnectFrom'}++;
      $Counts{'TlsConnectFrom'}{formathost($HostIP,$Host)}{$Type}++; 

   ### smtpd_tls_loglevel >= 1
   } elsif ( ($Host,$Type) = ($ThisLine =~ /^TLS connection established to ([^ ]*): (.*)$/)) {
      #TD TLS connection established to example.com: TLSv1 with cipher AES256-SHA (256/256 bits) 
      $Totals{'TlsConnectTo'}++;
      $Counts{'TlsConnectTo'}{$Host}{$Type}++; 

   ### smtpd_tls_loglevel >= 1
   } elsif ( ($Cert) = ($ThisLine =~ /^Unverified: (.*)/)) {
      #TD Unverified: subject_CN=(www|smtp|mailhost).(example.com|sample.net), issuer=someuser 
      $Totals{'TlsUnverified'}++;
      $Counts{'TlsUnverified'}{$Cert}++; 

   } elsif ( ($ThisLine =~ m/(lookup )?table ([^ ]+ )?has changed -- (restarting|exiting)$/)) {
      #TD table hash:/etc/postfix/helo_checks has changed -- restarting
      $Totals{'TableChanged'}++;

   } elsif ( ($Command,$Host,$HostIP) = ($ThisLine =~ /too many errors after ([^ ]*) from ([^[]*)\[($re_IP)\]$/o)) {
      #TD too many errors after AUTH from sample.net[10.0.0.1] 
      $Totals{'TooManyErrors'}++;
      $Counts{'TooManyErrors'}{"After $Command"}{formathost($HostIP,$Host)}++;

   # Note: no QID
   } elsif ( ($Host,$HostIP,$From,$To) = ($ThisLine =~ /^reject: RCPT from ([^[]+)\[($re_IP)\]: [45]52 Message size exceeds fixed limit; from=<([^>]*)> to=<([^>]+)>/o )) {
      #TD reject: RCPT from example.com[192.168.0.1]: 452 Message size exceeds fixed limit; from=<from@example.com> to=<to@sample.net>
      #TD reject: RCPT from example.com[192.168.0.1]: 552 Message size exceeds fixed limit; from=<from@example.com> to=<to@sample.net> proto=ESMTP helo=<example.com>
      # Note: similar code above: search RejectSize
      # Note: reject_warning does not seem to occur
      $From = "<>"		if ($From =~ /^$/);
      $Totals{'RejectSize'}++;
      $Counts{'RejectSize'}{formathost($HostIP,$Host)}{$To}{$From}++;
   }

   # rare messages (mostly debug) hit less frequently - keep far down the if-elsif chain
   # be sure anything placed here would not match any cases above
   elsif (( $ThisLine =~ /^statistics:/ )
      or  ( $ThisLine =~ /^[<>]+ / )
      or  ( $ThisLine =~ /^premature end-of-input (on|from) .* socket while reading input attribute name$/ )
      or  ( $ThisLine =~ /^Peer certi?ficate could not be verified$/ )   # missing i was a postfix typo
      or  ( $ThisLine =~ /^Peer verification:/ )
      or  ( $ThisLine =~ /^initializing the server-side TLS/ )
      or  ( $ThisLine =~ /^tlsmgr_cache_run_event/ )
      or  ( $ThisLine =~ /^SSL_accept/ )
      or  ( $ThisLine =~ /^connection (?:closed|established)/ )
      or  ( $ThisLine =~ /^connect to subsystem/ )
      or  ( $ThisLine =~ /^delete smtpd session/ )
      or  ( $ThisLine =~ /^put smtpd session/ )
      or  ( $ThisLine =~ /^save session/ )
      or  ( $ThisLine =~ /^Reusing old/ )
      or  ( $ThisLine =~ /^looking up session/ )
      or  ( $ThisLine =~ /^lookup smtpd session/ )
      or  ( $ThisLine =~ /^lookup (?:\S+) type/ )
      or  ( $ThisLine =~ /^xsasl_cyrus_server_/ )
      or  ( $ThisLine =~ /^watchdog_/ )
      or  ( $ThisLine =~ /^read smtpd TLS/ )
      or  ( $ThisLine =~ /^open smtpd TLS/ )
      or  ( $ThisLine =~ /^write smtpd TLS/ )
      or  ( $ThisLine =~ /^auto_clnt_/ )
      or  ( $ThisLine =~ /^Verified: / )
      or  ( $ThisLine =~ /^generic_checks:/ )
      or  ( $ThisLine =~ /^inet_addr_/ )
      or  ( $ThisLine =~ /^mac_parse:/ )
      or  ( $ThisLine =~ /^cert has expired/ )
      or  ( $ThisLine =~ /^daemon started/ )
      or  ( $ThisLine =~ /^master_notify:/ )
      or  ( $ThisLine =~ /^rewrite_clnt:/ )
      or  ( $ThisLine =~ /^dict_/ )
      or  ( $ThisLine =~ /^send attr / )
      or  ( $ThisLine =~ /^match_/ )
      or  ( $ThisLine =~ /^smtpd_check_/ )
      or  ( $ThisLine =~ /^input attribute / )
      or  ( $ThisLine =~ /^Run-time/ )
      or  ( $ThisLine =~ /^Compiled against/ )
      or  ( $ThisLine =~ /^private\// )
      or  ( $ThisLine =~ /^reject_unknown_/ )    # don't combine or shorten these reject_ patterns
      or  ( $ThisLine =~ /^reject_unauth_/ )
      or  ( $ThisLine =~ /^reject_non_/ )
      or  ( $ThisLine =~ /^permit_/ )
      or  ( $ThisLine =~ /^idle timeout/ )
      or  ( $ThisLine =~ /^get_dns_/ )
      or  ( $ThisLine =~ /^dns_/ )
      or  ( $ThisLine =~ /^chroot / )
      or  ( $ThisLine =~ /^process generation/ )
      or  ( $ThisLine =~ /^rewrite stream/ )
      or  ( $ThisLine =~ /^fsspace:/ )
      or  ( $ThisLine =~ /^master disconnect/ )
      or  ( $ThisLine =~ /^resolve_clnt/ )
      or  ( $ThisLine =~ /^ctable_/ )
      or  ( $ThisLine =~ /^extract_addr/ )
      or  ( $ThisLine =~ /^mynetworks:/ )
      or  ( $ThisLine =~ /^name_mask:/ )
      or  ( $ThisLine =~ /^reload configuration/ )
      or  ( $ThisLine =~ /^setting up TLS connection (from|to)/ )
      or  ( $ThisLine =~ /^starting TLS engine$/ )
      or  ( $ThisLine =~ /^terminating on signal 15$/ )
      or  ( $ThisLine =~ /^verify error:num=/ ) )
   {
         next;
   }

   # last case catches all unforeseen messages
   else {
      chomp($ThisLine);
      $UnmatchedList{$ThisLine}++;
   }
}

########################################
# Final tabulations, and report printing

# at detail 5, print level 1, detail 6: level 2, ...
my $max_level_global = $Detail - 4;;

# make some corrections now, due to double counting
$Totals{'MsgsAccepted'} -= $Totals{'MsgsResent'}   if ($Totals{'MsgsAccepted'} >= $Totals{'MsgsResent'});

$Totals{'TotalRejects'} = 
        $Totals{'RejectRelay'} 
      + $Totals{'RejectHelo'}
      + $Totals{'RejectUnknownUser'}
      + $Totals{'RejectRecip'}
      + $Totals{'RejectSender'}
      + $Totals{'RejectClient'}
      + $Totals{'RejectUnknownClient'}
      + $Totals{'RejectRBL'}
      + $Totals{'RejectHeader'}
      + $Totals{'RejectBody'}
      + $Totals{'RejectSize'}
      + $Totals{'RejectMilter'}
      + $Totals{'RejectInsufficientSpace'}
      + $Totals{'RejectConfigError'}
      + $Totals{'RejectVerify'}
      ;

# Note: Body, Header, Size, and Milter do not seem to have reject_warnings
$Totals{'TotalRejectWarns'} = 
        $Totals{'RejectWarnRelay'} 
      + $Totals{'RejectWarnHelo'}
      + $Totals{'RejectWarnUnknownUser'}
      + $Totals{'RejectWarnRecip'}
      + $Totals{'RejectWarnSender'}
      + $Totals{'RejectWarnClient'}
      + $Totals{'RejectWarnUnknownClient'}
      + $Totals{'RejectWarnRBL'}
      + $Totals{'RejectWarnInsufficientSpace'}
      + $Totals{'RejectWarnConfigError'}
      + $Totals{'RejectWarnVerify'}
      ;

$Totals{'TotalAcceptPlusReject'} = $Totals{'MsgsAccepted'} + $Totals{'TotalRejects'};

if (keys %Counts) {
   # Print the summary report
   #
   printReports ('Summary', @Formats);

   # Print the detailed report, if detail is sufficiently high
   #
   printReports ('Detailed', @Formats)   if ($Detail >= 5);
}

# Print unmatched lines
#
if (keys %UnmatchedList) {
   my $line;

   print "\n\n**Unmatched Entries**\n";
   foreach $line (sort {$UnmatchedList{$b}<=>$UnmatchedList{$a} } keys %UnmatchedList) {
      printf "%8d   $line\n", $UnmatchedList{$line};
   }
}


##################################################

# Inserts commas in numbers for easier readability
#
sub commify ($) {
    my $text = reverse $_[0];
    $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g;
    return scalar reverse $text;
}

# Formats IP and hostname for even column spacing
#
sub formathost($ $) {
   my ($hostip, $hostname) = @_;
   
   return undef  if ($hostip =~ /^$/ and $hostname =~ /^$/);
   return sprintf "%-${ipaddrwidth}s  \L$hostname", $hostip;
}

# Returns an RFC 3463 DSN messages given a DSN code
#
sub get_dsn_msg {
   my $dsn = shift;
   my ($msg, $class, $subject, $detail);

   return "DSN unavailable"  if ($dsn =~ /^$/);

   unless ($dsn =~ /^(\d)\.((\d{1,3})\.\d{1,3})$/) {
      print "Error: not a DSN code $dsn\n";
      return "Invalid DSN";
   }

   $class = $1; $subject = $3; $detail = $2;

   #print "Class: $class, Subject: $subject, Detail: $detail\n";

   if (exists $dsn_codes{class}{$class}) {
      $msg = $dsn_codes{class}{$class};
   }
   if (exists $dsn_codes{subject}{$subject}) {
      $msg .= ': ' . $dsn_codes{subject}{$subject};
   }
   if (exists $dsn_codes{detail}{$detail}) {
      $msg .= ': ' . $dsn_codes{detail}{$detail};
   }

   #print "get_dsn_msg: $msg\n" if ($msg);
   return $dsn . ': ' . $msg;
}


# $print_which_report = 1, print count summary; print_which_report = 2, prints hash details
#
sub printReports ($ \@) {
   my ($report, $formats) = @_; 
   my $kilobyte = 1024;
   my $megabyte = 1048576;
   my $gigabyte = 1073741824;
   my $terabyte = 1099511627776;
   my $i = 1;
   my $sepchar;
   my $output_occurred = 0;
   my $sect_had_output = 0;

   if ($report eq "Summary") {
      print "****** $report ", '*' x ($Max_Report_Width - 15), "\n\n"   if ($Detail >= 5);
   } elsif ($report eq "Detailed") {
      print "\n****** $report ", '*' x ($Max_Report_Width - 16), "\n";
   } else {
      die ("error: report set incorrectly in printReports: $report");
   }

   for ( @$formats ) {
      my $keyname = $_->[0];
      my $numfmt  = $_->[1];
      my $desc    = $_->[2];
      my $divisor = $_->[3];

      # print count summary
      if ($report =~ /^S/) {

         # print blank line if keyname is null string
         if ($keyname eq "__SECTION") {
            $sect_had_output = 0;
            next;
         }

         if ($keyname eq '\n') {
            print "\n"  if ($output_occurred && $sect_had_output);

         } elsif (($sepchar) = ($keyname =~ /^(.)$/)) {
            printf "%s   %s\n", $sepchar x 8, $sepchar x 48  if ($output_occurred && $sect_had_output);

         } elsif ($Totals{$keyname} > 0) {
            my $fmt = "%8";
            my $extra = " %25s";
            my $total = $Totals{$keyname};

            # Special Z format provides both unitized and unaltered totals, as appropriate
            if ($numfmt =~ /Z/) {
               if ($total >= $terabyte) {
                  $total /= $terabyte;
                  $fmt .= '.3fT';
               } elsif ($total >= $gigabyte) {
                  $total /= $gigabyte;
                  $fmt .= '.3fG';
               } elsif ($total >= $megabyte) {
                  $total /= $megabyte;
                  $fmt .= '.3fM';
               } elsif ($total >= $kilobyte) {
                  $total /= $kilobyte;
                  $fmt .= '.3fK';
               } else {
                  $fmt .= 'd ';
               }
            }
            else {
               $fmt .= "$numfmt ";
               $extra ='';
            }

            if ($divisor) {
               if ($$divisor == $Totals{$keyname}) {
                  printf "$fmt  %-40s 100.00%%\n", $total, $desc;
               }
               else {
                  printf "$fmt  %-40s  %5.2f%%\n", $total, $desc, $Totals{$keyname} * 100 / $$divisor;
               }
            }
            else {
              printf "$fmt  %-21s $extra\n", $total, $desc, commify ($Totals{$keyname});
            }
            $output_occurred++;
            $sect_had_output++;
         }
      }
      # print hashed details
      else {
         next if (! exists $Counts{$keyname});

         my $max_level = exists $MaxLevel{$keyname} ? $MaxLevel{$keyname} : 11;
         my ($count, $listref) = buildTree (%{$Counts{$keyname}}, $max_level, 0);

         if ($count > 0) {
            #printf "_______________________________________________\n"   if (1 != $i++);
            #printf "\n"   if (1 != $i++);
            # print the header
            $desc =~ s/^\s+//; 
            printf "\n%8d   $desc %s\n", $count, '-' x ($Max_Report_Width - 12 - length($desc))  if ((!exists $MaxLevel{$keyname}) or ($MaxLevel{$keyname} > 0));
            #printf "     %s\n", ' ' x length($desc);

            printTree ($listref);
         }
      }
   }

   print "\n";
}

sub printTree($) {
   my ($listref) = @_;
   my ($entry, $rets);
   my $cutlength = $Max_Report_Width - 3;

   #print "listref: $listref\n";

   foreach $entry (sort bycount @$listref) {
      if (ref($entry) ne "HASH") {
         die "Unexpected entry in tree: $entry\n";
      }
      #print "LEVEL: $entry->{LEVEL}, TOTAL: $entry->{TOTAL}, HASH: $entry, DATA: $entry->{DATA}\n";

      # XXX not sure if I want to keep this... just comment out for now
      # for readability, print a blank line to separate 2nd level headings, but only if children exist
      #
      #print "\n"  if (($entry->{LEVEL} == 0) && ($Detail > 5) && ($entry->{CHILDREF} != undef) && (@{$entry->{CHILDREF}} != 1));

      $rets = sprintf "%8d%s%s", $entry->{TOTAL}, '   ' x ($entry->{LEVEL} + 2),  $entry->{DATA};
      if ($debug) {
         printf "%-130s %-60s\n", $rets, $entry->{DEBUG};
      }
      else {
         $rets =~ s/^(.{$cutlength}).*$/$1.../o   if ($Detail <= 10);
         printf "%s\n", $rets;
      }
      printTree ($entry->{CHILDREF}) if ($entry->{CHILDREF} != undef);
   }
}

# XXX optimize this using packed default sorting.  Analysis shows speed isn't an issue though
sub bycount {
   # Sort by totals, then IP address if one exists, and finally by data as a string

   $b->{TOTAL} <=> $a->{TOTAL}

      ||

   pack('C4' => $a->{DATA} =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/o) cmp
      pack('C4' => $b->{DATA} =~ /(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/o)

      ||

   $a->{DATA} cmp $b->{DATA}
}


#
# Builds a tree of REC structures from the multi-key %Counts hashes
# 
# Parameters:
#    Hash:  A multi-key hash, with keys being used as category headings, and leaf data
#           being tallies for that set of keys
#    Level: This current recursion level.  Call with 0.
#
# Returns:
#    Listref: A listref, where each item in the list is a rec record, described as:
#           DATA:      a string: a heading, or log data
#           TOTAL:     an integer: which is the subtotal of this item's children
#           LEVEL:     an integer > 0: representing this entry's level in the tree
#           CHILDREF:  a listref: references a list consisting of this node's children
#    Total: The cummulative total of items found for a given invocation
#

sub buildTree(\% $ $) {
   my ($href, $max_level_item, $level) = @_; 
   my ($subtotal, $childList, $rec);

   my @tmpList;
   my $item;
   my $total = 0;

   @tmpList = ();

   foreach $item (sort keys %$href) {
      if (ref($href->{$item}) eq "HASH") {
         #print " " x ($level * 4), "HASH: LEVEL $level: Item: $item, type: \"", ref($href->{$item}), "\"\n";

         ($subtotal, $childList) = buildTree (%{$href->{$item}}, $max_level_item, $level + 1);

         if ($level < $max_level_global and $max_level_item > $level) {
            # me + children
            $rec = {
               DATA  => $item,
               TOTAL => $subtotal,
               LEVEL => $level,
            };
            $rec->{DEBUG} = "L$level: Count: $subtotal, max_level_global: $max_level_global, max_level_item: $max_level_item"      if ($debug);

         #   if ($level > $max_level_global) {
         #      $rec->{CHILDREF} = undef;
         #   }
         #   else {
               $rec->{CHILDREF} = $childList,
         #   }
            push (@tmpList, $rec);
         }

         $total += $subtotal;
      }
      else {
         if ($item !~ /^$/ and $level < $max_level_global and $max_level_item > $level) {
            $rec = {
               DATA  => $item,
               TOTAL => $href->{$item},
               LEVEL => $level,
               CHILDREF => undef,
            };
            $rec->{DEBUG} = "L$level: Count: $href->{$item}, max_level_global: $max_level_global, max_level_item: $max_level_item"      if ($debug);
            push (@tmpList,  $rec);
         }
         $total += $href->{$item};
      }
   }

   #print " " x ($level * 4), "LEVEL $level: Returning from level $level\n";

   return ($total, \@tmpList);
}

# Map environment variables of the form postfix_KEYNAME. where KEYNAME 
# is one of the Counts/Totals keys in the Format table above, into
# MaxValues. This allows the user to limit the level of detail output
# for the specified category. Unfortunatly, logwatch lowercases all
# items in a config file, so we need to map them to the camel-case keys
# in the Format table above.
#
# Also accepted is postfix_recipient_delimeter, which special case maps the 
# user's choice of postfix recipient_delimiter, to allow localpart splitting
# of email addresses into user and extension.
# 
sub get_env_vals {
   my ($envvar, $value, $var);

   while ( ($envvar, $value) = each %ENV ) {
      next if ($envvar !~ s/^postfix_//);

      if ($envvar =~ /^recipient_delimiter$/) {
         $Recipient_Delimiter = $value eq "+" ? '\+' : $value;
      }
      elsif ($envvar =~ /^max_report_width$/) {
         $Max_Report_Width = $value;
      }
      else {
         foreach $var ( @Formats ) {
            if ($envvar eq lc $var->[0]) {
               $MaxLevel{$var->[0]} = $value;
            }
         }
      }
   }
}

# Clean up a server's reply, to give some uniformity to reports
# XXX postfix reports dsn=5.0.0, host's reply may contain its own dsn's such as 511 and #5.1.1
# XXX should these be used instead?
#
sub cleanhostreply($ $ $ $) {
   my ($hostreply,$relay,$recip,$domain) = @_;

   my $fmtdhost = "";
   my ($r1, $r2, $host, $event);

   #print "RELAY: $relay, RECIP: $recip, DOMAIN: $domain\n";
   #print "HOSTREPLY: \"$hostreply\"\n";
   if (($host,$r1) = ($hostreply =~ /host (\S+) said: $re_DSN[\- ]"?(.*)"?$/o)) {
      # Strip recipient address from host's reply - we already have it in $recip.
      $r1 =~ s/[<(]?$recip[>)]?\W*//ig;

      # Strip and capture "in reply to XXX command" from host's reply
      if ($r1 =~ s/\s*[(]?(in reply to .* command)[)]?//) {
         $r2 = ": $1";
      }
   }
   elsif ($hostreply =~ /^connect to (\S+): (.*)$/) {
      $host = $1; $r1 = $2;
   }
   elsif ($hostreply =~ /^(delivery temporarily suspended): connect to (\S+): (.*)$/) {
      $host = $2; $r1 = "$1: $3";
   }

   elsif (($event,$host,$r1) = ($hostreply =~ /(lost connection|conversation) with (\S+) (.*)$/)) {
      $r1 = "$event $r1";
   }
   else {
      $r1 = $hostreply;
   }

   #print "R1: $r1, R2: $r2\n";
   if ($host =~ /^$/) {
      if ($relay =~ /([^[]+)\[($re_IP)\]/o) {
         $fmtdhost = formathost($2,$1);
      }
   }
   elsif ($host =~ /^([^[]+)\[($re_IP)\]/o) {
      $fmtdhost = formathost($2,$1);
   }
   else {
      $fmtdhost = $host;
   }

   # Coerce some uniformity upon the numerous forms of unknown recipients
   if (   $r1 =~ s/^user unknown(; rejecting)?$//i
       or $r1 =~ s/^invalid recipient[ :]//i
       or $r1 =~ s/^unknown user( account)?$//i
       or $r1 =~ s/^recipient unknown$//i
       or $r1 =~ s/^recipient address rejected: (?:undeliverable address: )?(?:no such user|user unknown)?(?: in .* table)?\s*//i
       or $r1 =~ s/^sorry, no mailbox here by that name[.\s]+//i
       or $r1 =~ s/^unknown recipient address(?:[.]| in .* recipient table)?\s*//i
       or $r1 =~ s/^user unknown in .* recipient table\s*//i )
   {
      $r1 = "Unknown recipient address" . ($r1 !~ /^$/ ? $r1 : "");
   }
   $r1 =~ s/for name=$domain //ig;

   return ("\u$r1$r2", $fmtdhost);
}

exit(0);

# vi: shiftwidth=3 tabstop=3 syntax=perl et
