summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJulien Valroff <julien@kirya.net>2010-01-04 19:39:20 +0100
committerJulien Valroff <julien@kirya.net>2010-01-04 19:39:20 +0100
commitd906efbb1aacfebd9931d49028f6240a667bae14 (patch)
tree937aad588d7076854da1f31f779c8e7ab99557d8
Imported Upstream version 1.14upstream/1.14
-rw-r--r--CHANGES251
-rw-r--r--COPYING339
-rw-r--r--README85
-rw-r--r--mailgraph-init42
-rwxr-xr-xmailgraph.cgi244
-rw-r--r--mailgraph.css21
-rwxr-xr-xmailgraph.pl976
7 files changed, 1958 insertions, 0 deletions
diff --git a/CHANGES b/CHANGES
new file mode 100644
index 0000000..57b4ddd
--- /dev/null
+++ b/CHANGES
@@ -0,0 +1,251 @@
+1.14 (2007-08-29)
+-----------------
+* add external css file: mailgraph.css (Patrick Nagel)
+ -> you will need to add this file to where mailgraph.cgi is
+* add support for exim (Nicola Worthington)
+* add support for SpamAssassin milter (Todd A. Green)
+* update support for amavis-milter (Joachim de Groot)
+* update support for amavisd-new (Pawel Madej)
+* update support for spamproxyd (Thomas Vander Stichele)
+* --ignore-host can now be specified multiple times
+
+1.13 (2007-03-28)
+-----------------
+* New mailgraph homepage URL: http://mailgraph.schweikert.ch
+* XHTML 1.0 strict output (Yllar Pajus)
+* add releative jump points (Hugo van der Kooij)
+* add commented-out code for clamassassin (Adrian von Bidder)
+* add support for Amavisd-new >= 2.4 (Christoph Kessinger)
+* add support for Borderware Mxtreme (Postfix variant, Johan Nilsson)
+* add support for the ClamAV SpamAssassin plugin (Thomas Brown)
+* add support for MxTreme mail gateway (Guido)
+* update support for Kaspersky AntiVirus (Igor Moskovko)
+* update support for AntiVir (Wolfram Schlich)
+* update support for drweb (Lev)
+* update support for MailScanner (Simon Hartl, Pierre-Yves Bonnetain)
+* update support for AntiVir (Frank Urban)
+* fix DST-switch timewarp in autumn (Parse::Syslog 1.09)
+* fix hidden rejected line behind area items (Axel Beckert)
+* fix virbl DNS name (Eddy Beliveau)
+
+1.12 (2005-10-16)
+-----------------
+* sendmail fixes (Ben Lentz, R. Scott Baer)
+* compatibility with rrdtool 1.2.0 and 1.2.1 (no --slope-mode)
+* compatibility with SpamAssassin 3.1.0 (Matias Lopez Bergero)
+* add support for newer Vexira versions (Alexandru Suchici)
+* sendmail fixes (Immo Goltz)
+* ensure that the correct RRA is always chosen
+* nice -19 for mailgraph.pl in example init script (Alexander Norman)
+
+1.11 (2005-06-05)
+-----------------
+* add support for clamsmtpd (Aaron Wolfe)
+* add support for AVMilter (Stephan A. Klein)
+* add chkconfig to init script (DanielC)
+* add support for bogofilter (Erwan David)
+* add support for Kaspersky anti SPAM (Igor Moskovko)
+* add --virbl-is-spam (Cyriel de Grijs)
+* detect as virus mails blocked by amavis by file-extension
+* sendmail fixes (Alexander Bochmann)
+* compatibility with RRDtool 1.2.x
+* document the --logtypes
+* recognize postfix/error bounces
+* --daemon-rrd is now always respected (also without --daemon)
+* ignore per-recipient log entries of new amavisd-new versions
+
+1.10 (2004-10-21)
+-----------------
+* dspam support (Nagilum)
+* change CGI to use parameters instead of PATH_INFO, which not all web-servers support
+* Avoid showing "milli-messages/s" on the y scale (R.M. Evers)
+* Added --rbl-is-spam flag (David Gibbs)
+* Sendmail fixes (David Gibbs)
+* amavisd-new <= 20030616 (R.M. Evers)
+* Sendmail fixes (David Gibbs)
+* Line intead of area for rejects
+
+1.9 (2004-07-11)
+----------------
+* implemented --ignore-host=HOST (use it instead of --ignore-localhost if the
+ antivirus is on another machine)
+* add automatic refresh to mailgraph.cgi (Frederic Massot)
+* reorganized RRDs::graph call to make it easier to move one line from the normal
+ plot to the error plot and vice versa
+* --only-virus-rrd bugfix (Marlon Dutra)
+* support metalog with --type=metalog and FreeBSD's verbose logging (Parse::Syslog 1.04)
+* cosmetic changes of the CGI output
+* show with commented code how to tag as spam all RBL rejects
+ (Philip Hallstrom)
+* added support for clamav-milter (David Gibbs)
+* update sendmail regexps (Hugo van der Kooij)
+
+1.8 (2004-02-07)
+----------------
+* amavisd: count spam to "spam-lovers" (D_PASS)
+* initial sendmail support (Hugo van der Kooij)
+* added --rrd-name option (Tycho Fruru)
+
+1.7 (2004-01-29)
+----------------
+* --ignore-localhost didn't work correctly (Samuel Kesterson)
+
+1.6 (2004-01-19)
+----------------
+* do not parse lines with timestamps in the future (warn instead)
+
+1.5 (2004-01-19)
+----------------
+* New amavisd code by Mark Martinec
+* Removed parsing for 'pipe' service since it doesn't make much sense
+* Support for MailScanner/SpamAssassin (Gabriele Oleotti)
+* Support for latest Postfix snapshot (Ralf Hildebrandt)
+* Support for bounces with Cyrus (Will)
+* Cosmetic fixes in the CGI
+* better regexps for amavis
+* implemented --only-mail-rrd and --only-virus-rrd
+
+1.4 (2003-06-14)
+----------------
+* another amavisd-new fix (Jens Stark)
+* support for CLAMD (Fredrik Wahl)
+* fix too permissive regexp for dection of amavis virus (Yifang Dai)
+* implemented --ignore-localhost option (localhost not anymore ignored
+ unless option is not given)
+
+1.3 (2003-06-10)
+----------------
+* support for MailScanner (Carlos Horowicz)
+* support for Amavisd-new 20030314p1
+
+1.2 (2003-01-05)
+----------------
+* fix option processing with Perl 5.8.0
+
+1.1 (2003-01-05)
+----------------
+* added example init script mailgraph-init
+* implemented --daemon option
+* implemented --verbose option (rossen)
+* support for BlackHole antivirus (rene)
+* support delivery through 'pipe' for Cyrus (rossen)
+* support AMaViS 0.3.12pre8 (MAnderson)
+
+1.0 (2002-12-16)
+----------------
+* better contrast in error graph, easily changeable colors
+* support for newest amavisd-new (bpratt and
+ Ralf.Hildebrandt)
+* support for AntiVir MailGate (paulb)
+* support for Postfix cleanup-DISCARD of newer versions (roland)
+* support for DrWeb Antivirus
+* Postfix 1.1.11 reports the queue also for rejects (Ralf.Hildebrandt)
+* support for SpamAssassin with Amavis (Ralf.Hildebrandt)
+* --host is now a perl regexp
+
+0.23 (2002-10-28)
+-----------------
+* fix off-by-one-hour error when running during daylight saving time switch
+* implement --host option
+* improve a little parsing speed
+
+0.22 (2002-09-24)
+-----------------
+* support for amavisd builtin spamassassin (erich)
+* do not count mail from/to localhost (erich)
+* show averages in graphs (erich)
+* stacked error graph with nice colors (erich)
+* support for Vexira antivirus (admin_at_wexoe.dk)
+* support for Avcheck antivirus (sdesse)
+
+0.21 (2002-08-13)
+-----------------
+* recognize spam detected by spamproxyd (dsalbego)
+* totals are now more precise
+* fixed average and maximum virus/spam statistics
+
+0.20 (2002-07-02)
+-----------------
+* added statistics for Viruses (amavisd) and SPAM (spamassassin with spamd)
+* recognize also bounces based on header_checks and body_checks (Roland Arendes)
+
+0.19 (2002-04-03)
+-----------------
+* support more than one mailgraph instances on the same machine
+* implemented option --year to specify a starting year other than the current year
+
+0.18 (2002-03-15)
+-----------------
+* send the images directly from the CGI, should make configuration of the CGI
+ much easier.
+* Parse::Syslog 0.05: more robust syslog parsing
+* run mailgraph.cgi in tainted mode (Anders Nordby)
+
+0.17 (2001-11-07)
+-----------------
+* Parse::Syslog 0.04 (unreleased): much faster syslog parsing,
+ more robust year-increment algorithm
+
+0.16 (2001-09-28)
+-----------------
+* fix wrong label in graph (msg/hour -> msgs/min !), reported by
+ S. William Schulz
+
+0.15 (2001-08-24)
+-----------------
+* use the Parse::Syslog module (embedded in the script, no need to install it)
+
+0.14 (2001-08-01)
+-----------------
+* allow for different paths for images directory and images URL.
+
+0.13 (2001-06-22)
+-----------------
+* 'total' are real totals now
+* workaround rrdtool RRA selection problem because of partial matches
+
+0.12 (2001-06-22)
+-----------------
+* removed graphing code from mailgraph.pl and added mailgraph.cgi
+* change RRAs so that rrdtool should always choose the correct one
+
+0.11 (2001-06-20)
+-----------------
+* make everything an option
+* use long options
+* add '--graph' option
+* update example.html and README
+
+0.10 (2001-05-10)
+-----------------
+* report also bounces produced by postfix/smtp (bug reported by nomad4)
+* small improvement of README
+
+0.9 (2001-01-19)
+----------------
+* improved "received" regular expression (didn't always match)
+ bug reported by Adrian P. van Bloois
+
+0.8 (2001-01-07)
+----------------
+* fix syslog-date parsing bug (January). Reported by
+ Piotr Wasilewski, Leif Nixon, and Jeje
+
+0.7 (2000-12-13)
+----------------
+* add "Max" to graph, generation timestamp
+* make graphs up-to time just before present. This should fix the
+ wrong-selected-RRA problem.
+
+0.6 (2000-12-04)
+----------------
+* removed maxinterval from Fail::Tail, was much too high and anyhow not necessary
+
+0.5 (2000-11-11)
+----------------
+* .png files were GIF... Now they are really PNG files.
+
+0.4 (2000-11-11)
+----------------
+* change graph generation period to 30 minutes
+* archive unpacks in a subdirectory
diff --git a/COPYING b/COPYING
new file mode 100644
index 0000000..e77696a
--- /dev/null
+++ b/COPYING
@@ -0,0 +1,339 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc.
+ 675 Mass Ave, Cambridge, MA 02139, USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users. This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it. (Some other Free Software Foundation software is covered by
+the GNU Library General Public License instead.) You can apply it to
+your programs, too.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+ To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+ For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have. You must make sure that they, too, receive or can get the
+source code. And you must show them these terms so they know their
+rights.
+
+ We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+ Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software. If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+ Finally, any free program is threatened constantly by software
+patents. We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary. To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ GNU GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License. The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language. (Hereinafter, translation is included without limitation in
+the term "modification".) Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+ 1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+ 2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) You must cause the modified files to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ b) You must cause any work that you distribute or publish, that in
+ whole or in part contains or is derived from the Program or any
+ part thereof, to be licensed as a whole at no charge to all third
+ parties under the terms of this License.
+
+ c) If the modified program normally reads commands interactively
+ when run, you must cause it, when started running for such
+ interactive use in the most ordinary way, to print or display an
+ announcement including an appropriate copyright notice and a
+ notice that there is no warranty (or else, saying that you provide
+ a warranty) and that users may redistribute the program under
+ these conditions, and telling the user how to view a copy of this
+ License. (Exception: if the Program itself is interactive but
+ does not normally print such an announcement, your work based on
+ the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+ a) Accompany it with the complete corresponding machine-readable
+ source code, which must be distributed under the terms of Sections
+ 1 and 2 above on a medium customarily used for software interchange; or,
+
+ b) Accompany it with a written offer, valid for at least three
+ years, to give any third party, for a charge no more than your
+ cost of physically performing source distribution, a complete
+ machine-readable copy of the corresponding source code, to be
+ distributed under the terms of Sections 1 and 2 above on a medium
+ customarily used for software interchange; or,
+
+ c) Accompany it with the information you received as to the offer
+ to distribute corresponding source code. (This alternative is
+ allowed only for noncommercial distribution and only if you
+ received the program in object code or executable form with such
+ an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it. For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable. However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License. Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+ 5. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Program or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+ 6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+ 7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all. For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded. In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+ 9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time. Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation. If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+ 10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission. For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this. Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+ NO WARRANTY
+
+ 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+ 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) 19yy <name of author>
+
+ This program is free software; you can redistribute it and/or modify
+ it under the terms of the GNU General Public License as published by
+ the Free Software Foundation; either version 2 of the License, or
+ (at your option) any later version.
+
+ This program is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ GNU General Public License for more details.
+
+ You should have received a copy of the GNU General Public License
+ along with this program; if not, write to the Free Software
+ Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+ Gnomovision version 69, Copyright (C) 19yy name of author
+ Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+ This is free software, and you are welcome to redistribute it
+ under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License. Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+ `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+ <signature of Ty Coon>, 1 April 1989
+ Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs. If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library. If this is what you want to do, use the GNU Library General
+Public License instead of this License.
diff --git a/README b/README
new file mode 100644
index 0000000..e8b0900
--- /dev/null
+++ b/README
@@ -0,0 +1,85 @@
+
+ -----------------------------------------------------
+ mailgraph - a RRDtool frontend for Postfix Statistics
+ by David Schweikert <david@schweikert.ch>
+ -----------------------------------------------------
+
+mailgraph is a very simple mail statistics RRDtool frontend for Postfix
+that produces daily, weekly, monthly and yearly graphs of received/sent
+and bounced/rejected mail (SMTP traffic).
+
+Get it from:
+
+
+ http://mailgraph.schweikert.ch/
+ ===============================
+
+
+Required Modules
+----------------
+
+- rrdtool and it's perl module (RRDs)
+ -> http://oss.oetiker.ch/rrdtool/
+
+- File::Tail (which requires Time::HiRes)
+ -> get it from CPAN
+
+Note that several Linux distributions will already have these modules as RPMs.
+
+
+Usage
+-----
+
+mailgraph is made of two scripts:
+
+- mailgraph.pl
+
+ This script does parse syslog and updates the RRD database (mailgraph.rrd)
+ in the current directory.
+
+ It is a deamon and will monitor your log-file for changes.
+ DO NOT RUN IT WITH CRON!
+
+ usage: mailgraph.pl [*options*]
+
+ -h, --help display this help and exit
+ -v, --verbose be verbose about what you do
+ -V, --version output version information and exit
+ -c, --cat causes the logfile to be only read and not monitored
+ -l, --logfile f monitor logfile f instead of /var/log/syslog
+ -y, --year starting year of the log file (default: current year)
+ --host=HOST use only entries for HOST (regexp) in syslog
+ -d, --daemon start in the background
+ --daemon-pid=FILE write PID to FILE instead of /var/run/mailgraph.pid
+ --daemon-rrd=DIR write RRDs to DIR instead of /var/log
+ --daemon-log=FILE write verbose-log to FILE instead of /var/log/mailgraph.log
+ --ignore-localhost ignore mail to/from localhost (used for virus scanner)\n";
+
+ If -c is not specified, mailgraph will monitor logfile for Postfix log entries
+ in logfile (/var/log/syslog unless -l is specified).
+
+- mailgraph.cgi
+
+ This is a CGI script that does generate graphics from the RRD database.
+
+ You have probably to change $rrd to point to where the RRD database is stored.
+
+ Note that "Bounced", "Viruses", and "Spam" are stacked one on another in the
+ graph, whereas "Rejected" is a line.
+
+
+Installation
+------------
+
+See the file mailgraph-init for an example init script that you can use to
+start mailgraph at system boot.
+
+You need to put mailgraph.cgi on somewhere accessible though a web-server, it
+needs to be executeable and the web-server needs to execute it as a CGI.
+
+
+License
+-------
+
+mailgraph is released under the GPL license. See the file COPYING included in
+the distribution for details.
diff --git a/mailgraph-init b/mailgraph-init
new file mode 100644
index 0000000..22b8357
--- /dev/null
+++ b/mailgraph-init
@@ -0,0 +1,42 @@
+#!/bin/sh
+
+# $Id: mailgraph-init 19 2005-06-13 11:23:22Z dws $
+# example init script for mailgraph
+#
+# chkconfig: 2345 82 28
+# description: mailgraph postfix log grapher.
+#
+# processname: mailgraph.pl
+# pidfile: /var/run/mailgraph.pid
+
+
+PATH=/bin:/usr/bin
+MAILGRAPH_PL=/usr/local/bin/mailgraph.pl
+MAIL_LOG=/var/log/syslog
+PID_FILE=/var/run/mailgraph.pid
+RRD_DIR=/var/lib
+
+case "$1" in
+'start')
+ echo "Starting mail statistics grapher: mailgraph";
+ nice -19 $MAILGRAPH_PL -l $MAIL_LOG -d \
+ --daemon-pid=$PID_FILE --daemon-rrd=$RRD_DIR
+ ;;
+
+'stop')
+ echo "Stopping mail statistics grapher: mailgraph";
+ if [ -f $PID_FILE ]; then
+ kill `cat $PID_FILE`
+ rm $PID_FILE
+ else
+ echo "mailgraph not running";
+ fi
+ ;;
+
+*)
+ echo "Usage: $0 { start | stop }"
+ exit 1
+ ;;
+
+esac
+exit 0
diff --git a/mailgraph.cgi b/mailgraph.cgi
new file mode 100755
index 0000000..0bff5dc
--- /dev/null
+++ b/mailgraph.cgi
@@ -0,0 +1,244 @@
+#!/usr/bin/perl -w
+
+# mailgraph -- postfix mail traffic statistics
+# copyright (c) 2000-2007 ETH Zurich
+# copyright (c) 2000-2007 David Schweikert <david@schweikert.ch>
+# released under the GNU General Public License
+
+use RRDs;
+use POSIX qw(uname);
+
+my $VERSION = "1.14";
+
+my $host = (POSIX::uname())[1];
+my $scriptname = 'mailgraph.cgi';
+my $xpoints = 540;
+my $points_per_sample = 3;
+my $ypoints = 160;
+my $ypoints_err = 96;
+my $rrd = 'mailgraph.rrd'; # path to where the RRD database is
+my $rrd_virus = 'mailgraph_virus.rrd'; # path to where the Virus RRD database is
+my $tmp_dir = '/tmp/mailgraph'; # temporary directory where to store the images
+
+my @graphs = (
+ { title => 'Last Day', seconds => 3600*24, },
+ { title => 'Last Week', seconds => 3600*24*7, },
+ { title => 'Last Month', seconds => 3600*24*31, },
+ { title => 'Last Year', seconds => 3600*24*365, },
+);
+
+my %color = (
+ sent => '000099', # rrggbb in hex
+ received => '009900',
+ rejected => 'AA0000',
+ bounced => '000000',
+ virus => 'DDBB00',
+ spam => '999999',
+);
+
+sub rrd_graph(@)
+{
+ my ($range, $file, $ypoints, @rrdargs) = @_;
+ my $step = $range*$points_per_sample/$xpoints;
+ # choose carefully the end otherwise rrd will maybe pick the wrong RRA:
+ my $end = time; $end -= $end % $step;
+ my $date = localtime(time);
+ $date =~ s|:|\\:|g unless $RRDs::VERSION < 1.199908;
+
+ my ($graphret,$xs,$ys) = RRDs::graph($file,
+ '--imgformat', 'PNG',
+ '--width', $xpoints,
+ '--height', $ypoints,
+ '--start', "-$range",
+ '--end', $end,
+ '--vertical-label', 'msgs/min',
+ '--lower-limit', 0,
+ '--units-exponent', 0, # don't show milli-messages/s
+ '--lazy',
+ '--color', 'SHADEA#ffffff',
+ '--color', 'SHADEB#ffffff',
+ '--color', 'BACK#ffffff',
+
+ $RRDs::VERSION < 1.2002 ? () : ( '--slope-mode'),
+
+ @rrdargs,
+
+ 'COMMENT:['.$date.']\r',
+ );
+
+ my $ERR=RRDs::error;
+ die "ERROR: $ERR\n" if $ERR;
+}
+
+sub graph($$)
+{
+ my ($range, $file) = @_;
+ my $step = $range*$points_per_sample/$xpoints;
+ rrd_graph($range, $file, $ypoints,
+ "DEF:sent=$rrd:sent:AVERAGE",
+ "DEF:msent=$rrd:sent:MAX",
+ "CDEF:rsent=sent,60,*",
+ "CDEF:rmsent=msent,60,*",
+ "CDEF:dsent=sent,UN,0,sent,IF,$step,*",
+ "CDEF:ssent=PREV,UN,dsent,PREV,IF,dsent,+",
+ "AREA:rsent#$color{sent}:Sent ",
+ 'GPRINT:ssent:MAX:total\: %8.0lf msgs',
+ 'GPRINT:rsent:AVERAGE:avg\: %5.2lf msgs/min',
+ 'GPRINT:rmsent:MAX:max\: %4.0lf msgs/min\l',
+
+ "DEF:recv=$rrd:recv:AVERAGE",
+ "DEF:mrecv=$rrd:recv:MAX",
+ "CDEF:rrecv=recv,60,*",
+ "CDEF:rmrecv=mrecv,60,*",
+ "CDEF:drecv=recv,UN,0,recv,IF,$step,*",
+ "CDEF:srecv=PREV,UN,drecv,PREV,IF,drecv,+",
+ "LINE2:rrecv#$color{received}:Received",
+ 'GPRINT:srecv:MAX:total\: %8.0lf msgs',
+ 'GPRINT:rrecv:AVERAGE:avg\: %5.2lf msgs/min',
+ 'GPRINT:rmrecv:MAX:max\: %4.0lf msgs/min\l',
+ );
+}
+
+sub graph_err($$)
+{
+ my ($range, $file) = @_;
+ my $step = $range*$points_per_sample/$xpoints;
+ rrd_graph($range, $file, $ypoints_err,
+ "DEF:bounced=$rrd:bounced:AVERAGE",
+ "DEF:mbounced=$rrd:bounced:MAX",
+ "CDEF:rbounced=bounced,60,*",
+ "CDEF:dbounced=bounced,UN,0,bounced,IF,$step,*",
+ "CDEF:sbounced=PREV,UN,dbounced,PREV,IF,dbounced,+",
+ "CDEF:rmbounced=mbounced,60,*",
+ "AREA:rbounced#$color{bounced}:Bounced ",
+ 'GPRINT:sbounced:MAX:total\: %8.0lf msgs',
+ 'GPRINT:rbounced:AVERAGE:avg\: %5.2lf msgs/min',
+ 'GPRINT:rmbounced:MAX:max\: %4.0lf msgs/min\l',
+
+ "DEF:virus=$rrd_virus:virus:AVERAGE",
+ "DEF:mvirus=$rrd_virus:virus:MAX",
+ "CDEF:rvirus=virus,60,*",
+ "CDEF:dvirus=virus,UN,0,virus,IF,$step,*",
+ "CDEF:svirus=PREV,UN,dvirus,PREV,IF,dvirus,+",
+ "CDEF:rmvirus=mvirus,60,*",
+ "STACK:rvirus#$color{virus}:Viruses ",
+ 'GPRINT:svirus:MAX:total\: %8.0lf msgs',
+ 'GPRINT:rvirus:AVERAGE:avg\: %5.2lf msgs/min',
+ 'GPRINT:rmvirus:MAX:max\: %4.0lf msgs/min\l',
+
+ "DEF:spam=$rrd_virus:spam:AVERAGE",
+ "DEF:mspam=$rrd_virus:spam:MAX",
+ "CDEF:rspam=spam,60,*",
+ "CDEF:dspam=spam,UN,0,spam,IF,$step,*",
+ "CDEF:sspam=PREV,UN,dspam,PREV,IF,dspam,+",
+ "CDEF:rmspam=mspam,60,*",
+ "STACK:rspam#$color{spam}:Spam ",
+ 'GPRINT:sspam:MAX:total\: %8.0lf msgs',
+ 'GPRINT:rspam:AVERAGE:avg\: %5.2lf msgs/min',
+ 'GPRINT:rmspam:MAX:max\: %4.0lf msgs/min\l',
+
+ "DEF:rejected=$rrd:rejected:AVERAGE",
+ "DEF:mrejected=$rrd:rejected:MAX",
+ "CDEF:rrejected=rejected,60,*",
+ "CDEF:drejected=rejected,UN,0,rejected,IF,$step,*",
+ "CDEF:srejected=PREV,UN,drejected,PREV,IF,drejected,+",
+ "CDEF:rmrejected=mrejected,60,*",
+ "LINE2:rrejected#$color{rejected}:Rejected",
+ 'GPRINT:srejected:MAX:total\: %8.0lf msgs',
+ 'GPRINT:rrejected:AVERAGE:avg\: %5.2lf msgs/min',
+ 'GPRINT:rmrejected:MAX:max\: %4.0lf msgs/min\l',
+
+ );
+}
+
+sub print_html()
+{
+ print "Content-Type: text/html\n\n";
+
+ print <<HEADER;
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
+<title>Mail statistics for $host</title>
+<meta http-equiv="Refresh" content="300" />
+<meta http-equiv="Pragma" content="no-cache" />
+<link rel="stylesheet" href="mailgraph.css" type="text/css" />
+</head>
+<body>
+HEADER
+
+ print "<h1>Mail statistics for $host</h1>\n";
+
+ print "<ul id=\"jump\">\n";
+ for my $n (0..$#graphs) {
+ print " <li><a href=\"#G$n\">$graphs[$n]{title}</a>&nbsp;</li>\n";
+ }
+ print "</ul>\n";
+
+ for my $n (0..$#graphs) {
+ print "<h2 id=\"G$n\">$graphs[$n]{title}</h2>\n";
+ print "<p><img src=\"$scriptname?${n}-n\" alt=\"mailgraph\"/><br/>\n";
+ print "<img src=\"$scriptname?${n}-e\" alt=\"mailgraph\"/></p>\n";
+ }
+
+ print <<FOOTER;
+<hr/>
+<table><tr><td>
+<a href="http://mailgraph.schweikert.ch/">Mailgraph</a> $VERSION
+by <a href="http://david.schweikert.ch/">David Schweikert</a></td>
+<td align="right">
+<a href="http://oss.oetiker.ch/rrdtool/"><img src="http://oss.oetiker.ch/rrdtool/.pics/rrdtool.gif" alt="" width="120" height="34"/></a>
+</td></tr></table>
+</body></html>
+FOOTER
+}
+
+sub send_image($)
+{
+ my ($file)= @_;
+
+ -r $file or do {
+ print "Content-type: text/plain\n\nERROR: can't find $file\n";
+ exit 1;
+ };
+
+ print "Content-type: image/png\n";
+ print "Content-length: ".((stat($file))[7])."\n";
+ print "\n";
+ open(IMG, $file) or die;
+ my $data;
+ print $data while read(IMG, $data, 16384)>0;
+}
+
+sub main()
+{
+ my $uri = $ENV{REQUEST_URI} || '';
+ $uri =~ s/\/[^\/]+$//;
+ $uri =~ s/\//,/g;
+ $uri =~ s/(\~|\%7E)/tilde,/g;
+ mkdir $tmp_dir, 0777 unless -d $tmp_dir;
+ mkdir "$tmp_dir/$uri", 0777 unless -d "$tmp_dir/$uri";
+
+ my $img = $ENV{QUERY_STRING};
+ if(defined $img and $img =~ /\S/) {
+ if($img =~ /^(\d+)-n$/) {
+ my $file = "$tmp_dir/$uri/mailgraph_$1.png";
+ graph($graphs[$1]{seconds}, $file);
+ send_image($file);
+ }
+ elsif($img =~ /^(\d+)-e$/) {
+ my $file = "$tmp_dir/$uri/mailgraph_$1_err.png";
+ graph_err($graphs[$1]{seconds}, $file);
+ send_image($file);
+ }
+ else {
+ die "ERROR: invalid argument\n";
+ }
+ }
+ else {
+ print_html;
+ }
+}
+
+main;
diff --git a/mailgraph.css b/mailgraph.css
new file mode 100644
index 0000000..f38cf41
--- /dev/null
+++ b/mailgraph.css
@@ -0,0 +1,21 @@
+* { margin: 0; padding: 0 }
+body { width: 630px; background-color: white;
+ font-family: sans-serif;
+ font-size: 12pt;
+ margin: 5px }
+h1 { margin-top: 20px; margin-bottom: 30px;
+ text-align: center }
+h2 { background-color: #ddd;
+ padding: 2px 0 2px 4px }
+hr { height: 1px;
+ border: 0;
+ border-top: 1px solid #aaa }
+table { border: 0px; width: 100% }
+img { border: 0 }
+a { text-decoration: none; color: #00e }
+a:hover { text-decoration: underline; }
+#jump { margin: 0 0 10px 4px }
+#jump li { list-style: none; display: inline;
+ font-size: 90%; }
+#jump li:after { content: "|"; }
+#jump li:last-child:after { content: ""; }
diff --git a/mailgraph.pl b/mailgraph.pl
new file mode 100755
index 0000000..5da3eda
--- /dev/null
+++ b/mailgraph.pl
@@ -0,0 +1,976 @@
+#!/usr/bin/perl -w
+
+# mailgraph -- an rrdtool frontend for mail statistics
+# copyright (c) 2000-2007 ETH Zurich
+# copyright (c) 2000-2007 David Schweikert <david@schweikert.ch>
+# released under the GNU General Public License
+
+######## Parse::Syslog 1.09 (automatically embedded) ########
+package Parse::Syslog;
+use Carp;
+use Symbol;
+use Time::Local;
+use IO::File;
+use strict;
+use vars qw($VERSION);
+my %months_map = (
+ 'Jan' => 0, 'Feb' => 1, 'Mar' => 2,
+ 'Apr' => 3, 'May' => 4, 'Jun' => 5,
+ 'Jul' => 6, 'Aug' => 7, 'Sep' => 8,
+ 'Oct' => 9, 'Nov' =>10, 'Dec' =>11,
+ 'jan' => 0, 'feb' => 1, 'mar' => 2,
+ 'apr' => 3, 'may' => 4, 'jun' => 5,
+ 'jul' => 6, 'aug' => 7, 'sep' => 8,
+ 'oct' => 9, 'nov' =>10, 'dec' =>11,
+);
+sub is_dst_switch($$$)
+{
+ my ($self, $t, $time) = @_;
+ # calculate the time in one hour and see if the difference is 3600 seconds.
+ # if not, we are in a dst-switch hour
+ # note that right now we only support 1-hour dst offsets
+ # cache the result
+ if(defined $self->{is_dst_switch_last_hour} and
+ $self->{is_dst_switch_last_hour} == $t->[3]<<5+$t->[2]) {
+ return @{$self->{is_dst_switch_result}};
+ }
+ # calculate a number out of the day and hour to identify the hour
+ $self->{is_dst_switch_last_hour} = $t->[3]<<5+$t->[2];
+ # calculating hour+1 (below) is a problem if the hour is 23. as far as I
+ # know, nobody does the DST switch at this time, so just assume it isn't
+ # DST switch if the hour is 23.
+ if($t->[2]==23) {
+ @{$self->{is_dst_switch_result}} = (0, undef);
+ return @{$self->{is_dst_switch_result}};
+ }
+ # let's see the timestamp in one hour
+ # 0: sec, 1: min, 2: h, 3: day, 4: month, 5: year
+ my $time_plus_1h = timelocal($t->[0], $t->[1], $t->[2]+1, $t->[3], $t->[4], $t->[5]);
+ if($time_plus_1h - $time > 4000) {
+ @{$self->{is_dst_switch_result}} = (3600, $time-$time%3600+3600);
+ }
+ else {
+ @{$self->{is_dst_switch_result}} = (0, undef);
+ }
+ return @{$self->{is_dst_switch_result}};
+}
+# fast timelocal, cache minute's timestamp
+# don't cache more than minute because of daylight saving time switch
+# 0: sec, 1: min, 2: h, 3: day, 4: month, 5: year
+sub str2time($$$$$$$$)
+{
+ my $self = shift @_;
+ my $GMT = pop @_;
+ my $lastmin = $self->{str2time_lastmin};
+ if(defined $lastmin and
+ $lastmin->[0] == $_[1] and
+ $lastmin->[1] == $_[2] and
+ $lastmin->[2] == $_[3] and
+ $lastmin->[3] == $_[4] and
+ $lastmin->[4] == $_[5])
+ {
+ $self->{last_time} = $self->{str2time_lastmin_time} + $_[0];
+ return $self->{last_time} + ($self->{dst_comp}||0);
+ }
+ my $time;
+ if($GMT) {
+ $time = timegm(@_);
+ }
+ else {
+ $time = timelocal(@_);
+ }
+ # compensate for DST-switch
+ # - if a timewarp is detected (1:00 -> 1:30 -> 1:00):
+ # - test if we are in a DST-switch-hour
+ # - compensate if yes
+ # note that we assume that the DST-switch goes like this:
+ # time 1:00 1:30 2:00 2:30 2:00 2:30 3:00 3:30
+ # stamp 1 2 3 4 3 3 7 8
+ # comp. 0 0 0 0 2 2 0 0
+ # result 1 2 3 4 5 6 7 8
+ # old Time::Local versions behave differently (1 2 5 6 5 6 7 8)
+ if(!$GMT and !defined $self->{dst_comp} and
+ defined $self->{last_time} and
+ $self->{last_time}-$time > 1200 and
+ $self->{last_time}-$time < 3600)
+ {
+ my ($off, $until) = $self->is_dst_switch(\@_, $time);
+ if($off) {
+ $self->{dst_comp} = $off;
+ $self->{dst_comp_until} = $until;
+ }
+ }
+ if(defined $self->{dst_comp_until} and $time > $self->{dst_comp_until}) {
+ delete $self->{dst_comp};
+ delete $self->{dst_comp_until};
+ }
+ $self->{str2time_lastmin} = [ @_[1..5] ];
+ $self->{str2time_lastmin_time} = $time-$_[0];
+ $self->{last_time} = $time;
+ return $time+($self->{dst_comp}||0);
+}
+sub _use_locale($)
+{
+ use POSIX qw(locale_h strftime);
+ my $old_locale = setlocale(LC_TIME);
+ for my $locale (@_) {
+ croak "new(): wrong 'locale' value: '$locale'" unless setlocale(LC_TIME, $locale);
+ for my $month (0..11) {
+ $months_map{strftime("%b", 0, 0, 0, 1, $month, 96)} = $month;
+ }
+ }
+ setlocale(LC_TIME, $old_locale);
+}
+sub new($$;%)
+{
+ my ($class, $file, %data) = @_;
+ croak "new() requires one argument: file" unless defined $file;
+ %data = () unless %data;
+ if(not defined $data{year}) {
+ $data{year} = (localtime(time))[5]+1900;
+ }
+ $data{type} = 'syslog' unless defined $data{type};
+ $data{_repeat}=0;
+ if(UNIVERSAL::isa($file, 'IO::Handle')) {
+ $data{file} = $file;
+ }
+ elsif(UNIVERSAL::isa($file, 'File::Tail')) {
+ $data{file} = $file;
+ $data{filetail}=1;
+ }
+ elsif(! ref $file) {
+ if($file eq '-') {
+ my $io = new IO::Handle;
+ $data{file} = $io->fdopen(fileno(STDIN),"r");
+ }
+ else {
+ $data{file} = new IO::File($file, "<");
+ defined $data{file} or croak "can't open $file: $!";
+ }
+ }
+ else {
+ croak "argument must be either a file-name or an IO::Handle object.";
+ }
+ if(defined $data{locale}) {
+ if(ref $data{locale} eq 'ARRAY') {
+ _use_locale @{$data{locale}};
+ }
+ elsif(ref $data{locale} eq '') {
+ _use_locale $data{locale};
+ }
+ else {
+ croak "'locale' parameter must be scalar or array of scalars";
+ }
+ }
+ return bless \%data, $class;
+}
+sub _year_increment($$)
+{
+ my ($self, $mon) = @_;
+ # year change
+ if($mon==0) {
+ $self->{year}++ if defined $self->{_last_mon} and $self->{_last_mon} == 11;
+ $self->{enable_year_decrement} = 1;
+ }
+ elsif($mon == 11) {
+ if($self->{enable_year_decrement}) {
+ $self->{year}-- if defined $self->{_last_mon} and $self->{_last_mon} != 11;
+ }
+ }
+ else {
+ $self->{enable_year_decrement} = 0;
+ }
+ $self->{_last_mon} = $mon;
+}
+sub _next_line($)
+{
+ my $self = shift;
+ my $f = $self->{file};
+ if(defined $self->{filetail}) {
+ return $f->read;
+ }
+ else {
+ return $f->getline;
+ }
+}
+sub _next_syslog($)
+{
+ my ($self) = @_;
+ while($self->{_repeat}>0) {
+ $self->{_repeat}--;
+ return $self->{_repeat_data};
+ }
+ my $file = $self->{file};
+ line: while(defined (my $str = $self->_next_line)) {
+ # date, time and host
+ $str =~ /^
+ (\S{3})\s+(\d+) # date -- 1, 2
+ \s
+ (\d+):(\d+):(\d+) # time -- 3, 4, 5
+ (?:\s<\w+\.\w+>)? # FreeBSD's verbose-mode
+ \s
+ ([-\w\.\@:]+) # host -- 6
+ \s+
+ (?:\[LOG_[A-Z]+\]\s+)? # FreeBSD
+ (.*) # text -- 7
+ $/x or do
+ {
+ warn "WARNING: line not in syslog format: $str";
+ next line;
+ };
+ my $mon = $months_map{$1};
+ defined $mon or croak "unknown month $1\n";
+ $self->_year_increment($mon);
+ # convert to unix time
+ my $time = $self->str2time($5,$4,$3,$2,$mon,$self->{year}-1900,$self->{GMT});
+ if(not $self->{allow_future}) {
+ # accept maximum one day in the present future
+ if($time - time > 86400) {
+ warn "WARNING: ignoring future date in syslog line: $str";
+ next line;
+ }
+ }
+ my ($host, $text) = ($6, $7);
+ # last message repeated ... times
+ if($text =~ /^(?:last message repeated|above message repeats) (\d+) time/) {
+ next line if defined $self->{repeat} and not $self->{repeat};
+ next line if not defined $self->{_last_data}{$host};
+ $1 > 0 or do {
+ warn "WARNING: last message repeated 0 or less times??\n";
+ next line;
+ };
+ $self->{_repeat}=$1-1;
+ $self->{_repeat_data}=$self->{_last_data}{$host};
+ return $self->{_last_data}{$host};
+ }
+ # marks
+ next if $text eq '-- MARK --';
+ # some systems send over the network their
+ # hostname prefixed to the text. strip that.
+ $text =~ s/^$host\s+//;
+ # discard ':' in HP-UX 'su' entries like this:
+ # Apr 24 19:09:40 remedy : su : + tty?? root-oracle
+ $text =~ s/^:\s+//;
+ $text =~ /^
+ ([^:]+?) # program -- 1
+ (?:\[(\d+)\])? # PID -- 2
+ :\s+
+ (?:\[ID\ (\d+)\ ([a-z0-9]+)\.([a-z]+)\]\ )? # Solaris 8 "message id" -- 3, 4, 5
+ (.*) # text -- 6
+ $/x or do
+ {
+ warn "WARNING: line not in syslog format: $str";
+ next line;
+ };
+ if($self->{arrayref}) {
+ $self->{_last_data}{$host} = [
+ $time, # 0: timestamp
+ $host, # 1: host
+ $1, # 2: program
+ $2, # 3: pid
+ $6, # 4: text
+ ];
+ }
+ else {
+ $self->{_last_data}{$host} = {
+ timestamp => $time,
+ host => $host,
+ program => $1,
+ pid => $2,
+ msgid => $3,
+ facility => $4,
+ level => $5,
+ text => $6,
+ };
+ }
+ return $self->{_last_data}{$host};
+ }
+ return undef;
+}
+sub _next_metalog($)
+{
+ my ($self) = @_;
+ my $file = $self->{file};
+ line: while(my $str = $self->_next_line) {
+ # date, time and host
+ $str =~ /^
+ (\S{3})\s+(\d+) # date -- 1, 2
+ \s
+ (\d+):(\d+):(\d+) # time -- 3, 4, 5
+ # host is not logged
+ \s+
+ (.*) # text -- 6
+ $/x or do
+ {
+ warn "WARNING: line not in metalog format: $str";
+ next line;
+ };
+ my $mon = $months_map{$1};
+ defined $mon or croak "unknown month $1\n";
+ $self->_year_increment($mon);
+ # convert to unix time
+ my $time = $self->str2time($5,$4,$3,$2,$mon,$self->{year}-1900,$self->{GMT});
+ my $text = $6;
+ $text =~ /^
+ \[(.*?)\] # program -- 1
+ # no PID
+ \s+
+ (.*) # text -- 2
+ $/x or do
+ {
+ warn "WARNING: text line not in metalog format: $text ($str)";
+ next line;
+ };
+ if($self->{arrayref}) {
+ return [
+ $time, # 0: timestamp
+ 'localhost', # 1: host
+ $1, # 2: program
+ undef, # 3: (no) pid
+ $2, # 4: text
+ ];
+ }
+ else {
+ return {
+ timestamp => $time,
+ host => 'localhost',
+ program => $1,
+ text => $2,
+ };
+ }
+ }
+ return undef;
+}
+sub next($)
+{
+ my ($self) = @_;
+ if($self->{type} eq 'syslog') {
+ return $self->_next_syslog();
+ }
+ elsif($self->{type} eq 'metalog') {
+ return $self->_next_metalog();
+ }
+ croak "Internal error: unknown type: $self->{type}";
+}
+
+#####################################################################
+#####################################################################
+#####################################################################
+
+use RRDs;
+
+use strict;
+use File::Tail;
+use Getopt::Long;
+use POSIX 'setsid';
+
+my $VERSION = "1.14";
+
+# config
+my $rrdstep = 60;
+my $xpoints = 540;
+my $points_per_sample = 3;
+
+my $daemon_logfile = '/var/log/mailgraph.log';
+my $daemon_pidfile = '/var/run/mailgraph.pid';
+my $daemon_rrd_dir = '/var/log';
+
+# global variables
+my $logfile;
+my $rrd = "mailgraph.rrd";
+my $rrd_virus = "mailgraph_virus.rrd";
+my $year;
+my $this_minute;
+my %sum = ( sent => 0, received => 0, bounced => 0, rejected => 0, virus => 0, spam => 0 );
+my $rrd_inited=0;
+
+my %opt = ();
+
+# prototypes
+sub daemonize();
+sub process_line($);
+sub event_sent($);
+sub event_received($);
+sub event_bounced($);
+sub event_rejected($);
+sub event_virus($);
+sub event_spam($);
+sub init_rrd($);
+sub update($);
+
+sub usage
+{
+ print "usage: mailgraph [*options*]\n\n";
+ print " -h, --help display this help and exit\n";
+ print " -v, --verbose be verbose about what you do\n";
+ print " -V, --version output version information and exit\n";
+ print " -c, --cat causes the logfile to be only read and not monitored\n";
+ print " -l, --logfile f monitor logfile f instead of /var/log/syslog\n";
+ print " -t, --logtype t set logfile's type (default: syslog)\n";
+ print " -y, --year starting year of the log file (default: current year)\n";
+ print " --host=HOST use only entries for HOST (regexp) in syslog\n";
+ print " -d, --daemon start in the background\n";
+ print " --daemon-pid=FILE write PID to FILE instead of /var/run/mailgraph.pid\n";
+ print " --daemon-rrd=DIR write RRDs to DIR instead of /var/log\n";
+ print " --daemon-log=FILE write verbose-log to FILE instead of /var/log/mailgraph.log\n";
+ print " --ignore-localhost ignore mail to/from localhost (used for virus scanner)\n";
+ print " --ignore-host=HOST ignore mail to/from HOST regexp (used for virus scanner)\n";
+ print " --only-mail-rrd update only the mail rrd\n";
+ print " --only-virus-rrd update only the virus rrd\n";
+ print " --rrd-name=NAME use NAME.rrd and NAME_virus.rrd for the rrd files\n";
+ print " --rbl-is-spam count rbl rejects as spam\n";
+ print " --virbl-is-virus count virbl rejects as viruses\n";
+
+ exit;
+}
+
+sub main
+{
+ Getopt::Long::Configure('no_ignore_case');
+ GetOptions(\%opt, 'help|h', 'cat|c', 'logfile|l=s', 'logtype|t=s', 'version|V',
+ 'year|y=i', 'host=s', 'verbose|v', 'daemon|d!',
+ 'daemon_pid|daemon-pid=s', 'daemon_rrd|daemon-rrd=s',
+ 'daemon_log|daemon-log=s', 'ignore-localhost!', 'ignore-host=s@',
+ 'only-mail-rrd', 'only-virus-rrd', 'rrd_name|rrd-name=s',
+ 'rbl-is-spam', 'virbl-is-virus'
+ ) or exit(1);
+ usage if $opt{help};
+
+ if($opt{version}) {
+ print "mailgraph $VERSION by david\@schweikert.ch\n";
+ exit;
+ }
+
+ $daemon_pidfile = $opt{daemon_pid} if defined $opt{daemon_pid};
+ $daemon_logfile = $opt{daemon_log} if defined $opt{daemon_log};
+ $daemon_rrd_dir = $opt{daemon_rrd} if defined $opt{daemon_rrd};
+ $rrd = $opt{rrd_name}.".rrd" if defined $opt{rrd_name};
+ $rrd_virus = $opt{rrd_name}."_virus.rrd" if defined $opt{rrd_name};
+
+ # compile --ignore-host regexps
+ if(defined $opt{'ignore-host'}) {
+ for my $ih (@{$opt{'ignore-host'}}) {
+ push @{$opt{'ignore-host-re'}}, qr{\brelay=[^\s,]*$ih}i;
+ }
+ }
+
+ if($opt{daemon} or $opt{daemon_rrd}) {
+ chdir $daemon_rrd_dir or die "mailgraph: can't chdir to $daemon_rrd_dir: $!";
+ -w $daemon_rrd_dir or die "mailgraph: can't write to $daemon_rrd_dir\n";
+ }
+
+ daemonize if $opt{daemon};
+
+ my $logfile = defined $opt{logfile} ? $opt{logfile} : '/var/log/syslog';
+ my $file;
+ if($opt{cat}) {
+ $file = $logfile;
+ }
+ else {
+ $file = File::Tail->new(name=>$logfile, tail=>-1);
+ }
+ my $parser = new Parse::Syslog($file, year => $opt{year}, arrayref => 1,
+ type => defined $opt{logtype} ? $opt{logtype} : 'syslog');
+
+ if(not defined $opt{host}) {
+ while(my $sl = $parser->next) {
+ process_line($sl);
+ }
+ }
+ else {
+ my $host = qr/^$opt{host}$/i;
+ while(my $sl = $parser->next) {
+ process_line($sl) if $sl->[1] =~ $host;
+ }
+ }
+}
+
+sub daemonize()
+{
+ open STDIN, '/dev/null' or die "mailgraph: can't read /dev/null: $!";
+ if($opt{verbose}) {
+ open STDOUT, ">>$daemon_logfile"
+ or die "mailgraph: can't write to $daemon_logfile: $!";
+ }
+ else {
+ open STDOUT, '>/dev/null'
+ or die "mailgraph: can't write to /dev/null: $!";
+ }
+ defined(my $pid = fork) or die "mailgraph: can't fork: $!";
+ if($pid) {
+ # parent
+ open PIDFILE, ">$daemon_pidfile"
+ or die "mailgraph: can't write to $daemon_pidfile: $!\n";
+ print PIDFILE "$pid\n";
+ close(PIDFILE);
+ exit;
+ }
+ # child
+ setsid or die "mailgraph: can't start a new session: $!";
+ open STDERR, '>&STDOUT' or die "mailgraph: can't dup stdout: $!";
+}
+
+sub init_rrd($)
+{
+ my $m = shift;
+ my $rows = $xpoints/$points_per_sample;
+ my $realrows = int($rows*1.1); # ensure that the full range is covered
+ my $day_steps = int(3600*24 / ($rrdstep*$rows));
+ # use multiples, otherwise rrdtool could choose the wrong RRA
+ my $week_steps = $day_steps*7;
+ my $month_steps = $week_steps*5;
+ my $year_steps = $month_steps*12;
+
+ # mail rrd
+ if(! -f $rrd and ! $opt{'only-virus-rrd'}) {
+ RRDs::create($rrd, '--start', $m, '--step', $rrdstep,
+ 'DS:sent:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 'DS:recv:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 'DS:bounced:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 'DS:rejected:ABSOLUTE:'.($rrdstep*2).':0:U',
+ "RRA:AVERAGE:0.5:$day_steps:$realrows", # day
+ "RRA:AVERAGE:0.5:$week_steps:$realrows", # week
+ "RRA:AVERAGE:0.5:$month_steps:$realrows", # month
+ "RRA:AVERAGE:0.5:$year_steps:$realrows", # year
+ "RRA:MAX:0.5:$day_steps:$realrows", # day
+ "RRA:MAX:0.5:$week_steps:$realrows", # week
+ "RRA:MAX:0.5:$month_steps:$realrows", # month
+ "RRA:MAX:0.5:$year_steps:$realrows", # year
+ );
+ $this_minute = $m;
+ }
+ elsif(-f $rrd) {
+ $this_minute = RRDs::last($rrd) + $rrdstep;
+ }
+
+ # virus rrd
+ if(! -f $rrd_virus and ! $opt{'only-mail-rrd'}) {
+ RRDs::create($rrd_virus, '--start', $m, '--step', $rrdstep,
+ 'DS:virus:ABSOLUTE:'.($rrdstep*2).':0:U',
+ 'DS:spam:ABSOLUTE:'.($rrdstep*2).':0:U',
+ "RRA:AVERAGE:0.5:$day_steps:$realrows", # day
+ "RRA:AVERAGE:0.5:$week_steps:$realrows", # week
+ "RRA:AVERAGE:0.5:$month_steps:$realrows", # month
+ "RRA:AVERAGE:0.5:$year_steps:$realrows", # year
+ "RRA:MAX:0.5:$day_steps:$realrows", # day
+ "RRA:MAX:0.5:$week_steps:$realrows", # week
+ "RRA:MAX:0.5:$month_steps:$realrows", # month
+ "RRA:MAX:0.5:$year_steps:$realrows", # year
+ );
+ }
+ elsif(-f $rrd_virus and ! defined $rrd_virus) {
+ $this_minute = RRDs::last($rrd_virus) + $rrdstep;
+ }
+
+ $rrd_inited=1;
+}
+
+sub process_line($)
+{
+ my $sl = shift;
+ my $time = $sl->[0];
+ my $prog = $sl->[2];
+ my $text = $sl->[4];
+
+ if($prog =~ /^postfix\/(.*)/) {
+ my $prog = $1;
+ if($prog eq 'smtp') {
+ if($text =~ /\bstatus=sent\b/) {
+ return if $opt{'ignore-localhost'} and
+ $text =~ /\brelay=[^\s\[]*\[127\.0\.0\.1\]/;
+ if(defined $opt{'ignore-host-re'}) {
+ for my $ih (@{$opt{'ignore-host-re'}}) {
+ warn "MATCH! $text\n" if $text =~ $ih;
+ return if $text =~ $ih;
+ }
+ }
+ event($time, 'sent');
+ }
+ elsif($text =~ /\bstatus=bounced\b/) {
+ event($time, 'bounced');
+ }
+ }
+ elsif($prog eq 'local') {
+ if($text =~ /\bstatus=bounced\b/) {
+ event($time, 'bounced');
+ }
+ }
+ elsif($prog eq 'smtpd') {
+ if($text =~ /^[0-9A-Z]+: client=(\S+)/) {
+ my $client = $1;
+ return if $opt{'ignore-localhost'} and
+ $client =~ /\[127\.0\.0\.1\]$/;
+ return if $opt{'ignore-host'} and
+ $client =~ /$opt{'ignore-host'}/oi;
+ event($time, 'received');
+ }
+ elsif($opt{'virbl-is-virus'} and $text =~ /^(?:[0-9A-Z]+: |NOQUEUE: )?reject: .*: 554.* blocked using virbl.dnsbl.bit.nl/) {
+ event($time, 'virus');
+ }
+ elsif($opt{'rbl-is-spam'} and $text =~ /^(?:[0-9A-Z]+: |NOQUEUE: )?reject: .*: 554.* blocked using/) {
+ event($time, 'spam');
+ }
+ elsif($text =~ /^(?:[0-9A-Z]+: |NOQUEUE: )?reject: /) {
+ event($time, 'rejected');
+ }
+ elsif($text =~ /^(?:[0-9A-Z]+: |NOQUEUE: )?milter-reject: /) {
+ if($text =~ /Blocked by SpamAssassin/) {
+ event($time, 'spam');
+ }
+ else {
+ event($time, 'rejected');
+ }
+ }
+ }
+ elsif($prog eq 'error') {
+ if($text =~ /\bstatus=bounced\b/) {
+ event($time, 'bounced');
+ }
+ }
+ elsif($prog eq 'cleanup') {
+ if($text =~ /^[0-9A-Z]+: (?:reject|discard): /) {
+ event($time, 'rejected');
+ }
+ }
+ }
+ elsif($prog eq 'sendmail' or $prog eq 'sm-mta') {
+ if($text =~ /\bmailer=local\b/ ) {
+ event($time, 'received');
+ }
+ elsif($text =~ /\bmailer=relay\b/) {
+ event($time, 'received');
+ }
+ elsif($text =~ /\bstat=Sent\b/ ) {
+ event($time, 'sent');
+ }
+ elsif($text =~ /\bmailer=esmtp\b/ ) {
+ event($time, 'sent');
+ }
+ elsif($text =~ /\bruleset=check_XS4ALL\b/ ) {
+ event($time, 'rejected');
+ }
+ elsif($text =~ /\blost input channel\b/ ) {
+ event($time, 'rejected');
+ }
+ elsif($text =~ /\bruleset=check_rcpt\b/ ) {
+ event($time, 'rejected');
+ }
+ elsif($text =~ /\bstat=virus\b/ ) {
+ event($time, 'virus');
+ }
+ elsif($text =~ /\bruleset=check_relay\b/ ) {
+ if (($opt{'virbl-is-virus'}) and ($text =~ /\bivirbl\b/ )) {
+ event($time, 'virus');
+ } elsif ($opt{'rbl-is-spam'}) {
+ event($time, 'spam');
+ } else {
+ event($time, 'rejected');
+ }
+ }
+ elsif($text =~ /\bsender blocked\b/ ) {
+ event($time, 'rejected');
+ }
+ elsif($text =~ /\bsender denied\b/ ) {
+ event($time, 'rejected');
+ }
+ elsif($text =~ /\brecipient denied\b/ ) {
+ event($time, 'rejected');
+ }
+ elsif($text =~ /\brecipient unknown\b/ ) {
+ event($time, 'rejected');
+ }
+ elsif($text =~ /\bUser unknown$/i ) {
+ event($time, 'bounced');
+ }
+ elsif($text =~ /\bMilter:.*\breject=55/ ) {
+ event($time, 'rejected');
+ }
+ }
+ elsif($prog eq 'exim') {
+ if($text =~ /^[0-9a-zA-Z]{6}-[0-9a-zA-Z]{6}-[0-9a-zA-Z]{2} <= \S+/) {
+ event($time, 'received');
+ }
+ elsif($text =~ /^[0-9a-zA-Z]{6}-[0-9a-zA-Z]{6}-[0-9a-zA-Z]{2} => \S+/) {
+ event($time, 'sent');
+ }
+ elsif($text =~ / rejected because \S+ is in a black list at \S+/) {
+ if($opt{'rbl-is-spam'}) {
+ event($time, 'spam');
+ } else {
+ event($time, 'rejected');
+ }
+ }
+ elsif($text =~ / rejected RCPT \S+: (Sender verify failed|Unknown user)/) {
+ event($time, 'rejected');
+ }
+ }
+ elsif($prog eq 'amavis' || $prog eq 'amavisd') {
+ if( $text =~ /^\([\w-]+\) (Passed|Blocked) SPAM(?:MY)?\b/) {
+ if($text !~ /\btag2=/) { # ignore new per-recipient log entry (2.2.0)
+ event($time, 'spam'); # since amavisd-new-2004xxxx
+ }
+ }
+ elsif($text =~ /^\([\w-]+\) (Passed|Not-Delivered)\b.*\bquarantine spam/) {
+ event($time, 'spam'); # amavisd-new-20030616 and earlier
+ }
+ elsif($text =~ /^\([\w-]+\) (Passed |Blocked )?INFECTED\b/) {
+ if($text !~ /\btag2=/) {
+ event($time, 'virus');# Passed|Blocked inserted since 2004xxxx
+ }
+ }
+ elsif($text =~ /^\([\w-]+\) (Passed |Blocked )?BANNED\b/) {
+ if($text !~ /\btag2=/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($text =~ /^Virus found\b/) {
+ event($time, 'virus');# AMaViS 0.3.12 and amavisd-0.1
+ }
+# elsif($text =~ /^\([\w-]+\) Passed|Blocked BAD-HEADER\b/) {
+# event($time, 'badh');
+# }
+ }
+ elsif($prog eq 'vagatefwd') {
+ # Vexira antivirus (old)
+ if($text =~ /^VIRUS/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($prog eq 'hook') {
+ # Vexira antivirus
+ if($text =~ /^\*+ Virus\b/) {
+ event($time, 'virus');
+ }
+ # Vexira antispam
+ elsif($text =~ /\bcontains spam\b/) {
+ event($time, 'spam');
+ }
+ }
+ elsif($prog eq 'avgatefwd' or $prog eq 'avmailgate.bin') {
+ # AntiVir MailGate
+ if($text =~ /^Alert!/) {
+ event($time, 'virus');
+ }
+ elsif($text =~ /blocked\.$/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($prog eq 'avcheck') {
+ # avcheck
+ if($text =~ /^infected/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($prog eq 'spamd') {
+ if($text =~ /^(?:spamd: )?identified spam/) {
+ event($time, 'spam');
+ }
+ # ClamAV SpamAssassin-plugin
+ elsif($text =~ /(?:result: )?CLAMAV/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($prog eq 'dspam') {
+ if($text =~ /spam detected from/) {
+ event($time, 'spam');
+ }
+ }
+ elsif($prog eq 'spamproxyd' or $prog eq 'spampd') {
+ if($text =~ /^\s*SPAM/ or $text =~ /^identified spam/) {
+ event($time, 'spam');
+ }
+ }
+ elsif($prog eq 'drweb-postfix') {
+ # DrWeb
+ if($text =~ /infected/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($prog eq 'BlackHole') {
+ if($text =~ /Virus/) {
+ event($time, 'virus');
+ }
+ if($text =~ /(?:RBL|Razor|Spam)/) {
+ event($time, 'spam');
+ }
+ }
+ elsif($prog eq 'MailScanner') {
+ if($text =~ /(Virus Scanning: Found)/ ) {
+ event($time, 'virus');
+ }
+ elsif($text =~ /Bounce to/ ) {
+ event($time, 'bounced');
+ }
+ elsif($text =~ /^Spam Checks: Found ([0-9]+) spam messages/) {
+ my $cnt = $1;
+ for (my $i=0; $i<$cnt; $i++) {
+ event($time, 'spam');
+ }
+ }
+ }
+ elsif($prog eq 'clamsmtpd') {
+ if($text =~ /status=VIRUS/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($prog eq 'clamav-milter') {
+ if($text =~ /Intercepted/) {
+ event($time, 'virus');
+ }
+ }
+ # uncommment for clamassassin:
+ #elsif($prog eq 'clamd') {
+ # if($text =~ /^stream: .* FOUND$/) {
+ # event($time, 'virus');
+ # }
+ #}
+ elsif ($prog eq 'smtp-vilter') {
+ if ($text =~ /clamd: found/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($prog eq 'avmilter') {
+ # AntiVir Milter
+ if($text =~ /^Alert!/) {
+ event($time, 'virus');
+ }
+ elsif($text =~ /blocked\.$/) {
+ event($time, 'virus');
+ }
+ }
+ elsif($prog eq 'bogofilter') {
+ if($text =~ /Spam/) {
+ event($time, 'spam');
+ }
+ }
+ elsif($prog eq 'filter-module') {
+ if($text =~ /\bspam_status\=(?:yes|spam)/) {
+ event($time, 'spam');
+ }
+ }
+ elsif($prog eq 'sta_scanner') {
+ if($text =~ /^[0-9A-F]+: virus/) {
+ event($time, 'virus');
+ }
+ }
+}
+
+sub event($$)
+{
+ my ($t, $type) = @_;
+ update($t) and $sum{$type}++;
+}
+
+# returns 1 if $sum should be updated
+sub update($)
+{
+ my $t = shift;
+ my $m = $t - $t%$rrdstep;
+ init_rrd($m) unless $rrd_inited;
+ return 1 if $m == $this_minute;
+ return 0 if $m < $this_minute;
+
+ print "update $this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}:$sum{virus}:$sum{spam}\n" if $opt{verbose};
+ RRDs::update $rrd, "$this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}" unless $opt{'only-virus-rrd'};
+ RRDs::update $rrd_virus, "$this_minute:$sum{virus}:$sum{spam}" unless $opt{'only-mail-rrd'};
+ if($m > $this_minute+$rrdstep) {
+ for(my $sm=$this_minute+$rrdstep;$sm<$m;$sm+=$rrdstep) {
+ print "update $sm:0:0:0:0:0:0 (SKIP)\n" if $opt{verbose};
+ RRDs::update $rrd, "$sm:0:0:0:0" unless $opt{'only-virus-rrd'};
+ RRDs::update $rrd_virus, "$sm:0:0" unless $opt{'only-mail-rrd'};
+ }
+ }
+ $this_minute = $m;
+ $sum{sent}=0;
+ $sum{received}=0;
+ $sum{bounced}=0;
+ $sum{rejected}=0;
+ $sum{virus}=0;
+ $sum{spam}=0;
+ return 1;
+}
+
+main;
+
+__END__
+
+=head1 NAME
+
+mailgraph.pl - rrdtool frontend for mail statistics
+
+=head1 SYNOPSIS
+
+B<mailgraph> [I<options>...]
+
+ --man show man-page and exit
+ -h, --help display this help and exit
+ --version output version information and exit
+ -h, --help display this help and exit
+ -v, --verbose be verbose about what you do
+ -V, --version output version information and exit
+ -c, --cat causes the logfile to be only read and not monitored
+ -l, --logfile f monitor logfile f instead of /var/log/syslog
+ -t, --logtype t set logfile's type (default: syslog)
+ -y, --year starting year of the log file (default: current year)
+ --host=HOST use only entries for HOST (regexp) in syslog
+ -d, --daemon start in the background
+ --daemon-pid=FILE write PID to FILE instead of /var/run/mailgraph.pid
+ --daemon-rrd=DIR write RRDs to DIR instead of /var/log
+ --daemon-log=FILE write verbose-log to FILE instead of /var/log/mailgraph.log
+ --ignore-localhost ignore mail to/from localhost (used for virus scanner)
+ --ignore-host=HOST ignore mail to/from HOST regexp (used for virus scanner)
+ --only-mail-rrd update only the mail rrd
+ --only-virus-rrd update only the virus rrd
+ --rrd-name=NAME use NAME.rrd and NAME_virus.rrd for the rrd files
+ --rbl-is-spam count rbl rejects as spam
+ --virbl-is-virus count virbl rejects as viruses
+
+=head1 DESCRIPTION
+
+This script does parse syslog and updates the RRD database (mailgraph.rrd) in
+the current directory.
+
+=head2 Log-Types
+
+The following types can be given to --logtype:
+
+=over 10
+
+=item syslog
+
+Traditional "syslog" (default)
+
+=item metalog
+
+Metalog (see http://metalog.sourceforge.net/)
+
+=back
+
+=head1 COPYRIGHT
+
+Copyright (c) 2000-2007 by ETH Zurich
+Copyright (c) 2000-2007 by David Schweikert
+
+=head1 LICENSE
+
+This program is free software; you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation; either version 2 of the License, or
+(at your option) any later version.
+
+This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+
+=head1 AUTHOR
+
+S<David Schweikert E<lt>david@schweikert.chE<gt>>
+
+=cut
+
+# vi: sw=8