diff options
author | Michael Scheidell <scheidell@FreeBSD.org> | 2011-11-28 16:35:43 +0000 |
---|---|---|
committer | Michael Scheidell <scheidell@FreeBSD.org> | 2011-11-28 16:35:43 +0000 |
commit | a9a3a85257b12c5d313e56aa68ddb21b7a3117ad (patch) | |
tree | 042f4ad5ed1a8db3632fe8a1032f3d91bd8bd10c /mail | |
parent | f9d30cef273d9fe616b51fbf95f59cee60814f50 (diff) | |
download | ports-a9a3a85257b12c5d313e56aa68ddb21b7a3117ad.tar.gz ports-a9a3a85257b12c5d313e56aa68ddb21b7a3117ad.zip |
PR:
Submitted by:
Reviewed by:
Approved by: gabor (mentor)
Obtained from:
MFC after:
Security:
Feature safe: yes
clean up a little for portlint sake.
The check for sa rules was not running right (rc keeps changing)
Backport DCC.pm from SA 3.4.0, SA bug: 6698
Notes
Notes:
svn path=/head/; revision=286585
Diffstat (limited to 'mail')
-rw-r--r-- | mail/p5-Mail-SpamAssassin/Makefile | 18 | ||||
-rw-r--r-- | mail/p5-Mail-SpamAssassin/files/patch-bug6698 | 1471 | ||||
-rw-r--r-- | mail/p5-Mail-SpamAssassin/pkg-install | 40 |
3 files changed, 1497 insertions, 32 deletions
diff --git a/mail/p5-Mail-SpamAssassin/Makefile b/mail/p5-Mail-SpamAssassin/Makefile index cec21770921a..03d2e356de74 100644 --- a/mail/p5-Mail-SpamAssassin/Makefile +++ b/mail/p5-Mail-SpamAssassin/Makefile @@ -7,7 +7,7 @@ PORTNAME= Mail-SpamAssassin PORTVERSION= 3.3.2 -PORTREVISION= 2 +PORTREVISION= 3 CATEGORIES= mail perl5 MASTER_SITES= ${MASTER_SITE_APACHE:S/$/:apache/} ${MASTER_SITE_PERL_CPAN:S/$/:cpan/} MASTER_SITE_SUBDIR= spamassassin/source/:apache Mail/:cpan @@ -35,15 +35,13 @@ CONFLICTS= ja-p5-Mail-SpamAssassin-[0-9]* PERL_CONFIGURE= yes USE_PERL5_RUN= 5.8.8+ USE_LDCONFIG= yes +DBDIR?= /var/db +CONTACT_ADDRESS?= The administrator of that system +USERS?= spamd +GROUPS?= spamd CONFIGURE_ARGS= SYSCONFDIR="${PREFIX}/etc" \ CONTACT_ADDRESS="${CONTACT_ADDRESS}" \ - LOCALSTATEDIR="/var/db/spamassassin" - -USERS= spamd -GROUPS= spamd - -# You can override it if you like -CONTACT_ADDRESS?= The administrator of that system + LOCALSTATEDIR="${DBDIR}/spamassassin" OPTIONS= AS_ROOT "Run spamd as root (recommended)" on \ SPAMC "Build spamd/spamc (not for amavisd)" on \ @@ -280,7 +278,7 @@ post-build: .endif pre-su-install: - @USER=${USERS} GROUP=${GROUPS} ${SH} ${PKGINSTALL} ${PKGNAME} PRE-INSTALL + @PREFIX=${PREFIX} BATCH=${BATCH} SU_CMD="${SU_CMD}" USER=${USERS} GROUP=${GROUPS} INSTALL="${INSTALL}" ${SH} ${PKGINSTALL} ${PKGNAME} PRE-INSTALL @${INSTALL_PROGRAM} ${WRKSRC}/spamc/libspamc.so ${PREFIX}/lib/libspamc.so.0 @${LN} -sf libspamc.so.0 ${PREFIX}/lib/libspamc.so .if !defined(WITHOUT_SSL) @@ -305,7 +303,7 @@ post-install: @[ -f ${PREFIX}/etc/mail/spamassassin/v320.pre ] || \ ${CP} ${PREFIX}/etc/mail/spamassassin/v320.pre.sample \ ${PREFIX}/etc/mail/spamassassin/v320.pre - @PKG_PREFIX=${PREFIX} BATCH=${BATCH} SU_CMD="${SU_CMD}" USER=${USERS} GROUP=${GROUPS} ${SH} ${PKGDIR}/pkg-install ${PKGNAME} POST-INSTALL + @PREFIX=${PREFIX} BATCH=${BATCH} SU_CMD="${SU_CMD}" USER=${USERS} GROUP=${GROUPS} INSTALL="${INSTALL}" ${SH} ${PKGINSTALL} ${PKGNAME} POST-INSTALL @[ -f ${PREFIX}/etc/mail/spamassassin/v330.pre ] || \ ${CP} ${PREFIX}/etc/mail/spamassassin/v330.pre.sample \ ${PREFIX}/etc/mail/spamassassin/v330.pre diff --git a/mail/p5-Mail-SpamAssassin/files/patch-bug6698 b/mail/p5-Mail-SpamAssassin/files/patch-bug6698 new file mode 100644 index 000000000000..4df2383b16fc --- /dev/null +++ b/mail/p5-Mail-SpamAssassin/files/patch-bug6698 @@ -0,0 +1,1471 @@ +--- lib/Mail/SpamAssassin/Plugin/DCC.pm 2011-06-06 19:59:17.000000000 -0400 ++++ lib/Mail/SpamAssassin/Plugin/DCC.pm 2011-11-26 07:22:36.000000000 -0500 +@@ -15,6 +15,20 @@ + # limitations under the License. + # </@LICENSE> + ++# Changes since SpamAssassin 3.3.2: ++# support for DCC learning. See dcc_learn_score. ++# deal with orphan dccifd sockets ++# use `cdcc -q` to not stall waiting to find a DCC server when deciding ++# whether DCC checks are enabled ++# use dccproc -Q or dccifd query if a pre-existing X-DCC header shows ++# the message has already been reported ++# dccproc now uses -w /var/dcc/whiteclnt so it acts more like dccifd ++# warn about the use of ancient versions of dccproc and dccifd ++# turn off dccifd greylisting ++# query instead of reporting mail messages that contain X-DCC headers and ++# and so has probably already been reported ++# try harder to find dccproc and cdcc when not explicitly configured ++ + =head1 NAME + + Mail::SpamAssassin::Plugin::DCC - perform DCC check of messages +@@ -30,30 +44,31 @@ + + The DCC or Distributed Checksum Clearinghouse is a system of servers + collecting and counting checksums of millions of mail messages. +-TheSpamAssassin.pm counts can be used by SpamAssassin to detect and +-reject or filter spam. +- +-Because simplistic checksums of spam can be easily defeated, the main +-DCC checksums are fuzzy and ignore aspects of messages. The fuzzy +-checksums are changed as spam evolves. ++The counts can be used by SpamAssassin to detect and filter spam. + +-Note that DCC is disabled by default in C<init.pre> because it is not +-open source. See the DCC license for more details. ++See http://www.dcc-servers.net/dcc/ for more information about DCC. + +-See http://www.rhyolite.com/anti-spam/dcc/ for more information about +-DCC. ++Note that DCC is disabled by default in C<v310.pre> because its use requires ++software that is not distributed with SpamAssassin and that has license ++restrictions for certain commercial uses. ++See the DCC license at http://www.dcc-servers.net/dcc/LICENSE for details. ++ ++Enable it by uncommenting the "loadplugin Mail::SpamAssassin::Plugin::DCC" ++confdir/v310.pre or by adding this line to your local.pre. It might also ++be necessary to install a DCC package, port, rpm, or equivalent from your ++operating system distributor or a tarball from the primary DCC source ++at http://www.dcc-servers.net/dcc/#download ++See also http://www.dcc-servers.net/dcc/INSTALL.html + + =head1 TAGS + + The following tags are added to the set, available for use in reports, + header fields, other plugins, etc.: + +- _DCCB_ DCC server ID in a response +- _DCCR_ response from DCC - header field body in X-DCC-*-Metrics +- _DCCREP_ response from DCC - DCC reputation in percents (0..100) +- +-Tag _DCCREP_ provides a nonempty value only with commercial DCC systems. +-This is the percentage of spam vs. ham sent from the first untrusted relay. ++ _DCCB_ DCC server ID in X-DCC-*-Metrics header field name ++ _DCCR_ X-DCC-*-Metrics header field body ++ _DCCREP_ DCC Reputation or percent bulk mail (0..100) from ++ commercial DCC software + + =cut + +@@ -75,8 +90,6 @@ + use vars qw(@ISA); + @ISA = qw(Mail::SpamAssassin::Plugin); + +-use vars qw($have_inet6); +- + sub new { + my $class = shift; + my $mailsaobject = shift; +@@ -87,7 +100,7 @@ + + # are network tests enabled? + if ($mailsaobject->{local_tests_only}) { +- $self->{dcc_disabled} = 1; ++ $self->{use_dcc} = 0; + dbg("dcc: local tests only, disabling DCC"); + } + else { +@@ -128,20 +141,23 @@ + + =item dcc_fuz2_max NUMBER + +-This option sets how often a message's body/fuz1/fuz2 checksum must have been +-reported to the DCC server before SpamAssassin will consider the DCC check as +-matched. +- +-As nearly all DCC clients are auto-reporting these checksums, you should set +-this to a relatively high value, e.g. C<999999> (this is DCC's MANY count). ++Sets how often a message's body/fuz1/fuz2 checksum must have been reported ++to the DCC server before SpamAssassin will consider the DCC check hit. ++C<999999> is DCC's MANY count. + + The default is C<999999> for all these options. + + =item dcc_rep_percent NUMBER + +-Only commercial DCC systems provide DCC reputation information. This is the +-percentage of spam vs. ham sent from the first untrusted relay. It will hit +-on new spam from spam sources. Default is C<90>. ++Only the commercial DCC software provides DCC Reputations. A DCC Reputation ++is the percentage of bulk mail received from the last untrusted relay in the ++path taken by a mail message as measured by all commercial DCC installations. ++See http://www.rhyolite.com/dcc/reputations.html ++You C<must> whitelist your trusted relays or MX servers with MX or ++MXDCC lines in /var/dcc/whiteclnt as described in the main DCC man page ++to avoid seeing your own MX servers as sources of bulk mail. ++See http://www.dcc-servers.net/dcc/dcc-tree/dcc.html#White-and-Blacklists ++The default is C<90>. + + =cut + +@@ -189,13 +205,9 @@ + =item dcc_home STRING + + This option tells SpamAssassin where to find the dcc homedir. +-If not given, it will try to get dcc to specify one, and if that fails it +-will try dcc's own default homedir of '/var/dcc'. +-If C<dcc_path> is not specified, it will default to looking in +-C<dcc_home/bin> for dcc client instead of relying on SpamAssassin to find it +-in the current PATH. If it isn't found there, it will look in the current +-PATH. If a C<dccifd> socket is found in C<dcc_home> or specified explicitly, +-it will use that interface instead of C<dccproc>. ++If not specified, try to use the locally configured directory ++from the C<cdcc homedir> command. ++Try /var/dcc if that command fails. + + =cut + +@@ -205,7 +217,7 @@ + type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, + code => sub { + my ($self, $key, $value, $line) = @_; +- if (!defined $value || !length $value) { ++ if (!defined $value || $value eq '') { + return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; + } + $value = untaint_file_path($value); +@@ -223,14 +235,16 @@ + + =item dcc_dccifd_path STRING + +-This option tells SpamAssassin where to find the dccifd socket. If +-C<dcc_dccifd_path> is not specified, it will default to looking for a socket +-named C<dccifd> in a directory C<dcc_home>. The C<dcc_dccifd_path> can be +-a Unix socket name (absolute path), or an INET socket specification in a form +-C<[host]:port> or C<host:port>, where a host can be an IPv4 or IPv6 address +-or a host name, and port is a TCP port number. In case of an IPv6 address the +-brackets are required syntax. If a C<dccifd> socket is found, the plugin will +-use it instead of C<dccproc>. ++This option tells SpamAssassin where to find the dccifd socket instead ++of a local Unix socket named C<dccifd> in the C<dcc_home> directory. ++If a socket is specified or found, use it instead of C<dccproc>. ++ ++If specifed, C<dcc_dccifd_path> is the absolute path of local Unix socket ++or an INET socket specified as C<[Host]:Port> or C<Host:Port>. ++Host can be an IPv4 or IPv6 address or a host name ++Port is a TCP port number. The brackets are required for an IPv6 address. ++ ++The default is C<undef>. + + =cut + +@@ -240,45 +254,60 @@ + type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, + code => sub { + my ($self, $key, $value, $line) = @_; +- $value = '' if !defined $value; +- $self->{dcc_dccifd_path_raw} = $value; # for logging purposes +- undef $self->{dcc_dccifd_host}; +- undef $self->{dcc_dccifd_port}; +- undef $self->{dcc_dccifd_socket}; +- local($1,$2,$3); +- if ($value eq '') { ++ ++ if (!defined $value || $value eq '') { + return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; +- } elsif ($value =~ m{^ (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) \z}sx) { +- # "[host]:port" or "host:port", where a host can be an IPv4 or IPv6 +- # address or a host name, and port is a TCP port number or service name +- my $host = defined $1 ? $1 : $2; +- my $port = $3; +- $self->{dcc_dccifd_host} = untaint_var($host); +- $self->{dcc_dccifd_port} = untaint_var($port); +- dbg("config: dcc_dccifd_path set to [%s]:%s", $host,$port); +- } else { # assume a unix socket ++ } ++ ++ local($1,$2,$3); ++ if ($value =~ m{^ (?: \[ ([^\]]*) \] | ([^:]*) ) : ([^:]*) \z}sx) { ++ my $host = untaint_var(defined $1 ? $1 : $2); ++ my $port = untaint_var($3); ++ if (!$host) { ++ info("config: missing or bad host name in dcc_dccifd_path '$value'"); ++ return $Mail::SpamAssassin::Conf::INVALID_VALUE; ++ } ++ if (!$port || $port !~ /^\d+\z/ || $port < 1 || $port > 65535) { ++ info("config: bad TCP port number in dcc_dccifd_path '$value'"); ++ return $Mail::SpamAssassin::Conf::INVALID_VALUE; ++ } ++ ++ $self->{dcc_dccifd_host} = $host; ++ $self->{dcc_dccifd_port} = $port; ++ if ($host !~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/) { ++ # remember to try IPv6 if we can with a host name or non-IPv4 address ++ $self->{dcc_dccifd_IPv6} = eval { require IO::Socket::INET6 }; ++ } ++ dbg("config: dcc_dccifd_path set to [%s]:%s", $host, $port); ++ ++ } else { ++ # assume a unix socket + if ($value !~ m{^/}) { +- info("config: dcc_dccifd_path should be an absolute socket path"); ++ info("config: dcc_dccifd_path '$value' is not an absolute path"); + # return $Mail::SpamAssassin::Conf::INVALID_VALUE; # abort or accept? + } + $value = untaint_file_path($value); +- # test disabled, dccifd may not yet be running at spamd startup time +- # if (!-S $value) { +- # info("config: dcc_dccifd_path '$value' isn't a local socket"); +- # return $Mail::SpamAssassin::Conf::INVALID_VALUE; +- # } ++ + $self->{dcc_dccifd_socket} = $value; + dbg("config: dcc_dccifd_path set to local socket %s", $value); ++ dbg("dcc: dcc_dccifd_path set to local socket %s", $value); + } ++ ++ $self->{dcc_dccifd_path_raw} = $value; + } + }); + + =item dcc_path STRING + +-This option tells SpamAssassin specifically where to find the C<dccproc> +-client instead of relying on SpamAssassin to find it in the current PATH. +-Note that if I<taint mode> is enabled in the Perl interpreter, you should +-use this, as the current PATH will have been cleared. ++Where to find the C<dccproc> client program instead of relying on SpamAssassin ++to find it in the current PATH or C<dcc_home/bin>. This must often be set, ++because the current PATH is cleared by I<taint mode> in the Perl interpreter, ++ ++If a C<dccifd> socket is found in C<dcc_home> or specified explicitly ++with C<dcc_dccifd_path>, use the C<dccifd(8)> interface instead of C<dccproc>. ++ ++The default is C<undef>. ++ + + =cut + +@@ -289,12 +318,12 @@ + type => $Mail::SpamAssassin::Conf::CONF_TYPE_STRING, + code => sub { + my ($self, $key, $value, $line) = @_; +- if (!defined $value || !length $value) { ++ if (!defined $value || $value eq '') { + return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE; + } + $value = untaint_file_path($value); + if (!-x $value) { +- info("config: dcc_path '$value' isn't an executable"); ++ info("config: dcc_path '$value' is not executable"); + return $Mail::SpamAssassin::Conf::INVALID_VALUE; + } + +@@ -304,7 +333,7 @@ + + =item dcc_options options + +-Specify additional options to the dccproc(8) command. Please note that only ++Specify additional options to the dccproc(8) command. Only + characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons. + + The default is C<undef>. +@@ -319,6 +348,7 @@ + code => sub { + my ($self, $key, $value, $line) = @_; + if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) { ++ info("config: dcc_options '$value' contains impermissible characters"); + return $Mail::SpamAssassin::Conf::INVALID_VALUE; + } + $self->{dcc_options} = $1; +@@ -327,8 +357,9 @@ + + =item dccifd_options options + +-Specify additional options to send to the dccifd(8) daemon. Please note that only +-characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons. ++Specify additional options to send to the dccifd daemon with ++the ASCII protocol described on the dccifd(8) man page. ++Only characters in the range [0-9A-Za-z ,._/-] are allowed for security reasons. + + The default is C<undef>. + +@@ -342,265 +373,306 @@ + code => sub { + my ($self, $key, $value, $line) = @_; + if ($value !~ m{^([0-9A-Za-z ,._/-]+)$}) { ++ info("config: dccifd_options '$value' contains impermissible characters"); + return $Mail::SpamAssassin::Conf::INVALID_VALUE; + } + $self->{dccifd_options} = $1; + } + }); + ++=item dcc_learn_score n (default: undef) ++ ++Report messages with total scores this much larger than the ++SpamAssassin spam threshold to DCC as spam. ++ ++=cut ++ ++ push (@cmds, { ++ setting => 'dcc_learn_score', ++ is_admin => 1, ++ default => undef, ++ type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC, ++ }); ++ + $conf->{parser}->register_commands(\@cmds); + } + ++ ++ ++ ++sub ck_dir { ++ my ($self, $dir, $tgt, $src) = @_; ++ ++ $dir = untaint_file_path($dir); ++ if (!stat($dir)) { ++ my $dir_errno = 0+$!; ++ if ($dir_errno == ENOENT) { ++ dbg("dcc: $tgt $dir from $src does not exist"); ++ } else { ++ dbg("dcc: $tgt $dir from $src is not accessible: $!"); ++ } ++ return; ++ } ++ if (!-d _) { ++ dbg("dcc: $tgt $dir from $src is not a directory"); ++ return; ++ } ++ ++ $self->{main}->{conf}->{$tgt} = $dir; ++ dbg("dcc: use '$tgt $dir' from $src"); ++} ++ + sub find_dcc_home { + my ($self) = @_; ++ my $dcc_libexec; ++ ++ # just once ++ return if defined $self->{dcc_version}; ++ $self->{dcc_version} = '?'; + + my $conf = $self->{main}->{conf}; +- return if !$conf->{use_dcc}; + +- my $dcchome = $conf->{dcc_home} || ''; + +- # If we're not given the DCC homedir, try getting DCC to tell us it. +- # If that fails, try the DCC default homedir of '/var/dcc'. +- if ($dcchome eq '') { ++ # 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; ++ my $cdcc = $self->dcc_pgm_path('cdcc'); ++ my $cmd = '-qV homedir libexecdir'; ++ if ($cdcc && open(CDCC, "$cdcc $cmd 2>&1 |")) { ++ my $cdcc_output = do { local $/ = undef; <CDCC> }; ++ close CDCC; + +- my $cdcc = Mail::SpamAssassin::Util::find_executable_in_env_path('cdcc'); ++ $cdcc_output =~ s/\n/ /g; # everything in 1 line for debugging ++ 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 : ''; ++ if ($cdcc_output =~ /\s+libexecdir=(\S+)/) { ++ $self->ck_dir($1, 'dcc_libexec', 'cdcc'); ++ } ++ } + +- my $cdcc_home = ''; +- if ($cdcc && -x $cdcc && open(CDCC, "$cdcc homedir 2>&1|")) { +- dbg("dcc: dcc_home not set, querying cdcc utility"); +- $cdcc_home = <CDCC> || ''; +- close CDCC; ++ # without a home, try the homedir from cdcc ++ if (!$conf->{dcc_home} && $cdcc_home) { ++ $self->ck_dir($cdcc_home, 'dcc_home', 'cdcc'); ++ } ++ # finally fall back to /var/dcc ++ if (!$conf->{dcc_home}) { ++ $self->ck_dir($conf->{dcc_home} = '/var/dcc', 'dcc_home', 'default') ++ } + +- chomp $cdcc_home; +- $cdcc_home =~ s/\s+homedir=//; +- dbg("dcc: cdcc reports homedir as '%s'", $cdcc_home); +- } +- +- # try first with whatever the cdcc utility reported +- my $cdcc_home_errno = 0; +- if ($cdcc_home eq '') { +- $cdcc_home_errno = ENOENT; +- } elsif (!stat($cdcc_home)) { +- $cdcc_home_errno = 0+$!; +- } +- if ($cdcc_home_errno == ENOENT) { +- # no such file +- } elsif ($cdcc_home_errno != 0) { +- dbg("dcc: cdcc reported homedir $cdcc_home is not accessible: $!"); +- } elsif (!-d _) { +- dbg("dcc: cdcc reported homedir $cdcc_home is not a directory"); +- } else { # ok +- dbg("dcc: cdcc reported homedir $cdcc_home exists, using it"); +- $dcchome = untaint_var($cdcc_home); +- } +- +- # try falling back to /var/dcc +- if ($dcchome eq '') { +- my $var_dcc_errno = stat('/var/dcc') ? 0 : 0+$!; +- if ($var_dcc_errno == ENOENT) { +- # no such file +- } elsif ($var_dcc_errno != 0) { +- dbg("dcc: dcc_home not set and dcc default homedir /var/dcc ". +- "is not accessible: $!"); +- } elsif (!-d _) { +- dbg("dcc: dcc_home not set and dcc default homedir /var/dcc ". +- "is not a directory"); +- } else { # ok +- dbg("dcc: dcc_home not set but dcc default homedir /var/dcc exists, ". +- "using it"); +- $dcchome = '/var/dcc'; ++ # fall back to $conf->{dcc_home}/libexec or /var/dcc/libexec for dccsight ++ if (!$conf->{dcc_libexec}) { ++ $self->ck_dir($conf->{dcc_home} . '/libexec', 'dcc_libexec', 'dcc_home'); + } ++ if (!$conf->{dcc_libexec}) { ++ $self->ck_dir('/var/dcc/libexec', 'dcc_libexec', 'dcc_home'); + } + +- if ($dcchome eq '') { +- dbg("dcc: unable to get homedir from cdcc ". +- "and the dcc default homedir was not found"); +- } +- +- # Remember found homedir path +- dbg("dcc: using '%s' as DCC homedir", $dcchome); +- $conf->{dcc_home} = $dcchome; ++ # format options for dccifd ++ my $opts = ($conf->{dccifd_options} || '') . "\n"; ++ if ($self->{dcc_version} =~ /\d+\.(\d+)\.(\d+)$/ && ++ ($1 < 3 || ($1 == 3 && $2 < 123))) { ++ if ($1 < 3 || ($1 == 3 && $2 < 50)) { ++ info("dcc: DCC version $self->{dcc_version} is years old, ". ++ "obsolete, and likely to cause problems. ". ++ "See http://www.dcc-servers.net/dcc/old-versions.html"); ++ } ++ $self->{dccifd_lookup_options} = "header " . $opts; ++ $self->{dccifd_report_options} = "header spam " . $opts; ++ } else { ++ # dccifd after version 1.2.123 understands "cksums" and "no-grey" ++ $self->{dccifd_lookup_options} = "cksums grey-off " . $opts; ++ $self->{dccifd_report_options} = "header spam grey-off " . $opts; + } + } + +-sub is_dccifd_available { +- my ($self) = @_; +- ++sub dcc_pgm_path { ++ my ($self, $pgm) = @_; ++ my $pgmpath; + my $conf = $self->{main}->{conf}; +- $self->{dccifd_available} = 0; + +- if (!$conf->{use_dcc}) { +- dbg("dcc: dccifd is not available: use_dcc is false"); +- } elsif (defined $conf->{dcc_dccifd_host}) { +- dbg("dcc: dccifd inet socket chosen: [%s]:%s", +- $conf->{dcc_dccifd_host}, $conf->{dcc_dccifd_port}); +- $self->{dccifd_available} = 1; +- } else { +- my $sockpath = $conf->{dcc_dccifd_socket}; +- my $dcchome = $conf->{dcc_home}; +- if (defined $sockpath) { +- dbg("dcc: dccifd local socket chosen: %s", $sockpath); +- } elsif (defined $conf->{dcc_dccifd_path_raw}) { +- # avoid falling back to defaults if explicitly provided but wrong +- } elsif (defined $dcchome && $dcchome ne '' && -S "$dcchome/dccifd") { +- $sockpath = "$dcchome/dccifd"; +- $conf->{dcc_dccifd_socket} = $sockpath; +- dbg("dcc: dccifd default local socket chosen: %s", $sockpath); ++ $pgmpath = $conf->{dcc_path}; ++ if (defined $pgmpath && $pgmpath ne '') { ++ # accept explicit setting for dccproc ++ return $pgmpath if $pgm eq 'dccproc'; ++ # try adapting it for cdcc and everything else ++ if ($pgmpath =~ s{[^/]+\z}{$pgm}s) { ++ $pgmpath = untaint_file_path($pgmpath); ++ if (-x $pgmpath) { ++ dbg("dcc: dcc_pgm_path, found %s in dcc_path: %s", $pgm,$pgmpath); ++ return $pgmpath; + } +- if (defined $sockpath && -S $sockpath && -w _ && -r _) { +- $self->{dccifd_available} = 1; +- } elsif (!defined $conf->{dcc_dccifd_path_raw}) { +- dbg("dcc: dccifd is not available: no r/w dccifd socket found"); +- } else { +- dbg("dcc: dccifd is not available: no r/w dccifd socket found: %s", +- $conf->{dcc_dccifd_path_raw}); + } + } + +- return $self->{dccifd_available}; ++ $pgmpath = Mail::SpamAssassin::Util::find_executable_in_env_path($pgm); ++ if (defined $pgmpath) { ++ dbg("dcc: dcc_pgm_path, found %s in env.path: %s", $pgm,$pgmpath); ++ return $pgmpath; ++ } ++ ++ # try dcc_home/bin, dcc_libexec, and some desperate last attempts ++ foreach my $dir ($conf->{dcc_home}.'/bin', $conf->{dcc_libexec}, ++ '/usr/local/bin', '/usr/local/dcc', '/var/dcc') { ++ $pgmpath = $dir . '/' . $pgm; ++ if (-x $pgmpath) { ++ dbg("dcc: dcc_pgm_path, found %s in %s: %s", $pgm,$dir,$pgmpath); ++ return $pgmpath; ++ } ++ } ++ ++ return; + } + +-sub is_dccproc_available { ++sub is_dccifd_available { + my ($self) = @_; + my $conf = $self->{main}->{conf}; + +- $self->{dccproc_available} = 0; ++ # dccifd remains available until it breaks ++ return $self->{dccifd_available} if $self->{dccifd_available}; + +- if (!$conf->{use_dcc}) { +- dbg("dcc: dccproc is not available: use_dcc is false"); +- return 0; ++ # deal with configured INET socket ++ if (defined $conf->{dcc_dccifd_host}) { ++ dbg("dcc: dccifd is available via INET socket [%s]:%s", ++ $conf->{dcc_dccifd_host}, $conf->{dcc_dccifd_port}); ++ return ($self->{dccifd_available} = 1); + } +- my $dcchome = $conf->{dcc_home} || ''; +- my $dccproc = $conf->{dcc_path} || ''; + +- if ($dccproc eq '' && ($dcchome ne '' && -x "$dcchome/bin/dccproc")) { +- $dccproc = "$dcchome/bin/dccproc"; ++ # the first time here, compute a default local socket based on DCC home ++ # from self->find_dcc_home() called elsewhere ++ my $sockpath = $conf->{dcc_dccifd_socket}; ++ if (!$sockpath) { ++ if ($conf->{dcc_dccifd_path_raw}) { ++ $sockpath = $conf->{dcc_dccifd_path_raw}; ++ } else { ++ $sockpath = "$conf->{dcc_home}/dccifd"; + } +- if ($dccproc eq '') { +- $dccproc = Mail::SpamAssassin::Util::find_executable_in_env_path('dccproc'); ++ $conf->{dcc_dccifd_socket} = $sockpath; + } + +- unless (defined $dccproc && $dccproc ne '' && -x $dccproc) { +- dbg("dcc: dccproc is not available: no dccproc executable found"); +- return 0; +- } ++ # check the socket every time because it can appear and disappear ++ return ($self->{dccifd_available} = 1) if (-S $sockpath && -w _ && -r _); + +- # remember any found dccproc ++ dbg("dcc: dccifd is not available; no r/w socket at %s", $sockpath); ++ return ($self->{dccifd_available} = 0); ++} ++ ++sub is_dccproc_available { ++ my ($self) = @_; ++ my $conf = $self->{main}->{conf}; ++ ++ # dccproc remains (un)available so check only once ++ return $self->{dccproc_available} if defined $self->{dccproc_available}; ++ ++ my $dccproc = $conf->{dcc_path}; ++ if (!defined $dccproc || $dccproc eq '') { ++ $dccproc = $self->dcc_pgm_path('dccproc'); + $conf->{dcc_path} = $dccproc; ++ if (!$dccproc || ! -x $dccproc) { ++ dbg("dcc: dccproc is not available: no dccproc executable found"); ++ return ($self->{dccproc_available} = 0); ++ } ++ } + +- dbg("dcc: dccproc is available: %s", $conf->{dcc_path}); +- $self->{dccproc_available} = 1; +- return 1; ++ dbg("dcc: %s is available", $conf->{dcc_path}); ++ return ($self->{dccproc_available} = 1); + } + + sub dccifd_connect { +- my($self) = @_; ++ my($self, $tag) = @_; + my $conf = $self->{main}->{conf}; + my $sockpath = $conf->{dcc_dccifd_socket}; +- my $host = $conf->{dcc_dccifd_host}; +- my $port = $conf->{dcc_dccifd_port}; + my $sock; ++ + if (defined $sockpath) { +- dbg("dcc: connecting to a local socket %s", $sockpath); +- $sock = IO::Socket::UNIX->new( +- Type => SOCK_STREAM, Peer => $sockpath); +- $sock or die "dcc: failed to connect to a socket $sockpath: $!\n"; +- } elsif (defined $host) { +- my $specified_path = $conf->{dcc_dccifd_path_raw}; +- if ($host eq '') { +- die "dcc: empty host specification: $specified_path\n"; +- } +- if (!defined $port || $port !~ /^\d+\z/ || $port < 1 || $port > 65535) { +- die "dcc: bad TCP port number: $specified_path\n"; +- } +- my $is_inet4 = $host =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/; +- if ($is_inet4) { # inet4 socket (IPv4 address) +- dbg("dcc: connecting to inet4 socket [%s]:%s", $host,$port); +- $sock = IO::Socket::INET->new( +- Proto => 'tcp', PeerAddr => $host, PeerPort => $port); +- } else { +- if (!defined $have_inet6) { +- $have_inet6 = eval { require IO::Socket::INET6 }; +- $have_inet6 = 0 if !defined $have_inet6; ++ $sock = IO::Socket::UNIX->new(Type => SOCK_STREAM, Peer => $sockpath); ++ if ($sock) { ++ dbg("$tag connected to local socket %s", $sockpath); ++ return $sock; + } +- if (!$have_inet6) { # fallback to an inet4 socket (IPv4) +- dbg("dcc: connecting(2) to inet4 socket [%s]:%s", $host,$port); +- $sock = IO::Socket::INET->new( +- Proto => 'tcp', PeerAddr => $host, PeerPort => $port); +- } else { # inet6 socket (IPv6) or a host name +- dbg("dcc: connecting to inet6 socket [%s]:%s", $host,$port); ++ $self->{dccifd_available} = 0; ++ info("$tag failed to connect to local socket $sockpath"); ++ return $sock ++ } ++ ++ # must be TCP/IP ++ my $host = $conf->{dcc_dccifd_host}; ++ my $port = $conf->{dcc_dccifd_port}; ++ ++ if ($conf->{dcc_dccifd_IPv6}) { ++ # try IPv6 if we can with a host name or non-IPv4 address ++ dbg("$tag connecting to inet6 socket [%s]:%s", $host,$port); + $sock = IO::Socket::INET6->new( + Proto => 'tcp', PeerAddr => $host, PeerPort => $port); ++ # fall back to IPv4 if that failed + } ++ if (!$sock) { ++ dbg("$tag connecting to inet4 socket [%s]:%s", $host, $port); ++ $sock = IO::Socket::INET->new( ++ Proto => 'tcp', PeerAddr => $host, PeerPort => $port); + } +- $sock or die "dcc: failed to connect to [$host]:$port : $!\n"; +- } else { +- die "dcc: dccifd socket not provided: $conf->{dcc_dccifd_path_raw}\n"; +- } ++ ++ info("failed to connect to [$host]:$port : $!") if !$sock; + return $sock; + } + ++# 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 ($self->is_dccifd_available()) { +- $self->{dcc_interface} = "dccifd"; +- $self->{dcc_disabled} = 0; +- } +- elsif ($self->is_dccproc_available()) { +- $self->{dcc_interface} = "dccproc"; +- $self->{dcc_disabled} = 0; ++ if (!$conf->{use_dcc}) { ++ $self->{dcc_disabled} = 1; ++ return; + } +- else { +- dbg("dcc: dccifd and dccproc are not available, disabling DCC"); +- $self->{dcc_interface} = "none"; ++ ++ $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; + } ++ ++ $self->{dcc_disabled} = 0; + } + + sub dcc_query { +- my ($self, $permsgstatus, $full) = @_; ++ my ($self, $permsgstatus, $fulltext) = @_; + + $permsgstatus->{dcc_checked} = 1; + ++ if (!$self->{main}->{conf}->{use_dcc}) { ++ dbg("dcc: DCC is not available: use_dcc is 0"); ++ return; ++ } ++ + # initialize valid tags + $permsgstatus->{tag_data}->{DCCB} = ""; + $permsgstatus->{tag_data}->{DCCR} = ""; + $permsgstatus->{tag_data}->{DCCREP} = ""; + +- # short-circuit if there's already a X-DCC header with value of +- # "bulk" from an upstream DCC check +- if ($permsgstatus->get('ALL') =~ +- /^(X-DCC-([^:]{1,80})?-?Metrics:.*bulk.*)$/m) { +- $permsgstatus->{dcc_response} = $1; ++ if ($$fulltext eq '') { ++ dbg("dcc: empty message; skipping dcc check"); + return; + } + +- my $timer = $self->{main}->time_method("check_dcc"); ++ if ($permsgstatus->get('ALL') =~ /^(X-DCC-.*-Metrics:.*)$/m) { ++ $permsgstatus->{dcc_raw_x_dcc} = $1; ++ # 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 /; ++ } + +- $self->find_dcc_home(); ++ my $timer = $self->{main}->time_method("check_dcc"); + + $self->get_dcc_interface(); +- my $result; +- if ($self->{dcc_disabled}) { +- $result = 0; +- } elsif ($$full eq '') { +- dbg("dcc: empty message, skipping dcc check"); +- $result = 0; +- } elsif ($self->{dccifd_available}) { +- my $client = $permsgstatus->{relays_external}->[0]->{ip}; +- my $clientname = $permsgstatus->{relays_external}->[0]->{rdns}; +- my $helo = $permsgstatus->{relays_external}->[0]->{helo} || ""; +- if ($client) { +- $client = $client . "\r" . $clientname if $clientname; +- } else { +- $client = "0.0.0.0"; +- } +- $self->dccifd_lookup($permsgstatus, $full, $client, $clientname, $helo); +- } else { +- my $client = $permsgstatus->{relays_external}->[0]->{ip}; +- $self->dccproc_lookup($permsgstatus, $full, $client); +- } ++ return if $self->{dcc_disabled}; ++ ++ my $envelope = $permsgstatus->{relays_external}->[0]; ++ ($permsgstatus->{dcc_raw_x_dcc}, ++ $permsgstatus->{dcc_cksums}) = $self->ask_dcc("dcc:", $permsgstatus, ++ $fulltext, $envelope); + } + + sub check_dcc { +@@ -609,28 +681,27 @@ + + $self->dcc_query($permsgstatus, $full) if !$permsgstatus->{dcc_checked}; + +- my $response = $permsgstatus->{dcc_response}; +- return 0 if !defined $response || $response eq ''; ++ my $x_dcc = $permsgstatus->{dcc_raw_x_dcc}; ++ return 0 if !defined $x_dcc || $x_dcc eq ''; + +- local($1,$2); +- if ($response =~ /^X-DCC-(.*)-Metrics: (.*)$/) { +- $permsgstatus->{tag_data}->{DCCB} = $1; +- $permsgstatus->{tag_data}->{DCCR} = $2; ++ if ($x_dcc =~ /^X-DCC-(.*)-Metrics: (.*)$/) { ++ $permsgstatus->set_tag('DCCB', $1); ++ $permsgstatus->set_tag('DCCR', $2); + } +- $response =~ s/many/999999/ig; +- $response =~ s/ok\d?/0/ig; ++ $x_dcc =~ s/many/999999/ig; ++ $x_dcc =~ s/ok\d?/0/ig; + + my %count = (body => 0, fuz1 => 0, fuz2 => 0, rep => 0); +- if ($response =~ /\bBody=(\d+)/) { ++ if ($x_dcc =~ /\bBody=(\d+)/) { + $count{body} = $1+0; + } +- if ($response =~ /\bFuz1=(\d+)/) { ++ if ($x_dcc =~ /\bFuz1=(\d+)/) { + $count{fuz1} = $1+0; + } +- if ($response =~ /\bFuz2=(\d+)/) { ++ if ($x_dcc =~ /\bFuz2=(\d+)/) { + $count{fuz2} = $1+0; + } +- if ($response =~ /\brep=(\d+)/) { ++ if ($x_dcc =~ /\brep=(\d+)/) { + $count{rep} = $1+0; + } + if ($count{body} >= $conf->{dcc_body_max} || +@@ -651,185 +722,185 @@ + } + + sub check_dcc_reputation_range { +- my ($self, $permsgstatus, $full, $min, $max) = @_; +- $self->dcc_query($permsgstatus, $full) if !$permsgstatus->{dcc_checked}; ++ 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; ++ } + +- my $response = $permsgstatus->{dcc_response}; +- return 0 if !defined $response || $response eq ''; ++ # 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 = 999 if !defined $max; ++ $max = 100 if !defined $max; + +- local $1; +- my $dcc_rep; +- $dcc_rep = $1+0 if defined $response && $response =~ /\brep=(\d+)/; +- if (defined $dcc_rep) { +- $dcc_rep = int($dcc_rep); # just in case, rule ranges are integer percents + 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'); +- $permsgstatus->{tag_data}->{DCCREP} = $dcc_rep; +- return $dcc_rep >= $min && $dcc_rep <= $max ? 1 : 0; ++ return $result; ++} ++ ++# get the X-DCC header line and save the checksums from dccifd or dccproc ++sub parse_dcc_response { ++ my ($self, $resp) = @_; ++ my ($raw_x_dcc, $cksums); ++ ++ # The first line is the header we want. It uses SMTP folded whitespace ++ # if it is long. The folded whitespace is always a single \t. ++ chomp($raw_x_dcc = shift @$resp); ++ my $v; ++ while (($v = shift @$resp) && $v =~ s/^\t(.+)\s*\n/ $1/) { ++ $raw_x_dcc .= $v; ++ } ++ ++ # skip the "reported:" line between the X-DCC header and any checksums ++ # remove ':' to avoid a bug in versions 1.3.115 - 1.3.122 in dccsight ++ # with the length of "Message-ID:" ++ $cksums = ''; ++ while (($v = shift @$resp) && $v =~ s/^([^:]*):/$1/) { ++ $cksums .= $v; + } +- return 0; ++ ++ return ($raw_x_dcc, $cksums); + } + +-sub dccifd_lookup { +- my ($self, $permsgstatus, $fulltext, $client, $clientname, $helo) = @_; ++sub ask_dcc { ++ my ($self, $tag, $permsgstatus, $fulltext, $envelope) = @_; + my $conf = $self->{main}->{conf}; +- my $response; +- my $left; +- my $right; +- my $timeout = $conf->{dcc_timeout}; +- my $opts = $conf->{dccifd_options}; +- my @opts = !defined $opts ? () : split(' ',$opts); ++ my ($pgm, $err, $sock, $pid, @resp); ++ my ($client, $clientname, $helo, $opts); + + $permsgstatus->enter_helper_run_mode(); + ++ my $timeout = $conf->{dcc_timeout}; + my $timer = Mail::SpamAssassin::Timeout->new( + { secs => $timeout, deadline => $permsgstatus->{master_deadline} }); +- my $err = $timer->run_and_catch(sub { + ++ $err = $timer->run_and_catch(sub { + local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; + +- my $sock = $self->dccifd_connect(); +- $sock or die "dcc: failed to connect to a dccifd socket"; +- +- # send the options and other parameters to the daemon +- $sock->print("header " . join(" ",@opts) . "\n") +- or die "dcc: failed write"; # options +- $sock->print($client . "\n") or die "dcc: failed write"; # client +- $sock->print($helo . "\n") or die "dcc: failed write"; # HELO value +- $sock->print("\n") or die "dcc: failed write"; # sender +- $sock->print("unknown\r\n") or die "dcc: failed write"; # recipients +- $sock->print("\n") or die "dcc: failed write"; # recipients +- +- $sock->print($$fulltext) or die "dcc: failed write"; +- +- $sock->shutdown(1) or die "dcc: failed socket shutdown: $!"; +- +- $sock->getline() or die "dcc: failed read status"; +- $sock->getline() or die "dcc: failed read multistatus"; ++ # prefer dccifd to dccproc ++ if ($self->{dccifd_available}) { ++ $pgm = 'dccifd'; + +- my @null = $sock->getlines(); +- if (!@null) { +- # no facility prefix on this +- die "dcc: failed to read header\n"; +- } ++ $sock = $self->dccifd_connect($tag); ++ if (!$sock) { ++ $self->{dccifd_available} = 0; ++ die("dccproc not available") if (!$self->is_dccproc_available()); + +- # the first line will be the header we want to look at +- chomp($response = shift @null); +- # but newer versions of DCC fold the header if it's too long... +- while (my $v = shift @null) { +- last unless ($v =~ s/^\s+/ /); # if this line wasn't folded, stop +- chomp $v; +- $response .= $v; ++ # 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("dcc: dccifd got response: %s", $response); +- +- }); +- +- $permsgstatus->leave_helper_run_mode(); +- +- if ($timer->timed_out()) { +- dbg("dcc: dccifd check timed out after $timeout secs."); +- return; + } + +- if ($err) { +- chomp $err; +- warn("dcc: dccifd -> check skipped: $err\n"); +- return; +- } ++ if ($self->{dccifd_available}) { + +- if (!defined $response || $response !~ /^X-DCC/) { +- dbg("dcc: dccifd check failed - no X-DCC returned: %s", $response); +- return; ++ # send the options and other parameters to the daemon ++ $client = $envelope->{ip}; ++ $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 { ++ $opts = $self->{dccifd_lookup_options}; ++ # only query if there is an X-DCC header ++ $opts =~ s/grey-off/& query/ if defined $permsgstatus->{dcc_raw_x_dcc}; ++ } ++ $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: $!"; + +- $response =~ s/[ \t]\z//; # strip trailing whitespace +- $permsgstatus->{dcc_response} = $response; +-} ++ $sock->getline() or die "failed read status\n"; ++ $sock->getline() or die "failed read multistatus\n"; + +-sub dccproc_lookup { +- my ($self, $permsgstatus, $fulltext, $client) = @_; +- my $conf = $self->{main}->{conf}; +- my $response; +- my %count = (body => 0, fuz1 => 0, fuz2 => 0, rep => 0); +- my $timeout = $conf->{dcc_timeout}; ++ @resp = $sock->getlines(); ++ die "failed to read dccifd response\n" if !@resp; + +- $permsgstatus->enter_helper_run_mode(); +- +- # use a temp file here -- open2() is unreliable, buffering-wise, under spamd ++ } 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 $pid; +- +- my $timer = Mail::SpamAssassin::Timeout->new( +- { secs => $timeout, deadline => $permsgstatus->{master_deadline} }); +- my $err = $timer->run_and_catch(sub { +- +- local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; + +- # note: not really tainted, this came from system configuration file +- my $path = untaint_file_path($conf->{dcc_path}); +- +- my $opts = $conf->{dcc_options}; ++ my $path = $conf->{dcc_path}; ++ $opts = $conf->{dcc_options}; + my @opts = !defined $opts ? () : split(' ',$opts); + untaint_var(\@opts); ++ unshift(@opts, '-w', 'whiteclnt'); ++ $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', 'many') if defined $permsgstatus->{dcc_raw_x_dcc}; ++ } else { ++ # learn or report spam ++ unshift(@opts, '-t', 'many'); ++ } + +- unshift(@opts, "-a", +- untaint_var($client)) if defined $client && $client ne ''; +- +- dbg("dcc: opening pipe: %s", +- join(' ', $path, "-H", "-x", "0", @opts, "< $tmpf")); ++ dbg("$tag opening pipe to %s", ++ join(' ', $path, "-C", "-x", "0", @opts, "<$tmpf")); + + $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC, +- $tmpf, 1, $path, "-H", "-x", "0", @opts); ++ $tmpf, 1, $path, "-C", "-x", "0", @opts); + $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 } + defined $nread or die "error reading from pipe: $!"; +- my @null = split(/^/m, $resp, -1); undef $resp; ++ @resp = split(/^/m, $resp, -1); undef $resp; + + my $errno = 0; close DCC or $errno = $!; + proc_status_ok($?,$errno) +- or info("dcc: [%s] finished: %s", $pid, exit_status_str($?,$errno)); +- +- if (!@null) { +- # no facility prefix on this +- die "failed to read header\n"; +- } ++ or info("$tag [%s] finished: %s", $pid, exit_status_str($?,$errno)); + +- # the first line will be the header we want to look at +- chomp($response = shift @null); +- # but newer versions of DCC fold the header if it's too long... +- while (my $v = shift @null) { +- last unless ($v =~ s/^\s+/ /); # if this line wasn't folded, stop +- chomp $v; +- $response .= $v; ++ die "failed to read X-DCC header from dccproc\n" if !@resp; + } +- +- unless (defined($response)) { +- # no facility prefix on this +- die "no response\n"; # yes, this is possible +- } +- +- dbg("dcc: got response: %s", $response); +- + }); + ++ if ($pgm eq 'dccproc') { + if (defined(fileno(*DCC))) { # still open + if ($pid) { +- if (kill('TERM',$pid)) { dbg("dcc: killed stale helper [$pid]") } +- else { dbg("dcc: killing helper application [$pid] failed: $!") } ++ 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("dcc: [%s] terminated: %s", $pid, exit_status_str($?,$errno)); ++ proc_status_ok($?,$errno) or info("$tag [%s] dccproc terminated: %s", ++ $pid, exit_status_str($?,$errno)); ++ } + } ++ + $permsgstatus->leave_helper_run_mode(); + + if ($timer->timed_out()) { +@@ -833,204 +904,182 @@ + $permsgstatus->leave_helper_run_mode(); + + if ($timer->timed_out()) { +- dbg("dcc: check timed out after $timeout seconds"); +- return; ++ dbg("$tag $pgm timed out after $timeout seconds"); ++ return (undef, undef); + } + + if ($err) { + chomp $err; +- if ($err eq "__brokenpipe__ignore__") { +- dbg("dcc: check failed: broken pipe"); +- } elsif ($err eq "no response") { +- dbg("dcc: check failed: no response"); +- } else { +- warn("dcc: check failed: $err\n"); +- } +- return; ++ info("$tag $pgm failed: $err\n"); ++ return (undef, undef); + } + +- if (!defined($response) || $response !~ /^X-DCC/) { +- $response ||= ''; +- dbg("dcc: check failed: no X-DCC returned (did you create a map file?): %s", $response); +- return; ++ 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 '%s'", $raw_x_dcc); ++ return (undef, undef); + } +- +- $permsgstatus->{dcc_response} = $response; ++ dbg("$tag %s responded with '%s'", $pgm, $raw_x_dcc); ++ return ($raw_x_dcc, $cksums); + } + +-# only supports dccproc right now +-sub plugin_report { ++# tell DCC server that the message is spam according to SpamAssassin ++sub check_post_learn { + my ($self, $options) = @_; + +- return if $options->{report}->{options}->{dont_report_to_dcc}; +- $self->get_dcc_interface(); +- return if $self->{dcc_disabled}; +- +- # get the metadata from the message so we can pass the external relay information +- $options->{msg}->extract_message_metadata($options->{report}->{main}); +- my $client = $options->{msg}->{metadata}->{relays_external}->[0]->{ip}; +- if ($self->{dccifd_available}) { +- my $clientname = $options->{msg}->{metadata}->{relays_external}->[0]->{rdns}; +- my $helo = $options->{msg}->{metadata}->{relays_external}->[0]->{helo} || ""; +- if ($client) { +- if ($clientname) { +- $client = $client . "\r" . $clientname; +- } +- } else { +- $client = "0.0.0.0"; +- } +- if ($self->dccifd_report($options, $options->{text}, $client, $helo)) { +- $options->{report}->{report_available} = 1; +- info("reporter: spam reported to DCC"); +- $options->{report}->{report_return} = 1; ++ # learn only if allowed ++ return if $self->{learn_disabled}; ++ my $conf = $self->{main}->{conf}; ++ if (!$conf->{use_dcc}) { ++ $self->{learn_disabled} = 1; ++ return; + } +- else { +- info("reporter: could not report spam to DCC via dccifd"); ++ my $learn_score = $conf->{dcc_learn_score}; ++ if (!defined $learn_score || $learn_score eq '') { ++ dbg("dcc: DCC learning not enabled by dcc_learn_score"); ++ $self->{learn_disabled} = 1; ++ return; + } +- } else { +- # use temporary file: open2() is unreliable due to buffering under spamd +- my $tmpf = $options->{report}->create_fulltext_tmpfile($options->{text}); + +- if ($self->dcc_report($options, $tmpf, $client)) { +- $options->{report}->{report_available} = 1; +- info("reporter: spam reported to DCC"); +- $options->{report}->{report_return} = 1; ++ # 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(); ++ if ($score < $required_score + $learn_score) { ++ dbg("dcc: score=%d required_score=%d dcc_learn_score=%d", ++ $score, $required_score, $learn_score); ++ return; + } +- else { +- info("reporter: could not report spam to DCC via dccproc"); + } +- $options->{report}->delete_fulltext_tmpfile(); ++ ++ # and if we checked the message ++ return if (!defined $permsgstatus->{dcc_raw_x_dcc}); ++ ++ # 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"); ++ return; + } ++ ++ # 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)); ++ ++ # 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, ++ \$fulltext, $envelope); ++ dbg("dcc: learned as spam") if defined $raw_x_dcc; + } + +-sub dccifd_report { +- my ($self, $options, $fulltext, $client, $helo) = @_; +- my $conf = $self->{main}->{conf}; +- my $timeout = $conf->{dcc_timeout}; +- # instead of header use whatever the report option is +- my $opts = $conf->{dccifd_options}; +- my @opts = !defined $opts ? () : split(' ',$opts); ++sub dccsight_learn { ++ my ($self, $permsgstatus, $old_cksums) = @_; ++ my ($raw_x_dcc, $new_cksums); ++ ++ return 0 if !$old_cksums; ++ ++ my $dccsight = $self->dcc_pgm_path('dccsight'); ++ if (!$dccsight) { ++ info("dcc: cannot find dccsight") if $dccsight eq ''; ++ return 0; ++ } + +- $options->{report}->enter_helper_run_mode(); +- my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout }); ++ $permsgstatus->enter_helper_run_mode(); + +- my $err = $timer->run_and_catch(sub { ++ # 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 $pid; + ++ my $timeout = $self->{main}->{conf}->{dcc_timeout}; ++ my $timer = Mail::SpamAssassin::Timeout->new( ++ { secs => $timeout, deadline => $permsgstatus->{master_deadline} }); ++ my $err = $timer->run_and_catch(sub { + local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; + +- my $sock = $self->dccifd_connect(); +- $sock or die "report: failed to connect to a dccifd socket"; ++ dbg("dcc: opening pipe to %s", ++ join(' ', $dccsight, "-t", "many", "<$tmpf")); + +- # send the options and other parameters to the daemon +- $sock->print("spam " . join(" ",@opts) . "\n") +- or die "report: dccifd failed write"; # options +- $sock->print($client . "\n") +- or die "report: dccifd failed write"; # client +- $sock->print($helo . "\n") +- or die "report: dccifd failed write"; # HELO value +- $sock->print("\n") +- or die "report: dccifd failed write"; # sender +- $sock->print("unknown\r\n") +- or die "report: dccifd failed write"; # recipients +- $sock->print("\n") +- or die "report: dccifd failed write"; # recipients ++ $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC, ++ $tmpf, 1, $dccsight, "-t", "many"); ++ $pid or die "$!\n"; + +- $sock->print($$fulltext) or die "report: dccifd failed write"; ++ # read+split avoids a Perl I/O bug (Bug 5985) ++ my($inbuf,$nread,$resp); $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; + +- $sock->shutdown(1) or die "report: dccifd failed socket shutdown: $!"; ++ my $errno = 0; close DCC or $errno = $!; ++ proc_status_ok($?,$errno) ++ or info("dcc: [%s] finished: %s", $pid, exit_status_str($?,$errno)); + +- $sock->getline() or die "report: dccifd failed read status"; +- $sock->getline() or die "report: dccifd failed read multistatus"; ++ die "dcc: failed to read learning response\n" if !@resp; + +- my @ignored = $sock->getlines(); ++ ($raw_x_dcc, $new_cksums) = $self->parse_dcc_response(\@resp); + }); + +- $options->{report}->leave_helper_run_mode(); ++ if (defined(fileno(*DCC))) { # still open ++ if ($pid) { ++ if (kill('TERM',$pid)) { ++ dbg("dcc: killed stale dccsight process [$pid]") ++ } else { ++ dbg("dcc: killing stale dccsight process [$pid] failed: $!") } ++ } ++ 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(); + + if ($timer->timed_out()) { +- dbg("reporter: DCC report via dccifd timed out after $timeout secs."); ++ dbg("dcc: dccsight timed out after $timeout seconds"); + return 0; + } + + if ($err) { + chomp $err; +- if ($err eq "__brokenpipe__ignore__") { +- dbg("reporter: DCC report via dccifd failed: broken pipe"); +- } else { +- warn("reporter: DCC report via dccifd failed: $err\n"); +- } ++ info("dcc: dccsight failed: $err\n"); + return 0; + } + ++ if ($raw_x_dcc) { ++ dbg("dcc: learned response: %s", $raw_x_dcc); + return 1; +-} +- +-sub dcc_report { +- my ($self, $options, $tmpf, $client) = @_; +- my $conf = $self->{main}->{conf}; +- my $timeout = $options->{report}->{conf}->{dcc_timeout}; +- +- # note: not really tainted, this came from system configuration file +- my $path = untaint_file_path($options->{report}->{conf}->{dcc_path}); +- my $opts = $conf->{dcc_options}; +- my @opts = !defined $opts ? () : split(' ',$opts); +- untaint_var(\@opts); +- +- # get the metadata from the message so we can pass the external relay info +- +- unshift(@opts, "-a", +- untaint_var($client)) if defined $client && $client ne ''; +- +- my $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout }); +- +- $options->{report}->enter_helper_run_mode(); +- my $err = $timer->run_and_catch(sub { +- +- local $SIG{PIPE} = sub { die "__brokenpipe__ignore__\n" }; +- +- dbg("report: opening pipe: %s", +- join(' ', $path, "-H", "-t", "many", "-x", "0", @opts, "< $tmpf")); +- +- my $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC, +- $tmpf, 1, $path, "-H", "-t", "many", "-x", "0", @opts); +- $pid or die "$!\n"; ++ } + +- my($inbuf,$nread,$nread_all); $nread_all = 0; +- # response is ignored, just check its existence +- while ( $nread=read(DCC,$inbuf,8192) ) { $nread_all += $nread } +- defined $nread or die "error reading from pipe: $!"; ++ return 0; ++} + +- dbg("dcc: empty response") if $nread_all < 1; ++sub plugin_report { ++ my ($self, $options) = @_; + +- my $errno = 0; close DCC or $errno = $!; +- # closing a pipe also waits for the process executing on the pipe to +- # complete, no need to explicitly call waitpid +- # my $child_stat = waitpid($pid,0) > 0 ? $? : undef; +- proc_status_ok($?,$errno) +- or die "dcc: reporter error: ".exit_status_str($?,$errno)."\n"; +- }); +- $options->{report}->leave_helper_run_mode(); ++ return if $options->{report}->{options}->{dont_report_to_dcc}; ++ $self->get_dcc_interface(); ++ return if $self->{dcc_disabled}; + +- if ($timer->timed_out()) { +- dbg("reporter: DCC report via dccproc timed out after $timeout seconds"); +- return 0; +- } ++ # 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); + +- if ($err) { +- chomp $err; +- if ($err eq "__brokenpipe__ignore__") { +- dbg("reporter: DCC report via dccproc failed: broken pipe"); ++ if (defined $raw_x_dcc) { ++ $options->{report}->{report_available} = 1; ++ info("reporter: spam reported to DCC"); ++ $options->{report}->{report_return} = 1; + } else { +- warn("reporter: DCC report via dccproc failed: $err\n"); ++ info("reporter: could not report spam to DCC"); + } +- return 0; +- } +- +- return 1; + } + + 1; +- +-=back +- +-=cut diff --git a/mail/p5-Mail-SpamAssassin/pkg-install b/mail/p5-Mail-SpamAssassin/pkg-install index 568dc5e832e4..f57f9f38ec81 100644 --- a/mail/p5-Mail-SpamAssassin/pkg-install +++ b/mail/p5-Mail-SpamAssassin/pkg-install @@ -1,10 +1,6 @@ #!/bin/sh -PKG_PREFIX=${PKG_PREFIX:-/usr/local} -USER=${USER:-spamd} -GROUP=${GROUP:-spamd} -HOME=/var/spool/${USER} -if [ "$2" = "POST-INSTALL" ];then +if [ "$2" = "POST-INSTALL" ];then ask() { local question default answer @@ -32,35 +28,35 @@ yesno() { } # Create pid directory - install -d -o ${USER} -g ${GROUP} /var/run/spamd - /usr/bin/su root -c "${PKG_PREFIX}/bin/spamassassin -x -L --lint" - if [ ${?} -eq 9 ];then - echo "***********************************************" - echo "*__ ___ ____ _ _ ___ _ _ ____ *" - echo "*\ \ / / \ | _ \| \ | |_ _| \ | |/ ___|*" - echo "* \ \ /\ / / _ \ | |_) | \| || || \| | | _ *" - echo "* \ V V / ___ \| _ <| |\ || || |\ | |_| |*" - echo "* \_/\_/_/ \_\_| \_\_| \_|___|_| \_|\____|*" - echo "* *" - echo "*You must install rules before starting spamd!*" - echo "***********************************************" + ${INSTALL} -d -o ${USER} -g ${GROUP} /var/run/spamd + ${PREFIX}/bin/spamassassin -x -L --lint + if [ ${?} -ne 0 ];then + echo " +******************************************************* +* _ _ _ _______ ______ __ _ _____ __ _ ______ * +* | | | |_____| |_____/ | \ | | | \ | | ____ * +* |__|__| | | | \_ | \_| __|__ | \_| |_____| * +* * +******************************************************* +* You must install rules before starting spamd! * +*******************************************************" if [ -z "${PACKAGE_BUILDING}" -a -z "${BATCH}" ]; then if yesno "Do you wish to run sa-update to fetch new rules" "N";then - ${PKG_PREFIX}/bin/sa-update || true + ${PREFIX}/bin/sa-update || true else echo "" fi - /usr/bin/su root -c "${PKG_PREFIX}/bin/spamassassin -x -L --lint" - if [ ${?} -eq 0 ] && grep '^load.*Rule2XSBody' ${PKG_PREFIX}/etc/mail/spamassassin/v320.pre > /dev/null ;then + ${PREFIX}/bin/spamassassin -x -L --lint + if [ ${?} -eq 0 ] && grep '^load.*Rule2XSBody' ${PREFIX}/etc/mail/spamassassin/v320.pre > /dev/null ;then if yesno "Do you wish to compile rules with re2c (will take a long time)" "N";then - ${PKG_PREFIX}/bin/sa-compile || true + ${PREFIX}/bin/sa-compile || true fi fi fi fi exit 0 -fi # post-install +fi # post-install exit 0 |