Mail::SpamAssassin::Plugin::SPF - perform SPF verification tests
loadplugin Mail::SpamAssassin::Plugin::SPF =cut # <@LICENSE> # Copyright 2004 Apache Software Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # </@LICENSE>
package Mail::SpamAssassin::Plugin::SPF;
use Mail::SpamAssassin::Plugin; use strict; use bytes;
use vars qw(@ISA); @ISA = qw(Mail::SpamAssassin::Plugin);
# constructor: register the eval rule sub new { my $class = shift; my $mailsaobject = shift;
# some boilerplate... $class = ref($class) || $class; my $self = $class->SUPER::new($mailsaobject); bless ($self, $class);
my $conf = $mailsaobject->{conf};
$self->register_eval_rule ("check_for_spf_pass");
$self->register_eval_rule ("check_for_spf_fail");
$self->register_eval_rule ("check_for_spf_softfail");
$self->register_eval_rule ("check_for_spf_helo_pass");
$self->register_eval_rule ("check_for_spf_helo_fail");
$self->register_eval_rule ("check_for_spf_helo_softfail");
return $self; }
###########################################################################
# SPF support sub check_for_spf_pass { my ($self, $scanner) = @_; $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked}; $scanner->{spf_pass}; }
sub check_for_spf_fail { my ($self, $scanner) = @_; $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked}; if ($scanner->{spf_failure_comment}) { $scanner->test_log ($scanner->{spf_failure_comment}); } $scanner->{spf_fail}; }
sub check_for_spf_softfail { my ($self, $scanner) = @_; $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked}; if ($scanner->{spf_failure_comment}) { $scanner->test_log ($scanner->{spf_failure_comment}); } $scanner->{spf_softfail}; }
sub check_for_spf_helo_pass { my ($self, $scanner) = @_; $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked}; $scanner->{spf_helo_pass}; }
sub check_for_spf_helo_fail { my ($self, $scanner) = @_; $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked}; if ($scanner->{spf_helo_failure_comment}) { $scanner->test_log ($scanner->{spf_helo_failure_comment}); } $scanner->{spf_helo_fail}; }
sub check_for_spf_helo_softfail { my ($self, $scanner) = @_; $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked}; if ($scanner->{spf_helo_failure_comment}) { $scanner->test_log ($scanner->{spf_helo_failure_comment}); } $scanner->{spf_helo_softfail}; }
sub _check_spf { my ($self, $scanner, $ishelo) = @_;
return unless $scanner->is_dns_available();
# skip SPF checks if the A/MX records are nonexistent for the From # domain, anyway, to avoid crappy messages from slowing us down # (bug 3016) return if $scanner->check_for_from_dns();
if ($ishelo) {
# SPF HELO-checking variant. This isn't really SPF at all ;)
$scanner->{spf_helo_checked} = 1;
$scanner->{spf_helo_pass} = 0;
$scanner->{spf_helo_fail} = 0;
$scanner->{spf_helo_softfail} = 0;
$scanner->{spf_helo_failure_comment} = undef;
} else {
# "real" SPF; checking the envelope-from (where we can)
$scanner->{spf_checked} = 1;
$scanner->{spf_pass} = 0;
$scanner->{spf_fail} = 0;
$scanner->{spf_softfail} = 0;
$scanner->{spf_failure_comment} = undef;
}
my $lasthop = $scanner->{relays_untrusted}->[0];
if (!defined $lasthop) {
dbg ("SPF: message was delivered entirely via trusted relays, not required");
return;
}
my $ip = $lasthop->{ip};
my $helo = $lasthop->{helo};
my $sender = '';
if ($ishelo) {
dbg ("SPF: checking HELO (helo=$helo, ip=$ip)");
if ($helo !~ /^\d+\.\d+\.\d+\.\d+$/) {
# get rid of hostname part of domain, understanding delegation
$helo = Mail::SpamAssassin::Util::RegistrarBoundaries::trim_domain ($helo);
}
dbg ("SPF: trimmed HELO down to '$helo'");
} else {
$sender = $lasthop->{envfrom};
if ($sender) {
dbg ("SPF: found Envelope-From in last untrusted Received header");
}
else {
# We cannot use the env-from data, since it went through 1 or
# more relays since the untrusted sender and they may have
# rewritten it.
#
if ($scanner->{num_relays_trusted} > 0) {
dbg ("SPF: relayed through one or more trusted relays, cannot use header-based Envelope-From, skipping");
return;
}
# we can (apparently) use whatever the current Envelope-From was,
# from the Return-Path, X-Envelope-From, or whatever header.
# it's better to get it from Received though, as that is updated
# hop-by-hop.
#
$sender = $scanner->get ("EnvelopeFrom");
}
if (!$sender) {
dbg ("SPF: cannot get Envelope-From, cannot use SPF");
return;
}
dbg ("SPF: checking EnvelopeFrom (helo=$helo, ip=$ip, envfrom=$sender)");
}
# this test could probably stand to be more strict, but try to test
# any invalid HELO hostname formats with a header rule
if ($ishelo && ($helo =~ /^\d+\.\d+\.\d+\.\d+$/ || $helo =~ /^[^.]+$/)) {
dbg ("SPF: cannot check HELO of '$helo', skipping");
return;
}
if (!$helo) {
dbg ("SPF: cannot get HELO, cannot use SPF");
return;
}
if ($scanner->server_failed_to_respond_for_domain($helo)) {
dbg ("SPF: we had a previous timeout on '$helo', skipping");
return;
}
my $query;
eval {
require Mail::SPF::Query;
if ($Mail::SPF::Query::VERSION < 1.996) {
die "Mail::SPF::Query 1.996 or later required, this is $Mail::SPF::Query::VERSION\n";
}
$query = Mail::SPF::Query->new (ip => $ip,
sender => $sender,
helo => $helo,
debug => $Mail::SpamAssassin::DEBUG->{rbl},
trusted => 1);
};
if ($@) {
dbg ("SPF: cannot load or create Mail::SPF::Query module");
return;
}
my ($result, $comment); my $timeout = 5;
eval {
local $SIG{ALRM} = sub { die "__alarm__\n" };
alarm($timeout);
($result, $comment) = $query->result();
alarm(0);
};
alarm 0;
if ($@) {
if ($@ =~ /^__alarm__$/) {
dbg ("SPF: lookup timed out after $timeout secs.");
} else {
warn ("SPF: lookup failed: $@\n");
}
return 0;
}
$result ||= 'softfail'; $comment ||= ''; $comment =~ s/\s+/ /gs; # no newlines please
if ($ishelo) {
if ($result eq 'pass') { $scanner->{spf_helo_pass} = 1; }
elsif ($result eq 'fail') { $scanner->{spf_helo_fail} = 1; }
elsif ($result eq 'softfail') { $scanner->{spf_helo_softfail} = 1; }
if ($result eq 'fail' || $result eq 'softfail') {
$scanner->{spf_helo_failure_comment} = "SPF failed: $comment";
}
} else {
if ($result eq 'pass') { $scanner->{spf_pass} = 1; }
elsif ($result eq 'fail') { $scanner->{spf_fail} = 1; }
elsif ($result eq 'softfail') { $scanner->{spf_softfail} = 1; }
if ($result eq 'fail' || $result eq 'softfail') {
$scanner->{spf_failure_comment} = "SPF failed: $comment";
}
}
dbg ("SPF: query for $sender/$ip/$helo: result: $result, comment: $comment");
}
###########################################################################
sub dbg { Mail::SpamAssassin::dbg (@_); }
1;