Whatever

mdf: A log handler

#!/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 = <CF>) {
			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 = <F>) {
		$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 = <STDIN>) {
	$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();

(2008-01-11)