sub ask_dcc()

in lib/Mail/SpamAssassin/Plugin/DCC.pm [972:1157]


sub ask_dcc {
  my ($self, $tag, $pms, $fulltext, $envelope) = @_;

  my $conf = $pms->{conf};
  my $timeout = $conf->{dcc_timeout};

  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" };

      $pms->{dcc_sock} = $self->dccifd_connect($tag);
      if (!$pms->{dcc_sock}) {
	$self->{dccifd_available} = 0;
	# fall back on dccproc if the socket is an orphan from
	# a killed dccifd daemon or some other obvious (no timeout) problem
	dbg("$tag dccifd failed: trying dccproc as fallback");
	return;
      }

      # send the options and other parameters to the daemon
      my $client = $envelope->{ip};
      my $clientname = $envelope->{rdns};
      if (!defined $client) {
	$client = '';
      } else {
	$client .= ("\r" . $clientname) if defined $clientname;
      }
      my $helo = $envelope->{helo} || '';
      my $opts;
      if ($tag eq 'dcc:') {
	$opts = $self->{dccifd_lookup_options};
	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};
      }

      $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;
      }
    });

    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);
    }

    # 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" };

      # use a temp file -- open2() is unreliable, buffering-wise, under spamd
      my $tmpf = $pms->create_fulltext_tmpfile();

      my @opts = split(/\s+/, $conf->{dcc_options} || '');
      untaint_var(\@opts);
      unshift(@opts, '-w', 'whiteclnt');
      my $client = $envelope->{ip};
      if ($client) {
        unshift(@opts, '-a', untaint_var($client));
      } else {
        # 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 $pms->{dcc_x_result};
      } else {
        # learn or report spam
        unshift(@opts, '-t', 'many');
      }
      if ($conf->{dcc_home}) {
        # set home directory explicitly
        unshift(@opts, '-h', $conf->{dcc_home});
      }

      dbg("$tag opening pipe to " .
        join(' ', $conf->{dcc_path}, "-C", "-x", "0", @opts, "<$tmpf"));

      $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
        $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);
      my $resp = '';
      while ($nread = read(DCC, $inbuf, 8192)) { $resp .= $inbuf }
      defined $nread  or die "error reading from pipe: $!";
      @resp = split(/^/m, $resp, -1);

      my $errno = 0;
      close DCC or $errno = $!;
      proc_status_ok($?,$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(fileno(*DCC))) { # still open
      if ($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 = $!;
      proc_status_ok($?,$errno) or info("$tag [%s] dccproc terminated: %s",
					$pid, exit_status_str($?,$errno));
    }

    $pms->leave_helper_run_mode();

    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, '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);
    }
  }

  return (undef, undef);
}