@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();
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
- 2*86400) {
warn
(
"SpamCop -> message older than 2 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
) >
$self
->{conf}->{spamcop_max_report_size}*1024) {
substr
(
$original
,(
$self
->{conf}->{spamcop_max_report_size}*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;