diff options
author | Julien Valroff <julien@kirya.net> | 2010-01-04 19:39:20 +0100 |
---|---|---|
committer | Julien Valroff <julien@kirya.net> | 2010-01-04 19:39:20 +0100 |
commit | d906efbb1aacfebd9931d49028f6240a667bae14 (patch) | |
tree | 937aad588d7076854da1f31f779c8e7ab99557d8 |
Imported Upstream version 1.14upstream/1.14
-rw-r--r-- | CHANGES | 251 | ||||
-rw-r--r-- | COPYING | 339 | ||||
-rw-r--r-- | README | 85 | ||||
-rw-r--r-- | mailgraph-init | 42 | ||||
-rwxr-xr-x | mailgraph.cgi | 244 | ||||
-rw-r--r-- | mailgraph.css | 21 | ||||
-rwxr-xr-x | mailgraph.pl | 976 |
7 files changed, 1958 insertions, 0 deletions
@@ -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 @@ -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. @@ -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> </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 |