#!/usr/bin/perl # $Header: /etc/postfix/RCS/postfix-filter.pl,v 1.2 2002/10/30 15:20:22 root Exp $ # # Copyright (C) 2002 Marcus Schwartz # This code is distributed under the GPL (see www.gnu.org) # ############################################################################## # usage: /etc/postfix/postfix-filter.pl [...] # # takes a mail message on stdin, processes it with # # * spamc -- SpamAssassin adds headers regarding the spam content # * sanitizer.pl -- Anomy Sanitizer "defangs" mime headers and attachments # # then re-injects the mail to the mail system with /usr/lib/sendmail # use strict; ############################################################################## # Local Settings # # mail with recipient addresses that match these regexps will be filtered my @filter_for = ( qr/\@(.+\.)?outer\.org$/, ); # local usernames (passed to spamc) will be determined by the first # () ref in the first regexp that matches the recipient address. if none # match, no username is passed to spamc my @local_usernames = ( qr/^([^+\@]+)[^\@]*\@(.+\.)?outer\.org$/, ); # filters to run my @filters = ( # [ 'spamassassin', \&filter_spamassassin ], [ 'dspam', \&filter_dspam ], # [ 'sanitizer', \&filter_sanitizer ], ); # name of the script for error messages my $SCRIPTNAME = 'postfix-filter.pl'; # where do temp files get put my $FILTER_DIR = '/var/filter/queue'; # spamassassin client my $SPAMC = '/usr/local/bin/spamc'; my @SPAMC_ARGS = (); my $DSPAM = '/usr/local/dspam/bin/dspam'; my @DSPAM_ARGS = (); # sendmail binary my $SENDMAIL = '/usr/local/lib/sendmail'; # anomy sanitizer binary and config file my $SANITIZER = '/usr/local/anomy/bin/sanitizer.pl'; my $SANICFG = '/etc/sanitizer.cfg'; $ENV{'ANOMY'} = '/usr/local/anomy'; # filter log my $LOGFILE = '/var/filter/log'; # longest wait for any filter program to complete my $TIMEOUT = 300; ############################################################################## # code # use FileHandle; use POSIX qw/strftime/; use POSIX ":sys_wait_h"; use Fcntl ':flock'; # clean up on exit END { opendir(DIR, $FILTER_DIR); my @files = grep { /^(in|out)\.$$/ } readdir(DIR); closedir(DIR); grep(s/^/$FILTER_DIR\//, @files); unlink(@files); } # postfix error return values my $EX_TEMPFAIL=75; my $EX_UNAVAILABLE=69; # command line arguements are ' [ ...]' my $sender = shift; my @recipients = @ARGV; # recipients who filtered mail has been succesfully sent to my @sent_recipients; # counters to make sure we get the entire input my ($size, $filesize); # move into the filter directory if (! chdir($FILTER_DIR)) { logmsg("FATAL ERROR: could not chdir($FILTER_DIR)"); print "$SCRIPTNAME: could not chdir($FILTER_DIR)\n"; exit $EX_TEMPFAIL; } ############################################################################## # dump the original message into a temp file # if (! open(INPUT, ">in.$$") ) { print "$SCRIPTNAME cannot save mail to temp file\n"; exit $EX_TEMPFAIL; } $size = 0; while() { $size += length($_); print INPUT $_; } close(INPUT); $filesize = (stat("in.$$"))[7]; if ($size != $filesize) { print "SCRIPTNAME could not save entire mail to temp file ($filesize/$size\n"; logmsg("could not save entire mail ($filesize/$size)"); exit $EX_TEMPFAIL; } ############################################################################## # for each recipient, scan the message and re-inject # my $index = 0; # recipient index number foreach my $recipient (@recipients) { my $local_user = undef; # recipient username on local machine my $ret; # return value of filter_pipe call my $message; # name of file for next filter input my $spamstatus; # results from the spam-status header my $msg; # the log text for the message my $do_filters = 0; # run filters for this recipient? $index++; $message = "in.$$"; foreach my $f (@filter_for) { if ($recipient =~ /$f/) { # debug("recipient matched /$f/"); $do_filters = 1; last; } } if ($do_filters) { foreach my $filter (@filters) { my ($filter_name, $filter_proc) = @{$filter}; my $outfile = "out.$$.$filter_name.$index"; $ret = &{$filter_proc}( $sender,$recipient,$message,$outfile); if ($ret) { $message = $ret; } else { logmsg("ERROR: $filter_name failed. skipping."); } } } ###################################################################### # re-inject the message into the mail system # $ret = filter_pipe($message, undef, $SENDMAIL, '-i', '-f', $sender, '--', $recipient); if ( defined($ret) && ($ret == 0) ) { push(@sent_recipients, $recipient); } else { print "$SCRIPTNAME could not queue the filtered mail: $ret\n"; logmsg("FATAL ERROR: could not queue filtered mail: $ret"); logmsg("FATAL ERROR: sender: $sender"); logmsg("FATAL ERROR: recipients: @recipients"); logmsg("FATAL ERROR: sent_recipients: @sent_recipients"); exit $EX_TEMPFAIL; } ###################################################################### # get the spam score for logging of the message # $spamstatus = 'Unknown 0.0/0.0'; if ($do_filters) { open(IN, "<$message"); my $dspam; my $sa; while() { chomp; if (/^X-DSPAM-Result: ([A-Za-z]+)/) { $dspam = $1; } if (/^X-DSPAM-Probability: ([0-9\.]+)/) { $dspam .= " [$1]"; } if (/^X-Spam-Status: ([A-Za-z]+), hits=(\-?[0-9\.]+) required=([0-9\.]+)/) { $spamstatus = "$1 $2/$3"; } if (/^$/) { if ($dspam) { $spamstatus = $dspam; } elsif ($sa) { $spamstatus = $sa; } last; } } close(IN); $msg = 'FILTERED'; } else { $msg = 'SKIPPED'; } ###################################################################### # log the message # logmsg("$msg: $recipient $sender $size $spamstatus"); } #my @times = times; #print STDERR "times: @times\n"; exit 0; sub debug { my $msg = shift; print STDERR $msg . "\n"; } sub filter_pipe { my $infile = shift; my $outfile = shift; my @filter = @_; my ($pid, $reaped); my $waituntil; my $filter_result; if (! -f $infile) { return undef; } $pid = fork(); if (! defined($pid)) { return undef; } if ($pid == 0) { filter_exec($infile,$outfile,@filter); } $waituntil = time + $TIMEOUT; while ( ($reaped < 1) && (time < $waituntil) ) { $reaped = waitpid($pid, &WNOHANG); select(undef,undef,undef,0.1); } if ($reaped < 1) { logmsg("ERROR: $filter[0] timeout. killing $pid"); kill(15, $pid); return undef; } $filter_result = $? >> 8; return $filter_result; } sub filter_exec { my $infile = shift; my $outfile = shift; my @filter = @_; my $in = new FileHandle; my $out = new FileHandle; close(STDIN); sysopen($in, $infile, O_RDONLY); close(STDOUT); if ($outfile) { sysopen($out, $outfile, O_WRONLY | O_CREAT); } exec(@filter); } sub get_local_username { foreach my $re (@local_usernames) { if ($_[0] =~ /$re/) { return $1; } } return undef; } sub logmsg { my $msg = shift; my $date = POSIX::strftime("%Y-%m-%d %H:%M:%S", localtime(time)); open(LOG, ">>$LOGFILE"); flock(LOG, LOCK_EX); seek(LOG, 0, 2); print LOG "$date [$$] $msg\n"; flock(LOG, LOCK_UN); } sub debug_file { my @files = @_; my $date = POSIX::strftime("%Y%m%d.%H%M%S", localtime(time)); foreach my $file (@files) { system("cp $file problem.$date.$file"); } } sub filter_spamassassin($$$$) { my $sender = shift; my $recipient = shift; my $infile = shift; my $outfile = shift; my $local_user; # unix username on local machine, if any my @filter; # filter command and args my ($ret,$size_in,$size_out); ###################################################################### # find the local username of the recipient, if any $local_user = get_local_username($recipient); ###################################################################### # pass the message thru spamc # if ($local_user) { @filter = ($SPAMC, @SPAMC_ARGS, '-u', $local_user); } else { @filter = ($SPAMC, @SPAMC_ARGS); } $ret = filter_pipe($infile, $outfile, @filter); if (defined($ret) && ($ret == 0) ) { # check to make sure the spamassassin output is at least # 90% of the # size of the input message. if not, just # send the input message. $size_in = (stat($infile))[7]; $size_out = (stat($outfile))[7]; if ( ($size_in * 0.9) > $size_out) { logmsg("ERROR: $filter[0] output " . "($size_out) is less than input ($size_in)"); debug_file($infile, $outfile); return undef; } else { # the spamassassin output is good return $outfile; } } else { # there was a problem with the filter, do not use its output logmsg("ERROR: could not properly exec $filter[0]"); return undef; } } ###################################################################### # pass a message thru dspam # sub filter_dspam($$$$) { my $sender = shift; my $recipient = shift; my $infile = shift; my $outfile = shift; my $local_user; # unix username on local machine, if any my @filter; # filter command and args my ($ret,$size_in,$size_out); ###################################################################### # find the local username of the recipient, if any $local_user = get_local_username($recipient); ###################################################################### # pass the message thru spamc # if ($local_user) { @filter = ($DSPAM, @DSPAM_ARGS, '--user', $local_user); } else { @filter = ($DSPAM, @DSPAM_ARGS); } $ret = filter_pipe($infile, $outfile, @filter); if (defined($ret) && ($ret == 0) ) { # check to make sure the spamassassin output is at least # 90% of the # size of the input message. if not, just # send the input message. $size_in = (stat($infile))[7]; $size_out = (stat($outfile))[7]; if ( ($size_in * 0.7) > $size_out) { logmsg("ERROR: $filter[0] output " . "($size_out) is less than input ($size_in)"); debug_file($infile, $outfile); return undef; } else { # the spamassassin output is good return $outfile; } } else { # there was a problem with the filter, do not use its output logmsg("ERROR: could not properly exec $filter[0]"); return undef; } } ###################################################################### # pass a message thru the anony sanitizer # sub filter_sanitizer($$$$) { my $sender = shift; my $recipient = shift; my $infile = shift; my $outfile = shift; my ($ret, $size_out); $ret = filter_pipe($infile, $outfile, $SANITIZER, $SANICFG); if ( defined($ret) ) { # make sure that the sanitizer output is at least 500 bytes, # otherwise send the un-sanitized mail $size_out = (stat($outfile))[7]; if ($size_out < 500) { # $message is already set to the input file debug_file($infile, $outfile); logmsg("ERROR: $SANITIZER output only $size_out bytes"); return undef; } else { # the anony output is good return $outfile; } } else { # problem with filter. leave $message alone logmsg("ERROR: could not properly exec $SANITIZER"); return undef; } }