#!/usr/bin/perl #*********************************************************************** # # mdf-log-handler # # mimedefang-filter mail log real time analyzer # # can be used like this in syslogd.conf: # mail.info |exec /usr/local/sbin/mdf-log-handler.pl # # $Id: mdf-log-handler.pl,v 1.10 2009/01/16 17:54:37 jonas Exp $ # # This program may be distributed under the terms of the GNU General # Public License, Version 2, or (at your option) any later version. # #*********************************************************************** use strict; use Socket; use DBI; use Sys::Syslog; use Date::Parse; use Date::Format; use Text::CSV_XS; my $debug = 0; #*********************************************************************** # Configuration. #*********************************************************************** my %Features; $Features{'Path:SPOOLDIR'} = '/var/spool/MIMEDefang'; # Add setting to config parser my %cfgcfg = (); $cfgcfg{'@i'}{i} = 0; $cfgcfg{'@i'}{c} = 0; sub add_cfg_cfg { my ($c,$v,$d,$t,$f) = @_; $d = '' unless (defined($d)); $t = 's' unless ($t); $f = '' unless ($f); $c =~ s/[-_]+//g; $c = lc($c); $$v = $d; $cfgcfg{'@i'}{c} ++; $cfgcfg{$c}{v} = $v; $cfgcfg{$c}{t} = lc($t); $cfgcfg{$c}{f} = lc($f); $cfgcfg{$c}{i} = $cfgcfg{'@i'}{c}; $cfgcfg{$c}{x} = 0; } use vars qw($WhiteNets); add_cfg_cfg('WhiteNets',\$WhiteNets,'10.0.0.0/255.255.0.0','mn'); use vars qw($debug_loglevel); add_cfg_cfg('DebugLogLevel',\$debug_loglevel,0,'i'); use vars qw($dc_unknown_user $dc_lost_conn $dc_timeout $dc_bad_sender $dc_noop $dc_abuse); add_cfg_cfg('dc_unknown_user',\$dc_unknown_user,0,'b'); add_cfg_cfg('dc_lost_connection',\$dc_lost_conn,0,'b'); add_cfg_cfg('dc_timeout',\$dc_timeout,0,'b'); add_cfg_cfg('dc_bad_sender',\$dc_bad_sender,0,'b'); add_cfg_cfg('dc_noop',\$dc_noop,0,'b'); add_cfg_cfg('dc_abuse',\$dc_abuse,0,'b'); use vars qw($database_spec $database_user $database_pass); add_cfg_cfg('database_spec',\$database_spec,'dbi:SQLite:dbname=%s/filterdata.db','p'); add_cfg_cfg('database_user',\$database_user,'','s'); add_cfg_cfg('database_pass',\$database_pass,'','s'); #*********************************************************************** # Code. #*********************************************************************** # Some initialization openlog("mdlogh","pid","mail"); my $sqldb = undef; my $sqldbd = '?'; read_cfg_cfg(); $debug_loglevel = 999 if ($debug); my $logssv = Text::CSV_XS->new({sep_char=>';',quote_char=>'"',binary=>1}); unlink('/tmp/mdlogh.log') if ($debug && (-f '/tmp/mdlogh.log')); sub logfile { return unless ($debug); return unless (open(LF,'>>','/tmp/mdlogh.log')); foreach my $ll (@_) { $ll =~ s/[\r\n]+$//s; $ll =~ s/^[\r\n]+//s; foreach my $l (split(/[\r\n]+/,$ll)) { print LF sprintf("%s: %s\n",time2str('%Y-%m-%m %H:%M:%S',time()),$l) if ($l ne ''); } } close(LF); } sub logsys { syslog(@_) unless ($debug); print sprintf("%s: %s\n",shift @_,sprintf(shift @_,@_)) if ($debug); } sub md_syslog { logsys(shift @_,'%s',@_); } sub log_msg { logsys('notice',@_); } sub debug_log { my $level = shift; my $msg = shift; logsys($level<0?'err':'info',"MDLDBG: $msg",@_) unless ($level>$debug_loglevel); } # Get a file path name sub get_file_path_name { my $f = shift; return $f if ($f =~ /[\/\\]/); foreach my $d ((sprintf('%s/%s',$Features{'Path:CONFDIR'},'filter'),$Features{'Path:CONFDIR'}, '/usr/local/etc/mimedefang/filter','/etc/mimedefang/filter', '/usr/local/etc/mimedefang','/etc/mimedefang', '/usr/local/etc/mail','/etc/mail', )) { if ($d =~ /mimedefang/) { return "$d/$f" if (-f "$d/$f"); return "$d/mimedefang-$f" if (-f "$d/mimedefang-$f"); } else { return "$d/mimedefang-$f" if (-f "$d/mimedefang-$f"); return "$d/$f" if (-f "$d/$f"); } } return ''; } # Read file into array sub read_a_file { my ($fn,$match,$noclean) = @_; my @l = (); if ($fn && (-f $fn) && (open(CF,'<',$fn))) { while (my $l = ) { unless ($noclean) { next if ($l =~ /^[;#]/); next if ($l =~ /^[\s\r\n]*$/s); next if ($match && $l !~ /^$match/i); } push @l, $l; } close(CF); } return \@l; } # read the configuration file sub read_cfg_cfg { my $cfgfn = get_file_path_name('filter.conf'); die('No filter config!') unless ($cfgfn); die('Cannot read filter config!') unless (open(F,'<',$cfgfn)); md_syslog('info',"Filter config: $cfgfn"); while (my $l = ) { $l =~ s/[\r\n]+//gs; next unless ($l); next if ($l =~ /^\s*[#;]/); if ($l =~ /^\$(\S+):\s*(.*?)\s*\$?$/) { md_syslog('info',"Config $1: $2"); } elsif ($l =~ /^\s*(\S+)\s*?[\s:=]\s*(\S.*?)\s*$/) { my $c = lc($1); my $v = $2; $c =~ s/[-_]+//g; next unless ($c); next if ($c =~ /^\@/); if (defined($cfgcfg{$c})) { if ($cfgcfg{$c}{x} && $cfgcfg{$c}{t} =~ /^m/i) { ${$cfgcfg{$c}{v}} .= ';' if (${$cfgcfg{$c}{v}} ne ''); ${$cfgcfg{$c}{v}} .= $v; } elsif (($cfgcfg{$c}{t} eq 'l' || $cfgcfg{$c}{t} =~ /^m/i) && $v =~ /^\s*\@\{\s*(.*?)\s*\}\s*$/) { my $lst = read_a_file(get_file_path_name($1,'.*\S')); if ($cfgcfg{$c}{t} eq 'l') { ${$cfgcfg{$c}{v}} = ''; foreach my $le (@{$lst}) { $le =~ s/[\r\n]+//gs; unless ($le =~ /\\[.@]/) { $le =~ s/\./\\./g; $le =~ s/\@/\\\@/g; } next unless ($le); ${$cfgcfg{$c}{v}} .= '|' if (${$cfgcfg{$c}{v}} ne ''); ${$cfgcfg{$c}{v}} .= $le } } elsif ($cfgcfg{$c}{t} =~ /^m/i) { foreach my $le (@{$lst}) { $le =~ s/[\r\n]+//gs; ${$cfgcfg{$c}{v}} .= ';' if (${$cfgcfg{$c}{v}} ne ''); ${$cfgcfg{$c}{v}} .= $le; } } } else { ${$cfgcfg{$c}{v}} = $v; $cfgcfg{$c}{x} = 1; } } } } close(F); #return; my @ck = sort { $cfgcfg{$a}{i} <=> $cfgcfg{$b}{i} } keys %cfgcfg; foreach my $c (@ck) { next if ($c =~ /^\@/); ${$cfgcfg{$c}{v}} = ${$cfgcfg{$cfgcfg{$c}{f}}{v}} if ($cfgcfg{$c}{f} && !${$cfgcfg{$c}{v}}); } foreach my $c (@ck) { next if ($c =~ /^\@/); if ($cfgcfg{$c}{t} eq 'l') { ${$cfgcfg{$c}{v}} = '' unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} =~ s/\s//g; unless (${$cfgcfg{$c}{v}} =~ /\\[.@]/) { ${$cfgcfg{$c}{v}} =~ s/\./\\./g; ${$cfgcfg{$c}{v}} =~ s/\@/\\\@/g; } ${$cfgcfg{$c}{v}} =~ s/,/\|/g if (${$cfgcfg{$c}{v}} !~ /[\(\)\|\{\}]/ && ${$cfgcfg{$c}{v}} =~ /\,/); ${$cfgcfg{$c}{v}} = sprintf('(%s)',${$cfgcfg{$c}{v}}) if (${$cfgcfg{$c}{v}} !~ /[\(\)]/ && ${$cfgcfg{$c}{v}} =~ /\|/); } elsif ($cfgcfg{$c}{t} eq 'a') { ${$cfgcfg{$c}{v}} = '' unless (${$cfgcfg{$c}{v}}); next unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} .= '@'.${$cfgcfg{'myfilterhostname'}{v}} if (${$cfgcfg{$c}{v}} =~ /^[^@]+$/); } elsif ($cfgcfg{$c}{t} eq 't') { ${$cfgcfg{$c}{v}} = '' unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} = sprintf(${$cfgcfg{$c}{v}},${$cfgcfg{'myfilterhostname'}{v}}); ${$cfgcfg{$c}{v}} =~ s/[\r\n]*$/\n\n/s; } elsif ($cfgcfg{$c}{t} eq 'b') { ${$cfgcfg{$c}{v}} = 0 unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} = 0 if (${$cfgcfg{$c}{v}} =~ /^\s*(false|no?|off|0+)\s*$/); ${$cfgcfg{$c}{v}} = 1 if (${$cfgcfg{$c}{v}}); } elsif ($cfgcfg{$c}{t} eq 'i') { ${$cfgcfg{$c}{v}} = 0 unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} = eval(${$cfgcfg{$c}{v}}); } elsif ($cfgcfg{$c}{t} eq 'ps') { ${$cfgcfg{$c}{v}} = $Features{'Path:SPOOLDIR'} unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} = sprintf('%s/%s',$Features{'Path:SPOOLDIR'},${$cfgcfg{$c}{v}}) if (${$cfgcfg{$c}{v}} !~ /[\/\\]/); } elsif ($cfgcfg{$c}{t} eq 'pc') { ${$cfgcfg{$c}{v}} = $Features{'Path:CONFDIR'} unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} = sprintf('%s/%s',$Features{'Path:CONFDIR'},${$cfgcfg{$c}{v}}) if (${$cfgcfg{$c}{v}} !~ /[\/\\]/); } elsif ($cfgcfg{$c}{t} eq 'p') { ${$cfgcfg{$c}{v}} = $Features{'Path:CONFDIR'} unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} = sprintf(${$cfgcfg{$c}{v}},$Features{'Path:SPOOLDIR'},$Features{'Path:CONFDIR'}); } elsif ($cfgcfg{$c}{t} eq 'mpsm') { my @pil = split(/\s*;\s*/,${$cfgcfg{$c}{v}}); for (my $i=0;$i<@pil;$i++) { my ($fn,$ft,$fo,$x) = split(/\s*,\s*/,$pil[$i]); $fn = sprintf('%s/%s',${$cfgcfg{'sendmailconfig'}{v}},$fn) if ($fn !~ /[\/\\]/); $fo = '?' unless ($fo); unless ($ft) { if ($fn =~ /table/i) { $ft = 'table' } else { $ft = 'list'; } } $pil[$i] = join(',',$fn,lc($ft),lc($fo)); } ${$cfgcfg{$c}{v}} = join(';',@pil); } elsif ($cfgcfg{$c}{t} eq 'mbs') { ${$cfgcfg{$c}{v}} = 0 unless (${$cfgcfg{$c}{v}}); ${$cfgcfg{$c}{v}} = 0 if (${$cfgcfg{$c}{v}} =~ /^\s*(false|no|n|off|0+)\s*$/); ${$cfgcfg{$c}{v}} = 1 if (${$cfgcfg{$c}{v}} =~ /^\s*(true|yes|y|on|\d*[1-9]\d*)\s*$/); } else { ${$cfgcfg{$c}{v}} = '' unless (${$cfgcfg{$c}{v}}); } } if ($database_spec =~ /sqlite/i) { $sqldbd = 'L'; } elsif ($database_spec =~ /mysql/i) { $sqldbd = 'M'; } } # Strip strings sub address_strip_nc($) { my ($a) = @_; $a = '' unless (defined($a)); $a =~ s/[\r\n]+//gs; $a =~ s/^[<\[]//; $a =~ s/[>\]]$//; #$a =~ s/^(.*\@)?\s*(\S+)(\s.*)?$/$1$2/; return $a; } sub address_strip($) { my ($a) = @_; return lc(address_strip_nc($a)); } # Checks against a small internal list sub check_ip_in_list($$) { my $ip = address_strip(shift); return 0 unless ($ip && $ip =~ /^\d+\.\d+\.\d+\.\d+$/); my $addr = inet_aton($ip); return 0 unless ($addr); foreach my $lst (@_) { next unless ($lst); foreach my $net (split(/;/,$lst)) { $net =~ s/\s+//g; next unless ($net); my ($na_s,$nm_s) = split(/\//,$net); $nm_s = '255.255.255.255' unless ($nm_s); my $na = inet_aton($na_s); my $nm = inet_aton($nm_s); next unless ($na && $nm); return 1 if (($addr & $nm) eq ($na & $nm)); } } return 0; } # Checks against a small internal IP address white list sub check_internal_whitelist($) { my($ip) = @_; return check_ip_in_list($ip,"127.0.0.1/255.255.255.255;$WhiteNets"); } # Checks if IP is a black net sub check_black_nets($) { my($ip) = @_; return check_ip_in_list($ip,'10.0.0.0/255.0.0.0;172.16.0.0/255.240.0.0;192.168.0.0/255.255.0.0;127.0.0.0/255.255.255.0'); } #*********************************************************************** # SQL. #*********************************************************************** sub sql_translate { my ($cmd) = @_; if ($sqldbd eq 'M') { $cmd =~ s/^INSERT OR IGNORE /INSERT IGNORE /; } return $cmd; } sub sql_do_commands { for (my $i=0;$i<@_;$i++) { my $cmd = sql_translate($_[$i]); #debug_log(0,'sql_do_commands: %s ',$cmd); return 0 unless ($cmd && defined($sqldb->do($cmd))); } return 1; } sub sql_exec_commands { for (my $i=0;$i<@_;$i++) { my @pars = @{$_[$i]}; my $cmd = sql_translate(shift @pars); return 0 unless ($cmd); my $st = $sqldb->prepare_cached($cmd); return 0 unless ($st); #debug_log(7,'sql_exec_commands: %s : %s',$cmd,join(' | ',@pars)); my $res = $st->execute(@pars); $st->finish; return 0 unless ($res); } return 1; } sub sql_disconnect { $sqldb->disconnect() if ($sqldb); $sqldb = undef; } sub sql_connect { my $f = shift; sql_disconnect() if ($f); if ($sqldb) { return 1 if ($sqldbd eq 'L'); sql_disconnect() unless (eval { $sqldb->ping }); return 1 if ($sqldb); debug_log(0,'SQL Connect Old (%s)',$sqldbd); } $sqldb = DBI->connect($database_spec,$database_user,$database_pass,{RaiseError=>0}); # (AutoCommit=>$sqldbd ne 'M') ?? unless ($sqldb) { debug_log(-1,'SQL Connect Failed (%s) %s',$sqldbd,$DBI::errstr); return 0; } if ($sqldbd eq 'L') { sql_do_commands('PRAGMA SYNCHRONOUS=OFF'); } #debug_log(0,'SQL Connect Ok (%s)',$sqldbd); return 1; } sub sql_quote { my $s = shift; sql_connect() unless ($sqldb); return $s unless ($sqldb); return $sqldb->quote($s); } sub sql_quotes { my @r = (); while (my $s = shift @_) { push @r, sql_quote($s); } return @r; } sub sql_do { return 0 unless (sql_connect()); my $ok = sql_do_commands(@_); return $ok if ($sqldb->{AutoCommit}); if ($ok) { #debug_log(0,'sql_do: commit'); return $sqldb->commit; } debug_log(-1,'sql_do: rollback'); $sqldb->rollback; return 0; } sub sql_execute_multi { return 0 unless (sql_connect()); my $ok = sql_exec_commands(@_); return $ok if ($sqldb->{AutoCommit}); return $sqldb->commit if ($ok); debug_log(-1,'sql_exec: rollback'); $sqldb->rollback; return 0; } sub sql_execute { return sql_execute_multi(\@_); } sub sql_select { my $cmd = shift; return undef unless (sql_connect()); return undef unless ($cmd); my $st; if (@_) { #debug_log(7,'sql_select: %s : %s',$cmd,join(' | ',@_)); $st = $sqldb->prepare_cached($cmd); $st->execute(@_) if ($st); } else { #debug_log(7,'sql_select: %s',$cmd); $st = $sqldb->prepare_cached("$cmd"); $st->execute if ($st); } debug_log(-1,'sql prepare error: %s',$cmd) unless ($st); return $st; } sub sql_select_one_row { my $st = sql_select(@_); return undef unless ($st); my @res = $st->fetchrow_array; $st->finish; return \@res; } sub sql_select_one { my $res = sql_select_one_row(@_); return undef unless ($res && @{$res}); return $res->[0]; } #*********************************************************************** # Application. #*********************************************************************** my $tim; my $mid; my $clnd = time(); my %mids = (); my %rads = (); my %rrls = (); sub nopar { my $str = join(' ',@_); $str =~ s/\(.*?\)//; $str =~ s/^\s+//; $str =~ s/\s+$//; $str =~ s/\s+/ /g; return $str; } sub clean_data { return unless (time()-$clnd > 30*60); $clnd = time(); my @dl = (); while (my ($mid, $mit) = each %mids) { push @dl, $mid if ($clnd-$mit > 2*60*60); } delete $mids{@dl} if (@dl); @dl = (); while (my ($hst, $inf) = each %rads) { push @dl, $hst if ($clnd-$inf->{t} > 60*60); } delete $rads{@dl} if (@dl); @dl = (); while (my ($rly, $rrt) = each %rrls) { push @dl, $rly if ($clnd-$rrt > 10*60); } delete $rrls{@dl} if (@dl); } sub dump_data { return unless ($debug); return unless (open(LF,'>>','/tmp/mdlogh.dat')); while (my ($mid, $mit) = each %mids) { print LF "M:$mid\n"; } while (my ($hst, $inf) = each %rads) { print LF "$hst=$inf->{h}\n"; } while (my ($rly, $rrt) = each %rrls) { print LF "R:$rly\n"; } close(LF); } sub timnow { my $now = $tim ? str2time($tim) : 0; return $now ? $now : time(); } sub xaddr { my ($xxx) = @_; return $rads{$xxx}->{h} if ($rads{$xxx}); return 0; } sub haddr { my ($host) = @_; if ($host =~ /^\s*\[(\d+\.\d+\.\d+\.\d+)\]/) { return $1; } return xaddr("h:$host"); } sub maddr { my ($mid) = @_; return xaddr("m:$mid"); } sub dict_add { my $why = shift; my $addr = shift; return unless ($addr); return if (check_internal_whitelist($addr) || check_black_nets($addr)); my $time = timnow(); return if ($why eq 'noop' && $rrls{$addr} && $time-$rrls{$addr}<10*60); $mids{$mid} = $time; $rrls{$addr} = $time unless ($why eq 'noop'); sql_execute('INSERT INTO dictionary (dc_stamp,dc_host) VALUES (?,?)',$time,$addr) unless ($debug); my @el = ($addr); while (@_) { my $e = shift @_; next if ($e =~ /^\s*$/); $e =~ s/^\s+//; $e =~ s/\s+$//; next if ($e =~ /^\[?$addr\]?$/); push @el, $e; } log_msg('%s: dc_%s %s %s',$mid?$mid:'?',$why,time2str('%Y-%m-%d %H:%M:%S',$time),join(' ',@el)); } sub sm_rargs { my @al = (); foreach my $l (@_) { while ($l =~ /^.*?(\S+)=(\S.*?), (.*)$/) { my $fn = $1; my $fv = $2; $l = $3; push @al, $fv if ($fn =~ /^arg\d+$/); } } return @al; } sub handle_sendmail { $mid = shift; my $log = shift; if ($log =~ /\srelay=(\S+)\s+\[(\d+\.\d+\.\d+\.\d+)\](?:,|$)/) { my ($addr,$host) = ($2,$1); return if (check_internal_whitelist($addr) || check_black_nets($addr)); my $inf = {t=>timnow(),h=>$addr}; $rads{"h:$host"} = $inf; $rads{"m:$mid"} = $inf if ($mid && $mid !~ /^.$/); } return if ($mid && $mids{$mid}); my $did = 1; if ($log =~ /^lost (\S+) channel from (\S+\s+)?\[(\d+\.\d+\.\d+\.\d+)\]/) { return dict_add('lost_conn',$3,$2,$1) if ($debug || $dc_lost_conn); } elsif ($log =~ /^(\S+): (.*?): Connection reset by (\S+)/) { return dict_add('lost_conn',haddr($3),$3,$1,$2) if ($debug || $dc_lost_conn); } elsif ($log =~ /^timeout waiting for input from (\S+) during (.*)$/) { return dict_add('timeout',haddr($1),$1,$2) if ($debug || $dc_timeout); } elsif ($log =~ /^(\S+\s+)?\[(\d+\.\d+\.\d+\.\d+)\]\s+(?:\([^\)]*\)\s+)?did not issue MAIL\/EXPN\/VRFY\/ETRN/) { return dict_add('noop',$2,$1) if ($debug || $dc_noop); } elsif ($log =~ /^(\S+)\s+(?:\([^\)]*\)\s+)?did not issue MAIL\/EXPN\/VRFY\/ETRN/) { return dict_add('noop',haddr($1),$1) if ($debug || $dc_noop); } elsif ($log =~ /^(\S+\s+)?\[(\d+\.\d+\.\d+\.\d+)\]:\s+.*?, throttling.$/) { return dict_add('abuse',$2,$1,'throttle') if ($debug || $dc_abuse); } elsif ($log =~ /^rejecting commands from (\S+\s+)?\[(\d+\.\d+\.\d+\.\d+)\]\s+.*pre-greeting/) { return dict_add('abuse',$2,$1,'greeting') if ($debug || $dc_abuse); } elsif ($log =~ /^POSSIBLE ATTACK\s(?:.+?\s)?from\s(\S+\s+)?\[(\d+\.\d+\.\d+\.\d+)\]:/) { return dict_add('abuse',$2,$1,'attack') if ($debug || $dc_abuse); } elsif ($log =~ /^POSSIBLE ATTACK\s(?:.+?\s)?from\s(\S+):/) { return dict_add('abuse',haddr($1),$1,'attack') if ($debug || $dc_abuse); } elsif ($log =~ /^(\S+):\s+(unexpected close|\S+ timeout) .*? from (\S+\s+)?\[(\d+\.\d+\.\d+\.\d+)\]/) { return dict_add('lost_conn',$4,$3,$1,$2) if ($debug || $dc_lost_conn); } elsif ($log =~ /^(\S+):\s+(unexpected close|\S+ timeout) .*? from (\S+),/) { return dict_add('lost_conn',haddr($3),$3,$1,$2) if ($debug || $dc_lost_conn); } elsif ($log =~ /^(\S+): premature EOM: Operation (.*?) with (\S+)$/) { return dict_add('lost_conn',haddr($3),$3,$1,$2) if ($debug || $dc_lost_conn); } elsif ($log =~ /(\S+)\.\.\. (User unknown|Invalid.*address|Unbalanced .?[<>].?)$/) { return dict_add('unknown_user',maddr($mid),$mid,$1,lc($2)) if ($debug || $dc_unknown_user); } elsif ($log =~ /^(.*?)\s+\(possible attack\)$/) { return dict_add('abuse',maddr($mid),$mid,nopar($1)) if ($debug || $dc_abuse); } elsif ($log =~ /^ruleset=(\S+),.*?\srelay=(\S+\s+)?\[(\d+\.\d+\.\d+\.\d+)\]/) { my ($rn,$hn,$ia) = ($1,$2,$3); if ($rn eq 'check_mail') { return dict_add('bad_sender',$ia,$hn,sm_rargs($log)) if ($debug || $dc_bad_sender); } elsif ($rn eq 'check_rcpt') { if ($log =~ /\sRelaying denied$/) { return dict_add('abuse',$ia,$hn,sm_rargs($log)) if ($debug || $dc_abuse); } else { return dict_add('unknown_user',$ia,$hn,sm_rargs($log)) if ($debug || $dc_unknown_user); } } elsif ($rn eq 'check_relay') { if ($log =~ /, reject=.*(limit exceeded|Too many)/) { return dict_add('abuse',$ia,$hn,sm_rargs($log),'limit') if ($debug || $dc_abuse); } else { $did = 0; } } else { $did = 0; } } elsif ($log =~ /^SYSERR\S+: (\S+): (.*?) on connection from (.*)$/) { my ($dw,$es,$aa) = ($1,$2,$3); my ($hn,$ia); if ($aa =~ /^(\S+)\s+\[(\d+\.\d+\.\d+\.\d+)\]/) { $hn = $1; $ia = $2; } elsif ($aa =~ /^\[(\d+\.\d+\.\d+\.\d+)\]/) { $ia = $1; } elsif ($aa =~ /^(\S+),/) { $hn = $1; $ia = haddr($hn); } if ($ia) { if ($es =~ /^(.*) timeout$/) { return dict_add('timeout',$ia,$hn,$dw,$1) if ($debug || $dc_timeout); } elsif ($es eq 'I/O error') { return dict_add('lost_conn',$ia,$hn,$dw,$es) if ($debug || $dc_lost_conn); } else { $did = 0; } } else { $did = 0; } } else { $did = 0; } logfile("sm $mid $log") unless ($did || $log =~ /^(?:Milter[:\s]|to=|from=|discarded|[A-Za-z0-9]{14,14}: DSN:|collect: premature EOM: unexpected close)/); } sub handle_mdstats { $mid = shift; my $what = shift; my $log = shift; if ($what eq 'info' && $logssv->parse($log)) { my @log = $logssv->fields(); shift @log; if (@log && $log[0] =~ /^[\[<]?(\d+\.\d+\.\d+\.\d+)[\]>]?$/) { my $inf = {t=>timnow(),h=>$log[0]}; $rads{"m:$mid"} = $inf if ($mid && $mid !~ /^.$/); $rads{"h:$log[1]"} = $inf if ($#log && $log[1]); } } elsif (($what eq 'tempfail' || $what eq 'reject') && $logssv->parse($log)) { my @log = $logssv->fields(); shift @log; shift @log; if (@log && $log[0] =~ /^[\[<]?(\d+\.\d+\.\d+\.\d+)[\]>]?$/) { my $addr = $1; $rads{"m:$mid"} = {t=>timnow(),h=>$addr} if ($mid && $mid !~ /^.$/); $rrls{$addr} = timnow() unless (check_internal_whitelist($addr) || check_black_nets($addr)); return; } } elsif ($what eq 'relay_info' && $logssv->parse($log)) { my @log = $logssv->fields(); return if ($#log<1 || $log[1] =~ /^\[$log[0]\]$/); return if (check_internal_whitelist($log[0]) || check_black_nets($log[0])); my $inf = {t=>timnow(),h=>$log[0]}; $rads{"h:$log[1]"} = $inf; $rads{"m:$mid"} = $inf if ($mid && $mid !~ /^.$/); return; } logfile("md $what $mid $log") unless ($what =~ /^(?:modified|quarantine|greylist|deliver|.*info|time|size|.*_.*|stream|discard)$/); } my $start = time(); while (my $l = ) { $l =~ s/[\s\r\n]+$//s; $tim = ''; if ($l =~ /^([A-Z][a-z][a-z]\s+\d+\s+\d+:\d+:\d+)\s+(.*)$/) { $tim = $1; $l = $2; } $l =~ s/^\S+\s+//; next if ($l =~ /^mdlogh\s/); if ($l =~ /^mimedefang.pl(?:\[\S+\])?:\s+(\S+): mdstats;([^;]+);(.*)$/) { handle_mdstats($1,$2,$3); } elsif ($l =~ /^(?:sendmail|sm-\S+)(?:\[\S+\])?:\s+([A-Za-z0-9]{14,14}):\s+(.*?)$/) { handle_sendmail($1,$2); } elsif ($l =~ /^(?:sendmail|sm-\S+)(?:\[\S+\])?:\s+(.*?)$/) { handle_sendmail('',$1); } clean_data(); } sql_disconnect(); dump_data();