]> gitweb.michael.orlitzky.com - amavis-logwatch.git/commitdiff
Upstream source.
authorMichael Orlitzky <michael@orlitzky.com>
Thu, 24 Aug 2017 11:01:40 +0000 (07:01 -0400)
committerMichael Orlitzky <michael@orlitzky.com>
Thu, 24 Aug 2017 11:01:40 +0000 (07:01 -0400)
Bugs [new file with mode: 0644]
Changes [new file with mode: 0644]
LICENSE [new file with mode: 0644]
Makefile [new file with mode: 0644]
README [new file with mode: 0644]
amavis-logwatch [new file with mode: 0644]
amavis-logwatch.1 [new file with mode: 0644]
amavis-logwatch.1.html [new file with mode: 0644]
amavis-logwatch.conf [new file with mode: 0644]

diff --git a/Bugs b/Bugs
new file mode 100644 (file)
index 0000000..15afe64
--- /dev/null
+++ b/Bugs
@@ -0,0 +1,2 @@
+- max_report_width doesn't fully limit width on all reports; currently
+  it controls the Detailed section output.
diff --git a/Changes b/Changes
new file mode 100644 (file)
index 0000000..d6c9401
--- /dev/null
+++ b/Changes
@@ -0,0 +1,786 @@
+2014-05-01 (version: 1.51.03)
+ - Fix: Update RE for TIMING lines to accomodate new output from amavis 2.8.1 with
+   Unix::Getrusage installed.
+ - Fix: Ignore 2.8.1's RUSAGE report
+ - Fix: increment spamdiscarded category on do_notify_and_quarantine: cutoff, blacklisted.
+ - Fix: Some log lines that were no longer being ignored (minor changes)
+ - Fix: Ignore some DSPAM lines: Thanks: Christian Rößner
+ - Change: Several minor changes in the SADiags section: "SA info: dns: [...]" lines
+   are ignored; canonicalize some PIDs and IDs, replacing them with with generic
+   "<PID>" and "<ID>" strings to allow grouping.
+ - Fix: Handle new amavisd startup messages
+ - Fix: Handle format changes for Amavisd-new 2.9.0
+
+2012-02-27 (version: 1.51.02)
+ - Fix: Ignore case of SPAM-TAG lines. Amavisd-new 2.7.0 changed the
+   SPAM-TAG line to Spam-tag, and changed its log level to 3.  This
+   log entry is used to trigger spam tagged counts.  Users on 2.7.0
+   will have to increase log_level to 3, or change the amavisd-new
+   source code as per the patch located in this message:
+   http://lists.amavis.org/pipermail/amavis-users/2012-January/001163.html
+ - Fix: Ignore more debug/misc log lines
+ - Thanks: Stefan Jakobs
+
+2012-01-14 (version: 1.51.01)
+ - Fix: Removed old references to $re_DSN, $re_QID, $re_DDD (obsoleted
+   and removed from shared packages) which prevented amavis-logwatch
+   from runnning.
+
+2012-01-11 (version: 1.51.00)
+ - Change: License is now the MIT/X-Consortium License:
+   http://www.opensource.org/licenses/mit-license.php
+   This allows re-inclusion into the logwatch project.
+
+2011-07-08 (version: 1.50.04)
+ - Fix: Support 2.7's log_short_templ changes ([:client_addr_port],
+   {actions_performed}).
+ - Fix: Support 2.7's TIMING lines, which now start with "size: "
+ - Fix: Support 2.7's new BAD-HEADER-[:ccat|minor]; The stuff after
+   BAD-HEADER is ignored.
+ - Fix: Ignore log parser compatibility "header_edits_for_quar..." lines.
+ - Fix: Additional minor 2.7 log line changes
+ - Fix: Ignore more debug lines.
+ - Thanks: Michael Orlitzky
+ - Fix: change method of untaint'ing data when evaluating per-recipient
+   boost in Hits; required for newer perl versions.
+
+2010-11-12 (version: 1.50.03)
+ - Fix: Eliminate use of $re_IP, as it is too complicated, expensive, and did
+   not correctly match all valid IPv6 addresses. Thanks: Michael Orlitzky
+ - Fix: Ignore extra entries after tests= in SPAM lines, for the case where the
+   tests are not surrounded by brackets (Maia, and older amavisd versions).
+ - Fix: Ignore more debug lines.
+   Thanks: Stefan Jakobs
+ - Internal: Update URL to Sourceforge.
+
+2010-03-09 (version: 1.50.02)
+ - Fix: tarball was missing html documentation. Thanks: Michael Orlitzky
+
+2010-03-02 (version: 1.50.01)
+ - New: Allow enabling/djsabling the summary section in config file, using
+   var $postfix_Show_Summary and command line option --[no]summary.
+   Thanks: Benedikt Bohm
+ - Change: Ignore all 'wbl' log lines, until they cannot be meaningfully
+   summarized.
+ - Fix: Score frequencies buckets were not sorted correctly, causing
+   erroneous bucket tallies.  Thanks: Victor Hugo dos Santos
+ - Fix: Ignored lines are placed into a list at runtime instead of
+   at compile time.  Works around perl bug #56202 I discovered
+   while working on postfix-logwatch.
+ - Fix: Support continuation lines that include the alert markers
+   (!) and (!!).
+ - Fix: Skip RUSAGE reporting lines
+ - Fix: Ignore more debug lines.
+ - Thanks: Chris Burton, Gabriele Beltrame, Jernej Porenta,
+   Danny (aka "Technik"), Ricardo Stella, Armin Tueting
+
+2009-07-19 (version: 1.50.00)
+ - New: MalwareToSpam report shows whether or not amavis turned
+   a virus scanner infected result into a spam score, and the score
+   assigned.
+ - New: Handle some new logging messages from amavisd 2.6.3.
+ - New: Counter for truncated messages passed to SA (amavis >= 2.6.3).
+ - New: Group white/blacklisted sender addrs by domain for limiting
+ - New: Section SADiags for amavis reported SpamAssassin diagnostics
+   (log lines started with "SA warn", "SA info", etc.).
+ - Change: Group official vs. unofficial ClamAV detections in
+   MalwareByScanner (and MalwareToSpam) reports.
+ - Change: Level of detail now shown in Detail section's title.
+ - Fix: Patch to ignore "storage and lookups will..." messages
+   when more than one SQL server is in use.
+ - Fix: Don't collect SpamScores (for the spam score frequencies and
+   spam score percentiles reports) when not necessary.
+ - Fix: Better diagnostic when limiter level is not specified on
+   the command line using the --limit option (eg:
+   "--limit malwarebyscanner" v. "--limit malwarebyscanner=10").
+ - Fix: Coerce previously unmatched "unknown bad table name" log
+   line into Warning section.
+ - Fix: Divide by zero error in print_summary_report(), found in
+   test case.
+ - Fix: Remove duplicates from test=[xxx,...], which occurs in
+   amavis < 2.6.2 when using @virus_name_to_spam_score_maps, which
+   would be seen in SpamAssassin Rule Hits reports as double-counted
+   AV:xxx hits.
+ - Fix: Routine to strip out trace information was too greedy.
+ - Fix: handle p0f lines w/short client info (eg. MYNETWORKS).
+ - Fix: Ignore "Error reading mail header section", which is diag
+   output for a condition created by a perl getline() bug.
+ - Fix: Handle other syslog formats.
+ - Fix: Update some missing options and information in man page.
+ - Fix: Ignore more debug lines.
+ - Internal: Ignored lines are now placed into a list, instead of
+   hard coded into the code.  In the future, this will allow users
+   to configure ignore patterns without making code modifications.
+ - Thanks: Jernej Porenta, Jerzy Sakol, Chris Burton, Armin T�ting
+
+2008-11-13 (version: 1.49.09)
+ - Fix: Work around minor amavis logging error which produces
+   empty _WARN|_DIE lines.  Thanks: Rob Sterenborg and Chris Burton
+ - Fix: remove debugging print which is likely never reached.
+
+2008-06-25 (version: 1.49.08)
+ - Fix: White and blacklisted was broken.  Added soft-hard
+   distinction to reports.
+ - Fix: Ignore more debug log lines.
+ - Thanks: Brendan
+ - Fix: debug keyword unmatched had incorrect value assigned,
+   disabling the ability to output the unaltered UNMATCHED debug
+   lines using --debug unmatched.
+ - Fix: The counter "Spam tagged" was being incremented for
+   SPAM lines (it should only increment on SPAM-TAG lines).
+   Thanks: Armin T�ting
+
+2008-06-24 (version: 1.49.07)
+ - New: Added p0f (passive fingerprinting) hits by contents type,
+   OS, and IP address in Detail section.  Controlled by level
+   limiter variable 'p0f', which is set to level 2 by default
+   (this shows OS genre, but suppresses further by-IP address
+   breakdown); increase to level 3 for per-IP breakdown.
+   This section may be moved into a suppliemental report later.
+ - Fix: Ignore more debug log lines.  Thanks: Brendan
+
+2008-06-18 (version: 1.49.06)
+ - New: Include hit counts after each key name in Timings report.
+ - New: SpamAssassin timings percentiles report.  Options:
+   sa_timings, sa_timings_percentiles.  Requires amavis version
+   2.6+ and SpamAssassin 3.3+.
+ - Change: Scan Times percentiles report units have been changed to
+   milliseconds rather than seconds.
+ - Change: Removed the "Hypothetical" row from timings reports; it
+   was not very meaningful.
+
+2008-06-11 (version: 1.49.05)
+ - New: Reduce some noise in smtpresponse section
+ - Thanks: Robert Brooks
+
+2008-06-10 (version: 1.49.04)
+ - New: Show count of Spam tagged messages in Summary.
+ - Fix: The fix in 1.49.02 of treating SPAMMY as ham was incorrect.
+   SPAMMY is spam, not ham.
+ - Fix: Removed extraneous spaces from end of log lines, left by
+   some syslog implementations.
+ - Fix: Ignore SQL reconnecting "NOTICE: reconnecting in response to..."
+ - Fix: Ignore "FuzzyOcr: Skipping ..., image too small" lines for now.
+ - Fix: Better handle when name field in MIME part is only whitespace
+ - Thanks: Robert Brooks
+
+2008-06-08 (version: 1.49.03)
+ - New: Count of penpals saved from kill reported in Summary.
+ - Fix: Include $amavis_Line_Style in config file.
+ - Fix: Clear start info earlier if "logging initialized" line is
+   present.  Otherwise, consider "starting.  " as the first line.
+ - Fix: Some startinfo output was occuring when --nodetail was set.
+ - Fix: Supplemental sections could not be re-enabled after using
+   --nodetail.
+ - Fix: SMTP: connection lines may not include size, which causes
+   unmatched lines. Thanks: Stefan F�rster
+ - Fix:  In the above case, the number of bytes scanned is not available.
+   Byte scanned is now calculated from the size data available in the
+   Passed/Blocked line.  Note: this number is slightly smaller than
+   that reported in the LMTP/SMTP connection line, so the total bytes
+   scanned number will be marginally reduced.
+
+2008-05-29 (version: 1.49.02)
+ - Change: level limiters are no longer unique command line options,
+   but are now parameters to the single option "--level" or "-l".
+   This reduces the number of command line options in the help list,
+   and simplifies the code added to support reject_reply_patterns.
+   For example, the config file level limiter $amavis_SpamBlocked = 2,
+   the command line option would be --limit spamblocked=2 or
+   -l SpamBlocked=2, rather than the previous --spamblocked=2.
+   This also means the --no variants for level limiters are gone
+   (eg. --nosent; instead use -l SpamBlocked=0).  There is no change
+   within the configuration files.  Limiters can still be abbreviated
+   so long as they are unambiguous.
+ - Change: dccproc is included in syslog_name in standalone mode.
+ - Change: nodsnsent* variants merged into single dsnsuppressed section,
+   which is now included in Detail section.
+ - Change: Moved total message scan counts/bytes to the top of the
+   Summary report.
+ - Change: Spam score percentiles report broken out by contents
+   category: unchecked, spam, and ham.  SA is not called when contents
+   category is VIRUS or BANNED; UNCHECKED may have a spam score, and
+   anything less than SPAM is considered ham.   Note: the count of
+   ham in the score reports may be less than the ham count shown in
+   the Summary section, as SA may have been bypassed (see the
+   SpamAssassin bypassed count in the summary report).
+   Encouraged by: Hannes Erven
+ - Change: Passed/Blocked sections are now ordered by priority, as 
+   defined by amavis: VIRUS, BANNED, UNCHECKED, SPAM, SPAMMY, BADH,
+   OVERSIZED, MTA, and CLEAN.
+ - New/Change: Summary section now includes both Passed/Blocked and
+   Ham/Spam breakdowns.  Previously, only a Ham/Spam breakdown was
+   present, and this was insufficient to accurately show how messages
+   were classified.  Passed/Blocked is the first grouping shown in
+   Summary section, and the Spam/Ham/etc. grouping is the second.
+   This sub-section can be suppressed with via --noby_ccat_summary or
+   amavis_show_by_ccat_summary = No.
+ - New: Option --line_style instructs how to handle lines lengths
+   longer than max_report_width.  Options are "wrap", "full", or
+   "truncate" (default).  The older --detail >= 11 is equivalent
+   to line_style=full; line_style=truncate or line_style=wrap has
+   higher precedence and will dictate how long lines are handled.
+   Option abbreviations are --truncate, --wrap, or --full.
+ - New: Single letter options for some long options; run with
+   --help to see list.  More may be added in the future.
+ - New: Beginning of support for amavis 2.6 log entries.
+   Bounce killer sections: bouncekilled, bouncerescued, and
+   bounceunverifiable.
+ - New: VirusScanSkipped section.
+ - New: Include hit counts in spam score percentiles and spam score
+   frequency reports.  Score is adjacent to row header.
+ - Fix: In spam score reports, spam scores are collected for any
+   contents category; previously, only ham and spam categories
+   were used.  If a score exists, it is used.
+ - Fix: SPAMMY was being treated as spam instead of ham.
+ - Fix: Passed UNCHECKED lines were reported as Unmatched, due to
+   spelling error (UNCHECED v. UNCHECKED) in RE.
+ - Fix: At detail > 10, log lines were truncated to max_report_width.
+ - Fix: Ignore dkim lines until better report is created.
+ - Fix: Ignore more debug log lines.
+   Thanks: Armin T�ting
+ - Fix: eliminate some extraneous newline output.
+ - Internal: debug output is now controlled by keywords.
+
+2008-03-17 (version: 1.49.01pre1)
+ - New: Option sect_vars shows names of section configuration
+   variables/command line options in titles of Detail report sections.
+   This allows easy correlation of corresponding configuration file
+   variables/command line option for each section. [ Default: off ]
+ - New: amavis-logwatch man page created.
+ - New: Threshold limiting and Top N lists for every level in each
+   Detail section.  Every level in each section can now be limited
+   with minimum count thresholds and top N lists.  See the updated
+   README file, the comments in the amavis-logwatch.conf file, and
+   the new amavis-logwatch man page.
+ - New: Added Defanged and DefangError sections
+ - New: Autolearn report available when autolearn entries exist. This
+   feature requires enabling autolearn entries in amavis' $log_templ
+   log template, at the end of the amavis program or by proper 
+   configuration of $log_templ in your amavisd.conf file.
+   Encouraged by: "Cube"
+ - Change: All recipients are now show when multiple recipients are
+   present.  Previously, only the first recipient was shown.  The
+   new option --first_recip_only will enable the previous behavior.
+ - Change: Timings percentiles report title and summary labels
+ - Change: Taint mode is now on by default in standalone mode.  It is
+   disabled upon installation in logwatch mode, as logwatch fails with
+   taint mode enabled.
+ - Change: Summary report format has been changed (once again).
+ - Fix: Significantly reduce memory footprint when detail < 5.
+ - Fix: Usage and --help now correctly show only Detail section
+   level limiter options that are available.  Previously, summary-
+   only counts were also display as level limiter options.
+ - Fix: configuration file reading code was not properly warning on
+   non-existent files
+ - Fix: use Received ip (%e macro in log_templ) if client IP (%a) is
+   not available.
+ - Fix: ignore diagnostic defang messages.  Thanks: Leon Kolchinsky
+ - Fix: SpamDiscarded messages were being added to TotalSpams, but
+   this is incorrect, as SpamDiscarded is already included in
+   SpamBlocked.  SpamBlocked is now reduced by SpamDiscarded, and
+   TotalSpams is correct again.
+ - Fix: Ignore more level 3+ log lines.
+ - Fix: unmatched SPAM-TAG lines when no quarantine enabled.
+   Thanks: Paul Dulaba
+ - Fix: sabypassed section was being double counted due to spam-scan
+   lines
+ - Fix: report header was one character too wide
+ - Internal: Converted source to be package-based to allow code sharing
+   with postfix-logwatch.  The single-file executable is auto-generated
+   from the packages.
+ - Internal: internal gen_test_log function to create sample log data
+   for testing. Reads '#TD' comments from within amavis-logwatch.
+ - Internal: converted all Section keynames to lowercase to avoid
+   silly case errors due to hash key case differences in Sections,
+   Opts, and Collecting hashes.
+ - Internal: Cleanup Passed/Blocked code
+
+2008-01-25 (version: 1.48.27)
+ - Released
+
+2008-01-19 (version: 1.48.27pre5)
+ - Fix: Relax email address REs
+ - Fix: Prefix unknown data with an asterisk as in postfix-logwatch
+   (eg. "*unknown")
+ - Fix: Generalize capture of "Empty result from ..." to support
+   any AV scanner
+   Thanks: Stefan Jakobs
+ - Fix: Ignore more level 5 log lines.
+
+2008-01-16 (version: 1.48.27pre4)
+ - Fix: Ignore "policy protocol: policy_bank=..." lines, and
+   report as warnings "policy protocol: INVALID..." lines
+   Thanks: Jurek Sakol
+ - Fix: Additional WarningSQL captures.
+ - Fix: Ignore more level 5 log lines.
+
+2008-01-14 (version: 1.48.27pre3)
+ - Fix: Ignore ! leader chars in "FWD via SMTP: ..." and similar.
+   Thanks: Stefan Jakobs
+
+2008-01-14 (version: 1.48.27pre2)
+ - New: SmtpResponse section for various negative and positive
+   SMTP reponse log lines.
+ - New: TmpPreserved section to capture log lines indicating when
+   amavis does not remove temporary directories
+ - Fix: Experimental: Handle more dccproc errors in DccError
+   Thanks: Stefan Jakobs
+ - Fix: ignore "save_info_preliminary..." and "save_info_final ..."
+
+2007-12-14 (version: 1.48.27pre1)
+ - New: Handle "Quarantine release xxx: missing X-Quarantine-ID" log
+   entries as warnings in new section WarningNoQuarantineID.
+ - New: Handle "WARN: address modified ..." log entries as warnings
+   in new section WarningAddressModified.
+
+2007-12-14 (version: 1.48.26)
+ - Fix: Handle empty sender in release from quarantine
+ - Fix: Handle amavis 2.5.3's "extra modules loaded after daemonizing"
+   message
+ - New: AVTimeout is now treated as a warning so it will show at the
+   top of the Summary report.  The scanner that timed out is available
+   in the Detail section.
+ - New: AVConnectFailure, WarningSmtpShutdown, WarningSQL sections.
+   These were previously grouped under the Warnings section; Like the
+   AVTimeout section, these too will also appear at the top of the 
+   Summary report.
+ - Fix: canonicalize several warnings which differed only by process
+   number, amavis id, callback info, etc.  Thanks: Stefan Jakobs
+ - Fix: several Warning Counts variables were inconsistently used.
+ - Fix: avoid useless reporting of Perl's pseudo-modules which can't,
+   be pre-loaded once (eg. those in .../unicore/lib/).
+
+2007-10-17 (version: 1.48.26pre4)
+ - Incompatible Change: some more config variable/option name changes
+   for naming consistency and shortening: ReleasedMsg -> Released,
+   CleanMsgPassed, CleanMsgBlocked -> CleanPassed, CleanBlocked respectively
+ - Change: uncomment all variables in the config file.  This ensures the
+   variables in the config file stay in sync with those used in the 
+   source.
+ - Fix: sync'd incorrect names in config file with source
+   Thanks: Isaac Ordonez
+ - New: Handle "Can't send SIG 0 to process [###]: Operation not permitted..."
+
+2007-10-14 (version: 1.48.26pre3)
+ - Changed: In timings report, always show actual totals instead of totals for
+   the data presented (eg. when --timings was < 100%).
+ - Changed: Total times shown in timings percentiles report previously 
+   were the sum of the percentiles columns.  This represented a cherry-picked,
+   hypothetical set of values.  Now, there are two totals shown: the first
+   is a percentiles report calculated from the actual set of per-message total
+   scan times reported by amavis, and the second is a hypothetical worst/best
+   case, whose percentiles are calculated from simple column summations.
+   Encouraged by: Robert Brooks
+
+2007-10-10 (version: 1.48.26pre2)
+ - New: Spam score frequency report, controlled with option --score_frequency.
+   Suggested by: Robert Brooks
+ - Incompatible Change: To reduce the number of configuration file variables 
+   and command line options, and to be more consistent, several config
+   variables/command line options have changed.  In general, the method now
+   employed is an overloading approach; specifying values for a report
+   section also enables that section.  The "no" variant disables a section,
+   and the new keyword "default" resets options to their internal defaults.
+   Run amavis-logwatch --help to see a list of the new options, and see the
+   comments in the default amavis-logwatch.conf file.
+ - Fix: IP isn't always reported; report "Unknown" in such cases 
+ - Internal: change how options are processed
+ - Internal: parameterize some report formatting
+
+2007-10-02 (version: 1.48.26pre1)
+ - New: show percentiles totals at bottom of percentile report.
+   Suggested by: Robert Brooks
+
+2007-09-21 (version: 1.48.25)
+ - Fix: Ignore "mail checking ended: " and "AM.PDP" lines.  Also, escape
+   a dot in an ignore pattern, and remove duplicate.
+   Thanks: Curtis Doty, Mark Martinec
+
+2007-09-17 (version: 1.48.24)
+ - Fix: Remove excessive blank lines in summary output
+
+2007-09-16 (version: 1.48.23)
+ - Fix: Support SMTP connections to amavis
+ - Experimental: Reformated summary section to better show the relationship
+   between counts, percentages, and totals.  Feedback encouraged.
+
+2007-09-13 (version: 1.48.22)
+ - Fix: no -D option to install in FreeBSD.  Use -d instead in Makefile
+
+2007-09-01 (version: 1.48.21)
+ - Fix: use ask_av and run_av as virus indicators for Malware by Scanner,
+   as previously used virus_scan line does not report accurate information
+   when multiple scanners report different malware names
+ - Fix: ignore "broken pipe (don't worry), retrying" messages
+ - Fix: Directory permission within tarball was not executable
+ - Fix: remove rooted path in md5 file
+ - Fix: Makefile install-logwatch rule was missing a parenthesis
+ - Fix: Makefile updates from Till Mass
+
+2007-08-31 (version: 1.48.20)
+ - Change: Include GPLv2 license
+ - Change: Include version number in tarball file name
+ - Internal: Move CVS log comments to Changes file
+
+2007-08-29 (version: 1.48.19)
+ - New: added --syslog_name ($amavis_Syslog_Name) for installations where
+   syslog name is not "amavis" (eg. /usr/sbin/amavisd).
+   New: ignore additional amavis log entries
+   Thanks: Jordi Espasa Clofent
+
+2007-08-15 (version: 1.48.18)
+ - Internal: change variable Formats to the more obvious named Sections
+
+2007-08-14 (version: 1.48.17)
+ - New: capture "skip local delivery..." messages, controlled with
+   LocalDeliverySkipped variable/option.  Thanks Andrew M. Kinnard
+ - Output help and version info on STDOUT for easier pipelines to a pager
+ - Internal: split printReports into printSummaryReport and printDetailReport
+
+2007-08-13 (version: 1.48.16)
+ - New: enable detail output for clean blocked messages
+ - Fix: disable startinfo report when --nodetail is given
+
+2007-08-04 (version: 1.48.15)
+ - New: option --nodetail zeros out all detail levels, to more easily obtain
+   only specified detailed reports (eg: --nodetail --spamblocked=4) will
+   show only spam blocked (to level 4) in the details section.
+ - New: option --nosummary disables the summary section
+ - New: detailed section command line arguments can now be specified with the
+   prefix "no", to set the level to 0 (eg. --nospampassed is equivalent to
+   --spampassed 0).
+
+2007-08-03 (version: 1.48.14)
+ - New: option --config_file allows specifying a configuration file via
+   command line. Options in configuration file act as though they were
+   set on the command line in order, with ealier settings being over-
+   ridden by most recent settings.  Multiple config_file options may
+   be specified.
+ - Fix: Yes/True and No/False config values weren't being read properly
+   in standalone
+
+2007-08-02 (version: 1.48.13)
+ - Ignore "storage and lookups will use the same connection to SQL"
+ - Support bracketed IP in LMTP messages (eg. "LMTP:[127.0.0.1]:10026...")
+ - Thanks John Beaver
+
+2007-07-13 (version: 1.48.12)
+ - FreeBSD change in v1.48.11 botched non-freeBSD systems
+
+2007-07-13 (version: 1.48.11)
+ - Support FreeBSD (<facility.priority> preceeds hostname in syslog)
+
+2007-07-06 (v1.48.10)
+ - Capture and summarize cabextract messages
+
+2007-07-05 (v1.48.9)
+ - Generalize archive extraction problem messages.   The new variable
+   amavis_ArchiveExtract replaces amavis_EmptyMember and EncryptedArchive.
+ - Generalize some fatal and warning messages
+ - Capture and summarize more MAIA and MySQL entries
+ - Add MAIA Fake Sender section
+
+2007-07-03 (v1.48.8)
+ - Experimental: syntax to limit Top N level 1 output lines.  Variables
+   that control depth levels in detailed reports can be specified as
+   m.n, where m is the maximum level to output, and n specifies the number
+   of level 1 items output.   Eg: $amavis_SpamBlocked=2.10, will output
+   the top 10 level 1 items, with each item providing 2 levels of detail.
+ - Capture "Config files read"
+
+2007-07-03 (v1.48.7)
+ - Reorganized code; favors newer amavis releases
+ - Show scores assigned to each SA test in spam/ham reports
+ - Catch and report file(1) bad/extra output messages
+ - increased width of timing report for maia
+ - Add additional ccats MTA-BLOCKED, OVERSIZED, OTHER
+ - Support older amavis entries (as in maia)
+
+2007-06-07
+ - Changed NoSubject title to more accurately indicate Subject header insertion
+
+2007-05-31
+ - Fix bug which caused config file to be required in standalone mode
+
+2007-05-28
+ - Added content-type section (log_level >= 2)
+ - Added SpamAssassin bypassed count summary ($sa_mail_body_size_limit)
+ - Only print report headers for SA rules report when data exists
+ - Fix end points of percentiles calculations
+ - In percentile reports, when N% is requested for output, show *at least*
+   N% (previously was *at most*, which might have produced no values)
+ - NOTE: Some older amavis log entries may be flagged as Unmatched, as code
+   restructuring now favors recent amavis releases.  Please report any
+   Unmatched entries and I will correct the problems immediately.
+
+2007-05-27
+ - Can't ignore all do_quarantine_and_notify messages; one indicates quarantine cutoff level
+ - Additional continuation line fix; several log entries end with "..." and are not
+   continuation lines, causing subsequent log line to be ignored.  Continuation lines are
+   exactly 980 chars - see comments in source.
+ - Internal: Group similar messages
+ - Add zero-width assertions and use strict IP RE in bycount sort subroutine 
+   to match IP addresses more reliably
+
+2007-05-23
+ - Fix alignment issue in Top N Spam/Ham reports
+ - Provide a better (working) solution to avoid the SPAM/SPAM-TAG duplication 
+   mentioned below.
+
+2007-05-18
+ - Allow supplementary reports to be printed even when TotalMsgs is 0;
+   This typically would not occur in practice, but is useful for debugging.
+ - Some lines ending with "..." are not actually continuation lines; treat
+   them as ordinary lines for further consideration (Thanks Eray Aslan)
+ - When both SPAM and SPAM-TAG are present, don't double count (which
+   doubles SA test score accumulators). (Thanks Eray Aslan)
+ - Rework white/blacklisted section; sender is now tracked. (Thanks Dan Horne)
+
+2007-05-15
+ - Revision 1.48  2007/05/16 04:27:17  mrc
+
+2007-05-16
+ - Ignore a few more log lines for log_level 3 
+ - Fixed problem setting SARules config variable; renamed Show_SARules
+
+2007-05-14
+ - Revision 1.47  2007/05/14 17:28:21  mrc
+ - Forgot to update Version string
+
+2007-05-14
+ - Revision 1.46  2007/05/14 17:20:45  mrc
+
+2007-05-08
+ - Minor changes to sync up with postfix-logwatch release
+ - Capture MailZu quarantine release messages
+
+2007-05-07
+ - Handle amavis' Hits that includes, but does not add/subtract boost
+   scores (eg. 1.03-3.5)
+ - The option --timing_percentiles was not being recognized, due to name
+   change from --percentiles.  Both are recognized.
+
+2007-05-04
+ - Ignore "The amavisd daemon is already running" messages
+
+2007-05-02
+ - Reworked the Startup info section.  Only shows at detail 10 by default
+
+2007-05-01
+ - Add Ham / Spam hits summary
+ - Experimental: Show Top N Spam/Ham SA rules hit
+ - Minor SPAM/SPAM-TAG RE update
+
+2007-04-28
+ - Experimental: Show hit Bayesian buckets
+ - Experimental: Show hit SA tests
+ - More cleanup (refactor common code, replace most global variables with lexicals,
+   lowercase non-global variable names, shorten variable names, etc.)
+
+2007-04-25
+ - Renamed script to "amavis-logwatch" to avoid confusion when running
+   in standalone mode.  See the README.
+ - Experimental: Show spam score percentiles.
+ - Additional standalone features. Try: logwatch_amavis/amavis --help
+
+2007-04-24
+ - Minor RE adjustment for amavis 2.5.0
+ - Report SA version when shown in INFO line (amavis 2.5.0)
+ - Top N percent of Timings report is now configurable (amavis_Timings)
+ - Display of startup info can be disabled (amavis_StartInfo)
+
+2007-03-31
+ - Capture SMTP shutdown messages
+ - Capture and report TEMPFAIL messages
+ - Updated comments regarding IPv6 from Mark Martinec
+ - Support for running in standalone mode (independent of logwatch)
+
+2007-03-27
+ - Revision 1.45  2007/03/27 01:09:48  mrc
+
+2007-03-25
+ - Handle multiple viruses in list in Malware by Scanner
+
+2007-03-15
+ - Handle multiple recipients in Release from quarantine
+ - Swap From and To keys in Release from quarantine, to handle multiple
+   recipients
+
+2007-03-03
+ - add inc_unmatched subroutine for easier debug of unmatched lines
+
+2007-03-01
+ - Fix problem in sort routine where IP addresses were being captured
+   anywhere in an output line for comparison via pack 'C4' - only
+   attempt IP comparison if an IP address is the start of an output line.
+
+2007-02-27
+ - Revision 1.44  2007/02/27 20:31:42  mrc
+ - New section for Passed|Blocked UNCHECKED
+ - Change summary output criteria to check for any non-zero Totals
+
+2007-02-17
+ - Revision 1.43  2007/02/17 18:43:39  mrc
+ - Ensure no output occurs when nothing is captured
+ - Sync up the shared routines with the postfix code
+ - Made maximum report width configurable in amavis.conf
+ - All lines in report now obey max report width
+ - Warn about dccproc unable to open map error
+
+2007-02-16
+ - Revision 1.42  2007/02/16 06:21:20  mrc
+
+2007-02-11
+ - Capture and summarize Bad Address Syntax message
+   (in postfix, can occur when strict_rfc821_envelopes is not set)
+
+2007-02-08
+ - Group similar BAD HEADER messages and remove From key
+
+2007-02-07
+ - Revision 1.41  2007/02/07 04:19:43  mrc
+
+2007-02-05
+ - Update IPv6 RE to accept amavis' "IPv6:" prefix
+ - Liberalize several REs to better capture policy bank names
+ - Ignore lines:
+     "OS_fingerprint:"
+     "run_as_subprocess: child process... "
+     "Sophie broken pipe ..."
+     "penpals: (bonus|prev Subject:|this Subject:"
+     "virus_scan: \(bad jpeg: Invalid marker segm len"
+ - Ignore lines from upcoming 2.5.0 version:
+     "adding SA score \S+ to existing"
+     "Turning AV infection into a spam report:"
+ - Correct bad syntax of "$#{@$aref}" with "$#$aref"
+ - Enable "use strict"
+ - Declare global vars
+ - Thanks: Mark Martinec
+
+2007-02-02
+ - Fixed Banned RE, which would fail to capture a banned filename
+   that contained an right paren.
+
+2007-02-02
+ - Revision 1.40  2007/02/02 18:41:09  mrc
+
+2007-02-01
+ - Revision 1.39  2007/02/01 20:16:52  mrc
+ - Removed inadvertent debug prints, and extra newline from timing reports.
+
+2007-02-01
+ - Revision 1.38  2007/02/01 19:58:50  mrc
+ - Changed amavis timing report to display percentiles
+   instead of mean.  Percentiles are user configurable.
+   Suggested by: Mark Martinec
+
+2007-02-01
+ - Removed Min/Max columns, and replaced them instead
+   by adding 0 and 100 percentiles to the default list.
+ - Allow for multiple continuation lines
+ - Fixed issue with unmatched lines that contain a %
+
+2007-01-31
+ - Revision 1.37  2007/01/31 06:38:43  mrc
+ - Minor updates for amavis 2.4.5 log messages changes
+ - Capture and summarize panic messages in new Panic section
+
+2007-01-30
+ - Revision 1.36  2007/01/31 02:50:15  mrc
+ - Provide amavis timing report (detail >= 5)
+ - Correctly capture amavis log continuation lines
+ - Capture additional ClamAV warnings
+
+2007-01-27
+ - Revision 1.35  2007/01/27 20:29:21  mrc
+ - Pin important errors to top of report
+ - Handle IPv6 addresses
+ - Thanks: Harald Geiger, Geert Janssens, Leon Kolchinsky, Mark Richards
+
+2007-01-26
+ - Add Recipient key in Malware Blocked section
+ - Lowercase Recipient email addresses
+
+2007-01-24
+ - Added Malware by Scanner section
+
+2007-01-23
+ - Capture and summarize:
+     All "(!+)..." debug lines, placed in warning section
+     DSN Notification debug lines
+
+2007-01-22
+ - Capture and summarize:
+     "(!)WARN: ..." lines
+     "INFO: truncated N header line(s) longer than 998 characters"
+ - Ignore "...email.txt no longer exists, can't re-use it" lines
+
+2007-01-19
+ - Use same tree-building and reporting code as postfix filter
+ - Detailed summary lines are sorted first by count, then by IP and lexically
+ - Provide ability to configure per section maximum detail
+ - Added bytes scanned summary
+ - Ignore additional log lines:
+     "Waiting for the process [NNN] to terminate"
+     "do_notify_and_quarantine: rec_ccat"
+     "Valid PID file (younger than sys uptime ..."
+     "Sending SIGxxx to amavisd"
+     "Daemon [NNN] terminated by SIG..."
+ - Capture and report on missed or ignored log lines
+     additional "SA TIMED OUT" messages
+     A/V timeouts
+     encrypted archive members
+    "logging initialized, log level N, syslog: amavis.mail"
+ - Make "extra code modules loaded" report consistent with others
+ - Spam discarded (not quarantined) percentage is now shown as
+   percentage of Total scanned instead of Spam blocked
+
+# old logwatch amavis CVS log
+
+Revision 1.34  2006/11/12 18:39:34  bjorn
+Change by MrC:
+- Catches more amavis continuation lines
+- Reports on mail released from quarantine via "amavisd-release"
+- Reports supplemental minor bad headers available in amavisd 2.4.4+
+- Suppresses empty detail lines in generic doReport routine
+
+Revision 1.33  2006/10/16 22:19:05  mike
+Amavis patch for protocols from Mike Cappella -mgt
+
+Revision 1.32  2006/07/06 00:25:00  bjorn
+Additional filtering and counting, by Geert Janssens
+
+Revision 1.31  2006/06/29 16:02:05  bjorn
+Corrected two previous log entries.
+
+Revision 1.30  2006/06/27 15:17:53  bjorn
+Improved parsing and counting from Geert Janssens.
+
+Revision 1.29  2006/06/24 16:06:12  bjorn
+Major rewrite from Mike Cappella.
+
+Revision 1.28  2006/05/26 18:32:50  bjorn
+Additional regexp adjustment, by 'Who Knows'.
+
+Revision 1.27  2006/05/21 18:17:41  bjorn
+Complies with recent amavis releases, by Who Knows.
+
+Revision 1.26  2006/04/02 17:26:52  kirk
+fixed spelling error
+
+Revision 1.25  2006/01/29 23:52:53  bjorn
+Print out virus names and sender, by Felix Schwarz.
+
+Revision 1.24  2005/12/07 19:15:56  bjorn
+Detect and count timeouts, by 'Who Knows'.
+
+Revision 1.23  2005/11/30 05:34:10  bjorn
+Corrected regexp with space, by Markus Lude.
+
+Revision 1.22  2005/11/22 18:34:32  bjorn
+Recognize 'Passed' bad headers, by "Who Knows".
+
+Revision 1.21  2005/10/26 05:40:43  bjorn
+Additional patches for amavisd-new, by Mike Cappella
diff --git a/LICENSE b/LICENSE
new file mode 100644 (file)
index 0000000..1be1fb0
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,17 @@
+##########################################################################
+# Amavis-logwatch: written and maintained by:
+#
+#    Mike "MrC" Cappella <mike (at) cappella (dot) us>
+#      http://logreporters.sourceforge.net/
+#
+# Please send all comments, suggestions, bug reports regarding this
+# program/module to the email address above.  I will respond as quickly
+# as possible. [MrC]
+#
+#######################################################
+### All work since Dec 12, 2006 (logwatch CVS revision 1.28)
+### Copyright (c) 2006-2012  Mike Cappella
+###
+### Covered under the included MIT/X-Consortium License:
+###    http://www.opensource.org/licenses/mit-license.php
+##########################################################################
diff --git a/Makefile b/Makefile
new file mode 100644 (file)
index 0000000..f5d7b78
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,65 @@
+program = amavis-logwatch
+program_logwatch = amavis
+
+src = $(program) \
+      $(program).conf \
+      $(program).1 \
+      $(program).1.html \
+      Changes Bugs Makefile README LICENSE
+
+prefix_logwatch = /etc/logwatch
+prefix_standalone = /usr/local
+
+scriptdir_logwatch   = $(prefix_logwatch)/scripts/services
+confdir_logwatch     = $(prefix_logwatch)/conf/services
+
+scriptdir_standalone = $(prefix_standalone)/bin
+confdir_standalone   = $(prefix_standalone)/etc
+mandir_standalone    = $(prefix_standalone)/man/man1
+
+toolsdir             = ../tools
+
+INSTALL = /usr/bin/install -c
+
+install:
+       @echo 'Run "make install-logwatch" to install as a logwatch filter'
+       @echo 'Run "make install-standalone" to install as a standalone utility'
+       @echo 'Run "make install-all" to install both'
+
+install-logwatch:
+       $(INSTALL) -d -m 0755 $(DESTDIR)$(scriptdir_logwatch) $(DESTDIR)$(confdir_logwatch) || exit 1;
+       $(INSTALL) -m 0644 $(program) $(DESTDIR)$(scriptdir_logwatch)/$(program_logwatch) || exit 1;
+       $(INSTALL) -m 0644 $(program).conf $(DESTDIR)$(confdir_logwatch)/$(program_logwatch).conf || exit 1;
+       # removes taint mode (-T) to run under logwatch
+       perl -e 'while (<>) { $$.==1 and s/\s+-T$$//; print "$$_";}' -i $(DESTDIR)$(scriptdir_logwatch)/$(program_logwatch)
+
+install-standalone:
+       $(INSTALL) -d -m 0755 $(DESTDIR)$(scriptdir_standalone) $(DESTDIR)$(confdir_standalone) || exit 1;
+       $(INSTALL) -m 0755 $(program) $(DESTDIR)$(scriptdir_standalone)/$(program) || exit 1;
+       $(INSTALL) -m 0644 $(program).conf $(DESTDIR)$(confdir_standalone)/$(program).conf || exit 1;
+       $(INSTALL) -m 0644 $(program).1 $(DESTDIR)$(mandir_standalone)/$(program).1 || exit 1;
+
+install-all: install-logwatch install-standalone
+
+uninstall-logwatch:
+       -rm $(DESTDIR)$(scriptdir_logwatch)/$(program_logwatch) $(DESTDIR)$(confdir_logwatch)/$(program_logwatch).conf
+
+uninstall-standalone:
+       -rm $(DESTDIR)$(scriptdir_standalone)/$(program) $(DESTDIR)$(confdir_standalone)/$(program).conf \
+            $(DESTDIR)$(mandir_standalone)/$(program).1
+
+uninstall-all: uninstall-logwatch uninstall-standalone
+
+release: program htmlpage
+       vers=`egrep 'Version[ ]*=' $(program) | sed "s/.*'\(.*\)';/\1/"` ; \
+       echo Preparing version $$vers; \
+       rel=$(program)-$$vers ; \
+       tar -czvf $${rel}.tgz --group=0 --owner=0 --mode=644 --transform=s",^,$${rel}/," $(src) ; \
+       md5sum $${rel}.tgz  > $${rel}.tgz.md5 ; \
+       chmod 644 $${rel}.tgz  $${rel}.tgz.md5 
+
+program:
+       $(toolsdir)/build_from_modules $(program) >| $(program);
+
+htmlpage:
+       groff -m mandoc -T ascii $(program).1 | $(toolsdir)/man2html -t 'Man page: $(program)(1)' >| $(program).1.html;
diff --git a/README b/README
new file mode 100644 (file)
index 0000000..b9e549d
--- /dev/null
+++ b/README
@@ -0,0 +1,90 @@
+##### Overview
+
+The amavis-logwatch(1) utility is an Amavisd-new log parser, that pro-
+duces summaries, details, and statistics regarding the operation of
+Amavisd-new.
+
+This utility can be used as a standalone program, or as a Logwatch fil-
+ter module to produce Amavis summary and detailed reports from within
+Logwatch.
+
+A key feature of amavis-logwatch is its ability to produce a very wide
+range of reports with data grouped and sorted as much  as  possible  to
+reduce  noise  and highlight patterns.  Brief summary reports provide a
+quick overview of general  Amavis  operations  and  message  scanning,
+calling out warnings that may require attention.  Detailed reports pro-
+vide easy to scan, hierarchically-arranged and  organized  information,
+with as much or little detail as desired.
+
+##### Installation: standalone
+
+The included Makefile will install the amavis-logwatch utility
+for you.  Run:
+
+    make install-standalone
+
+to install the utility and its configuration file.  Installation
+will default to /usr/local/bin and /usr/local/etc, respectively.
+##### Installation: logwatch
+
+To use amavis-logwatch as a logwatch script, the files:
+    amavis-logwatch
+    amavis-logwatch.conf
+will need to be installed into one of logwatch's known directories.
+To avoid overwriting your existing default logwatch filter files,
+the enclosed files can be installed into the global logwatch
+installation directory, which is typically:
+
+   /etc/logwatch
+
+The included Makefile can be used to install the files into
+/etc/logwatch for you. To install the filter into an existing
+logwatch installation, run:
+
+    make install-logwatch
+
+NOTE: the files amavis-logwatch and amavis-logwatch.conf must be
+renamed (by removing the "-logwatch" suffix), for logwatch to
+function correctly.  The Makefile takes care of this.  The
+Makefile also disables perl's taint mode (-T) when running under
+logwatch.  If you install the files manually, be sure to remove
+the -T from the first line of the amavis filter.
+
+For non-standard installations, you will need to determine your
+global logwatch directory, and define "prefix" in the attached
+Makefile.
+
+Alternatively, you can manually copy the files to their proper
+locations:
+
+    cp amavis-logwatch       /etc/logwatch/scripts/services/amavis
+    cp amavis-logwatch.conf  /etc/logwatch/conf/services/amavis.conf
+    [ remove -T from line 1 of /etc/logwatch/scripts/services/amavis ]
+
+    Optional:
+    cp amavis-logwatch.1     /usr/local/man/man1/amavis-logwatch.1
+
+##### Usage
+
+The amavis-logwatch utility is used standalone as:
+
+   amavis-logwatch /path/to/amavislog
+
+For brief help:
+
+   amavis-logwatch --help
+
+To use within logwatch:
+
+   logwatch --service amavis ...
+
+See the amavis-logwatch(1) man page for complete details, and
+see comments in the postfix-logwatch.conf file for additional
+information.
+
+Mike Cappella
+mike (at) cappella (dot) us
+last updated: 01/11/2012
diff --git a/amavis-logwatch b/amavis-logwatch
new file mode 100644 (file)
index 0000000..4ad07f6
--- /dev/null
@@ -0,0 +1,4067 @@
+#!/usr/bin/perl -T
+
+##########################################################################
+# Amavis-logwatch: written and maintained by:
+#
+#    Mike "MrC" Cappella <mike (at) cappella (dot) us>
+#      http://logreporters.sourceforge.net/
+#
+# Please send all comments, suggestions, bug reports regarding this
+# program/module to the email address above.  I will respond as quickly
+# as possible. [MrC]
+#
+# Questions regarding the logwatch program itself should be directed to
+# the logwatch project at:
+#   http://sourceforge.net/projects/logwatch/support
+#
+#######################################################
+### All work since Dec 12, 2006 (logwatch CVS revision 1.28)
+### Copyright (c) 2006-2012  Mike Cappella
+### 
+### Covered under the included MIT/X-Consortium License:
+###    http://www.opensource.org/licenses/mit-license.php
+### All modifications and contributions by other persons to
+### this script are assumed to have been donated to the
+### Logwatch project and thus assume the above copyright
+### and licensing terms.  If you want to make contributions
+### under your own copyright or a different license this
+### must be explicitly stated in the contribution an the
+### Logwatch project reserves the right to not accept such
+### contributions.  If you have made significant
+### contributions to this script and want to claim
+### copyright please contact logwatch-devel@lists.sourceforge.net.
+##########################################################
+
+##########################################################################
+# The original amavis logwatch filter was written by
+# Jim O'Halloran <jim @ kendle.com.au>, and has had many contributors over
+# the years.
+#
+# CVS log removed: see Changes file for amavis-logwatch at
+#    http://logreporters.sourceforge.net/
+# or included with the standalone amavis-logwatch distribution
+##########################################################################
+
+package Logreporters;
+use 5.008;
+use strict;
+use warnings;
+no warnings "uninitialized";
+use re 'taint';
+
+our $Version         = '1.51.03';
+our $progname_prefix = 'amavis';
+
+# Specifies the default configuration file for use in standalone mode.
+my $config_file = "/usr/local/etc/${progname_prefix}-logwatch.conf";
+
+#MODULE: ../Logreporters/Utils.pm
+package Logreporters::Utils;
+
+use 5.008;
+use strict;
+use re 'taint';
+use warnings;
+
+BEGIN {
+   use Exporter ();
+   use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
+   $VERSION = '1.003';
+   @ISA = qw(Exporter);
+   @EXPORT = qw(&formathost &get_percentiles &get_percentiles2 &get_frequencies &commify &unitize
+                &get_usable_sectvars &add_section &begin_section_group &end_section_group
+                &get_version &unique_list);
+   @EXPORT_OK = qw(&gen_test_log);
+}
+
+use subs qw (@EXPORT @EXPORT_OK);
+
+
+# Formats IP and hostname for even column spacing
+#
+sub formathost($ $) {
+   # $_[0] : hostip
+   # $_[1] : hostname;
+
+   if (! $Logreporters::Config::Opts{'unknown'} and $_[1] eq 'unknown') {
+      return $_[0];
+   }
+
+   return sprintf "%-$Logreporters::Config::Opts{'ipaddr_width'}s  %s",
+      $_[0] eq '' ? '*unknown' :    $_[0],
+      $_[1] eq '' ? '*unknown' : lc $_[1];
+}
+
+# Add a new section to the end of a section table
+#
+sub add_section($$$$$;$) {
+   my $sref = shift;
+   die "Improperly specified Section entry: $_[0]" if !defined $_[3];
+
+   my $entry  = {
+      CLASS     => 'DATA',
+      NAME      => $_[0],
+      DETAIL    => $_[1],
+      FMT       => $_[2],
+      TITLE     => $_[3],
+   };
+   $entry->{'DIVISOR'}   = $_[4] if defined $_[4];
+   push @$sref, $entry;
+}
+
+{
+my $group_level = 0;
+
+# Begin a new section group.  Groups can nest.
+#
+sub begin_section_group($;@) {
+   my $sref = shift;
+   my $group_name = shift;
+   my $entry  = {
+      CLASS     => 'GROUP_BEGIN',
+      NAME      => $group_name,
+      LEVEL     => ++$group_level,
+      HEADERS   => [ @_ ],
+   };
+   push @$sref, $entry;
+}
+
+# Ends a section group.
+#
+sub end_section_group($;@) {
+   my $sref = shift;
+   my $group_name = shift;
+   my $entry  = {
+      CLASS     => 'GROUP_END',
+      NAME      => $group_name,
+      LEVEL     => --$group_level,
+      FOOTERS   => [ @_ ],
+   };
+   push @$sref, $entry;
+}
+}
+
+# Generate and return a list of section table entries or
+# limiter key names, skipping any formatting entries.
+# If 'namesonly' is set, limiter key names are returned,
+# otherwise an array of section array records is returned.
+sub get_usable_sectvars(\@ $) {
+   my ($sectref,$namesonly) = @_;
+   my (@sect_list, %unique_names);
+
+   foreach my $sref (@$sectref) {
+      #print "get_usable_sectvars: $sref->{NAME}\n";
+      next unless $sref->{CLASS} eq 'DATA';
+      if ($namesonly) {
+         $unique_names{$sref->{NAME}} = 1;
+      }
+      else {
+         push @sect_list, $sref;
+      }
+   }
+   # return list of unique names
+   if ($namesonly) {
+      return keys %unique_names;
+   }
+   return @sect_list;
+}
+
+# Print program and version info, preceeded by an optional string, and exit.
+# 
+sub get_version() {
+
+   print STDOUT "@_\n"  if ($_[0]);
+   print STDOUT "$Logreporters::progname: $Logreporters::Version\n";
+   exit 0;
+}
+
+
+# Returns a list of percentile values given a
+# sorted array of numeric values.  Uses the formula:
+#
+# r = 1 + (p(n-1)/100) = i + d  (Excel method)
+#
+# r = rank
+# p = desired percentile
+# n = number of items
+# i = integer part
+# d = decimal part
+#
+# Arg1 is an array ref to the sorted series
+# Arg2 is a list of percentiles to use
+
+sub get_percentiles(\@ @) {
+   my ($aref,@plist) = @_;
+   my ($n, $last, $r, $d, $i, @vals, $Yp);
+
+   $last = $#$aref;
+   $n = $last + 1;
+   #printf "%6d" x $n . "\n", @{$aref};
+
+   #printf "n: %4d, last: %d\n", $n, $last;
+   foreach my $p (@plist) {
+      $r = 1 + ($p * ($n - 1) / 100.0);
+      $i = int ($r);           # integer part
+      # domain: $i = 1 .. n
+      if ($i == $n) {
+        $Yp = $aref->[$last];
+      }
+      elsif ($i == 0) {
+        $Yp = $aref->[0];
+        print "CAN'T HAPPEN: $Yp\n";
+      }
+      else {
+         $d = $r - $i;         # decimal part
+        #p = Y[i] + d(Y[i+1] - Y[i]), but since we're 0 based, use i=i-1
+         $Yp = $aref->[$i-1] + ($d * ($aref->[$i] - $aref->[$i-1]));
+      }
+      #printf "\np(%6.2f), r: %6.2f, i: %6d, d: %6.2f, Yp: %6d", $p, $r, $i, $d, $Yp;
+      push @vals, $Yp;
+   }
+
+   return @vals;
+}
+
+sub get_num_scores($) {
+   my $scoretab_r = shift;
+
+   my $totalscores = 0;
+
+   for (my $i = 0; $i < @$scoretab_r; $i += 2) {
+      $totalscores += $scoretab_r->[$i+1]
+   }
+
+   return $totalscores;
+}
+
+# scoretab
+#
+#  (score1, n1), (score2, n2), ... (scoreN, nN) 
+#     $i   $i+1
+#
+# scores are 0 based (0 = 1st score)
+sub get_nth_score($ $) {
+   my ($scoretab_r, $n) = @_;
+
+   my $i = 0;
+   my $n_cur_scores = 0;
+   #print "Byscore (", .5 * @$scoretab_r, "): "; for (my $i = 0; $i < $#$scoretab_r / 2; $i++) { printf "%9s (%d) ", $scoretab_r->[$i], $scoretab_r->[$i+1]; } ; print "\n";
+
+   while ($i < $#$scoretab_r) {
+      #print "Samples_seen: $n_cur_scores\n";
+      $n_cur_scores += $scoretab_r->[$i+1];
+      if ($n_cur_scores >= $n) {
+         #printf "range: %s  %s  %s\n", $i >= 2 ? $scoretab_r->[$i - 2] : '<begin>', $scoretab_r->[$i], $i+2 > $#$scoretab_r ? '<end>' : $scoretab_r->[$i + 2];
+         #printf "n: $n, i: %8d, n_cur_scores: %8d, score: %d x %d hits\n", $i, $n_cur_scores, $scoretab_r->[$i], $scoretab_r->[$i+1];
+         return $scoretab_r->[$i];
+      }
+
+      $i += 2;
+   }
+   print "returning last score $scoretab_r->[$i]\n";
+   return $scoretab_r->[$i];
+}
+
+sub get_percentiles2(\@ @) {
+   my ($scoretab_r, @plist) = @_;
+   my ($n, $last, $r, $d, $i, @vals, $Yp);
+
+   #$last = $#$scoretab_r - 1;
+   $n = get_num_scores($scoretab_r);
+   #printf "\n%6d" x $n . "\n", @{$scoretab_r};
+
+   #printf "\n\tn: %4d, @$scoretab_r\n", $n;
+   foreach my $p (@plist) {
+  ###print "\nPERCENTILE: $p\n";
+      $r = 1 + ($p * ($n - 1) / 100.0);
+      $i = int ($r);           # integer part
+      if ($i == $n) {
+        #print "last:\n";
+        #$Yp = $scoretab_r->[$last];
+        $Yp = get_nth_score($scoretab_r, $n);
+      }
+      elsif ($i == 0) {
+        #$Yp = $scoretab_r->[0];
+        print "1st: CAN'T HAPPEN\n";
+        $Yp = get_nth_score($scoretab_r, 1);
+      }
+      else {
+         $d = $r - $i;         # decimal part
+        #p = Y[i] + d(Y[i+1] - Y[i]), but since we're 0 based, use i=i-1
+         my $ithvalprev = get_nth_score($scoretab_r, $i);
+         my $ithval     = get_nth_score($scoretab_r, $i+1);
+         $Yp = $ithvalprev + ($d * ($ithval - $ithvalprev));
+      }
+      #printf "p(%6.2f), r: %6.2f, i: %6d, d: %6.2f, Yp: %6d\n", $p, $r, $i, $d, $Yp;
+      push @vals, $Yp;
+   }
+
+   return @vals;
+}
+
+
+
+# Returns a list of frequency distributions given an incrementally sorted
+# set of sorted scores, and an incrementally sorted list of buckets
+#
+# Arg1 is an array ref to the sorted series
+# Arg2 is a list of frequency buckets to use
+sub get_frequencies(\@ @) { 
+   my ($aref,@blist) = @_;
+
+   my @vals = ( 0 ) x (@blist);
+   my @sorted_blist = sort { $a <=> $b } @blist;
+   my $bucket_index = 0;
+
+OUTER: foreach my $score (@$aref) {
+      #print "Score: $score\n";
+      for my $i ($bucket_index .. @sorted_blist - 1) {
+         #print "\tTrying Bucket[$i]: $sorted_blist[$i]\n";
+         if ($score > $sorted_blist[$i]) {
+            $bucket_index++;
+         }
+         else {
+            #printf "\t\tinto Bucket[%d]\n", $bucket_index;
+            $vals[$bucket_index]++;
+            next OUTER;
+         }
+      }
+      #printf "\t\tinto Bucket[%d]\n", $bucket_index - 1;
+      $vals[$bucket_index - 1]++;
+   }
+
+   return @vals;
+}
+
+# Inserts commas in numbers for easier readability
+#
+sub commify ($) {
+    return undef if ! defined ($_[0]);
+
+    my $text = reverse $_[0];
+    $text =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g;
+    return scalar reverse $text;
+}
+
+# Unitize a number, and return appropriate printf formatting string
+#
+sub unitize($ $) {
+   my ($num, $fmt) = @_;
+   my $kilobyte = 2**10;
+   my $megabyte = 2**20;
+   my $gigabyte = 2**30;
+   my $terabyte = 2**40;
+
+   if ($num >= $terabyte) {
+      $num /= $terabyte;
+      $fmt .= '.3fT';
+   } elsif ($num >= $gigabyte) {
+      $num /= $gigabyte;
+      $fmt .= '.3fG';
+   } elsif ($num >= $megabyte) {
+      $num /= $megabyte;
+      $fmt .= '.3fM';
+   } elsif ($num >= $kilobyte) {
+      $num /= $kilobyte;
+      $fmt .= '.3fK';
+   } else {
+      $fmt .= 'd ';
+   }
+
+   return ($num, $fmt);
+}
+
+# Returns a sublist of the supplied list of elements in an unchanged order,
+# where only the first occurrence of each defined element is retained
+# and duplicates removed
+#
+# Borrowed from amavis 2.6.2
+#
+sub unique_list(@) {
+   my ($r) = @_ == 1 && ref($_[0]) ? $_[0] : \@_;  # accept list, or a list ref
+   my (%seen);
+   my (@unique) = grep { defined($_) && !$seen{$_}++ } @$r;
+
+   return @unique;
+}
+
+# Generate a test maillog file from the '#TD' test data lines
+# The test data file is placed in /var/tmp/maillog.autogen
+#
+# arg1: "postfix" or "amavis"
+# arg2: path to postfix-logwatch or amavis-logwatch from which to read '#TD' data
+#
+# Postfix TD syntax:
+#    TD<service><QID>(<count>) log entry
+#
+sub gen_test_log($) {
+   my $scriptpath = shift;
+
+   my $toolname = $Logreporters::progname_prefix;
+   my $datafile = "/var/tmp/maillog-${toolname}.autogen";
+
+   die "gen_test_log: invalid toolname $toolname"  if ($toolname !~ /^(postfix|amavis)$/);
+
+   eval {
+      require Sys::Hostname;
+      require Fcntl;
+   } or die "Unable to create test data file: required module(s) not found\n$@";
+
+   my $syslogtime = localtime;
+   $syslogtime =~ s/^....(.*) \d{4}$/$1/;
+
+   my ($hostname) = split /\./, Sys::Hostname::hostname();
+
+  # # avoid -T issues
+  # delete @ENV{'IFS', 'CDPATH', 'ENV', 'BASH_ENV'};
+
+   my $flags = &Fcntl::O_CREAT|&Fcntl::O_WRONLY|&Fcntl::O_TRUNC;
+   sysopen(FH, $datafile, $flags) or die "Can't create test data file: $!";
+   print "Generating test log data file from $scriptpath: $datafile\n";
+
+   my $id;
+   @ARGV = ($scriptpath);
+   if ($toolname eq 'postfix') {
+      my %services = (
+          DEF   => 'smtpd',
+          bQ    => 'bounce',
+          cN    => 'cleanup',
+          cQ    => 'cleanup',
+          lQ    => 'local',
+          m     => 'master',
+          p     => 'pickup',
+          pQ    => 'pickup',
+          ppQ   => 'pipe',
+          pfw   => 'postfwd',
+          pg    => 'postgrey',
+          pgQ   => 'postgrey',
+          ps    => 'postsuper',
+          qQ    => 'qmgr',
+          s     => 'smtp',
+          sQ    => 'smtp',
+          sd    => 'smtpd',
+          sdN   => 'smtpd',
+          sdQ   => 'smtpd',
+          spf   => 'policy-spf',
+          vN    => 'virtual',
+          vQ    => 'virtual',
+      );
+      $id = 'postfix/smtp[12345]';
+
+      while (<>) {
+         if (/^\s*#TD([a-zA-Z]*[NQ]?)(\d+)?(?:\(([^)]+)\))? (.*)$/) {
+            my ($service,$count,$qid,$line) = ($1, $2, $3, $4);
+
+            #print "SERVICE: %s, QID: %s, COUNT: %s, line: %s\n", $service, $qid, $count, $line;
+
+            if ($service eq '') {
+               $service = 'DEF';
+            }
+            die ("No such service: \"$service\": line \"$_\"")  if (!exists $services{$service});
+
+            $id = $services{$service} . '[123]';
+            $id = 'postfix/' . $id    unless $services{$service} eq 'postgrey';
+            #print "searching for service: \"$service\"\n\tFound $id\n";
+            if    ($service =~ /N$/) { $id .= ': NOQUEUE'; }
+            elsif ($service =~ /Q$/) { $id .= $qid ? $qid : ': DEADBEEF'; }
+
+            $line =~ s/ +/ /g;
+            $line =~ s/^ //g;
+            #print "$syslogtime $hostname $id: \"$line\"\n" x ($count ? $count : 1);
+            print FH "$syslogtime $hostname $id: $line\n" x ($count ? $count : 1);
+         }
+      }
+   }
+   else { #amavis
+      my %services = (
+          DEF   => 'amavis',
+          dcc   => 'dccproc',
+      );
+      while (<>) {
+         if (/^\s*#TD([a-z]*)(\d+)? (.*)$/) {
+            my ($service,$count,$line) = ($1, $2, $3);
+            if ($service eq '') {
+               $service = 'DEF';
+            }
+            die ("No such service: \"$service\": line \"$_\"")  if (!exists $services{$service});
+            $id = $services{$service} . '[123]:';
+            if ($services{$service} eq 'amavis') {
+               $id .= ' (9999-99)';
+            }
+            print FH "$syslogtime $hostname $id $line\n" x ($count ? $count : 1)
+         }
+      }
+   }
+
+   close FH or die "Can't close $datafile: $!";
+}
+
+1;
+
+#MODULE: ../Logreporters/Config.pm
+package Logreporters::Config;
+
+use 5.008;
+use strict;
+use re 'taint';
+use warnings;
+
+
+BEGIN {
+   use Exporter ();
+   use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
+   $VERSION = '1.002';
+   @ISA = qw(Exporter);
+   @EXPORT = qw(&init_run_mode &add_option &get_options &init_cmdline &get_vars_from_file
+                &process_limiters &process_debug_opts &init_getopts_table_common &zero_opts
+                @Optspec %Opts %Configvars @Limiters %line_styles $fw1 $fw2 $sep1 $sep2
+                &D_CONFIG &D_ARGS &D_VARS &D_TREE &D_SECT &D_UNMATCHED &D_TEST &D_ALL
+             );
+}
+
+use subs @EXPORT;
+
+our  @Optspec = ();      # options table used by Getopts
+
+our %Opts = ();         # program-wide options
+our %Configvars = ();   # configuration file variables
+our @Limiters;
+
+# Report separator characters and widths
+our ($fw1,$fw2)   = (22, 10);
+our ($sep1,$sep2) = ('=', '-');
+
+use Getopt::Long;
+
+
+BEGIN {
+   import Logreporters::Utils qw(&get_usable_sectvars);
+}
+
+our %line_styles = (
+   truncate => 0,
+   wrap     => 1,
+   full     => 2,
+);
+
+sub init_run_mode($);
+sub confighash_to_cmdline(\%);
+sub get_vars_from_file(\% $);
+sub process_limiters(\@);
+sub add_option(@);
+sub get_options($);
+sub init_getopts_table_common(@);
+sub set_supplemental_reports($$);
+# debug constants
+sub D_CONFIG ()    { 1<<0 }
+sub D_ARGS ()      { 1<<1 }
+sub D_VARS ()      { 1<<2 }
+sub D_TREE ()      { 1<<3 }
+sub D_SECT ()      { 1<<4 }
+sub D_UNMATCHED () { 1<<5 }
+
+sub D_TEST ()      { 1<<30 }
+sub D_ALL ()       { 1<<31 }
+
+my %debug_words = (
+   config     => D_CONFIG,
+   args       => D_ARGS,
+   vars       => D_VARS,
+   tree       => D_TREE,
+   sect       => D_SECT,
+   unmatched  => D_UNMATCHED,
+
+   test       => D_TEST,
+   all        => 0xffffffff,
+);
+
+# Clears %Opts hash and initializes basic running mode options in
+# %Opts hash by setting keys: 'standalone', 'detail', and 'debug'.
+# Call early.
+#
+sub init_run_mode($) {
+   my $config_file = shift;
+   $Opts{'debug'} = 0;
+
+   # Logwatch passes a filter's options via environment variables.
+   # When running standalone (w/out logwatch), use command line options
+   $Opts{'standalone'} = exists ($ENV{LOGWATCH_DETAIL_LEVEL}) ? 0 : 1;
+
+   # Show summary section by default
+   $Opts{'summary'} = 1;
+
+   if ($Opts{'standalone'}) {
+      process_debug_opts($ENV{'LOGREPORTERS_DEBUG'}) if exists ($ENV{'LOGREPORTERS_DEBUG'});
+   }
+   else {
+      $Opts{'detail'} = $ENV{'LOGWATCH_DETAIL_LEVEL'};
+      # XXX
+      #process_debug_opts($ENV{'LOGWATCH_DEBUG'}) if exists ($ENV{'LOGWATCH_DEBUG'});
+   }
+
+   # first process --debug, --help, and --version options
+   add_option ('debug=s',                   sub { process_debug_opts($_[1]); 1});
+   add_option ('version',                   sub { &Logreporters::Utils::get_version(); 1;});
+   get_options(1);
+
+   # now process --config_file, so that all config file vars are read first
+   add_option ('config_file|f=s',           sub { get_vars_from_file(%Configvars, $_[1]); 1;});
+   get_options(1);
+
+   # if no config file vars were read
+   if ($Opts{'standalone'} and ! keys(%Configvars) and -f $config_file) {
+      print "Using default config file: $config_file\n" if $Opts{'debug'} & D_CONFIG;
+      get_vars_from_file(%Configvars, $config_file);
+   }
+}
+
+sub get_options($) {
+   my $pass_through = shift;
+   #$SIG{__WARN__} = sub { print "*** $_[0]*** options error\n" };
+   # ensure we're called after %Opts is initialized
+   die "get_options: program error: %Opts is emtpy" unless exists $Opts{'debug'};
+
+   my $p = new Getopt::Long::Parser;
+
+   if ($pass_through) {
+      $p->configure(qw(pass_through permute));
+   }
+   else {
+      $p->configure(qw(no_pass_through no_permute));
+   }
+   #$p->configure(qw(debug));
+
+   if ($Opts{'debug'} & D_ARGS) {
+      print "\nget_options($pass_through): enter\n";
+      printf "\tARGV(%d): ", scalar @ARGV;
+      print @ARGV, "\n";
+      print "\t$_ ", defined $Opts{$_} ? "=> $Opts{$_}\n" : "\n"  foreach sort keys %Opts;
+   }
+
+   if ($p->getoptions(\%Opts, @Optspec) == 0) {
+      print STDERR "Use ${Logreporters::progname} --help for options\n";
+      exit 1;
+   }
+   if ($Opts{'debug'} & D_ARGS) {
+      print "\t$_ ", defined $Opts{$_} ? "=> $Opts{$_}\n" : "\n"  foreach sort keys %Opts;
+      printf "\tARGV(%d): ", scalar @ARGV;
+      print @ARGV, "\n";
+      print "get_options: exit\n";
+   }
+}
+
+sub add_option(@) {
+   push @Optspec, @_;
+}
+
+# untaint string, borrowed from amavisd-new
+sub untaint($) {
+   no re 'taint';
+
+   my ($str);
+   if (defined($_[0])) {
+      local($1);            # avoid Perl taint bug: tainted global $1 propagates taintedness
+      $str = $1  if $_[0] =~ /^(.*)$/;
+   }
+
+   return $str;
+}
+
+sub init_getopts_table_common(@) {
+   my @supplemental_reports = @_;
+
+   print "init_getopts_table_common: enter\n"   if $Opts{'debug'} & D_ARGS;
+
+   add_option ('help',                       sub { print STDOUT Logreporters::usage(undef); exit 0 });
+   add_option ('gen_test_log=s',             sub { Logreporters::Utils::gen_test_log($_[1]); exit 0; });
+   add_option ('detail=i');
+   add_option ('nodetail',                   sub { 
+      # __none__ will set all limiters to 0 in process_limiters
+      # since they are not known (Sections table is not yet built).
+      push @Limiters, '__none__';
+      # 0 = disable supplemental_reports
+      set_supplemental_reports(0, \@supplemental_reports);
+   });
+   add_option ('max_report_width=i');
+   add_option ('summary!');
+   add_option ('show_summary=i',             sub { $Opts{'summary'} = $_[1]; 1; });
+   # untaint ipaddr_width for use w/sprintf() in Perl v5.10
+   add_option ('ipaddr_width=i',             sub { $Opts{'ipaddr_width'} = untaint ($_[1]); 1; });
+
+   add_option ('sect_vars!');
+   add_option ('show_sect_vars=i',           sub { $Opts{'sect_vars'} = $_[1]; 1; });
+
+   add_option ('syslog_name=s');
+   add_option ('wrap',                       sub { $Opts{'line_style'} = $line_styles{$_[0]}; 1; });
+   add_option ('full',                       sub { $Opts{'line_style'} = $line_styles{$_[0]}; 1; });
+   add_option ('truncate',                   sub { $Opts{'line_style'} = $line_styles{$_[0]}; 1; });
+   add_option ('line_style=s',               sub {
+      my $style = lc($_[1]);
+      my @list = grep (/^$style/, keys %line_styles);
+      if (! @list) {
+         print STDERR "Invalid line_style argument \"$_[1]\"\n";
+         print STDERR "Option line_style argument must be one of \"wrap\", \"full\", or \"truncate\".\n";
+         print STDERR "Use $Logreporters::progname --help for options\n";
+         exit 1;
+      }
+      $Opts{'line_style'} = $line_styles{lc($list[0])};
+      1;
+   });
+
+   add_option ('limit|l=s',                 sub {
+      my ($limiter,$lspec) = split(/=/, $_[1]);
+      if (!defined $lspec) {
+         printf STDERR "Limiter \"%s\" requires value (ex. --limit %s=10)\n", $_[1],$_[1];
+         exit 2;
+      }
+      foreach my $val (split(/(?:\s+|\s*,\s*)/, $lspec)) {
+         if ($val !~ /^\d+$/ and 
+             $val !~ /^(\d*)\.(\d+)$/ and
+             $val !~ /^::(\d+)$/ and
+             $val !~ /^:(\d+):(\d+)?$/ and
+             $val !~ /^(\d+):(\d+)?:(\d+)?$/)
+         {
+            printf STDERR "Limiter value \"$val\" invalid in \"$limiter=$lspec\"\n";
+            exit 2;
+         }
+      }
+      push @Limiters, lc $_[1];
+   });
+
+   print "init_getopts_table_common: exit\n"   if $Opts{'debug'} & D_ARGS;
+}
+
+sub get_option_names() {
+   my (@ret, @tmp);
+   foreach (@Optspec) {
+      if (ref($_) eq '') {       # process only the option names
+         my $spec = $_;
+         $spec =~ s/=.*$//;
+         $spec =~ s/([^|]+)\!$/$1|no$1/g;
+         @tmp = split /[|]/, $spec;
+         #print "PUSHING: @tmp\n";
+         push @ret, @tmp;
+      }
+   }
+   return @ret;
+}
+
+# Set values for the configuration variables passed via hashref.
+# Variables are of the form ${progname_prefix}_KEYNAME.
+#
+# Because logwatch lowercases all config file entries, KEYNAME is
+# case-insensitive.
+#
+sub init_cmdline() {
+   my ($href, $configvar, $value, $var);
+
+   # logwatch passes all config vars via environment variables
+   $href = $Opts{'standalone'} ? \%Configvars : \%ENV;
+
+   # XXX: this is cheeze: need a list of valid limiters, but since
+   # the Sections table is not built yet, we don't know what is
+   # a limiter and what is an option, as there is no distinction in
+   # variable names in the config file (perhaps this should be changed).
+   my @valid_option_names = get_option_names();
+   die "Options table not yet set" if ! scalar @valid_option_names;
+
+   print "confighash_to_cmdline: @valid_option_names\n"  if $Opts{'debug'} & D_ARGS;
+   my @cmdline = ();
+   while (($configvar, $value) = each %$href) {
+      if ($configvar =~ s/^${Logreporters::progname_prefix}_//o) {
+         # distinguish level limiters from general options
+         # would be easier if limiters had a unique prefix
+         $configvar = lc $configvar;
+         my $ret = grep (/^$configvar$/i, @valid_option_names);
+         if ($ret == 0) {
+            print "\tLIMITER($ret): $configvar = $value\n"  if $Opts{'debug'} & D_ARGS;
+            push @cmdline, '-l', "$configvar" . "=$value";
+         }
+         else {
+            print "\tOPTION($ret): $configvar = $value\n"  if $Opts{'debug'} & D_ARGS;
+            unshift @cmdline, $value  if defined ($value);
+            unshift @cmdline, "--$configvar";
+         }
+      }
+   }
+   unshift @ARGV, @cmdline;
+}
+
+# Obtains the variables from a logwatch-style .conf file, for use
+# in standalone mode.  Returns an ENV-style hash of key/value pairs.
+#
+sub get_vars_from_file(\% $) {
+   my ($href, $file) = @_;
+   my ($var, $val);
+
+   print "get_vars_from_file: enter: processing file: $file\n" if $Opts{'debug'} & D_CONFIG;
+
+   my  $message = undef;
+   my $ret = stat ($file);
+   if ($ret == 0) { $message = $!; }
+   elsif (! -r _) { $message = "Permission denied"; }
+   elsif (  -d _) { $message = "Is a directory"; }
+   elsif (! -f _) { $message = "Not a regular file"; }
+
+   if ($message) {
+      print STDERR "Configuration file \"$file\": $message\n";
+      exit 2;
+   }
+
+   my $prog = $Logreporters::progname_prefix;
+   open FILE, '<', "$file" or die "unable to open configuration file $file: $!";
+   while (<FILE>) {
+      chomp;
+      next if (/^\s*$/);   # ignore all whitespace lines
+      next if (/^\*/);     # ignore logwatch's *Service lines
+      next if (/^\s*#/);   # ignore comment lines
+      if (/^\s*\$(${prog}_[^=\s]+)\s*=\s*"?([^"]+)"?$/o) {
+         ($var,$val) = ($1,$2);
+         if    ($val =~ /^(?:no|false)$/i) { $val = 0; }
+         elsif ($val =~ /^(?:yes|true)$/i) { $val = 1; }
+         elsif ($val eq '')                { $var =~ s/${prog}_/${prog}_no/; $val = undef; }
+
+         print "\t\"$var\" => \"$val\"\n"  if $Opts{'debug'} & D_CONFIG;
+
+         $href->{$var} = $val;
+      }
+   }
+   close FILE         or die "failed to close configuration handle for $file: $!";
+   print "get_vars_from_file: exit\n" if $Opts{'debug'} & D_CONFIG;
+}
+
+sub process_limiters(\@) {
+   my ($sectref) = @_;
+
+   my ($limiter, $var, $val, @errors);
+   my @l = get_usable_sectvars(@$sectref, 1);
+
+   if ($Opts{'debug'} & D_VARS) {
+      print "process_limiters: enter\n";
+      print "\tLIMITERS: @Limiters\n";
+   }
+   while ($limiter = shift @Limiters) {
+      my @matched = ();
+
+      printf "\t%-30s  ",$limiter   if $Opts{'debug'} & D_VARS;
+      # disable all limiters when limiter is __none__: see 'nodetail' cmdline option
+      if ($limiter eq '__none__') {
+         $Opts{$_} = 0 foreach @l;
+         next;
+      }
+
+      ($var,$val) = split /=/, $limiter;
+
+      if ($val eq '') {
+         push @errors, "Limiter \"$var\" requires value (ex. --limit limiter=10)";
+         next;
+      }
+
+      # try exact match first, then abbreviated match next
+      if (scalar (@matched = grep(/^$var$/, @l)) == 1 or scalar (@matched = grep(/^$var/, @l)) == 1) {
+         $limiter = $matched[0];    # unabbreviate limiter
+         print "MATCH: $var: $limiter => $val\n" if $Opts{'debug'} & D_VARS;
+         # XXX move limiters into section hash entry...
+         $Opts{$limiter} = $val;
+         next;
+      }
+      print "matched=", scalar @matched, ": @matched\n" if $Opts{'debug'} & D_VARS;
+
+      push @errors, "Limiter \"$var\" is " . (scalar @matched == 0 ? "invalid" : "ambiguous: @matched");
+   }
+   print "\n" if $Opts{'debug'} & D_VARS;
+
+   if (@errors) {
+      print STDERR "$_\n" foreach @errors;
+      exit 2;
+   }
+
+   # Set the default value of 10 for each section if no limiter exists.
+   # This allows output for each section should there be no configuration
+   # file or missing limiter within the configuration file. 
+   foreach (@l) {
+      $Opts{$_} = 10 unless exists $Opts{$_};
+   }
+
+   # Enable collection for each section if a limiter is non-zero.
+   foreach (@l) {
+      #print "L is: $_\n";
+      #print "DETAIL: $Opts{'detail'}, OPTS: $Opts{$_}\n";
+      $Logreporters::TreeData::Collecting{$_} = (($Opts{'detail'} >= 5) && $Opts{$_}) ? 1 : 0;
+   }
+   #print "OPTS: \n"; map { print "$_ => $Opts{$_}\n"} keys %Opts;
+   #print "COLLECTING: \n"; map { print "$_ => $Logreporters::TreeData::Collecting{$_}\n"} keys %Logreporters::TreeData::Collecting;
+}
+
+# Enable/disable supplemental reports
+# arg1:     0=off, 1=on
+# arg2,...: list of supplemental report keywords
+sub set_supplemental_reports($$) {
+   my ($onoff,$aref) = @_;
+
+   $Opts{$_} = $onoff foreach (@$aref);
+}
+
+sub process_debug_opts($) {
+   my $optstring = shift;
+
+   my @errors = ();
+   foreach (split(/\s*,\s*/, $optstring)) {
+      my $word = lc $_;
+      my @matched = grep (/^$word/, keys %debug_words);
+
+      if (scalar @matched == 1) {
+         $Opts{'debug'} |= $debug_words{$matched[0]};
+         next;
+      }
+
+      if (scalar @matched == 0) {
+         push @errors, "Unknown debug keyword \"$word\"";
+      }
+      else {  # > 1
+         push @errors, "Ambiguous debug keyword abbreviation \"$word\": (matches: @matched)";
+      }
+   }
+   if (@errors) {
+      print STDERR "$_\n" foreach @errors;
+      print STDERR "Debug keywords: ", join (' ', sort keys %debug_words), "\n";
+      exit 2;
+   }
+}
+
+# Zero the options controlling level specs and those
+# any others passed via Opts key.
+#
+# Zero the options controlling level specs in the
+# Detailed section, and set all other report options
+# to disabled. This makes it easy via command line to
+# disable the entire summary section, and then re-enable
+# one or more sections for specific reports.
+#
+#   eg. progname --nodetail --limit forwarded=2
+#
+sub zero_opts ($ @) {
+   my $sectref = shift;
+   # remaining args: list of Opts keys to zero
+
+   map { $Opts{$_} = 0; print "zero_opts: $_ => 0\n" if $Opts{'debug'} & D_VARS;} @_;
+   map { $Opts{$_} = 0 } get_usable_sectvars(@$sectref, 1);
+}
+
+1;
+
+#MODULE: ../Logreporters/TreeData.pm
+package Logreporters::TreeData;
+
+use 5.008;
+use strict;
+use re 'taint';
+use warnings;
+no warnings "uninitialized";
+
+BEGIN {
+   use Exporter ();
+   use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
+   $VERSION = '1.001';
+   @ISA = qw(Exporter);
+   @EXPORT = qw(%Totals %Counts %Collecting $END_KEY);
+   @EXPORT_OK = qw(&printTree &buildTree);
+
+}
+
+use subs @EXPORT_OK;
+
+BEGIN {
+   import Logreporters::Config qw(%line_styles);
+}
+
+# Totals and Counts are the log line accumulator hashes.
+# Totals: maintains per-section grand total tallies for use in Summary section
+# Counts: is a multi-level hash, which maintains per-level key totals.
+our (%Totals, %Counts);
+
+# The Collecting hash determines which sections will be captured in
+# the Counts hash.  Counts are collected only if a section is enabled,
+# and this hash obviates the need to test both existence and
+# non-zero-ness of the Opts{'keyname'} (either of which cause capture).
+# XXX The Opts hash could be used ....
+our %Collecting = ();
+
+sub buildTree(\% $ $ $ $ $);
+sub printTree($ $ $ $ $);
+=pod
+[ a:b:c, ... ]
+
+which would be interpreted as follows:
+
+a = show level a detail
+b = show at most b items at this level
+c = minimun count that will be shown
+=cut
+
+sub printTree($ $ $ $ $) {
+   my ($treeref, $lspecsref, $line_style, $max_report_width, $debug) = @_;
+   my ($entry, $line);
+   my $cutlength = $max_report_width - 3;
+
+   my $topn = 0;
+   foreach $entry (sort bycount @$treeref) {
+      ref($entry) ne "HASH" and die "Unexpected entry in tree: $entry\n";
+
+      #print "LEVEL: $entry->{LEVEL}, TOTAL: $entry->{TOTAL}, HASH: $entry, DATA: $entry->{DATA}\n";
+
+      # Once the top N lines have been printed, we're done
+      if ($lspecsref->[$entry->{LEVEL}]{topn}) {
+         if ($topn++ >= $lspecsref->[$entry->{LEVEL}]{topn} ) {
+            print '     ', '   ' x ($entry->{LEVEL} + 3), "...\n"
+               unless ($debug) and do {
+                     $line = '     ' . '   ' x ($entry->{LEVEL} + 3) . '...';
+                     printf "%-130s L%d: topn reached(%d)\n", $line, $entry->{LEVEL} + 1, $lspecsref->[$entry->{LEVEL}]{topn};
+               };
+            last;
+         }
+      }
+
+      # Once the item's count falls below the given threshold, we're done at this level
+      # unless a top N is specified, as threshold has lower priority than top N
+      elsif ($lspecsref->[$entry->{LEVEL}]{threshold}) {
+         if ($entry->{TOTAL} <= $lspecsref->[$entry->{LEVEL}]{threshold}) {
+            print '     ', '   ' x ($entry->{LEVEL} + 3), "...\n"
+               unless ($debug) and do {
+                  $line = '     ' . ('   ' x ($entry->{LEVEL} + 3)) . '...';
+                  printf "%-130s L%d: threshold reached(%d)\n", $line, $entry->{LEVEL} + 1, $lspecsref->[$entry->{LEVEL}]{threshold};
+               };
+            last;
+         }
+      }
+
+      $line = sprintf "%8d%s%s", $entry->{TOTAL}, '   ' x ($entry->{LEVEL} + 2),  $entry->{DATA};
+
+      if ($debug) {
+         printf "%-130s %-60s\n", $line, $entry->{DEBUG};
+      }
+
+      # line_style full, or lines < max_report_width
+
+      #printf "MAX: $max_report_width, LEN: %d, CUTLEN $cutlength\n", length($line);
+      if ($line_style == $line_styles{'full'} or length($line) <= $max_report_width) {
+         print $line, "\n";
+      }
+      elsif ($line_style == $line_styles{'truncate'}) {
+         print substr ($line,0,$cutlength), '...', "\n";
+      }
+      elsif ($line_style == $line_styles{'wrap'}) {
+         my $leader = ' ' x 8 . '   ' x ($entry->{LEVEL} + 2);
+         print substr ($line, 0, $max_report_width, ''), "\n";
+         while (length($line)) {
+            print $leader, substr ($line, 0, $max_report_width - length($leader), ''), "\n";
+         }
+      }
+      else {
+         die ('unexpected line style');
+      }
+
+      printTree ($entry->{CHILDREF}, $lspecsref, $line_style, $max_report_width, $debug)   if (exists $entry->{CHILDREF});
+   }
+}
+
+my $re_IP_strict = qr/\b(25[0-5]|2[0-4]\d|[01]?\d{1,2})\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})\.(25[0-5]|2[0-4]\d|[01]?\d{1,2})\b/;
+# 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
+
+   local $SIG{__WARN__} = sub { print "*** PLEASE REPORT:\n*** $_[0]*** Unexpected: \"$a->{DATA}\", \"$b->{DATA}\"\n" };
+
+   $b->{TOTAL} <=> $a->{TOTAL}
+
+      ||
+
+   pack('C4' => $a->{DATA} =~ /^$re_IP_strict/o) cmp pack('C4' => $b->{DATA} =~ /^$re_IP_strict/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
+#
+# Use the special key variable $END_KEY, which is "\a\a" (two ASCII bell's) to end a,
+# nested hash early, or the empty string '' may be used as the last key.
+
+our $END_KEY = "\a\a";
+
+sub buildTree(\% $ $ $ $ $) {
+   my ($href, $max_level_section, $levspecref, $max_level_global, $recurs_level, $show_unique, $debug) = @_;
+   my ($subtotal, $childList, $rec);
+
+   my @treeList = ();
+   my $total = 0;
+
+   foreach my $item (sort keys %$href) {
+      if (ref($href->{$item}) eq "HASH") {
+         #print " " x ($recurs_level * 4), "HASH: LEVEL $recurs_level: Item: $item, type: \"", ref($href->{$item}), "\"\n";
+
+         ($subtotal, $childList) = buildTree (%{$href->{$item}}, $max_level_section, $levspecref, $max_level_global, $recurs_level + 1, $debug);
+
+         if ($recurs_level < $max_level_global and $recurs_level < $max_level_section) {
+            # me + children
+            $rec = {
+               DATA     => $item,
+               TOTAL    => $subtotal,
+               LEVEL    => $recurs_level,
+               CHILDREF => $childList,
+            };
+
+            if ($debug) {
+               $rec->{DEBUG} = sprintf "L%d: levelspecs: %2d/%2d/%2d/%2d, Count: %10d",
+                     $recurs_level + 1, $max_level_global, $max_level_section,
+                     $levspecref->[$recurs_level]{topn}, $levspecref->[$recurs_level]{threshold}, $subtotal;
+            }
+            push (@treeList, $rec);
+         }
+      }
+      else {
+         if ($item ne '' and $item ne $END_KEY and $recurs_level < $max_level_global and $recurs_level < $max_level_section) {
+            $rec = {
+               DATA  => $item,
+               TOTAL => $href->{$item},
+               LEVEL => $recurs_level,
+               #CHILDREF => undef,
+            };
+            if ($debug) {
+               $rec->{DEBUG} = sprintf "L%d: levelspecs: %2d/%2d/%2d/%2d, Count: %10d",
+                     $recurs_level, $max_level_global, $max_level_section,
+                     $levspecref->[$recurs_level]{topn}, $levspecref->[$recurs_level]{threshold}, $href->{$item};
+            }
+            push (@treeList,  $rec);
+         }
+         $subtotal = $href->{$item};
+      }
+
+      $total += $subtotal;
+   }
+
+   #print " " x ($recurs_level * 4), "LEVEL $recurs_level: Returning from recurs_level $recurs_level\n";
+
+   return ($total, \@treeList);
+}
+
+1;
+
+#MODULE: ../Logreporters/Reports.pm
+package Logreporters::Reports;
+
+use 5.008;
+use strict;
+use re 'taint';
+use warnings;
+no warnings "uninitialized";
+
+BEGIN {
+   use Exporter ();
+   use vars qw(@ISA @EXPORT @EXPORT_OK %EXPORT_TAGS $VERSION);
+   $VERSION = '1.002';
+   @ISA = qw(Exporter);
+   @EXPORT = qw(&inc_unmatched &print_unmatched_report &print_percentiles_report2
+                &print_summary_report &print_detail_report);
+   @EXPORT_OK = qw();
+}
+
+use subs @EXPORT_OK;
+
+BEGIN {
+   import Logreporters::Config qw(%Opts $fw1 $fw2 $sep1 $sep2 &D_UNMATCHED &D_TREE);
+   import Logreporters::Utils qw(&commify &unitize &get_percentiles &get_percentiles2);
+   import Logreporters::TreeData qw(%Totals %Counts &buildTree &printTree);
+}
+
+my (%unmatched_list);
+
+our $origline;       # unmodified log line, for error reporting and debug
+
+sub inc_unmatched($) {
+   my ($id) = @_;
+   $unmatched_list{$origline}++;
+   print "UNMATCHED($id): \"$origline\"\n"  if $Opts{'debug'} & D_UNMATCHED;
+}
+
+# Print unmatched lines
+#
+sub print_unmatched_report() {
+   return unless (keys %unmatched_list);
+
+   print "\n\n**Unmatched Entries**\n";
+   foreach my $line (sort {$unmatched_list{$b}<=>$unmatched_list{$a} } keys %unmatched_list) {
+      printf "%8d   %s\n", $unmatched_list{$line}, $line;
+   }
+}
+
+=pod
+   ****** Summary ********************************************************
+          2   Miscellaneous warnings 
+
+      20621   Total messages scanned ----------------  100.00%
+    662.993M  Total bytes scanned                  695,198,092
+   ========   ================================================
+
+      19664   Ham -----------------------------------   95.36%
+      19630     Clean passed                            95.19%
+         34     Bad header passed                        0.16%
+
+        942   Spam ----------------------------------    4.57%
+        514     Spam blocked                             2.49%
+        428     Spam discarded (no quarantine)           2.08%
+
+         15   Malware -------------------------------    0.07%
+         15     Malware blocked                          0.07%
+
+
+       1978   SpamAssassin bypassed 
+         18   Released from quarantine 
+       1982   Whitelisted           
+          3   Blacklisted           
+         12   MIME error            
+         51   Bad header (debug supplemental) 
+         28   Extra code modules loaded at runtime 
+=cut
+# Prints the Summary report section
+#
+sub print_summary_report (\@) {
+   my ($sections) = @_;
+   my ($keyname,$cur_level);
+   my @lines;
+
+   my $expand_header_footer = sub {
+      my $line = undef;
+
+      foreach my $horf (@_) {
+         # print blank line if keyname is newline
+         if ($horf eq "\n") {
+            $line .= "\n";
+         }
+         elsif (my ($sepchar) = ($horf =~ /^(.)$/o)) {
+            $line .= sprintf "%s   %s\n", $sepchar x 8, $sepchar x 50;
+         }
+         else {
+            die "print_summary_report: unsupported header or footer type \"$horf\"";
+         }
+      }
+      return $line;
+   };
+
+   if ($Opts{'detail'} >= 5) {
+      my $header = "****** Summary ";
+      print $header, '*' x ($Opts{'max_report_width'} - length $header), "\n\n";
+   }
+
+   my @headers;
+   foreach my $sref (@$sections) {
+      # headers and separators
+      die "Unexpected Section $sref"  if (ref($sref) ne 'HASH');
+
+      # Start of a new section group.
+      # Expand and save headers to output at end of section group.
+      if ($sref->{CLASS} eq 'GROUP_BEGIN') {
+         $cur_level = $sref->{LEVEL};
+         $headers[$cur_level] = &$expand_header_footer(@{$sref->{HEADERS}});
+      }
+
+      elsif ($sref->{CLASS} eq 'GROUP_END') {
+         my $prev_level = $sref->{LEVEL};
+
+         # If this section had lines to output, tack on headers and footers,
+         # removing extraneous newlines.
+         if ($lines[$cur_level]) {
+            # squish multiple blank lines
+            if ($headers[$cur_level] and substr($headers[$cur_level],0,1) eq "\n") {
+               if ( ! defined $lines[$prev_level][-1] or $lines[$prev_level][-1] eq "\n") {
+                  $headers[$cur_level] =~ s/^\n+//;
+               }
+            }
+
+            push @{$lines[$prev_level]}, $headers[$cur_level]  if $headers[$cur_level];
+            push @{$lines[$prev_level]}, @{$lines[$cur_level]};
+            my $f = &$expand_header_footer(@{$sref->{FOOTERS}});
+            push @{$lines[$prev_level]}, $f   if $f;
+            $lines[$cur_level] = undef;
+         }
+
+         $headers[$cur_level] = undef;
+         $cur_level = $prev_level;
+      }
+
+      elsif ($sref->{CLASS} eq 'DATA') {
+         # Totals data
+         $keyname = $sref->{NAME};
+         if ($Totals{$keyname} > 0) {
+            my ($numfmt, $desc, $divisor) = ($sref->{FMT}, $sref->{TITLE}, $sref->{DIVISOR});
+
+            my $fmt   = '%8';
+            my $extra = ' %25s';
+            my $total = $Totals{$keyname};
+
+            # Z format provides  unitized or unaltered totals, as appropriate
+            if ($numfmt eq 'Z') {
+               ($total, $fmt) = unitize ($total, $fmt);
+            }
+            else {
+               $fmt .= "$numfmt ";
+               $extra = '';
+            }
+
+            if ($divisor and $$divisor) {
+               # XXX generalize this
+               if (ref ($desc) eq 'ARRAY') {
+                  $desc = @$desc[0] . ' ' . @$desc[1] x (42 - 2 - length(@$desc[0]));
+               }
+
+               push @{$lines[$cur_level]}, 
+                  sprintf "$fmt  %-42s %6.2f%%\n", $total, $desc,
+                     $$divisor == $Totals{$keyname} ? 100.00 : $Totals{$keyname} * 100 / $$divisor;
+            }
+            else {
+               push @{$lines[$cur_level]}, 
+                  sprintf "$fmt  %-23s $extra\n", $total, $desc, commify ($Totals{$keyname});
+            }
+         }
+      }
+      else {
+         die "print_summary_report: unexpected control...";
+      }
+   }
+   print @{$lines[0]};
+   print "\n";
+}
+
+# Prints the Detail report section
+# 
+# Note: side affect; deletes each key in Totals/Counts 
+# after printout.  Only the first instance of a key in
+# the Section table will result in Detail output.
+sub print_detail_report (\@) {
+   my ($sections) = @_;
+   my $header_printed = 0;
+
+   return unless (keys %Counts);
+
+#use Devel::Size qw(size total_size);
+
+   foreach my $sref ( @$sections ) {
+      next unless $sref->{CLASS} eq 'DATA';
+      # only print detail for this section if DETAIL is enabled
+      # and there is something in $Counts{$keyname}
+      next unless $sref->{DETAIL};
+      next unless exists $Counts{$sref->{NAME}};
+
+      my $keyname = $sref->{NAME};
+      my $max_level = undef;
+      my $print_this_key = 0;
+
+      my @levelspecs = ();
+      clear_level_specs($max_level, \@levelspecs);
+      if (exists $Opts{$keyname}) {
+         $max_level = create_level_specs($Opts{$keyname}, $Opts{'detail'}, \@levelspecs);
+         $print_this_key = 1  if ($max_level);
+      }
+      else {
+         $print_this_key = 1;
+      }
+      #print_level_specs($max_level,\@levelspecs);
+
+      # at detail 5, print level 1, detail 6: level 2, ...
+
+#print STDERR "building: $keyname\n";
+      my ($count, $treeref) = 
+            buildTree (%{$Counts{$keyname}}, defined ($max_level) ? $max_level : 11,
+                       \@levelspecs, $Opts{'detail'} - 4, 0, $Opts{'debug'} & D_TREE);
+
+      if ($count > 0) {
+         if ($print_this_key) {
+            my $desc = $sref->{TITLE};
+            $desc =~ s/^\s+//;
+
+            if (! $header_printed) {
+               my $header = "****** Detail ($max_level) ";
+               print $header, '*' x ($Opts{'max_report_width'} - length $header), "\n";
+               $header_printed = 1;
+            }
+            printf "\n%8d   %s %s\n", $count, $desc,
+                     $Opts{'sect_vars'} ?
+                       ('-' x ($Opts{'max_report_width'} - 18 - length($desc) - length($keyname))) . " [ $keyname ] -" :
+                        '-' x ($Opts{'max_report_width'} - 12 - length($desc))
+         }
+
+         printTree ($treeref, \@levelspecs, $Opts{'line_style'}, $Opts{'max_report_width'},
+                    $Opts{'debug'} & D_TREE);
+      }
+#print STDERR "Total size Counts: ", total_size(\%Counts), "\n";
+#print STDERR "Total size Totals: ", total_size(\%Totals), "\n";
+      $treeref = undef;
+      $Totals{$keyname} = undef;
+      delete $Totals{$keyname};
+      delete $Counts{$keyname};
+   }
+   #print "\n";
+}
+
+=pod
+
+Print out a standard percentiles report
+
+   === Delivery Delays Percentiles ===============================================================
+                          0%       25%       50%       75%       90%       95%       98%      100%
+   -----------------------------------------------------------------------------------------------
+   Before qmgr          0.01      0.70      1.40  45483.70  72773.08  81869.54  87327.42  90966.00
+   In qmgr              0.00      0.00      0.00      0.01      0.01      0.01      0.01      0.01
+   Conn setup           0.00      0.00      0.00      0.85      1.36      1.53      1.63      1.70
+   Transmission         0.03      0.47      0.92      1.61      2.02      2.16      2.24      2.30
+   Total                0.05      1.18      2.30  45486.15  72776.46  81873.23  87331.29  90970.00
+   ===============================================================================================
+
+   === Postgrey Delays Percentiles ===========================================================
+                      0%       25%       50%       75%       90%       95%       98%      100%
+   -------------------------------------------------------------------------------------------
+   Postgrey       727.00    727.00    727.00    727.00    727.00    727.00    727.00    727.00
+   ===========================================================================================
+ tableref: 
+   data table: ref to array of arrays, first cell is label, subsequent cells are data
+ title:
+   table's title
+ percentiles_str:
+   string of space or comma separated integers, which are the percentiles
+   calculated and output as table column data
+=cut
+sub print_percentiles_report2($$$) {
+   my ($tableref, $title, $percentiles_str) = @_;
+
+   return unless @$tableref;
+
+   my $myfw2 = $fw2 - 1;
+   my @percents = split /[ ,]/, $percentiles_str;
+
+   # Calc y label width from the hash's keys. Each key is padded with the 
+   # string "#: ", # where # is a single-digit sort index.
+   my $y_label_max_width = 0;
+   for (@$tableref) {
+      $y_label_max_width = length($_->[0])   if (length($_->[0]) > $y_label_max_width);
+   }
+
+   # Titles row
+   my $col_titles_str = sprintf "%-${y_label_max_width}s" . "%${myfw2}s%%" x @percents , ' ', @percents;
+   my $table_width = length($col_titles_str);
+
+   # Table header row
+   my $table_header_str = sprintf "%s %s ", $sep1 x 3, $title;
+   $table_header_str .= $sep1 x ($table_width - length($table_header_str));
+
+   print "\n", $table_header_str;
+   print "\n", $col_titles_str;
+   print "\n", $sep2 x $table_width;
+
+   my (@p, @coldata, @xformed);
+   foreach (@$tableref) {
+      my ($title, $ref) = ($_->[0], $_->[1]);
+      #xxx my @sorted = sort { $a <=> $b } @{$_->[1]};
+
+      my @byscore = ();
+
+      for my $bucket (sort { $a <=> $b } keys %$ref) {
+      #print "Key: $title: Bucket: $bucket = $ref->{$bucket}\n";
+      # pairs: bucket (i.e. key), tally
+         push @byscore, $bucket, $ref->{$bucket};
+      }
+
+
+      my @p = get_percentiles2 (@byscore, @percents);
+      printf "\n%-${y_label_max_width}s" . "%${fw2}.2f" x scalar (@p), $title, @p;
+   }
+
+=pod
+   foreach (@percents) {
+      #printf "\n%-${y_label_max_width}s" . "%${fw2}.2f" x scalar (@p), substr($title,3), @p;
+      printf "\n%3d%%", $title;
+      foreach my $val (@{shift @xformed}) {
+         my $unit;
+         if ($val > 1000) {
+            $unit = 's';
+            $val /= 1000;
+         }
+         else {
+            $unit = '';
+         }
+         printf "%${fw3}.2f%-2s", $val, $unit;
+      }
+   }
+=cut
+
+   print "\n", $sep1 x $table_width, "\n";
+}
+
+sub clear_level_specs($ $) {
+   my ($max_level,$lspecsref) = @_;
+   #print "Zeroing $max_level rows of levelspecs\n";
+   $max_level = 0 if (not defined $max_level);
+   for my $x (0..$max_level) {
+      $lspecsref->[$x]{topn}      = undef;
+      $lspecsref->[$x]{threshold} = undef;
+   }
+}
+
+# topn      = 0 means don't limit
+# threshold = 0 means no min threshold
+sub create_level_specs($ $ $) {
+   my ($optkey,$gdetail,$lspecref) = @_;
+
+   return 0 if ($optkey eq "0");
+
+   my $max_level = $gdetail;           # default to global detail level
+   my (@specsP1, @specsP2, @specsP3);
+
+   #printf "create_level_specs: key: %s => \"%s\", max_level: %d\n", $optkey, $max_level;
+
+   foreach my $sp (split /[\s,]+/, $optkey) {
+      #print "create_level_specs:  SP: \"$sp\"\n";
+      # original level specifier
+      if ($sp =~ /^\d+$/) {
+         $max_level = $sp;
+         #print "create_level_specs:  max_level set: $max_level\n";
+      }
+      # original level specifier + topn at level 1
+      elsif ($sp =~ /^(\d*)\.(\d+)$/) {
+         if ($1) { $max_level = $1; }
+         else    { $max_level = $gdetail; }          # top n specified, but no max level
+
+         # force top N at level 1 (zero based)
+         push @specsP1, { level => 0, topn => $2, threshold => 0 };
+      }
+      # newer level specs 
+      elsif ($sp =~ /^::(\d+)$/) {
+         push @specsP3, { level => undef, topn => 0, threshold => $1 };
+      }
+      elsif ($sp =~ /^:(\d+):(\d+)?$/) {
+         push @specsP2, { level => undef, topn => $1, threshold => defined $2 ? $2 : 0 };
+      }
+      elsif ($sp =~ /^(\d+):(\d+)?:(\d+)?$/) {
+         push @specsP1, { level => ($1 > 0 ? $1 - 1 : 0), topn => $2 ? $2 : 0, threshold => $3 ? $3 : 0 };
+      }
+      else {
+         print STDERR "create_level_specs: unexpected levelspec ignored: \"$sp\"\n";
+      }
+   }
+
+   #foreach my $sp (@specsP3, @specsP2, @specsP1) {
+   #   printf "Sorted specs: L%d, topn: %3d, threshold: %3d\n", $sp->{level}, $sp->{topn}, $sp->{threshold};
+   #}
+
+   my ($min, $max);
+   foreach my $sp ( @specsP3, @specsP2, @specsP1) {
+      ($min, $max) = (0, $max_level);
+      
+      if (defined $sp->{level}) {
+         $min = $max = $sp->{level};
+      }
+      for my $level ($min..$max) {
+         #printf "create_level_specs: setting L%d, topn: %s, threshold: %s\n", $level, $sp->{topn}, $sp->{threshold};
+         $lspecref->[$level]{topn}      = $sp->{topn}          if ($sp->{topn});
+         $lspecref->[$level]{threshold} = $sp->{threshold}     if ($sp->{threshold});
+      }
+   }
+
+   return $max_level;
+}
+
+sub print_level_specs($ $) {
+   my ($max_level,$lspecref) = @_;
+   for my $level (0..$max_level) {
+      printf "LevelSpec Row %d: %3d %3d\n", $level, $lspecref->[$level]{topn}, $lspecref->[$level]{threshold};
+   }
+}
+
+
+1;
+
+
+package Logreporters;
+
+BEGIN {
+   import Logreporters::Utils;
+   import Logreporters::Config;
+   import Logreporters::TreeData qw(%Totals %Counts %Collecting printTree buildTree);
+   import Logreporters::Reports;
+}
+use 5.008;
+use strict;
+use warnings;
+no warnings "uninitialized";
+use re 'taint';
+
+use Getopt::Long;
+use File::Basename;
+
+our $progname        =  fileparse($0);
+
+# the list of supplemental reports available in the Detail section
+#p0f 
+my @supplemental_reports = qw(
+   autolearn score_percentiles score_frequencies sarules timings sa_timings startinfo
+);
+
+# Default values for various options, used if no config file exists,
+# or some option is not set.
+# 
+# These are used to reset default values after an option has been
+# disabled (via undef'ing its value).  This allows a report to be
+# disabled via config file or --nodetail, but reenabled via subsequent
+# command line option
+my %Defaults = (
+   detail                 => 10,                      # report level detail
+   max_report_width       => 100,                     # maximum line width for report output
+   line_style             => undef,                   # lines > max_report_width, 0=truncate,1=wrap,2=full
+   syslog_name            => $progname_prefix,        # amavis' syslog service name
+   sect_vars              => 0,                       # show section vars in detail report hdrs
+   ipaddr_width           => 15,                      # width for printing ip addresses
+   first_recip_only       => 0,                       # Show only the first recipient, or all
+
+   autolearn              => 1,                       # show Autolearn report
+   bayes                  => 1,                       # show hit Bayesian buckets
+   #p0f                    => 'all all',               # p0f hits report
+   sarules                => '20 20',                 # show SpamAssassin rules hit
+   score_frequencies      => '-10 -5 0 5 10 20 30',   # buckets shown in spam scores report
+   score_percentiles      => '0 50 90 95 98 100',     # percentiles shown in spam scores report
+   startinfo              => 1,                       # show amavis startup info
+   timings                => 95,                      # show top N% of the timings report
+   timings_percentiles    => '0 5 25 50 75 95 100',   # percentiles shown in timing report
+   sa_timings             => 95,                      # show top N% of the SA timings report
+   sa_timings_percentiles => '0 5 25 50 75 95 100',   # percentiles shown in SA timing report
+);
+
+my $usage_str = <<"END_USAGE";
+Usage: $progname [ ARGUMENTS ] [logfile ...]
+
+   ARGUMENTS can be one or more of options listed below.  Later options override earlier ones.
+   Any argument may be abbreviated to an unambiguous length.  Input comes from named logfiles,
+   or STDIN.
+
+   --debug AREAS                       provide debug output for AREAS
+   --help                              print usage information
+   --version                           print program version
+
+   --config_file FILE, -f FILE         use alternate configuration file FILE
+   --syslog_name PATTERN               only consider log lines that match
+                                       syslog service name PATTERN
+
+   --detail LEVEL                      print LEVEL levels of detail
+                                       (default: 10)
+   --nodetail                          set all detail levels to 0
+   --[no]summary                       display the summary section
+
+   --ipaddr_width WIDTH                use WIDTH chars for IP addresses in
+                                       address/hostname pairs
+   --line_style wrap|full|truncate     disposition of lines > max_report_width
+                                       (default: truncate)
+   --full                              same as --line_style=full
+   --truncate                          same as --line_style=truncate
+   --wrap                              same as --line_style=wrap
+   --max_report_width WIDTH            limit report width to WIDTH chars
+                                       (default: 100)
+   --limit L=V, -l L=V                 set level limiter L with value V
+   --[no]sect_vars                     [do not] show config file var/cmd line
+                                       option names in section titles
+
+   --[no]autolearn                     show autolearn report
+   --[no]by_ccat_summary               include by contents category grouping in summary
+   --[no]first_recip_only              show first recipient only, or all recipients
+   --nosarules                         disable SpamAssassin spam and ham rules hit reports
+   --sarules "S,H"                     enable SpamAssassin spam and ham rules reports, showing
+   --sarules "default"                 showing the top S spam and top H ham rules hit (range:
+                                       0..., "all", or the keyword "default").
+   --noscore_frequencies               disable spam score frequency report
+   --score_frequencies "B1 [B2 ...]"   enable spam score frequency report, using buckets
+   --score_frequencies "default"       specified with B1 [B2 ...] (range: real numbers), or using their
+                                       internal default values when the keyword "default" is given
+   --noscore_percentiles               disable spam score percentiles report
+   --score_percentiles "P1 [P2 ...]"   enable spam score percentiles report, using percentiles
+   --score_percentiles "default"       specified with P1 [P2 ...] (range: 0...100), or using their
+                                       internal default values when the keyword "default" is given
+   --[no]startinfo                     show latest amavis startup details, if available
+
+   --nosa_timings                      disable the SA timings report (same as --sa_timings 0)
+   --sa_timings PERCENT                show top PERCENT percent of the SA timings report (range: 0...100)
+   --sa_timings_percentiles "P1 [P2 ...]"
+                                       set SA timings report percentiles to P1 [P2 ...]  (range: 0...100)
+
+   --notimings                         disable the timings report (same as --timings 0)
+   --timings PERCENT                   show top PERCENT percent of the timings report (range: 0...100)
+   --timings_percentiles "P1 [P2 ...]" set timings report percentiles to P1 [P2 ...]  (range: 0...100)
+END_USAGE
+
+# local prototypes
+sub usage($);
+sub init_getopts_table();
+sub init_defaults();
+sub build_sect_table();
+
+sub parse_vals($$);
+sub triway_opts($$);
+
+sub printSpamScorePercentilesReport;
+sub printSpamScoreFrequencyReport;
+sub printAutolearnReport;
+sub printSARulesReport;
+sub printTimingsReport($$$$);
+sub printStartupInfoReport;
+sub strip_trace($);
+sub prioritize_cmdline(@);
+
+sub create_ignore_list();
+sub check_ignore_list($ \@);
+
+# lines that match any RE in this list will be ignored.
+# see create_ignore_list();
+my @ignore_list_final = ();
+
+# The Sections table drives Summary and Detail reports.  For each entry in the
+# table, if there is data avaialable, a line will be output in the Summary report.
+# Additionally, a sub-section will be output in the Detail report if both the
+# global --detail, and the section's limiter variable, are sufficiently high (a
+# non-existent section limiter variable is considered to be sufficiently high).
+#
+my @Sections;
+
+# Initialize main running mode and basic opts
+init_run_mode($config_file);
+
+# Configure the Getopts options table
+init_getopts_table();
+
+# Place configuration file/environment variables onto command line
+init_cmdline();
+
+# Initialize default values
+init_defaults();
+
+# Process command line arguments, 0=no_permute,no_pass_through
+get_options(0);
+
+# Build the Section table
+build_sect_table();
+
+# Run through the list of Limiters, setting the limiters in %Opts.
+process_limiters(@Sections);
+
+# Set collection for any enabled supplemental sections
+foreach (@supplemental_reports) {
+   $Logreporters::TreeData::Collecting{$_} = (($Opts{'detail'} >= 5) && $Opts{$_}) ? 1 : 0;
+}
+
+# Don't collect SpamScores when not necessary
+$Collecting{'spamscores'} = ($Opts{'detail'} >= 5 && ($Opts{'score_percentiles'} || $Opts{'score_frequencies'})) ? 1 : 0;
+
+if (! defined $Opts{'line_style'}) {
+   # default line style to full if detail >= 11, or truncate otherwise
+   $Opts{'line_style'} =
+      ($Opts{'detail'} > 10) ? $line_styles{'full'} : $line_styles{'truncate'};
+}
+
+# Create the list of REs used to match against log lines
+create_ignore_list();
+
+my (%Timings, %TimingsSA, @TimingsTotals, @TimingsSATotals);
+my (%SaveLine, %StartInfo);
+my (%SpamScores, %spamtags, %p0ftags);
+
+# Priority: VIRUS BANNED UNCHECKED SPAM SPAMMY BADH OVERSIZED MTA CLEAN 
+my %ccatmajor_to_sectkey = (
+   'INFECTED'    => 'malware',
+   'BANNED'      => 'bannedname',
+   'UNCHECKED'   => 'unchecked',
+   'SPAM'        => 'spam',
+   'SPAMMY'      => 'spammy',
+   'BAD-HEADER'  => 'badheader',
+   'OVERSIZED'   => 'oversized',
+   'MTA-BLOCKED' => 'mta',
+   'CLEAN'       => 'clean',
+   'TEMPFAIL'    => 'tempfail',
+   'OTHER'       => 'other',
+);
+
+my %ccatmajor_to_priority = (
+   'INFECTED'    => 9,
+   'BANNED'      => 8,
+   'UNCHECKED'   => 7,
+   'SPAM'        => 6,
+   'SPAMMY'      => 5,
+   'BAD-HEADER'  => 4,
+   'OVERSIZED'   => 3,
+   'MTA-BLOCKED' => 2,
+   'CLEAN'       => 1,
+   'TEMPFAIL'    => 0,
+   'OTHER'       => 0,
+);
+
+# for reports 
+my %ccatmajor_to_spamham = (
+   'INFECTED'    => 'malware',
+   'BANNED'      => 'bannedname',
+   'UNCHECKED'   => 'unchecked',
+   'SPAM'        => 'spam',
+   'SPAMMY'      => 'spam',
+   'BAD-HEADER'  => 'ham',
+   'OVERSIZED'   => 'ham',
+   'MTA-BLOCKED' => 'ham',
+   'CLEAN'       => 'ham',
+   'TEMPFAIL'    => 'ham',
+   'OTHER'       => 'ham',
+);
+
+my $logline_maxlen = 980;
+
+# Create the list of REs against which log lines are matched.                                                                                         
+# Lines that match any of the patterns in this list are ignored.                                                                                      
+#                                                                                                                                                     
+# Note: This table is created at runtime, due to a Perl bug which                                                                                     
+# I reported as perl bug #56202:                                                                                                                      
+#                                                                                                                                                     
+#    http://rt.perl.org/rt3/Public/Bug/Display.html?id=56202                                                                                          
+#                                                                                                                                                     
+
+sub create_ignore_list() {
+   push @ignore_list_final, qr/^lookup_ip_acl/;
+   push @ignore_list_final, qr/^lookup_acl/;
+   push @ignore_list_final, qr/^lookup_hash/;
+   push @ignore_list_final, qr/^lookup_re/;
+   push @ignore_list_final, qr/^lookup_ldap/;
+   push @ignore_list_final, qr/^lookup_sql_field.* result=[YN]$/;
+   push @ignore_list_final, qr/^lookup .* does not match$/;
+   push @ignore_list_final, qr/^lookup [[(]/;
+   push @ignore_list_final, qr/^lookup => /;
+   push @ignore_list_final, qr/^lookup: /;
+   push @ignore_list_final, qr/^save_info_preliminary/; # log level 4
+   push @ignore_list_final, qr/^save_info_final/;       # log level 4
+   push @ignore_list_final, qr/^sql: /;
+   push @ignore_list_final, qr/^sql_storage: retrying/;
+   push @ignore_list_final, qr/^sql flush: /;
+   push @ignore_list_final, qr/^sql print/;
+   push @ignore_list_final, qr/^sql begin transaction/;
+   push @ignore_list_final, qr/^sql rollback/;
+   push @ignore_list_final, qr/^mail_via_sql: /;
+   push @ignore_list_final, qr/^CALLING SA check$/;
+   push @ignore_list_final, qr/^calling SA parse,/;
+   push @ignore_list_final, qr/^timer set to \d+/;
+   push @ignore_list_final, qr/^query_keys/;
+   push @ignore_list_final, qr/^find_or_save_addr: /;
+   push @ignore_list_final, qr/^header: /;
+   push @ignore_list_final, qr/^DO_QUARANTINE, /;
+   push @ignore_list_final, qr/^DEBUG_ONESHOT: /;
+   push @ignore_list_final, qr/^TempDir::/;
+   push @ignore_list_final, qr/^check_mail_begin_task: /;
+   push @ignore_list_final, qr/^program: .*?(anomy|altermime|disclaimer).*? said: /; # log_level 2
+   push @ignore_list_final, qr/^body (?:type|hash): /;
+   push @ignore_list_final, qr/^\d+\.From: <.*>, \d+.Mail_From:/;
+   push @ignore_list_final, qr/^The amavisd daemon is (?:apparently )?not running/;
+   push @ignore_list_final, qr/^rw_loop/;
+   push @ignore_list_final, qr/^[SL]MTP[><]/;
+   push @ignore_list_final, qr/^[SL]MTP response for/;
+   push @ignore_list_final, qr/^dsn:/i,   # DSN or dsn
+   push @ignore_list_final, qr/^enqueue: /;
+   push @ignore_list_final, qr/^write_header: /;
+   push @ignore_list_final, qr/^banned check: /;
+   push @ignore_list_final, qr/^child_finish_hook/;
+   push @ignore_list_final, qr/^inspect_dsn:/;
+   push @ignore_list_final, qr/^client IP address unknown/;
+   push @ignore_list_final, qr/^final_destiny/;
+   push @ignore_list_final, qr/^one_response_for_all/;
+   push @ignore_list_final, qr/^headers CLUSTERING/;
+   push @ignore_list_final, qr/^notif=/;
+   push @ignore_list_final, qr/^\(about to connect/;
+   push @ignore_list_final, qr/^Original mail size/;
+   push @ignore_list_final, qr/^TempDir removal/;
+   push @ignore_list_final, qr/^Issued a new file name/;
+   push @ignore_list_final, qr/^starting banned checks/;
+   push @ignore_list_final, qr/^skip admin notification/;
+   push @ignore_list_final, qr/^do_notify_and_quarantine - done/;
+   push @ignore_list_final, qr/^do_[a-zA-Z]+.* done$/i;
+   push @ignore_list_final, qr/^Remote host presents itself as:/;
+   push @ignore_list_final, qr/^connect_to_ldap/;
+   push @ignore_list_final, qr/^connect_to_sql: trying /;
+   push @ignore_list_final, qr/^ldap begin_work/;
+   push @ignore_list_final, qr/^Connecting to LDAP server/;
+   push @ignore_list_final, qr/^loaded base policy bank/;
+   push @ignore_list_final, qr/^\d+\.From:/;
+   push @ignore_list_final, qr/^Syslog (retries|warnings)/;
+   push @ignore_list_final, qr/^smtp connection cache/;
+   push @ignore_list_final, qr/^smtp cmd> /;
+   push @ignore_list_final, qr/^smtp session/;
+   push @ignore_list_final, qr/^Ignoring stale PID file/;
+   push @ignore_list_final, qr/^mime_decode_preamble/;
+   push @ignore_list_final, qr/^doing banned check for/;
+   push @ignore_list_final, qr/^open_on_specific_fd/;
+   push @ignore_list_final, qr/^reparenting /;
+   push @ignore_list_final, qr/^Issued a new pseudo part: /;
+   push @ignore_list_final, qr/^run_command: /;
+   push @ignore_list_final, qr/^result line from file/;
+   push @ignore_list_final, qr/^Charging /;
+   push @ignore_list_final, qr/^check_for_banned /;
+   push @ignore_list_final, qr/^Extracting mime components$/;
+   push @ignore_list_final, qr/^response to /;
+   push @ignore_list_final, qr/^File-type of /;
+   push @ignore_list_final, qr/^Skip admin notification, /;
+   push @ignore_list_final, qr/^run_av: /;
+   push @ignore_list_final, qr/^string_to_mime_entity /;
+   push @ignore_list_final, qr/^ndn_needed=/;
+   push @ignore_list_final, qr/^sending RCPT TO:/;
+   push @ignore_list_final, qr/^decode_parts: /;
+   push @ignore_list_final, qr/^decompose_part: /;
+   push @ignore_list_final, qr/^setting body type: /;
+   push @ignore_list_final, qr/^mime_decode_epilogue: /;
+   push @ignore_list_final, qr/^string_to_mime_entity: /;
+   push @ignore_list_final, qr/^at the END handler: /;
+   push @ignore_list_final, qr/^Amavis::.* called$/;
+   push @ignore_list_final, qr/^Amavis::.* close,/;
+   push @ignore_list_final, qr/^dkim: /;         # XXX provide stats
+   push @ignore_list_final, qr/^collect banned table/;
+   push @ignore_list_final, qr/^collect_results from/;
+   push @ignore_list_final, qr/^blocking contents category is/;
+   push @ignore_list_final, qr/^running file\(/;
+   push @ignore_list_final, qr/^Found av scanner/;
+   push @ignore_list_final, qr/^Found myself/;
+   push @ignore_list_final, qr/^mail_via_smtp/;
+   push @ignore_list_final, qr/^switch_to_client_time/;
+   push @ignore_list_final, qr/^parse_message_id/;
+   push @ignore_list_final, qr/^parse_received: /;
+   push @ignore_list_final, qr/^parse_ip_address_from_received: /;
+   push @ignore_list_final, qr/^fish_out_ip_from_received: /;
+   push @ignore_list_final, qr/^Waiting for the process \S+ to terminate/;
+   push @ignore_list_final, qr/^Valid PID file \(younger than sys uptime/;
+   push @ignore_list_final, qr/^Sending SIG\S+ to amavisd/;
+   push @ignore_list_final, qr/^Can't send SIG\S+ to process/;
+   push @ignore_list_final, qr/^killing process/;
+   push @ignore_list_final, qr/^no need to kill process/;
+   push @ignore_list_final, qr/^process .* is still alive/;
+   push @ignore_list_final, qr/^Daemon \[\d+\] terminated by SIG/;
+   push @ignore_list_final, qr/^storage and lookups will use .* to SQL/;
+   push @ignore_list_final, qr/^idle_proc, /;
+   push @ignore_list_final, qr/^switch_to_my_time/;
+   push @ignore_list_final, qr/^TempDir::strip: /;
+   push @ignore_list_final, qr/^rmdir_recursively/;
+   push @ignore_list_final, qr/^sending [SL]MTP response/;
+   push @ignore_list_final, qr/^prolong_timer/;
+   push @ignore_list_final, qr/^process_request:/;
+   push @ignore_list_final, qr/^exiting process_request/;
+   push @ignore_list_final, qr/^post_process_request_hook: /;
+   push @ignore_list_final, qr/^SMTP session over/;
+   push @ignore_list_final, qr/^updating snmp variables/;
+   push @ignore_list_final, qr/^best_try_originator_ip/;
+   push @ignore_list_final, qr/^mail checking ended: /; # log level 2
+   push @ignore_list_final, qr/^The amavisd daemon is already running/;
+   push @ignore_list_final, qr/^AUTH not needed/;
+   push @ignore_list_final, qr/^load: \d+ %, total idle/;
+   push @ignore_list_final, qr/^policy protocol: [^=]+=\S+(?:,\S+)*$/;   # allow "policy protocol: INVALID ..." later
+   push @ignore_list_final, qr/^penpals: /;
+   push @ignore_list_final, qr/^Not calling virus scanners, no files to scan in/;
+   push @ignore_list_final, qr/^local delivery: /;
+   push @ignore_list_final, qr/^run_as_subprocess: child process \S*: Broken pipe/;
+   push @ignore_list_final, qr/^initializing Mail::SpamAssassin/;
+   push @ignore_list_final, qr/^Error reading mail header section/;   # seems to occur gen. due to perl getline() bug
+   push @ignore_list_final, qr/^flatten_and_tidy_dir/;
+   push @ignore_list_final, qr/^do_7zip: member/;
+   push @ignore_list_final, qr/^Expanding \S+ archive/;
+   push @ignore_list_final, qr/^files_to_scan:/;
+   push @ignore_list_final, qr/^Unzipping p\d+/;
+   push @ignore_list_final, qr/^writing mail text to SQL/;
+   push @ignore_list_final, qr/^strip_tempdir/;
+   push @ignore_list_final, qr/^no parts, file/;
+   push @ignore_list_final, qr/^warnsender_with_pass/;
+   push @ignore_list_final, qr/^RETURNED FROM SA check/;
+   push @ignore_list_final, qr/^mime_traverse: /;
+   push @ignore_list_final, qr/^do_spam: /;
+   push @ignore_list_final, qr/^prepare_tempdir: /;
+   push @ignore_list_final, qr/^check_header: /;
+   push @ignore_list_final, qr/^skip admin notification/;
+   push @ignore_list_final, qr/^do_executable: not a/;
+   push @ignore_list_final, qr/^Skip spam admin notification, no administrators$/;
+   push @ignore_list_final, qr/^skip banned check for/;
+   push @ignore_list_final, qr/^is_outgoing /;
+   push @ignore_list_final, qr/^NO Disclaimer/;
+   push @ignore_list_final, qr/^Using \(\S+\) on file/;
+   push @ignore_list_final, qr/^no anti-spam code loaded/;
+   push @ignore_list_final, qr/^entered child_init_hook/;
+   push @ignore_list_final, qr/^body type/;
+   push @ignore_list_final, qr/^establish_or_refresh/;
+   push @ignore_list_final, qr/^get_body_digest/;
+   push @ignore_list_final, qr/^ask_daemon_internal/;
+   push @ignore_list_final, qr/^Turning AV infection into a spam report, name already accounted for/;
+   push @ignore_list_final, qr/^Calling virus scanners/;
+   push @ignore_list_final, qr/^timer stopped after /;
+   push @ignore_list_final, qr/^virus_presence /;
+   push @ignore_list_final, qr/^cache entry /;
+   push @ignore_list_final, qr/^generate_mail_id /;
+   push @ignore_list_final, qr/^Load low precedence policybank/;
+   push @ignore_list_final, qr/^warm restart on /;             # XXX could be placed instartup info
+   push @ignore_list_final, qr/^Signalling a SIGHUP to a running daemon/;
+   push @ignore_list_final, qr/^Deleting db files /;
+   push @ignore_list_final, qr/^address modified \(/;
+   push @ignore_list_final, qr/^Request: AM\.PDP /;
+   push @ignore_list_final, qr/^DSPAM result: /;
+   push @ignore_list_final, qr/^bind to \//;
+   push @ignore_list_final, qr/^ZMQ enabled: /;
+
+   push @ignore_list_final, qr/^Inserting header field: X-Amavis-Hold: /;
+   push @ignore_list_final, qr/^Decoding of .* failed, leaving it unpacked: /;
+
+      # various forms of "Using ..."
+      # more specific, interesting variants already captured: search "Using"
+   push @ignore_list_final, qr/^Using \(.*\) on dir:/;
+   push @ignore_list_final, qr/^Using [^:]+: \(built-in interface\)/;
+   push @ignore_list_final, qr/^Using \(.*\): /;
+   push @ignore_list_final, qr/: sleeping for /;
+   push @ignore_list_final, qr/creating socket by /;
+
+        # unanchored
+   push @ignore_list_final, qr/\bRUSAGE\b/;
+   push @ignore_list_final, qr/: Sending .* to UNIX socket/;
+}
+
+# Notes:
+#
+#   - IN REs, always use /o flag or qr// at end of RE when RE uses unchanging interpolated vars
+#   - In REs, email addresses may be empty "<>" - capture using *, not + ( eg. from=<[^>]*> )
+#   - See additional notes below, search for "Note:".
+#   - XXX indicates change, fix or more thought required
+
+# Main processing loop
+#
+while (<>) {
+   chomp;
+   s/ +$//;
+   next if $_ eq '';
+
+   $Logreporters::Reports::origline = $_;
+
+   if ($Opts{'standalone'}) {
+      next unless s/^[A-Z][a-z]{2} [ \d]\d \d{2}:\d{2}:\d{2} (?:<[^>]+> )?\S+ $Opts{'syslog_name'}(?:\[\d+\])?: (?:\[ID \d+ \w+\.\w+\] )?//o;
+   }
+
+   my $p1 = $_;
+   my ($p2, $pid);
+   my $action = "blocked";    # default action is blocked if not present in log
+
+   # For now, ignore the amavis startup timing lines.  Need to do this
+   # before stripping out the amavis pid to differentiate these from the
+   # scan timing reports
+   next if ($p1 =~ /^TIMING/);
+
+   my $linelen = length $p1;
+   # Strip amavis process id-instance id, or release id
+   if (($pid,$p2) = ($p1 =~ /^\(([^)]+)\) (.*)$/ )) {
+      $p1 = $p2;
+   }
+
+   # Handle continuation lines.  Continuation lines should be in order per PID, meaning line1, line2, line3,
+   # but never line3, line1, line2.
+   #
+   # amavis log lines as chopped by sub write_log are exactly 980 characters long starting with '(' as in:
+   #  amavis[47061]: (47061-15) SPAM, etc  ...
+   #                 ^ <-----980------------->
+   # but this can be changed in amavis via $logline_maxlen.
+   # There may also be the alert markers (!) and (!!) preceeding any continuation ellipsis.
+   #
+
+   # ... a continued line ...
+   if ($p1 =~ s/^(\([!]{1,2}\))?\.\.\.//) {
+      if (!exists($SaveLine{$pid})) {
+         my $alert = $1;
+         #printf "Unexpected continue line: \"%s\"\n", $p1;
+         $SaveLine{$pid} = $alert || '';
+      }
+      $SaveLine{$pid} .= $p1;
+      next if $SaveLine{$pid} =~ s/\.\.\.$//;  # next if line has more pieces
+   }
+
+   # this line continues ...
+   if ($p1 =~ /\.\.\.$/ and $linelen == $logline_maxlen) {
+      $p1 =~ s/\.\.\.$//;
+      $SaveLine{$pid} = $p1;
+      next;
+   }
+
+   if (exists($SaveLine{$pid})) {
+      # printf "END OF SaveLine: %s\n", $SaveLine{$pid};
+      $p1 = delete $SaveLine{$pid};
+   }
+
+   #if (length($p1) > 10000) {
+   #   printf "Long log entry %d chars: \"%s\"\n", length($p1), $p1;
+   #   next;
+   #}
+
+   next if (
+        # Place REs here that should ignore log lines otherwise caught below.
+        # Some are located here historically, and need to be checked for candidates
+        # to be relocated to ignore_list_final.
+           ($p1 =~ /^do_ascii/) 
+        or ($p1 =~ /^Checking/)
+        or ($p1 =~ /^header_edits_for_quar: /)
+        or ($p1 =~ /^Not-Delivered/)
+        or ($p1 =~ /^SpamControl/)
+        or ($p1 =~ /^Perl/)
+        or ($p1 =~ /^ESMTP/)
+        or ($p1 =~ /^(?:\(!+\))?(\S+ )?(?:FWD|SEND) from /)            # log level 4
+        or ($p1 =~ /^(?:\(!+\))?(\S+ )?(?:ESMTP|FWD|SEND) via /)       # log level 4
+        or ($p1 =~ /^tempdir being removed/)
+        or ($p1 =~ /^do_notify_and_quar(?:antine)?: .*ccat/)
+        or ($p1 =~ /^cached [a-zA-Z0-9]+ /)
+        or ($p1 =~ /^loaded policy bank/)
+        or ($p1 =~ /^p\.path/)
+        or ($p1 =~ /^virus_scan: /)
+        or ($p1 =~ /^Requesting (a |)process rundown after [0-9]+ tasks/)
+        or ($p1 =~ /^Cached (virus|spam) check expired/)
+        or ($p1 =~ /^pr(?:esent|ovid)ing full original message to scanners as/)  # log level 2
+        or ($p1 =~ /^Actual message size [0-9]+ B(,| greater than the) declared [0-9]+ B/)
+        or ($p1 =~ /^disabling DSN/)
+        or ($p1 =~ /^Virus ([^,]+ )?matches [^,]+, sender addr ignored/)
+        or ($p1 =~ /^release /)
+        or ($p1 =~ /^adding SA score \S+ to existing/)
+        or ($p1 =~ /^Maia:/)   # redundant
+        or ($p1 =~ /^AM\.PDP  /)  # this appears to be always have two spaces
+                                  # because in amavisd::preprocess_policy_query() when $ampdp is
+                                  # set, it will pass an unset $attr_ref->{'mail_id'} to do_log(1
+        or ($p1 =~ /^_(?:WARN|DIE):$/)  # bug: empty _WARN|_DIE: http://marc.info/?l=amavis-user&m=121725098111422&w=2
+
+        # non-begin anchored
+        or ($p1 =~ /result: clean$/)
+        or ($p1 =~ /DESTROY called$/)
+        or ($p1 =~ /email\.txt no longer exists, can't re-use it/)
+        or ($p1 =~ /SPAM\.TAG2/)
+        or ($p1 =~ /BAD-HEADER\.TAG2/)
+        or ($p1 =~ /: Connecting to socket/)
+        or ($p1 =~ /broken pipe \(don't worry\), retrying/)
+        or ($p1 =~ /(?:Sending|on dir:) (?:CONT)?SCAN /)
+   );
+
+   my ($ip, $from, $to, $key,, $reason, $item,
+       $decoder, $scanner, $stage, $sectkey);
+
+   # Coerce older "INFECTED" quarantined lines into "Blocked INFECTED",
+   # to be processed in the Passed/Blocked section.
+   if ($p1 =~ /^INFECTED.*, quarantine/) {
+      $p1 = 'Blocked ' . $p1;
+   }
+
+   # SPAM entry occurs at kill level
+   # SPAM-TAG entry occurs at log level 2, when spam header is inserted
+   # log_level >= 2 || (log_level > 2 && syslog_priority=debug)
+   my ($tagtype,$fromto,$isspam,$tags,$tests,$autolearn);
+
+   # amavisd-new 2.7.0 changes SPAM-TAG to Spam-tag and its log_level to 3
+   if (($tagtype,$fromto,$isspam,$tags,$tests,$autolearn) = ($p1 =~ /^((?i:SPAM(?:-TAG)?)), (.*), (Yes|No), score=[-+x\d.]+(.*) tests=\[([^\]]*)](?:, autolearn=(\w+))?/) or
+       ($tagtype,$fromto,$isspam,$tags,$tests) =            ($p1 =~ /^((?i:SPAM(?:-TAG)?)), (.*), (Yes|No), hits=[-+x\d.]+(.*) tests=(.*)(?:, quarantine )?/)) {
+
+      #TD SPAM, <from@example.com> -> <to@sample.com>, Yes, score=17.709 tag=-10 tag2=6.31 kill=6.31 tests=[AWL=-0.678, BAYES_99=4], autolearn=spam, quarantine Cc4+GUJhgpqh (spam-quarantine)
+      #TD SPAM, <from@example.com> -> <to@sample.net>, Yes, score=21.161 tag=x tag2=8.15 kill=8.15 tests=[BAYES_99=2.5, FORGED_RCVD_HELO=0.135], autolearn=no, quarantine m6lWPoTGJ2O (spam-quarantine)
+      #TD SPAM, <from@example.com> -> <to@sample.net>, Yes, score=17.887 tag=-10 tag2=6.31 kill=6.31 tests=[BAYES_99=4], autolearn=spam, quarantine VFYjDOVTW4zd (spam-quarantine)
+      #TD SPAM-TAG, <from@example.com> -> <to@sample.net>, No, score=-0.069 tagged_above=-10 required=6.31 tests=[BAYES_00=-2.599, FROM_ENDS_IN_NUMS=2.53]
+      #TD SPAM-TAG, <from@example.com> -> <to@sample.net>, No, score=-1.294 required=8.15 tests=[BAYES_00=-2.599, FROM_LOCAL_HEX=1.305]
+      # pre 2.3.3
+      #TD SPAM-TAG, <from@example.com> -> <to@sample.net>, Yes, hits=6.159 tagged_above=-999 required=3.4 tests=BAYES_99=3.5, FUZZY_CPILL=0.518, HTML_MESSAGE=0.001, URIBL_WS_SURBL=2.14
+      #TD SPAM, <from@example.com> -> <to@sample.net>, Yes, hits=8.1 tag1=-999.0 tag2=7.0 kill=7.0 tests=MANGLED_TAKE, UPPERCASE_25_50, quarantine spam-14156-09 (maia-spam-quarantine) 
+
+      $Totals{'tagged'}++   if uc($tagtype) eq 'SPAM-TAG';
+
+      if ($tests) {
+         my $type = $isspam =~ /^Y/ ? 'Spam' : 'Ham';
+
+         # Note: A SPAM line may be followed by an almost identical SPAM-TAG line.  To avoid double counting,
+         # maintain a list of (abbreviated) SPAM tag lines keyed by pid.  Since pid's are recycled,
+         # maintain an approximation of uniqueness by combining several components from the log
+         # line (we can't use the date information, as in logwatch, it is not present).
+         # XXX: It is safe to delete an entry when the final Passed/Block line occurs
+
+         #TD SPAM, <from@example.com> -> <to@sample.net>, Yes, score=34.939 tag=x tag2=6.31 kill=6.31 tests=[DATE_IN_FUTURE_03_06=1.961], autolearn=disabled
+         #TD SPAM-TAG, <from@example.com> -> <to@sample.net>, Yes, score=34.939 required=6.31 tests=[DATE_IN_FUTURE_03_06=1.961]
+         #TD SPAM, <from@example.com> -> tod@sample.net>, Yes, score=31.565 tag=x tag2=6.9 kill=6.9 tests=[AV:Sanesecurity.Phishing.Bank.2666.UNOFFICIAL=4.1, AV:Sanesecurity.Phishing.Bank.2666.UNOFFICIAL=4.1, BAYES_99=4, DCC_CHECK=4, DIGEST_MULTIPLE=0.001, FORGED_MUA_OUTLOOK=3.116, FORGED_OUTLOOK_HTML=0.001, FORGED_OUTLOOK_TAGS=0.001, HTML_MESSAGE=0.001, L_AV_SS_Phish=5, MIME_HTML_ONLY=1.457, NORMAL_HTTP_TO_IP=0.001, RAZOR2_CF_RANGE_51_100=2, RAZOR2_CF_RANGE_E4_51_100=1.5, RAZOR2_CF_RANGE_E8_51_100=1.5, RAZOR2_CHECK=3, RDNS_NONE=0.1, URIBL_PH_SURBL=1.787] autolearn=spam
+
+
+         my $tagstr = $fromto . '/' . $isspam . '/' . $tests;
+         if (uc($tagtype) eq 'SPAM-TAG' and exists $spamtags{$pid}) {
+            next if ($spamtags{$pid} eq $tagstr);
+         }
+         $spamtags{$pid} = $tagstr;
+
+         #for (split /=[^,]+(?:, +|$)/, $tests) 
+         # amavis < 2.6.2 would double list AV names when using
+         # @virus_name_to_spam_score_maps.
+         my @unique_tests = unique_list (split /, +/, $tests);
+         for (@unique_tests) {
+           # skip possible trailing junk ("quarantine, ...") when older non-bracked tests=xxx is used
+           next if ! /[^=]+=[\-.\d]+/;
+            my ($id,$val) = split /=/;
+            if ($id =~ /^BAYES_\d+$/) {
+               $Counts{'bayes'}{$id}++    if ($Collecting{'bayes'});
+            }
+            if ($Opts{'sarules'}) {
+               if    ($id eq 'DKIM_POLICY_SIGNSOME') { $val = 0   }
+               elsif ($id eq 'AWL')                  { $val = '-' }
+               $Counts{'sarules'}{$type}{sprintf "%6s %s", $val, $id}++;
+            }
+         }
+         # Handled below
+         #autolearn= is available only at ll>=3 or SPAM messages; so ham doesn't naturally occur here
+         # SA 2.5/2.6 : ham/spam/no
+         # SA 3.0+    : ham/spam/no/disabled failed/unavailable
+         #$Counts{'autolearn'}{$type}{$autolearn}++    if ($Opts{'autolearn'});
+      }
+   }
+
+   # Passed or Blocked
+   elsif ($p1 =~ /^(Passed|Blocked)(.*)/) {
+      $action = lcfirst $1;
+      ($p1 = $2) =~ s/^\s+//;
+
+      $p1 =~ s/^,/CLEAN,/;      # canonicalize older log entries
+      #print "P1: \"$p1\"\n";
+
+      # amavis 20030616p10-5
+      #TD Passed, <from@example.com> -> <to@sample.net>, Message-ID: <652.44494541@example.com>, Hits: 4.377
+      #TD Passed, <from@example.com> -> <to@sample.net>, Message-ID: <B5C@example.com>, Hits: - 
+      #TD Passed, <from@example.com> -> <to@sample.net>, quarantine IJHkgliCm2Ia, Message-ID: <20080307140552.16E127641E@example.com>, Hits: 0.633
+
+      #TD Passed CLEAN, [10.0.0.1] [10.0.0.1] <from@example.com> -> <to@sample.net>, Message-ID: <2qxz191@example.com>, mail_id: w4DHD8, Hits: -2.599, size: 3045, queued_as: 2056, 2664 ms
+      #TD Passed CLEAN, [10.0.0.1] [10.0.0.1] <from@example.com> -> <to@sample.net>, Message-ID: <2qxz191@example.com>, mail_id: w4DHD8, Hits: -2.541-3, size: 3045, queued_as: 2056, 2664 ms
+      #TD Blocked SPAM, [10.0.0.1] [192.168.0.1] <bogus@example.com> -> <to@sample.net>, quarantine: spam-EzEbE9W, Message-ID: <117894@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms
+      #TD Blocked SPAM, LOCAL [10.0.0.1] [10.0.0.2] <bogus@example.com> -> <to@sample.net>, quarantine: spam-EzEbE9W, Message-ID: <110394@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms
+      #TD Blocked SPAM, [IPv6:2001:630:d0:f102:230:48ff:fe77:96e] [192.168.0.1] <joe@example.com> -> <user@sample.net>, quarantine: spam-EzEbE9W, Message-ID: <11780394@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms
+      #TD Passed SPAMMY, ORIGINATING/MYNETS LOCAL [10.0.0.1] [10.0.0.1] <from@example.com> -> <to1@sample.net>,<to2@sample.net>, quarantine: spam-EzEbE9W, Message-ID: <11780394@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms
+      #TD Blocked SPAM, B-BANK/C-BANK/B-BANK [10.0.0.1] [10.0.0.1] <from@sample.net> -> <to@example.com>, quarantine: spam-EzEbE9W, Message-ID: <11780394@example.com>, mail_id: EzEbE9W, Hits: 6.364, size: 16493, 6292 ms
+      #TD Blocked SPAM, [10.0.0.1] [10.0.0.1] <from@example.com> -> <to@sample.net>, quarantine: spam-AV49p5, Message-ID: <1.007@sample.net>, mail_id: AV49p5, Hits: 7.487, size: 27174, 4406 ms
+      #TD Passed SPAM, MYNETS <root@example.com> -> <root@example.com>, quarantine: spam-V3Wq, Message-ID: <220.1B@example.com>, mail_id: V3Wq, Hits: 7, size: 8838, queued_as: C63EC, 18 ms
+      #TD Passed SPAM, <> -> <"fred).flintstone"@domain.tld>, Message-ID: <200801180104.CAA23669@aserver.sub.adomain.tld>, mail_id: 6AzQ1g0l5RgP, Hits: 9.061, size: 5555, queued_as: C1840506CB8, 8766 ms
+      #TD Blocked INFECTED (HTML.Phishing.Bank-43), [198.168.0.1] [10.0.0.1] <bogus@example.com> -> <to@sample.net>, quarantine: virus-SCwJcs, Message-ID: <509@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms
+      #TD Blocked INFECTED (Trojan.Downloader.Small-9993), LOCAL [10.0.0.2] [10.0.0.2] <bogus@example.net> -> <to@example.com>, quarantine: virus-SCwJcs, Message-ID: <9009@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms
+      #TD Blocked BANNED (multipart/report | message/partial,.txt), [192.168.0.1] [10.0.0.2] <> -> <someuser@sample.net>, quarantine: virus-SCwJcs, Message-ID: <509@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms
+      #TD Blocked BANNED (multipart/report | message/partial,.txt), LOCAL [192.168.0.1] [10.0.0.2] <> -> <someuser@sample.net>, quarantine: virus-SCwJcs, Message-ID: <509@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms
+      #TD Blocked BANNED (multipart/mixed | application/octet-stream,.asc,=?iso-8859-1?Q?FTP=5FFile=5F (1)=File(1).reg), [192.168.0.0] [192.168.0.0] <from@example.com> -> <to@sample.us>, quarantine: virus-SCwJcs, Message-ID: <509@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms
+      #TD Blocked BANNED (multipart/related | application/zip,.zip,card.zip | .exe,.exe-ms,Card.exe), [10.0.0.2] [10.0.0.2] <from@example.com> -> <to@sample.net>, quarantine: banned-9OXm4Q3ah, Message-ID: <08517$@from>, mail_id: 9OXm4Q3ah, Hits: -, size: 2366, 3803 ms
+      #TD Passed BAD-HEADER, [192.168.0.1] [10.0.0.2] <bogus@example.com> -> <someuser@sample.net>, quarantine: virus-SCwJcs, Message-ID: <df@acm.org>, mail_id: SCwJcs, Hits: 2.54 size: 4134, 3721 ms
+      #TD Passed BAD-HEADER, LOCAL [192.168.0.1] [10.0.0.2] <bogus@example.com> -> <someuser@sample.net>, quarantine: virus-SCwJcs, Message-ID: <df@acm.org>, mail_id: SCwJcs, Hits: 3.2 size: 4134, 3721 ms
+      #TD Passed BAD-HEADER, MYNETS AM.PDP [127.0.0.1] [127.0.0.1] <bogus@example.com> -> <someuser@sample.net>, quarantine: virus-SCwJcs, Message-ID: <df@acm.org>, mail_id: SCwJcs, Hits: 1.2 size: 4134, 3721 ms
+      #TD Passed BAD-HEADER, ORIGINATING/MYNETS LOCAL [10.0.0.1] [10.0.0.1] <from@sample.net> -> <to1@sample.net>,<to2@sample.net>,<to3@example.com>, quarantine: virus-SCwJcs, Message-ID: <df@acm.org>, mail_id: SCwJcs, Hits: -, size: 4134, 3721 ms
+      #TD Passed BAD-HEADER, [10.0.0.1] [10.0.0.2] <from@example.com> -> <to@sample.net>, quarantine: badh-lxR, Message-ID: <7fm@example.com>, mail_id: lxR, Hits: -2.292, size: 422, queued_as: E3B, 981 ms
+      #TD Passed UNCHECKED, MYNETS LOCAL [192.168.0.1] [192.168.0.1] <from@sample.net> -> <to@example.com> Message-ID: <002e01c759c7$5de437b0$0a02a8c0@somehost>, mail_id: 7vtR-7BAvHZV, Hits: -, queued_as: B5420C2E10, 6585 ms
+      #TD Blocked MTA-BLOCKED, LOCAL [192.168.0.1] [192.168.0.2] <from@example.com> -> <to@sample.net>, Message-ID: <438548@example.com>, mail_id: tfgTCiyvFw, Hits: -2.54, size: 4895, 31758 ms
+      #TD Blocked OVERSIZED, LOCAL [10.0.0.1] [10.0.0.1] <f@example.com> -> <t@sample.net>, Message-ID: <435@example.com>, mail_id: tfTivFw, Hits: -2.54, size: 444444895, 31758 ms
+      #TD Blocked OTHER, LOCAL [10.0.0.1] [10.0.0.1] <f@example.com> -> <t@sample.net>, Message-ID: <435@example.com>, mail_id: tfTivFw, Hits: -2.54, size: 495, 31758 ms
+      #TD Blocked TEMPFAIL, [10.0.0.2] [10.0.0.1] <user@example.com> -> <to@sample.net>, Message-ID: <200703302301.9f1899470@example.com>, mail_id: bgf52ZCNbPo, Hits: -2.586, 3908 ms
+
+      #2.3.1
+      #<>,<info@example.com>,Passed,Hits=-3.3,Message-ID=<200506440.1.sample.net>,Size=51458 
+      #20030616p10-5
+      #Not-Delivered, <from@example.com> -> <to@localhost>, quarantine spam-ea32770-03, Message-ID: <BAA618FE2CB585@localhost>, Hits: 9.687 
+
+      # malwarepassed, malwareblocked
+      # xxx very old
+      # Virus found - quarantined|
+      #amavisd-new-20030616
+      # INFECTED (JS/IllWill-A), <from@[127.0.0.1]> -> <to@sample.net>, quarantine virus-20040811-207-0-03, Message-ID: <0440.5577-101@sample.net>, Hits: -
+      # INFECTED (Exploit.HTML.IFrame, Worm.SomeFool.P), <from@sample.net> -> <to@example.com>,<to2@example.com>, quarantine qiO2ZG4K, Message-ID: <200608.5A5@mail.example.com>, Hits: -
+      #XXX (?:(Passed|Blocked) )?INFECTED \(([^\)]+)\),[A-Z .]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^(<]+)[)>]/o ))
+      #XXX elsif (($action, $key, $ip, $from, $to) = ( $p1 =~ /^(?:Virus found - quarantined|(?:(Passed|Blocked) )?INFECTED) \(([^\)]+)\),[A-Z .]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^(<]+)[(>]/o ))
+
+      # the first IP is the envelope sender.
+      if ($p1 !~ /^(CLEAN|SPAM(?:MY)?|INFECTED \(.*?\)|BANNED \(.*?\)|BAD-HEADER(?:-\d)?|UNCHECKED|MTA-BLOCKED|OVERSIZED|OTHER|TEMPFAIL)(?: {[^}]+})?, ([^[]+ )?(?:([^<]+) )?[<(](.*?)[>)] -> ([(<].*?[)>]), (?:.*Hits: ([-+.\d]+))(?:.* size: (\d+))?(?:.* autolearn=(\w+))?/) {
+         inc_unmatched('passblock');
+         next;
+      }
+      my $trigger;
+      my ($ccatmajor, $pbanks, $ips, $from, $reciplist, $hits, $size, $autolearn) = ($1, $2, $3, $4, $5, $6, $7, $8);
+
+      $Totals{'bytesscanned'} += $size  if defined $size;
+
+      #print "ccatmajor: \"$ccatmajor\", pbanks: \"$pbanks\"\n";
+      if ($ccatmajor =~ /^(INFECTED|BANNED) \((.*)\)$/) {
+         ($ccatmajor, $trigger) = ($1, $2);
+         #print "\tccatmajor: \"$ccatmajor\", trigger: \"$trigger\"\n";
+      }
+
+      $ccatmajor =~ s/(BAD-HEADER)-\d/$1/; # strip amavis 2.7's [:ccat|minor] BAD-HEADER sub-classification
+      $sectkey = $ccatmajor_to_sectkey{$ccatmajor} . $action;
+      $Totals{$sectkey}++;
+
+      # Not checked by spamassassin, due to $sa_mail_body_size_limit or @bypass_spam_checks_maps
+      if ($hits eq '-') {
+         # Don't increment sabypassed for INFECTED (SA intentionally not called)
+         unless ($ccatmajor eq 'INFECTED') {
+            # The following order is used, the first condition met decides the outcome:
+            #  1. a virus is detected: mail is considered infected;
+            #  2. contains banned name or type: mail is considered banned;
+            #  3. spam level is above kill level for at least one recipient, or a sender is blacklisted: mail is considered spam;
+            #  4. bad (invalid) headers: mail is considered as having a bad header.
+            # Priority: VIRUS BANNED UNCHECKED SPAM SPAMMY BADH OVERSIZED MTA CLEAN 
+            $Totals{'sabypassed'}++;
+         }
+      } else {
+         if ($Collecting{'spamscores'}) {
+            no re 'taint';
+            if ($hits =~ /^(-?[.\d]+)([-+])([.\d]+)$/) {
+               $hits = eval $1.$2.$3;   # untaint $hits, to sum $1 and $3 values
+            }
+            # SA not called for ccats INFECTED and BANNED (Hits: -).
+            # UNCHECKED may have a score, so we can't distinguish Ham from Spam
+            push @{$SpamScores{$ccatmajor_to_spamham{$ccatmajor}}}, $hits;
+         }
+      }
+
+      # autolearn is available here only if enabled in amavis template
+      if ($autolearn ne '' and $Opts{'autolearn'}) {
+      #if ($autolearn ne '' and ($ccatmajor eq 'SPAM' or $ccatmajor eq 'CLEAN')) {
+         #  SA 2.5/2.6 : ham/spam/no
+         #  SA 3.0+    : ham/spam/no/disabled/failed/unavailable
+         # printf "INC: autolearn: %s, %s: %d\n", $ccatmajor eq 'SPAM' ? 'Spam' : 'Ham', $autolearn, $Opts{'autolearn'};;
+         # Priorities other than SPAM will be considered HAM for autolearn stats
+         $Counts{'autolearn'}{$ccatmajor eq 'SPAM' ? 'Spam' : 'Ham'}{$autolearn}++;
+      }
+
+      # p0f fingerprinting
+      if (exists $p0ftags{$pid}) {
+         my ($ip,$score,$os) = split(/\//, $p0ftags{$pid});
+         $Counts{'p0f'}{ucfirst($ccatmajor_to_spamham{$ccatmajor})}{$os}{$ip}++;
+         #print "Deleting p0ftag: $pid\n";
+         delete $p0ftags{$pid};
+      }
+
+      next unless ($Collecting{$sectkey});
+      # cleanpassed never gets here...
+
+      # prefer xforward IP if it exists
+      # $ip_a => %a  original SMTP session client IP address (empty if unknown, e.g. no XFORWARD)
+      # $ip_e => %e  best guess of the originator IP address collected from the Received trace
+      my ($ip_a, $ip_e) = split(/ /, $ips, 2);
+
+      $ip = $ip_a ? $ip_a : $ip_e;
+      $ip =~ s/[[\]]//g;
+      #print "ip: \"$ip\", ip_a: \"$ip_a\", ip_e: \"$ip_e\", from: \"$from\", reciplist: \"$reciplist\"; hits: \"$hits\"\n";
+      $ip   = '*unknown IP'  if ($ip   eq '');
+      $from = '<>'           if ($from eq '');
+
+      # Show first recipient only, or all
+      my @recips = split /,/, $reciplist;
+      @recips = map { /^<(.+)>$/ } @recips;
+      # show only first recipient
+      $to = lc ($Opts{'first_recip_only'} ? $recips[0] : "@recips");
+
+      if ($ccatmajor eq 'INFECTED') {        # $ccatmajor: INFECTED  malwarepassed, malwareblocked
+         $Counts{$sectkey}{$trigger}{$to}{$ip}{$from}++;
+      }
+      elsif ($ccatmajor eq 'BANNED') {       # $ccatmajor: BANNED  bannednamepassed, bannednameblocked
+         $Counts{$sectkey}{$to}{$trigger}{$ip}{$from}++;
+      } else {
+         # $ccatmajor: CLEAN | SPAM{MY} | BAD-HEADER | UNCHECKED | MTA-BLOCKED | OVERSIZED | OTHER | TEMPFAIL
+         # cleanpassed, cleanblocked, spampassed, spamblocked, badheaderpassed, badheaderblocked
+         # uncheckedpassed, uncheckblocked, mtapassed, mtablocked, oversizedpassed, oversizedblocked
+         # otherpassed, otherblocked, tempfailpassed, tempfailblocked
+         $Counts{$sectkey}{$to}{$ip}{$from}++;
+      }
+
+      # old...
+      #XXX elsif (($action, $item, $ip, $from, $to) = ( $p1 =~ /^(?:(Blocked|Passed) )?BANNED (?:name\/type )?\((.+)\),[^[]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^(<]+)[(>]/o)) 
+      #XXXX  elsif (($action, $ip, $from, $to) = ( $p1 =~ /^(?:(Passed|Blocked) )?UNCHECKED,[^[]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^>)]*)[)>]/o ))
+      #XXX elsif (($action, $ip, $from, $to) = ( $p1 =~ /^(?:(Passed|Blocked) )?TEMPFAIL,[^[]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [<(]([^>)]*)[>)] -> [(<]([^>)]*)[)>]/o ))
+      #XXX elsif (($action, $ip, $from, $to) = ( $p1 =~ /^(?:(Blocked|Passed) )?BAD-HEADER,[^[]*(?: \[($re_IP)\])?(?: \[$re_IP\])* [(<]([^>)]*)[)>](?: -> [(<]([^>)]+)[)>])[^:]*/o ))
+      # amavis 2.3.1
+      #BAD-HEADER, <> -> <info@example.com>, Message-ID: <200506440.1.sample.net>, Hits=-3.3 tag1=3.0 tag2=7.5 kill=7.5, tests=ALL_TRUSTED=-3.3, [10.0.0.1] 
+   } # end Passed or Blocked
+
+   # MAIA
+   elsif ($p1 =~ /^FAKE SENDER, ([^:]+): ($[^,]+), (.*)$/o) {
+      #TD FAKE SENDER, SPAM: 192.168.0.1, bogus@example.com
+      $Totals{'fakesender'}++;            next unless ($Collecting{'fakesender'});
+      $Counts{'fakesender'}{$1}{$2}{$3}++;
+   }
+
+   elsif ($p1 =~ /^p\d+ \d+(?:\/\d+)* Content-Type: ([^,]+)(?:, size: [^,]+, name: (.*))?/) {
+      my ($ts, $name) = ($1, $2);
+      #TD p006 1 Content-Type: multipart/mixed
+      #TD p008 1/1 Content-Type: multipart/signed
+      #TD p001 1/1/1 Content-Type: text/plain, size: 460 B, name: 
+      #TD p002 1/1/2 Content-Type: application/pgp-signature, size: 189 B, name: 
+      #TD p002 1/2 Content-Type: application/octet-stream, size: 3045836 B, name: abc.pdf
+      next unless ($Collecting{'contenttype'});
+      my ($type, $subtype) = $ts !~ '""' ? split /\//, $ts : ('unspecified', 'unspecified');
+
+      $name = '' if !defined $name or $name =~ /^\s*$/;
+      $Counts{'contenttype'}{$type}{$subtype}{$name}++;
+   }
+
+   # LMTP/SMTP connection
+   # NOTE:  no longer used. size data now being obtained from Passed/Block line, as size info may not be available here
+   #elsif (my ($size) = ($p1 =~ /^[LS]MTP:(?:\[$re_IP\])?:\d+ [^:]+: [<(](?:.*?)[>)] -> \S+ (?:SIZE=(\d+))?.*?Received: / )) {
+   elsif ($p1 =~ /^[LS]MTP:/) {
+     #TD LMTP::10024 /var/spool/amavis/tmp/amavis-20070119T144757-09086: <from@example.com> -> <to@sample.net> SIZE=1000 Received: from mail.sample.net ([127.0.0.1]) by localhost (mail.sample.net [127.0.0.1]) (amavisd-new, port 10024) with LMTP for <to@sample.net>; Fri, 19 Jan 2007 15:41:45 -0800 (PST)
+     #TD SMTP:[127.0.0.1]:10024 /var/spool/amavis/tmp/amavis-20070119T144757-09086: <from@example.com> -> <to@sample.net>,<recip@sample.net> SIZE=2500000 Received: from mail.sample.net ([127.0.0.1]) by localhost (mail.sample.net [127.0.0.1]) (amavisd-new, port 10024) with LMTP for <to@sample.net>; Fri, 19 Jan 2007 15:41:45 -0800 (PST)
+     #TD SMTP::10024 /var/lib/amavis/tmp/amavis-27-26927: <from@example.com> -> <to@example.net> Received: from localhost ([127.0.0.1]) by localhost (example.com [127.0.0.1]) (amavisd-new, port 10024) with SMTP for <to@example.net>; Sat,  7 Jun 2008 23:09:34 +0200 (CEST)
+     #$Totals{'bytesscanned'} += $size  if defined $size;
+   }
+
+   #(\S+) ([^[(]+)(.*)$
+   elsif ($p1 =~ /^OS_fingerprint: (\S+) ([-\d.]+) (\S+)(?: ([^[(]+|\[[^]]+\]))?/o) {
+      #TD OS_fingerprint: 213.193.24.113 29.789 Linux 2.6 (newer, 1) (up: 1812 hrs), (distance 14, link: ethernet/modem)
+      #TD OS_fingerprint: 10.47.2.155 -1.312 MYNETWORKS
+      # Note: safe to delete entry when the final Passed/Block line occurs
+      if ($Collecting{'p0f'}) {
+         my ($genre,$vers) = ($3,$4);
+         #print "p0f:\t$3\t\t$vers\n";
+         if ($genre eq 'Windows') {
+            local($1);
+            $vers = $1  if $vers =~ /^(\S+) /;
+            $genre .= ' ' . $vers;
+         }
+         elsif ($genre eq 'UNKNOWN') {
+            $genre = 'Unknown';
+         }
+         $p0ftags{$pid} = join('/', $1,$2,$genre);
+         #print "Added PID: $pid, $p0ftags{$pid}\n";
+      }
+   }
+
+   elsif ( ($reason) = ( $p1 =~ /^BAD HEADER from [^:]+: (.+)$/) or
+           ($reason) = ( $p1 =~ /check_header: \d, (.+)$/)) {
+      # When log_level > 1, provide additional header or MIME violations
+
+      # amavisd < 2.4.0, log_level >= 1
+      #TD BAD HEADER from <bogus@example.com>: Improper use of control character (char 0D hex) in message header 'Received': Received: example.com[10.0.0.1\r]
+      #TD BAD HEADER from <bogus@example.com>: Non-encoded 8-bit data (char F7 hex) in message header 'Subject': Subject: \367\345\370\361 \344\351\351\362\345\365\n    
+      #TD BAD HEADER from <bogus@example.com>: MIME error: error: part did not end with expected boundary
+      #TD BAD HEADER from (bulk ) <bogus@bounces@lists.example.com>: Non-encoded 8-bit data (char E6 hex) in message header 'Subject': Subject: spam\\346ham\\n 
+      #TD BAD HEADER from (list) <bogus@bounces@lists.example.com>: MIME error: error: part did not end with expected boundary
+      #  amavisd >= 2.4.3, log_level >= 2
+      #TD check_header: 2, Non-encoded 8-bit data (char AE hex): Subject: RegionsNet\\256 Online Banking\\n
+      #TD check_header: 2, Non-encoded 8-bit data (char E1 hex): From: "any user" <from\\341k@example.com>\\n
+      #TD check_header: 3, Improper use of control character (char 0D hex): Content-type: text/html; charset=i...
+      #TD check_header: 8, Duplicate header field: "Reply-To"
+      #TD check_header: 8, Duplicate header field: "Subject"
+      #TD check_header: 4, Improper folded header field made up entirely of whitespace (char 09 hex): X-Loop-Detect: 3\\n\\t\\n
+      #TD check_header: 4, Improper folded header field made up entirely of whitespace: Received: ...8 ;         Thu, 10 Jan 2008 03:41:35 +0100\\n\\t \\n
+
+
+      my $subreason;
+      if ($reason =~ /^(.*?) \((char \S+ hex)\)(.*)$/) {
+         $reason = $1;
+         my ($char,$sub) = ($2,$3);
+
+         $sub =~ s/^in message header '[^:]+': //;
+         $sub =~ s/^: //;
+         $subreason = "$char: $sub";
+      }
+      elsif ($reason =~ /^(Improper folded header field made up entirely of whitespace):? (.*)/) {
+         $reason = $1;
+         $subreason = $2;
+      }
+      elsif ($reason =~ /^(Duplicate header field): "(.+)"$/) {
+         $reason = $1;
+         $subreason = $2;
+      }
+      elsif ($reason =~ /^(MIME error): (?:error: )?(.+)$/) {
+         $reason = $1;
+         $subreason = $2;
+      }
+
+      $Totals{'badheadersupp'}++;            next unless ($Collecting{'badheadersupp'});
+      $Counts{'badheadersupp'}{$reason}{$subreason}++;
+   }
+
+   elsif ($p1 =~ /^truncating a message passed to SA at/) {
+      #TD truncating a message passed to SA at 431018 bytes, orig 1875912
+      $Totals{'truncatedmsg'}++;
+   }
+
+   elsif ($p1 =~ /: spam level exceeds quarantine cutoff level/ or
+          $p1 =~ /: cutoff, blacklisted/) {
+      #TD do_notify_and_quarantine: spam level exceeds quarantine cutoff level 20
+      #TD do_notify_and_quarantine: cutoff, blacklisted
+      $Totals{'spamdiscarded'}++;
+   }
+
+   elsif ( $p1 =~ /^spam_scan: (.*)$/) {
+      #if ($1 =~ /^not wasting time on SA, message longer than/ ) {
+         #TD spam_scan: not wasting time on SA, message longer than 409600 bytes: 1326+4115601
+         # this causes duplicate counts, and the subsequent Passed/Blocked log line
+         # will have "Hits: -," whereby sabypassed is incremented.
+         #$Totals{'sabypassed'}++;
+      #}
+      # ignore other spam_scan lines
+   }
+
+   # WARN:
+   elsif ( ($reason) = ( $p1 =~ /^WARN: MIME::Parser error: (.*)$/ )) {
+      # WARN: MIME::Parser error: unexpected end of header
+      $Totals{'mimeerror'}++;                next unless ($Collecting{'mimeerror'});
+      $Counts{'mimeerror'}{$reason}++;
+   }
+
+   elsif ($p1 =~ /^WARN: address modified \((\w+)\): <(.*?)> -> <(.*)>$/) {
+      #TD WARN: address modified (sender): <root> -> <root@>
+      #TD WARN: address modified (recip): <root> -> <root@>
+      #TD WARN: address modified (recip): <postmaster> -> <postmaster@>
+      #TD WARN: address modified (recip): <"test@example.com"@> -> <"teszt@example.com">
+      #TD WARN: address modified (sender): <fr\344om@sample.net> -> <"fr\344om"@sample.net>
+      $Totals{'warningaddressmodified'}++;   next unless ($Collecting{'warningaddressmodified'});
+      $Counts{'warningaddressmodified'}{$1 eq 'sender' ? "Sender address" : "Recipient address"}{"$2 -> $3"}++;
+   }
+
+   # NOTICE:
+   elsif ($p1 =~ /^NOTICE: (.*)$/) {
+      # uninteresting
+      #TD NOTICE: reconnecting in response to: err=2006, HY000, DBD::mysql::st execute failed: MySQL server has gone away at (eval 71) line 166, <GEN168> line 4.
+      next if ($1 =~ /^Disconnected from SQL server/); # redundant
+      next if ($1 =~ /^do_search: trying again: LDAP_OPERATIONS_ERROR/);
+      next if ($1 =~ /^reconnecting in response to: /);
+
+
+      if ($1 =~ /^Not sending DSN, spam level ([\d.]+ )?exceeds DSN cutoff level/) {
+         #TD NOTICE: Not sending DSN, spam level exceeds DSN cutoff level for all recips, mail intentionally dropped
+         $Totals{'dsnsuppressed'}++;
+         $Counts{'dsnsuppressed'}{'DSN cutoff exceeded'}++;
+      }
+      elsif ($1 =~ /^Not sending DSN to believed-to-be-faked sender/) {
+         #TD NOTICE: Not sending DSN to believed-to-be-faked sender <user@example.com>, mail containing VIRUS intentionally dropped 
+         $Totals{'dsnsuppressed'}++;
+         $Counts{'dsnsuppressed'}{'Sender likely faked'}++;
+      }
+      elsif ($1 =~ /^DSN contains [^;]+; bounce is not bounc[ai]ble, mail intentionally dropped/) {
+         $Totals{'dsnsuppressed'}++;
+         $Counts{'dsnsuppressed'}{'Not bounceable'}++;
+      }
+      elsif ($1 =~ /^UNABLE TO SEND DSN to /) {
+         #TD NOTICE: UNABLE TO SEND DSN to <user@example.com>: 554 5.7.1 Failed, id=19838-01, from MTA([127.0.0.1]:10025): 554 5.7.1 <user@example.com>: Recipient address rejected: Access denied
+         $Totals{'dsnsuppressed'}++;
+         $Counts{'dsnsuppressed'}{'Unable to send'}++;
+      }
+
+      elsif ($1 =~ /^Skipping (?:bad|extra) output from file\(1\)/) {
+         #TD NOTICE: Skipping extra output from file(1): blah
+         #TD NOTICE: Skipping bad output from file(1) at [1, p002], got: blah
+         $Totals{'fileoutputskipped'}++;
+      }
+      elsif (($p1) = ($1 =~ /^Virus scanning skipped: (.*)$/)) {
+         #TD NOTICE: Virus scanning skipped: Maximum number of files (1500) exceeded at (eval 57) line 1283, <GEN212> line 1501.
+         $Totals{'virusscanskipped'}++;      next unless ($Collecting{'virusscanskipped'});
+         $Counts{'virusscanskipped'}{strip_trace($p1)}++;
+      }
+      else {
+         inc_unmatched('NOTICE');
+         next;
+      }
+   }
+
+   # INFO:
+   elsif ($p1 =~ /^INFO: (.*)$/) {
+      next if ($1 =~ /^unfolded \d+ illegal all-whitespace continuation line/);
+      next if ($1 =~ /^removed bare CR/);
+
+      if ($1 =~ /^truncat(ed|ing)/) {
+         #TD INFO: truncating long header field (len=2639): X-Spam-Report: =?iso-8859-1?Q?=0A=0A*__1=2E7_SUBJECT=5FENCODED=5FTWICE_Subject=3A_MIME_e?= =?iso-885...
+         #TD INFO: truncated 1 header line(s) longer than 998 characters
+         $Totals{'truncatedheader'}++;
+      } elsif ( $1 =~ /^no existing header field 'Subject', inserting it/) {
+         $Totals{'nosubject'}++;
+      }
+      elsif (my ($savers1, $savers2, $item) = ( $1 =~ /^(?:SA version: ([^,]+), ([^,]+), )?no optional modules: (.+)$/)) {
+         #TD INFO: SA version: 3.1.8, 3.001008, no optional modules: DBD::mysql Mail::SpamAssassin::Plugin::DKIM Mail::SpamAssassin::Plugin::URIDetail Error
+         next unless ($Opts{'startinfo'});
+         if ($savers1 ne '') {
+            $StartInfo{'sa_version'} = "$savers1 ($savers2)";
+         }
+         foreach my $code (split / /, $item) {
+            $StartInfo{'Code'}{'Not loaded'}{$code} = "";
+         }
+      }
+      elsif (my ($name) = ( $1 =~ /^(unknown banned table name \S+), .+$/)) {
+         #TD INFO: unknown banned table name 1, recip=r@example.com
+         $Totals{'warning'}++;      next unless ($Collecting{'warning'});
+         $Counts{'warning'}{ucfirst $name}++;
+      }
+      else {
+         inc_unmatched('INFO');
+         next;
+      }
+   }
+
+   elsif ( ($action,$reason,$from,$to) = ($p1 =~ /^DSN: NOTIFICATION: Action:([^,]+), ([^,]+), <(.*?)> -> <(.*?)>/)) {
+      #TD DSN: NOTIFICATION: Action:failed, LOCAL 554 Banned, <from@example.net> -> <to@example.com>
+      #TD DSN: NOTIFICATION: Action:delayed, LOCAL 454 Banned, <from@example.com> -> <to@example.net>
+
+      $Totals{'dsnnotification'}++;    next unless ($Collecting{'dsnnotification'});
+      $Counts{'dsnnotification'}{$action}{$reason}{"$from -> $to"}++;
+   }
+
+   elsif (($item, $from, $to) = ( $p1 =~ /^Quarantined message release(?: \([^)]+\))?: ([^ ]+) <(.*?)> -> (.+)$/) or
+          ($item, $from, $to) = ( $p1 =~ /^Quarantine release ([^ ]+): overriding recips <([^>]*)> by (.+)$/)) {
+      #TD Quarantine release arQcr95dNHaW: overriding recips <TO@EXAMPLE.COM> by <to@example.com>
+      #TD Quarantined message release: hiyPJOsD2m9Z <from@sample.net> -> <to@example.com>
+      #TD Quarantined message release: hiyPJOsD2m9Z <> -> <to@recipient.maildir>,<anyone@example.com>
+      # 2.6+
+      #TD Quarantined message release (miscategorized): Iu6+0u1voOA <from@example.com> -> <to@example.net>
+      $Totals{'released'}++;           next unless ($Collecting{'released'});
+      $from = '<>' if ($from eq '');
+      $to =~ s/[<>]//g;
+      $Counts{'released'}{"\L$from"}{$to}{$item}++;
+   }
+   elsif ($p1 =~ /^Quarantine release ([^:]+): missing X-Quarantine-ID$/) {
+      #TD Quarantine release 7ejEBC7MThSc: missing X-Quarantine-ID
+      $Totals{'warningnoquarantineid'}++; next unless ($Collecting{'warningnoquarantineid'});
+      $Counts{'warningnoquarantineid'}{$1}++;
+   }
+
+   elsif ( ($stage,$reason) = ($p1 =~ /^Negative SMTP resp\S* +to ([^:]+): *(.*)$/)) {
+      #TD Negative SMTP response to data-dot (<u@example.com>): 550 5.7.1 Header Spam Rule 4
+      $Totals{'smtpresponse'}++;       next unless ($Collecting{'smtpresponse'});
+      $Counts{'smtpresponse'}{'Negative response'}{$stage}{$reason}++;
+   }
+   elsif ( ($stage,$reason) = ($p1 =~ /^smtp resp to ([^:]+): *(.*)$/)) {
+      #TD smtp resp to NOOP (idle 4799.4 s): 421 4.4.2 nops.overtops.org Error: timeout exceeded 
+      #TD smtp resp to MAIL (pip): 250 2.1.0 Ok
+      $Totals{'smtpresponse'}++;       next unless ($Collecting{'smtpresponse'});
+      $stage =~ s/ [\d.]+ s//;
+      $Counts{'smtpresponse'}{'Response'}{$stage}{$reason}++;
+   }
+
+   elsif ( ($item) = ($p1 =~ /^response to RCPT TO for <([^>]*)>: "501 Bad address syntax"/)) {
+      #TD response to RCPT TO for <""@example.com>: "501 Bad address syntax"
+      $Totals{'badaddress'}++;         next unless ($Collecting{'badaddress'});
+      $Counts{'badaddress'}{$item}++;
+   }
+
+   # do_unip: archive extraction
+   elsif ($p1 =~ s/^do_unzip: \S+, //) {
+      $Totals{'archiveextract'}++;     next unless ($Collecting{'archiveextract'});
+
+      if ( $p1 =~ s/^\d+ members are encrypted, //) {
+         #TD do_unzip: p003, 4 members are encrypted, none extracted, archive retained
+         $Counts{'archiveextract'}{'Encrypted'}{$p1}++;
+
+      } elsif ( $p1 =~ /^zero length members, archive retained/) {
+         #TD do_unzip: p002, zero length members, archive retained
+         $Counts{'archiveextract'}{'Empty member'}{''}++;
+
+      } elsif ($p1 =~ s/^unsupported compr\. method: //) {
+         #TD do_unzip: p003, unsupported compr. method: 99
+         $Counts{'archiveextract'}{'Unsupported compression'}{$p1}++;
+      }
+      else {
+         $Counts{'archiveextract'}{'*unknown'}{$p1}++;
+      }
+   }
+
+   # do_cabextract: archive extraction
+   elsif ($p1 =~ s/^do_cabextract: //) {
+      #TD do_cabextract: can't parse toc line:  File size | Date       Time     | Name
+      #TD do_cabextract: can't parse toc line: All done, no errors.
+      $Totals{'archiveextract'}++;     next unless ($Collecting{'archiveextract'});
+
+      if ($p1 =~ /^([^:]+):\s*(.*)/) {
+         $Counts{'archiveextract'}{"\u$1"}{$2}++;
+      } else {
+         $Counts{'archiveextract'}{$p1}{''}++;
+      }
+   }
+
+   elsif ($p1 =~ /^(?:\(!\) *)?SA TIMED OUT,/) {
+      $Totals{'satimeout'}++;
+   }
+
+   elsif ($p1 =~ /^mangling (.*)$/) {
+      $p1 = $1;
+      if ($p1 =~ /^by (.+?) failed: (.+?), mail will pass unmodified$/) {
+         #TD mangling by altermine failed: SomeText, mail will pass unmodified
+         $Totals{'defangerror'}++;        next unless ($Collecting{'defangerror'});
+         $Counts{'defangerror'}{$1}{$2}++;
+      }
+      # other mangle message skipped
+      else {
+         #TD mangling YES: 1 (orig: 1), discl_allowed=0, <from@example.com> -> <to@sample.net>
+         #TD mangling by built-in defanger: 1, <user@example.com>
+         next;
+      }
+   }
+   elsif ($p1 =~ /^DEFANGING MAIL: (.+)$/) {
+      # log_level 1
+      #TD DEFANGING MAIL: WARNING: possible mail bomb, NOT CHECKED FOR VIRUSES:\n  Exceeded storage quota 5961070 bytes by d... 
+      #TD DEFANGING MAIL: WARNING: bad headers - Improper use of control character (char 0D hex): To: <to@example.com\\r>,\\n\\t<to@example.com>
+      # could use instead...
+      #do_log(1,"mangling by %s (%s) done, new size: %d, orig %d bytes", $actual_mail_mangle, $mail_mangle, $repl_size, $msginfo->msg_size);
+      $Totals{'defanged'}++;        next unless ($Collecting{'defanged'});
+      $Counts{'defanged'}{$1}++;
+   }
+
+   elsif ($p1 =~ /^PenPalsSavedFromKill [-.\d]+,/) {
+      #TD PenPalsSavedFromKill 8.269-3.160, <ulyanov@steelpro.com.ua> -> <recipient1@recipientdomain.com>
+      $Totals{'penpalsaved'}++;
+   }
+
+   # I don't know how many variants of time outs there are... I suppose we'll fix as we go
+   elsif (($p1 =~ /^\(!+\)([^ ]*) is taking longer than \d+ s and will be killed/) or 
+          ($p1 =~ /^\(!+\)(.*) av-scanner FAILED: timed out/) or
+          ($p1 =~ /^(?:\(!+\))?(.*): timed out/))
+   {
+      #TD (!)/usr/local/bin/uvscan is taking longer than 10 s and will be killed
+      #TD (!!)NAI McAfee AntiVirus (uvscan) av-scanner FAILED: timed out
+      #TD ClamAV-clamd: timed out, retrying (1)
+      #TD (!)Sophie: timed out, retrying (2)
+
+      $Totals{'avtimeout'}++;                next unless ($Collecting{'avtimeout'});
+      $Counts{'avtimeout'}{$1}++;
+   }
+   elsif (($p2) = ($p1 =~ /SMTP shutdown: (.*)$/)) {                      # log level -1
+      #TD SMTP shutdown: Error writing a SMTP response to the socket: Broken pipe at (eval 49) line 836, <GEN232> line 51.
+      #TD SMTP shutdown: tempdir is to be PRESERVED: /var/amavis/tmp/amavis-20070704T095350-13145
+      strip_trace($p2);
+      if ($p2 =~ /^tempdir is to be PRESERVED: (.*)\/([^\/]+)$/) {
+         $Totals{'tmppreserved'}++;
+         $Counts{'tmppreserved'}{$1}{$2}++   if ($Collecting{'tmppreserved'});
+         $p2 = "Preserved tempdir in $1";
+      }
+      $Totals{'warningsmtpshutdown'}++;      next unless ($Collecting{'warningsmtpshutdown'});
+      $Counts{'warningsmtpshutdown'}{ucfirst($p2)}++;
+   }
+
+   elsif (($p1 =~ /PRESERVING EVIDENCE in (.*)\/([^\/]+)$/) or
+         ($p1 =~ /tempdir is to be PRESERVED: (.*)\/([^\/]+)$/)) {
+      #TD (!)TempDir removal: tempdir is to be PRESERVED: /var/amavis/tmp/amavis-20080110T173606-05767
+      # log level -1
+      #TD PRESERVING EVIDENCE in /var/amavis/tmp/amavis-20070704T111558-14883
+      $Totals{'tmppreserved'}++;             next unless ($Collecting{'tmppreserved'});
+      $Counts{'tmppreserved'}{$1}{$2}++;
+   }
+
+   elsif ($p1 =~ /^Open relay\? Nonlocal recips but not originating/) {
+      $Totals{'warningsecurity'}++;
+      $Counts{'warningsecurity'}{$p1}++    if ($Collecting{'warningsecurity'});
+   }
+
+   # keep before general warnings below, so sadiag gets first crack at log
+   # lines beginning with "(!) ...".
+   elsif ($p1 =~ /^(?:\(!+\))?\!?SA (warn|info|error): (.*)$/) {
+      #TD SA warn: FuzzyOcr: Cannot find executable for gocr
+      my ($level,$msg) = ($1,$2);
+
+      # XXX later, maybe break out stats on FuzzyOcr
+      # skip "image too small" for now
+      if ($msg =~ /^FuzzyOcr: Skipping .+, image too small$/) {
+         #TD SA warn: FuzzyOcr: Skipping ocrad, image too small
+         #TD SA warn: FuzzyOcr: Skipping ocrad-decolorize, image too small
+         #$Counts{'sadiags'}{'fuzzyocr'}{'image too small'}++;
+         next;
+      }
+      elsif ($msg =~ /dns: \[\.\.\.\]/) {
+        #TD SA info: dns: [...] ;; ADDITIONAL SECTION (1 record)
+        next;
+      }
+      # canonicalize some PIDs and IDs
+      elsif ($msg =~ s/^pyzor: \[\d+\] error/pyzor: [<PID>] error/) {
+        #TD SA info: pyzor: [11550] error: TERMINATED, signal 15 (000f)
+      }
+      elsif ($msg =~ /dns: no likely matching queries for id \d+/) {
+        $msg =~ s/\d+/<ID>/;
+      }
+      elsif ($msg =~ /dns: no callback for id \d+/) {
+        $msg =~ s/\d+.*$/<ID>.../;
+      }
+
+      # report other SA warn's
+      $Totals{'sadiags'}++;
+      next unless ($Collecting{'sadiags'});
+      $Counts{'sadiags'}{ucfirst($level)}{$msg}++;
+   }
+
+   # catchall for most other warnings
+   elsif (($p1 =~ /^\(!+\)/) or
+          ($p1 =~ /^TROUBLE/) or
+          ($p1 =~ /Can't (?:connect to UNIX|send to) socket/) or
+          ($p1 =~ /: Empty result from /) or
+          ($p1 =~ /: Error reading from socket: Connection reset by peer/) or
+          ($p1 =~ /open\(.*\): Permission denied/) or
+          ($p1 =~ /^_?WARN: /) or
+          ($p1 =~ /Can't send SIG \d+ to process \[\d+\]: Operation not permitted/) or
+          ($p1 =~ /(policy protocol: INVALID(?: AM\.PDP)? ATTRIBUTE LINE: .*)$/) or
+          ($p1 =~ /(DKIM signature verification disabled, corresponding features not available. If not intentional.*)$/)
+         )
+   {
+      #TD (!)loading policy bank "AM.PDP-SOCK": unknown field "0"
+      #TD (!!)policy_server FAILED: SQL quarantine code not enabled at (eval 37) line 306, <GEN6> line 4.
+      #TD (!!)policy_server FAILED: Can't open file /var/spool/amavis/quarantine/spam-CFJYXmeS+FLy: Permission denied at (eval 37) line 330, <GEN28> line 5.
+      #TD ClamAV-clamd: Empty result from /var/run/clamav/clamd, retrying (1)
+      #TDdcc open(/var/dcc/map): Permission denied
+      #TD TROUBLE in check_mail:  FAILED: Died at /usr/sbin/amavisd-maia line 2872, <GEN4> line 22.
+      #TD TROUBLE in check_mail: spam_scan FAILED: DBD::mysql::st execute failed: MySQL server has gone away at /usr/sbin/amavisd-maia line 3786, <GEN4> line 3036.
+      #TD TROUBLE in process_request: DBD::mysql::st execute failed: MySQL server has gone away at (eval 35) line 258, <GEN18> line 3.
+      #TD TROUBLE in process_request: DBD::mysql::st execute failed: Lost connection to MySQL server during query at (eval 35) line 258, <GEN3> line 3.
+      #TD TROUBLE in process_request: Can't call method "disconnect" on an undefined value at /usr/sbin/amavisd-maia line 2895, <GEN4> line 22.
+      #TD TROUBLE: recipient not done: <to@example.com> smtp response ...
+      #TD (!!)TROUBLE in process_request: Can't create file /var/amavis/tmp/amavis-98/email.txt: File exists at /usr/local/sbin/amavisd line 4774, <GEN12> line 4.
+      #TD TROUBLE: lookup table is an unknown object: object ...
+      #TD (!) policy protocol: INVALID ATTRIBUTE LINE: /var/spool/courier/tmp/114528/D967099\n 
+      #TD (!) policy protocol: INVALID AM.PDP ATTRIBUTE LINE: /var/spool/courier/tmp/114528/D967099\n 
+      #TD _WARN: bayes: cannot open bayes databases /var/spool/amavis/.spamassassin/bayes_* R/W: lock failed: Interrupted system call\n
+
+      $p1 =~ s/^\(!+\)s*//;
+
+      if ($p1 =~ /^WARN: (Using cpio instead of pax .*)$/) {
+         #TD (!)WARN: Using cpio instead of pax can be a security risk; please add:  $pax='pax';  to amavisd.conf and check that the pax(1) utility is available on the system!
+         $Totals{'warningsecurity'}++;
+         $Counts{'warningsecurity'}{$1}++    if ($Collecting{'warningsecurity'});
+         next;
+      }
+
+      $p1 =~ s/, retrying\s+\(\d+\)$//;
+      strip_trace($p1);
+
+      # canonicalize variations of the same message
+      $p1 =~ s/^run_av \(([^,]+), built-in i\/f\)/$1/;
+      $p1 =~ s/ av-scanner FAILED: CODE\(0x[^)]+\)/:/;
+      $p1 =~ s/^(.+: Too many retries to talk to \S+) .*/$1/;
+
+      if (($p1 =~ /(\S+): Can't (?:connect|send) to (?:UNIX )?(.*)$/) or
+          ($p1 =~ /(\S+): (Too many retries to talk to .*)$/))
+      {
+
+         #TD (!)ClamAV-clamd: Can't connect to UNIX socket /var/run/clamav/clamd.socket: No such file or directory, retrying (2)
+         #TD (!)ClamAV-clamd: Can't connect to UNIX socket /var/run/clamav/clamd: Connection refused, retrying (2)
+         #TD ClamAV-clamd: Can't connect to UNIX socket /var/run/clamav/clamd: Connection refused, retrying (1)
+         #TD ClamAV-clamd: Can't send to socket /var/run/clamav/clamd: Transport endpoint is not connected, retrying (1)
+         #TD Sophie: Can't send to socket /var/run/sophie: Transport endpoint is not connected, retrying (1)
+         #TD (!)run_av (Sophie, built-in i/f): Too many retries to talk to /var/run/sophie (timed out) at (eval 55) line 310, <GEN16> line 16.
+         #TD (!)run_av (ClamAV-clamd, built-in i/f): Too many retries to talk to /var/run/clamav/clamd.socket (Can't connect to UNIX socket /var/run/clamav/clamd.socket: No such file or directory) at (eval 52) line 310.
+         #TD (!!)ClamAV-clamd av-scanner FAILED: CODE(0x804fa08) Too many retries to talk to /var/run/clamav/clamd.socket (Can't connect to UNIX socket /var/run/clamav/clamd.socket: No such file or directory) at (eval 52) line 310. at (eval 52) line 511.
+         #TD (!!)Sophie av-scanner FAILED: CODE(0x814fd24) Too many retries to talk to /var/run/sophie (timed out) at (eval 55) line 310, <GEN16> line 16. at (eval 55) line 511, <GEN16> line 16.
+
+         $Totals{'avconnectfailure'}++;
+         $Counts{'avconnectfailure'}{$1}{ucfirst($2)}++     if ($Collecting{'avconnectfailure'});
+         next;
+      }
+
+      # simplify or canonicalize variations of the same message
+      $p1 =~ s/^TROUBLE(:| in) //;
+      $p1 =~ s/^_?WARN: //;
+      $p1 =~ s/Can't create file \S+: (.+)$/Can't create file: $1/;
+      $p1 =~ s/Can't send SIG \d+ to process \[\d+\]/Can't send SIG to process/;
+
+      $Totals{'warning'}++;      next unless ($Collecting{'warning'});
+      $Counts{'warning'}{$p1}++;
+   }
+
+   # Begin forced warnings: Keep this code below warning catchall
+   elsif ($p1 =~ /^lookup_sql: /) {
+      #TD lookup_sql: 2006, MySQL server has gone away
+      $Totals{'warningsql'}++;   next unless ($Collecting{'warningsql'});
+      $Counts{'warningsql'}{'SQL died'}++;
+
+   } elsif (($reason,$item) = ($p1 =~ /^connect_to_sql: ([^']+) '\S+': (.*?)(?: \(\d+\))?$/) or
+            ($item,$reason) = ($p1 =~ /^lookup_sql_field\((.*)\) \(WARN: (no such field in the SQL table)\)/)) {
+      #TD connect_to_sql: unable to connect to DSN 'DBI:mysql:maia:sqlhost1.example.com': Lost connection to MySQL server during query
+      #TD connect_to_sql: unable to connect to DSN 'DBI:mysql:maia:sqlhost2.example.com': Can't connect to MySQL server on 'sqlhost2.example.com' (111)
+      #TD lookup_sql_field(id) (WARN: no such field in the SQL table), "from@example.com" result=undef
+      $Totals{'warningsql'}++;   next unless ($Collecting{'warningsql'});
+      $Counts{'warningsql'}{ucfirst("$reason: $item")}++;
+   }
+   # End forced warnings
+
+   # panic
+   elsif ( ($p2) = ($p1 =~ /^(?:\(!\)\s*)?PANIC, (.*)$/)) {
+      #TD PANIC, PANIC, SA produced a clone process of [19122], TERMINATING CLONE [19123]
+
+      $Totals{'panic'}++;        next unless ($Collecting{'panic'});
+      $Counts{'panic'}{$p2}++;
+
+   }
+
+   # fatal
+   elsif ( $p1 =~ /^Requesting process rundown after fatal error$/) {
+      #TD Requesting process rundown after fatal error
+      $Totals{'fatal'}++;        next unless ($Collecting{'fatal'});
+      $Counts{'fatal'}{$p1}++;
+
+   # DCC
+   } elsif (($reason) = ($p1 =~ /^(missing message body; fatal error)/) or
+            ($reason) = ($p1 =~ /^(try to start dccifd)/)) {
+      $Totals{'dccerror'}++;     next unless ($Collecting{'dccerror'});
+      $Counts{'dccerror'}{ucfirst($reason)}++;
+   }
+   elsif ($p1 =~ /^continue not asking DCC \d+ seconds after failure/) {
+      $Totals{'dccerror'}++;     next unless ($Collecting{'dccerror'});
+      $Counts{'dccerror'}{'Continue not asking DCC after failure'}++;
+   }
+   elsif ($p1 =~ /^no DCC answer from (\S+) after \d+ ms$/) {
+      $Totals{'dccerror'}++;     next unless ($Collecting{'dccerror'});
+      $Counts{'dccerror'}{"No answer from $1"}++;
+   }
+
+   elsif ( ($reason, $from, $to) = ($p1 =~ /^skip local delivery\((\d+)\): <(.*?)> -> <(.*?)>$/)) {
+      $Totals{'localdeliveryskipped'}++;   next unless ($Collecting{'localdeliveryskipped'});
+      $from = '<>' if ($from eq '');
+      $reason = $reason == 1 ? "No localpart" : $reason == 2 ? "Local alias is null" : "Other";
+      $Counts{'localdeliveryskipped'}{$reason}{$from}{$to}++;
+   }
+
+   # hard and soft whitelisted/blacklisted
+   elsif ($p1 =~ /^wbl: (.*)$/) {
+      # ignore wbl entries, can't think of good way to reliably summarize.
+      # and 'black or whitelisted by all' makes using by-white or -black list
+      # groupings impossible
+      next;
+=cut
+      $p1 = $1;
+
+      # TD wbl: black or whitelisted by all recips
+      next if ($p1 =~ /^black or whitelisted/); # not clear how to report this, so skip
+      next if ($p1 =~ /^checking sender/);                    # ll 4
+      next if ($p1 =~ /^(LDAP) query keys/);                  # ll 5
+      next if ($p1 =~ /^(LDAP) recip/);                       # ll 5
+      next if ($p1 =~ /^recip <[^>]*> (?:black|white)listed sender/);  # ll 5
+
+      # lookup order: SQL, LDAP, static
+      if ($p1 =~ s/^\(SQL\) recip <[^>]*>//) {
+         next if ($p1 =~ /^, \S+ matches$/);                  # ll 5
+         next if ($p1 =~ /^, rid=/);                          # ll 4
+         next if ($p1 =~ /^ is neutral to sender/);           # ll 5
+         next if ($p1 =~ /^ (?:white|black)listed sender </); # ll 5
+         # ll -1
+         #wbl: (SQL) recip <%s> whitelisted sender <%s>, '.  unexpected wb field value
+      }
+      #ll2
+      # wbl: (SQL) soft-(white|black)listed (%s) sender <%s> => <%s> (rid=%s)',  $val, $sender, $recip, $user_id);
+      # multiple senders: message sender, then "from", etc.
+      #ll2
+      # wbl: soft-(white|black)listed (%s) sender <%s> => <%s>,
+
+      #TD wbl: whitelisted sender <sender@example.com>
+      #TD wbl: soft-whitelisted (-3) sender <from@example.com> => <to@sample.net>, recip_key="."
+      #TD wbl: whitelisted by user@example.com, but not by all, sender <bounces@example.net>, <user@example.org>
+      # wbl: (whitelisted|blacklisted|black or whitelisted by all recips|(white|black)listed by xxx,yyy,... but not by all) sender %s
+
+      if ($p1 =~ /^(?:\(SQL\) )?(?:(soft)-)?((?:white|black)listed)(?: \([^)]+\))? sender <([^>]*)>/) {
+         my ($type,$list,$sender) = ($1,$2,$3);
+         $Totals{$list}++;       next unless ($Collecting{$list});
+         $type = $type ? 'Soft' : 'Hard' ;
+         my ($localpart, $domainpart) = split (/@/, lc $sender);
+         ($localpart, $domainpart) = ($sender, '*unspecified')   if ($domainpart eq '');
+         $Counts{$list}{$type}{$domainpart}{$localpart}++;
+      }
+      else {
+         inc_unmatched('wbl');
+         next;
+      }
+=cut
+   }
+
+   # XXX: WHITELISTED or BLACKLISTED should be caught in SPAM tag above
+   elsif (($p1 =~ /^white_black_list: whitelisted sender/) or
+          ($p1 =~ /.* WHITELISTED/) ) {
+      $Totals{'whitelisted'}++;
+
+   } elsif (($p1 =~ /^white_black_list: blacklisted sender/) or
+               ( $p1 =~ /.* BLACKLISTED/) ) {
+      $Totals{'blacklisted'}++;
+
+   } elsif ($p1 =~ /^Turning AV infection into a spam report: score=([^,]+), (.+)$/) {
+      #TD Turning AV infection into a spam report: score=4.1, AV:Sanesecurity.ScamL.375.UNOFFICIAL=4.1
+      #TD Turning AV infection into a spam report: score=3.4, AV:Sanesecurity.Phishing.Cur.180.UNOFFICIAL=3.1,AV:Sanesecurity.Phishing.Cur.180.UNOFFICIAL=3.4
+      #BAT.Backdoor.Poisonivy.E178-SecuriteInfo.com
+
+      next unless ($Collecting{'malwaretospam'});
+      #my $score_max = $1;
+      my @list = split (/,/, $2);
+      @list = unique_list(\@list);
+      foreach (@list) {
+         my ($name,$score) = split (/=/,$_);
+         $name =~ s/^AV://;
+         my $type = $name =~ s/\.UNOFFICIAL$// ? 'Unofficial' : 'Official';
+         # strip trailing numeric variant (...Phishing.Cur.863)
+         my $variant = $name =~ s/([.-]\d+)$// ?  $1 : '*invariant';
+         $Counts{'malwaretospam'}{$type}{$name}{$variant}{$score}++
+      }
+
+   # The virus_scan line reports only the one virus name when more than one scanner detects a virus.
+   # Use instead the ask_av and run_av lines (see below)
+   #
+   #} elsif ( my ($malware, $scanners) = ($p1 =~ /virus_scan: \(([^)]+)\), detected by \d+ scanners: (.*)$/ )) {
+      #TD virus_scan: (HTML.Phishing.Bank-43), detected by 1 scanners: ClamAV-clamd
+      #TD virus_scan: (Worm.SomeFool.D, Worm.SomeFool.D), detected by 1 scanners: ClamAV-clamd
+      #TD virus_scan: (Trojan.Downloader.Small-9993), detected by 2 scanners: ClamAV-clamd, NAI McAfee AntiVirus (uvscan)
+   #   foreach (split /, /, $scanners) {
+   #      #$Totals{'malwarebyscanner'}++;       # No summary output: redundant w/malwarepassed,malwareblocked}
+   #      $Counts{'malwarebyscanner'}{"$_"}{$malware}++;
+   #   }
+
+   } elsif ($p1 =~ /^(?:ask_av|run_av) (.*)$/) {
+      next unless ($Collecting{'malwarebyscanner'});
+
+      if (my ($scanner, $name) = ($1 =~ /^\((.+)\):(?: [^:]+)? INFECTED: ([^,]+)/)) {
+         #TD ask_av (ClamAV-clamd): /var/amavis/tmp/amavis-20070830T070403-13776/parts INFECTED: Email.Malware.Sanesecurity.07082700
+         #TD run_av (NAI McAfee AntiVirus (uvscan)): INFECTED: W32/Zhelatin.gen!eml, W32/Zhelatin.gen!eml
+         my $type = $name =~ s/\.UNOFFICIAL$// ? 'Unofficial' : 'Official';
+         my $variant = '';
+         if ($name =~ s/([.-]\d+)$//) {     # strip trailing numeric variant (...Phishing.Cur.863)
+            $variant = $1;
+         }
+         $Counts{'malwarebyscanner'}{$scanner}{$type}{$name}{$variant}++;
+      }
+      # currently ignoring other ask_av or run_av lines
+   }
+
+   # Extra Modules loaded at runtime
+   #TD extra modules loaded after daemonizing/chrooting: Mail/SPF/Query.pm
+   elsif (($item) = ( $p1 =~ /^extra modules loaded(?: after daemonizing(?:\/chrooting)?)?: (.+)$/)) {
+      #TD extra modules loaded: PerlIO.pm, PerlIO/scalar.pm
+      foreach my $code (split /, /, $item) {
+         #TD extra modules loaded: unicore/lib/gc_sc/Digit.pl, unicore/lib/gc_sc/SpacePer.pl
+         # avoid useless reporting of pseudo-modules which can't be pre-loaded once
+         unless ($code =~ m#^unicore/lib/#) {
+            $Totals{'extramodules'}++;
+            $Counts{'extramodules'}{$code}++    if ($Collecting{'extramodules'});
+         }
+      }
+
+   # Timing report
+   } elsif (my ($total,$report) = ( $p1 =~ /^(?:size: \d+, )?TIMING \[total (\d+) ms(?:, [^]]+)?\] - (.+)$/)) {
+     next if ($report =~ /^got data/);    # skip amavis release timing
+      #TD TIMING [total 5808 ms] - SMTP greeting: 5 (0%)0, SMTP LHLO: 1 (0%)0, SMTP pre-MAIL: 2 (0%)0, SMTP pre-DATA-flush: 5 (0%)0, SMTP DATA: 34 (1%)1, check_init: 1 (0%)1
+      # older format, maia mailguard
+      #TD TIMING [total 3795 ms] - SMTP EHLO: 1 (0%), SMTP pre-MAIL: 0 (0%), maia_read_system_config: 1 (0%), maia_get_mysql_size_limit: 0 (0%), SA check: 3556 (94%), rundown: 0 (0%)
+      # v2.8.1
+      # .... size: 3815, TIMING [total 1901 ms, cpu 657 ms] - ...
+
+
+      # Timing line is incomplete - let's report it
+      if ($p1 !~ /\d+ \(\d+%\)\d+$/ and $p1 !~ /\d+ \(\d+%\)$/) {
+         inc_unmatched('timing');
+         next;
+      }
+
+      if ($Opts{'timings'}) {
+         my @pairs = split(/[,:] /, $report);
+         while (my ($key,$value) = @pairs) {
+            #4 (0%)0
+            my ($ms) = ($value =~ /^([\d.]+) /);
+            # maintain a per-test list of timings
+            push @{$Timings{$key}}, $ms;
+            shift @pairs; shift @pairs;
+         }
+         push @TimingsTotals, $total;
+      }
+
+   } elsif ((($total,$report) = ( $p1 =~ /^TIMING-SA total (\d+) ms - (.+)$/ )) or
+            (($total,$report) = ( $p1 =~ /^TIMING-SA \[total (\d+) ms, cpu \d+ ms\] - (.+)$/ ))) {
+         #TIMING-SA [total 3219 ms, cpu 432 ms] - parse: 6 (0.2%), ext
+      #TD TIMING-SA total 5478 ms - parse: 1.69 (0.0%), extract_message_metadata: 16 (0.3%), get_uri_detail_list: 2 (0.0%), tests_pri_-1000: 25 (0.4%), tests_pri_-950: 0.67 (0.0%), tests_pri_-900: 0.83 (0.0%), tests_pri_-400: 19 (0.3%), check_bayes: 17 (0.3%), tests_pri_0: 5323 (97.2%), check_spf: 12 (0.2%), poll_dns_idle: 0.81 (0.0%), check_dkim_signature: 1.50 (0.0%), check_razo r2: 5022 (91.7%), check_dcc: 192 (3.5%), check_pyzor: 0.02 (0.0%), tests_pri_500: 9 (0.2%), tests_pri_1000: 24 (0.4%), total_awl: 23 (0.4%), check_awl: 10 (0.2%), update_awl: 8 (0.1%), learn: 36 (0.7%), get_report: 1.77 (0.0%)
+
+      # Timing line is incomplete - let's report it
+      if ($p1 !~ /[\d.]+ \([\d.]+%\)[\d.]+$/ and $p1 !~ /[\d.]+ \([\d.]+%\)$/) {
+         inc_unmatched('timing-sa');
+         next;
+      }
+      if ($Opts{'sa_timings'}) {
+         my @pairs = split(/[,:] /, $report);
+         while (my ($key,$value) = @pairs) {
+            #4 (0%)0
+            my ($ms) = ($value =~ /^([\d.]+) /);
+            # maintain a per-SA test list of timings
+            push @{$TimingsSA{$key}}, $ms;
+            shift @pairs; shift @pairs;
+         }
+         push @TimingsSATotals, $total;
+      }
+
+   # Bounce killer: 2.6+
+   } elsif ($p1 =~ /^bounce (.*)$/) {
+      #TD bounce killed, <user@example.com> -> <to@example.net>, from: user@example.com, message-id: <CA8E335-CC-2EFB@example.com>, return-path: <user@example.com>
+      #TD bounce rescued by domain, <user@example.com> -> <to@example.net>, from: user@example.com, message-id: <CA8E335-CC-2EFB@example.com>, return-path: <user@example.com>
+      #TD bounce rescued by originating, <user@example.com> -> <to@example.net>, from: user@example.com, message-id: <CA8E335-CC-2EFB@example.com>, return-path: <user@example.com>
+      #TD bounce rescued by: pen pals disabled, <user@example.com> -> <to@example.net>, from: user@example.com, message-id: <CA8E335-CC-2EFB@example.com>, return-path: <user@example.com>
+      $p2 = $1;
+
+      if ($p2 =~ /^killed, <(.+?)> -> /) {
+         $Totals{'bouncekilled'}++;
+         $Counts{'bouncekilled'}{$1 eq '' ? '<>' : $1}++    if ($Collecting{'bouncekilled'});
+      }
+      elsif ($p2 =~ /^rescued by ([^,]+), <(.+?)> -> /) {
+         # note: ignores "rescued by: pen pals disabled"
+         $Totals{'bouncerescued'}++;
+         $Counts{'bouncerescued'}{'By ' . $1}{$2 eq '' ? '<>' : $2}++    if ($Collecting{'bouncerescued'});
+      }
+      elsif ($p2 =~ /^unverifiable, <(.+?)> -> /) {
+         # note: ignores "rescued by: pen pals disabled"
+         $Totals{'bounceunverifiable'}++;
+         $Counts{'bounceunverifiable'}{$1 eq '' ? '<>' : $1}++    if ($Collecting{'bounceunverifiable'});
+      }
+      #TD bounce unverifiable, <postmaster@nurturegood.com> -> <dave@davewolloch.com>
+      #TD bounce unverifiable, <> -> <Dave@davewolloch.com>
+   }
+
+   # Decoders
+   elsif (my ($suffix, $info) = ( $p1 =~ /^Internal decoder for (\.\S*)\s*(?:\(([^)]*)\))?$/)) {
+      #TD Internal decoder for .gz   (backup, not used)
+      #TD Internal decoder for .zip 
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Decoders'}{'Internal'}{$suffix} = $info;
+   }
+
+   elsif (($suffix, $decoder) = ( $p1 =~ /^No decoder for\s+(\.\S*)\s*(?:tried:\s+(.*))?$/)) {
+      #TD No decoder for       .tnef tried: tnef
+      # older
+      #TD No decoder for       .doc
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Decoders'}{'None'}{$suffix} = "tried: " . ($decoder ? $decoder : "unknown");
+   }
+
+   elsif (($suffix, $decoder) = ( $p1 =~ /^Found decoder for\s+(\.\S*)\s+at\s+(.*)$/)) {
+      #TD Found decoder for    .bz2  at /usr/bin/bzip2 -d
+      #TD Found decoder for    .bz2  at /usr/bin/7za (backup, not used)
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Decoders'}{'External'}{$suffix} = exists $StartInfo{'Decoders'}{'External'}{$suffix} ?
+              join '; ', $StartInfo{'Decoders'}{'External'}{$suffix}, $decoder : $decoder;
+   }
+
+   # AV Scanners
+   elsif (my ($tier, $scanner, $location) = ( $p1 =~ /^Found (primary|secondary) av scanner (.+) at (.+)$/)) {
+      #TD Found primary av scanner NAI McAfee AntiVirus (uvscan) at /usr/local/bin/uvscan
+      #TD Found secondary av scanner ClamAV-clamscan at /usr/local/bin/clamscan
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'AVScanner'}{"\u$tier"}{$scanner} = $location;
+
+   } elsif (($tier, $scanner, $location) = ( $p1 =~ /^No (primary|secondary) av scanner: (.+)$/)) {
+      #TD No primary av scanner: CyberSoft VFind
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'AVScanner'}{"\u$tier (not found)"}{$scanner} = '';
+
+   } elsif ( (($tier, $scanner) = ( $p1 =~ /^Using internal av scanner code for \(([^)]+)\) (.+)$/)) or
+             (($tier, $scanner) = ( $p1 =~ /^Using (.*) internal av scanner code for (.+)$/))) {
+      #TD Using internal av scanner code for (primary) ClamAV-clamd
+      #TD Using primary internal av scanner code for ClamAV-clamd
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'AVScanner'}{"\u$tier internal"}{$scanner} = '';
+
+   # (Un)Loaded code, protocols, etc.
+   } elsif (my ($code, $loaded) = ( $p1 =~ /^(\S+)\s+(?:proto? |base |protocol )?\s*(?:code)?\s+((?:NOT )?loaded)$/)) {
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Code'}{"\u\L$loaded"}{$code} = "";
+
+   } elsif (my ($module, $vers) = ( $p1 =~ /^Module (\S+)\s+(.+)$/)) {
+      #TD Module Amavis::Conf        2.086
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Code'}{'Loaded'}{$module} = $vers;
+
+   } elsif (($module, my $families) = ( $p1 =~ /^socket module (\S+),\s+(.+)$/)) {
+      #TD socket module IO::Socket::IP, protocol families available: INET, INET6
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Code'}{'Loaded'}{$module} = $families;
+
+   } elsif (($code, $location) = ( $p1 =~ /^Found \$(\S+)\s+at\s+(.+)$/)) {
+      #TD Found $file            at /usr/bin/file
+      #TD Found $uncompress at /usr/bin/gzip -d
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Code'}{'Loaded'}{$code} = $location;
+
+   } elsif (($code, $location) = ( $p1 =~ /^No \$(\S+),\s+not using it/)) {
+      #TD No $dspam,             not using it
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Code'}{'Not loaded'}{$code} = $location;
+
+   } elsif (($code, $location) = ( $p1 =~ /^No ext program for\s+([^,]+), (tried: .+)/)) {
+      #TD No ext program for   .kmz, tried: 7za, 7z
+      #TD No ext program for   .F, tried: unfreeze, freeze -d, melt, fcat
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Code'}{'Not found'}{$code} = $location;
+
+
+   } elsif ( $p1 =~ /^starting\.\s+(.+) at \S+ (?:amavisd-new-|Maia Mailguard )([^,]+),/) {
+      #TD starting.  /usr/local/sbin/amavisd at mailhost.example.com amavisd-new-2.5.0 (20070423), Unicode aware, LANG="C"
+      #TD starting.  /usr/sbin/amavisd-maia at vwsw02.eon.no Maia Mailguard 1.0.2, Unicode aware, LANG=en_US.UTF-8
+      next unless ($Opts{'startinfo'});
+      %StartInfo = ()  if !exists $StartInfo{'Logging'};
+      $StartInfo{'ampath'}    = $1;
+      $StartInfo{'amversion'} = $2;
+
+   } elsif ( $p1 =~ /^config files read: (.*)$/) {
+      #TD config files read: /etc/amavisd.conf, /etc/amavisd-overrides.conf
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'Configs'} = "$1";
+
+   } elsif ($p1 =~ /^Creating db in ([^;]+); [^,]+, (.*)$/) {
+      #TD Creating db in /var/spool/amavis/db/; BerkeleyDB 0.31, libdb 4.4
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'db'} = "$1\t($2)";
+
+   } elsif ($p1 =~ /^BerkeleyDB-based Amavis::Cache not available, using memory-based local cache$/) {
+      #TD BerkeleyDB-based Amavis::Cache not available, using memory-based local cache
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'db'} = "BerkeleyDB\t(memory-based cache: Amavis::Cache unavailable)";
+   } elsif (my ($log) = ($p1 =~ /^logging initialized, log (level \d+, (?:STDERR|syslog: \S+))/)) {
+      next unless ($Opts{'startinfo'});
+      %StartInfo = ();        # first amavis log entry, clear out previous start info
+      $StartInfo{'Logging'} = $log;
+
+   } elsif (( $p1 =~ /^(:?perl=[^,]*, )?user=([^,]*), EUID: (\d+) [(](\d+)[)];\s+group=([^,]*), EGID: ([\d ]+)[(]([\d ]+)[)]/)) {
+      # uninteresting...
+      #next unless ($Opts{'startinfo'});
+      #$StartInfo{'IDs'}{'user'} = $1;
+      #$StartInfo{'IDs'}{'euid'} = $2;
+      #$StartInfo{'IDs'}{'uid'} = $3;
+      #$StartInfo{'IDs'}{'group'} = $4;
+      #$StartInfo{'IDs'}{'egid'} = $5;
+      #$StartInfo{'IDs'}{'gid'} = $6;
+   } elsif ($p1 =~ /^after_chroot_init: EUID: (\d+) [(](\d+)[)]; +EGID: ([\d ]+)[(]([\d ]+)[)]/) {
+      #TD after_chroot_init: EUID: 999 (999);  EGID: 54322 54322 54322 (54322 54322 54322)
+      # uninteresting...
+
+   } elsif ($p1 =~ /^SpamAssassin debug facilities: (.*)$/) {
+      next unless ($Opts{'startinfo'});
+      $StartInfo{'sa_debug'} = $1;
+
+   # amavis >= 2.6.3
+   } elsif ($p1 =~ /^SpamAssassin loaded plugins: (.*)$/) {
+      #TD SpamAssassin loaded plugins: AWL, AutoLearnThreshold, Bayes, BodyEval, Check, DCC, DKIM, DNSEval, HTMLEval, HTTPSMismatch, Hashcash, HeaderEval, ImageInfo, MIMEEval, MIMEHeader, Pyzor, Razor2, RelayEval, ReplaceTags, SPF, SpamCop, URIDNSBL, URIDetail, URIEval, VBounce, WLBLEval, WhiteListSubject
+      next unless ($Opts{'startinfo'});
+      map { $StartInfo{'SAPlugins'}{'Loaded'}{$_} = '' } split(/, /, $1);
+
+   } elsif (($p2) = ( $p1 =~ /^Net::Server: (.*)$/ )) {
+      next unless ($Opts{'startinfo'});
+      if ($p2 =~ /^.*starting! pid\((\d+)\)/) {
+         #TD Net::Server: 2007/05/02-11:05:24 Amavis (type Net::Server::PreForkSimple) starting! pid(4405)
+         $StartInfo{'Server'}{'pid'} = $1;
+      } elsif ($p2 =~ /^Binding to UNIX socket file (.*) using/) {
+         #TD Net::Server: Binding to UNIX socket file /var/spool/amavis/amavisd.sock using SOCK_STREAM
+         $StartInfo{'Server'}{'socket'} = $1;
+      } elsif ($p2 =~ /^Binding to TCP port (\d+) on host (.*)$/) {
+         #TD Net::Server: Binding to TCP port 10024 on host 127.0.0.1
+         $StartInfo{'Server'}{'ip'} = "$2:$1";
+      } elsif ($p2 =~ /^Setting ([ug]id) to "([^"]+)"$/) {
+         $StartInfo{'Server'}{$1} = $2;
+         #TD Net::Server: Setting gid to "91 91"
+         #TD Net::Server: Setting uid to "91"
+      }
+      # skip others
+   }
+
+   # higher debug level or rare messages skipped last
+   elsif (! check_ignore_list ($p1, @ignore_list_final)) {
+      inc_unmatched('final');
+   }
+}
+
+########################################
+# Final tabulations, and report printing
+
+
+# spamblocked includes spamdiscarded; adjust here
+$Totals{'spamblocked'} -= $Totals{'spamdiscarded'};
+
+
+#Totals: Blocked/Passed totals
+$Totals{'totalblocked'} += $Totals{$_} foreach (
+   qw(
+      malwareblocked
+      bannednameblocked
+      uncheckedblocked
+      spamblocked
+      spamdiscarded
+      spammyblocked
+      badheaderblocked
+      oversizedblocked
+      mtablocked
+      cleanblocked
+      tempfailblocked
+      otherblocked
+   ));
+
+$Totals{'totalpassed'} += $Totals{$_} foreach (
+   qw(
+      malwarepassed
+      bannednamepassed
+      uncheckedpassed
+      spampassed
+      spammypassed
+      badheaderpassed
+      oversizedpassed
+      mtapassed
+      cleanpassed
+      tempfailpassed
+      otherpassed
+   ));
+
+# Priority: VIRUS BANNED UNCHECKED SPAM SPAMMY BADH OVERSIZED MTA CLEAN
+#Totals: Ham/Spam
+
+$Totals{'totalmalware'} += $Totals{$_} foreach (
+   qw(malwarepassed malwareblocked));
+
+$Totals{'totalbanned'} += $Totals{$_} foreach (
+   qw(bannednamepassed bannednameblocked));
+
+$Totals{'totalunchecked'} += $Totals{$_} foreach (
+   qw(uncheckedpassed uncheckedblocked));
+
+$Totals{'totalspammy'} += $Totals{$_} foreach (
+   qw(spammypassed spammyblocked));
+
+$Totals{'totalbadheader'} += $Totals{$_} foreach (
+   qw(badheaderpassed badheaderblocked));
+
+$Totals{'totaloversized'} += $Totals{$_} foreach (
+   qw(oversizedpassed oversizedblocked));
+
+$Totals{'totalmta'} += $Totals{$_} foreach (
+   qw(mtapassed mtablocked));
+
+$Totals{'totalclean'} += $Totals{$_} foreach (
+   qw(cleanpassed cleanblocked));
+
+$Totals{'totalother'} += $Totals{$_} foreach (
+   qw(tempfailpassed tempfailblocked otherpassed otherblocked));
+
+$Totals{'totalspam'} += $Totals{$_} foreach (
+   qw(spampassed spamblocked spamdiscarded totalspammy));
+
+# everything lower priority than SPAMMY is considered HAM
+$Totals{'totalham'} += $Totals{$_} foreach (
+   qw(totalbadheader totaloversized totalmta totalclean));
+
+$Totals{'totalmsgs'} += $Totals{$_} foreach (
+   qw(totalmalware totalbanned totalunchecked totalspam totalham totalother));
+
+# Print the summary report if any key has non-zero data.
+# Note: must explicitely check for any non-zero data,
+# as Totals always has some keys extant.
+#
+if ($Opts{'summary'}) {
+   for (keys %Totals) {
+      if ($Totals{$_}) {
+         print_summary_report (@Sections);
+         last;
+      }
+   }
+}
+
+# Print the detailed report, if detail is sufficiently high
+#
+if ($Opts{'detail'} >= 5) {
+   print_detail_report (@Sections);
+   printAutolearnReport;
+   printSpamScorePercentilesReport;
+   printSpamScoreFrequencyReport;
+   printSARulesReport;
+   printTimingsReport("Scan Timing Percentiles", \%Timings, \@TimingsTotals, $Opts{'timings'});
+   printTimingsReport("SA Timing Percentiles", \%TimingsSA, \@TimingsSATotals, 0-$Opts{'sa_timings'});
+   printStartupInfoReport        if ($Opts{'detail'} >= 10);
+}
+
+#{
+#use Data::Dumper;
+#print Dumper(\%p0ftags);
+#print Dumper($Counts{'p0f'});
+#}
+
+# Finally, print any unmatched lines
+#
+print_unmatched_report();
+
+# Evaluates a given line against the list of ignore patterns.
+#
+sub check_ignore_list($ \@) {
+   my ($line, $listref) = @_;
+
+   foreach (@$listref) {
+      return 1 if $line =~ /$_/;
+   }
+
+   return 0;
+}
+
+
+# Spam score percentiles report
+#
+=pod
+   ==================================================================================
+   Spam Score Percentiles        0%       50%       90%       95%       98%      100%
+   ----------------------------------------------------------------------------------
+   Score Spam (100)           6.650    21.906    34.225    36.664    38.196    42.218
+   Score Ham (1276)         -17.979    -2.599     0.428     2.261     3.472     6.298
+   ==================================================================================
+=cut
+sub printSpamScorePercentilesReport {
+   return unless ($Opts{'score_percentiles'} and keys %SpamScores);
+
+   #printf "Scores $_ (%d): @{$SpamScores{$_}}\n", scalar @{$SpamScores{$_}} foreach keys %SpamScores;
+   my (@p, @sorted);
+   my @percents = split /[\s,]+/, $Opts{'score_percentiles'};
+   my $myfw2 = $fw2 - 1;
+
+   print  "\n", $sep1 x $fw1, $sep1 x $fw2 x @percents;
+   printf "\n%-${fw1}s" . "%${myfw2}s%%" x @percents ,   "Spam Score Percentiles", @percents;
+   print  "\n", $sep2 x $fw1, $sep2 x $fw2 x @percents;
+
+   foreach my $ccat (keys %SpamScores) {
+      @sorted = sort { $a <=> $b } @{$SpamScores{$ccat}};
+      @p = get_percentiles (@sorted, @percents);
+      printf "\n%-${fw1}s" . "%${fw2}.3f" x scalar (@p), "Score \u$ccat (" . scalar (@sorted) . ')', @p;
+   }
+
+   print  "\n", $sep1 x $fw1, $sep1 x $fw2 x @percents, "\n";
+}
+
+# Spam score frequency report
+#
+=pod
+   ======================================================================================================
+   Spam Score Frequency      <= -10     <= -5      <= 0      <= 5     <= 10     <= 20     <= 30      > 30
+   ------------------------------------------------------------------------------------------------------
+   Hits (1376)                   29       168       921       170        29        33         1        25
+   Percent of Hits            2.11%    12.21%    66.93%    12.35%     2.11%     2.40%     0.07%     1.82%
+   ======================================================================================================
+=cut
+sub printSpamScoreFrequencyReport {
+   return unless ($Opts{'score_frequencies'} and keys %SpamScores);
+
+   my @scores = ();
+   push @scores, @{$SpamScores{$_}}  foreach (keys %SpamScores);
+   my $nscores = scalar @scores;
+
+   my @sorted  = sort { $a <=> $b } @scores;
+   my @buckets = sort { $a <=> $b } split /[\s,]+/, $Opts{'score_frequencies'};
+   push @buckets, $buckets[-1] + 1;
+   #print "Scores: @sorted\n";
+
+   my @p = get_frequencies (@sorted, @buckets);
+
+   my @ranges = ( 0 ) x @buckets;
+   my $last = @buckets - 1;
+   $ranges[0]   = sprintf "%${fw2}s", " <= $buckets[0]";
+   $ranges[-1]  = sprintf "%${fw2}s", " > $buckets[-2]";
+   for my $i (1 .. @buckets - 2) {
+      $ranges[$i] = sprintf "%${fw2}s", " <= $buckets[$i]";
+   }
+
+   print  "\n", $sep1 x $fw1, $sep1 x $fw2 x @buckets;
+   printf "\n%-${fw1}s" . "%-${fw2}s" x @buckets ,     "Spam Score Frequency", @ranges;
+   print  "\n", $sep2 x $fw1, $sep2 x $fw2 x @buckets;
+   printf "\n%-${fw1}s" . "%${fw2}d" x scalar (@p),    "Hits ($nscores)", @p;
+   my $myfw2 = $fw2 - 1;
+   printf "\n%-${fw1}s" . "%${myfw2}.2f%%" x scalar (@p),    "Percent of Hits", map {($_ / $nscores) * 100.0; } @p;
+   print  "\n", $sep1 x $fw1, $sep1 x $fw2 x @buckets, "\n";
+}
+
+# SpamAssassin rules report
+#
+=pod
+   ===========================================================================
+   SpamAssassin Rule Hits: Spam
+   ---------------------------------------------------------------------------
+   Rank     Hits    % Msgs   % Spam    % Ham      Score Rule
+   ----     ----    ------   ------    -----      ----- ----
+      1       44    81.48%   93.62%    0.00%      1.961 URIBL_BLACK
+      2       44    81.48%   93.62%   14.29%      0.001 HTML_MESSAGE
+      3       42    77.78%   89.36%    0.00%      2.857 URIBL_JP_SURBL
+      4       38    70.37%   80.85%   14.29%      2.896 RCVD_IN_XBL
+      5       37    68.52%   78.72%    0.00%      2.188 RCVD_IN_BL_SPAMCOP_NET
+   ...
+   ===========================================================================
+
+   ===========================================================================
+   SpamAssassin Rule Hits: Ham
+   ---------------------------------------------------------------------------
+   Rank     Hits    % Msgs   % Spam    % Ham      Score Rule
+   ----     ----    ------   ------    -----      ----- ----
+      1        5     9.26%    2.13%   71.43%      0.001 STOX_REPLY_TYPE
+      2        4     7.41%    0.00%   57.14%     -0.001 SPF_PASS
+      3        4     7.41%    6.38%   57.14%          - AWL
+      4        1     1.85%    0.00%   14.29%      0.303 TVD_RCVD_SINGLE
+      5        1     1.85%   25.53%   14.29%        0.1 RDNS_DYNAMIC
+   ...
+   ===========================================================================
+=cut
+sub printSARulesReport {
+   return unless (keys %{$Counts{'sarules'}});
+
+   our $maxlen = 0;
+
+   sub getSAHitsReport($ $) {
+      my ($type, $topn) = @_;
+      my $i = 1;
+      my @report = ();
+
+      return if ($topn eq '0');     # topn can be numeric, or the string "all"
+
+      for (sort { $Counts{'sarules'}{$type}{$b} <=> $Counts{'sarules'}{$type}{$a} } keys %{$Counts{'sarules'}{$type}}) {
+
+         # only show top n lines; all when topn is "all"
+         if ($topn ne 'all' and $i > $topn) {
+            push @report, "...\n";
+            last;
+         }
+         my $n     = $Counts{'sarules'}{$type}{$_};
+         my $nham  = $Counts{'sarules'}{'Ham'}{$_};
+         my $nspam = $Counts{'sarules'}{'Spam'}{$_};
+         # rank, count, % msgs, % spam, % ham
+         push @report, sprintf "%4d %8d   %6.2f%%  %6.2f%%  %6.2f%%     %s\n",
+            $i++,
+            $n,
+            $Totals{'totalmsgs'} == 0 ? 0 : 100.0 * $n     / $Totals{'totalmsgs'},
+            $Totals{'totalspam'} == 0 ? 0 : 100.0 * $nspam / $Totals{'totalspam'},
+            $Totals{'totalham'}  == 0 ? 0 : 100.0 * $nham  / $Totals{'totalham'},
+            $_;
+         my $len = length($report[-1]) - 1;
+         $maxlen = $len  if ($len > $maxlen);
+      }
+
+      if (scalar @report) {
+         print "\n", $sep1 x $maxlen, "\n";
+         print "SpamAssassin Rule Hits: $type\n";
+         print $sep2 x $maxlen, "\n";
+         print "Rank     Hits    % Msgs   % Spam    % Ham      Score Rule\n";
+         print "----     ----    ------   ------    -----      ----- ----\n";
+         print @report;
+         print $sep1 x $maxlen, "\n";
+      }
+   }
+   
+   my ($def_limit_spam, $def_limit_ham) = split /[\s,]+/, $Defaults{'sarules'};
+   my ($limit_spam, $limit_ham)         = split /[\s,]+/, $Opts{'sarules'};
+   $limit_spam = $def_limit_spam    if $limit_spam eq '';
+   $limit_ham  = $def_limit_ham     if $limit_ham  eq '';
+
+   getSAHitsReport('Spam', $limit_spam);
+   getSAHitsReport('Ham',  $limit_ham);
+}
+
+# Autolearn report, only available if enabled in amavis $log_templ template
+#
+=pod
+   ======================================================================
+   Autolearn          Msgs      Spam       Ham   % Msgs   % Spam    % Ham
+   ----------------------------------------------------------------------
+   Spam                 36        36         0   66.67%   76.60%    0.00%
+   Ham                   2         0         2    3.70%    0.00%   28.57%
+   No                    7         4         3   12.96%    8.51%   42.86%
+   Disabled              6         6         0   11.11%   12.77%    0.00%
+   Failed                2         1         1    3.70%    2.13%   14.29%
+   ----------------------------------------------------------------------
+   Totals               53        47         6   98.15%  100.00%   85.71%
+   ======================================================================
+=cut
+sub printAutolearnReport {
+   #print "printAutolearnReport:\n"    if ($Opts{'debug'});
+   return unless (keys %{$Counts{'autolearn'}});
+
+   our $maxlen = 0;
+   our ($nhamtotal, $nspamtotal);
+
+   sub getAutolearnReport($) {
+      my ($type) = @_;
+      my @report = ();
+
+      #  SA 2.5/2.6 : ham/spam/no
+      #  SA 3.0+    : ham/spam/no/disabled/failed/unavailable
+      for (qw(spam ham no disabled failed unavailable)) {
+
+         next unless (exists $Counts{'autolearn'}{'Spam'}{$_} or exists $Counts{'autolearn'}{'Ham'}{$_});
+         #print "printAutolearnReport: type: $_\n"    if ($Opts{'debug'});
+
+         my $nham  = exists $Counts{'autolearn'}{'Ham'}{$_}  ? $Counts{'autolearn'}{'Ham'}{$_}  : 0;
+         my $nspam = exists $Counts{'autolearn'}{'Spam'}{$_} ? $Counts{'autolearn'}{'Spam'}{$_} : 0;
+         my $nboth = $nham + $nspam;
+         $nhamtotal += $nham; $nspamtotal += $nspam;
+         # type, nspam, nham, % msgs, % spam, % ham
+         push @report, sprintf "%-13s %9d %9d %9d  %6.2f%%  %6.2f%%  %6.2f%%\n",
+            ucfirst $_,
+            $nspam + $nham,
+            $nspam,
+            $nham,
+            $Totals{'totalmsgs'} == 0 ? 0 : 100.0 * $nboth / $Totals{'totalmsgs'},
+            $Totals{'totalspam'} == 0 ? 0 : 100.0 * $nspam / $Totals{'totalspam'},
+            $Totals{'totalham'}  == 0 ? 0 : 100.0 * $nham  / $Totals{'totalham'};
+
+         my $len = length($report[-1]) - 1;
+         $maxlen = $len  if ($len > $maxlen);
+      }
+      return @report;
+   }
+   
+   my @report_spam = getAutolearnReport('Spam');
+
+   if (scalar @report_spam) {
+      print "\n", $sep1 x $maxlen, "\n";
+      print "Autolearn          Msgs      Spam       Ham   % Msgs   % Spam    % Ham\n";
+      print $sep2 x $maxlen, "\n";
+      print @report_spam;
+      print $sep2 x $maxlen, "\n";
+
+      printf "%-13s %9d %9d %9d  %6.2f%%  %6.2f%%  %6.2f%%\n",
+            'Totals',
+            $nspamtotal + $nhamtotal,
+            $nspamtotal,
+            $nhamtotal,
+            $Totals{'totalmsgs'} == 0 ? 0 : 100.0 * ($nspamtotal + $nhamtotal) / $Totals{'totalmsgs'},
+            $Totals{'totalspam'} == 0 ? 0 : 100.0 * $nspamtotal / $Totals{'totalspam'},
+            $Totals{'totalham'}  == 0 ? 0 : 100.0 * $nhamtotal  / $Totals{'totalham'};
+      print $sep1 x $maxlen, "\n";
+   }
+}
+
+
+# Timings percentiles report, used for amavis message scanning and spamassassin timings 
+=pod
+   ========================================================================================================================
+   Scan Timing Percentiles       % Time    Total (ms)        0%        5%       25%       50%       75%       95%      100%
+   ------------------------------------------------------------------------------------------------------------------------
+   AV-scan-2 (3)                 69.23%       7209.00   2392.00   2393.50   2399.50   2407.00   2408.50   2409.70   2410.00
+   SA check (2)                  19.74%       2056.00    942.00    950.60    985.00   1028.00   1071.00   1105.40   1114.00
+   SMTP DATA (3)                  5.49%        572.00    189.00    189.20    190.00    191.00    191.50    191.90    192.00
+   AV-scan-1 (3)                  0.82%         85.00     11.00     12.60     19.00     27.00     37.00     45.00     47.00
+   ...
+   ------------------------------------------------------------------------------------------------------------------------
+   Total                                     10413.00   2771.00   2867.10   3251.50   3732.00   3821.00   3892.20   3910.00
+   ========================================================================================================================
+
+   ========================================================================================================================
+   SA Timing Percentiles         % Time    Total (ms)        0%        5%       25%       50%       75%       95%      100%
+   ------------------------------------------------------------------------------------------------------------------------
+   tests_pri_0 (1)               97.17%       5323.00   5323.00   5323.00   5323.00   5323.00   5323.00   5323.00   5323.00
+   check_razor2 (1)              91.68%       5022.00   5022.00   5022.00   5022.00   5022.00   5022.00   5022.00   5022.00
+   check_dcc (1)                  3.50%        192.00    192.00    192.00    192.00    192.00    192.00    192.00    192.00
+   learn (1)                      0.66%         36.00     36.00     36.00     36.00     36.00     36.00     36.00     36.00
+   tests_pri_-1000 (1)            0.46%         25.00     25.00     25.00     25.00     25.00     25.00     25.00     25.00
+   ...
+   ------------------------------------------------------------------------------------------------------------------------
+   Total                                      5478.00   5478.00   5478.00   5478.00   5478.00   5478.00   5478.00   5478.00
+   ========================================================================================================================
+=cut
+sub printTimingsReport($$$$) {
+   my ($title, $timingsref, $totalsref, $cutoff) = @_;
+   my @tkeys = keys %$timingsref;
+   return unless scalar @tkeys;
+
+   my (@p, @sorted, %perkey_totals, @col_subtotals);
+   my ($pcnt,$max_pcnt,$max_rows,$time_total_actual,$time_total_hypo,$subtotal_pcnt);
+   my @percents = split /[\s,]+/, $Opts{'timings_percentiles'};
+   my $header_footer = $sep1 x 50 . ($sep1 x 10) x @percents;
+   my $header_end    = $sep2 x 50 . ($sep2 x 10) x @percents;
+   my $title_width = '-28';
+
+   print "\n$header_footer\n";
+   printf "%${title_width}s  %6s %13s" ." %8s%%" x @percents , $title, "% Time", "Total (ms)", @percents;
+   print "\n$header_end\n";
+
+   # Sum the total time for each timing key
+   foreach my $key (@tkeys) {
+      foreach my $timeval (@{$$timingsref{$key}}) {
+         $perkey_totals{$key} += $timeval;
+      }
+   }
+
+   # Sum total time spent scanning
+   map {$time_total_actual += $_} @$totalsref;
+
+   # cutoff value used to limit the number of rows of output
+   #   positive cutoff is a percentage of cummulative time
+   #   negative cutoff limits number of rows
+   if ($cutoff >= 0) {
+      $max_pcnt = $cutoff != 100 ? $cutoff : 150;  # 150% avoids roundoff errors
+   }
+   else {
+      $max_rows = -$cutoff;
+   }
+   my $rows = 0;
+   # sort each timing key's values, required to compute the list of percentiles 
+   for (sort { $perkey_totals{$b} <=> $perkey_totals{$a} } @tkeys) {
+      last if (($max_rows and $rows >= $max_rows) or ($max_pcnt and $subtotal_pcnt >= $max_pcnt));
+
+      $pcnt = ($perkey_totals{$_} / $time_total_actual) * 100,
+      @sorted = sort { $a <=> $b } @{$$timingsref{$_}};
+      @p = get_percentiles (@sorted, @percents);
+
+      $subtotal_pcnt += $pcnt;
+      printf "%${title_width}s %6.2f%% %13.2f" . " %9.2f" x scalar (@p) . "\n",
+               $_ .  ' (' . scalar(@{$$timingsref{$_}}) . ')', # key ( number of elements )
+               $pcnt,                                          # percent of total time
+               #$perkey_totals{$_} / 1000,                     # total time for this test
+               $perkey_totals{$_},                             # total time for this test
+               #map {$_ / 1000} @p;                            # list of percentiles
+               @p;                                             # list of percentiles
+      $rows++;
+   }
+   print "...\n"  if ($rows != scalar @tkeys);
+
+   print "$header_end\n";
+   # actual total time as reported by amavis
+   @sorted = sort { $a <=> $b } @$totalsref;
+   @p = get_percentiles (@sorted, @percents);
+   printf "%${title_width}s         %13.2f" . " %9.2f" x scalar (@p) . "\n",
+            'Total',
+            #$time_total_actual / 1000,
+            $time_total_actual,
+            #map {$_ / 1000} @p;
+             @p;
+
+   print "$header_footer\n";
+}
+
+# Most recent startup info report
+#
+sub printStartupInfoReport {
+
+   return unless (keys %StartInfo);
+
+   sub print2col($ $) {
+      my ($label,$val) = @_;
+      printf "%-50s %s\n", $label, $val;
+   }
+
+   print "\nAmavis Startup\n";
+
+   print2col ("    Amavis",       $StartInfo{'ampath'})             if (exists $StartInfo{'ampath'});
+   print2col ("        Version",  $StartInfo{'amversion'})          if (exists $StartInfo{'amversion'});
+   print2col ("        PID",      $StartInfo{'Server'}{'pid'})      if (exists $StartInfo{'Server'}{'pid'});
+   print2col ("        Socket",   $StartInfo{'Server'}{'socket'})   if (exists $StartInfo{'Server'}{'socket'});
+   print2col ("        TCP port", $StartInfo{'Server'}{'ip'})       if (exists $StartInfo{'Server'}{'ip'});
+   print2col ("        UID",      $StartInfo{'Server'}{'uid'})      if (exists $StartInfo{'Server'}{'uid'});
+   print2col ("        GID",      $StartInfo{'Server'}{'gid'})      if (exists $StartInfo{'Server'}{'gid'});
+   print2col ("        Logging",  $StartInfo{'Logging'})            if (exists $StartInfo{'Logging'});
+   print2col ("        Configuration Files",  $StartInfo{'Configs'})            if (exists $StartInfo{'Configs'});
+   print2col ("    SpamAssassin", $StartInfo{'sa_version'})         if (exists $StartInfo{'sa_version'});
+   print2col ("    SpamAssassin Debug Facilities", $StartInfo{'sa_debug'})     if (exists $StartInfo{'sa_debug'});
+   print2col ("    Database",     $StartInfo{'db'})                 if (exists $StartInfo{'db'});
+   #if (keys %{$StartInfo{'IDs'}}) {
+   #   print "    Process startup user/group:\n";
+   #   print "        User:  $StartInfo{'IDs'}{'user'}, EUID: $StartInfo{'IDs'}{'euid'}, UID: $StartInfo{'IDs'}{'uid'}\n";
+   #   print "        Group: $StartInfo{'IDs'}{'group'}, EGID: $StartInfo{'IDs'}{'egid'}, GID: $StartInfo{'IDs'}{'gid'}\n";
+   #}
+
+   sub print_modules ($ $) {
+      my ($key, $label) = @_;
+      print "    $label\n";
+      foreach (sort keys %{$StartInfo{$key}}) {
+         print "        $_\n";
+         foreach my $module (sort keys %{$StartInfo{$key}{$_}}) {
+            if ($StartInfo{$key}{$_}{$module}) {
+               print2col ("            " . $module, $StartInfo{$key}{$_}{$module});
+            }
+            else {
+               print2col ("            " . $module, "");
+            }
+         }
+      }
+   };
+   print_modules('AVScanner', 'Antivirus scanners');
+   print_modules('Code',      'Code, modules and external programs');
+   print_modules('Decoders',  'Decoders');
+   print_modules('SAPlugins', 'SpamAssassin plugins');
+}
+
+# Initialize the Getopts option list.  Requires the Section table to
+# be built already.
+#
+sub init_getopts_table() {
+   print "init_getopts_table: enter\n"  if $Opts{'debug'} & D_ARGS;
+
+   init_getopts_table_common(@supplemental_reports);
+
+   add_option ('first_recip_only!');
+   add_option ('show_first_recip_only=i',   sub { $Opts{'first_recip_only'} = $_[1]; 1;});
+   add_option ('startinfo!');
+   add_option ('show_startinfo=i',          sub { $Opts{'startinfo'} = $_[1]; 1; });
+   add_option ('by_ccat_summary!');
+   add_option ('show_by_ccat_summary=i',    sub { $Opts{'by_ccat_summary'} = $_[1]; 1; });
+   add_option ('noscore_percentiles',       \&triway_opts);
+   add_option ('score_percentiles=s',       \&triway_opts);
+   add_option ('noscore_frequencies',       \&triway_opts);
+   add_option ('score_frequencies=s',       \&triway_opts);
+   add_option ('nosa_timings',              sub { $Opts{'sa_timings'} = 0; 1; });
+   add_option ('sa_timings=i');
+   add_option ('sa_timings_percentiles=s');
+   add_option ('notimings',                 sub { $Opts{'timings'} = 0; 1; });
+   add_option ('timings=i');
+   add_option ('timings_percentiles=s');
+   add_option ('nosarules',                 \&triway_opts);
+   add_option ('sarules=s',                 \&triway_opts);
+   #add_option ('nop0f',                     \&triway_opts);
+   #add_option ('p0f=s',                     \&triway_opts);
+   add_option ('autolearn!');
+   add_option ('show_autolearn=i',          sub { $Opts{'autolearn'} = $_[1]; 1; });
+}
+
+# Builds the entire @Section table used for data collection
+#
+# Each Section entry has as many as six fields:
+#
+#   1. Section array reference
+#   2. Key to %Counts, %Totals accumulator hashes, and %Collecting hash
+#   3. Output in Detail report? (must also a %Counts accumulator)
+#   4. Numeric output format specifier for Summary report
+#   5. Section title for Summary and Detail reports
+#   6. A hash to a divisor used to calculate the percentage of a total for that key
+#
+# Use begin_section_group/end_section_group to create groupings around sections.
+# 
+# Sections can be freely reordered if desired, but maintain proper group nesting.
+#
+sub build_sect_table() {
+   print "build_sect_table: enter\n"  if $Opts{'debug'} & D_SECT;
+   my $S = \@Sections;
+
+   # References to these are used in the Sections table below; we'll predeclare them.
+   $Totals{'totalmsgs'} = 0;
+
+   # Place configuration and critical errors first
+
+   #    SECTIONREF, NAME,                 DETAIL, FMT, TITLE,                             DIVISOR
+   begin_section_group ($S, 'warnings');
+   add_section ($S, 'fatal',                   1, 'd', '*Fatal');
+   add_section ($S, 'panic',                   1, 'd', '*Panic');
+   add_section ($S, 'warningsecurity',         1, 'd', '*Warning: Security risk');
+   add_section ($S, 'avtimeout',               1, 'd', '*Warning: Virus scanner timeout');
+   add_section ($S, 'avconnectfailure',        1, 'd', '*Warning: Virus scanner connection failure');
+   add_section ($S, 'warningsmtpshutdown',     1, 'd', '*Warning: SMTP shutdown');
+   add_section ($S, 'warningsql',              1, 'd', '*Warning: SQL problem');
+   add_section ($S, 'warningaddressmodified',  1, 'd', '*Warning: Email address modified');
+   add_section ($S, 'warningnoquarantineid',   1, 'd', '*Warning: Message missing X-Quarantine-ID header');
+   add_section ($S, 'warning',                 1, 'd', 'Miscellaneous warnings');
+   end_section_group ($S, 'warnings');
+
+   begin_section_group ($S, 'scanned', "\n");
+   add_section ($S, 'totalmsgs',               0, 'd', [ 'Total messages scanned', '-' ],  \$Totals{'totalmsgs'});
+   add_section ($S, 'bytesscanned',            0, 'Z', 'Total bytes scanned');     # Z means print scaled as in 1k, 1m, etc.
+   end_section_group ($S, 'scanned', $sep1);
+
+   # Blocked / Passed
+   # Priority: VIRUS BANNED UNCHECKED SPAM SPAMMY BADH OVERSIZED MTA CLEAN
+   begin_section_group ($S, 'passblock', "\n");
+   begin_section_group ($S, 'blocked', "\n");
+   add_section ($S, 'totalblocked',            0, 'd', [ 'Blocked', '-' ],                 \$Totals{'totalmsgs'});
+   add_section ($S, 'malwareblocked',          1, 'd', '  Malware blocked',                \$Totals{'totalmsgs'});
+   add_section ($S, 'bannednameblocked',       1, 'd', '  Banned name blocked',            \$Totals{'totalmsgs'});
+   add_section ($S, 'uncheckedblocked',        1, 'd', '  Unchecked blocked',              \$Totals{'totalmsgs'});
+   add_section ($S, 'spamblocked',             1, 'd', '  Spam blocked',                   \$Totals{'totalmsgs'});
+   add_section ($S, 'spamdiscarded',           0, 'd', '  Spam discarded (no quarantine)', \$Totals{'totalmsgs'});
+   add_section ($S, 'spammyblocked',           1, 'd', '  Spammy blocked',                 \$Totals{'totalmsgs'});
+   add_section ($S, 'badheaderblocked',        1, 'd', '  Bad header blocked',             \$Totals{'totalmsgs'});
+   add_section ($S, 'oversizedblocked',        1, 'd', '  Oversized blocked',              \$Totals{'totalmsgs'});
+   add_section ($S, 'mtablocked',              1, 'd', '  MTA blocked',                    \$Totals{'totalmsgs'});
+   add_section ($S, 'cleanblocked',            1, 'd', '  Clean blocked',                  \$Totals{'totalmsgs'});
+   add_section ($S, 'tempfailblocked',         1, 'd', '  Tempfail blocked',               \$Totals{'totalmsgs'});
+   add_section ($S, 'otherblocked',            1, 'd', '  Other blocked',                  \$Totals{'totalmsgs'});
+   end_section_group ($S, 'blocked');
+
+   begin_section_group ($S, 'passed', "\n");
+   add_section ($S, 'totalpassed',             0, 'd', [ 'Passed', '-' ],                  \$Totals{'totalmsgs'});
+   add_section ($S, 'malwarepassed',           1, 'd', '  Malware passed',                 \$Totals{'totalmsgs'});
+   add_section ($S, 'bannednamepassed',        1, 'd', '  Banned name passed',             \$Totals{'totalmsgs'});
+   add_section ($S, 'uncheckedpassed',         1, 'd', '  Unchecked passed',               \$Totals{'totalmsgs'});
+   add_section ($S, 'spampassed',              1, 'd', '  Spam passed',                    \$Totals{'totalmsgs'});
+   add_section ($S, 'spammypassed',            1, 'd', '  Spammy passed',                  \$Totals{'totalmsgs'});
+   add_section ($S, 'badheaderpassed',         1, 'd', '  Bad header passed',              \$Totals{'totalmsgs'});
+   add_section ($S, 'oversizedpassed',         1, 'd', '  Oversized passed',               \$Totals{'totalmsgs'});
+   add_section ($S, 'mtapassed',               1, 'd', '  MTA passed',                     \$Totals{'totalmsgs'});
+   add_section ($S, 'cleanpassed',             1, 'd', '  Clean passed',                   \$Totals{'totalmsgs'});
+   add_section ($S, 'tempfailpassed',          1, 'd', '  Tempfail passed',                \$Totals{'totalmsgs'});
+   add_section ($S, 'otherpassed',             1, 'd', '  Other passed',                   \$Totals{'totalmsgs'});
+   end_section_group ($S, 'passed');
+   end_section_group ($S, 'passblock', $sep1);
+
+   if ($Opts{'by_ccat_summary'}) {
+      # begin level 1 group
+      begin_section_group ($S, 'by_ccat', "\n");
+
+      # begin level 2 groupings
+      begin_section_group ($S, 'malware', "\n");     # level 2
+      add_section ($S, 'totalmalware',            0, 'd', [ 'Malware', '-' ],                 \$Totals{'totalmsgs'});
+      add_section ($S, 'malwarepassed',           0, 'd', '  Malware passed',                 \$Totals{'totalmsgs'});
+      add_section ($S, 'malwareblocked',          0, 'd', '  Malware blocked',                \$Totals{'totalmsgs'});
+      end_section_group ($S, 'malware');
+
+      begin_section_group ($S, 'banned', "\n");
+      add_section ($S, 'totalbanned',             0, 'd', [ 'Banned', '-' ],                  \$Totals{'totalmsgs'});
+      add_section ($S, 'bannednamepassed',        0, 'd', '  Banned file passed',             \$Totals{'totalmsgs'});
+      add_section ($S, 'bannednameblocked',       0, 'd', '  Banned file blocked',            \$Totals{'totalmsgs'});
+      end_section_group ($S, 'banned');
+
+      begin_section_group ($S, 'unchecked', "\n");
+      add_section ($S, 'totalunchecked',          0, 'd', [ 'Unchecked', '-' ],               \$Totals{'totalmsgs'});
+      add_section ($S, 'uncheckedpassed',         0, 'd', '  Unchecked passed',               \$Totals{'totalmsgs'});
+      add_section ($S, 'uncheckedblocked',        0, 'd', '  Unchecked blocked',              \$Totals{'totalmsgs'});
+      end_section_group ($S, 'unchecked');
+
+      begin_section_group ($S, 'spam', "\n");
+      add_section ($S, 'totalspam',               0, 'd', [ 'Spam', '-' ],                    \$Totals{'totalmsgs'});
+      add_section ($S, 'spammypassed',            0, 'd', '  Spammy passed',                  \$Totals{'totalmsgs'});
+      add_section ($S, 'spammyblocked',           0, 'd', '  Spammy blocked',                 \$Totals{'totalmsgs'});
+      add_section ($S, 'spampassed',              0, 'd', '  Spam passed',                    \$Totals{'totalmsgs'});
+      add_section ($S, 'spamblocked',             0, 'd', '  Spam blocked',                   \$Totals{'totalmsgs'});
+      add_section ($S, 'spamdiscarded',           0, 'd', '  Spam discarded (no quarantine)', \$Totals{'totalmsgs'});
+      end_section_group ($S, 'spam');
+
+      begin_section_group ($S, 'ham', "\n");
+      add_section ($S, 'totalham',                0, 'd', [ 'Ham', '-' ],                     \$Totals{'totalmsgs'});
+      add_section ($S, 'badheaderpassed',         0, 'd', '  Bad header passed',              \$Totals{'totalmsgs'});
+      add_section ($S, 'badheaderblocked',        0, 'd', '  Bad header blocked',             \$Totals{'totalmsgs'});
+      add_section ($S, 'oversizedpassed',         0, 'd', '  Oversized passed',               \$Totals{'totalmsgs'});
+      add_section ($S, 'oversizedblocked',        0, 'd', '  Oversized blocked',              \$Totals{'totalmsgs'});
+      add_section ($S, 'mtapassed',               0, 'd', '  MTA passed',                     \$Totals{'totalmsgs'});
+      add_section ($S, 'mtablocked',              0, 'd', '  MTA blocked',                    \$Totals{'totalmsgs'});
+      add_section ($S, 'cleanpassed',             0, 'd', '  Clean passed',                   \$Totals{'totalmsgs'});
+      add_section ($S, 'cleanblocked',            0, 'd', '  Clean blocked',                  \$Totals{'totalmsgs'});
+      end_section_group ($S, 'ham');
+
+      begin_section_group ($S, 'other', "\n");
+      add_section ($S, 'totalother',              0, 'd', [ 'Other', '-' ],                   \$Totals{'totalmsgs'});
+      add_section ($S, 'tempfailpassed',          0, 'd', '  Tempfail passed',                \$Totals{'totalmsgs'});
+      add_section ($S, 'tempfailblocked',         0, 'd', '  Tempfail blocked',               \$Totals{'totalmsgs'});
+      add_section ($S, 'otherpassed',             0, 'd', '  Other passed',                   \$Totals{'totalmsgs'});
+      add_section ($S, 'otherblocked',            0, 'd', '  Other blocked',                  \$Totals{'totalmsgs'});
+      end_section_group ($S, 'other');
+      # end level 2 groupings
+
+      # end level 1 group
+      end_section_group ($S, 'by_ccat', $sep1);
+   }
+
+   begin_section_group ($S, 'misc', "\n");
+   add_section ($S, 'virusscanskipped',        1, 'd', 'Virus scan skipped');
+   add_section ($S, 'sabypassed',              0, 'd', 'SpamAssassin bypassed');
+   add_section ($S, 'satimeout',               0, 'd', 'SpamAssassin timeout');
+   add_section ($S, 'released',                1, 'd', 'Released from quarantine');
+   add_section ($S, 'defanged',                1, 'd', 'Defanged');
+   add_section ($S, 'truncatedheader',         0, 'd', 'Truncated headers > 998 characters');
+   add_section ($S, 'truncatedmsg',            0, 'd', 'Truncated message passed to SpamAssassin');
+   add_section ($S, 'tagged',                  0, 'd', 'Spam tagged');
+   add_section ($S, 'smtpresponse',            1, 'd', 'SMTP response');
+   add_section ($S, 'badaddress',              1, 'd', 'Bad address syntax');
+   add_section ($S, 'fakesender',              1, 'd', 'Fake sender');
+   add_section ($S, 'archiveextract',          1, 'd', 'Archive extraction problem');
+   add_section ($S, 'dsnsuppressed',           1, 'd', 'DSN suppressed');
+   add_section ($S, 'dsnnotification',         1, 'd', 'DSN notification (debug supplemental)');
+   add_section ($S, 'bouncekilled',            1, 'd', 'Bounce killed');
+   add_section ($S, 'bouncerescued',           1, 'd', 'Bounce rescued');
+   add_section ($S, 'bounceunverifiable',      1, 'd', 'Bounce unverifiable');
+   add_section ($S, 'nosubject',               0, 'd', 'Subject header inserted');
+   add_section ($S, 'whitelisted',             1, 'd', 'Whitelisted');
+   add_section ($S, 'blacklisted',             1, 'd', 'Blacklisted');
+   add_section ($S, 'penpalsaved',             1, 'd', 'Penpals saved from kill');
+   add_section ($S, 'tmppreserved',            1, 'd', 'Preserved temporary directory');
+   add_section ($S, 'dccerror',                1, 'd', 'DCC error');
+   add_section ($S, 'mimeerror',               1, 'd', 'MIME error');
+   add_section ($S, 'defangerror',             1, 'd', 'Defang error');
+   add_section ($S, 'badheadersupp',           1, 'd', 'Bad header (debug supplemental)');
+   add_section ($S, 'fileoutputskipped',       0, 'd', 'File(1) output skipped');
+   add_section ($S, 'localdeliveryskipped',    1, 'd', 'Local delivery skipped');
+   add_section ($S, 'extramodules',            1, 'd', 'Extra code modules loaded at runtime');
+   add_section ($S, 'malwarebyscanner',        1, 'd', 'Malware by scanner');
+   add_section ($S, 'malwaretospam',           1, 'd', 'Malware to spam conversion');
+   add_section ($S, 'contenttype',             1, 'd', 'Content types');
+   add_section ($S, 'bayes',                   1, 'd', 'Bayes probability');
+   add_section ($S, 'p0f',                     1, 'd', 'p0f fingerprint');
+   add_section ($S, 'sadiags',                 1, 'd', 'SpamAssassin diagnostics');
+   end_section_group ($S, 'misc');
+
+   print "build_sect_table: exit\n"  if $Opts{'debug'} & D_SECT;
+}
+
+# XXX create array of defaults for detail <5, 5-9, >10
+sub init_defaults() {
+   map { $Opts{$_} = $Defaults{$_} unless exists $Opts{$_} } keys %Defaults;
+   if (! $Opts{'standalone'}) {
+      # LOGWATCH these take affect if no env present (eg. nothing in conf file)
+      #  0 to 4 nostartinfo, notimings,   nosarules,        score_frequencies=0,        score_percentiles=0,      noautolearn
+      #  5 to 9 nostartinfo, timings=95,  sarules = 20 20,  score_frequencies=defaults, score_percentiles=defaults, autolearn
+      # 10 +    startinfo,   timings=100, sarules = all all score_frequencies=defaults, score_percentiles=defaults, autolearn
+
+      if ($Opts{'detail'} < 5) {          # detail 0 to 4, disable all supplimental reports
+         $Opts{'autolearn'}         = 0;
+         #$Opts{'p0f'}               = 0;
+         $Opts{'timings'}           = 0;
+         $Opts{'sa_timings'}        = 0;
+         $Opts{'sarules'}           = 0;
+         $Opts{'startinfo'}         = 0;
+         $Opts{'score_frequencies'} = '';
+         $Opts{'score_percentiles'} = '';
+      }
+      elsif ($Opts{'detail'} < 10) {      # detail 5 to 9, disable startinfo report
+         $Opts{'startinfo'}         = 0;
+      }
+      else {                              # detail 10 and up, full reports
+         #$Opts{'p0f'}              = 'all all';
+         $Opts{'timings'}          = 100;    
+         $Opts{'sa_timings'}       = 100;    
+         $Opts{'sarules'}          = 'all all';
+      }
+   }
+}
+
+# Return a usage string,  built from:
+#  arg1 +
+#  $usage_str +
+#  a string built from each usable entry in the @Sections table.
+#
+sub usage($) {
+   my $ret = "";
+   $ret = "@_\n"  if ($_[0]);
+   $ret .= $usage_str;
+   my ($name, $desc);
+   foreach my $sect (get_usable_sectvars(@Sections, 0)) {
+      $name = lc $sect->{NAME};
+      $desc = $sect->{TITLE};
+      $ret .= sprintf "   --%-38s%s\n", "$name" . ' LEVEL', "$desc";
+   }
+   $ret .= "\n";
+   return $ret;
+}
+
+sub strip_trace($) {
+   # at (eval 37) line 306, <GEN6> line 4.
+   # at /usr/sbin/amavisd-maia line 2895, <GEN4> line 22.
+   #$_[0] =~ s/ at \(.+\) line \d+(?:, \<GEN\d+\> line \d+)?\.$//;
+   #$_[0] =~ s/ at (\S+) line \d+(?:, \<GEN\d+\> line \d+)?\.$/: $1/;
+   while ($_[0] =~ s/ at (?:\(eval \d+\)|\S+) line \d+(?:, \<GEN\d+\> line \d+)?\.//) {
+      ;
+   }
+   #print "strip_trace: \"$_[0]\"\n";
+   return $_[0];
+}
+
+# Getopt helper, sets an option in Opts hash to one of three
+# values: its default, the specified value, or 0 if the option
+# was the "no" prefixed variant.
+#
+sub triway_opts ($ $) {
+   my ($opt,$val) = @_;
+
+   print "triway_opts: OPT: $opt, VAL: $val\n"    if $Opts{'debug'} & D_ARGS;
+   die "Option \"--${opt}\" requires an argument" if ($val =~ /^--/);
+
+   if ($opt =~ s/^no//i) {
+      $Opts{$opt} = 0;
+   } elsif ('default' =~ /^${val}$/i) {
+      $Opts{$opt} = $Defaults{$opt};
+   }
+   else {
+      $Opts{$opt} = $val;
+   }
+}
+
+exit(0);
+
+# vi: shiftwidth=3 tabstop=3 syntax=perl et
diff --git a/amavis-logwatch.1 b/amavis-logwatch.1
new file mode 100644 (file)
index 0000000..d8488a4
--- /dev/null
@@ -0,0 +1,923 @@
+.TH AMAVIS-LOGWATCH 1 
+.ad
+.fi
+.SH NAME
+amavis-logwatch
+\-
+An Amavisd-new log parser and analysis utility
+.SH "SYNOPSIS"
+.na
+.nf
+.fi
+\fBamavis-logwatch\fR [\fIoptions\fR] [\fIlogfile ...\fR]
+.SH DESCRIPTION
+.ad
+.fi
+The \fBamavis-logwatch\fR(1) utility is an Amavisd-new log parser
+that produces summaries, details, and statistics regarding
+the operation of Amavisd-new (henceforth, simply called Amavis).
+.PP
+This utility can be used as a
+standalone program, or as a Logwatch filter module to produce
+Amavisd-new summary and detailed reports from within Logwatch.
+.PP
+\fBAmavis-logwatch\fR is able to produce
+a wide range of reports with data grouped and sorted as much as possible
+to reduce noise and highlight patterns.
+Brief summary reports provide a
+quick overview of general Amavis operations and message
+delivery, calling out warnings that may require attention.
+Detailed reports provide easy to scan, hierarchically-arranged
+and organized information, with as much or little detail as
+desired.
+.PP
+Much of the interesting data is available when Amavis'
+$log_level is set to at least 2.
+See \fBAmavis Log Level\fR below.
+.PP
+\fBAmavis-logwatch\fR outputs two principal sections: a \fBSummary\fR section
+and a \fBDetailed\fR section.
+For readability and quick scanning, all event or hit counts appear in the left column,
+followed by brief description of the event type, and finally additional
+statistics or count representations may appear in the rightmost column.
+
+The following segment from a sample Summary report illustrates:
+.RS 4
+.nf
+
+****** Summary ********************************************
+
+       9   Miscellaneous warnings 
+
+   20313   Total messages scanned ----------------  100.00%
+1008.534M  Total bytes scanned                1,057,524,252
+========   ================================================
+
+    1190   Blocked -------------------------------    5.86%
+      18     Malware blocked                          0.09%
+       4     Banned name blocked                      0.02%
+     416     Spam blocked                             2.05%
+     752     Spam discarded (no quarantine)           3.70%
+
+   19123   Passed --------------------------------   94.14%
+      47     Bad header passed                        0.23%
+   19076     Clean passed                            93.91%
+========   ================================================
+
+      18   Malware -------------------------------    0.09%
+      18     Malware blocked                          0.09%
+
+       4   Banned --------------------------------    0.02%
+       4     Banned file blocked                      0.02%
+
+    1168   Spam ----------------------------------    5.75%
+     416     Spam blocked                             2.05%
+     752     Spam discarded (no quarantine)           3.70%
+
+   19123   Ham -----------------------------------   94.14%
+      47     Bad header passed                        0.23%
+   19076     Clean passed                            93.91%
+========   ================================================
+
+    1982   SpamAssassin bypassed 
+      32   Released from quarantine 
+       2   DSN notification (debug supplemental) 
+       2   Bounce unverifiable   
+    2369   Whitelisted           
+       2   Blacklisted           
+      12   MIME error            
+      58   Bad header (debug supplemental) 
+      40   Extra code modules loaded at runtime 
+
+.fi
+.RE 0
+The report indicates there were 9 general warnings, and
+\fBAmavis\fR scanned a total of 20313 messages
+for a total of 1008.53 megabytes or 1,057,524,252 bytes.
+The next summary groups shows the Blocked / Passed overview, 
+with 1190 Blocked messages (broken down as 18 messages blocked as malware,
+4 messages with banned names, 416 spam messages, and 752 discarded
+messages), and 19123 Passed messages (47 messages with bad headers
+and 19076 clean messages).
+
+The next (optional) summary grouping shows message disposition by contents category.  
+There were 18 malware messages and 4 banned file messages (all blocked), 
+1168 Spam messages, of which 416 were blocked (quarantined) and 752 discarded.
+Finally, there were 19123 messages consdidered to be Ham (i.e. not spam), 47
+of which contained bad headers.
+
+Additional count summaries for a variety of events are also listed.
+.PP
+There are dozens of sub-sections available in the \fBDetailed\fR report, each of
+whose output can be controlled in various ways.
+Each sub-section attempts to group and present the most meaningful data at superior levels,
+while pushing less useful or \fInoisy\fR data towards inferior levels.
+The goal is to provide as much benefit as possible from smart grouping of
+data, to allow faster report scanning, pattern identification, and problem solving.
+Data is always sorted in descending order by count, and then numerically by IP address
+or alphabetically as appropriate.
+.PP
+The following Spam blocked segment from a sample \fBDetailed\fR report
+illustrates the basic hierarchical level structure of \fBamavis-logwatch\fR:
+.RS 4
+.nf
+
+****** Detailed *******************************************
+
+   19346   Spam blocked -----------------------------------
+     756      from@example.com
+      12         10.0.0.2
+      12            <>
+      12         192.168.2.2
+      12            <>
+       5         192.168.2.1
+     ...
+
+.fi
+.RE 0
+.PP
+The \fBamavis-logwatch\fR utility reads from STDIN or from the named Amavis
+\fIlogfile\fR.
+Multiple \fIlogfile\fR arguments may be specified, each processed
+in order.
+The user running \fBamavis-logwatch\fR must have read permission on
+each named log file.
+.PP
+.SS Options
+The options listed below affect the operation of \fBamavis-logwatch\fR.
+Options specified later on the command line override earlier ones.
+Any option may be abbreviated to an unambiguous length.
+
+.IP "\fB--[no]autolearn\fR"
+.PD 0
+.IP "\fB--show_autolearn \fIboolean\fR"
+.PD
+Enables (disables) output of the autolearn report.
+This report is only available if the default Amavis \fB$log_templ\fR
+has been modified to provide autolearn results in log entries.
+This can be done by uncommenting two lines in the Amavis program itself (where the
+default log templates reside), or by correctly adding the \fB$log_templ\fR
+variable to the \fBamavisd.conf\fR file.
+See Amavis' \fBREADME.customize\fR and search near the end
+of the Amavisd program for "autolearn".
+.IP "\fB--[no]by_ccat_summary\fR"
+.PD 0
+.IP "\fB--show_by_ccat_summary \fIboolean\fR"
+.PD
+Enables (disables) the by contents category summary in the \fBSummary\fR section.
+Default: enabled.
+.IP "\fB-f \fIconfig_file\fR"
+.PD 0
+.IP "\fB--config_file \fIconfig_file\fR"
+.PD
+Use an alternate configuration file \fIconfig_file\fR instead of
+the default.
+This option may be used more than once.
+Multiple configuration files will be processed in the order presented on the command line.
+See \fBCONFIGURATION FILE\fR below.
+.IP "\fB--debug \fIkeywords\fR"
+Output debug information during the operation of \fBamavis-logwatch\fR.
+The parameter \fIkeywords\fR is one or more comma or space separated keywords.
+To obtain the list of valid keywords, use --debug xxx where xxx is any invalid keyword.
+.IP "\fB--detail \fIlevel\fR"
+Sets the maximum detail level for \fBamavis-logwatch\fR to \fIlevel\fR.
+This option is global, overriding any other output limiters described below.
+
+The \fBamavis-logwatch\fR utility
+produces a \fBSummary\fR section, a \fBDetailed\fR section, and
+additional report sections.
+With \fIlevel\fR less than 5, \fBamavis-logwatch\fR will produce
+only the \fBSummary\fR section.
+At \fIlevel\fR 5 and above, the \fBDetailed\fR section, and any
+additional report sections are candidates for output.
+Each incremental increase in \fIlevel\fR generates one additional
+hierarchical sub-level of output in the \fBDetailed\fR section of the report.
+At \fIlevel\fR 10, all levels are output.
+Lines that exceed the maximum report width (specified with 
+\fBmax_report_width\fR) will be cut.
+Setting \fIlevel\fR to 11 will prevent lines in the report from being cut (see also \fB--line_style\fR).
+.IP "\fB--[no]first_recip_only\fR"
+.PD 0
+.IP "\fB--show_first_recip_only \fIboolean\fR"
+.PD
+Specifies whether or not to sort by, and show, only the first
+recipient when a scanned messages contains multiple recipients.
+.IP "\fB--help\fR"
+Print usage information and a brief description about command line options.
+.IP "\fB--ipaddr_width \fIwidth\fR"
+Specifies that IP addresses in address/hostname pairs should be printed
+with a field width of \fIwidth\fR characters.
+Increasing the default may be useful for systems using long IPv6 addresses.
+.IP "\fB-l limiter=levelspec\fR"
+.PD 0
+.IP "\fB--limit limiter=levelspec\fR"
+.PD
+Sets the level limiter \fIlimiter\fR with the specification \fIlevelspec\fR.
+.IP "\fB--line_style \fIstyle\fR"
+Specifies how to handle long report lines.
+Three styles are available: \fBfull\fR, \fBtruncate\fR, and \fBwrap\fR.
+Setting \fIstyle\fR to \fBfull\fR will prevent cutting lines to \fBmax_report_width\fR; 
+this is what occurs when \fBdetail\fR is 11 or higher.
+When \fIstyle\fR is \fBtruncate\fR (the default), 
+long lines will be truncated according to \fBmax_report_width\fR.
+Setting \fIstyle\fR to \fBwrap\fR will wrap lines longer than \fBmax_report_width\fR such that
+left column hit counts are not obscured.
+This option takes precedence over the line style implied by the \fBdetail\fR level.
+The options \fB--full\fR, \fB--truncate\fR, and \fB--wrap\fR are synonyms.
+
+.IP "\fB--nodetail\fR"
+Disables the \fBDetailed\fR section of the report, and all supplemental reports.
+This option provides a convenient mechanism to quickly disable all sections
+under the \fBDetailed\fR report, where subsequent command line
+options may re-enable one or more sections to create specific reports.
+
+.PD 0
+.IP "\fB--sarules \fR\`\fIS,H\fR\'"
+.IP "\fB--sarules default"
+.PD
+Enables the SpamAssassin Rules Hit report.
+The comma-separated \fIS\fR and \fIH\fR arguments are top N values for the Spam and Ham
+reports, respectively, and can be any integer greater than or equal to 0, or the keyword \fBall\fR.
+The keyword \fBdefault\fR uses the built-in default values.
+.IP "\fB--nosarules\fR"
+Disables the SpamAssassin Rules Hit report.
+
+.PD 0
+.IP "\fB--sa_timings \fR\fInrows\fR"
+Enables the SpamAssassin Timings percentiles report.
+The report can be limited to the top N rows with the \fInrows\fR argument.
+This report requires Amavis 2.6+ and SpamAssassin 3.3+.
+.PD
+.IP "\fB--sa_timings_percentiles \fR\`\fIP1 [P2 ...]\fR\'"
+Specifies the percentiles shown in the SpamAssassin Timings report.
+The arguments \fIP1 ...\fR are integers from 0 to 100 inclusive.
+Their order will be preserved in the report.
+.IP "\fB--nosa_timings\fR"
+Disables the SpamAssassin Timings report.
+.IP "\fB--version\fR"
+Print \fBamavis-logwatch\fR version information.
+
+.PD 0
+.IP "\fB--score_frequencies \fR\`\fIB1 [B2 ...]\fR\'"
+.IP "\fB--score_frequencies default"
+.PD
+Enables the Spam Score Frequency report.
+The arguments \fIB1 ...\fR are frequency distribution buckets, and can be any real numbers.
+Their order will be preserved in the report.
+The keyword \fBdefault\fR uses the built-in default values.
+.IP "\fB--noscore_frequencies\fR"
+Disables the Spam Score Frequency report.
+
+.PD 0
+.IP "\fB--score_percentiles \fR\`\fIP1 [P2 ...]\fR\'"
+.IP "\fB--score_percentiles default"
+.PD
+Enables the Spam Score Percentiles report.
+The arguments \fIP1 ...\fR specify the percentiles shown in the report,
+and are integers from 0 to 100 inclusive.
+The keyword \fBdefault\fR uses the built-in default values.
+.IP "\fB--noscore_percentiles\fR"
+Disables the Spam Score Percentiles report.
+
+.IP "\fB--[no]sect_vars\fR"
+.PD 0
+.IP "\fB--show_sect_vars \fIboolean\fR"
+.PD
+Enables (disables) supplementing each \fBDetailed\fR section title
+with the name of that section's level limiter.
+The name displayed is the command line option (or configuration
+file variable) used to limit that section's output.
+.
+With the large number of level limiters available in \fBamavis-logwatch\fR,
+this a convenient mechanism for determining exactly which level limiter
+affects a section.
+.IP "\fB--[no]startinfo\fR"
+.PD 0
+.IP "\fB--show_startinfo \fIboolean\fR"
+.PD
+Enables (disables) the Amavis startup report showing most recent Amavis startup details.
+.IP "\fB--[no]summary\fR"
+.IP "\fB--show_summary\fR"
+Enables (disables) displaying of the the \fBSummary\fR section of the report.
+The variable Amavis_Show_Summary in used in a configuration file.
+.IP "\fB--syslog_name \fInamepat\fR"
+Specifies the syslog service name that \fBamavis-logwatch\fR uses
+to match syslog lines.
+Only log lines whose service name matches
+the perl regular expression \fInamepat\fR will be used by
+\fBamavis-logwatch\fR; all non-matching lines are silently ignored.
+This is useful when a pre-installed Amavis package uses a name
+other than the default (\fBamavis\fR).
+
+\fBNote:\fR if you use parenthesis in your regular expression, be sure they are cloistering
+and not capturing: use  \fB(?:\fIpattern\fB)\fR instead of \fB(\fIpattern\fB)\fR.
+
+.PD 0
+.IP "\fB--timings \fR\fIpercent\fR"
+Enables the Amavis Scan Timings percentiles report.
+The report can be top N-percent limited with the \fIpercent\fR argument.
+.PD
+.IP "\fB--timings_percentiles \fR\`\fIP1 [P2 ...]\fR\'"
+Specifies the percentiles shown in the Scan Timings report.
+The arguments \fIP1 ...\fR are integers from 0 to 100 inclusive.
+Their order will be preserved in the report.
+.IP "\fB--notimings\fR"
+Disables the Amavis Scan Timings report.
+.IP "\fB--version\fR"
+Print \fBamavis-logwatch\fR version information.
+
+.SS Level Limiters
+.PP
+The output of every section in the \fBDetailed\fR report is controlled by a level limiter.
+The name of the level limiter variable will be output when the \fBsect_vars\fR option is set.
+Level limiters are set either via command line in standalone mode with \fB--limit \fIlimiter\fB=\fIlevelspec\fR option,
+or via configuration file variable \fB$amavis_\fIlimiter\fB=\fIlevelspec\fR.
+Each limiter requires a \fIlevelspec\fR argument, which is described below in \fBLEVEL CONTROL\fR.
+
+The list of level limiters is shown below.
+
+.de TQ
+.  br
+.  ns
+.  TP \\$1
+..
+
+.PD 0
+.PP
+Amavis major contents category (ccatmajor) sections, listed in order of priority:
+VIRUS, BANNED, UNCHECKED, SPAM, SPAMMY, BADH, OVERSIZED, MTA, CLEAN.
+
+.IP "\fBMalwareBlocked"
+.IP "\fBMalwarePassed"
+Blocked or passed messages that contain malware (ccatmajor: VIRUS).
+
+.IP "\fBBannedNameBlocked"
+.IP "\fBBannedNamePassed"
+Blocked or passed messages that contain banned names in MIME parts (ccatmajor: BANNED).
+
+.IP "\fBUncheckedBlocked"
+.IP "\fBUncheckedPassed"
+Blocked or passed messages that were not checked by a virus scanner or SpamAssassin (Amavis ccatmajor: UNCHECKED).
+
+.IP "\fBSpamBlocked"
+.IP "\fBSpamPassed"
+Blocked or passed messages that were considered spam that reached kill level (Amavis ccatmajor: SPAM)
+
+.IP "\fBSpammyBlocked"
+.IP "\fBSpammyPassed"
+Blocked or passed messages that were considered spam, but did not reach kill level (Amavis ccatmajor: SPAMMY)
+
+.IP "\fBBadHeaderBlocked"
+.IP "\fBBadHeaderPassed"
+Blocked or passed messages that contain bad mail headers (ccatmajor: BAD-HEADER).
+
+.IP "\fBOversizedBlocked"
+.IP "\fBOversizedPassed"
+Blocked or passed messages that were considered oversized (Amavis ccatmajor: OVERSIZED).
+
+.IP "\fBMtaBlocked"
+.IP "\fBMtaPassed"
+Blocked or passed messages due to failure to re-inject to MTA (Amavis ccatmajor: MTA-BLOCKED).
+Occurrences of this event indicates a configuration problem.
+[ note: I don't believe mtapassed occurs, but exists for completeness.]
+
+.IP "\fBOtherBlocked"
+.IP "\fBOtherPassed"
+Blocked or passed messages that are not any of other major contents categories (Amavis ccatmajor: OTHER).
+
+
+.IP "\fBTempFailBlocked"
+.IP "\fBTempfailPassed"
+Blocked or passed messages that had a temporary failure (Amavis ccatmajor: TEMPFAIL)
+
+.IP "\fBCleanBlocked"
+.IP "\fBCleanPassed "
+Messages blocked or passed which were considered clean (Amavis ccatmajor: CLEAN; i.e. non-spam, non-viral).
+
+.PP
+Other sections, arranged alphabetically:
+
+.IP "\fBAvConnectFailure"
+Problems connecting to Anti-Virus scanner(s).
+
+.IP "\fBAvTimeout"
+Timeouts awaiting responses from Anti-Virus scanner(s).
+
+.IP "\fBArchiveExtract"
+Archive extraction problems.
+
+.IP "\fBBadHeaderSupp"
+Supplemental debug information regarding messages containing bad mail headers.
+
+.IP "\fBBayes"
+Messages frequencies by Bayesian probability buckets.
+
+.IP "\fBBadAddress"
+Invalid mail address syntax.
+
+.IP "\fBBlacklisted"
+Messages that were (soft-)blacklisted.  See also Whitelisted below.
+
+.IP "\fBBounceKilled"
+.IP "\fBBounceRescued"
+.IP "\fBBounceUnverifiable"
+Disposition of incoming bounce messages (DSNs).
+
+.IP "\fBContentType"
+MIME attachment breakdown by type/subtype.
+
+.IP "\fBDccError"
+Errors encountered with or returned by DCC.
+
+.IP "\fBDefangError"
+Errors encountered during defang process.
+
+.IP "\fBDefanged"
+Messages defanged (rendered harmless).
+
+.IP "\fBDsnNotification"
+Errors encountered during attempt to send delivery status notification.
+
+.IP "\fBDsnSuppressed"
+Delivery status notification (DSN) intentionally suppressed.
+
+.IP "\fBExtraModules"
+Additional code modules Amavis loaded during runtime.
+
+.IP "\fBFakeSender"
+Forged sender addresses, as determimed by Amavis.
+
+.IP "\fBFatal"
+Fatal events.  These are presented at the top of the report, as they may require attention.
+
+.IP "\fBLocalDeliverySkipped"
+Failures delivering to a local address.
+
+.IP "\fBMalwareByScanner"
+Breakdown of malware by scanner(s) that detected the malware.
+
+.IP "\fBMimeError"
+Errors encountered during MIME extraction.
+
+.IP "\fBPanic"
+Panic events.  These are presented at the top of the report, as they may require attention.
+
+.IP "\fBp0f"
+Passive fingerprint (p0f) hits, grouped by mail contents type (virus, unchecked, banned, spam, ham),
+next by operating system genre, and finally by IP address.
+Note: Windows systems are refined by Windows OS version, whereas versions of other operating systems
+are grouped generically.
+
+.IP "\fBReleased"
+Messages that were released from Amavis quarantine.
+
+.IP "\fBSADiags"
+Diagnostics as reported from SpamAssassin.
+
+.IP "\fBSmtpResponse"
+SMTP responses received during dialog with MTA.  These log entries are primarly debug.
+
+.IP "\fBTmpPreserved"
+Temporary directories preserved by Amavis when some component encounters a problem or failure.
+Directories listed and their corresponding log entries should be evaluated for problems.
+
+.IP "\fBVirusScanSkipped"
+Messages that could not be scanned by a virus scanner.
+
+.IP "\fBWarning"
+Warning events not categorized in specific warnings below.
+These are presented at the top of the report, as they may require attention.
+
+.IP "\fBWarningAddressModified"
+Incomplete email addresses modified by Amavis for safety.
+
+.IP "\fBWarningNoQuarantineId"
+Attempts to release a quarantined message that did not contain an X-Quarantine-ID header.
+
+.IP "\fBWarningSecurity \fIlevelspec\fR"
+Insecure configuration or utility used by Amavis.
+
+.IP "\fBWarningSmtpShutdown"
+Failures during SMTP conversation with MTA.
+
+.IP "\fBWarningSql"
+Failures to communicate with, or error replies from, SQL service.
+
+.IP "\fBWhitelisted"
+Messages that were (soft-)whitelisted.  See also Blacklisted above.
+
+.PD
+.SH LEVEL CONTROL
+.ad
+.fi
+The \fBDetailed\fR section of the report consists of a number of sub-sections,
+each of which is controlled both globally and independently.
+Two settings influence the output provided in the \fBDetailed\fR report: 
+a global detail level (specified with \fB--detail\fR) which has final (big hammer)
+output-limiting control over the \fBDetailed\fR section,
+and sub-section specific detail settings (small hammer), which allow further limiting
+of the output for a sub-section.
+Each sub-section may be limited to a specific depth level, and each sub-level may be limited with top N or threshold limits.
+The \fIlevelspec\fR argument to each of the level limiters listed above is used to accomplish this.
+
+It is probably best to continue explanation of sub-level limiting with the following well-known outline-style hierarchy, and
+some basic examples:
+.nf
+
+    level 0
+       level 1
+          level 2
+             level 3
+                level 4
+                level 4
+          level 2
+             level 3
+                level 4
+                level 4
+                level 4
+             level 3
+                level 4
+             level 3
+       level 1
+          level 2
+             level 3
+                level 4
+.fi
+.PP
+The simplest form of output limiting suppresses all output below a specified level.
+For example, a \fIlevelspec\fR set to "2" shows only data in levels 0 through 2.
+Think of this as collapsing each sub-level 2 item, thus hiding all inferior levels (3, 4, ...),
+to yield:
+.nf
+
+    level 0
+       level 1
+          level 2
+          level 2
+       level 1
+          level 2
+.fi
+.PP
+Sometimes the volume of output in a section is too great, and it is useful to suppress any data that does not exceed a certain threshold value.
+Consider a dictionary spam attack, which produces very lengthy lists of hit-once recipient email or IP addresses.
+Each sub-level in the hierarchy can be threshold-limited by setting the \fIlevelspec\fR appropriately.
+Setting \fIlevelspec\fR to the value "2::5" will suppress any data at level 2 that does not exceed a hit count of 5.
+.PP
+Perhaps producing a top N list, such as top 10 senders, is desired.
+A \fIlevelspec\fR of "3:10:" limits level 3 data to only the top 10 hits.
+.PP
+With those simple examples out of the way, a \fIlevelspec\fR is defined as a whitespace- or comma-separated list of one or more of the following:
+.IP "\fIl\fR"
+Specifies the maximum level to be output for this sub-section, with a range from 0 to 10.
+if \fIl\fR is 0, no levels will be output, effectively disabling the sub-section
+(level 0 data is already provided in the Summary report, so level 1 is considered the first useful level in the \fBDetailed\fR report).
+Higher values will produce output up to and including the specified level.
+.IP "\fIl\fB.\fIn\fR"
+Same as above, with the addition that \fIn\fR limits this section's level 1 output to
+the top \fIn\fR items.
+The value for \fIn\fR can be any integer greater than 1.
+(This form of limiting has less utility than the syntax shown below. It is provided for
+backwards compatibility; users are encouraged to use the syntax below).
+.IP "\fIl\fB:\fIn\fB:\fIt\fR"
+This triplet specifies level \fIl\fR, top \fIn\fR, and minimum threshold \fIt\fR.
+Each of the values are integers, with \fIl\fR being the level limiter as described above, \fIn\fR being
+a top \fIn\fR limiter for the level \fIl\fR, and \fIt\fR being the threshold limiter for level \fIl\fR.
+When both \fIn\fR and \fIt\fR are specified, \fIn\fR has priority, allowing top \fIn\fR lists (regardless of
+threshold value).
+If the value of \fIl\fR is omitted, the specified values for \fIn\fR and/or \fIt\fR are used for
+all levels available in the sub-section.
+This permits a simple form of wildcarding (eg. place minimum threshold limits on all levels).
+However, specific limiters always override wildcard limiters.
+The first form of level limiter may be included in \fIlevelspec\fR to restrict output, regardless of how many triplets are present.
+.PP
+All three forms of limiters are effective only when \fBamavis-logwatch\fR's detail level is 5
+or greater (the \fBDetailed\fR section is not activated until detail is at least 5).
+.PP
+See the \fBEXAMPLES\fR section for usage scenarios.
+.SH CONFIGURATION FILE
+.ad
+\fBAmavis-logwatch\fR can read configuration settings from a configuration file.
+Essentially, any command line option can be placed into a configuration file, and
+these settings are read upon startup.
+
+Because \fBamavis-logwatch\fR can run either standalone or within Logwatch,
+to minimize confusion, \fBamavis-logwatch\fR inherits Logwatch's configuration
+file syntax requirements and conventions.
+These are:
+.IP \(bu 4'. 
+White space lines are ignored.
+.IP \(bu 4'. 
+Lines beginning with \fB#\fR are ignored
+.IP \(bu 4'. 
+Settings are of the form:
+.nf
+
+        \fIoption\fB = \fIvalue\fR
+
+.fi
+.IP \(bu 4'. 
+Spaces or tabs on either side of the \fB=\fR character are ignored.
+.IP \(bu 4'. 
+Any \fIvalue\fR protected in double quotes will be case-preserved.
+.IP \(bu 4'. 
+All other content is reduced to lowercase (non-preserving, case insensitive).
+.IP \(bu 4'. 
+All \fBamavis-logwatch\fR configuration settings must be prefixed with "\fB$amavis_\fR" or
+\fBamavis-logwatch\fR will ignore them.
+.IP \(bu 4'. 
+When running under Logwatch, any values not prefixed with "\fB$amavis_\fR" are
+consumed by Logwatch; it only passes to \fBamavis-logwatch\fR (via environment variable)
+settings it considers valid.
+.IP \(bu 4'. 
+The values \fBTrue\fR and \fBYes\fR are converted to 1, and \fBFalse\fR and \fBNo\fR are converted to 0.
+.IP \(bu 4'. 
+Order of settings is not preserved within a configuration file (since settings are passed
+by Logwatch via environment variables, which have no defined order).
+.PP
+To include a command line option in a configuration file,
+prefix the command line option name with the word "\fB$amavis_\fR".
+The following configuration file setting and command line option are equivalent:
+.nf
+
+        \fB$amavis_Line_Style = Truncate\fR
+
+        \fB--line_style Truncate\fR
+
+.fi
+Level limiters are also prefixed with \fB$amavis_\fR, but on the command line are specified with the \fB--limit\fR option:
+.nf
+
+        \fB$amavis_SpamBlocked = 2\fR
+
+        \fB--limit SpamBlocked=2\fR
+
+.fi
+
+
+The order of command line options and configuration file processing occurs as follows:
+1) The default configuration file is read if it exists and no \fB--config_file\fR was specified on a command line.
+2) Configuration files are read and processed in the order found on the command line.
+3) Command line options override any options already set either via command line or from any configuration file.
+
+Command line options are interpreted when they are seen on the command line, and later options will override previously set options.
+
+
+.SH "EXIT STATUS"
+.na
+.nf
+.ad
+.fi
+The \fBamavis-logwatch\fR utility exits with a status code of 0, unless an error
+occurred, in which case a non-zero exit status is returned.
+.SH "EXAMPLES"
+.na
+.nf
+.ad
+.fi
+.SS Running Standalone
+\fBNote:\fR \fBamavis-logwatch\fR reads its log data from one or more named Amavis log files, or from STDIN.
+For brevity, where required, the examples below use the word \fIfile\fR as the command line
+argument meaning \fI/path/to/amavis.log\fR.
+Obviously you will need to substitute \fIfile\fR with the appropriate path.
+.nf
+.PP
+To run \fBamavis-logwatch\fR in standalone mode, simply run:
+.nf
+.RS 4
+.PP
+\fBamavis-logwatch \fIfile\fR
+.RE 0
+.nf
+.PP
+A complete list of options and basic usage is available via:
+.nf
+.RS 4
+.PP
+\fBamavis-logwatch --help\fR
+.RE 0
+.nf
+.PP
+To print a summary only report of Amavis log data:
+.nf
+.RS 4
+.PP
+\fBamavis-logwatch --detail 1 \fIfile\fR
+.RE 0
+.fi
+.PP
+To produce a summary report and a one-level detail report for May 25th:
+.nf
+.RS 4
+.PP
+\fBgrep 'May 25' \fIfile\fB | amavis-logwatch --detail 5\fR
+.RE 0
+.fi
+.PP
+To produce only a top 10 list of Sent email domains, the summary report and detailed reports
+are first disabled. Since commands line options are read and enabled left-to-right,
+the Sent section is re-enabled to level 1 with a level 1 top 10 limiter:
+.nf
+.RS 4
+.PP
+\fBamavis-logwatch --nosummary --nodetail \\
+   --limit spamblocked '1 1:10:' \fIfile\fR
+.RE 0
+.fi
+.PP
+The following command and its sample output shows a more complex level limiter example.
+The command gives the top 4 spam blocked recipients (level 1), and under with each recipient
+the top 2 sending IPs (level 2) and finally below that, only envelope from addresses (level 3) with hit counts
+greater than 6.
+Ellipses indicate top N or threshold-limited data:
+.nf
+.RS 4
+.PP
+\fBamavis-logwatch --nosummary --nodetail \\
+        --limit spamblocked '1:4: 2:2: 3::6' \fIfile\fR
+.nf
+
+19346   Spam blocked -----------------------------------
+  756      joe@example.com
+   12         10.0.0.1
+   12            <>
+   12         10.99.99.99
+   12            <>
+            ...
+  640      fred@example.com
+    8         10.0.0.1
+    8            <>
+    8         192.168.3.19
+    8            <>
+            ...
+  595      peter@sample.net
+    8         10.0.0.1
+    8            <>
+    7         192.168.3.3
+    7            <>
+            ...
+  547      paul@example.us
+    8         192.168.3.19
+    8            <>
+    7         10.0.0.1
+    7            <>
+             ...
+          ...
+.fi
+.RE 0
+.fi
+.SS Running within Logwatch
+\fBNote:\fR Logwatch versions prior to 7.3.6, unless configured otherwise, required the \fB--print\fR option to print to STDOUT instead of sending reports via email.
+Since version 7.3.6, STDOUT is the default output destination, and the \fB--print\fR option has been replaced
+by \fB--output stdout\fR. Check your configuration to determine where report output will be directed, and add the appropriate option to the commands below.
+.PP
+To print a summary report for today's Amavis log data:
+.nf
+.RS 4
+.PP
+\fBlogwatch --service amavis --range today --detail 1\fR
+.RE 0
+.nf
+.PP
+To print a report for today's Amavis log data, with one level
+of detail in the \fBDetailed\fR section:
+.nf
+.RS 4
+.PP
+\fBlogwatch --service amavis --range today --detail 5\fR
+.RE 0
+.fi
+.PP
+To print a report for yesterday, with two levels of detail in the \fBDetailed\fR section:
+.nf
+.RS 4
+.PP
+\fBlogwatch --service amavis --range yesterday --detail 6\fR
+.RE 0
+.fi
+.PP
+To print a report from Dec 12th through Dec 14th, with four levels of detail in the \fBDetailed\fR section:
+.nf
+.RS 4
+.PP
+\fBlogwatch --service amavis --range \\
+        'between 12/12 and 12/14' --detail 8\fR
+.RE 0
+.PP
+To print a report for today, with all levels of detail:
+.nf
+.RS 4
+.PP
+\fBlogwatch --service amavis --range today --detail 10\fR
+.RE 0
+.PP
+Same as above, but leaves long lines uncropped:
+.nf
+.RS 4
+.PP
+\fBlogwatch --service amavis --range today --detail 11\fR
+.RE 0
+.SS "Amavis Log Level"
+.PP
+Amavis provides additional log information when the variable 
+\fB$log_level\fR is increased above the default 0 value.
+This information is used by the \fBamavis-logwatch\fR utility to provide additional reports,
+not available with the default \fB$log_level\fR=0 value.
+A \fB$log_level\fR of 2 is suggested.
+.PP
+If you prefer not to increase the noise level in your main mail or Amavis logs,
+you can configure syslog to log Amavis' output to multiple log files,
+where basic log entries are routed to your main mail log(s) and more detailed
+entries routed to an Amavis-specific log file used to feed the \fBamavis-logwatch\fR utility.
+.PP
+A convenient way to accomplish this is to change the Amavis
+configuration variables in \fBamavisd.conf\fR as shown below:
+.nf
+
+    amavisd.conf:
+        $log_level = 2;
+        $syslog_facility = 'local5';
+        $syslog_priority = 'debug';
+
+.fi
+.PP
+This increases \fB$log_level\fR to 2, and sends Amavis' log entries to
+an alternate syslog facility (eg. \fBlocal5\fR, user), which can then be
+routed to one or more log files, including your main mail log file:
+.nf
+
+    syslog.conf:
+        #mail.info                         -/var/log/maillog
+        mail.info;local5.notice            -/var/log/maillog
+
+        local5.info                        -/var/log/amavisd-info.log
+
+.fi
+.PP
+\fBAmavis\fR' typical \fB$log_level\fR 0 messages will be directed to both your maillog
+and to the \fBamavisd-info.log\fR file, but higher \fB$log_level\fR messages
+will only be routed to the \fBamavisd-info.log\fR file.
+For additional information on Amavis' logging, search the
+file \fBRELEASE_NOTES\fR in the Amavis distribution for:
+.nf
+
+    "syslog priorities are now dynamically derived"
+
+.fi
+.SH "ENVIRONMENT"
+.na
+.nf
+.ad
+.fi
+The \fBamavis-logwatch\fR program uses the following (automatically set) environment
+variables when running under Logwatch:
+.IP \fBLOGWATCH_DETAIL_LEVEL\fR
+This is the detail level specified with the Logwatch command line argument \fB--detail\fR
+or the \fBDetail\fR setting in the ...conf/services/amavis.conf configuration file.
+.IP \fBLOGWATCH_DEBUG\fR
+This is the debug level specified with the Logwatch command line argument \fB--debug\fR.
+.IP \fBamavis_\fIxxx\fR
+The Logwatch program passes all settings \fBamavis_\fIxxx\fR in the configuration file ...conf/services/amavis.conf
+to the \fBamavis\fR filter (which is actually named .../scripts/services/amavis) via environment variable.
+.SH "FILES"
+.na
+.nf
+.SS Standalone mode
+.IP "/usr/local/bin/amavis-logwatch"
+The \fBamavis-logwatch\fR program
+.IP "/usr/local/etc/amavis-logwatch.conf"
+The \fBamavis-logwatch\fR configuration file in standalone mode
+.SS Logwatch mode
+.IP "/etc/logwatch/scripts/services/amavis"
+The Logwatch \fBamavis\fR filter
+.IP "/etc/logwatch/conf/services/amavis.conf"
+The Logwatch \fBamavis\fR filter configuration file
+.SH "SEE ALSO"
+.na
+.nf
+logwatch(8), system log analyzer and reporter
+.SH "README FILES"
+.na
+.ad
+.nf
+README, an overview of \fBamavis-logwatch\fR
+Changes, the version change list history
+Bugs, a list of the current bugs or other inadequacies
+Makefile, the rudimentary installer
+LICENSE, the usage and redistribution licensing terms
+.SH "LICENSE"
+.na
+.nf
+.ad
+Covered under the included MIT/X-Consortium License:
+http://www.opensource.org/licenses/mit-license.php
+
+.SH "AUTHOR(S)"
+.na
+.nf
+Mike Cappella
+
+.fi
+The original \fBamavis\fR Logwatch filter was written by
+Jim O'Halloran, and has had many contributors over the years.
+They are entirely not responsible for any errors, problems or failures since the current author's
+hands have touched the source code.
diff --git a/amavis-logwatch.1.html b/amavis-logwatch.1.html
new file mode 100644 (file)
index 0000000..24a0af2
--- /dev/null
@@ -0,0 +1,888 @@
+<!doctype html public "-//W3C//DTD HTML 4.01 Transitional//EN"
+        "http://www.w3.org/TR/html4/loose.dtd">
+<html> <head>
+<meta http-equiv="Content-Type" content="text/html; charset=us-ascii">
+<title> Man page: amavis-logwatch(1) </title>
+</head> <body> <pre>
+AMAVIS-LOGWATCH(1)          General Commands Manual         AMAVIS-LOGWATCH(1)
+
+
+
+<b>NAME</b>
+       amavis-logwatch - An Amavisd-new log parser and analysis utility
+
+<b>SYNOPSIS</b>
+       <b>amavis-logwatch</b> [<i>options</i>] [<i>logfile ...</i>]
+
+<b>DESCRIPTION</b>
+       The  <b>amavis-logwatch</b>(1)  utility is an Amavisd-new log parser that pro-
+       duces summaries, details, and statistics  regarding  the  operation  of
+       Amavisd-new (henceforth, simply called Amavis).
+
+       This utility can be used as a standalone program, or as a Logwatch fil-
+       ter module to produce Amavisd-new summary  and  detailed  reports  from
+       within Logwatch.
+
+       <b>Amavis-logwatch</b>  is  able  to produce a wide range of reports with data
+       grouped and sorted as much as possible to reduce  noise  and  highlight
+       patterns.   Brief  summary  reports provide a quick overview of general
+       Amavis operations and message delivery, calling out warnings  that  may
+       require  attention.   Detailed reports provide easy to scan, hierarchi-
+       cally-arranged and organized information, with as much or little detail
+       as desired.
+
+       Much  of  the  interesting data is available when Amavis' $log_level is
+       set to at least 2.  See <b>Amavis Log Level</b> below.
+
+       <b>Amavis-logwatch</b> outputs two principal sections: a <b>Summary</b> section and a
+       <b>Detailed</b> section.  For readability and quick scanning, all event or hit
+       counts appear in the left column, followed by brief description of  the
+       event  type, and finally additional statistics or count representations
+       may appear in the rightmost column.
+
+       The following segment from a sample Summary report illustrates:
+
+           ****** Summary ********************************************
+
+                  9   Miscellaneous warnings
+
+              20313   Total messages scanned ----------------  100.00%
+           1008.534M  Total bytes scanned                1,057,524,252
+           ========   ================================================
+
+               1190   Blocked -------------------------------    5.86%
+                 18     Malware blocked                          0.09%
+                  4     Banned name blocked                      0.02%
+                416     Spam blocked                             2.05%
+                752     Spam discarded (no quarantine)           3.70%
+
+              19123   Passed --------------------------------   94.14%
+                 47     Bad header passed                        0.23%
+              19076     Clean passed                            93.91%
+           ========   ================================================
+
+                 18   Malware -------------------------------    0.09%
+                 18     Malware blocked                          0.09%
+
+                  4   Banned --------------------------------    0.02%
+                  4     Banned file blocked                      0.02%
+
+               1168   Spam ----------------------------------    5.75%
+                416     Spam blocked                             2.05%
+                752     Spam discarded (no quarantine)           3.70%
+
+              19123   Ham -----------------------------------   94.14%
+                 47     Bad header passed                        0.23%
+              19076     Clean passed                            93.91%
+           ========   ================================================
+
+               1982   SpamAssassin bypassed
+                 32   Released from quarantine
+                  2   DSN notification (debug supplemental)
+                  2   Bounce unverifiable
+               2369   Whitelisted
+                  2   Blacklisted
+                 12   MIME error
+                 58   Bad header (debug supplemental)
+                 40   Extra code modules loaded at runtime
+
+       The report indicates there were 9 general warnings, and <b>Amavis</b>  scanned
+       a  total  of  20313  messages  for  a  total  of  1008.53  megabytes or
+       1,057,524,252 bytes.  The next  summary  groups  shows  the  Blocked  /
+       Passed overview, with 1190 Blocked messages (broken down as 18 messages
+       blocked as malware, 4 messages with banned names,  416  spam  messages,
+       and  752  discarded  messages),  and 19123 Passed messages (47 messages
+       with bad headers and 19076 clean messages).
+
+       The next (optional) summary grouping shows message disposition by  con-
+       tents  category.  There were 18 malware messages and 4 banned file mes-
+       sages (all blocked), 1168 Spam messages,  of  which  416  were  blocked
+       (quarantined)  and  752  discarded.  Finally, there were 19123 messages
+       consdidered to be Ham (i.e. not spam), 47 of which contained bad  head-
+       ers.
+
+       Additional count summaries for a variety of events are also listed.
+
+       There are dozens of sub-sections available in the <b>Detailed</b> report, each
+       of whose output can be controlled in various  ways.   Each  sub-section
+       attempts to group and present the most meaningful data at superior lev-
+       els, while pushing less useful or <i>noisy</i> data towards  inferior  levels.
+       The  goal is to provide as much benefit as possible from smart grouping
+       of data, to allow faster report scanning, pattern  identification,  and
+       problem  solving.   Data is always sorted in descending order by count,
+       and then numerically by IP address or alphabetically as appropriate.
+
+       The following Spam blocked segment from a sample <b>Detailed</b> report illus-
+       trates the basic hierarchical level structure of <b>amavis-logwatch</b>:
+
+           ****** Detailed *******************************************
+
+              19346   Spam blocked -----------------------------------
+                756      from@example.com
+                 12         10.0.0.2
+                 12            &lt;&gt;
+                 12         192.168.2.2
+                 12            &lt;&gt;
+                  5         192.168.2.1
+                ...
+
+
+       The  <b>amavis-logwatch</b>  utility reads from STDIN or from the named Amavis
+       <i>logfile</i>.  Multiple <i>logfile</i> arguments may be specified,  each  processed
+       in  order.   The user running <b>amavis-logwatch</b> must have read permission
+       on each named log file.
+
+   <b>Options</b>
+       The options listed  below  affect  the  operation  of  <b>amavis-logwatch</b>.
+       Options specified later on the command line override earlier ones.  Any
+       option may be abbreviated to an unambiguous length.
+
+
+       <b>--[no]autolearn</b>
+       <b>--show_autolearn</b> <i>boolean</i>
+              Enables (disables) output of the autolearn report.  This  report
+              is only available if the default Amavis <b>$log_templ</b> has been mod-
+              ified to provide autolearn results in log entries.  This can  be
+              done  by  uncommenting  two  lines  in the Amavis program itself
+              (where the default log templates reside), or by correctly adding
+              the  <b>$log_templ</b>  variable to the <b>amavisd.conf</b> file.  See Amavis'
+              <b>README.customize</b> and search near the end of the Amavisd  program
+              for "autolearn".
+
+       <b>--[no]by_ccat_summary</b>
+       <b>--show_by_ccat_summary</b> <i>boolean</i>
+              Enables  (disables) the by contents category summary in the <b>Sum-</b>
+              <b>mary</b> section.  Default: enabled.
+
+       <b>-f</b> <i>config</i><b>_</b><i>file</i>
+       <b>--config_file</b> <i>config</i><b>_</b><i>file</i>
+              Use an alternate configuration file <i>config</i><b>_</b><i>file</i> instead  of  the
+              default.  This option may be used more than once.  Multiple con-
+              figuration files will be processed in the order presented on the
+              command line.  See <b>CONFIGURATION FILE</b> below.
+
+       <b>--debug</b> <i>keywords</i>
+              Output  debug  information  during  the operation of <b>amavis-log-</b>
+              <b>watch</b>.  The parameter <i>keywords</i> is one or  more  comma  or  space
+              separated  keywords.   To obtain the list of valid keywords, use
+              --debug xxx where xxx is any invalid keyword.
+
+       <b>--detail</b> <i>level</i>
+              Sets the maximum detail  level  for  <b>amavis-logwatch</b>  to  <i>level</i>.
+              This  option  is  global,  overriding  any other output limiters
+              described below.
+
+              The  <b>amavis-logwatch</b>  utility  produces  a  <b>Summary</b>  section,  a
+              <b>Detailed</b>  section,  and  additional report sections.  With <i>level</i>
+              less than 5, <b>amavis-logwatch</b> will produce only the <b>Summary</b>  sec-
+              tion.  At <i>level</i> 5 and above, the <b>Detailed</b> section, and any addi-
+              tional report sections are candidates for output.   Each  incre-
+              mental  increase  in <i>level</i> generates one additional hierarchical
+              sub-level of output in the <b>Detailed</b> section of the  report.   At
+              <i>level</i>  10, all levels are output.  Lines that exceed the maximum
+              report width (specified  with  <b>max_report_width</b>)  will  be  cut.
+              Setting  <i>level</i> to 11 will prevent lines in the report from being
+              cut (see also <b>--line_style</b>).
+
+       <b>--[no]first_recip_only</b>
+       <b>--show_first_recip_only</b> <i>boolean</i>
+              Specifies whether or not to sort by, and show,  only  the  first
+              recipient when a scanned messages contains multiple recipients.
+
+       <b>--help</b> Print  usage  information  and a brief description about command
+              line options.
+
+       <b>--ipaddr_width</b> <i>width</i>
+              Specifies that IP addresses in address/hostname pairs should  be
+              printed  with a field width of <i>width</i> characters.  Increasing the
+              default may be useful for systems using long IPv6 addresses.
+
+       <b>-l limiter=levelspec</b>
+       <b>--limit limiter=levelspec</b>
+              Sets the level limiter <i>limiter</i> with the specification <i>levelspec</i>.
+
+       <b>--line_style</b> <i>style</i>
+              Specifies how to handle long report  lines.   Three  styles  are
+              available: <b>full</b>, <b>truncate</b>, and <b>wrap</b>.  Setting <i>style</i> to <b>full</b> will
+              prevent cutting lines to <b>max_report_width</b>; this is  what  occurs
+              when  <b>detail</b>  is  11  or  higher.   When  <i>style</i> is <b>truncate</b> (the
+              default),  long   lines   will   be   truncated   according   to
+              <b>max_report_width</b>.   Setting <i>style</i> to <b>wrap</b> will wrap lines longer
+              than <b>max_report_width</b> such that left column hit counts  are  not
+              obscured.   This  option  takes  precedence  over the line style
+              implied by the <b>detail</b> level.  The  options  <b>--full</b>,  <b>--truncate</b>,
+              and <b>--wrap</b> are synonyms.
+
+
+       <b>--nodetail</b>
+              Disables  the <b>Detailed</b> section of the report, and all supplemen-
+              tal reports.  This option provides  a  convenient  mechanism  to
+              quickly  disable  all  sections under the <b>Detailed</b> report, where
+              subsequent command line options may re-enable one or  more  sec-
+              tions to create specific reports.
+
+       <b>--sarules</b> `<i>S,H</i>'
+       <b>--sarules default</b>
+              Enables  the SpamAssassin Rules Hit report.  The comma-separated
+              <i>S</i> and <i>H</i> arguments are top N values for the Spam and Ham reports,
+              respectively, and can be any integer greater than or equal to 0,
+              or the keyword <b>all</b>.   The  keyword  <b>default</b>  uses  the  built-in
+              default values.
+
+       <b>--nosarules</b>
+              Disables the SpamAssassin Rules Hit report.
+
+       <b>--sa_timings</b> <i>nrows</i>
+              Enables the SpamAssassin Timings percentiles report.  The report
+              can be limited to the top N rows with the <i>nrows</i> argument.   This
+              report requires Amavis 2.6+ and SpamAssassin 3.3+.
+
+       <b>--sa_timings_percentiles</b> `<i>P1 [P2 ...]</i>'
+              Specifies  the  percentiles  shown  in  the SpamAssassin Timings
+              report.  The arguments <i>P1 ...</i> are integers from 0 to 100  inclu-
+              sive.  Their order will be preserved in the report.
+
+       <b>--nosa_timings</b>
+              Disables the SpamAssassin Timings report.
+
+       <b>--version</b>
+              Print <b>amavis-logwatch</b> version information.
+
+       <b>--score_frequencies</b> `<i>B1 [B2 ...]</i>'
+       <b>--score_frequencies default</b>
+              Enables  the  Spam Score Frequency report.  The arguments <i>B1 ...</i>
+              are frequency distribution buckets, and can be any real numbers.
+              Their  order  will  be  preserved  in  the  report.  The keyword
+              <b>default</b> uses the built-in default values.
+
+       <b>--noscore_frequencies</b>
+              Disables the Spam Score Frequency report.
+
+       <b>--score_percentiles</b> `<i>P1 [P2 ...]</i>'
+       <b>--score_percentiles default</b>
+              Enables the Spam Score Percentiles report.  The arguments <i>P1 ...</i>
+              specify  the  percentiles  shown in the report, and are integers
+              from 0 to 100 inclusive.  The keyword <b>default</b> uses the  built-in
+              default values.
+
+       <b>--noscore_percentiles</b>
+              Disables the Spam Score Percentiles report.
+
+
+       <b>--[no]sect_vars</b>
+       <b>--show_sect_vars</b> <i>boolean</i>
+              Enables  (disables)  supplementing  each  <b>Detailed</b> section title
+              with the name of that section's level limiter.   The  name  dis-
+              played  is  the command line option (or configuration file vari-
+              able) used to limit that section's output.  With the large  num-
+              ber  of level limiters available in <b>amavis-logwatch</b>, this a con-
+              venient mechanism for determining exactly  which  level  limiter
+              affects a section.
+
+       <b>--[no]startinfo</b>
+       <b>--show_startinfo</b> <i>boolean</i>
+              Enables (disables) the Amavis startup report showing most recent
+              Amavis startup details.
+
+       <b>--[no]summary</b>
+
+       <b>--show_summary</b>
+              Enables (disables) displaying of the the <b>Summary</b> section of  the
+              report.   The variable Amavis_Show_Summary in used in a configu-
+              ration file.
+
+       <b>--syslog_name</b> <i>namepat</i>
+              Specifies the syslog service name that <b>amavis-logwatch</b>  uses  to
+              match  syslog  lines.  Only log lines whose service name matches
+              the perl regular expression <i>namepat</i> will be used by  <b>amavis-log-</b>
+              <b>watch</b>;  all  non-matching  lines  are silently ignored.  This is
+              useful when a pre-installed Amavis package  uses  a  name  other
+              than the default (<b>amavis</b>).
+
+              <b>Note:</b> if you use parenthesis in your regular expression, be sure
+              they are cloistering and not capturing: use  <b>(?:</b><i>pattern</i><b>)</b> instead
+              of <b>(</b><i>pattern</i><b>)</b>.
+
+       <b>--timings</b> <i>percent</i>
+              Enables  the Amavis Scan Timings percentiles report.  The report
+              can be top N-percent limited with the <i>percent</i> argument.
+
+       <b>--timings_percentiles</b> `<i>P1 [P2 ...]</i>'
+              Specifies the percentiles shown in the Scan Timings report.  The
+              arguments  <i>P1  ...</i>  are integers from 0 to 100 inclusive.  Their
+              order will be preserved in the report.
+
+       <b>--notimings</b>
+              Disables the Amavis Scan Timings report.
+
+       <b>--version</b>
+              Print <b>amavis-logwatch</b> version information.
+
+
+   <b>Level Limiters</b>
+       The output of every section in the <b>Detailed</b> report is controlled  by  a
+       level  limiter.   The name of the level limiter variable will be output
+       when the <b>sect_vars</b> option is set.  Level limiters are  set  either  via
+       command  line in standalone mode with <b>--limit</b> <i>limiter</i><b>=</b><i>levelspec</i> option,
+       or via configuration  file  variable  <b>$amavis_</b><i>limiter</i><b>=</b><i>levelspec</i>.   Each
+       limiter  requires  a  <i>levelspec</i>  argument,  which is described below in
+       <b>LEVEL CONTROL</b>.
+
+       The list of level limiters is shown below.
+
+
+       Amavis major contents category (ccatmajor) sections, listed in order of
+       priority: VIRUS, BANNED, UNCHECKED, SPAM, SPAMMY, BADH, OVERSIZED, MTA,
+       CLEAN.
+
+       <b>MalwareBlocked</b>
+       <b>MalwarePassed</b>
+              Blocked or passed  messages  that  contain  malware  (ccatmajor:
+              VIRUS).
+
+       <b>BannedNameBlocked</b>
+       <b>BannedNamePassed</b>
+              Blocked  or  passed  messages  that contain banned names in MIME
+              parts (ccatmajor: BANNED).
+
+       <b>UncheckedBlocked</b>
+       <b>UncheckedPassed</b>
+              Blocked or passed messages that were  not  checked  by  a  virus
+              scanner or SpamAssassin (Amavis ccatmajor: UNCHECKED).
+
+       <b>SpamBlocked</b>
+       <b>SpamPassed</b>
+              Blocked  or  passed  messages  that  were  considered  spam that
+              reached kill level (Amavis ccatmajor: SPAM)
+
+       <b>SpammyBlocked</b>
+       <b>SpammyPassed</b>
+              Blocked or passed messages that were considered  spam,  but  did
+              not reach kill level (Amavis ccatmajor: SPAMMY)
+
+       <b>BadHeaderBlocked</b>
+       <b>BadHeaderPassed</b>
+              Blocked  or passed messages that contain bad mail headers (ccat-
+              major: BAD-HEADER).
+
+       <b>OversizedBlocked</b>
+       <b>OversizedPassed</b>
+              Blocked  or  passed  messages  that  were  considered  oversized
+              (Amavis ccatmajor: OVERSIZED).
+
+       <b>MtaBlocked</b>
+       <b>MtaPassed</b>
+              Blocked  or  passed  messages due to failure to re-inject to MTA
+              (Amavis ccatmajor:  MTA-BLOCKED).   Occurrences  of  this  event
+              indicates a configuration problem.  [ note: I don't believe mta-
+              passed occurs, but exists for completeness.]
+
+       <b>OtherBlocked</b>
+       <b>OtherPassed</b>
+              Blocked or passed messages that are not any of other major  con-
+              tents categories (Amavis ccatmajor: OTHER).
+
+
+       <b>TempFailBlocked</b>
+       <b>TempfailPassed</b>
+              Blocked  or passed messages that had a temporary failure (Amavis
+              ccatmajor: TEMPFAIL)
+
+       <b>CleanBlocked</b>
+       <b>CleanPassed</b>
+              Messages blocked or passed which were considered  clean  (Amavis
+              ccatmajor: CLEAN; i.e. non-spam, non-viral).
+
+       Other sections, arranged alphabetically:
+
+       <b>AvConnectFailure</b>
+              Problems connecting to Anti-Virus scanner(s).
+
+       <b>AvTimeout</b>
+              Timeouts awaiting responses from Anti-Virus scanner(s).
+
+       <b>ArchiveExtract</b>
+              Archive extraction problems.
+
+       <b>BadHeaderSupp</b>
+              Supplemental debug information regarding messages containing bad
+              mail headers.
+
+       <b>Bayes</b>  Messages frequencies by Bayesian probability buckets.
+
+       <b>BadAddress</b>
+              Invalid mail address syntax.
+
+       <b>Blacklisted</b>
+              Messages that were  (soft-)blacklisted.   See  also  Whitelisted
+              below.
+
+       <b>BounceKilled</b>
+       <b>BounceRescued</b>
+       <b>BounceUnverifiable</b>
+              Disposition of incoming bounce messages (DSNs).
+
+       <b>ContentType</b>
+              MIME attachment breakdown by type/subtype.
+
+       <b>DccError</b>
+              Errors encountered with or returned by DCC.
+
+       <b>DefangError</b>
+              Errors encountered during defang process.
+
+       <b>Defanged</b>
+              Messages defanged (rendered harmless).
+
+       <b>DsnNotification</b>
+              Errors  encountered during attempt to send delivery status noti-
+              fication.
+
+       <b>DsnSuppressed</b>
+              Delivery status notification (DSN) intentionally suppressed.
+
+       <b>ExtraModules</b>
+              Additional code modules Amavis loaded during runtime.
+
+       <b>FakeSender</b>
+              Forged sender addresses, as determimed by Amavis.
+
+       <b>Fatal</b>  Fatal events.  These are presented at the top of the report,  as
+              they may require attention.
+
+       <b>LocalDeliverySkipped</b>
+              Failures delivering to a local address.
+
+       <b>MalwareByScanner</b>
+              Breakdown of malware by scanner(s) that detected the malware.
+
+       <b>MimeError</b>
+              Errors encountered during MIME extraction.
+
+       <b>Panic</b>  Panic  events.  These are presented at the top of the report, as
+              they may require attention.
+
+       <b>p0f</b>    Passive fingerprint (p0f) hits, grouped by  mail  contents  type
+              (virus,  unchecked, banned, spam, ham), next by operating system
+              genre, and finally by IP address.   Note:  Windows  systems  are
+              refined by Windows OS version, whereas versions of other operat-
+              ing systems are grouped generically.
+
+       <b>Released</b>
+              Messages that were released from Amavis quarantine.
+
+       <b>SADiags</b>
+              Diagnostics as reported from SpamAssassin.
+
+       <b>SmtpResponse</b>
+              SMTP responses received  during  dialog  with  MTA.   These  log
+              entries are primarly debug.
+
+       <b>TmpPreserved</b>
+              Temporary  directories  preserved  by Amavis when some component
+              encounters a problem or failure.  Directories listed  and  their
+              corresponding log entries should be evaluated for problems.
+
+       <b>VirusScanSkipped</b>
+              Messages that could not be scanned by a virus scanner.
+
+       <b>Warning</b>
+              Warning  events  not  categorized  in  specific  warnings below.
+              These are presented at the  top  of  the  report,  as  they  may
+              require attention.
+
+       <b>WarningAddressModified</b>
+              Incomplete email addresses modified by Amavis for safety.
+
+       <b>WarningNoQuarantineId</b>
+              Attempts  to  release a quarantined message that did not contain
+              an X-Quarantine-ID header.
+
+       <b>WarningSecurity</b> <i>levelspec</i>
+              Insecure configuration or utility used by Amavis.
+
+       <b>WarningSmtpShutdown</b>
+              Failures during SMTP conversation with MTA.
+
+       <b>WarningSql</b>
+              Failures to communicate with, or error replies  from,  SQL  ser-
+              vice.
+
+       <b>Whitelisted</b>
+              Messages  that  were  (soft-)whitelisted.   See also Blacklisted
+              above.
+
+
+<b>LEVEL CONTROL</b>
+       The <b>Detailed</b> section of the report consists of  a  number  of  sub-sec-
+       tions,  each  of  which  is controlled both globally and independently.
+       Two settings influence the output provided in the  <b>Detailed</b>  report:  a
+       global detail level (specified with <b>--detail</b>) which has final (big ham-
+       mer) output-limiting control over the <b>Detailed</b> section, and sub-section
+       specific  detail  settings (small hammer), which allow further limiting
+       of the output for a sub-section.  Each sub-section may be limited to  a
+       specific  depth  level, and each sub-level may be limited with top N or
+       threshold limits.  The <i>levelspec</i> argument to each of the level limiters
+       listed above is used to accomplish this.
+
+       It  is probably best to continue explanation of sub-level limiting with
+       the following well-known outline-style hierarchy, and some basic  exam-
+       ples:
+
+           level 0
+              level 1
+                 level 2
+                    level 3
+                       level 4
+                       level 4
+                 level 2
+                    level 3
+                       level 4
+                       level 4
+                       level 4
+                    level 3
+                       level 4
+                    level 3
+              level 1
+                 level 2
+                    level 3
+                       level 4
+
+       The  simplest  form  of  output  limiting suppresses all output below a
+       specified level.  For example, a <i>levelspec</i> set to "2" shows  only  data
+       in  levels  0  through 2.  Think of this as collapsing each sub-level 2
+       item, thus hiding all inferior levels (3, 4, ...), to yield:
+
+           level 0
+              level 1
+                 level 2
+                 level 2
+              level 1
+                 level 2
+
+       Sometimes the volume of output in a section is too  great,  and  it  is
+       useful  to  suppress  any data that does not exceed a certain threshold
+       value.  Consider a dictionary spam attack, which produces very  lengthy
+       lists  of  hit-once recipient email or IP addresses.  Each sub-level in
+       the hierarchy can be threshold-limited by setting the <i>levelspec</i>  appro-
+       priately.  Setting <i>levelspec</i> to the value "2::5" will suppress any data
+       at level 2 that does not exceed a hit count of 5.
+
+       Perhaps producing a top N list, such as top 10 senders, is desired.   A
+       <i>levelspec</i> of "3:10:" limits level 3 data to only the top 10 hits.
+
+       With  those simple examples out of the way, a <i>levelspec</i> is defined as a
+       whitespace- or comma-separated list of one or more of the following:
+
+       <i>l</i>      Specifies the maximum level to be output for  this  sub-section,
+              with a range from 0 to 10.  if <i>l</i> is 0, no levels will be output,
+              effectively disabling the sub-section (level 0 data  is  already
+              provided  in  the  Summary  report, so level 1 is considered the
+              first useful level in the <b>Detailed</b> report).  Higher values  will
+              produce output up to and including the specified level.
+
+       <i>l</i><b>.</b><i>n</i>    Same  as  above,  with the addition that <i>n</i> limits this section's
+              level 1 output to the top <i>n</i> items.  The value for <i>n</i> can  be  any
+              integer greater than 1.  (This form of limiting has less utility
+              than the syntax shown below. It is provided for  backwards  com-
+              patibility; users are encouraged to use the syntax below).
+
+       <i>l</i><b>:</b><i>n</i><b>:</b><i>t</i>  This  triplet specifies level <i>l</i>, top <i>n</i>, and minimum threshold <i>t</i>.
+              Each of the values are integers, with <i>l</i> being the level  limiter
+              as described above, <i>n</i> being a top <i>n</i> limiter for the level <i>l</i>, and
+              <i>t</i> being the threshold limiter for level <i>l</i>.  When both  <i>n</i>  and  <i>t</i>
+              are  specified, <i>n</i> has priority, allowing top <i>n</i> lists (regardless
+              of threshold value).  If the value of <i>l</i> is omitted,  the  speci-
+              fied  values for <i>n</i> and/or <i>t</i> are used for all levels available in
+              the sub-section.  This permits a simple form of wildcarding (eg.
+              place  minimum  threshold  limits on all levels).  However, spe-
+              cific limiters always override  wildcard  limiters.   The  first
+              form  of  level limiter may be included in <i>levelspec</i> to restrict
+              output, regardless of how many triplets are present.
+
+       All three forms of limiters are effective only  when  <b>amavis-logwatch</b>'s
+       detail  level  is  5  or greater (the <b>Detailed</b> section is not activated
+       until detail is at least 5).
+
+       See the <b>EXAMPLES</b> section for usage scenarios.
+
+<b>CONFIGURATION FILE</b>
+       <b>Amavis-logwatch</b> can read configuration settings  from  a  configuration
+       file.   Essentially,  any command line option can be placed into a con-
+       figuration file, and these settings are read upon startup.
+
+       Because <b>amavis-logwatch</b> can run either standalone or  within  Logwatch,
+       to  minimize  confusion, <b>amavis-logwatch</b> inherits Logwatch's configura-
+       tion file syntax requirements and conventions.  These are:
+
+       <b>o</b>   White space lines are ignored.
+
+       <b>o</b>   Lines beginning with <b>#</b> are ignored
+
+       <b>o</b>   Settings are of the form:
+
+                   <i>option</i> <b>=</b> <i>value</i>
+
+
+       <b>o</b>   Spaces or tabs on either side of the <b>=</b> character are ignored.
+
+       <b>o</b>   Any <i>value</i> protected in double quotes will be case-preserved.
+
+       <b>o</b>   All other content is reduced  to  lowercase  (non-preserving,  case
+           insensitive).
+
+       <b>o</b>   All  <b>amavis-logwatch</b>  configuration  settings must be prefixed with
+           "<b>$amavis_</b>" or <b>amavis-logwatch</b> will ignore them.
+
+       <b>o</b>   When  running  under  Logwatch,  any  values  not   prefixed   with
+           "<b>$amavis_</b>"  are consumed by Logwatch; it only passes to <b>amavis-log-</b>
+           <b>watch</b> (via environment variable) settings it considers valid.
+
+       <b>o</b>   The values <b>True</b> and <b>Yes</b> are converted to 1, and <b>False</b>  and  <b>No</b>  are
+           converted to 0.
+
+       <b>o</b>   Order  of  settings  is  not  preserved within a configuration file
+           (since settings are passed by Logwatch via  environment  variables,
+           which have no defined order).
+
+       To  include  a  command line option in a configuration file, prefix the
+       command line option name with the word "<b>$amavis_</b>".  The following  con-
+       figuration file setting and command line option are equivalent:
+
+               <b>$amavis_Line_Style = Truncate</b>
+
+               <b>--line_style Truncate</b>
+
+       Level limiters are also prefixed with <b>$amavis_</b>, but on the command line
+       are specified with the <b>--limit</b> option:
+
+               <b>$amavis_SpamBlocked = 2</b>
+
+               <b>--limit SpamBlocked=2</b>
+
+
+
+       The order of command line options  and  configuration  file  processing
+       occurs  as  follows:  1)  The  default configuration file is read if it
+       exists and no <b>--config_file</b> was specified on a command line.   2)  Con-
+       figuration  files are read and processed in the order found on the com-
+       mand line.  3) Command line options override any  options  already  set
+       either via command line or from any configuration file.
+
+       Command  line options are interpreted when they are seen on the command
+       line, and later options will override previously set options.
+
+
+
+<b>EXIT STATUS</b>
+       The <b>amavis-logwatch</b> utility exits with a status code of  0,  unless  an
+       error occurred, in which case a non-zero exit status is returned.
+
+<b>EXAMPLES</b>
+   <b>Running Standalone</b>
+       <b>Note:  amavis-logwatch</b> reads its log data from one or more named Amavis
+       log files, or from STDIN.  For brevity, where  required,  the  examples
+       below   use  the  word  <i>file</i>  as  the  command  line  argument  meaning
+       <i>/path/to/amavis.log</i>.  Obviously you will need to substitute  <i>file</i>  with
+       the appropriate path.
+
+       To run <b>amavis-logwatch</b> in standalone mode, simply run:
+
+           <b>amavis-logwatch</b> <i>file</i>
+
+       A complete list of options and basic usage is available via:
+
+           <b>amavis-logwatch --help</b>
+
+       To print a summary only report of Amavis log data:
+
+           <b>amavis-logwatch --detail 1</b> <i>file</i>
+
+       To produce a summary report and a one-level detail report for May 25th:
+
+           <b>grep 'May 25'</b> <i>file</i> <b>| amavis-logwatch --detail 5</b>
+
+       To produce only a top 10 list of Sent email domains, the summary report
+       and detailed reports are first disabled. Since  commands  line  options
+       are  read  and enabled left-to-right, the Sent section is re-enabled to
+       level 1 with a level 1 top 10 limiter:
+
+           <b>amavis-logwatch --nosummary --nodetail \</b>
+              <b>--limit spamblocked '1 1:10:'</b> <i>file</i>
+
+       The following command and its sample output shows a more complex  level
+       limiter  example.   The command gives the top 4 spam blocked recipients
+       (level 1), and under with each recipient the top 2 sending  IPs  (level
+       2)  and finally below that, only envelope from addresses (level 3) with
+       hit counts greater than 6.  Ellipses indicate top N  or  threshold-lim-
+       ited data:
+
+           <b>amavis-logwatch --nosummary --nodetail \</b>
+                   <b>--limit spamblocked '1:4: 2:2: 3::6'</b> <i>file</i>
+
+           19346   Spam blocked -----------------------------------
+             756      joe@example.com
+              12         10.0.0.1
+              12            &lt;&gt;
+              12         10.99.99.99
+              12            &lt;&gt;
+                     ...
+             640      fred@example.com
+               8         10.0.0.1
+               8            &lt;&gt;
+               8         192.168.3.19
+               8            &lt;&gt;
+                     ...
+             595      peter@sample.net
+               8         10.0.0.1
+               8            &lt;&gt;
+               7         192.168.3.3
+               7            &lt;&gt;
+                     ...
+             547      paul@example.us
+               8         192.168.3.19
+               8            &lt;&gt;
+               7         10.0.0.1
+               7            &lt;&gt;
+                      ...
+                   ...
+
+   <b>Running within Logwatch</b>
+       <b>Note:</b>  Logwatch  versions  prior to 7.3.6, unless configured otherwise,
+       required the <b>--print</b> option to  print  to  STDOUT  instead  of  sending
+       reports  via  email.  Since version 7.3.6, STDOUT is the default output
+       destination, and the <b>--print</b> option has been replaced by <b>--output  std-</b>
+       <b>out</b>.  Check your configuration to determine where report output will be
+       directed, and add the appropriate option to the commands below.
+
+       To print a summary report for today's Amavis log data:
+
+           <b>logwatch --service amavis --range today --detail 1</b>
+
+       To print a report for today's Amavis log data, with one level
+       of detail in the <b>Detailed</b> section:
+
+           <b>logwatch --service amavis --range today --detail 5</b>
+
+       To print a report for yesterday, with  two  levels  of  detail  in  the
+       <b>Detailed</b> section:
+
+           <b>logwatch --service amavis --range yesterday --detail 6</b>
+
+       To  print  a report from Dec 12th through Dec 14th, with four levels of
+       detail in the <b>Detailed</b> section:
+
+           <b>logwatch --service amavis --range \</b>
+                   <b>'between 12/12 and 12/14' --detail 8</b>
+
+       To print a report for today, with all levels of detail:
+
+           <b>logwatch --service amavis --range today --detail 10</b>
+
+       Same as above, but leaves long lines uncropped:
+
+           <b>logwatch --service amavis --range today --detail 11</b>
+
+   <b>Amavis Log Level</b>
+       Amavis provides additional log information when the variable <b>$log_level</b>
+       is  increased  above  the default 0 value.  This information is used by
+       the <b>amavis-logwatch</b> utility to provide additional reports,  not  avail-
+       able  with  the  default <b>$log_level</b>=0 value.  A <b>$log_level</b> of 2 is sug-
+       gested.
+
+       If you prefer not to increase the noise level  in  your  main  mail  or
+       Amavis logs, you can configure syslog to log Amavis' output to multiple
+       log files, where basic log entries are routed to your main mail  log(s)
+       and more detailed entries routed to an Amavis-specific log file used to
+       feed the <b>amavis-logwatch</b> utility.
+
+       A convenient way to accomplish this is to change the Amavis  configura-
+       tion variables in <b>amavisd.conf</b> as shown below:
+
+           amavisd.conf:
+               $log_level = 2;
+               $syslog_facility = 'local5';
+               $syslog_priority = 'debug';
+
+
+       This  increases  <b>$log_level</b>  to  2, and sends Amavis' log entries to an
+       alternate syslog facility (eg. <b>local5</b>, user), which can then be  routed
+       to one or more log files, including your main mail log file:
+
+           syslog.conf:
+               #mail.info                         -/var/log/maillog
+               mail.info;local5.notice            -/var/log/maillog
+
+               local5.info                        -/var/log/amavisd-info.log
+
+
+       <b>Amavis</b>'  typical  <b>$log_level</b>  0  messages will be directed to both your
+       maillog and to the <b>amavisd-info.log</b> file, but  higher  <b>$log_level</b>  mes-
+       sages will only be routed to the <b>amavisd-info.log</b> file.  For additional
+       information on Amavis' logging, search the file  <b>RELEASE_NOTES</b>  in  the
+       Amavis distribution for:
+
+           "syslog priorities are now dynamically derived"
+
+
+<b>ENVIRONMENT</b>
+       The  <b>amavis-logwatch</b>  program  uses  the  following (automatically set)
+       environment variables when running under Logwatch:
+
+       <b>LOGWATCH_DETAIL_LEVEL</b>
+              This is the detail level specified  with  the  Logwatch  command
+              line argument <b>--detail</b> or the <b>Detail</b> setting in the ...conf/ser-
+              vices/amavis.conf configuration file.
+
+       <b>LOGWATCH_DEBUG</b>
+              This is the debug level specified with the Logwatch command line
+              argument <b>--debug</b>.
+
+       <b>amavis_</b><i>xxx</i>
+              The  Logwatch program passes all settings <b>amavis_</b><i>xxx</i> in the con-
+              figuration file ...conf/services/amavis.conf to the <b>amavis</b>  fil-
+              ter  (which  is  actually named .../scripts/services/amavis) via
+              environment variable.
+
+<b>FILES</b>
+   <b>Standalone mode</b>
+       /usr/local/bin/amavis-logwatch
+              The <b>amavis-logwatch</b> program
+
+       /usr/local/etc/amavis-logwatch.conf
+              The <b>amavis-logwatch</b> configuration file in standalone mode
+
+   <b>Logwatch mode</b>
+       /etc/logwatch/scripts/services/amavis
+              The Logwatch <b>amavis</b> filter
+
+       /etc/logwatch/conf/services/amavis.conf
+              The Logwatch <b>amavis</b> filter configuration file
+
+<b>SEE ALSO</b>
+       logwatch(8), system log analyzer and reporter
+
+<b>README FILES</b>
+       README, an overview of <b>amavis-logwatch</b>
+       Changes, the version change list history
+       Bugs, a list of the current bugs or other inadequacies
+       Makefile, the rudimentary installer
+       LICENSE, the usage and redistribution licensing terms
+
+<b>LICENSE</b>
+       Covered under the included MIT/X-Consortium License:
+       http://www.opensource.org/licenses/mit-license.php
+
+
+<b>AUTHOR(S)</b>
+       Mike Cappella
+
+       The original <b>amavis</b> Logwatch filter was written by Jim O'Halloran, and
+       has had many contributors over the years.  They are entirely not
+       responsible for any errors, problems or failures since the current
+       author's hands have touched the source code.
+
+
+
+                                                            AMAVIS-LOGWATCH(1)
+</pre> </body> </html>
diff --git a/amavis-logwatch.conf b/amavis-logwatch.conf
new file mode 100644 (file)
index 0000000..2166461
--- /dev/null
@@ -0,0 +1,224 @@
+
+###########################################################################
+# $Id: amavis.conf,v 1.10 2007/05/16 04:57:16 mrc Exp $
+###########################################################################
+
+# You can put comments anywhere you want to.  They are effective for the
+# rest of the line.
+
+# this is in the format of <name> = <value>.  Whitespace at the beginning
+# and end of the lines is removed.  Whitespace before and after the = sign
+# is removed.  Everything is case *insensitive*.
+
+# Yes = True  = On  = 1
+# No  = False = Off = 0
+
+Title = "Amavisd-new"
+
+# Which logfile group...
+LogFile = maillog
+
+# Specifies the global maximum detail level
+#
+#Detail = 10
+
+# Only give lines pertaining to the amavis service...
+#
+# The variables OnlyService and amavis_Syslog_Name are regular
+# expression patterns, and should both be set to match the
+# amavis service name used in your syslog entries.
+#*OnlyService = (usr/sbin/amavisd|dccproc)
+*OnlyService = (amavis|dccproc)
+*RemoveHeaders
+
+# Set this to the service name used in amavis syslog entries
+#
+# Note: if you use parenthesis in your regular expression, be sure it
+# is cloistering and not capturing: use (?:pattern) instead of (pattern).
+# $amavis_Syslog_Name = "/usr/sbin/amavisd"
+$amavis_Syslog_Name = "(?:amavis|dccproc)"
+
+#
+# Set the variable below to specify the maximum report width.
+# for Detail <= 10
+#
+$amavis_Max_Report_Width = 100
+
+# Specifies how to handle line lengths greater than Max_Report_Width.
+# Options are Truncate (default), Wrap, or Full.
+# for Detail <= 10
+#
+$amavis_Line_Style = Truncate
+
+# Show names of detail section variables/command line options in 
+# detail report titles.  For command line, use --[no]sect_vars,
+# without an argument.
+# 
+$amavis_Show_Sect_Vars = No
+
+# In reports, for tallying purposes, use (and show) only the first
+# recipient when a message contains multple recipients.
+# 
+$amavis_Show_First_Recip_Only = No
+
+# Include a by-contents category grouping in the summary report.
+# 
+$amavis_Show_By_Ccat_Summary = Yes
+
+# Amavis Timings Report
+#
+# Specifies the percentiles of collected data to show in the timing report.
+# Valid values are from 0 to 100, inclusive.
+#
+$amavis_Timings_Percentiles = "0 5 25 50 75 95 100"
+
+# 
+# Show top N percent of the amavis scan timings report
+# 
+$amavis_Timings = 95
+
+# Amavis SpamAssassin Timings Report
+#
+# Specifies the percentiles of collected data to show in the
+# SpamAssassin timing report (requires amavis 2.6+, SA 3.3+).
+# Valid values are from 0 to 100, inclusive.
+#
+$amavis_SA_Timings_Percentiles = "0 5 25 50 75 95 100"
+
+# 
+# Show top N rows of the SpamAssassin timings report
+# Requires: amavis 2.6+, SpamAssassin 3.3+ 
+# 
+$amavis_SA_Timings = 100
+
+# Spam Score Percentiles Report
+#
+# Specifies the percentiles shown in the spam scores frequency report. 
+# Valid values range from 0 to 100, inclusive.  The keyword "default"
+# can be used instead to reset the values to their built-in default.
+# 
+$amavis_Score_Percentiles = "0 50 90 95 98 100"
+
+# Spam Score Frequency Report
+#
+# Specifies the buckets shown in the spam scores frequency  report. 
+# Valid values are real numbers.  The keyword "default"
+# can be used instead to reset the values to their built-in default.
+# 
+$amavis_Score_Frequencies = "-10 -5 0 5 10 20 30"
+
+# SpamAssassin Spam / Ham Rules Hit Report
+# 
+# Specifies the number of top S spam and top H ham hits to show in the
+# SpamAssassin Spam and Ham rules hit report.  The value is a list
+# separated by whitespace or a comma.  The order is "spam,ham". The
+# keyword "all" means unlimited limit, and 0 specifies none.  For
+# example, the value "all,10" would show all Spam rules hit, but only
+# the top 10 ham rules, whereas "0,all" would prevent the Spam hit
+# report, and show all the hit Ham rules.
+#
+$amavis_SARules  = "20 20"
+
+# Autolearn Report
+#
+# Shows the autolearn report when autolearn entries are present in
+# amavis log entries.  To make these available, the default
+# $log_templ variable needs to be modified.  This can be done by 
+# uncommenting two lines in the amavis program itself (where the
+# default log templates reside), by correctly adding the $log_templ
+# variable to the amavisd.conf file.  See amavis' README.customize
+# and the end of the amavisd program, searching for "autolearn".
+$amavis_Show_Autolearn = Yes
+
+# If available, show most recent amavis startup details
+# 
+$amavis_Show_StartInfo = Yes
+
+# Show the summary section.  For command line, use --[no]summary,
+# without an argument.
+$amavis_Show_Summary = Yes
+
+# Level Limiters
+#
+# The variables below control the maximum output level for a given
+# category.  A level of 1 indicates only one level of detailed output
+# in the Detailed report section.  The Summary section is only avail-
+# able at logwatch --Detail level >= 5.  Increasing the Detail level
+# by one adds one level of additional detail in the Summary section.
+#
+# For example, Detail 5 would output one additional level of detail,
+# Detail 6 two levels, etc. all the way up to 10.  Finally, Detail
+# 11 yields uncropped lines of output.
+#
+# You can control the maximum number of level 1 lines by appending
+# a period and a number. The value 2.10 would indicate 2 levels
+# of detail, but only 10 level 1 lines.  For example, setting
+# $amavis_SpamBlocked = 1.20 yields a top 20 list of blocked spam.
+#
+# A more  useful form of limiting uses triplets in the form l:n:t.
+# This  triplet specifies level l, top n, and minimum threshold t.
+# Each of the values are integers, with l being the level  limiter
+# as described above, n being a top n limiter for the level l, and
+# t being the threshold limiter for level l.  When both  n  and  t
+# are  specified, n has priority, allowing top n lists (regardless
+# of threshold value).  If the value of l is omitted,  the  speci-
+# fied  values for n and/or t are used for all levels available in
+# the sub-section.  This permits a simple form of wildcarding (eg.
+# place  minimum  threshold  limits on all levels).  However, spe-
+# cific limiters always override  wildcard  limiters.   The  first
+# form  of  level limiter may be included in levelspec to restrict
+# output, regardless of how many triplets are present.
+
+$amavis_CleanPassed = 0
+$amavis_CleanBlocked = 10
+$amavis_SpamPassed = 10
+$amavis_SpamBlocked = 10
+$amavis_SpammyPassed = 10
+$amavis_SpammyBlocked = 10
+$amavis_MalwarePassed = 10
+$amavis_MalwareBlocked = 10
+$amavis_BannedNamePassed = 10
+$amavis_BannedNameBlocked = 10
+$amavis_BadHeaderPassed = 10
+$amavis_BadHeaderBlocked = 10
+$amavis_MTABlocked = 10
+$amavis_OversizedBlocked = 10
+$amavis_OtherBlocked = 10
+
+$amavis_AVConnectFailure = 10
+$amavis_AVTimeout = 10
+$amavis_ArchiveExtract = 10
+$amavis_BadHeaderSupp = 10
+$amavis_Bayes = 10
+$amavis_Blacklisted = 10
+$amavis_BounceKilled = 10
+$amavis_BounceRescued = 10
+$amavis_BounceUnverifiable = 10
+$amavis_ContentType = 10
+$amavis_DccError = 10
+$amavis_DefangError = 10
+$amavis_Defanged = 10
+$amavis_DsnNotification = 10
+$amavis_DsnSuppressed = 10
+$amavis_ExtraModules = 10
+$amavis_FakeSender = 10
+$amavis_LocalDeliverySkipped = 10
+$amavis_MalwareByScanner = 10
+$amavis_MalwareToSpam = 10
+$amavis_MimeError = 10
+$amavis_p0f = 2
+$amavis_Released = 10
+$amavis_SADiags = 10
+$amavis_SmtpResponse = 10
+$amavis_TmpPreserved = 10
+$amavis_VirusScanSkipped = 1
+$amavis_Warning = 10
+$amavis_WarningAddressModified = 2
+$amavis_WarningNoQuarantineID = 1
+$amavis_WarningSecurity = 10
+$amavis_WarningSmtpShutdown = 10
+$amavis_WarningSQL = 10
+$amavis_Whitelisted = 10
+
+
+# vi: shiftwidth=3 tabstop=3 et