package Mail::SpamAssassin::Dns;
1;
package Mail::SpamAssassin::PerMsgStatus;
use Mail::SpamAssassin::Conf;
use Mail::SpamAssassin::PerMsgStatus;
use Mail::SpamAssassin::Constants qw(:ip);
use File::Spec;
use IO::Socket;
use IPC::Open2;
use POSIX ":sys_wait_h";
use strict;
use bytes;
use Carp;
use vars qw{
$KNOWN_BAD_DIALUP_RANGES @EXISTING_DOMAINS $IS_DNS_AVAILABLE $VERSION
};
@EXISTING_DOMAINS = qw{
adelphia.net
akamai.com
apache.org
cingular.com
colorado.edu
comcast.net
doubleclick.com
ebay.com
gmx.net
google.com
intel.com
kernel.org
linux.org
mit.edu
motorola.com
msn.com
sourceforge.net
sun.com
w3.org
yahoo.com
};
$IS_DNS_AVAILABLE = undef;
$VERSION = 'bogus';
BEGIN {
no strict;
local ($^W) = 0;
eval {
require Net::DNS;
require Net::DNS::Resolver;
};
eval {
require Razor2::Client::Agent;
};
eval {
require MIME::Base64;
};
eval {
require IO::Socket::UNIX;
};
};
use constant BGSOCK => 0;
use constant RULES => 1;
use constant SETS => 2;
sub do_rbl_lookup {
my ($self, $rule, $set, $type, $server, $host, $subtest) = @_;
if (!defined $self->{dnspending}->{$type}->{$host}->[BGSOCK]) {
dbg("rbl: launching DNS $type query for $host in background", "rbl", -1);
$self->{rbl_launch} = time;
$self->{dnspending}->{$type}->{$host}->[BGSOCK] =
$self->{res}->bgsend($host, $type);
}
push @{$self->{dnspending}->{$type}->{$host}->[SETS]}, $set;
if (defined $subtest) {
$self->{dnspost}->{$set}->{$subtest} = $rule;
}
else {
push @{$self->{dnspending}->{$type}->{$host}->[RULES]}, $rule;
}
}
sub register_rbl_subtest {
my ($self, $rule, $set, $subtest) = @_;
$self->{dnspost}->{$set}->{$subtest} = $rule;
}
sub do_dns_lookup {
my ($self, $rule, $type, $host) = @_;
if (!defined $self->{dnspending}->{$type}->{$host}->[BGSOCK]) {
dbg("dns: launching DNS $type query for $host in background", "rbl", -1);
$self->{rbl_launch} = time;
$self->{dnspending}->{$type}->{$host}->[BGSOCK] =
$self->{res}->bgsend($host, $type);
}
push @{$self->{dnspending}->{$type}->{$host}->[RULES]}, $rule;
}
sub dnsbl_hit {
my ($self, $rule, $question, $answer) = @_;
my $log = "";
if (substr($rule, 0, 2) ne "__") {
if ($answer->type eq 'TXT') {
$log = $answer->rdatastr;
$log =~ s/^"(.*)"$/$1/;
$log =~ s/(http:\/\/\S+)/<$1>/g;
}
elsif ($question->string =~ m/^(\d+)\.(\d+)\.(\d+)\.(\d+)\.(\S+\w)/) {
$log = "$4.$3.$2.$1 listed in $5";
}
}
$self->{dnsresult}->{$rule}->{$log} = 1;
}
sub dnsbl_uri {
my ($self, $question, $answer) = @_;
my $qname = $question->qname;
my $rdatastr = $answer->rdatastr;
if (defined $qname && defined $rdatastr) {
my $qclass = $question->qclass;
my $qtype = $question->qtype;
my @vals;
push(@vals, "class=$qclass") if $qclass ne "IN";
push(@vals, "type=$qtype") if $qtype ne "A";
my $uri = "dns:$qname" . (@vals ? "?" . join(";", @vals) : "");
push @{ $self->{dnsuri}->{$uri} }, $rdatastr;
}
}
sub process_dnsbl_result {
my ($self, $query) = @_;
my $packet = $self->{res}->bgread($query->[BGSOCK]);
undef $query->[BGSOCK];
return unless (defined $packet &&
defined $packet->header &&
defined $packet->question &&
defined $packet->answer);
my $question = ($packet->question)[0];
return if !defined $question;
if ($self->{sender_host} &&
$question->qname eq $self->{sender_host} &&
$question->qtype =~ /^(?:A|MX)$/ &&
$packet->header->rcode =~ /^(?:NXDOMAIN|SERVFAIL)$/ &&
++$self->{sender_host_fail} == 2)
{
for my $rule (@{$query->[RULES]}) {
$self->got_hit($rule, "DNS: ");
}
}
foreach my $answer ($packet->answer) {
next if !defined $answer;
$self->dnsbl_uri($question, $answer);
next if ($answer->type ne 'A' && $answer->type ne 'TXT');
next if ($answer->type eq 'A' && $answer->rdatastr !~ /^127\./);
for my $rule (@{$query->[RULES]}) {
$self->dnsbl_hit($rule, $question, $answer);
}
for my $set (@{$query->[SETS]}) {
if ($self->{dnspost}->{$set}) {
$self->process_dnsbl_set($set, $question, $answer);
}
}
}
}
sub process_dnsbl_set {
my ($self, $set, $question, $answer) = @_;
my $rdatastr = $answer->rdatastr;
while (my ($subtest, $rule) = each %{ $self->{dnspost}->{$set} }) {
next if defined $self->{tests_already_hit}->{$rule};
if ($subtest eq $rdatastr) {
$self->dnsbl_hit($rule, $question, $answer);
}
elsif ($subtest =~ s/^sb://) {
if ($self->{conf}->{user_defined_rules}->{$rule}) {
dbg ("RBL: skipping rule '$rule': not supported when user-defined");
next;
}
$rdatastr =~ s/^"?\d+-//;
$rdatastr =~ s/"$//;
my %sb = ($rdatastr =~ m/(?:^|\|)(\d+)=([^|]+)/g);
my $undef = 0;
while ($subtest =~ m/\bS(\d+)\b/g) {
if (!defined $sb{$1}) {
$undef = 1;
last;
}
$subtest =~ s/\bS(\d+)\b/\$sb{$1}/;
}
$subtest =~ /^(.*)$/;
my $untainted = $1;
$subtest = $untainted;
$self->got_hit($rule, "SenderBase: ") if !$undef && eval $subtest;
}
elsif ($subtest =~ /^\d+$/) {
if ($rdatastr =~ m/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/ &&
Mail::SpamAssassin::Util::my_inet_aton($rdatastr) & $subtest)
{
$self->dnsbl_hit($rule, $question, $answer);
}
}
else {
my $test = qr/$subtest/;
if ($rdatastr =~ /$test/) {
$self->dnsbl_hit($rule, $question, $answer);
}
}
}
}
sub harvest_dnsbl_queries {
my ($self) = @_;
return if !defined $self->{rbl_launch};
my $timeout = $self->{conf}->{rbl_timeout} + $self->{rbl_launch};
my @waiting = (values %{ $self->{dnspending}->{A} },
values %{ $self->{dnspending}->{MX} },
values %{ $self->{dnspending}->{TXT} });
my @left;
my $total;
@waiting = grep { defined $_->[BGSOCK] } @waiting;
$total = scalar @waiting;
while (@waiting) {
@left = ();
for my $query (@waiting) {
if ($self->{res}->bgisready($query->[BGSOCK])) {
$self->process_dnsbl_result($query);
}
else {
push(@left, $query);
}
}
$self->{main}->call_plugins ("check_tick", { permsgstatus => $self });
last unless @left;
last if time >= $timeout;
@waiting = @left;
my $dynamic = (int($self->{conf}->{rbl_timeout}
* (1 - (($total - @left) / $total) ** 2) + 0.5)
+ $self->{rbl_launch});
$timeout = $dynamic if ($dynamic < $timeout);
sleep 1;
}
dbg("RBL: success for " . ($total - @left) . " of $total queries", "rbl", 0);
for my $query (@left) {
my $string = '';
if (defined @{$query->[SETS]}) {
$string = join(",", grep defined, @{$query->[SETS]});
}
elsif (defined @{$query->[RULES]}) {
$string = join(",", grep defined, @{$query->[RULES]});
}
my $delay = time - $self->{rbl_launch};
dbg("DNS: timeout for $string after $delay seconds", "rbl", 0);
undef $query->[BGSOCK];
}
while (my ($rule, $logs) = each %{ $self->{dnsresult} }) {
for my $log (keys %{$logs}) {
$self->test_log($log) if $log;
}
if (!defined $self->{tests_already_hit}->{$rule}) {
$self->got_hit($rule, "RBL: ");
}
}
while (my ($dnsuri, $answers) = each %{ $self->{dnsuri} }) {
$self->{tag_data}->{RBL} .= "<$dnsuri>" .
" [" . join(", ", @{ $answers }) . "]\n";
}
chomp $self->{tag_data}->{RBL} if defined $self->{tag_data}->{RBL};
}
sub rbl_finish {
my ($self) = @_;
foreach my $type (keys %{$self->{dnspending}}) {
foreach my $host (keys %{$self->{dnspending}->{$type}}) {
if (defined $self->{dnspending}->{$type}->{$host}->[BGSOCK]) {
eval {
delete $self->{dnspending}->{$type}->{$host}->[BGSOCK];
};
}
}
}
delete $self->{rbl_launch};
delete $self->{dnspending};
delete $self->{dnscache};
delete $self->{dnspost};
delete $self->{dnsresult};
delete $self->{dnsuri};
}
sub is_razor2_available {
my ($self) = @_;
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, ignoring Razor2", "razor", -1);
return 0;
}
if (!$self->{conf}->{use_razor2}) { return 0; }
if (eval { require Razor2::Client::Agent; }) {
dbg("Razor2 is available", "razor", -1);
return 1;
}
else {
dbg("Razor2 is not available", "razor", -1);
return 0;
}
}
sub razor2_lookup {
my ($self, $fulltext) = @_;
my $timeout=$self->{conf}->{razor_timeout};
$self->{razor2_cf_score} = 0;
return $self->{razor2_result} if ( defined $self->{razor2_result} );
$self->{razor2_result} = 0;
if (!$self->is_razor2_available()) { return 0; }
if ($Mail::SpamAssassin::DEBUG->{enabled}) {
open (OLDOUT, ">&STDOUT");
open (STDOUT, ">&STDERR");
}
$self->enter_helper_run_mode();
eval {
local ($^W) = 0;
require Razor2::Client::Agent;
local $SIG{ALRM} = sub { die "alarm\n" };
alarm $timeout;
my $rc = Razor2::Client::Agent->new('razor-check');
if ($rc) {
my %opt = (
debug => ($Mail::SpamAssassin::DEBUG->{enabled} and
$Mail::SpamAssassin::DEBUG->{razor} < -2),
foreground => 1,
config => $self->{conf}->{razor_config}
);
$rc->{opt} = \%opt;
$rc->do_conf() or die $rc->errstr;
my $tmptext = $$fulltext;
my @msg = (\$tmptext);
my $objects = $rc->prepare_objects( \@msg )
or die "error in prepare_objects";
$rc->get_server_info() or die $rc->errprefix("checkit");
alarm $timeout;
my $sigs = $rc->compute_sigs($objects)
or die "error in compute_sigs";
if ( ! $rc->local_check( $objects->[0] ) ) {
if (!$rc->connect()) {
die "could not connect to any servers\n";
}
$rc->check($objects) or die $rc->errprefix("checkit");
$rc->disconnect() or die $rc->errprefix("checkit");
alarm 0;
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);
}
dbg("Using results from Razor v".$Razor2::Client::Version::VERSION);
$self->{razor2_result} = $objects->[0]->{spam} || 0;
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} || 0; my $tmpct = $tmp->{ct} || 0; my $engine = $cf->{sent}->[$response]->{e};
dbg("Found Razor2 part: part=$part engine=$engine ct=$tmpct cf=$tmpcf");
$self->{razor2_cf_score} = $tmpcf if ( !$tmpct && $tmpcf > $self->{razor2_cf_score} );
}
}
else {
my $text = "part=$part noresponse";
$text .= " skipme=1" if ( $cf->{skipme} );
dbg("Found Razor2 part: $text");
}
$part++;
}
}
else {
dbg("It looks like the internal Razor object has changed format! Tell spamassassin-devel!");
}
}
}
else {
warn "undefined Razor2::Client::Agent\n";
}
alarm 0;
};
alarm 0;
if ($@) {
if ( $@ =~ /alarm/ ) {
dbg("razor2 check timed out after $timeout secs.");
} elsif ($@ =~ /(?:could not connect|network is unreachable)/) {
dbg("razor2 check could not connect to any servers");
} else {
warn("razor2 check skipped: $! $@");
}
}
srand;
$self->leave_helper_run_mode();
if ($Mail::SpamAssassin::DEBUG->{enabled}) {
open (STDOUT, ">&OLDOUT");
close OLDOUT;
}
dbg("Razor2 results: spam? ".$self->{razor2_result}." highest cf score: ".$self->{razor2_cf_score});
if ($self->{razor2_result} > 0) {
return 1;
}
return 0;
}
sub is_dccifd_available {
my ($self) = @_;
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, ignoring DCCifd");
return 0;
}
my $dcchome = $self->{conf}->{dcc_home} || '';
my $dccifd = $self->{conf}->{dcc_dccifd_path} || '';
if (!$dccifd && ($dcchome && -S "$dcchome/dccifd")) {
$dccifd = "$dcchome/dccifd";
}
unless ($dccifd && -S $dccifd && -w _ && -r _ ) {
dbg ("DCCifd is not available: no r/w dccifd socket found.");
return 0;
}
$self->{conf}->{dcc_dccifd_path} = $dccifd;
dbg ("DCCifd is available: ".$self->{conf}->{dcc_dccifd_path});
return 1;
}
sub is_dcc_available {
my ($self) = @_;
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, ignoring DCC");
return 0;
}
if (!$self->{conf}->{use_dcc}) { return 0; }
my $dcchome = $self->{conf}->{dcc_home} || '';
my $dccproc = $self->{conf}->{dcc_path} || '';
if (!$dccproc && ($dcchome && -x "$dcchome/bin/dccproc")) {
$dccproc = "$dcchome/bin/dccproc";
}
unless ($dccproc) {
$dccproc = Mail::SpamAssassin::Util::find_executable_in_env_path('dccproc');
}
unless ($dccproc && -x $dccproc) {
dbg ("DCC is not available: no executable dccproc found.");
return 0;
}
$self->{conf}->{dcc_path} = $dccproc;
dbg ("DCC is available: ".$self->{conf}->{dcc_path});
return 1;
}
sub dccifd_lookup {
my ($self, $fulltext) = @_;
my $response = "";
my %count;
my $left;
my $right;
my $timeout=$self->{conf}->{dcc_timeout};
my $sockpath;
$count{body} = 0;
$count{fuz1} = 0;
$count{fuz2} = 0;
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, ignoring DCCifd");
return 0;
}
if ($$fulltext eq '') {
dbg ("empty message, ignoring DCCifd");
return 0;
}
if ( ! $self->{conf}->{dcc_home} ) {
dbg ("dcc_home not defined, should not get here");
return 0;
}
$sockpath = $self->{conf}->{dcc_dccifd_path};
if ( ! -S $sockpath || ! -w _ || ! -r _ ) {
dbg ("dccifd not a socket, should not get here");
return 0;
}
$self->enter_helper_run_mode();
eval {
local $SIG{ALRM} = sub { die "alarm\n" };
alarm($timeout);
my $sock = IO::Socket::UNIX->new(Type => SOCK_STREAM,
Peer => $sockpath) || dbg("failed to open socket") && die;
$sock->print("header\n") || dbg("failed write") && die; $sock->print("0.0.0.0\n") || dbg("failed write") && die; $sock->print("\n") || dbg("failed write") && die; $sock->print("\n") || dbg("failed write") && die; $sock->print("unknown\r\n") || dbg("failed write") && die; $sock->print("\n") || dbg("failed write") && die;
$sock->print($$fulltext);
$sock->shutdown(1) || dbg("failed socket shutdown: $!") && die;
$sock->getline() || dbg("failed read status") && die;
$sock->getline() || dbg("failed read multistatus") && die;
my @null = $sock->getlines();
if ( $ dbg("failed read header");
die;
}
chomp($response = shift @null);
while ( my $v = shift @null ) {
last unless ( $v =~ s/^\s+/ / ); chomp $v;
$response .= $v;
}
dbg("DCCifd: got response: $response");
};
alarm(0);
$self->leave_helper_run_mode();
if ($@) {
$response = undef;
if ($@ =~ /alarm/) {
dbg ("DCCifd check timed out after $timeout secs.");
return 0;
} else {
warn ("DCCifd -> check skipped: $! $@");
return 0;
}
}
if (!defined $response || $response !~ /^X-DCC/) {
dbg ("DCCifd -> check failed - no X-DCC returned: $response");
return 0;
}
if ($response =~ /^X-DCC-(.*)-Metrics: (.*)$/) {
$self->{tag_data}->{DCCB} = $1;
$self->{tag_data}->{DCCR} = $2;
}
$response =~ s/many/999999/ig;
$response =~ s/ok\d?/0/ig;
if ($response =~ /Body=(\d+)/) {
$count{body} = $1+0;
}
if ($response =~ /Fuz1=(\d+)/) {
$count{fuz1} = $1+0;
}
if ($response =~ /Fuz2=(\d+)/) {
$count{fuz2} = $1+0;
}
if ($count{body} >= $self->{conf}->{dcc_body_max} || $count{fuz1} >= $self->{conf}->{dcc_fuz1_max} || $count{fuz2} >= $self->{conf}->{dcc_fuz2_max}) {
dbg ("DCCifd: Listed! BODY: $count{body} of $self->{conf}->{dcc_body_max} FUZ1: $count{fuz1} of $self->{conf}->{dcc_fuz1_max} FUZ2: $count{fuz2} of $self->{conf}->{dcc_fuz2_max}");
return 1;
}
return 0;
}
sub dcc_lookup {
my ($self, $fulltext) = @_;
my $response = undef;
my %count;
my $timeout=$self->{conf}->{dcc_timeout};
$count{body} = 0;
$count{fuz1} = 0;
$count{fuz2} = 0;
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, ignoring DCC");
return 0;
}
if (!$self->{conf}->{use_dcc}) { return 0; }
$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;
}
dbg("DCC command: ".join(' ', $path, "-H", $opts, "< '$tmpf'", "2>&1"),'dcc',-1);
my $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*DCC,
$tmpf, 1, $path, "-H", split(' ', $opts));
$pid or die "$!\n";
my @null = <DCC>;
close DCC;
if ( $ dbg("failed read header");
die;
}
chomp($response = shift @null);
while ( my $v = shift @null ) {
last unless ( $v =~ s/^\s+/ / ); chomp $v;
$response .= $v;
}
unless (defined($response)) {
die ("no response\n"); }
dbg("DCC: got response: $response");
alarm(0);
$self->cleanup_kids($pid);
};
alarm 0;
$self->leave_helper_run_mode();
if ($@) {
if ($@ =~ /^__alarm__$/) {
dbg ("DCC -> check timed out after $timeout secs.");
} elsif ($@ =~ /^__brokenpipe__$/) {
dbg ("DCC -> check failed: Broken pipe.");
} elsif ($@ eq "no response\n") {
dbg ("DCC -> check failed: no response");
} else {
warn ("DCC -> check failed: $@\n");
}
return 0;
}
if (!defined($response) || $response !~ /^X-DCC/) {
dbg ("DCC -> check failed: no X-DCC returned (did you create a map file?): $response");
return 0;
}
if ($response =~ /^X-DCC-(.*)-Metrics: (.*)$/) {
$self->{tag_data}->{DCCB} = $1;
$self->{tag_data}->{DCCR} = $2;
}
$response =~ s/many/999999/ig;
$response =~ s/ok\d?/0/ig;
if ($response =~ /Body=(\d+)/) {
$count{body} = $1+0;
}
if ($response =~ /Fuz1=(\d+)/) {
$count{fuz1} = $1+0;
}
if ($response =~ /Fuz2=(\d+)/) {
$count{fuz2} = $1+0;
}
if ($count{body} >= $self->{conf}->{dcc_body_max} || $count{fuz1} >= $self->{conf}->{dcc_fuz1_max} || $count{fuz2} >= $self->{conf}->{dcc_fuz2_max}) {
dbg ("DCC: Listed! BODY: $count{body} of $self->{conf}->{dcc_body_max} FUZ1: $count{fuz1} of $self->{conf}->{dcc_fuz1_max} FUZ2: $count{fuz2} of $self->{conf}->{dcc_fuz2_max}");
return 1;
}
return 0;
}
sub is_pyzor_available {
my ($self) = @_;
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, ignoring Pyzor");
return 0;
}
if (!$self->{conf}->{use_pyzor}) { return 0; }
my $pyzor = $self->{conf}->{pyzor_path} || '';
unless ($pyzor) {
$pyzor = Mail::SpamAssassin::Util::find_executable_in_env_path('pyzor');
if ($pyzor) { $self->{conf}->{pyzor_path} = $pyzor; }
}
unless ($pyzor && -x $pyzor) {
dbg ("Pyzor is not available: pyzor not found");
return 0;
}
dbg ("Pyzor is available: ".$self->{conf}->{pyzor_path});
return 1;
}
sub pyzor_lookup {
my ($self, $fulltext) = @_;
my $response = undef;
my $pyzor_count;
my $pyzor_whitelisted;
my $timeout=$self->{conf}->{pyzor_timeout};
$pyzor_count = 0;
$pyzor_whitelisted = 0;
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, ignoring Pyzor");
return 0;
}
if (!$self->{conf}->{use_pyzor}) { return 0; }
$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 = $self->{conf}->{pyzor_options};
$opts =~ s/[^-A-Za-z0-9 \/_]/_/gs;
dbg("Pyzor command: ".join(' ', $path, $opts, "check", "< '$tmpf'", "2>&1"),'pyzor',-1);
my $pid = Mail::SpamAssassin::Util::helper_app_pipe_open(*PYZOR,
$tmpf, 1, $path, split(' ', $opts), "check");
$pid or die "$!\n";
$response = <PYZOR>;
close PYZOR;
unless (defined($response)) {
die ("no response\n"); }
chomp $response;
dbg("Pyzor: got response: $response");
alarm(0);
$self->cleanup_kids($pid);
};
alarm 0;
$self->leave_helper_run_mode();
if ($@) {
if ($@ =~ /^__alarm__$/) {
dbg ("Pyzor -> check timed out after $timeout secs.");
} elsif ($@ =~ /^__brokenpipe__$/) {
dbg ("Pyzor -> check failed: Broken pipe.");
} elsif ($@ eq "no response\n") {
dbg ("Pyzor -> check failed: no response");
} else {
warn ("Pyzor -> check failed: $@\n");
}
return 0;
}
if ($response =~ /^\S+\t.*?\t(\d+)\t(\d+)\s*$/) {
$pyzor_whitelisted = $2+0;
if ($pyzor_whitelisted == 0) {
$pyzor_count = $1+0;
}
} else {
dbg ("Pyzor: couldn't grok response \"$response\"");
}
if ($pyzor_whitelisted) {
$self->{tag_data}->{PYZOR} = "Whitelisted.";
} else {
$self->{tag_data}->{PYZOR} = "Reported $pyzor_count times.";
}
if ($pyzor_count >= $self->{conf}->{pyzor_max}) {
dbg ("Pyzor: Listed! $pyzor_count of $self->{conf}->{pyzor_max} and whitelist is $pyzor_whitelisted");
return 1;
}
return 0;
}
sub load_resolver {
my ($self) = @_;
if (defined $self->{res}) { return 1; }
$self->{no_resolver} = 1;
eval {
require Net::DNS;
$self->{res} = Net::DNS::Resolver->new;
if (defined $self->{res}) {
$self->{no_resolver} = 0;
$self->{res}->retry(1); $self->{res}->retrans(0); $self->{res}->dnsrch(0); $self->{res}->defnames(0); $self->{res}->tcp_timeout(3); $self->{res}->udp_timeout(3); $self->{res}->persistent_tcp(1);
$self->{res}->persistent_udp(1);
}
1;
};
dbg ("is Net::DNS::Resolver available? " .
($self->{no_resolver} ? "no" : "yes"));
if (!$self->{no_resolver} && defined $Net::DNS::VERSION) {
dbg("Net::DNS version: ".$Net::DNS::VERSION);
}
return (!$self->{no_resolver});
}
sub lookup_ns {
my ($self, $dom) = @_;
return unless $self->load_resolver();
return if ($self->server_failed_to_respond_for_domain ($dom));
my $nsrecords;
dbg ("looking up NS for '$dom'");
if (exists $self->{dnscache}->{NS}->{$dom}) {
$nsrecords = $self->{dnscache}->{NS}->{$dom};
} else {
eval {
my $query = $self->{res}->search($dom, 'NS');
my @nses = ();
if ($query) {
foreach my $rr ($query->answer) {
if ($rr->type eq "NS") { push (@nses, $rr->nsdname); }
}
}
$nsrecords = $self->{dnscache}->{NS}->{$dom} = [ @nses ];
};
if ($@) {
dbg ("NS lookup failed horribly, perhaps bad resolv.conf setting?");
return undef;
}
}
$nsrecords;
}
sub lookup_mx {
my ($self, $dom) = @_;
return unless $self->load_resolver();
return if ($self->server_failed_to_respond_for_domain ($dom));
my $mxrecords;
dbg ("looking up MX for '$dom'");
if (exists $self->{dnscache}->{MX}->{$dom}) {
$mxrecords = $self->{dnscache}->{MX}->{$dom};
} else {
eval {
my @recs = Net::DNS::mx ($self->{res}, $dom);
my @ips = map { $_->exchange } @recs;
$mxrecords = $self->{dnscache}->{MX}->{$dom} = [ @ips ];
};
if ($@) {
dbg ("MX lookup failed horribly, perhaps bad resolv.conf setting?");
return undef;
}
}
$mxrecords;
}
sub lookup_mx_exists {
my ($self, $dom) = @_;
my $ret = 0;
my $recs = $self->lookup_mx ($dom);
if (!defined $recs) { return undef; }
if (scalar @{$recs}) { $ret = 1; }
dbg ("MX for '$dom' exists? $ret");
return $ret;
}
sub lookup_ptr {
my ($self, $dom) = @_;
return undef unless $self->load_resolver();
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, not looking up PTR");
return undef;
}
my $IP_IN_RESERVED_RANGE = IP_IN_RESERVED_RANGE;
if ($dom =~ /${IP_IN_RESERVED_RANGE}/) {
dbg ("IP is reserved, not looking up PTR: $dom");
return undef;
}
return if ($self->server_failed_to_respond_for_domain ($dom));
dbg ("looking up PTR record for '$dom'");
my $name = '';
if (exists $self->{dnscache}->{PTR}->{$dom}) {
$name = $self->{dnscache}->{PTR}->{$dom};
} else {
eval {
my $query = $self->{res}->search($dom);
if ($query) {
foreach my $rr ($query->answer) {
if ($rr->type eq "PTR") {
$name = $rr->ptrdname; last;
}
}
}
$name = $self->{dnscache}->{PTR}->{$dom} = $name;
};
if ($@) {
dbg ("PTR lookup failed horribly, perhaps bad resolv.conf setting?");
return undef;
}
}
dbg ("PTR for '$dom': '$name'");
return $name;
}
sub lookup_a {
my ($self, $name) = @_;
return undef unless $self->load_resolver();
if ($self->{main}->{local_tests_only}) {
dbg ("local tests only, not looking up A records");
return undef;
}
return if ($self->server_failed_to_respond_for_domain ($name));
dbg ("looking up A records for '$name'");
my @addrs = ();
if (exists $self->{dnscache}->{A}->{$name}) {
my $addrptr = $self->{dnscache}->{A}->{$name};
@addrs = @{$addrptr};
} else {
eval {
my $query = $self->{res}->search($name);
if ($query) {
foreach my $rr ($query->answer) {
if ($rr->type eq "A") {
push (@addrs, $rr->address);
}
}
}
$self->{dnscache}->{A}->{$name} = [ @addrs ];
};
if ($@) {
dbg ("A lookup failed horribly, perhaps bad resolv.conf setting?");
return undef;
}
}
dbg ("A records for '$name': ".join (' ', @addrs));
return @addrs;
}
sub is_dns_available {
my ($self) = @_;
my $dnsopt = $self->{conf}->{dns_available};
my @domains;
return $IS_DNS_AVAILABLE if (defined $IS_DNS_AVAILABLE);
$IS_DNS_AVAILABLE = 0;
if ($dnsopt eq "no") {
dbg ("dns_available set to no in config file, skipping test", "dnsavailable", -1);
return $IS_DNS_AVAILABLE;
}
goto done if ($self->{main}->{local_tests_only});
if ($dnsopt eq "yes") {
$IS_DNS_AVAILABLE = 1;
dbg ("dns_available set to yes in config file, skipping test", "dnsavailable", -1);
return $IS_DNS_AVAILABLE;
}
if (defined $Net::DNS::VERSION) {
if (Mail::SpamAssassin::Util::am_running_on_windows()) {
if ($Net::DNS::VERSION < 0.46) {
dbg("Net::DNS version is $Net::DNS::VERSION, but need 0.46 for Win32",
"dnsavailable", -1);
return $IS_DNS_AVAILABLE;
}
}
else {
if ($Net::DNS::VERSION < 0.34) {
dbg("Net::DNS version is $Net::DNS::VERSION, but need 0.34",
"dnsavailable", -1);
return $IS_DNS_AVAILABLE;
}
}
}
goto done unless $self->load_resolver();
if ($dnsopt =~ /test:\s+(.+)$/) {
my $servers=$1;
dbg("servers: $servers");
@domains = split (/\s+/, $servers);
dbg("Looking up NS records for user specified servers: ".join(", ", @domains), "dnsavailable", -1);
} else {
@domains = @EXISTING_DOMAINS;
}
for(my $retry = 3; $retry > 0 and $ my $domain = splice(@domains, rand(@domains), 1);
dbg ("trying ($retry) $domain...", "dnsavailable", -2);
my $result = $self->lookup_ns($domain);
if(defined $result && scalar @$result > 0) {
if ( $result ) {
dbg ("NS lookup of $domain succeeded => Dns available (set dns_available to hardcode)", "dnsavailable", -1);
$IS_DNS_AVAILABLE = 1;
last;
}
}
else {
dbg ("NS lookup of $domain failed horribly => Perhaps your resolv.conf isn't pointing at a valid server?", "dnsavailable", -1);
$IS_DNS_AVAILABLE = 0; last;
}
}
dbg ("All NS queries failed => DNS unavailable (set dns_available to override)", "dnsavailable", -1) if ($IS_DNS_AVAILABLE == 0);
done:
dbg ("is DNS available? $IS_DNS_AVAILABLE");
return $IS_DNS_AVAILABLE;
}
sub server_failed_to_respond_for_domain {
my ($self, $dom) = @_;
if ($self->{dns_server_too_slow}->{$dom}) {
dbg ("DNS: server for '$dom' failed to reply previously, not asking again");
return 1;
}
return 0;
}
sub set_server_failed_to_respond_for_domain {
my ($self, $dom) = @_;
dbg ("DNS: server for '$dom' failed to reply, marking as bad");
$self->{dns_server_too_slow}->{$dom} = 1;
}
sub enter_helper_run_mode {
my ($self) = @_;
dbg ("entering helper-app run mode");
$self->{old_slash} = $/; %{$self->{old_env}} = ();
if ( defined %ENV ) {
while (my ($key, $value) = each %ENV) {
$self->{old_env}->{$key} = $value if defined $value;
}
}
Mail::SpamAssassin::Util::clean_path_in_taint_mode();
my $newhome;
if ($self->{main}->{home_dir_for_helpers}) {
$newhome = $self->{main}->{home_dir_for_helpers};
} else {
$newhome = (Mail::SpamAssassin::Util::portable_getpwuid ($>))[7];
}
if ($newhome) {
$ENV{'HOME'} = Mail::SpamAssassin::Util::untaint_file_path ($newhome);
}
}
sub leave_helper_run_mode {
my ($self) = @_;
dbg ("leaving helper-app run mode");
$/ = $self->{old_slash};
%ENV = %{$self->{old_env}};
}
sub cleanup_kids {
my ($self, $pid) = @_;
if ($SIG{CHLD} && $SIG{CHLD} ne 'IGNORE') { waitpid ($pid, 0);
}
}
1;