use
POSIX
qw(PIPE_BUF WNOHANG)
;
our
@ISA
=
qw(Mail::SpamAssassin::Plugin)
;
sub
new {
my
$class
=
shift
;
my
$mailsaobject
=
shift
;
$class
=
ref
(
$class
) ||
$class
;
my
$self
=
$class
->SUPER::new(
$mailsaobject
);
bless
(
$self
,
$class
);
$self
->{razor2_available} = 0;
if
(
$mailsaobject
->{local_tests_only}) {
dbg(
"razor2: local tests only, skipping Razor"
);
}
else
{
$self
->{razor2_available} = 1;
dbg(
"razor2: razor2 is available, version "
.
$Razor2::Client::Version::VERSION
.
"\n"
);
}
else
{
dbg(
"razor2: razor2 is not available"
);
}
}
$self
->register_eval_rule(
"check_razor2"
,
$Mail::SpamAssassin::Conf::TYPE_FULL_EVALS
);
$self
->register_eval_rule(
"check_razor2_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_razor2'
,
default
=> 1,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
,
});
push
(
@cmds
, {
setting
=>
'razor_fork'
,
is_admin
=> 1,
default
=> am_running_on_windows()?0:1,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC
,
});
push
(
@cmds
, {
setting
=>
'razor_timeout'
,
is_admin
=> 1,
default
=> 5,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
,
});
push
(
@cmds
, {
setting
=>
'razor_config'
,
is_admin
=> 1,
type
=>
$Mail::SpamAssassin::Conf::CONF_TYPE_STRING
,
});
$conf
->{parser}->register_commands(\
@cmds
);
}
sub
razor2_access {
my
(
$self
,
$fulltext
,
$type
,
$deadline
) =
@_
;
my
$timeout
=
$self
->{main}->{conf}->{razor_timeout};
my
$return
= 0;
my
@results
;
my
$debug
=
$type
eq
'check'
?
'razor2'
:
'reporter'
;
if
(would_log(
'dbg'
,
$debug
)) {
open
(OLDOUT,
">&STDOUT"
);
open
(STDOUT,
">&STDERR"
);
}
Mail::SpamAssassin::PerMsgStatus::enter_helper_run_mode(
$self
);
my
$rnd
=
rand
(0x7fffffff);
my
$timer
= Mail::SpamAssassin::Timeout->new(
{
secs
=>
$timeout
,
deadline
=>
$deadline
});
my
$err
=
$timer
->run_and_catch(
sub
{
local
($^W) = 0;
my
$rc
= Razor2::Client::Agent->new(
"razor-$type"
);
if
(
$rc
) {
$rc
->{opt} = {
debug
=> (would_log(
'dbg'
,
$debug
) > 1),
foreground
=> 1,
config
=>
$self
->{main}->{conf}->{razor_config}
};
$rc
->do_conf() or
die
"$debug: "
.
$rc
->errstr;
my
$ident
;
if
(
$type
ne
'check'
) {
$ident
=
$rc
->get_ident
or
die
(
"$type requires authentication"
);
}
my
@msg
= (
$fulltext
);
my
$objects
=
$rc
->prepare_objects(\
@msg
)
or
die
"$debug: error in prepare_objects"
;
unless
(
$rc
->get_server_info()) {
my
$error
=
$rc
->errprefix(
"$debug: spamassassin"
) ||
"$debug: razor2 had unknown error during get_server_info"
;
die
$error
;
}
$timer
->
reset
();
my
$sigs
=
$rc
->compute_sigs(
$objects
)
or
die
"$debug: error in compute_sigs"
;
if
(
$type
ne
'check'
|| !
$rc
->local_check(
$objects
->[0])) {
$rc
->
connect
() or
die
"$debug: could not connect to any servers\n"
;
if
(
$type
eq
'check'
) {
unless
(
$rc
->check(
$objects
)) {
my
$error
=
$rc
->errprefix(
"$debug: spamassassin"
) ||
"$debug: razor2 had unknown error during check"
;
die
$error
;
}
}
else
{
unless
(
$rc
->authenticate(
$ident
)) {
my
$error
=
$rc
->errprefix(
"$debug: spamassassin"
) ||
"$debug: razor2 had unknown error during authenticate"
;
die
$error
;
}
unless
(
$rc
->report(
$objects
)) {
my
$error
=
$rc
->errprefix(
"$debug: spamassassin"
) ||
"$debug: razor2 had unknown error during report"
;
die
$error
;
}
}
unless
(
$rc
->disconnect()) {
my
$error
=
$rc
->errprefix(
"$debug: spamassassin"
) ||
"$debug: razor2 had unknown error during disconnect"
;
die
$error
;
}
}
$return
= 1;
if
(
ref
(
$rc
->{logref}) &&
exists
$rc
->{logref}->{fd}) {
my
$untie
= 1;
foreach
my
$log
(
*STDOUT
{IO},
*STDERR
{IO}) {
if
(
$log
==
$rc
->{logref}->{fd}) {
$untie
= 0;
last
;
}
}
if
(
$untie
) {
close
(
$rc
->{logref}->{fd}) or
die
"error closing log: $!"
;
}
}
if
(
$type
eq
'check'
) {
push
(
@results
, {
result
=>
$objects
->[0]->{spam} });
my
$part
= 0;
my
$arrayref
=
$objects
->[0]->{p} ||
$objects
;
if
(
defined
$arrayref
) {
foreach
my
$cf
(@{
$arrayref
}) {
if
(
exists
$cf
->{resp}) {
for
(
my
$response
=0;
$response
<@{
$cf
->{resp}};
$response
++) {
my
$tmp
=
$cf
->{resp}->[
$response
];
my
$tmpcf
=
$tmp
->{cf};
my
$tmpct
=
$tmp
->{ct};
my
$engine
=
$cf
->{sent}->[
$response
]->{e};
$tmpcf
= 0
unless
defined
$tmpcf
;
$tmpct
= 0
unless
defined
$tmpct
;
$engine
= 0
unless
defined
$engine
;
push
(
@results
,
{
part
=>
$part
,
engine
=>
$engine
,
contested
=>
$tmpct
,
confidence
=>
$tmpcf
});
}
}
else
{
push
(
@results
, {
part
=>
$part
,
noresponse
=> 1 });
}
$part
++;
}
}
else
{
dbg(
"$debug: it looks like the internal Razor object has changed format!"
);
}
}
}
else
{
warn
"$debug: undefined Razor2::Client::Agent\n"
;
}
});
$rnd
^=
int
(
rand
(0xffffffff));
srand
;
$rnd
^=
int
(
rand
(0xffffffff));
srand
(
$rnd
& 0x7fffffff);
Mail::SpamAssassin::PerMsgStatus::leave_helper_run_mode(
$self
);
if
(
$timer
->timed_out()) {
dbg(
"$debug: razor2 $type timed out after $timeout seconds"
);
}
if
(
$err
) {
chomp
$err
;
if
(
$err
=~ /(?:could not
connect
|network is unreachable)/) {
dbg(
"$debug: razor2 $type could not connect to any servers"
);
}
elsif
(
$err
=~ /timeout/i) {
dbg(
"$debug: razor2 $type timed out connecting to servers"
);
}
else
{
warn
(
"$debug: razor2 $type failed: $! $err"
);
}
}
if
(would_log(
'dbg'
,
$debug
)) {
open
(STDOUT,
">&OLDOUT"
);
close
OLDOUT;
}
return
wantarray
? (
$return
,
@results
) :
$return
;
}
sub
plugin_report {
my
(
$self
,
$options
) =
@_
;
return
unless
$self
->{razor2_available};
return
if
$self
->{main}->{local_tests_only};
return
unless
$self
->{main}->{conf}->{use_razor2};
return
if
$options
->{report}->{options}->{dont_report_to_razor};
my
$timer
=
$self
->{main}->time_method(
"razor2_report"
);
if
(
$self
->razor2_access(
$options
->{text},
'report'
,
undef
)) {
$options
->{report}->{report_available} = 1;
info(
'reporter: spam reported to Razor'
);
$options
->{report}->{report_return} = 1;
}
else
{
info(
'reporter: could not report spam to Razor'
);
}
}
sub
plugin_revoke {
my
(
$self
,
$options
) =
@_
;
my
$timer
=
$self
->{main}->time_method(
"razor2_revoke"
);
return
unless
$self
->{razor2_available};
return
if
$self
->{main}->{local_tests_only};
return
unless
$self
->{main}->{conf}->{use_razor2};
return
if
$options
->{revoke}->{options}->{dont_report_to_razor};
if
(
$self
->razor2_access(
$options
->{text},
'revoke'
,
undef
)) {
$options
->{revoke}->{revoke_available} = 1;
info(
'reporter: spam revoked from Razor'
);
$options
->{revoke}->{revoke_return} = 1;
}
else
{
info(
'reporter: could not revoke spam from Razor'
);
}
}
sub
finish_parsing_start {
my
(
$self
,
$opts
) =
@_
;
if
(
$opts
->{conf}->{razor_fork}) {
foreach
(@{
$opts
->{conf}->{eval_to_rule}->{check_razor2}}) {
dbg(
"razor2: adjusting rule $_ priority to -100"
);
$opts
->{conf}->{priority}->{
$_
} = -100;
}
foreach
(@{
$opts
->{conf}->{eval_to_rule}->{check_razor2_range}}) {
dbg(
"razor2: adjusting rule $_ priority to -100"
);
$opts
->{conf}->{priority}->{
$_
} = -100;
}
}
}
sub
check_razor2 {
my
(
$self
,
$pms
,
$full
) =
@_
;
return
0
unless
$self
->{razor2_available};
return
0
unless
$self
->{main}->{conf}->{use_razor2};
return
$pms
->{razor2_result}
if
(
defined
$pms
->{razor2_result});
return
0
if
$pms
->{razor2_running};
$pms
->{razor2_running} = 1;
my
$timer
=
$self
->{main}->time_method(
"check_razor2"
);
if
(!
$self
->{main}->{conf}->{razor_fork}) {
(
undef
,
my
@results
) =
$self
->razor2_access(
$full
,
'check'
,
$pms
->{master_deadline});
return
$self
->_check_result(
$pms
, \
@results
);
}
$pms
->{razor2_rulename} =
$pms
->get_current_eval_rule_name();
$pms
->{razor2_backchannel} = Mail::SpamAssassin::SubProcBackChannel->new();
my
$back_selector
=
''
;
$pms
->{razor2_backchannel}->set_selector(\
$back_selector
);
eval
{
$pms
->{razor2_backchannel}->setup_backchannel_parent_pre_fork();
} or
do
{
dbg(
"razor2: backchannel pre-setup failed: $@"
);
delete
$pms
->{razor2_backchannel};
return
0;
};
my
$pid
=
fork
();
if
(!
defined
$pid
) {
info(
"razor2: child fork failed: $!"
);
delete
$pms
->{razor2_backchannel};
return
0;
}
if
(!
$pid
) {
$0 =
"$0 (razor2)"
;
$SIG
{CHLD} =
'DEFAULT'
;
$SIG
{PIPE} =
'IGNORE'
;
$SIG
{
$_
} =
sub
{
eval
{ dbg(
"razor2: child process $$ caught signal $_[0]"
); };
force_die(6);
}
foreach
am_running_on_windows()?
qw(INT HUP TERM QUIT)
:
qw(INT HUP TERM TSTP QUIT USR1 USR2)
;
dbg(
"razor2: child process $$ forked"
);
$pms
->{razor2_backchannel}->setup_backchannel_child_post_fork();
(
undef
,
my
@results
) =
$self
->razor2_access(
$full
,
'check'
,
$pms
->{master_deadline});
my
$backmsg
;
eval
{
$backmsg
= Storable::freeze(\
@results
);
};
if
($@) {
dbg(
"razor2: child return value freeze failed: $@"
);
force_die(0);
}
if
(!
syswrite
(
$pms
->{razor2_backchannel}->{parent},
$backmsg
)) {
dbg(
"razor2: child backchannel write failed: $!"
);
}
force_die(0);
}
$pms
->{razor2_pid} =
$pid
;
eval
{
$pms
->{razor2_backchannel}->setup_backchannel_parent_post_fork(
$pid
);
} or
do
{
dbg(
"razor2: backchannel post-setup failed: $@"
);
delete
$pms
->{razor2_backchannel};
return
0;
};
return
;
}
sub
check_tick {
my
(
$self
,
$opts
) =
@_
;
$self
->_check_forked_result(
$opts
->{permsgstatus}, 0);
}
sub
check_cleanup {
my
(
$self
,
$opts
) =
@_
;
$self
->_check_forked_result(
$opts
->{permsgstatus}, 1);
}
sub
_check_forked_result {
my
(
$self
,
$pms
,
$finish
) =
@_
;
return
0
if
!
$pms
->{razor2_backchannel};
return
0
if
!
$pms
->{razor2_pid};
my
$timer
=
$self
->{main}->time_method(
"check_razor2"
);
$pms
->{razor2_abort} =
$pms
->{deadline_exceeded} ||
$pms
->{shortcircuited};
my
$kid_pid
=
$pms
->{razor2_pid};
my
$pid
=
waitpid
(
$kid_pid
,
$finish
&& !
$pms
->{razor2_abort} ? 0 : WNOHANG);
if
(
$pid
== 0) {
if
(
$pms
->{razor2_abort}) {
dbg(
"razor2: bailing out due to deadline/shortcircuit"
);
kill
(
'TERM'
,
$kid_pid
);
if
(
waitpid
(
$kid_pid
, WNOHANG) == 0) {
sleep
(1);
if
(
waitpid
(
$kid_pid
, WNOHANG) == 0) {
dbg(
"razor2: child process $kid_pid still alive, KILL"
);
kill
(
'KILL'
,
$kid_pid
);
waitpid
(
$kid_pid
, 0);
}
}
delete
$pms
->{razor2_pid};
delete
$pms
->{razor2_backchannel};
}
return
0;
}
elsif
(
$pid
== -1) {
dbg(
"razor2: child process $kid_pid already handled?"
);
delete
$pms
->{razor2_backchannel};
return
0;
}
$pms
->rule_ready(
$pms
->{razor2_rulename});
dbg(
"razor2: child process $kid_pid finished, reading results"
);
my
$backmsg
;
my
$ret
=
sysread
(
$pms
->{razor2_backchannel}->{latest_kid_fh},
$backmsg
, am_running_on_windows()?512:PIPE_BUF);
if
(!
defined
$ret
||
$ret
== 0) {
dbg(
"razor2: could not read result from child: "
.(
$ret
== 0 ? 0 : $!));
delete
$pms
->{razor2_backchannel};
return
0;
}
delete
$pms
->{razor2_backchannel};
my
$results
;
eval
{
$results
= Storable::thaw(
$backmsg
);
};
if
($@) {
dbg(
"razor2: child return value thaw failed: $@"
);
return
;
}
$self
->_check_result(
$pms
,
$results
);
}
sub
_check_result {
my
(
$self
,
$pms
,
$results
) =
@_
;
$self
->{main}->call_plugins (
'process_razor_result'
,
{
results
=>
$results
,
permsgstatus
=>
$pms
}
);
foreach
my
$result
(
@$results
) {
if
(
exists
$result
->{result}) {
$pms
->{razor2_result} =
$result
->{result}
if
$result
->{result};
}
elsif
(
$result
->{noresponse}) {
dbg(
'razor2: part='
.
$result
->{part} .
' noresponse'
);
}
else
{
dbg(
'razor2: part='
.
$result
->{part} .
' engine='
.
$result
->{engine} .
' contested='
.
$result
->{contested} .
' confidence='
.
$result
->{confidence});
next
if
$result
->{contested};
my
$cf
=
$pms
->{razor2_cf_score}->{
$result
->{engine}} || 0;
if
(
$result
->{confidence} >
$cf
) {
$pms
->{razor2_cf_score}->{
$result
->{engine}} =
$result
->{confidence};
}
}
}
$pms
->{razor2_result} ||= 0;
$pms
->{razor2_cf_score} ||= {};
dbg(
"razor2: results: spam? "
.
$pms
->{razor2_result});
while
(
my
(
$engine
,
$cf
) =
each
%{
$pms
->{razor2_cf_score}}) {
dbg(
"razor2: results: engine $engine, highest cf score: $cf"
);
}
if
(
$self
->{main}->{conf}->{razor_fork}) {
if
(
$pms
->{razor2_rulename} &&
$pms
->{razor2_result}) {
$pms
->got_hit(
$pms
->{razor2_rulename},
""
,
ruletype
=>
'eval'
);
}
if
(
$pms
->{razor2_range_callbacks}) {
foreach
(@{
$pms
->{razor2_range_callbacks}}) {
$self
->check_razor2_range(
$pms
,
''
,
@$_
);
}
}
}
return
$pms
->{razor2_result};
}
sub
check_razor2_range {
my
(
$self
,
$pms
,
$body
,
$engine
,
$min
,
$max
,
$rulename
) =
@_
;
return
0
unless
$self
->{razor2_available};
return
0
unless
$self
->{main}->{conf}->{use_razor2};
if
(!
defined
$rulename
) {
$rulename
=
$pms
->get_current_eval_rule_name();
}
if
(
$pms
->{razor2_abort}) {
$pms
->rule_ready(
$rulename
);
return
;
}
if
(
$self
->{main}->{conf}->{razor_fork}) {
if
(!
defined
$pms
->{razor2_result}) {
dbg(
"razor2: delaying check_razor2_range call for $rulename"
);
push
@{
$pms
->{razor2_range_callbacks}},
[
$engine
,
$min
,
$max
,
$rulename
];
return
;
}
}
else
{
if
(!
$pms
->{razor2_running}) {
$self
->check_razor2(
$pms
,
$body
);
}
}
$pms
->rule_ready(
$rulename
);
my
$cf
= 0;
if
(
$engine
) {
$cf
=
$pms
->{razor2_cf_score}->{
$engine
};
return
0
unless
defined
$cf
;
}
else
{
while
(
my
(
$engine
,
$ecf
) =
each
%{
$pms
->{razor2_cf_score}}) {
if
(
$ecf
>
$cf
) {
$cf
=
$ecf
;
}
}
}
if
(
$cf
>=
$min
&&
$cf
<=
$max
) {
my
$cf_str
=
sprintf
(
"cf: %3d"
,
$cf
);
$pms
->test_log(
$cf_str
,
$rulename
);
if
(
$self
->{main}->{conf}->{razor_fork}) {
$pms
->got_hit(
$rulename
,
""
,
ruletype
=>
'eval'
);
}
return
1;
}
return
0;
}
sub
has_fork { 1 }
1;