package Mail::SpamAssassin::Reporter;
use strict;
use bytes;
use Carp;
use POSIX ":sys_wait_h";
use constant HAS_NET_DNS => eval { require Net::DNS; };
use constant HAS_NET_SMTP => eval { require Net::SMTP; };
use vars qw{
@ISA $VERSION
};
@ISA = qw();
$VERSION = 'bogus';
sub new {
my $class = shift;
$class = ref($class) || $class;
my ($main, $msg, $options) = @_;
my $self = {
'main' => $main,
'msg' => $msg,
'options' => $options,
};
$self->{conf} = $self->{main}->{conf};
bless ($self, $class);
$self;
}
sub report {
my ($self) = @_;
my $return = 1;
my $available = 0;
my $text = $self->{main}->remove_spamassassin_markup ($self->{msg});
if (!$self->{options}->{dont_report_to_dcc} && $self->is_dcc_available()) {
if ($self->dcc_report($text)) {
$available = 1;
dbg ("SpamAssassin: spam reported to DCC.");
$return = 0;
}
else {
dbg ("SpamAssassin: could not report spam to DCC.");
}
}
if (!$self->{options}->{dont_report_to_pyzor} && $self->is_pyzor_available()) {
if ($self->pyzor_report($text)) {
$available = 1;
dbg ("SpamAssassin: spam reported to Pyzor.");
$return = 0;
}
else {
dbg ("SpamAssassin: could not report spam to Pyzor.");
}
}
if (!$self->{options}->{dont_report_to_razor} && $self->is_razor_available()) {
if ($self->razor_report($text)) {
$available = 1;
dbg ("SpamAssassin: spam reported to Razor.");
$return = 0;
}
else {
dbg ("SpamAssassin: could not report spam to Razor.");
}
}
if (!$self->{options}->{dont_report_to_spamcop} && $self->is_spamcop_available()) {
if ($self->spamcop_report($text)) {
$available = 1;
dbg ("SpamAssassin: spam reported to SpamCop.");
$return = 0;
}
else {
dbg ("SpamAssassin: could not report spam to SpamCop.");
}
}
$self->delete_fulltext_tmpfile();
if ( $available == 0 ) {
warn "SpamAssassin: no Internet hashing methods available, so couldn't report.\n";
}
return $return;
}
sub revoke {
my ($self) = @_;
my $return = 1;
my $text = $self->{main}->remove_spamassassin_markup ($self->{msg});
if (!$self->{main}->{local_tests_only}
&& !$self->{options}->{dont_report_to_razor}
&& $self->is_razor_available()) {
if ($self->razor_revoke($text)) {
dbg ("SpamAssassin: spam revoked from Razor.");
$return = 0;
}
else {
dbg ("SpamAssassin: could not revoke spam from Razor.");
}
}
return $return;
}
sub adie {
my $msg = shift;
alarm 0;
die $msg;
}
sub close_pipe_fh {
my ($self, $fh) = @_;
return if close ($fh);
my $exitstatus = $?;
dbg ("raw exit code: $exitstatus");
if (WIFEXITED ($exitstatus) && (WEXITSTATUS ($exitstatus))) {
die "Exited with non-zero exit code " . WEXITSTATUS ($exitstatus) . "\n";
}
if (WIFSIGNALED ($exitstatus)) {
die "Exited due to signal " . WTERMSIG ($exitstatus) . "\n";
}
}
sub razor_report {
my ($self, $fulltext, $revoke) = @_;
my $timeout=$self->{conf}->{razor_timeout};
my $response;
my $type = (defined($revoke) && $revoke) ? 'revoke' : 'report';
if ($Mail::SpamAssassin::DEBUG->{enabled}) {
open (OLDOUT, ">&STDOUT");
open (STDOUT, ">&STDERR");
}
$self->enter_helper_run_mode();
eval { require Razor2::Client::Agent; };
if ( !$@ ) {
eval {
local ($^W) = 0;
local $SIG{ALRM} = sub { die "alarm\n" };
alarm $timeout;
my $rc = Razor2::Client::Agent->new("razor-$type");
if ($rc) {
my %opt = (
debug => $Mail::SpamAssassin::DEBUG->{enabled},
foreground => 1,
config => $self->{conf}->{razor_config}
);
$rc->{opt} = \%opt;
$rc->do_conf() or adie($rc->errstr);
my $ident = $rc->get_ident
or adie ("Razor2 $type requires authentication");
my @msg = (\$fulltext);
my $objects = $rc->prepare_objects( \@msg )
or adie ("error in prepare_objects");
$rc->get_server_info() or adie $rc->errprefix("reportit");
alarm $timeout;
my $sigs = $rc->compute_sigs($objects)
or adie ("error in compute_sigs");
$rc->connect() or adie ($rc->errprefix("reportit"));
$rc->authenticate($ident) or adie ($rc->errprefix("reportit"));
$rc->report($objects) or adie ($rc->errprefix("reportit"));
$rc->disconnect() or adie ($rc->errprefix("reportit"));
$response = 1; }
else {
warn "undefined Razor2::Client::Agent\n";
}
alarm 0;
dbg("Razor2: spam $type, response is \"$response\".");
};
alarm 0;
if ($@) {
if ( $@ =~ /alarm/ ) {
dbg("razor2 $type timed out after $timeout secs.");
} elsif ($@ =~ /could not connect/) {
dbg("razor2 $type could not connect to any servers");
} elsif ($@ =~ /timeout/i) {
dbg("razor2 $type timed out connecting to razor servers");
} else {
warn "razor2 $type failed: $! $@";
}
undef $response;
}
}
srand;
$self->leave_helper_run_mode();
if ($Mail::SpamAssassin::DEBUG->{enabled}) {
open (STDOUT, ">&OLDOUT");
close OLDOUT;
}
if (defined($response) && $response+0) {
return 1;
} else {
return 0;
}
}
sub razor_revoke {
my ($self, $fulltext) = @_;
return $self->razor_report($fulltext, 1);
}
sub dcc_report {
my ($self, $fulltext) = @_;
my $timeout=$self->{conf}->{dcc_timeout};
$self->enter_helper_run_mode();
my $tmpf = $self->create_fulltext_tmpfile(\$fulltext);
eval {
local $SIG{ALRM} = sub { die "__alarm__\n" };
local $SIG{PIPE} = sub { die "__brokenpipe__\n" };
alarm $timeout;
my $path = Mail::SpamAssassin::Util::untaint_file_path ($self->{conf}->{dcc_path});
my $opts = '';
if ( $self->{conf}->{dcc_options} =~ /^([^\;\'\"\0]+)$/ ) {
$opts = $1;
}
my $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
$tmpf, 1, $path, "-t", "many", split(' ', $opts));
$pid or die "$!\n";
my @ignored = <DCC>;
$self->close_pipe_fh (\*DCC);
alarm(0);
waitpid ($pid, 0);
};
alarm 0;
$self->leave_helper_run_mode();
if ($@) {
if ($@ =~ /^__alarm__$/) {
dbg ("DCC -> report timed out after $timeout secs.");
} elsif ($@ =~ /^__brokenpipe__$/) {
dbg ("DCC -> report failed: Broken pipe.");
} else {
warn ("DCC -> report failed: $@\n");
}
return 0;
}
return 1;
}
sub pyzor_report {
my ($self, $fulltext) = @_;
my $timeout=$self->{conf}->{pyzor_timeout};
$self->enter_helper_run_mode();
my $tmpf = $self->create_fulltext_tmpfile(\$fulltext);
eval {
local $SIG{ALRM} = sub { die "__alarm__\n" };
local $SIG{PIPE} = sub { die "__brokenpipe__\n" };
alarm $timeout;
my $path = Mail::SpamAssassin::Util::untaint_file_path ($self->{conf}->{pyzor_path});
my $opts = '';
if ( $self->{conf}->{pyzor_options} =~ /^([^\;\'\"\0]+)$/ ) {
$opts = $1;
}
my $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*PYZOR,
$tmpf, 1, $path, split(' ', $opts), "report");
$pid or die "$!\n";
my @ignored = <PYZOR>;
$self->close_pipe_fh (\*PYZOR);
alarm(0);
waitpid ($pid, 0);
};
alarm 0;
$self->leave_helper_run_mode();
if ($@) {
if ($@ =~ /^__alarm__$/) {
dbg ("Pyzor -> report timed out after $timeout secs.");
} elsif ($@ =~ /^__brokenpipe__$/) {
dbg ("Pyzor -> report failed: Broken pipe.");
} else {
warn ("Pyzor -> report failed: $@\n");
}
return 0;
}
return 1;
}
sub smtp_dbg {
my ($command, $smtp) = @_;
dbg("SpamCop -> sent $command");
my $code = $smtp->code();
my $message = $smtp->message();
my $debug;
$debug .= $code if $code;
$debug .= ($code ? " " : "") . $message if $message;
chomp $debug;
dbg("SpamCop -> received $debug");
return 1;
}
sub spamcop_report {
my ($self, $original) = @_;
my $header = $original;
$header =~ s/\r?\n\r?\n.*//s;
my $date = Mail::SpamAssassin::Util::receive_date($header);
if ($date && $date < time - 3*86400) {
warn ("SpamCop -> message older than 3 days, not reporting\n");
return 0;
}
my $boundary = "----------=_" . sprintf("%08X.%08X",time,int(rand(2**32)));
while ($original =~ /^\Q${boundary}\E$/m) {
$boundary .= "/".sprintf("%08X",int(rand(2**32)));
}
my $description = "spam report via " . Mail::SpamAssassin::Version();
my $trusted = $self->{msg}->{metadata}->{relays_trusted_str};
my $untrusted = $self->{msg}->{metadata}->{relays_untrusted_str};
my $user = $self->{main}->{'username'} || 'unknown';
my $host = Mail::SpamAssassin::Util::fq_hostname() || 'unknown';
my $from = $self->{conf}->{spamcop_from_address} || "$user\@$host";
my %head = (
'To' => $self->{conf}->{spamcop_to_address},
'From' => $from,
'Subject' => 'report spam',
'Date' => Mail::SpamAssassin::Util::time_to_rfc822_date(),
'Message-Id' =>
sprintf("<%08X.%08X@%s>",time,int(rand(2**32)),$host),
'MIME-Version' => '1.0',
'Content-Type' => "multipart/mixed; boundary=\"$boundary\"",
);
if (length($original) > 64*1024) {
substr($original,(64*1024)) = "\n[truncated by SpamAssassin]\n";
}
my $body = <<"EOM";
This is a multi-part message in MIME format.
--$boundary
Content-Type: message/rfc822; x-spam-type=report
Content-Description: $description
Content-Disposition: attachment
Content-Transfer-Encoding: 8bit
X-Spam-Relays-Trusted: $trusted
X-Spam-Relays-Untrusted: $untrusted
$original
--$boundary--
EOM
my $message;
while (my ($k, $v) = each %head) {
$message .= "$k: $v\n";
}
$message .= "\n" . $body;
my $failure;
my $mx = $head{To};
my $hello = Mail::SpamAssassin::Util::fq_hostname() || $from;
$mx =~ s/.*\@//;
$hello =~ s/.*\@//;
for my $rr (Net::DNS::mx($mx)) {
my $exchange = Mail::SpamAssassin::Util::untaint_hostname($rr->exchange);
next unless $exchange;
my $smtp;
if ($smtp = Net::SMTP->new($exchange,
Hello => $hello,
Port => 587,
Timeout => 10))
{
if ($smtp->mail($from) && smtp_dbg("FROM $from", $smtp) &&
$smtp->recipient($head{To}) && smtp_dbg("TO $head{To}", $smtp) &&
$smtp->data($message) && smtp_dbg("DATA", $smtp) &&
$smtp->quit() && smtp_dbg("QUIT", $smtp))
{
warn("SpamCop -> report to $exchange succeeded\n") if defined $failure;
return 1;
}
my $code = $smtp->code();
my $text = $smtp->message();
$failure = "$code $text" if ($code && $text);
}
$failure ||= "Net::SMTP error";
chomp $failure;
warn("SpamCop -> report to $exchange failed: $failure\n");
}
return 0;
}
sub dbg { Mail::SpamAssassin::dbg (@_); }
sub create_fulltext_tmpfile { Mail::SpamAssassin::PerMsgStatus::create_fulltext_tmpfile(@_) }
sub delete_fulltext_tmpfile { Mail::SpamAssassin::PerMsgStatus::delete_fulltext_tmpfile(@_) }
sub is_dcc_available {
Mail::SpamAssassin::PerMsgStatus::is_dcc_available(@_);
}
sub is_pyzor_available {
Mail::SpamAssassin::PerMsgStatus::is_pyzor_available(@_);
}
sub is_razor_available {
Mail::SpamAssassin::PerMsgStatus::is_razor2_available(@_);
}
sub is_spamcop_available {
my ($self) = @_;
return (HAS_NET_DNS &&
HAS_NET_SMTP &&
$self->{conf}{scores}{'RCVD_IN_BL_SPAMCOP_NET'});
}
sub enter_helper_run_mode { Mail::SpamAssassin::PerMsgStatus::enter_helper_run_mode(@_); }
sub leave_helper_run_mode { Mail::SpamAssassin::PerMsgStatus::leave_helper_run_mode(@_); }
1;