Skip to content

Instantly share code, notes, and snippets.

@mdklapwijk
Last active August 20, 2018 19:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mdklapwijk/4f8d2fc39f09f4aa615cbf8ffae0379a to your computer and use it in GitHub Desktop.
Save mdklapwijk/4f8d2fc39f09f4aa615cbf8ffae0379a to your computer and use it in GitHub Desktop.
#!/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
# modifications to handle rsyslog high precision format
# copyright (c) 2018 by M.D. Klapwijk
# 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
my ($year, $mon, $day, $hour, $min, $sec, $host, $text);
if($self->{type} eq 'rsyslog') {
($year, $mon, $day, $hour, $min, $sec, $host, $text) = $str =~ /^
(\d+)-(\d+)-(\d+)T(\d+):(\d+):(\d+)\S+ # datetime
\s+
(\S+) # host
\s+
(.*) # text
$/x or do
{
warn "WARNING: line not in high precision rsyslog format: $str";
next line;
};
$mon--;
$self->{year}=$year;
}
else {
my($montxt);
($mon, $day, $hour, $min, $sec, $host, $text) = $str =~ /^
(\S{3})\s+(\d+) # date
\s
(\d+):(\d+):(\d+) # time
(?:\s<\w+\.\w+>)? # FreeBSD's verbose-mode
\s
([-\w\.\@:]+) # host
\s+
(?:\[LOG_[A-Z]+\]\s+)? # FreeBSD
(.*) # text
$/x or do
{
warn "WARNING: line not in syslog format: $str";
next line;
};
$mon = $months_map{$montxt};
defined $mon or croak "unknown month $montxt\n";
$self->_year_increment($mon);
}
# convert to unix time
my $time = $self->str2time($sec,$min,$hour,$day,$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;
}
}
# 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' || $self->{type} eq 'rsyslog') {
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/lib/mailgraph';
# global variables
my $logfile;
my $rrd = "mailgraph.rrd";
my $rrd_virus = "mailgraph_virus.rrd";
my $rrd_greylist = "mailgraph_greylist.rrd";
my $year;
my $this_minute;
my %sum = ( sent => 0, received => 0, bounced => 0, rejected => 0, virus => 0, spam => 0, greylisted => 0, delayed => 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 event_greylisted($);
sub event_delayed($);
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/lib/mailgraph\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 " --no-mail-rrd no update mail rrd\n";
print " --no-virus-rrd no update virus rrd\n";
print " --no-greylist-rrd no update greylist 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@',
'no-mail-rrd', 'no-virus-rrd', 'no-greylist-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};
$rrd_greylist = $opt{rrd_name}."_greylist.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{'no-mail-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{'no-virus-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;
}
# greylist rrd
if(! -f $rrd_greylist and ! $opt{'no-greylist-rrd'}) {
RRDs::create($rrd_greylist, '--start', $m, '--step', $rrdstep,
'DS:greylisted:ABSOLUTE:'.($rrdstep*2).':0:U',
'DS:delayed: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_greylist and ! defined $rrd_greylist) {
$this_minute = RRDs::last($rrd_greylist) + $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' || $prog eq 'postscreen') {
if($text =~ /^(?:[\dA-F]+|[\dB-DF-HJ-NP-TV-Zb-df-hj-np-tv-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 =~ /^(?:[\dA-F]+: |[\dB-DF-HJ-NP-TV-Zb-df-hj-np-tv-z]+: |NOQUEUE: )?reject: .*: 554.* blocked using virbl.dnsbl.bit.nl/) {
event($time, 'virus');
}
elsif($opt{'rbl-is-spam'} and $text =~ /^(?:[\dA-F]+: |[\dB-DF-HJ-NP-TV-Zb-df-hj-np-tv-z]+: |NOQUEUE: )?reject: .*: 554.* blocked using/) {
event($time, 'spam');
}
elsif($text =~ /Greylisted/) {
event($time, 'greylisted');
}
elsif($text =~ /^(?:[\dA-F]+: |[\dB-DF-HJ-NP-TV-Zb-df-hj-np-tv-z]+: |NOQUEUE: )?reject: /) {
event($time, 'rejected');
}
elsif($text =~ /^(?:[\dA-F]+: |[\dB-DF-HJ-NP-TV-Zb-df-hj-np-tv-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 =~ /(?:[\dA-F]+|[\dB-DF-HJ-NP-TV-Zb-df-hj-np-tv-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($text =~ /infected message from/) {
event($time, 'virus');
}
}
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');
}
elsif($text =~ /Message.*infected by/) {
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');
}
}
elsif($prog eq 'postgrey') {
# Old versions (up to 1.27)
if($text =~ /delayed [0-9]+ seconds: client/) {
event($time, 'delayed');
}
# New versions (from 1.28)
if($text =~ /delay=[0-9]+/) {
event($time, 'delayed');
}
}
elsif($prog eq 'grossd') {
if($text =~ /a\=greylist/) {
event($time, 'greylisted');
}
}
}
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}:$sum{greylisted}:$sum{delayed}\n" if $opt{verbose};
RRDs::update $rrd, "$this_minute:$sum{sent}:$sum{received}:$sum{bounced}:$sum{rejected}" unless $opt{'no-mail-rrd'};
RRDs::update $rrd_virus, "$this_minute:$sum{virus}:$sum{spam}" unless $opt{'no-virus-rrd'};
RRDs::update $rrd_greylist, "$this_minute:$sum{greylisted}:$sum{delayed}" unless $opt{'no-greylist-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:0:0 (SKIP)\n" if $opt{verbose};
RRDs::update $rrd, "$sm:0:0:0:0" unless $opt{'no-mail-rrd'};
RRDs::update $rrd_virus, "$sm:0:0" unless $opt{'no-virus-rrd'};
RRDs::update $rrd_greylist, "$sm:0:0" unless $opt{'no-greylist-rrd'};
}
}
$this_minute = $m;
$sum{sent}=0;
$sum{received}=0;
$sum{bounced}=0;
$sum{rejected}=0;
$sum{virus}=0;
$sum{spam}=0;
$sum{greylisted}=0;
$sum{delayed}=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)
--no-mail-rrd do not update mail rrd
--no-virus-rrd do not update virus rrd
--no-greylist-rrd do not update greylist 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 rsyslog
High precision format "rsyslog"
=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
Copyright (c) 2018 by M.D. Klapwijk
=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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment