=head1 NAME
Mail::SpamAssassin::Plugin::Razor2 - perform Razor check of messages
=head1 SYNOPSIS
loadplugin Mail::SpamAssassin::Plugin::Razor2
=head1 DESCRIPTION
Vipul's Razor is a distributed, collaborative, spam detection and
filtering network based on user submissions of spam. Detection is done
with signatures that efficiently spot mutating spam content and user
input is validated through reputation assignments.
Note that Razor2 is disabled by default in C<init.pre> because it is not
available for unlimited free use. It is currently free for personal
use, subject to capacity constraints. See the Cloudmark SpamNet Service
Policy for more details.
See http://razor.sourceforge.net/ for more information about Razor.
=head1 USER SETTINGS
=over 4
=cut
package Mail::SpamAssassin::Plugin::Razor2;
use Mail::SpamAssassin::Plugin;
use Mail::SpamAssassin::Logger;
use Mail::SpamAssassin::Timeout;
use strict;
use warnings;
use bytes;
use vars qw(@ISA);
@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 {
if (eval { require Razor2::Client::Agent; }) {
$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");
$self->register_eval_rule("check_razor2_range");
$self->set_config($mailsaobject->{conf});
return $self;
}
sub set_config {
my ($self, $conf) = @_;
my @cmds = ();
=item use_razor2 (0|1) (default: 1)
Whether to use Razor2, if it is available.
=cut
push(@cmds, {
setting => 'use_razor2',
default => 1,
type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
});
=back
=head1 ADMINISTRATOR SETTINGS
=over 4
=item razor_timeout n (default: 5)
How many seconds you wait for Razor to complete before you go on without
the results
=cut
push(@cmds, {
setting => 'razor_timeout',
is_admin => 1,
default => 5,
type => $Mail::SpamAssassin::Conf::CONF_TYPE_NUMERIC,
});
=item razor_config filename
Define the filename used to store Razor's configuration settings.
Currently this is left to Razor to decide.
=cut
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) = @_;
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 $timer = Mail::SpamAssassin::Timeout->new({ secs => $timeout });
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;
}
}
close $rc->{logref}->{fd} if ($untie);
}
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";
}
});
srand;
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};
if ($self->razor2_access($options->{text}, 'report')) {
$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) = @_;
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')) {
$options->{revoke}->{revoke_available} = 1;
dbg('reporter: spam revoked from Razor');
$options->{revoke}->{revoke_return} = 1;
}
else {
dbg('reporter: could not revoke spam from Razor');
}
}
sub check_razor2 {
my ($self, $permsgstatus, $full) = @_;
return $permsgstatus->{razor2_result} if (defined $permsgstatus->{razor2_result});
$permsgstatus->{razor2_result} = 0;
$permsgstatus->{razor2_cf_score} = { '4' => 0, '8' => 0 };
return unless $self->{razor2_available};
return unless $self->{main}->{conf}->{use_razor2};
my $return;
my @results = ();
($return, @results) = $self->razor2_access($full, 'check');
$self->{main}->call_plugins ('process_razor_result',
{ results => \@results, permsgstatus => $permsgstatus }
);
foreach my $result (@results) {
if (exists $result->{result}) {
$permsgstatus->{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 = $permsgstatus->{razor2_cf_score}->{$result->{engine}} || 0;
if ($result->{confidence} > $cf) {
$permsgstatus->{razor2_cf_score}->{$result->{engine}} = $result->{confidence};
}
}
}
dbg("razor2: results: spam? " . $permsgstatus->{razor2_result});
while(my ($engine, $cf) = each %{$permsgstatus->{razor2_cf_score}}) {
dbg("razor2: results: engine $engine, highest cf score: $cf");
}
return $permsgstatus->{razor2_result};
}
sub check_razor2_range {
my ($self, $permsgstatus, $body, $engine, $min, $max) = @_;
return unless $self->{razor2_available};
return unless $self->{main}->{conf}->{use_razor2};
return unless $self->{main}->{conf}->{scores}->{'RAZOR2_CHECK'};
unless (defined $permsgstatus->{razor2_result}) {
$self->check_razor2($permsgstatus, $body);
}
my $cf = 0;
if ($engine) {
$cf = $permsgstatus->{razor2_cf_score}->{$engine};
return unless defined $cf;
}
else {
while(my ($engine, $ecf) = each %{$permsgstatus->{razor2_cf_score}}) {
if ($ecf > $cf) {
$cf = $ecf;
}
}
}
if ($cf >= $min && $cf <= $max) {
$permsgstatus->test_log(sprintf("cf: %3d", $cf));
return 1;
}
return;
}
1;
=back
=cut