diff -urN original/amavisd modified/amavisd --- original/amavisd 2005-12-30 16:16:15.000000000 -0500 +++ modified/amavisd 2005-12-30 16:20:06.000000000 -0500 @@ -12541,9 +12541,10 @@ $VERSION = '2.042'; @ISA = qw(Exporter); } -use Errno qw(EAGAIN); use FileHandle; -use Mail::SpamAssassin; + +# TODO: Make configurable +use lib "/usr/lib/amavisd-new/spamscanners"; BEGIN { import Amavis::Conf qw(:platform :sa $daemon_user c cr ca); @@ -12555,38 +12556,15 @@ } use subs @EXPORT_OK; - -use vars qw($spamassassin_obj); +use File::Spec; +use vars qw($scanner_obj); # called at startup, before the main fork sub init() { - do_log(1, "SpamControl: initializing Mail::SpamAssassin"); - my($saved_umask) = umask; - $spamassassin_obj = Mail::SpamAssassin->new({ - debug => $sa_debug, - save_pattern_hits => $sa_debug, - dont_copy_prefs => 1, - local_tests_only => $sa_local_tests_only, - home_dir_for_helpers => $helpers_home, - stop_at_threshold => 0, -# DEF_RULES_DIR => '/usr/local/share/spamassassin', -# LOCAL_RULES_DIR => '/etc/mail/spamassassin', -#see man Mail::SpamAssassin for other options - }); -# $Mail::SpamAssassin::DEBUG->{rbl}=-3; -# $Mail::SpamAssassin::DEBUG->{dcc}=-3; -# $Mail::SpamAssassin::DEBUG->{pyzor}=-3; -# $Mail::SpamAssassin::DEBUG->{bayes}=-3; -# $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64; - if ($sa_auto_whitelist && Mail::SpamAssassin::Version() < 3) { - do_log(1, "SpamControl: turning on SA auto-whitelisting (AWL)"); - # create a factory for the persistent address list - my($addrlstfactory) = Mail::SpamAssassin::DBBasedAddrList->new; - $spamassassin_obj->set_persistent_address_list_factory($addrlstfactory); - } - $spamassassin_obj->compile_now; # ensure all modules etc. are preloaded - alarm(0); # seems like SA forgets to clear alarm in some cases - umask($saved_umask); # restore our umask, SA clobbered it + # TODO should be configurable + require SpamAssassin; + $scanner_obj = new Amavis::SpamScanners::SpamAssassin; + $scanner_obj->init(); do_log(1, "SpamControl: done"); } @@ -12745,18 +12723,6 @@ # or just returns undef if it did not complete its jobs sub spam_scan($$) { my($conn,$msginfo) = @_; - my($spam_level,$spam_status,$spam_report,$autolearn_status); my(@lines); - my($hdr_edits) = $msginfo->header_edits; - if (!$hdr_edits) { - $hdr_edits = Amavis::Out::EditHeader->new; - $msginfo->header_edits($hdr_edits); - } - my($dspam_signature,$dspam_result,$dspam_fname); - push(@lines, sprintf("Return-Path: %s\n", # fake a local delivery agent - qquote_rfc2821_local($msginfo->sender))); - push(@lines, sprintf("X-Envelope-To: %s\n", - join(",\n ",qquote_rfc2821_local(@{$msginfo->recips})))); - my($fh) = $msginfo->mail_text; my($mbsl) = c('sa_mail_body_size_limit'); if ( defined $mbsl && ($msginfo->orig_body_size > $mbsl || @@ -12767,166 +12733,8 @@ "longer than $mbsl bytes: ". $msginfo->orig_header_size .'+'. $msginfo->orig_body_size); } else { - if ($dspam eq '') { - do_log(5,"spam_scan: DSPAM not available, skipping it"); - } else { - # pass the mail to DSPAM, extract its result headers and feed them to SA - $dspam_fname = $msginfo->mail_tempdir . '/dspam.msg'; - my($dspam_fh) = IO::File->new; # will receive output from DSPAM - $dspam_fh->open($dspam_fname, O_CREAT|O_EXCL|O_WRONLY, 0640) - or die "Can't create file $dspam_fname: $!"; - $fh->seek(0,0) or die "Can't rewind mail file: $!"; - my($proc_fh,$pid) = run_command('&'.fileno($fh), "&1", $dspam, - qw(--stdout --deliver=spam,innocent - --mode=tum --feature=chained,noise - --enable-signature-headers - --user), $daemon_user, - ); # --mode=teft - # qw(--stdout --deliver-spam) # dspam < 3.0 - # keep X-DSPAM-*, ignore other changes e.g. Content-Transfer-Encoding - my($all_local) = !grep { !lookup(0,$_,@{ca('local_domains_maps')}) } - @{$msginfo->recips}; - my($first_line); my($ln); - # scan mail header from DSPAM - for (undef $!; defined($ln=$proc_fh->getline); undef $!) { - $dspam_fh->print($ln) or die "Can't write to $dspam_fname: $!"; - if (!defined($first_line)) - { $first_line = $ln; do_log(5,"spam_scan: from DSPAM: $first_line") } - last if $ln eq $eol; - local($1,$2); - if ($ln =~ /^(X-DSPAM[^:]*):[ \t]*(.*)$/) { # does not handle folding - my($hh,$hb) = ($1,$2); - $dspam_signature = $hb if $ln =~ /^X-DSPAM-Signature:/i; - $dspam_result = $hb if $ln =~ /^X-DSPAM-Result:/i; - do_log(3,$ln); push(@lines,$ln); # store header in array passed to SA - # add DSPAM header fields to passed mail - $hdr_edits->append_header($hh,$hb) if $all_local; - } - } - defined $ln || $!==0 || $!==EAGAIN - or die "Error reading from DSPAM process: $!"; - my($nbytes,$buff); - while (($nbytes=$proc_fh->read($buff,16384)) > 0) { #copy body from DSPAM - $dspam_fh->print($buff) or die "Can't write to $dspam_fname: $!"; - } - defined $nbytes or die "Error reading: $!"; - my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); - $dspam_fh->close or die "Error closing $dspam_fname: $!"; - do_log(-1,sprintf("WARN: DSPAM problem, %s, result=%s", - exit_status_str($?,$err), $first_line) - ) if $retval || !defined $first_line; - do_log(4,"spam_scan: DSPAM gave: $dspam_signature, $dspam_result"); - section_time('DSPAM'); - } - # read mail into memory in preparation for SpamAssasin - $fh->seek(0,0) or die "Can't rewind mail file: $!"; - my($body_lines)=0; my($ln); - for (undef $!; defined($ln=<$fh>); undef $!) # header - { push(@lines,$ln); last if $ln eq $eol } - defined $ln || $!==0 or die "Error reading mail header: $!"; - for (undef $!; defined($ln=<$fh>); undef $!) # body - { push(@lines,$ln); $body_lines++ } - defined $ln || $!==0 or die "Error reading mail body: $!"; - section_time('SA msg read'); - - my($sa_required, $sa_tests); - my($saved_umask) = umask; - my($remaining_time) = alarm(0); # check how much time is left - eval { - # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose, - # disabling it before returning. It seems it only uses timer when - # external tests are enabled, so in order for our timeout to be - # useful, $sa_local_tests_only needs to be true (e.g. 1). - local $SIG{ALRM} = sub { - my($s) = Carp::longmess("SA TIMED OUT, backtrace:"); - # crop at some rather arbitrary limit - if (length($s) > 900) { $s = substr($s,0,900-3) . "..." } - do_log(-1,$s); - }; - # prepared to wait no more than n seconds - alarm($sa_timeout) if $sa_timeout > 0; - my($mail_obj); my($sa_version) = Mail::SpamAssassin::Version(); - do_log(5,"calling SA parse, SA version $sa_version"); - # *** note that $sa_version could be 3.0.1, which is not really numeric! - if ($sa_version >= 3) { - $mail_obj = $spamassassin_obj->parse(\@lines); - } else { # 2.63 or earlier - $mail_obj = Mail::SpamAssassin::NoMailAudit->new(data => \@lines, - add_From_line => 0); - } - section_time('SA parse'); - do_log(4,"CALLING SA check"); - my($per_msg_status); - { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.0 bug, $1 gets tainted - $per_msg_status = $spamassassin_obj->check($mail_obj); - } - my($rem_t) = alarm(0); - do_log(4,"RETURNED FROM SA check, time left: $rem_t s"); - - { local($1,$2,$3,$4); # avoid Perl 5.8.0..5.8.3...? taint bug - $spam_level = $per_msg_status->get_hits; - $sa_required = $per_msg_status->get_required_hits; # not used - if ($sa_version >= 3) { # access private SA method, unsupported - $sa_tests = $per_msg_status->_get_tag('TESTSSCORES',','); - $autolearn_status = $per_msg_status->get_autolearn_status; - } else { - $sa_tests = $per_msg_status->get_names_of_tests_hit; - } - $spam_report = $per_msg_status->get_report; # taints $1 and $2 ! - - # example of how to gather aditional information from SA: - # my($trusted) = $per_msg_status->_get_tag('RELAYSTRUSTED'); - # $hdr_edits->append_header('X-TESTING',$trusted); - - #Experimental, unfinished: - # $per_msg_status->rewrite_mail; - # my($entity) = nomailaudit_to_mime_entity($mail_obj); - - $per_msg_status->finish; - } - }; - section_time('SA check'); - umask($saved_umask); # SA changes umask to 0077 - prolong_timer('spam_scan_SA', $remaining_time); # restart the timer - if ($@ ne '') { # SA timed out? - chomp($@); - die "$@\n" if $@ ne "timed out"; - } - $sa_tests =~ s/,\s*/,/g; $spam_status = "tests=[" . $sa_tests . "]"; - add_entropy($spam_level,$sa_tests); - - if ($dspam ne '' && defined $spam_level) { # DSPAM auto-learn - my($eat,@options); - @options = (qw(--stdout --mode=tum --user), $daemon_user); # --mode=teft - if ( $spam_level > 7.0 && $dspam_result eq 'Innocent') { - $eat = 'SPAM'; push(@options, qw(--class=spam --source=error)); -# @options = qw(--stdout --addspam); # dspam < 3.0 - } - elsif ($spam_level < 0.5 && $dspam_result eq 'Spam') { - $eat = 'HAM'; push(@options, qw(--class=innocent --source=error)); -# @options = qw(--stdout --falsepositive); # dspam < 3.0 - } - if (defined $eat && $dspam_signature ne '') { - do_log(2,"DSPAM learn $eat ($spam_level), $dspam_signature"); - my($proc_fh,$pid) = run_command($dspam_fname, "&1", $dspam, @options); - # consume remaining output to avoid broken pipe - my($nbytes,$buff); - while (($nbytes=$proc_fh->read($buff,4096)) > 0) { } - defined $nbytes or die "Error reading from DSPAM process: $!"; - my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); -# do_log(-1,"DSPAM learn $eat response:".$output) if $output ne ''; - $retval==0 or die ("DSPAM learn $eat FAILED: ".exit_status_str($?,$err)); - section_time('DSPAM learn'); - } - } - } - if (defined $dspam_fname) { - if (($spam_level > 5.0 ? 1 : 0) != ($dspam_result eq 'Spam' ? 1 : 0)) - { do_log(2,"DSPAM: different opinions: $dspam_result, $spam_level") } - unlink($dspam_fname) or die "Can't delete file $dspam_fname: $!"; + return $scanner_obj->check($msginfo); } - do_log(3,"spam_scan: hits=$spam_level $spam_status"); - ($spam_level, $spam_status, $spam_report, $autolearn_status); } #sub nomailaudit_to_mime_entity($) { diff -urN original/spamscanners/SpamAssassin.pm modified/spamscanners/SpamAssassin.pm --- original/spamscanners/SpamAssassin.pm 1969-12-31 19:00:00.000000000 -0500 +++ modified/spamscanners/SpamAssassin.pm 2005-12-30 16:16:05.000000000 -0500 @@ -0,0 +1,390 @@ +# Copyright (C) same as the amavisd-new 2.3.3, All Rights Reserved. +# +# 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 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., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + +# This code is taken with only minor modifications from amavisd-new 2.3.3. Therefore +# credit should go to the original authors (Mark Martinec and others). +# Blame any bugs to Felix Schwarz . + +# ATTENTION: Code is completely untested! I just wanted to remove it from the main amavisd script. + +package Amavis::SpamScanners::SpamAssassin; + +use strict; +no strict "vars"; # fs: because otherwise I can't access variables from Amavis::Conf. Why? + +use Exporter qw(import); +our @EXPORT_OK = (); + +use English qw(-no_match_vars); +use Errno qw(EAGAIN); +use FileHandle; +use Mail::SpamAssassin; + +import Amavis::Boot; +import Amavis::Conf qw(:platform :sa $daemon_user c cr ca); +import Amavis::Util qw(ll do_log retcode exit_status_str run_command + prolong_timer add_entropy); +import Amavis::rfc2821_2822_Tools; +import Amavis::Timing qw(section_time); +import Amavis::Lookup qw(lookup); + +my $VERSION = 0.1; +my $NAME = 'SpamAssassin'; + +# -------------------------------------------------------------------------------- +# internal methods + +sub getCommonSAModules() { + my $self = shift(); + my @modules = qw( + Mail::SpamAssassin::Locker::Flock + Mail::SpamAssassin::Locker::UnixNFSSafe + Mail::SpamAssassin::DBBasedAddrList + Mail::SpamAssassin::SQLBasedAddrList + Mail::SpamAssassin::PersistentAddrList + Mail::SpamAssassin::PerMsgLearner + Mail::SpamAssassin::AutoWhitelist + Mail::SpamAssassin::BayesStore::DBM + Mail::SpamAssassin::BayesStore::SQL + Mail::SpamAssassin::Plugin::Hashcash + Mail::SpamAssassin::Plugin::RelayCountry + Mail::SpamAssassin::Plugin::SPF + Mail::SpamAssassin::Plugin::URIDNSBL + + DBD::mysql Sys::Hostname::Long + Mail::SPF::Query Razor2::Client::Agent Net::CIDR::Lite + Net::DNS::RR::SOA Net::DNS::RR::NS Net::DNS::RR::MX + Net::DNS::RR::A Net::DNS::RR::AAAA Net::DNS::RR::PTR + Net::DNS::RR::CNAME Net::DNS::RR::TXT Net::Ping + # ??? ArchiveIterator Reporter Data::Dumper Getopt::Long Sys::Syslog lib + # Mail::SpamAssassin::BayesStore::SDBM + ); + return @modules; +} + +sub getSA2Modules() { + my $self = shift(); + my @modules = qw( + Mail::SpamAssassin::UnixLocker Mail::SpamAssassin::BayesStoreDBM + Mail::SpamAssassin::SpamCopURI + URI URI::Escape URI::Heuristic URI::QueryParam URI::Split URI::URL + URI::WithBase URI::_foreign URI::_generic URI::_ldap URI::_login + URI::_query URI::_segment URI::_server URI::_userpass URI::data URI::ftp + URI::gopher URI::http URI::https URI::ldap URI::ldapi URI::ldaps + URI::mailto URI::mms URI::news URI::nntp URI::pop URI::rlogin URI::rsync + URI::rtsp URI::rtspu URI::sip URI::sips URI::snews URI::ssh URI::telnet + URI::tn3270 URI::urn URI::urn::isbn URI::urn::oid + URI::file URI::file::Base URI::file::Unix URI::file::Win32 + ); + return @modules; +} + +sub getSA31Modules() { + my $self = shift(); + my @modules = qw( + Mail::SpamAssassin::BayesStore::MySQL + Mail::SpamAssassin::Plugin::AutoLearnThreshold + Mail::SpamAssassin::Plugin::ReplaceTags + Mail::SpamAssassin::Plugin::MIMEHeader + Mail::SpamAssassin::Plugin::AWL Mail::SpamAssassin::Plugin::DCC + Mail::SpamAssassin::Plugin::Pyzor Mail::SpamAssassin::Plugin::Razor2 + Mail::SpamAssassin::Plugin::SpamCop + Mail::SpamAssassin::Plugin::WhiteListSubject + Mail::SpamAssassin::Plugin::DomainKeys + Mail::DomainKeys::Header Mail::DomainKeys::Message + Mail::DomainKeys::Policy Mail::DomainKeys::Signature + Mail::DomainKeys::Key Mail::DomainKeys::Key::Public + Crypt::OpenSSL::RSA + auto::Crypt::OpenSSL::RSA::_new auto::Crypt::OpenSSL::RSA::DESTROY + auto::Crypt::OpenSSL::RSA::load_public_key + auto::Crypt::OpenSSL::RSA::new_public_key + IP::Country::Fast + ); + # BayesStore::PgSQL BayesStore::SDBM + # Plugin::AntiVirus Plugin::DomainKeys Plugin::NetCache Plugin::TextCat + return @modules; +} + +sub sa_version() { + my $self = shift(); + return $self->{'sa_version'}; +} + +sub loadSpamAssassinModules() { + my $self = shift(); + + # comment in the original code: must be loaded before chroot takes place + my @modules = $self->getCommonSAModules(); + # *** note that $sa_version could be 3.0.1, which is not really numeric! + if (defined $self->sa_version()) { + if ($self->sa_version() < 3) { + push(@modules, $self->getSA2Modules()); + } elsif ($self->sa_version() > 3.1) { + push(@modules, $self->getSA31Modules()); + } + } + my($missing) = Amavis::Boot::fetch_modules('PRE-COMPILE OPTIONAL MODULES', 0, + @modules) if @modules; + do_log(2, 'INFO: no optional modules: '.join(' ',@$missing)) + if ref $missing && @$missing; +} + +sub initializeSpamAssassin() { + my $self = shift(); + do_log(1, "SpamControl: initializing Mail::SpamAssassin"); + my($saved_umask) = umask; + my $spamassassin_obj = Mail::SpamAssassin->new({ + debug => $sa_debug, + save_pattern_hits => $sa_debug, + dont_copy_prefs => 1, + local_tests_only => $sa_local_tests_only, + home_dir_for_helpers => $helpers_home, + stop_at_threshold => 0, +# DEF_RULES_DIR => '/usr/local/share/spamassassin', +# LOCAL_RULES_DIR => '/etc/mail/spamassassin', +#see man Mail::SpamAssassin for other options + }); +# $Mail::SpamAssassin::DEBUG->{rbl}=-3; +# $Mail::SpamAssassin::DEBUG->{dcc}=-3; +# $Mail::SpamAssassin::DEBUG->{pyzor}=-3; +# $Mail::SpamAssassin::DEBUG->{bayes}=-3; +# $Mail::SpamAssassin::DEBUG->{rulesrun}=4+64; + if ($sa_auto_whitelist && $self->sa_version() < 3) { + do_log(1, "SpamControl: turning on SA auto-whitelisting (AWL)"); + # create a factory for the persistent address list + my($addrlstfactory) = Mail::SpamAssassin::DBBasedAddrList->new; + $spamassassin_obj->set_persistent_address_list_factory($addrlstfactory); + } + $spamassassin_obj->compile_now; # ensure all modules etc. are preloaded + alarm(0); # seems like SA forgets to clear alarm in some cases + umask($saved_umask); # restore our umask, SA clobbered it + $self->{'spamassassin_obj'} = $spamassassin_obj; +} + +# -------------------------------------------------------------------------------- + + +sub new() { + my($class) = shift(); + my $self = bless({}, $class); + $self->{'initialized'} = 0; + $self->{'sa_version'} = undef; + $self->{'spamassassin_obj'} = undef; + return $self; +} + +sub init() { + my $self = shift(); + $self->{'sa_version'} = Mail::SpamAssassin::Version(); + $self->loadSpamAssassinModules(); + $self->initializeSpamAssassin(); + $self->{'initialized'} = 1; + do_log(1, "$NAME plugin for amavisd-new $VERSION initialized."); +} + + +sub check($) { + my ($self, $msginfo) = @_; + + my $spamassassin_obj = $self->{'spamassassin_obj'}; + my($dspam_signature,$dspam_result,$dspam_fname); + my($fh) = $msginfo->mail_text; + my($spam_level,$spam_status,$spam_report,$autolearn_status); my(@lines); + my($hdr_edits) = $msginfo->header_edits; + if (!$hdr_edits) { + $hdr_edits = Amavis::Out::EditHeader->new; + $msginfo->header_edits($hdr_edits); + } + my($dspam_signature,$dspam_result,$dspam_fname); + push(@lines, sprintf("Return-Path: %s\n", # fake a local delivery agent + qquote_rfc2821_local($msginfo->sender))); + push(@lines, sprintf("X-Envelope-To: %s\n", + join(",\n ",qquote_rfc2821_local(@{$msginfo->recips})))); + my($fh) = $msginfo->mail_text; + my($mbsl) = c('sa_mail_body_size_limit'); + if ( defined $mbsl && + ($msginfo->orig_body_size > $mbsl || + $msginfo->orig_header_size + 1 + $msginfo->orig_body_size + > 5*1024 + $mbsl) + ) { + do_log(1,"spam_scan: not wasting time on SA, message ". + "longer than $mbsl bytes: ". + $msginfo->orig_header_size .'+'. $msginfo->orig_body_size); + } else { + if ($dspam eq '') { + do_log(5,"spam_scan: DSPAM not available, skipping it"); + } else { + # pass the mail to DSPAM, extract its result headers and feed them to SA + $dspam_fname = $msginfo->mail_tempdir . '/dspam.msg'; + my($dspam_fh) = IO::File->new; # will receive output from DSPAM + $dspam_fh->open($dspam_fname, O_CREAT|O_EXCL|O_WRONLY, 0640) + or die "Can't create file $dspam_fname: $!"; + $fh->seek(0,0) or die "Can't rewind mail file: $!"; + my($proc_fh,$pid) = run_command('&'.fileno($fh), "&1", $dspam, + qw(--stdout --deliver=spam,innocent + --mode=tum --feature=chained,noise + --enable-signature-headers + --user), $daemon_user, + ); # --mode=teft + # qw(--stdout --deliver-spam) # dspam < 3.0 + # keep X-DSPAM-*, ignore other changes e.g. Content-Transfer-Encoding + my($all_local) = !grep { !lookup(0,$_,@{ca('local_domains_maps')}) } + @{$msginfo->recips}; + my($first_line); my($ln); + # scan mail header from DSPAM + for (undef $!; defined($ln=$proc_fh->getline); undef $!) { + $dspam_fh->print($ln) or die "Can't write to $dspam_fname: $!"; + if (!defined($first_line)) + { $first_line = $ln; do_log(5,"spam_scan: from DSPAM: $first_line") } + last if $ln eq $eol; + local($1,$2); + if ($ln =~ /^(X-DSPAM[^:]*):[ \t]*(.*)$/) { # does not handle folding + my($hh,$hb) = ($1,$2); + $dspam_signature = $hb if $ln =~ /^X-DSPAM-Signature:/i; + $dspam_result = $hb if $ln =~ /^X-DSPAM-Result:/i; + do_log(3,$ln); push(@lines,$ln); # store header in array passed to SA + # add DSPAM header fields to passed mail + $hdr_edits->append_header($hh,$hb) if $all_local; + } + } + defined $ln || $!==0 || $!==EAGAIN + or die "Error reading from DSPAM process: $!"; + my($nbytes,$buff); + while (($nbytes=$proc_fh->read($buff,16384)) > 0) { #copy body from DSPAM + $dspam_fh->print($buff) or die "Can't write to $dspam_fname: $!"; + } + defined $nbytes or die "Error reading: $!"; + my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); + $dspam_fh->close or die "Error closing $dspam_fname: $!"; + do_log(-1,sprintf("WARN: DSPAM problem, %s, result=%s", + exit_status_str($?,$err), $first_line) + ) if $retval || !defined $first_line; + do_log(4,"spam_scan: DSPAM gave: $dspam_signature, $dspam_result"); + section_time('DSPAM'); + } + # read mail into memory in preparation for SpamAssasin + $fh->seek(0,0) or die "Can't rewind mail file: $!"; + my($body_lines)=0; my($ln); + for (undef $!; defined($ln=<$fh>); undef $!) # header + { push(@lines,$ln); last if $ln eq $eol } + defined $ln || $!==0 or die "Error reading mail header: $!"; + for (undef $!; defined($ln=<$fh>); undef $!) # body + { push(@lines,$ln); $body_lines++ } + defined $ln || $!==0 or die "Error reading mail body: $!"; + section_time('SA msg read'); + + my($sa_required, $sa_tests); + my($saved_umask) = umask; + my($remaining_time) = alarm(0); # check how much time is left + eval { + # NOTE ON TIMEOUTS: SpamAssassin may use timer for its own purpose, + # disabling it before returning. It seems it only uses timer when + # external tests are enabled, so in order for our timeout to be + # useful, $sa_local_tests_only needs to be true (e.g. 1). + local $SIG{ALRM} = sub { + my($s) = Carp::longmess("SA TIMED OUT, backtrace:"); + # crop at some rather arbitrary limit + if (length($s) > 900) { $s = substr($s,0,900-3) . "..." } + do_log(-1,$s); + }; + # prepared to wait no more than n seconds + alarm($sa_timeout) if $sa_timeout > 0; + my($mail_obj); my($sa_version) = Mail::SpamAssassin::Version(); + do_log(5,"calling SA parse, SA version $sa_version"); + # *** note that $sa_version could be 3.0.1, which is not really numeric! + if ($sa_version >= 3) { + $mail_obj = $spamassassin_obj->parse(\@lines); + } else { # 2.63 or earlier + $mail_obj = Mail::SpamAssassin::NoMailAudit->new(data => \@lines, + add_From_line => 0); + } + section_time('SA parse'); + do_log(4,"CALLING SA check"); + my($per_msg_status); + { local($1,$2,$3,$4,$5,$6); # avoid Perl 5.8.0 bug, $1 gets tainted + $per_msg_status = $spamassassin_obj->check($mail_obj); + } + my($rem_t) = alarm(0); + do_log(4,"RETURNED FROM SA check, time left: $rem_t s"); + + { local($1,$2,$3,$4); # avoid Perl 5.8.0..5.8.3...? taint bug + $spam_level = $per_msg_status->get_hits; + $sa_required = $per_msg_status->get_required_hits; # not used + if ($sa_version >= 3) { # access private SA method, unsupported + $sa_tests = $per_msg_status->_get_tag('TESTSSCORES',','); + $autolearn_status = $per_msg_status->get_autolearn_status; + } else { + $sa_tests = $per_msg_status->get_names_of_tests_hit; + } + $spam_report = $per_msg_status->get_report; # taints $1 and $2 ! + + # example of how to gather aditional information from SA: + # my($trusted) = $per_msg_status->_get_tag('RELAYSTRUSTED'); + # $hdr_edits->append_header('X-TESTING',$trusted); + + #Experimental, unfinished: + # $per_msg_status->rewrite_mail; + # my($entity) = nomailaudit_to_mime_entity($mail_obj); + + $per_msg_status->finish; + } + }; + section_time('SA check'); + umask($saved_umask); # SA changes umask to 0077 + prolong_timer('spam_scan_SA', $remaining_time); # restart the timer + if ($@ ne '') { # SA timed out? + chomp($@); + die "$@\n" if $@ ne "timed out"; + } + $sa_tests =~ s/,\s*/,/g; $spam_status = "tests=[" . $sa_tests . "]"; + add_entropy($spam_level,$sa_tests); + + if ($dspam ne '' && defined $spam_level) { # DSPAM auto-learn + my($eat,@options); + @options = (qw(--stdout --mode=tum --user), $daemon_user); # --mode=teft + if ( $spam_level > 7.0 && $dspam_result eq 'Innocent') { + $eat = 'SPAM'; push(@options, qw(--class=spam --source=error)); +# @options = qw(--stdout --addspam); # dspam < 3.0 + } + elsif ($spam_level < 0.5 && $dspam_result eq 'Spam') { + $eat = 'HAM'; push(@options, qw(--class=innocent --source=error)); +# @options = qw(--stdout --falsepositive); # dspam < 3.0 + } + if (defined $eat && $dspam_signature ne '') { + do_log(2,"DSPAM learn $eat ($spam_level), $dspam_signature"); + my($proc_fh,$pid) = run_command($dspam_fname, "&1", $dspam, @options); + # consume remaining output to avoid broken pipe + my($nbytes,$buff); + while (($nbytes=$proc_fh->read($buff,4096)) > 0) { } + defined $nbytes or die "Error reading from DSPAM process: $!"; + my($err); $proc_fh->close or $err = $!; my($retval) = retcode($?); +# do_log(-1,"DSPAM learn $eat response:".$output) if $output ne ''; + $retval==0 or die ("DSPAM learn $eat FAILED: ".exit_status_str($?,$err)); + section_time('DSPAM learn'); + } + } + } + if (defined $dspam_fname) { + if (($spam_level > 5.0 ? 1 : 0) != ($dspam_result eq 'Spam' ? 1 : 0)) + { do_log(2,"DSPAM: different opinions: $dspam_result, $spam_level") } + unlink($dspam_fname) or die "Can't delete file $dspam_fname: $!"; + } + do_log(3,"spam_scan: hits=$spam_level $spam_status"); + ($spam_level, $spam_status, $spam_report, $autolearn_status); +} + + +return 1;