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 <felix@schwarz-online.org>.
+
+# 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;
