You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by he...@apache.org on 2018/10/09 14:18:09 UTC

svn commit: r1843287 - in /spamassassin/trunk: UPGRADE lib/Mail/SpamAssassin/Plugin/DCC.pm

Author: hege
Date: Tue Oct  9 14:18:08 2018
New Revision: 1843287

URL: http://svn.apache.org/viewvc?rev=1843287&view=rev
Log:
DCC check is now asynchronous with dccifd

Modified:
    spamassassin/trunk/UPGRADE
    spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm

Modified: spamassassin/trunk/UPGRADE
URL: http://svn.apache.org/viewvc/spamassassin/trunk/UPGRADE?rev=1843287&r1=1843286&r2=1843287&view=diff
==============================================================================
--- spamassassin/trunk/UPGRADE (original)
+++ spamassassin/trunk/UPGRADE Tue Oct  9 14:18:08 2018
@@ -33,6 +33,7 @@ Note for Users Upgrading to SpamAssassin
 - Razor2 razor_fork option added. It will fork separate Razor2 process and
   read in the results later asynchronously, increasing throughput.
 
+- DCC check is now done asynchronously if using dccifd, improving throughput
 
 
 Note for Users Upgrading to SpamAssassin 3.4.2

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm
URL: http://svn.apache.org/viewvc/spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm?rev=1843287&r1=1843286&r2=1843287&view=diff
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm (original)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/DCC.pm Tue Oct  9 14:18:08 2018
@@ -87,6 +87,7 @@ use Mail::SpamAssassin::Util qw(untaint_
                                 proc_status_ok exit_status_str);
 use Errno qw(ENOENT EACCES);
 use IO::Socket;
+use IO::Select;
 
 our @ISA = qw(Mail::SpamAssassin::Plugin);
 
