#!/usr/bin/perl #*********************************************************************** # # mdf-report # # mimedefang-filter reporter # # $Id: mdf-report.pl,v 1.22 2009/04/03 18:35:59 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 MIME::Head; use MIME::Words; use Encode; use HTML::Entities; use CGI; use CGI::Pretty; use MIME::Lite; use Email::MessageID; use Net::SMTP; use Fortune; use CSS::Tiny; use DBI; use Date::Format; use Text::CSV_XS; #*********************************************************************** # Config. #*********************************************************************** my $debug = 0; my $quiet = 0; my $sqldb; my $sqldbd = '?'; my $mailto = ''; my $forget = 0; my $FilterRevision = '$Revision: 1.22 $'; my $FilterUtilVers = '?'; if ('$Id: mdf-report.pl,v 1.22 2009/04/03 18:35:59 jonas Exp $' =~ /^\S+?:\s(\S+?),v\s([.\d]+)\s/) { $FilterUtilVers = "$1 $2"; } my %Features; $Features{'Path:QUARANTINEDIR'} = '/var/spool/MD-Quarantine'; # 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($AdminAddress $MyFilterHostName); add_cfg_cfg('AdminAddress',\$AdminAddress,'postmaster','a'); add_cfg_cfg('MyFilterHostName',\$MyFilterHostName,'host.domain.tld','s'); # md: $DaemonAddress use vars qw($DaemonAddress $MailResultMailer); add_cfg_cfg('DaemonAddress',\$DaemonAddress,'mailer-daemon','a'); add_cfg_cfg('MailResultMailer',\$MailResultMailer,'127.0.0.1:25','s'); use vars qw ($hilo_entries); add_cfg_cfg('hilo_entries',\$hilo_entries,0,'i'); 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'); use vars qw($silly_fortunes $silly_oneliners); add_cfg_cfg('silly_fortunes',\$silly_fortunes,''); add_cfg_cfg('silly_oneliners',\$silly_oneliners,''); use vars qw($quarantine_url); add_cfg_cfg('quarantine_url',\$quarantine_url,'','s'); # Get a file path name sub get_file_path_name { my $f = shift; return $f if ($f =~ /[\/\\]/); foreach my $d (('/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 the configuration file sub read_cfg_params { my @pp = @ARGV; @ARGV = (); foreach my $p (@pp) { $p =~ s/^-+//; if ($p =~ /^d(ata)?b(ase)?[-_]?s(pec|pecification)?[:=]\s*(.*?)\s*$/i) { $database_spec = $4; } elsif ($p =~ /^d(ata)?b(ase)?[-_]?u(sr|ser)?[:=]\s*(.*?)\s*$/i) { $database_user = $4; } elsif ($p =~ /^d(ata)?b(ase)?[-_]?p(ass|assword|wd)?[:=]\s*(.*?)\s*$/i) { $database_pass = $4; } else { push @ARGV, $p; } } } sub read_cfg_cfg { my $cfgfn = get_file_path_name('filter.conf'); if ($cfgfn) { die('No filter config!') unless ($cfgfn); $Features{'Path:CONFDIR'} = $cfgfn; $Features{'Path:CONFDIR'} =~ s/(\/filter)?\/[^\/]*$//; 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*?[\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; } else { ${$cfgcfg{$c}{v}} = $v; $cfgcfg{$c}{x} = 1; } } } } close(F); #return; } else { $Features{'Path:CONFDIR'} = '.'; } read_cfg_params(); $Features{'Path:SPOOLDIR'} = '.' unless ($Features{'Path:SPOOLDIR'} && (-d $Features{'Path:SPOOLDIR'})); $Features{'Path:QUARANTINEDIR'} = '.' unless ($Features{'Path:QUARANTINEDIR'} && (-d $Features{'Path:QUARANTINEDIR'})); 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; ${$cfgcfg{$c}{v}} =~ s/\./\\./g if (${$cfgcfg{$c}{v}} !~ /\\\./); ${$cfgcfg{$c}{v}} =~ s/\@/\\\@/g if (${$cfgcfg{$c}{v}} !~ /\\\@/); ${$cfgcfg{$c}{v}} =~ s/\./\\./g if (${$cfgcfg{$c}{v}} !~ /\\\./); ${$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'}); } 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'; } } #*********************************************************************** # SQL. #*********************************************************************** my $sqlr; sub sql_disconnect { $sqldb->disconnect() if ($sqldb); $sqldb = undef; } sub sql_connect { return 1 if ($sqldb); $sqldb = DBI->connect($database_spec,$database_user,$database_pass,{RaiseError=>0}); return 0 unless ($sqldb); return 1; } sub sql_translate { my ($cmd) = @_; $cmd =~ s/ LIMIT -1$//; return $cmd; } 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); my $res = $st->execute(@pars); $st->finish; return 0 unless ($res); } return 1; } 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_query { my $cmd = sql_translate(shift); return undef unless (sql_connect() && $cmd); my $st = $sqldb->prepare($cmd); return undef unless ($st); $st->execute(@_); return $st; } sub sql_select_one_row { my $st = sql_query(@_); 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]; } #*********************************************************************** # Code. #*********************************************************************** sub fwords { my $ws = join(' ',@_); $ws =~ s/_/ /g; $ws =~ s/\b([a-z])/uc($1)/ge; return $ws; } # create file sub create_file { my ($fn) = @_; return 1 if (-f $fn); return 0 unless (open(NF,'>>',$fn)); close(NF); return 1; } my @qdir_reported = (); my %qdir_reported = (); sub mark_qdir_reported { return unless (@qdir_reported); unless ($forget) { print "\tqdir\n" unless ($quiet); return if ($debug); for (my $i=0;$i<@qdir_reported;$i++) { next if $qdir_reported{$qdir_reported[$i]}; $qdir_reported{$qdir_reported[$i]} = create_file($qdir_reported[$i]); } } @qdir_reported = (); } my @logs_reported = (); my %logs_reported = (); sub mark_logs_reported { return unless (@logs_reported); unless ($forget) { print "\tlogs\n" unless ($quiet); return if ($debug); my @dels = ([]); for (my $i=0;$i<@logs_reported;$i++) { next if $qdir_reported{$logs_reported[$i]}; push @dels, [] if (@{$dels[$#dels]} > 512); push @{$dels[$#dels]}, $logs_reported[$i]; } foreach my $del (@dels) { next unless (@{$del}); if (sql_execute('DELETE FROM logs WHERE logs_id IN (?)',join(',',@{$del}))) { for (my $i=0;$i<@{$del};$i++) { logs_reported{$del->[$i]} = 1; } } } } @logs_reported = (); } sub mark_reported { return unless (@qdir_reported || @logs_reported); if ($forget) { @qdir_reported = (); @logs_reported = (); return; } print "Marking\n"; mark_qdir_reported(); mark_logs_reported(); } # Get a fortune cookie... sub get_fortune_cookie { my ($fn,$l1,$d) = @_; $d = '' unless ($d); return $d unless ($fn); return $d unless (-f $fn); my $fo = 0; my $fh; eval { $fh = new Fortune($fn); }; return $d unless ($fh); if (-f "$fn.dat") { eval { $fh->read_header(); $fo = 1; }; } else { eval { $fh->compute_header(); $fo = 1; }; } if ($fo) { my $c; my $t = 0; do { $t ++; $c = $fh->get_random_fortune; next unless ($c); $c =~ s/^[\r\n\s]+//s; if ($l1) { $c =~ s/[\r\n\s]+$//s; next if ($c =~ /[\r\n]/); $c =~ s/\s+/ /gs; } next if ($c =~ /[^\r\n\s\x20-\x7E]/); } while ($t < 10 && !$c); $d = $c if ($c); } $fh->close_file; return $d; } # Get a fortune sub get_fortune { return get_fortune_cookie($silly_fortunes); } # Get a one-liner sub get_oneliner { return get_fortune_cookie($silly_oneliners,1); } sub read_file { my ($fn,$one) = @_; my @r = (); if (open(F,'<',$fn)) { while (my $l = ) { $l =~ s/[\r\n]+$//; push @r, $l; last if ($one); } close(F); } return @r; } sub read_lines { my @r = read_file(@_); return undef unless (@r); return \@r; } sub read_line { my @r = read_file(@_,1); return undef unless (@r); $r[0] =~ s/[\r\n]+$//; return $r[0]; } sub read_text { my @r = read_file(@_); return undef unless (@r); return join("\n",@r,''); } sub decode_header { my $r = join('',@_); $r =~ s/[\r\n]+$//; if ($r =~ /=\?(\S+)\?[QBqb]\?.*\?=/) { my $x = $r; $r = ''; while ($x =~ /^(.*?)(=\?[^\?]*\?[QqBb]\?)(.*?)(\?=)(.*)$/) { $r .= "$1$2"; my $d = $3; my $e = $4; $x = $5; $d =~ s/\?/=3F/gs; $r .= "$d$e"; } my @vl = MIME::Words::decode_mimewords($r); if (@vl) { $r = ''; foreach my $v (@vl) { if ($v->[1]) { $v->[1] =~ s/^unicode-\d-\d-(utf-\d)$/$1/; my $dd; eval { $dd = decode($v->[1],$v->[0]); }; unless (defined($dd)) { eval { $dd = decode('iso-8859-1',$v->[0]); }; unless (defined($dd)) { $dd = $v->[0]; $dd =~ s/=\?\S+\?[QBqb]\?//gs; $dd =~ s/\?=//gs; $dd =~ s/=[A-Fa-f0-9][A-Fa-f0-9]/?/gs; } } $r .= $dd; } else { $r .= $v->[0]; } } } } $r =~ s/[\s\r\n\t]+/ /gs; $r =~ s/^\s+//; $r =~ s/\s+$//; return $r; } #*********************************************************************** # Reports. #*********************************************************************** my $cgi = $debug ? new CGI : new CGI::Pretty; sub css { my $css = CSS::Tiny->new(); $css->{'body'} = { 'background-color' => 'rgb(220,255,220)', 'color' => 'black', 'margin' => '1%', 'font-family' => 'arial, helvetica, sans-serif', }; $css->{'h1'} = { 'text-align' => 'center', 'font-weight' => 'bold', 'margin-top' => '0em', 'margin-bottom' => '1em', 'margin-left' => '0', 'clear' => 'both', }; $css->{'h2'} = { 'font-weight' => 'bold', 'margin-top' => '1em', 'margin-bottom' => '0.5em', 'margin-left' => '0', 'clear' => 'both', }; $css->{'tr'} = { 'vertical-align' => 'baseline' }; $css->{'table.top tr'} = { 'vertical-align' => 'top' }; $css->{'tr.even'} = { 'background-color' => 'rgb(220,220,255)' }; $css->{'tr.odd'} = { 'background-color' => 'rgb(255,220,220)' }; $css->{'th'} = { 'background-color' => 'rgb(0,50,0)', 'color' => 'rgb(220,255,220)', }; $css->{'td'} = { 'border-style' => 'none', }; my $r = $css->write_string; $r =~ s/\{\s+/{/gs; $r =~ s/\s+\}/}/gs; $r =~ s/\s+\{/{/gs; $r =~ s/: /:/gs; return $r; } sub report_mail { my $what = shift; my $addr = shift; my $subj = shift; my $html = $cgi->start_html(-title=>$subj,-style=>{-verbatim=>css}).join(' ',@_).$cgi->end_html; unless ($debug) { #$html =~ s/>[\s\r\n]+[\s\r\n]+/>/gs; $html =~ s/[\s\r\n]+]*>)/$1\n/; $html =~ s/a>\| | new( 'X-MDF-Report:' => "($FilterUtilVers) <$what>", 'Message-ID:' => Email::MessageID->new($MyFilterHostName)->format, From => $DaemonAddress, To => $mt, Subject => $subj, Type => 'multipart/mixed', ); $eml->attach( Type => 'text/plain', Encoding => '7bit', Disposition => 'inline', Data => get_oneliner, ); $eml->attach( Type => 'text/html', Encoding => 'quoted-printable', Disposition => 'inline', Data => $html, ); $eml->attach( Type => 'text/plain', Encoding => '7bit', Disposition => 'inline', Data => get_fortune, ); if ($debug) { print $eml->as_string; return 0; } print "\tsend mail ($mt)\n" unless ($quiet); my $smtp = Net::SMTP->new($MailResultMailer); return 0 unless ($smtp); my $sok = ($smtp->mail($DaemonAddress) && $smtp->recipient($mt) && $smtp->data($eml->as_string)); $smtp->quit; return $sok; } my %qdir_ids = ('?'=>0); sub list_quarantine { my ($force,$all,$list) = @_; return 1 if (!$force && $qdir_ids{'?'}); print "\tlist qdirs\n" unless ($quiet); return 0 unless (opendir('D',$Features{'Path:QUARANTINEDIR'})); my $now = time(); while (my $f = readdir(D)) { next if ($f =~ /^\./); my $pn = $Features{'Path:QUARANTINEDIR'}.'/'.$f; if (-d $pn) { my $qid = read_line("$pn/SENDMAIL-QID"); $qdir_ids{$qid} = $pn if ($qid && $qid ne '?'); next unless ($list); next if ($now - (stat(_))[9] < 12*60*60); if (-f "$pn/reported") { $qdir_reported{"$pn/reported"} = 1; next unless ($all); } push @{$list}, {fn=>$f,pn=>$pn,qid=>$qid?encode_entities($qid):''}; } } closedir(D); $qdir_ids{'?'} ++; return 1; } my %qdir_hds = (); sub read_header { my ($qid,$qdir) = @_; $qid = '' if (length($qid) < 2); $qdir = '' if (length($qdir) < 2); return undef unless ($qid || $qdir); return $qdir_hds{$qid}{''} if ($qid && $qdir_hds{$qid}{''}); $qdir = $qdir_ids{$qid} unless ($qdir); return undef unless ($qdir && length($qdir)>1); return undef unless (-e "$qdir/HEADERS"); my $hdr = MIME::Head->from_file("$qdir/HEADERS"); return undef unless ($hdr); $qdir_hds{$qid}{''} = $hdr if ($qid); return $hdr; } sub get_header { my ($qid,$qdir,$hdrn) = @_; $qid = '' if (length($qid) < 2); $qdir = '' if (length($qdir) < 2); return '' unless ($hdrn && ($qid || $qdir)); $hdrn = lc($hdrn); return $qdir_hds{$qid}{$hdrn} if ($qid && $qdir_hds{$qid}{$hdrn}); my $hdr = read_header($qid,$qdir); return '' unless ($hdr); my $val = $hdr->get($hdrn); return '' unless (defined($val)); $val = decode_header($val) if ($val); $qdir_hds{$qid}{$hdrn} = $val if ($qid); return $val; } sub quarantine_table { my ($qdl,$tables,$fa,$hm) = @_; print "\tmake table ($fa,$hm)\n" unless ($quiet); my @trl = (); foreach my $qd (sort { $a->{fn} cmp $b->{fn} } @{$qdl}) { next unless ($qd->{fa} eq $fa && $qd->{hm} eq $hm); $qd->{inc} = 1; my @qfi = (); if ($quarantine_url && $qd->{fn}) { push @qfi, $cgi->a({href=>sprintf('%s/%s',$quarantine_url,$qd->{fn})},$qd->{fn}); } elsif ($qd->{fn}) { push @qfi, $qd->{fn}; } push @qfi, $qd->{qid} if ($qd->{qid}); push @trl, $cgi->TR({class=>$#trl%2?'odd':'even'}, #$cgi->td("$fa$hm"), #$cgi->td($qd->{qid}), $cgi->td($qd->{snd}), $cgi->td(join($cgi->br,@{$qd->{rcpt}})), $cgi->td($qd->{subj}), $cgi->td($qd->{msgs}), $cgi->td(join($cgi->br,@qfi)), ); } return '' unless (@trl); unshift @trl, $cgi->TR( #$cgi->th('what'), #$cgi->th('Queue ID'), $cgi->th('Sender'), $cgi->th('Recipient(s)'), $cgi->th('Subject'), $cgi->th('Information'), $cgi->th('Q'), ); my $hs; if ($fa eq 'S') { $hs = 'Stopped'; } elsif ($fa eq 'R') { $hs = 'Removed'; } elsif ($fa eq 'M') { $hs = 'Modified'; } else { $hs = 'Handled'; } if ($hm eq 'M') { $hs .= ' Message'; } elsif ($hm eq 'P') { $hs .= ' Part'; } else { $hs .= ' Thing'; } $hs .= 's' if ($#trl > 0); my $hsa = lc($hs); $hsa =~ s/\s+/_/g; $hsa =~ s/[^_a-z0-9]//g; push @{$tables}, $cgi->a({href=>"#$hsa"},encode_entities($hs)); return $cgi->div($cgi->h2({id=>$hsa},encode_entities($hs)),$cgi->table(@trl)); } sub quarantine_report { print "Quarantine Report\n" unless ($quiet); my ($all) = @_; my @qdl = (); return unless (list_quarantine(1,$all,\@qdl)); return unless (@qdl); print "\tread qdirs\n" unless ($quiet); my $dc = 0; foreach my $qd (@qdl) { my %msgf = (); my %flag = (); my %dir = (); next unless (opendir(D,$qd->{pn})); print "\t\t$qd->{fn}\n" if ($debug); while (my $f = readdir(D)) { next if ($f =~ /^\./); next if ($f eq 'reported'); $dir{$f} = 1; if ($f =~ /^FLAG\.(.+)$/) { $flag{lc($1)} = 1; } elsif ($f =~ /^MSG.(\d+)$/) { $msgf{$1} = $f; } } closedir(D); next unless (%dir); $dc ++; if ($flag{'stopped'}) { $qd->{fa} = 'S'; } elsif ($flag{'removed'}) { $qd->{fa} = 'R'; } elsif ($flag{'modified'}) { $qd->{fa} = 'M'; } else { $qd->{fa} = '?'; } if ($dir{'ENTIRE_MESSAGE'}) { $qd->{hm} = 'M'; } else { $qd->{hm} = 'P'; } $qd->{qid} = ''; $qd->{snd} = ''; $qd->{rcpt} = []; $qd->{subj} = ''; $qd->{snd} = encode_entities(read_line("$qd->{pn}/SENDER")) if ($dir{'SENDER'}); if ($dir{'RECIPIENTS'}) { $qd->{rcpt} = read_lines("$qd->{pn}/RECIPIENTS"); if ($qd->{rcpt}) { for (my $i=0;$i<@{$qd->{rcpt}};$i++) { $qd->{rcpt}->[$i] = encode_entities($qd->{rcpt}->[$i]); } } } $qd->{subj} = encode_entities(get_header($qd->{qid},$qd->{pn},'Subject')); foreach my $mn (sort { $a <=> $b } keys %msgf) { my $msg = read_text("$qd->{pn}/$msgf{$mn}"); next unless ($msg); $msg =~ s/[\s\n]+$//s; $msg =~ s/^\n+//s; next unless ($msg); $msg = encode_entities($msg); $msg =~ s/\n+/$cgi->br/gse; $qd->{msgs} = '' unless ($qd->{msgs}); $qd->{msgs} .= $cgi->p($msg); } if ($dir{'SPAM_REPORT'}) { foreach my $l (read_file("$qd->{pn}/SPAM_REPORT")) { if ($l =~ /^Score:/i) { $qd->{msgs} = '' unless ($qd->{msgs}); $qd->{msgs} .= $cgi->p(encode_entities($l)); last; } } } } return unless ($dc); my $html = ''; my @tables = (); $html .= quarantine_table(\@qdl,\@tables,'S','M'); $html .= quarantine_table(\@qdl,\@tables,'R','M'); $html .= quarantine_table(\@qdl,\@tables,'S','P'); $html .= quarantine_table(\@qdl,\@tables,'R','P'); $html .= quarantine_table(\@qdl,\@tables,'M','M'); $html .= quarantine_table(\@qdl,\@tables,'M','P'); $html .= quarantine_table(\@qdl,\@tables,'?','M'); $html .= quarantine_table(\@qdl,\@tables,'?','P'); return unless ($html); return unless (report_mail('quarantine','','MDF Quarantine Report', $cgi->h1('MDF Quarantine Report'), $cgi->p(join(' | ',@tables)), $html)); foreach my $qd (@qdl) { next unless ($qd->{inc}); push @qdir_reported, "$qd->{pn}/reported"; } } my %rej_lines = (); my %rej_reasons = (); my $rej_collected = 0; sub reject_collect { return 0 if ($rej_collected<0); return 1 if ($rej_collected); print "\tcollect\n" unless ($quiet); return 0 unless (list_quarantine()); my $logcsv = Text::CSV_XS->new({sep_char=>',',quote_char=>"'",binary=>1}); my $logssv = Text::CSV_XS->new({sep_char=>';',quote_char=>'"',binary=>1}); my $now = time(); return 0 unless (sql_connect()); #sql_execute('DELETE FROM logs WHERE logs_type=? AND logs_cont!=? AND logs_cont!=?', # 'stats','reject','discard'); my $st = sql_query('SELECT logs_id,logs_mqid,logs_stamp,logs_line FROM logs WHERE logs_stampexecute; %rej_lines = (); %rej_reasons = (); while (my @res = $st->fetchrow_array) { my $qid = $res[1]; next unless ($logssv->parse($res[3])); my @flds = $logssv->fields; $flds[0] = 'phish' if ($flds[0] eq 'virus' && $flds[1] =~ /\.Phishing\./i); $flds[0] = 'eicar' if ($flds[0] eq 'virus' && $flds[1] =~ /^Eicar[-_.]Test[-_.]/i); $rej_reasons{$flds[0]}{t} = 0 unless ($rej_reasons{$flds[0]}{t}); $rej_reasons{$flds[0]}{t} ++; my $bad = 0; foreach my $i ((1,6)) { if (defined($flds[$i]) && $flds[$i] ne '') { unless ($logcsv->parse($flds[$i])) { $bad = 1; last; } $flds[$i] = [$logcsv->fields]; } else { $flds[$i] = []; } } next if ($bad); $flds[3] = undef if ($flds[3] eq "[$flds[2]]"); $flds[4] = undef if (lc($flds[4]) eq lc($flds[3])); $flds[7] = decode_header($flds[7]) if ($flds[7]); $rej_reasons{$flds[0]}{l} = [] unless ($rej_reasons{$flds[0]}{l}); $rej_reasons{$flds[0]}{c} = 0 unless ($rej_reasons{$flds[0]}{c}); $rej_reasons{$flds[0]}{c} ++; $rej_lines{$qid} = [] unless $rej_lines{$qid}; push @{$rej_lines{$qid}}, { id => $res[0], stamp => $res[2], #line => $res[3], #fields => \@flds, reason => $flds[0], info => $flds[1], relay => $flds[2], host => $flds[3], helo => $flds[4], sender => $flds[5], rcpts => $flds[6], subject => $flds[7], }; push @{$rej_reasons{$flds[0]}{l}}, {q=>$qid,l=>$#{$rej_lines{$qid}}}; $qdir_ids{$qid} = '?' if ($qid && $qid ne '?' && !$qdir_ids{$qid}); $rej_collected ++; } $st->finish; unless ($rej_collected) { $rej_collected = -1; return 0 }; return 1; } sub reject_cmp { my $r = $a->[0] <=> $b->[0]; $r = $r ? $r : $a->[7] cmp $b->[7]; return $r if ($r); for my $i ((5,1,2,3,4)) { $r = $a->[$i] cmp $b->[$i]; return $r if ($r); } return 0; } sub reject_table { my ($what,$logs,$all,$tables,$usersr,$totals) = @_; print "\tmake table ($what)\n" unless ($quiet); my $infoslash = $what eq 'spam'; my @used = (); my @nouse = (); my @tll = (); my %lid = (); my $ls = $logs; $nouse[1] = 1 if ($what =~ /^(mail_from|unknown_local_user)$/); $nouse[2] = 1 if ($usersr || $what =~ /^(bad_recipient_map|bad_user|unknown_user)$/); $nouse[5] = 1 if ($usersr); $nouse[6] = 1 unless ($usersr); foreach my $li (@{$ls}) { my $line = $rej_lines{$li->{q}}->[$li->{l}]; next unless ($line); next if (!$all && length($li->{q})>1 && $qdir_ids{$li->{q}} && length($qdir_ids{$li->{q}})>1); if ($usersr) { next unless ($line->{rcpts} && @{$line->{rcpts}}); my $skip = 1; foreach my $rcpt (@{$line->{rcpts}}) { if ($rcpt =~ /^$usersr$/i) { $skip = 0; last; } } next if ($skip); } my $lused = 0; my @tl = (); $tl[8] = $line->{id}; if (!$nouse[1]) { if ($usersr) { foreach my $hn ('From','Sender','Reply-To') { $tl[1] = get_header($li->{q},'',$hn); last if ($tl[1]); } } $tl[1] = $line->{sender} if (!$tl[1] && defined($line->{sender}) && $line->{sender} ne ''); if ($tl[1]) { $tl[1] = encode_entities($tl[1]); $used[1] = 1; $lused = 1; } } if (!$nouse[2] && $line->{rcpts} && @{$line->{rcpts}}) { $tl[2] = join($cgi->br,map { encode_entities($_) } @{$line->{rcpts}}); $used[2] = 1; $lused = 1; } if (!$nouse[3]) { $tl[3] = $line->{subject} if (defined($line->{subject}) && $line->{subject} ne ''); $tl[3] = get_header($li->{q},'','Subject') unless (defined($tl[3])); if (defined($tl[3]) && $tl[3] ne '') { $tl[3] = encode_entities($tl[3]); $used[3] = 1; $lused = 1; } } if (!$nouse[4] && $line->{info} && @{$line->{info}}) { if ($infoslash) { $tl[4] = join(' / ',map { encode_entities($_) } @{$line->{info}}); } else { $tl[4] = join($cgi->br,map { encode_entities($_) } @{$line->{info}}); } $used[4] = 1; $lused = 1; } if (!$nouse[5]) { my @xinfo = (); foreach my $xi ('relay','host','helo') { next if ($xi eq $what); push @xinfo, sprintf('%s: %s',ucfirst($xi),encode_entities($line->{$xi})) if (defined($line->{$xi}) && $line->{$xi} ne ''); } if (@xinfo) { $tl[5] = join($cgi->br,@xinfo); $used[5] = 1; $lused = 1; } } if (!$nouse[6] && length($li->{q})>1 && $qdir_ids{$li->{q}} && length($qdir_ids{$li->{q}})>0) { $tl[6] = $qdir_ids{$li->{q}}; $tl[6] =~ s/^[^\/]*\///; $used[6] = 1; } next unless ($lused); if ($what eq 'spam') { $tl[7] = sprintf('%09.4f',$line->{info}->[0]); } elsif ($what eq 'virus') { $tl[7] = sprintf('%u|%s',$line->{info}->[0]&&$line->{info}->[0]=~/\.Phishing\./i?1:0,$line->{info}->[0]); } else { $tl[7] = ''; foreach my $inf (@{$line->{info}}) { if ($inf =~ /^\d+$/) { $tl[7] .= sprintf('|%127s',$inf); } else { $tl[7] .= sprintf('|%-127s',$inf); } } } my $rid = join("\x01",@tl); if ($lid{$rid}) { $tll[$lid{$rid}]->[0] ++; $used[0] = 1; } else { $tl[0] = 1; push @tll, \@tl; $lid{$rid} = $#tll; } if ($totals) { $totals->{$what} = 0 unless ($totals->{$what}); $totals->{$what} ++; } } my @table = (); foreach my $tl (sort reject_cmp @tll) { my @ll = (); my $fi = 0; for (my $i=0;$i<7;$i++) { next unless ($used[$i]); if (defined($tl->[$i]) && $tl->[$i] ne '') { $fi = 1; } else { $tl->[$i]; } push @ll, $cgi->td($tl->[$i]); } next unless ($fi); #push @logs_reported, $tl->[8]; push @table, $cgi->TR({class=>$#table%2?'odd':'even'},@ll); } return '' unless (@table); my @head = (); push @head, $cgi->th('Count') if ($used[0]); push @head, $cgi->th('Sender') if ($used[1]); push @head, $cgi->th('Recipients') if ($used[2]); push @head, $cgi->th('Subject') if ($used[3]); push @head, $cgi->th('Info') if ($used[4]); push @head, $cgi->th('Mailer') if ($used[5]); unshift @table, $cgi->TR(@head); push @{$tables}, $cgi->a({href=>"#$what"},encode_entities(fwords($what))); return $cgi->div($cgi->h2({id=>$what},encode_entities(fwords($what))),$cgi->table(@table)); } sub reject_report { print "Rejection Report\n" unless ($quiet); my ($show,$hide,$all) = @_; return unless (reject_collect()); my $html = ''; my @summary = (); foreach my $rsn (sort { $rej_reasons{$a}{t} <=> $rej_reasons{$b}{t} } keys %rej_reasons) { push @summary, $cgi->TR({class=>$#summary%2?'odd':'even'},$cgi->td(fwords($rsn)),$cgi->td($rej_reasons{$rsn}{t})); } if (@summary) { #push @tables, $cgi->a({href=>'#summary'},'Summary'); $html .= $cgi->div($cgi->h2({id=>'summary'},'Summary'),$cgi->table(@summary)); } my @tables = (); foreach my $rsn (sort keys %rej_reasons) { next unless ($rej_reasons{$rsn}{c}); next unless ($rsn =~ /^$show$/i && $rsn !~ /^$hide$/i); $html .= reject_table($rsn,$rej_reasons{$rsn}{l},$all,\@tables); } return unless ($html); #print $html; return; return unless (report_mail("rejections:$show",'','MDF Rejection Report', $cgi->h1('MDF Rejection Report'), $cgi->p(join(' | ',@tables)), $html)); } sub reject_report_user { my $show = shift; my $hide = shift; my $all = shift; my $user = shift; $user =~ s/^<(.*?)>$/$1/; @_ = ($user) unless (@_); my @usersr = (); my @userst = (); foreach my $usr (@_) { my $usrx = $usr; $usrx =~ s/\s+//g; next unless ($usrx); $usrx =~ s/^<(.*?)>$/$1/; push @userst, "<$usrx>"; push @usersr, "?"; } return unless (@usersr); print "Rejection Report (".join(',',@userst).")\n" unless ($quiet); return unless (reject_collect()); my @tables = (); my %totals = (); my $html = ''; foreach my $rsn (sort keys %rej_reasons) { next unless ($rej_reasons{$rsn}{c}); next unless ($rsn =~ /^$show$/i && $rsn !~ /^$hide$/i); $html .= reject_table($rsn,$rej_reasons{$rsn}{l},$all,\@tables,join('|',@usersr),\%totals); } my @summary = (); foreach my $rsn (sort { $totals{$a} <=> $totals{$b} } keys %totals) { next unless ($totals{$rsn}); push @summary, $cgi->TR({class=>$#summary%2?'odd':'even'},$cgi->td(fwords($rsn)),$cgi->td($totals{$rsn})); } if (@summary) { #push @tables, $cgi->a({href=>'#summary'},'Summary'); $html = $cgi->div($cgi->h2({id=>'summary'},'Summary'),$cgi->table(@summary)).$html; } return unless ($html); return unless (report_mail("user:$show",$user,'MDF Rejection Report', $cgi->h1('MDF Rejection Report'), $cgi->blockquote(encode_entities(join(' ',@userst))), $cgi->p(join(' | ',@tables)), $html)); } sub reject_report_users { my ($rtufn) = @_; $rtufn = get_file_path_name('user-reports'); return unless (open(UF,'<',$rtufn)); my @users = ; close(UF); foreach my $l (@users) { $l =~ s/[\r\n]+//gs; next unless ($l); next if ($l =~ /^\s*[#;]/); reject_report_user('spam|phish|virus','',1,split(/\s+/,$l)); } } sub hiloscores_table { my $ord = shift; my $lmt = shift; my $lst = shift; return unless (sql_connect()); my $st = sql_query("SELECT hilo_id,hilo_stamp,hilo_score,hilo_info,hilo_report FROM hiloscores ORDER BY hilo_score $ord LIMIT $lmt"); return unless ($st); $st->execute; my @trl = (); while (my @res = $st->fetchrow_array) { next unless ($res[1] && $res[2] && $res[3]); my $date = time2str('%Y-%m-%d %H:%M:%S',$res[1]); my $score = $res[2]; my $info = ''; if ($res[3]) { my @itl = (); foreach my $l (split(/[\r\n]+/,$res[3])) { if ($l =~ /^([^:]+)\s*:\s*(.*?)\s*$/) { my $k = $1; my $v = decode_header($2); $k = encode_entities("$k:"); $k =~ s/ /\ /g; push @itl, $cgi->TR( $cgi->td($k), $cgi->td(encode_entities($v)), ); } } $info = $cgi->table({class=>'top'},@itl) if (@itl); } my $report = ''; if ($res[4]) { $res[4] =~ s/\r?\n/\n/gs; $res[4] =~ s/\r/\n/gs; if ($res[4] =~ /----------/s) { $res[4] =~ s/[- A-Za-z]+:\s+\([^\n\)]*\)\n\s*\n[ a-z]+\n[- ]+----------\n//gs; $res[4] =~ s/[ a-z]+\n[- ]+----------\n//gs; $res[4] =~ s/[- ]+----------\n//gs; } $res[4] =~ s/\n+/\n/gs; my @hti = (); foreach my $l (split(/[\r\n]+/,$res[4])) { if ($l =~ /^\s*(-?\d+\.\d+)\s+(\S+)\s*(.*?)\s*$/) { push @hti, {sc=>encode_entities($1),rn=>encode_entities($2),rd=>[]}; push @{$hti[$#hti]->{rd}}, encode_entities($3) if ($3); } elsif ($l =~ /^\s*(\S+.*?)\s*$/ && @hti) { push @{$hti[$#hti]->{rd}}, encode_entities($1); } } my @htl = (); foreach my $ri (@hti) { push @htl, $cgi->TR( $cgi->td($ri->{sc}), $cgi->td($ri->{rn}), $cgi->td(join($cgi->br,@{$ri->{rd}})), ); } $report = $cgi->table({class=>'top'},@htl) if (@htl); } push @trl, $cgi->TR({class=>$#trl%2?'odd':'even'}, $cgi->td($date), $cgi->td($score), $cgi->td($info), $cgi->td($report), ); } $st->finish; return '' unless (@trl); @trl = reverse @trl if ($ord eq 'ASC'); unshift @trl, $cgi->TR( $cgi->th('Date'), $cgi->th('Score'), $cgi->th('Info'), $cgi->th('Report'), ); my $hs = "$lst Score List"; my $hsa = lc($hs); $hsa =~ s/\s+/_/g; $hsa =~ s/[^_a-z0-9]//g; #push @{$tables}, $cgi->a({href=>"#$hsa"},$hs); return $cgi->div($cgi->h2({id=>$hsa},encode_entities($hs)),$cgi->table({class=>'top'},@trl)); } sub hiloscores_report { print "High/Low Scores Report\n" unless ($quiet); my $html = ''; $html .= hiloscores_table('DESC',$hilo_entries,'High'); $html .= hiloscores_table('ASC',$hilo_entries,'Low'); return unless ($html); report_mail('hiloscores','','Spam High and Low Scores Report', $cgi->h1('Spam High and Low Scores Report'), $html); } sub external_local_table { return '' unless (sql_connect()); my $st = sql_query('SELECT logs_id,logs_mqid,logs_stamp,logs_line FROM logs WHERE logs_type=? AND logs_cont=?', 'stats','notice'); return '' unless ($st); my $logssv = Text::CSV_XS->new({sep_char=>';',quote_char=>'"',binary=>1}); my %addresses = (); my @qids = (); while (my @res = $st->fetchrow_array) { my $qid = $res[1]; next unless ($logssv->parse($res[3])); my @flds = $logssv->fields; next unless (@flds && $flds[0] eq 'external_local'); my $stq = sql_query('SELECT logs_mqid FROM logs WHERE logs_type=? AND logs_cont=? AND logs_mqid=?', 'stats','deliver',$qid); next unless ($stq); my @resq = $stq->fetchrow_array; $stq->finish; next if ($#resq || $resq[0] ne $qid); $flds[3] =~ s/^<(.*)>$/$1/; $addresses{$flds[3]} = { cnt => 0, qid => {}, relays => {}, hosts => {}, oldt => 0, newt => 0, olds => '', news => '', } unless ($addresses{$flds[3]}); push @qids, $res[1]; my $relay = "[$flds[1]]"; $relay = "$flds[2] $relay" if ($flds[2] && $flds[2] !~ /^\[?$flds[1]\]?$/); $addresses{$flds[3]}->{cnt} ++; $addresses{$flds[3]}->{qid}->{$res[1]} = 1; $addresses{$flds[3]}->{rly}->{$relay} = 1; if ($res[2]>$addresses{$flds[3]}->{newt}) { $addresses{$flds[3]}->{newt} = $res[2]; $addresses{$flds[3]}->{news} = time2str('%Y-%m-%d',$res[2]); } if (!$addresses{$flds[3]}->{oldt} || $res[2]<$addresses{$flds[3]}->{oldt}) { $addresses{$flds[3]}->{oldt} = $res[2]; $addresses{$flds[3]}->{olds} = time2str('%Y-%m-%d',$res[2]); } push @logs_reported, $res[0]; } $st->finish; return '' unless (%addresses); my @trl = (); foreach my $addr (sort { $addresses{$a}->{new} <=> $addresses{$b}->{new} } keys %addresses) { push @trl, $cgi->TR({class=>$#trl%2?'odd':'even'}, $cgi->td($addr), $cgi->td($addresses{$addr}->{olds} eq $addresses{$addr}->{news} ? $addresses{$addr}->{olds} : join($cgi->br,$addresses{$addr}->{olds},$addresses{$addr}->{news})), $cgi->td($addresses{$addr}->{cnt}), $cgi->td(join($cgi->br,keys %{$addresses{$addr}->{rly}})), $cgi->td(join($cgi->br,keys %{$addresses{$addr}->{qid}})), ); } #@trl = reverse @trl if ($ord eq 'ASC'); unshift @trl, $cgi->TR( $cgi->th('Address'), $cgi->th('Date(s)'), $cgi->th('Count'), $cgi->th('Relays'), $cgi->th('QIDs'), ); return $cgi->div($cgi->h2({id=>'external_local'},'Local senders from external relays'),$cgi->table(@trl)); } sub external_local_report { print "External Local Report\n" unless ($quiet); my $html = external_local_table(); return unless ($html); report_mail('externlocal','','External Local Report',$html); } read_cfg_cfg(); #$forget = 1; #$debug = 1; for (my $i=0;$i<@ARGV;$i++) { my ($c,$p) = split(/[:=]+/,$ARGV[$i]); $c =~ s/[^A-Za-z]+//g; unless ($c) { print "? $ARGV[$i]\n" unless ($quiet); next; } $c = lc($c); if ($c =~ /^d(e?bu?g)?$/) { $debug = 1; } elsif ($p && $c =~ /^m(ail)?to?$/) { $mailto = $p; } elsif ($c =~ /^q(uarantine?)?$/) { quarantine_report(0); } elsif ($c =~ /^f(ull)?q(uarantine?)?$/) { quarantine_report(1); } elsif ($c =~ /^(h|hi|high)?(l|lo|low)?(s|score)?(l|lists?)?$/) { hiloscores_report(); } elsif ($c =~ /^r(eject(ion)?s?)?$/) { reject_report('.*','(|unknown_user|bad_recipient_map|helo|bad_user|mail_from|relay|unknown_local_user)',0); } elsif ($c =~ /^f(ull)?r(eject(ion)?s?)?$/) { reject_report('.*','',1); } elsif ($c =~ /^s(pam)?r(eject(ion)?s?)?$/) { reject_report('spam(_.*)?','',0); } elsif ($c =~ /^(\S+?)r(eject(ion)?s?)?$/) { reject_report(lc($1),'',0); } elsif ($c =~ /^u(sers?)(r(eports?)?)?$/) { reject_report_users(); } elsif ($p && $c =~ /^u(?:sers?)(?:r(?:eports?)?)?$/) { reject_report_users($p); } elsif ($c =~ /^(e|x|extern(?:al)?)l(oc(?:al)?)?$/) { external_local_report(); } elsif ($c =~ /^q(uiet)?$/) { $quiet = 1; } elsif ($c =~ /^f(orget)?$/) { $forget = 1; } else { print "? $ARGV[$i]\n" unless ($quiet); } } mark_reported(); sql_disconnect();