proc_status_ok exit_status_str)
;
use
Errno
qw(ENOENT EACCES)
;
our
@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
our
$io_socket_module_name
;
BEGIN {
$io_socket_module_name
=
'IO::Socket::IP'
;
$io_socket_module_name
=
'IO::Socket::INET6'
;
$io_socket_module_name
=
'IO::Socket::INET'
;
}
}
sub
new {
my
$class
=
shift
;
my
$mailsaobject
=
shift
;
$class
=
ref
(
$class
) ||
$class
;
my
$self
=
$class
->SUPER::new(
$mailsaobject
);
bless
(
$self
,
$class
);
if
(
$mailsaobject
->{local_tests_only}) {
$self
->{dcc_disabled} = 1;
dbg(
"dcc: local tests only, disabling DCC"
);
}
$self
->register_eval_rule(
"check_dcc"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_dcc_reputation_range"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->set_config(
$mailsaobject
->{conf});
return
$self
;
}
sub
set_config {
my
(
$self
,
$conf
) =
@_
;
my
@cmds
;
push
(
@cmds
, {
setting
=>
'use_dcc'
,
default
=> 1,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
,
});
push
(
@cmds
, {
setting
=>
'use_dcc_rep'
,
default
=> 1,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_BOOL
,
});
push
(
@cmds
, {
setting
=>
'dcc_body_max'
,
default
=> 999999,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
},
{
setting
=>
'dcc_fuz1_max'
,
default
=> 999999,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
},
{
setting
=>
'dcc_fuz2_max'
,
default
=> 999999,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
},
{
setting
=>
'dcc_rep_percent'
,
default
=> 90,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
});
push
(
@cmds
, {
setting
=>
'dcc_timeout'
,
is_admin
=> 1,
default
=> 5,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
,
});
push
(
@cmds
, {
setting
=>
'dcc_home'
,
is_admin
=> 1,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_STRING
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
if
(!
defined
$value
||
$value
eq
''
) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
$value
= untaint_file_path(
$value
);
my
$stat_errn
=
stat
(
$value
) ? 0 : 0+$!;
if
(
$stat_errn
!= 0 || !-d _) {
my
$msg
=
$stat_errn
== ENOENT ?
"does not exist"
: !-d _ ?
"is not a directory"
:
"not accessible: $!"
;
info(
"config: dcc_home \"$value\" $msg"
);
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
$self
->{dcc_home} =
$value
;
}
});
push
(
@cmds
, {
setting
=>
'dcc_dccifd_path'
,
is_admin
=> 1,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_STRING
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
if
(!
defined
$value
||
$value
eq
''
) {
return
$Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE
;
}
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
;
dbg(
"config: dcc_dccifd_path set to [%s]:%s"
,
$host
,
$port
);
}
else
{
if
(
$value
!~ m{^/}) {
info(
"config: dcc_dccifd_path '$value' is not an absolute path"
);
}
$value
= untaint_file_path(
$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
;
}
});
push
(
@cmds
, {
setting
=>
'dcc_path'
,
is_admin
=> 1,
default
=>
undef
,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_STRING
,
code
=>
sub
{
my
(
$self
,
$key
,
$value
,
$line
) =
@_
;
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' is not executable"
);
return
$Mail::SpamAssassin::Conf::INVALID_VALUE
;
}
$self
->{dcc_path} =
$value
;
}
});
push
(
@cmds
, {
setting
=>
'dcc_options'
,
is_admin
=> 1,
default
=>
undef
,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_STRING
,
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;
}
});
push
(
@cmds
, {
setting
=>
'dccifd_options'
,
is_admin
=> 1,
default
=>
undef
,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_STRING
,
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;
}
});
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
) =
@_
;
return
if
defined
$self
->{dcc_version};
$self
->{dcc_version} =
'?'
;
my
$conf
=
$self
->{main}->{conf};
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;
$cdcc_output
=~ s/\s+/ /gs;
$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 :
''
;
if
(
$cdcc_output
=~ /\s+libexecdir=(\S+)/) {
$self
->ck_dir($1,
'dcc_libexec'
,
'cdcc'
);
}
}
if
(!
$conf
->{dcc_home} &&
$cdcc_home
) {
$self
->ck_dir(
$cdcc_home
,
'dcc_home'
,
'cdcc'
);
}
if
(!
$conf
->{dcc_home}) {
$self
->ck_dir(
$conf
->{dcc_home} =
'/var/dcc'
,
'dcc_home'
,
'default'
)
}
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'
);
}
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. "
.
}
$self
->{dccifd_lookup_options} =
"header "
.
$opts
;
$self
->{dccifd_report_options} =
"header spam "
.
$opts
;
}
else
{
$self
->{dccifd_lookup_options} =
"cksums grey-off "
.
$opts
;
$self
->{dccifd_report_options} =
"header spam grey-off "
.
$opts
;
}
}
sub
dcc_pgm_path {
my
(
$self
,
$pgm
) =
@_
;
my
$pgmpath
;
my
$conf
=
$self
->{main}->{conf};
$pgmpath
=
$conf
->{dcc_path};
if
(
defined
$pgmpath
&&
$pgmpath
ne
''
) {
return
$pgmpath
if
$pgm
eq
'dccproc'
;
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
;
}
}
}
$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
;
}
foreach
my
$dir
(!
defined
$conf
->{dcc_home} ? () :
$conf
->{dcc_home}.
'/bin'
,
$conf
->{dcc_libexec},
'/usr/local/bin'
,
'/usr/local/dcc'
,
'/var/dcc'
) {
next
unless
defined
$dir
;
$pgmpath
=
$dir
.
'/'
.
$pgm
;
if
(-x
$pgmpath
) {
dbg(
"dcc: dcc_pgm_path, found %s in %s: %s"
,
$pgm
,
$dir
,
$pgmpath
);
return
$pgmpath
;
}
}
return
;
}
sub
is_dccifd_available {
my
(
$self
) =
@_
;
return
$self
->{dccifd_available}
if
$self
->{dccifd_available};
$self
->find_dcc_home();
my
$conf
=
$self
->{main}->{conf};
if
(
defined
$conf
->{dcc_dccifd_host}) {
dbg(
"dcc: dccifd is available via socket [%s]:%s"
,
$conf
->{dcc_dccifd_host},
$conf
->{dcc_dccifd_port});
return
(
$self
->{dccifd_available} = 1);
}
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"
;
}
$conf
->{dcc_dccifd_socket} =
$sockpath
;
}
return
(
$self
->{dccifd_available} = 1)
if
(-S
$sockpath
&& -w _ && -r _);
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};
return
$self
->{dccproc_available}
if
defined
$self
->{dccproc_available};
$self
->find_dcc_home();
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: %s is available"
,
$conf
->{dcc_path});
return
(
$self
->{dccproc_available} = 1);
}
sub
dccifd_connect {
my
(
$self
,
$tag
) =
@_
;
my
$conf
=
$self
->{main}->{conf};
my
$sockpath
=
$conf
->{dcc_dccifd_socket};
my
$sock
;
if
(
defined
$sockpath
) {
dbg(
"$tag connecting to local socket $sockpath"
);
$sock
= IO::Socket::UNIX->new(
Type
=> SOCK_STREAM,
Peer
=>
$sockpath
);
info(
"$tag failed to connect to local socket $sockpath"
)
if
!
$sock
;
}
else
{
my
$host
=
$conf
->{dcc_dccifd_host};
my
$port
=
$conf
->{dcc_dccifd_port};
dbg(
"$tag connecting to [%s]:%s using %s"
,
$host
,
$port
,
$io_socket_module_name
);
$sock
=
$io_socket_module_name
->new(
Proto
=>
'tcp'
,
PeerAddr
=>
$host
,
PeerPort
=>
$port
);
info(
"$tag failed to connect to [%s]:%s using %s: %s"
,
$host
,
$port
,
$io_socket_module_name
, $!)
if
!
$sock
;
}
$self
->{dccifd_available} = 0
if
!
$sock
;
return
$sock
;
}
sub
get_dcc_interface {
my
(
$self
) =
@_
;
if
(!
$self
->is_dccifd_available() && !
$self
->is_dccproc_available()) {
dbg(
"dcc: dccifd or dccproc is not available"
);
return
0;
}
return
1;
}
sub
check_tick {
my
(
$self
,
$opts
) =
@_
;
$self
->_check_async(
$opts
, 0);
my
$pms
=
$opts
->{permsgstatus};
if
(
$pms
->{dcc_range_callbacks}) {
while
(@{
$pms
->{dcc_range_callbacks}}) {
my
$cb_args
=
shift
@{
$pms
->{dcc_range_callbacks}};
$self
->check_dcc_reputation_range(
$pms
,
@$cb_args
);
}
}
}
sub
check_cleanup {
my
(
$self
,
$opts
) =
@_
;
$self
->_check_async(
$opts
, 1);
my
$pms
=
$opts
->{permsgstatus};
if
(
$pms
->{dcc_range_callbacks}) {
while
(@{
$pms
->{dcc_range_callbacks}}) {
my
$cb_args
=
shift
@{
$pms
->{dcc_range_callbacks}};
$self
->check_dcc_reputation_range(
$pms
,
@$cb_args
);
}
}
}
sub
_check_async {
my
(
$self
,
$opts
,
$timeout
) =
@_
;
my
$pms
=
$opts
->{permsgstatus};
return
if
!
$pms
->{dcc_sock};
my
$timer
=
$self
->{main}->time_method(
"check_dcc"
);
$pms
->{dcc_abort} =
$pms
->{dcc_abort} ||
$pms
->{deadline_exceeded} ||
$pms
->{shortcircuited};
if
(
$pms
->{dcc_abort}) {
$timeout
= 0;
}
elsif
(
$timeout
) {
$timeout
=
$self
->{main}->{conf}->{dcc_timeout} -
(
time
-
$pms
->{dcc_async_start});
$timeout
= 1
if
$timeout
< 1;
$timeout
= 20
if
$timeout
> 20;
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
;
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};
if
(
$timer
->timed_out()) {
info(
"dcc: dccifd read failed"
);
}
elsif
(
$err
) {
chomp
$err
;
info(
"dcc: dccifd read failed: $err"
);
}
else
{
shift
@resp
;
shift
@resp
;
if
(
@resp
) {
dbg(
"dcc: dccifd raw response: "
.
join
(
""
,
@resp
));
(
$pms
->{dcc_x_result},
$pms
->{dcc_cksums}) =
$self
->parse_dcc_response(\
@resp
,
'dccifd'
);
if
(
$pms
->{dcc_x_result}) {
dbg(
"dcc: dccifd parsed response: $pms->{dcc_x_result}"
);
(
$pms
->{dcc_result},
$pms
->{dcc_rep}) =
$self
->check_dcc_result(
$pms
,
$pms
->{dcc_x_result});
if
(
$pms
->{dcc_result}) {
foreach
(@{
$pms
->{conf}->{eval_to_rule}->{check_dcc}}) {
$pms
->got_hit(
$_
,
""
,
ruletype
=>
'eval'
);
}
}
else
{
foreach
(@{
$pms
->{conf}->{eval_to_rule}->{check_dcc}}) {
$pms
->rule_ready(
$_
);
}
}
}
}
else
{
info(
"dcc: empty response from dccifd?"
);
}
}
}
elsif
(
$pms
->{dcc_abort}) {
dbg(
"dcc: bailing out due to deadline/shortcircuit"
);
delete
$pms
->{dcc_sock};
delete
$pms
->{dcc_range_callbacks};
}
elsif
(
$timeout
) {
dbg(
"dcc: no response from dccifd, timed out"
);
delete
$pms
->{dcc_sock};
delete
$pms
->{dcc_range_callbacks};
}
else
{
dbg(
"dcc: still waiting for dccifd response"
);
}
}
sub
check_dnsbl {
my
(
$self
,
$opts
) =
@_
;
return
0
if
$self
->{dcc_disabled};
return
0
if
!
$self
->{main}->{conf}->{use_dcc};
my
$pms
=
$opts
->{permsgstatus};
return
0
if
!
grep
{
$pms
->{conf}->{scores}->{
$_
}}
( @{
$pms
->{conf}->{eval_to_rule}->{check_dcc}},
@{
$pms
->{conf}->{eval_to_rule}->{check_dcc_reputation_range}} );
if
(
$self
->is_dccifd_available()) {
$self
->_launch_dcc(
$pms
);
}
}
sub
_launch_dcc {
my
(
$self
,
$pms
) =
@_
;
return
if
$pms
->{dcc_running};
$pms
->{dcc_running} = 1;
my
$timer
=
$self
->{main}->time_method(
"check_dcc"
);
$pms
->{tag_data}->{DCCB} =
''
;
$pms
->{tag_data}->{DCCR} =
''
;
$pms
->{tag_data}->{DCCREP} =
''
;
my
$fulltext
=
$pms
->{msg}->get_pristine();
if
(
$fulltext
eq
''
) {
dbg(
"dcc: empty message; skipping dcc check"
);
$pms
->{dcc_result} = 0;
$pms
->{dcc_abort} = 1;
return
;
}
if
(!
$self
->get_dcc_interface()) {
$pms
->{dcc_result} = 0;
$pms
->{dcc_abort} = 1;
return
;
}
my
$envelope
=
$pms
->{relays_external}->[0];
(
$pms
->{dcc_x_result},
$pms
->{dcc_cksums}) =
$self
->ask_dcc(
'dcc:'
,
$pms
, \
$fulltext
,
$envelope
);
return
;
}
sub
check_dcc {
my
(
$self
,
$pms
) =
@_
;
return
0
if
$self
->{dcc_disabled};
return
0
if
!
$pms
->{conf}->{use_dcc};
return
0
if
$pms
->{dcc_abort};
if
(
$pms
->{dcc_async_start}) {
return
;
}
return
$pms
->{dcc_result}
if
defined
$pms
->{dcc_result};
$self
->_launch_dcc(
$pms
);
return
if
$pms
->{dcc_async_start};
if
(!
defined
$pms
->{dcc_x_result}) {
$pms
->{dcc_abort} = 1;
return
0;
}
(
$pms
->{dcc_result},
$pms
->{dcc_rep}) =
$self
->check_dcc_result(
$pms
,
$pms
->{dcc_x_result});
return
$pms
->{dcc_result};
}
sub
check_dcc_reputation_range {
my
(
$self
,
$pms
,
undef
,
$min
,
$max
,
$cb_rulename
) =
@_
;
return
0
if
$self
->{dcc_disabled};
return
0
if
!
$pms
->{conf}->{use_dcc};
return
0
if
!
$pms
->{conf}->{use_dcc_rep};
return
0
if
$pms
->{dcc_abort};
my
$timer
=
$self
->{main}->time_method(
"check_dcc"
);
if
(
exists
$pms
->{dcc_rep}) {
my
$result
;
if
(
$pms
->{dcc_rep} < 0) {
$result
= 0;
}
else
{
$min
= 0
if
!
defined
$min
;
$max
= 100
if
!
defined
$max
;
$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
(
defined
$cb_rulename
) {
if
(
$result
) {
$pms
->got_hit(
$cb_rulename
,
""
,
ruletype
=>
'eval'
);
}
else
{
$pms
->rule_ready(
$cb_rulename
);
}
return
0;
}
else
{
return
$result
;
}
}
else
{
if
(!
defined
$cb_rulename
) {
my
$rulename
=
$pms
->get_current_eval_rule_name();
push
@{
$pms
->{dcc_range_callbacks}}, [
undef
,
$min
,
$max
,
$rulename
];
return
;
}
}
return
0;
}
sub
check_dcc_result {
my
(
$self
,
$pms
,
$x_dcc
) =
@_
;
my
$dcc_result
= 0;
my
$dcc_rep
= -1;
if
(!
defined
$x_dcc
||
$x_dcc
eq
''
) {
return
(
$dcc_result
,
$dcc_rep
);
}
my
$conf
=
$pms
->{conf};
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;
my
%count
= (
body
=> 0,
fuz1
=> 0,
fuz2
=> 0,
rep
=> 0);
if
(
$x_dcc
=~ /\bBody=(\d+)/) {
$count
{body} = $1+0;
}
if
(
$x_dcc
=~ /\bFuz1=(\d+)/) {
$count
{fuz1} = $1+0;
}
if
(
$x_dcc
=~ /\bFuz2=(\d+)/) {
$count
{fuz2} = $1+0;
}
if
(
$pms
->{conf}->{use_dcc_rep} &&
$x_dcc
=~ /\brep=(\d+)/) {
$count
{rep} = $1+0;
$dcc_rep
=
$count
{rep};
$pms
->set_tag(
'DCCREP'
,
$dcc_rep
);
}
if
(
$count
{body} >=
$conf
->{dcc_body_max} ||
$count
{fuz1} >=
$conf
->{dcc_fuz1_max} ||
$count
{fuz2} >=
$conf
->{dcc_fuz2_max} ||
$count
{rep} >=
$conf
->{dcc_rep_percent})
{
dbg(
sprintf
(
"dcc: listed: BODY=%s/%s FUZ1=%s/%s FUZ2=%s/%s REP=%s/%s"
,
map
{
defined
$_
?
$_
:
'undef'
} (
$count
{body},
$conf
->{dcc_body_max},
$count
{fuz1},
$conf
->{dcc_fuz1_max},
$count
{fuz2},
$conf
->{dcc_fuz2_max},
$count
{rep},
$conf
->{dcc_rep_percent})
));
$dcc_result
= 1;
}
return
(
$dcc_result
,
$dcc_rep
);
}
sub
parse_dcc_response {
my
(
$self
,
$resp
,
$pgm
) =
@_
;
my
(
$raw_x_dcc
,
$cksums
);
chomp
(
$raw_x_dcc
=
shift
@$resp
);
my
$v
;
while
((
$v
=
shift
@$resp
) &&
$v
=~ s/^\t(.+)\s*\n/ $1/) {
$raw_x_dcc
.=
$v
;
}
$cksums
=
''
;
while
((
$v
=
shift
@$resp
) &&
$v
=~ s/^([^:]*):/$1/) {
$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
,
$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;
dbg(
"$tag dccifd failed: trying dccproc as fallback"
);
return
;
}
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}) {
$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: $!"
;
if
(
$tag
ne
'dcc:'
) {
@resp
=
$pms
->{dcc_sock}->getlines();
delete
$pms
->{dcc_sock};
shift
@resp
;
shift
@resp
;
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
);
}
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
);
}
}
return
(
'async'
,
undef
)
if
$pms
->{dcc_async_start};
}
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"
};
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
{
unshift
(
@opts
,
'-R'
);
}
if
(
$tag
eq
'dcc:'
) {
unshift
(
@opts
,
'-Q'
)
if
defined
$pms
->{dcc_x_result};
}
else
{
unshift
(
@opts
,
'-t'
,
'many'
);
}
if
(
$conf
->{dcc_home}) {
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"
;
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
))) {
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
);
}
sub
check_post_learn {
my
(
$self
,
$opts
) =
@_
;
return
if
$self
->{dcc_disabled};
return
if
!
$self
->{main}->{conf}->{use_dcc};
my
$pms
=
$opts
->{permsgstatus};
return
if
$pms
->{dcc_abort};
my
$conf
=
$self
->{main}->{conf};
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
;
}
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
);
return
;
}
}
return
if
(!
defined
$pms
->{dcc_x_result});
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"
);
my
$old_cksums
=
$pms
->{dcc_cksums};
return
if
(
$old_cksums
&&
$self
->dccsight_learn(
$pms
,
$old_cksums
));
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
,
$pms
,
$old_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;
}
$pms
->enter_helper_run_mode();
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
=>
$pms
->{master_deadline} });
my
$err
=
$timer
->run_and_catch(
sub
{
local
$SIG
{PIPE} =
sub
{
die
"__brokenpipe__ignore__\n"
};
dbg(
"dcc: opening pipe to %s"
,
join
(
' '
,
$dccsight
,
"-t"
,
"many"
,
"<$tmpf"
));
$pid
= Mail::SpamAssassin::Util::helper_app_pipe_open(
*DCC
,
$tmpf
, 1,
$dccsight
,
"-t"
,
"many"
);
$pid
or
die
"$!\n"
;
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);
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
,
'dccsight'
);
});
if
(
defined
(
fileno
(
*DCC
))) {
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
));
}
$pms
->delete_fulltext_tmpfile(
$tmpf
);
$pms
->leave_helper_run_mode();
if
(
$timer
->timed_out()) {
dbg(
"dcc: dccsight timed out after $timeout seconds"
);
return
0;
}
elsif
(
$err
) {
chomp
$err
;
info(
"dcc: dccsight failed: $err\n"
);
return
0;
}
if
(
$raw_x_dcc
ne
''
) {
dbg(
"dcc: learned response: $raw_x_dcc"
);
return
1;
}
return
0;
}
sub
plugin_report {
my
(
$self
,
$opts
) =
@_
;
return
if
$self
->{dcc_disabled};
return
if
!
$self
->{main}->{conf}->{use_dcc};
return
if
$opts
->{report}->{options}->{dont_report_to_dcc};
return
if
!
$self
->get_dcc_interface();
my
$report
=
$opts
->{report};
my
$timer
=
$self
->{main}->time_method(
"dcc_report"
);
$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
) {
$report
->{report_available} =
$report
->{report_return} = 1;
info(
"reporter: spam reported to DCC"
);
}
else
{
info(
"reporter: could not report spam to DCC"
);
}
}
1;