@@ -111,7 +112,7 @@ sub new {
 
   # are network tests enabled?
   if ($mailsaobject->{local_tests_only}) {
-    $self->{use_dcc} = 0;
+    $self->{dcc_disabled} = 1;
     dbg("dcc: local tests only, disabling DCC");
   }
   else {
@@ -408,9 +409,6 @@ SpamAssassin spam threshold to DCC as sp
   $conf->{parser}->register_commands(\@cmds);
 }
 
-
-
-
 sub ck_dir {
   my ($self, $dir, $tgt, $src) = @_;
 
@@ -442,7 +440,6 @@ sub find_dcc_home {
 
   my $conf = $self->{main}->{conf};
 
-
   # Get the DCC software version for talking to dccifd and formating the
   # dccifd options and the built-in DCC homedir.  Use -q to prevent delays.
   my $cdcc_home;
@@ -452,7 +449,8 @@ sub find_dcc_home {
     my $cdcc_output = do { local $/ = undef; <CDCC> };
     close CDCC;
 
-    $cdcc_output =~ s/\n/ /g;		# everything in 1 line for debugging
+    $cdcc_output =~ s/\s+/ /gs;		# everything in 1 line for debugging
+    $cdcc_output =~ s/\s+$//;
     dbg("dcc: `%s %s` reports '%s'", $cdcc, $cmd, $cdcc_output);
     $self->{dcc_version} = ($cdcc_output =~ /^(\d+\.\d+\.\d+)/) ? $1 : '';
     $cdcc_home = ($cdcc_output =~ /\s+homedir=(\S+)/) ? $1 : '';
@@ -538,11 +536,12 @@ sub dcc_pgm_path {
 
 sub is_dccifd_available {
   my ($self) = @_;
-  my $conf = $self->{main}->{conf};
 
   # dccifd remains available until it breaks
   return $self->{dccifd_available} if $self->{dccifd_available};
 
+  my $conf = $self->{main}->{conf};
+
   # deal with configured INET or INET6 socket
   if (defined $conf->{dcc_dccifd_host}) {
     dbg("dcc: dccifd is available via socket [%s]:%s",
@@ -574,7 +573,7 @@ sub is_dccproc_available {
   my $conf = $self->{main}->{conf};
 
   # dccproc remains (un)available so check only once
-  return $self->{dccproc_available} if  defined $self->{dccproc_available};
+  return $self->{dccproc_available} if defined $self->{dccproc_available};
 
   my $dccproc = $conf->{dcc_path};
   if (!defined $dccproc || $dccproc eq '') {
@@ -619,74 +618,235 @@ sub dccifd_connect {
 # check for dccifd every time in case enough uses of dccproc starts dccifd
 sub get_dcc_interface {
   my ($self) = @_;
-  my $conf = $self->{main}->{conf};
 
-  if (!$conf->{use_dcc}) {
-    $self->{dcc_disabled} = 1;
-    return;
-  }
-
-  $self->find_dcc_home();
   if (!$self->is_dccifd_available() && !$self->is_dccproc_available()) {
-    dbg("dcc: dccifd and dccproc are not available");
-    $self->{dcc_disabled} = 1;
+    dbg("dcc: dccifd or dccproc is not available");
+    return 0;
   }
 
-  $self->{dcc_disabled} = 0;
+  return 1;
 }
 
-sub dcc_query {
-  my ($self, $permsgstatus, $fulltext) = @_;
+sub check_tick {
+  my ($self, $opts) = @_;
 
-  $permsgstatus->{dcc_checked} = 1;
+  $self->_check_async($opts, 0);
+}
 
-  if (!$self->{main}->{conf}->{use_dcc}) {
-    dbg("dcc: DCC is not available: use_dcc is 0");
-    return;
+sub check_cleanup {
+  my ($self, $opts) = @_;
+
+  $self->_check_async($opts, 1);
+
+  my $pms = $opts->{permsgstatus};
+
+  # Finish callbacks
+  if ($pms->{dcc_range_callbacks}) {
+    foreach (@{$pms->{dcc_range_callbacks}}) {
+      $self->check_dcc_reputation_range($pms, @$_);
+    }
+  }
+}
+
+sub _check_async {
+  my ($self, $opts, $timeout) = @_;
+  my $pms = $opts->{permsgstatus};
+
+  return if !$pms->{dcc_sock};
+
+  my $timer = $self->{main}->time_method("check_dcc");
+
+  if ($pms->{deadline_exceeded}) {
+    $timeout = 1;
+  } elsif ($timeout) {
+    # Calculate how much time left from original timeout
+    $timeout = $self->{main}->{conf}->{dcc_timeout} -
+      (time - $pms->{dcc_async_start});
+    $timeout = 1 if $timeout < 1;
+    $timeout = 20 if $timeout > 20; # hard sanity check
+    dbg("dcc: final wait for dccifd, timeout in $timeout sec");
+  }
+
+  if (IO::Select->new($pms->{dcc_sock})->can_read($timeout)) {
+    dbg("dcc: reading dccifd response");
+    my @resp;
+    # if DCC is ready, should never block? timeout 1s just in case
+    my $timer = Mail::SpamAssassin::Timeout->new({ secs => 1 });
+    my $err = $timer->run_and_catch(sub {
+      local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
+      @resp = $pms->{dcc_sock}->getlines();
+    });
+    delete $pms->{dcc_sock};
+    $pms->{dcc_result} = 0;
+    if ($timer->timed_out()) {
+      info("dcc: dccifd read failed");
+    } elsif ($err) {
+      chomp $err;
+      info("dcc: dccifd read failed: $err");
+    } else {
+      shift @resp; shift @resp; # ignore status/multistatus line
+      if (@resp) {
+        ($pms->{dcc_x_result}, $pms->{dcc_cksums}) =
+          $self->parse_dcc_response(\@resp, 'dccifd');
+        if ($pms->{dcc_x_result}) {
+          dbg("dcc: dccifd responded with '$pms->{dcc_x_result}'");
+          $pms->{dcc_result} = $self->check_dcc_result($pms, $pms->{dcc_x_result});
+          if ($pms->{dcc_result}) {
+            $pms->got_hit($pms->{dcc_rulename}, "", ruletype => 'eval');
+          }
+        }
+      } else {
+        info("dcc: empty response from dccifd?");
+      }
+    }
+  } elsif ($timeout) {
+    dbg("dcc: no response from dccifd, timed out");
+    delete $pms->{dcc_sock};
+  } else {
+    dbg("dcc: still waiting for dccifd response");
+  }
+}
+
+sub finish_parsing_start {
+  my ($self, $opts) = @_;
+
+  # If using dccifd, hard adjust priority -100 to launch early async
+  $self->find_dcc_home();
+  if ($self->is_dccifd_available()) {
+    foreach (@{$opts->{conf}->{eval_to_rule}->{check_dcc}}) {
+      if (exists $opts->{conf}->{tests}->{$_}) {
+        dbg("dcc: adjusting rule $_ priority to -100");
+        $opts->{conf}->{priority}->{$_} = -100;
+      }
+    }
+    foreach (@{$opts->{conf}->{eval_to_rule}->{check_dcc_reputation_range}}) {
+      if (exists $opts->{conf}->{tests}->{$_}) {
+        dbg("dcc: adjusting rule $_ priority to -100");
+        $opts->{conf}->{priority}->{$_} = -100;
+      }
+    }
   }
+}
+
+sub check_dcc {
+  my ($self, $pms, $fulltext, $rulename) = @_;
+
+  return 0 if $self->{dcc_disabled};
+  return 0 if !$self->{main}->{conf}->{use_dcc};
+
+  return $pms->{dcc_result} if defined $pms->{dcc_result};
+
+  return 0 if $pms->{dcc_running};
+  $pms->{dcc_running} = 1;
+
+  my $timer = $self->{main}->time_method("check_dcc");
 
   # initialize valid tags
-  $permsgstatus->{tag_data}->{DCCB} = "";
-  $permsgstatus->{tag_data}->{DCCR} = "";
-  $permsgstatus->{tag_data}->{DCCREP} = "";
+  $pms->{tag_data}->{DCCB} = '';
+  $pms->{tag_data}->{DCCR} = '';
+  $pms->{tag_data}->{DCCREP} = '';
 
   if ($$fulltext eq '') {
     dbg("dcc: empty message; skipping dcc check");
-    return;
+    return 0;
   }
 
-  if ($permsgstatus->get('ALL') =~ /^(X-DCC-.*-Metrics:.*)$/m) {
-    $permsgstatus->{dcc_raw_x_dcc} = $1;
+  if ($pms->get('ALL-TRUSTED') =~ /^(X-DCC-[^:]*?-Metrics: .*)$/m) {
     # short-circuit if there is already a X-DCC header with value of
     # "bulk" from an upstream DCC check
     # require "bulk" because then at least one body checksum will be "many"
     # and so we know the X-DCC header is not forged by spammers
-    return if $permsgstatus->{dcc_raw_x_dcc} =~ / bulk /;
+    #if ($1 =~ / bulk /) {
+    #  return $self->check_dcc_result($pms, $1);
+    #}
+  }
+
+  $pms->{dcc_rulename} = $pms->get_current_eval_rule_name();
+
+  return 0 if !$self->get_dcc_interface();
+
+  my $envelope = $pms->{relays_external}->[0];
+  ($pms->{dcc_x_result}, $pms->{dcc_cksums}) =
+    $self->ask_dcc('dcc:', $pms, $fulltext, $envelope);
+  if (!defined $pms->{dcc_x_result}) {
+    $pms->{dcc_result} = 0;
+  } elsif ($pms->{dcc_x_result} eq 'async') {
+    return 0; # read result later
+  } else {
+    $pms->{dcc_result} =
+      $self->check_dcc_result($pms, $pms->{dcc_x_result});
   }
 
-  my $timer = $self->{main}->time_method("check_dcc");
+  return $pms->{dcc_result};
+}
 
-  $self->get_dcc_interface();
-  return if $self->{dcc_disabled};
+sub check_dcc_reputation_range {
+  my ($self, $pms, $fulltext, $min, $max, $rulename) = @_;
+
+  return 0 if $self->{dcc_disabled};
+  return 0 if !$self->{main}->{conf}->{use_dcc};
+
+  # Check if callback overriding rulename
+  my $cb;
+  if (!defined $rulename) {
+    $rulename = $pms->get_current_eval_rule_name();
+  } else {
+    $cb = 1;
+  }
+
+  if (!defined $pms->{dcc_result}) {
+    dbg("dcc: delaying check_dcc_reputation_range call for $rulename");
+    # array matches check_dcc_reputation_range() argument order
+    push @{$pms->{dcc_range_callbacks}}, [$fulltext, $min, $max, $rulename];
+    return 0;
+  }
+
+  next if !defined $pms->{dcc_x_result};
 
-  my $envelope = $permsgstatus->{relays_external}->[0];
-  ($permsgstatus->{dcc_raw_x_dcc},
-   $permsgstatus->{dcc_cksums}) = $self->ask_dcc("dcc:", $permsgstatus,
-						 $fulltext, $envelope);
+  # this is called several times per message, so parse the X-DCC header once
+  if (!defined $pms->{dcc_rep}) {
+    my $dcc_rep;
+    if ($pms->{dcc_x_result} =~ /\brep=(\d+)/) {
+      $dcc_rep = $1+0;
+      $pms->set_tag('DCCREP', $dcc_rep);
+    } else {
+      $dcc_rep = -1;
+    }
+    $pms->{dcc_rep} = $dcc_rep;
+  }
+
+  # no X-DCC header or no reputation in the X-DCC header, perhaps for lack
+  # of data in the DCC Reputation server
+  return 0 if $pms->{dcc_rep} < 0;
+
+  # cover the entire range of reputations if not told otherwise
+  $min = 0   if !defined $min;
+  $max = 100 if !defined $max;
+
+  my $result = $pms->{dcc_rep} >= $min && $pms->{dcc_rep} <= $max ? 1 : 0;
+  dbg("dcc: dcc_rep %s, min %s, max %s => result=%s",
+      $pms->{dcc_rep}, $min, $max, $result ? 'YES' : 'no');
+
+  if ($cb) {
+    # Callback needs to got_hit()
+    if ($result) {
+      $pms->got_hit($rulename, "", ruletype => 'eval');
+    }
+  } else {
+    return $result;
+  }
 }
 
-sub check_dcc {
-  my ($self, $permsgstatus, $full) = @_;
-  my $conf = $self->{main}->{conf};
+sub check_dcc_result {
+  my ($self, $pms, $x_dcc) = @_;
 
-  $self->dcc_query($permsgstatus, $full)  if !$permsgstatus->{dcc_checked};
+  return 0 if !defined $x_dcc || $x_dcc eq '';
 
-  my $x_dcc = $permsgstatus->{dcc_raw_x_dcc};
-  return 0  if !defined $x_dcc || $x_dcc eq '';
+  my $conf = $pms->{main}->{conf};
 
-  if ($x_dcc =~ /^X-DCC-(.*)-Metrics: (.*)$/) {
-    $permsgstatus->set_tag('DCCB', $1);
-    $permsgstatus->set_tag('DCCR', $2);
+  if ($x_dcc =~ /^X-DCC-([^:]*?)-Metrics: (.*)$/) {
+    $pms->set_tag('DCCB', $1);
+    $pms->set_tag('DCCR', $2);
   }
   $x_dcc =~ s/many/999999/ig;
   $x_dcc =~ s/ok\d?/0/ig;
@@ -721,40 +881,9 @@ sub check_dcc {
   return 0;
 }
 
-sub check_dcc_reputation_range {
-  my ($self, $permsgstatus, $fulltext, $min, $max) = @_;
-
-  # this is called several times per message, so parse the X-DCC header once
-  my $dcc_rep = $permsgstatus->{dcc_rep};
-  if (!defined $dcc_rep) {
-    $self->dcc_query($permsgstatus, $fulltext)  if !$permsgstatus->{dcc_checked};
-    my $x_dcc = $permsgstatus->{dcc_raw_x_dcc};
-    if (defined $x_dcc && $x_dcc =~ /\brep=(\d+)/) {
-      $dcc_rep = $1+0;
-      $permsgstatus->set_tag('DCCREP', $dcc_rep);
-    } else {
-      $dcc_rep = -1;
-    }
-    $permsgstatus->{dcc_rep} = $dcc_rep;
-  }
-
-  # no X-DCC header or no reputation in the X-DCC header, perhaps for lack
-  # of data in the DCC Reputation server
-  return 0 if $dcc_rep < 0;
-
-  # cover the entire range of reputations if not told otherwise
-  $min = 0   if !defined $min;
-  $max = 100 if !defined $max;
-
-  my $result = $dcc_rep >= $min && $dcc_rep <= $max ? 1 : 0;
-  dbg("dcc: dcc_rep %s, min %s, max %s => result=%s",
-      $dcc_rep, $min, $max, $result?'YES':'no');
-  return $result;
-}
-
 # get the X-DCC header line and save the checksums from dccifd or dccproc
 sub parse_dcc_response {
-  my ($self, $resp) = @_;
+  my ($self, $resp, $pgm) = @_;
   my ($raw_x_dcc, $cksums);
 
   # The first line is the header we want.  It uses SMTP folded whitespace
@@ -773,175 +902,211 @@ sub parse_dcc_response {
     $cksums .= $v;
   }
 
+  if (!defined $raw_x_dcc || $raw_x_dcc !~ /^X-DCC/) {
+    info("dcc: instead of X-DCC header, $pgm returned '%s'", $raw_x_dcc||'');
+  }
+
   return ($raw_x_dcc, $cksums);
 }
 
 sub ask_dcc {
-  my ($self, $tag, $permsgstatus, $fulltext, $envelope) = @_;
-  my $conf = $self->{main}->{conf};
-  my ($pgm, $err, $sock, $pid, @resp);
-  my ($client, $clientname, $helo, $opts);
-
-  $permsgstatus->enter_helper_run_mode();
+  my ($self, $tag, $pms, $fulltext, $envelope) = @_;
 
+  my $conf = $self->{main}->{conf};
   my $timeout = $conf->{dcc_timeout};
-  my $timer = Mail::SpamAssassin::Timeout->new(
-	  { secs => $timeout, deadline => $permsgstatus->{master_deadline} });
 
-  $err = $timer->run_and_catch(sub {
-    local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
+  if ($self->is_dccifd_available()) {
+    my @resp;
+    my $timer = Mail::SpamAssassin::Timeout->new(
+      { secs => $timeout, deadline => $pms->{master_deadline} });
+    my $err = $timer->run_and_catch(sub {
+      local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
-    # prefer dccifd to dccproc
-    if ($self->{dccifd_available}) {
-      $pgm = 'dccifd';
-
-      $sock = $self->dccifd_connect($tag);
-      if (!$sock) {
+      $pms->{dcc_sock} = $self->dccifd_connect($tag);
+      if (!$pms->{dcc_sock}) {
 	$self->{dccifd_available} = 0;
-	die("dccproc not available") if (!$self->is_dccproc_available());
-
 	# fall back on dccproc if the socket is an orphan from
 	# a killed dccifd daemon or some other obvious (no timeout) problem
-	dbg("$tag fall back on dccproc");
+	dbg("$tag dccifd failed: trying dccproc as fallback");
+	return;
       }
-    }
-
-    if ($self->{dccifd_available}) {
 
       # send the options and other parameters to the daemon
-      $client = $envelope->{ip};
-      $clientname = $envelope->{rdns};
+      my $client = $envelope->{ip};
+      my $clientname = $envelope->{rdns};
       if (!defined $client) {
 	$client = '';
       } else {
 	$client .= ("\r" . $clientname) if defined $clientname;
       }
-      $helo = $envelope->{helo} || '';
-      if ($tag ne "dcc:") {
-	$opts = $self->{dccifd_report_options}
-      } else {
+      my $helo = $envelope->{helo} || '';
+      my $opts;
+      if ($tag eq 'dcc:') {
 	$opts = $self->{dccifd_lookup_options};
-	if (defined $permsgstatus->{dcc_raw_x_dcc}) {
+	if (defined $pms->{dcc_x_result}) {
 	  # only query if there is an X-DCC header
 	  $opts =~ s/grey-off/grey-off query/;
 	}
+      } else {
+	$opts = $self->{dccifd_report_options};
       }
 
-      $sock->print($opts)	   or die "failed write options\n";
-      $sock->print($client . "\n") or die "failed write SMTP client\n";
-      $sock->print($helo . "\n")   or die "failed write HELO value\n";
-      $sock->print("\n")	   or die "failed write sender\n";
-      $sock->print("unknown\n\n")  or die "failed write 1 recipient\n";
-      $sock->print($$fulltext)     or die "failed write mail message\n";
-      $sock->shutdown(1) or die "failed socket shutdown: $!";
+      $pms->{dcc_sock}->print($opts)  or die "failed write options\n";
+      $pms->{dcc_sock}->print("$client\n")  or die "failed write SMTP client\n";
+      $pms->{dcc_sock}->print("$helo\n")  or die "failed write HELO value\n";
+      $pms->{dcc_sock}->print("\n")  or die "failed write sender\n";
+      $pms->{dcc_sock}->print("unknown\n\n")  or die "failed write 1 recipient\n";
+      $pms->{dcc_sock}->print($$fulltext)  or die "failed write mail message\n";
+      $pms->{dcc_sock}->shutdown(1)  or die "failed socket shutdown: $!";
+
+      # don't async report and learn
+      if ($tag ne 'dcc:') {
+        @resp = $pms->{dcc_sock}->getlines();
+        delete $pms->{dcc_sock};
+        shift @resp; shift @resp; # ignore status/multistatus line
+        if (!@resp) {
+          die("no response");
+        }
+      } else {
+        $pms->{dcc_async_start} = time;
+      }
+    });
 
-      $sock->getline()   or die "failed read status\n";
-      $sock->getline()   or die "failed read multistatus\n";
+    if ($timer->timed_out()) {
+      delete $pms->{dcc_sock};
+      dbg("$tag dccifd timed out after $timeout seconds");
+      return (undef, undef);
+    } elsif ($err) {
+      delete $pms->{dcc_sock};
+      chomp $err;
+      info("$tag dccifd failed: $err");
+      return (undef, undef);
+    }
 
-      @resp = $sock->getlines();
-      die "failed to read dccifd response\n" if !@resp;
+    # report, learn
+    if ($tag ne 'dcc:') {
+      my ($raw_x_dcc, $cksums) = $self->parse_dcc_response(\@resp, 'dccifd');
+      if ($raw_x_dcc) {
+        dbg("$tag dccifd responded with '$raw_x_dcc'");
+        return ($raw_x_dcc, $cksums);
+      } else {
+        return (undef, undef);
+      }
+    }
+
+    # async lookup
+    return ('async', undef) if $pms->{dcc_async_start};
+
+    # or falling back to dccproc..
+  }
+
+  if ($self->is_dccproc_available()) {
+    $pms->enter_helper_run_mode();
+
+    my $pid;
+    my @resp;
+    my $timer = Mail::SpamAssassin::Timeout->new(
+      { secs => $timeout, deadline => $pms->{master_deadline} });
+    my $err = $timer->run_and_catch(sub {
+      local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
-    } else {
-      $pgm = 'dccproc';
       # use a temp file -- open2() is unreliable, buffering-wise, under spamd
-      # first ensure that we do not hit a stray file from some other filter.
-      $permsgstatus->delete_fulltext_tmpfile();
-      my $tmpf = $permsgstatus->create_fulltext_tmpfile($fulltext);
-
-      my $path = $conf->{dcc_path};
-      $opts = $conf->{dcc_options};
-      my @opts = !defined $opts ? () : split(' ',$opts);
+      my $tmpf = $pms->create_fulltext_tmpfile($fulltext);
+
+      my @opts = split(/\s+/, $conf->{dcc_options} || '');
       untaint_var(\@opts);
       unshift(@opts, '-w', 'whiteclnt');
-      $client = $envelope->{ip};
+      my $client = $envelope->{ip};
       if ($client) {
-	unshift(@opts, '-a', untaint_var($client));
+        unshift(@opts, '-a', untaint_var($client));
       } else {
-	# get external relay IP address from Received: header if not available
-	unshift(@opts, '-R');
+        # get external relay IP address from Received: header if not available
+        unshift(@opts, '-R');
       }
-      if ($tag eq "dcc:") {
-	# query instead of report if there is an X-DCC header from upstream
-	unshift(@opts, '-Q') if defined $permsgstatus->{dcc_raw_x_dcc};
+      if ($tag eq 'dcc:') {
+        # query instead of report if there is an X-DCC header from upstream
+        unshift(@opts, '-Q') if defined $pms->{dcc_x_result};
       } else {
-	# learn or report spam
-	unshift(@opts, '-t', 'many');
+        # learn or report spam
+        unshift(@opts, '-t', 'many');
       }
       if ($conf->{dcc_home}) {
         # set home directory explicitly
         unshift(@opts, '-h', $conf->{dcc_home});
-      };
+      }
 
-      defined $path  or die "no dcc_path found\n";
       dbg("$tag opening pipe to " .
-	  join(' ', $path, "-C", "-x", "0", @opts, "<$tmpf"));
+        join(' ', $conf->{dcc_path}, "-C", "-x", "0", @opts, "<$tmpf"));
 
       $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
-		$tmpf, 1, $path, "-C", "-x", "0", @opts);
+        $tmpf, 1, $conf->{dcc_path}, "-C", "-x", "0", @opts);
       $pid or die "DCC: $!\n";
 
       # read+split avoids a Perl I/O bug (Bug 5985)
-      my($inbuf,$nread,$resp); $resp = '';
-      while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf }
+      my($inbuf, $nread);
+      my $resp = '';
+      while ($nread = read(DCC, $inbuf, 8192)) { $resp .= $inbuf }
       defined $nread  or die "error reading from pipe: $!";
-      @resp = split(/^/m, $resp, -1);  undef $resp;
+      @resp = split(/^/m, $resp, -1);
 
-      my $errno = 0;  close DCC or $errno = $!;
+      my $errno = 0;
+      close DCC or $errno = $!;
       proc_status_ok($?,$errno)
-	  or info("$tag [%s] finished: %s", $pid, exit_status_str($?,$errno));
+        or info("$tag [%s] finished: %s", $pid, exit_status_str($?,$errno));
 
       die "failed to read X-DCC header from dccproc\n" if !@resp;
-    }
-  });
 
-  if (defined $pgm && $pgm eq 'dccproc') {
-    if (defined(fileno(*DCC))) {	# still open
+    });
+
+    if (defined(fileno(*DCC))) { # still open
       if ($pid) {
-	if (kill('TERM',$pid)) {
+        if (kill('TERM', $pid)) {
 	  dbg("$tag killed stale dccproc process [$pid]")
 	} else {
 	  dbg("$tag killing dccproc process [$pid] failed: $!")
 	}
       }
-      my $errno = 0;  close(DCC) or $errno = $!;
+      my $errno = 0;
+      close(DCC) or $errno = $!;
       proc_status_ok($?,$errno) or info("$tag [%s] dccproc terminated: %s",
 					$pid, exit_status_str($?,$errno));
     }
-  }
 
-  $permsgstatus->leave_helper_run_mode();
+    $pms->delete_fulltext_tmpfile();
 
-  if ($timer->timed_out()) {
-    dbg("$tag %s timed out after %d seconds", $pgm||'', $timeout);
-    return (undef, undef);
-  }
+    $pms->leave_helper_run_mode();
 
-  if ($err) {
-    chomp $err;
-    info("$tag %s failed: %s", $pgm||'', $err);
-    return (undef, undef);
-  }
+    if ($timer->timed_out()) {
+      dbg("$tag dccproc timed out after $timeout seconds");
+      return (undef, undef);
+    } elsif ($err) {
+      chomp $err;
+      info("$tag dccproc failed: $err");
+      return (undef, undef);
+    }
 
-  my ($raw_x_dcc, $cksums) = $self->parse_dcc_response(\@resp);
-  if (!defined $raw_x_dcc || $raw_x_dcc !~ /^X-DCC/) {
-    info("$tag instead of X-DCC header, $pgm returned '$raw_x_dcc'");
-    return (undef, undef);
+    my ($raw_x_dcc, $cksums) = $self->parse_dcc_response(\@resp, 'dccproc');
+    if ($raw_x_dcc) {
+      dbg("$tag dccproc responded with '$raw_x_dcc'");
+      return ($raw_x_dcc, $cksums);
+    } else {
+      info("$tag instead of X-DCC header, dccproc returned '$raw_x_dcc'");
+      return (undef, undef);
+    }
   }
-  dbg("$tag $pgm responded with '$raw_x_dcc'");
-  return ($raw_x_dcc, $cksums);
+
+  return (undef, undef);
 }
 
 # tell DCC server that the message is spam according to SpamAssassin
 sub check_post_learn {
-  my ($self, $options) = @_;
+  my ($self, $opts) = @_;
+
+  return if $self->{dcc_disabled};
+  return if !$self->{main}->{conf}->{use_dcc};
 
   # learn only if allowed
-  return if $self->{learn_disabled};
   my $conf = $self->{main}->{conf};
-  if (!$conf->{use_dcc}) {
-    $self->{learn_disabled} = 1;
-    return;
-  }
   my $learn_score = $conf->{dcc_learn_score};
   if (!defined $learn_score || $learn_score eq '') {
     dbg("dcc: DCC learning not enabled by dcc_learn_score");
@@ -951,10 +1116,10 @@ sub check_post_learn {
 
   # and if SpamAssassin concluded that the message is spam
   # worse than our threshold
-  my $permsgstatus = $options->{permsgstatus};
-  if ($permsgstatus->is_spam()) {
-    my $score = $permsgstatus->get_score();
-    my $required_score = $permsgstatus->get_required_score();
+  my $pms = $opts->{permsgstatus};
+  if ($pms->is_spam()) {
+    my $score = $pms->get_score();
+    my $required_score = $pms->get_required_score();
     if ($score < $required_score + $learn_score) {
       dbg("dcc: score=%d required_score=%d dcc_learn_score=%d",
 	  $score, $required_score, $learn_score);
@@ -963,33 +1128,32 @@ sub check_post_learn {
   }
 
   # and if we checked the message
-  return if (!defined $permsgstatus->{dcc_raw_x_dcc});
+  return if (!defined $pms->{dcc_x_result});
 
   # and if the DCC server thinks it was not spam
-  if ($permsgstatus->{dcc_raw_x_dcc} !~ /\b(Body|Fuz1|Fuz2)=\d/) {
-    dbg("dcc: already known as spam; no need to learn");
+  if ($pms->{dcc_x_result} !~ /\b(Body|Fuz1|Fuz2)=\d/) {
+    dbg("dcc: already known as spam; no need to learn: $pms->{dcc_x_result}");
     return;
   }
 
+  my $timer = $self->{main}->time_method("dcc_learn");
+
   # dccsight is faster than dccifd or dccproc if we have checksums,
   #   which we do not have with dccifd before 1.3.123
-  my $old_cksums = $permsgstatus->{dcc_cksums};
-  return if ($old_cksums && $self->dccsight_learn($permsgstatus, $old_cksums));
+  my $old_cksums = $pms->{dcc_cksums};
+  return if ($old_cksums && $self->dccsight_learn($pms, $old_cksums));
 
   # Fall back on dccifd or dccproc without saved checksums or dccsight.
   # get_dcc_interface() was called when the message was checked
-
-  # is getting the full text this way kosher?  Is get_pristine() public?
-  my $fulltext = $permsgstatus->{msg}->get_pristine();
-  my $envelope = $permsgstatus->{relays_external}->[0];
-  my ($raw_x_dcc, $cksums) = $self->ask_dcc("dcc: learn:", $permsgstatus,
+  my $fulltext = $pms->{msg}->get_pristine();
+  my $envelope = $pms->{relays_external}->[0];
+  my ($raw_x_dcc, undef) = $self->ask_dcc('dcc: learn:', $pms,
 					    \$fulltext, $envelope);
   dbg("dcc: learned as spam") if defined $raw_x_dcc;
 }
 
 sub dccsight_learn {
-  my ($self, $permsgstatus, $old_cksums) = @_;
-  my ($raw_x_dcc, $new_cksums);
+  my ($self, $pms, $old_cksums) = @_;
 
   return 0 if !$old_cksums;
 
@@ -999,17 +1163,17 @@ sub dccsight_learn {
     return 0;
   }
 
-  $permsgstatus->enter_helper_run_mode();
+  $pms->enter_helper_run_mode();
 
   # use a temp file here -- open2() is unreliable, buffering-wise, under spamd
-  # ensure that we do not hit a stray file from some other filter.
-  $permsgstatus->delete_fulltext_tmpfile();
-  my $tmpf = $permsgstatus->create_fulltext_tmpfile(\$old_cksums);
+  my $tmpf = $pms->create_fulltext_tmpfile(\$old_cksums);
+
+  my ($raw_x_dcc, $new_cksums);
   my $pid;
 
   my $timeout = $self->{main}->{conf}->{dcc_timeout};
   my $timer = Mail::SpamAssassin::Timeout->new(
-	   { secs => $timeout, deadline => $permsgstatus->{master_deadline} });
+	   { secs => $timeout, deadline => $pms->{master_deadline} });
   my $err = $timer->run_and_catch(sub {
     local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" };
 
@@ -1021,47 +1185,51 @@ sub dccsight_learn {
     $pid or die "$!\n";
 
     # read+split avoids a Perl I/O bug (Bug 5985)
-    my($inbuf,$nread,$resp); $resp = '';
-    while ( $nread=read(DCC,$inbuf,8192) ) { $resp .= $inbuf }
+    my($inbuf, $nread);
+    my $resp = '';
+    while ($nread = read(DCC, $inbuf, 8192)) { $resp .= $inbuf }
     defined $nread  or die "error reading from pipe: $!";
-    my @resp = split(/^/m, $resp, -1);  undef $resp;
+    my @resp = split(/^/m, $resp, -1);
 
-    my $errno = 0;  close DCC or $errno = $!;
+    my $errno = 0;
+    close DCC or $errno = $!;
     proc_status_ok($?,$errno)
 	  or info("dcc: [%s] finished: %s", $pid, exit_status_str($?,$errno));
 
     die "dcc: failed to read learning response\n" if !@resp;
 
-    ($raw_x_dcc, $new_cksums) = $self->parse_dcc_response(\@resp);
+    ($raw_x_dcc, $new_cksums) = $self->parse_dcc_response(\@resp, 'dccsight');
   });
 
   if (defined(fileno(*DCC))) {	  # still open
     if ($pid) {
-      if (kill('TERM',$pid)) {
-	dbg("dcc: killed stale dccsight process [$pid]")
+      if (kill('TERM', $pid)) {
+	dbg("dcc: killed stale dccsight process [$pid]");
       } else {
-	dbg("dcc: killing stale dccsight process [$pid] failed: $!") }
+	dbg("dcc: killing stale dccsight process [$pid] failed: $!");
+      }
     }
-    my $errno = 0;  close(DCC) or $errno = $!;
+    my $errno = 0;
+    close(DCC) or $errno = $!;
     proc_status_ok($?,$errno) or info("dcc: dccsight [%s] terminated: %s",
 				      $pid, exit_status_str($?,$errno));
   }
-  $permsgstatus->delete_fulltext_tmpfile();
-  $permsgstatus->leave_helper_run_mode();
+
+  $pms->delete_fulltext_tmpfile();
+
+  $pms->leave_helper_run_mode();
 
   if ($timer->timed_out()) {
     dbg("dcc: dccsight timed out after $timeout seconds");
     return 0;
-  }
-
-  if ($err) {
+  } elsif ($err) {
     chomp $err;
     info("dcc: dccsight failed: $err\n");
     return 0;
   }
 
-  if ($raw_x_dcc) {
-    dbg("dcc: learned response: %s", $raw_x_dcc);
+  if ($raw_x_dcc ne '') { #TODO check if working
+    dbg("dcc: learned response: $raw_x_dcc");
     return 1;
   }
 
@@ -1069,22 +1237,26 @@ sub dccsight_learn {
 }
 
 sub plugin_report {
-  my ($self, $options) = @_;
+  my ($self, $opts) = @_;
 
-  return if $options->{report}->{options}->{dont_report_to_dcc};
-  $self->get_dcc_interface();
   return if $self->{dcc_disabled};
+  return if !$self->{main}->{conf}->{use_dcc};
+  return if $opts->{report}->{options}->{dont_report_to_dcc};
 
-  # get the metadata from the message so we can report the external relay
-  $options->{msg}->extract_message_metadata($options->{report}->{main});
-  my $envelope = $options->{msg}->{metadata}->{relays_external}->[0];
-  my ($raw_x_dcc, $cksums) = $self->ask_dcc("reporter:", $options->{report},
-					    $options->{text}, $envelope);
+  return if !$self->get_dcc_interface();
+
+  my $report = $opts->{report};
 
+  my $timer = $self->{main}->time_method("dcc_report");
+
+  # get the metadata from the message so we can report the external relay
+  $opts->{msg}->extract_message_metadata($report->{main});
+  my $envelope = $opts->{msg}->{metadata}->{relays_external}->[0];
+  my ($raw_x_dcc, undef) = $self->ask_dcc('reporter:', $report,
+					    $opts->{text}, $envelope);
   if (defined $raw_x_dcc) {
-    $options->{report}->{report_available} = 1;
+    $report->{report_available} = $report->{report_return} = 1;
     info("reporter: spam reported to DCC");
-    $options->{report}->{report_return} = 1;
   } else {
     info("reporter: could not report spam to DCC");
   }