summaryrefslogtreecommitdiff
path: root/mailgraph.pl
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 /mailgraph.pl
Imported Upstream version 1.14upstream/1.14
Diffstat (limited to 'mailgraph.pl')
-rwxr-xr-xmailgraph.pl976
1 files changed, 976 insertions, 0 deletions
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