mirror of
https://git.proxmox.com/git/proxmox-spamassassin
synced 2025-04-28 12:19:37 +00:00
880 lines
31 KiB
Perl
880 lines
31 KiB
Perl
# <@LICENSE>
|
|
# Licensed to the Apache Software Foundation (ASF) under one or more
|
|
# contributor license agreements. See the NOTICE file distributed with
|
|
# this work for additional information regarding copyright ownership.
|
|
# The ASF licenses this file to you 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>
|
|
|
|
=head1 NAME
|
|
|
|
Mail::SpamAssassin::Plugin::SPF - perform SPF verification tests
|
|
|
|
=head1 SYNOPSIS
|
|
|
|
loadplugin Mail::SpamAssassin::Plugin::SPF
|
|
|
|
=head1 DESCRIPTION
|
|
|
|
This plugin checks a message against Sender Policy Framework (SPF)
|
|
records published by the domain owners in DNS to fight email address
|
|
forgery and make it easier to identify spams.
|
|
|
|
It's recommended to use MTA filter (pypolicyd-spf / spf-engine etc), so this
|
|
plugin can reuse the Received-SPF and/or Authentication-Results header results as is.
|
|
Otherwise throughput could suffer, DNS lookups done by this plugin are not
|
|
asynchronous.
|
|
Those headers will also help when SpamAssassin is not able to correctly detect EnvelopeFrom.
|
|
|
|
=cut
|
|
|
|
package Mail::SpamAssassin::Plugin::SPF;
|
|
|
|
use Mail::SpamAssassin::Plugin;
|
|
use Mail::SpamAssassin::Logger;
|
|
use Mail::SpamAssassin::Timeout;
|
|
use strict;
|
|
use warnings;
|
|
# use bytes;
|
|
use re 'taint';
|
|
|
|
our @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);
|
|
|
|
$self->register_eval_rule ("check_for_spf_pass", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_neutral", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_none", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_fail", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_softfail", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_permerror", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_temperror", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_helo_pass", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_helo_neutral", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_helo_none", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_helo_fail", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_helo_softfail", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_helo_permerror", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_helo_temperror", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_welcomelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_spf_whitelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); # removed in 4.1
|
|
$self->register_eval_rule ("check_for_def_spf_welcomelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
$self->register_eval_rule ("check_for_def_spf_whitelist_from", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS); # removed in 4.1
|
|
$self->register_eval_rule ("check_spf_skipped_noenvfrom", $Mail::SpamAssassin::Conf::TYPE_HEAD_EVALS);
|
|
|
|
$self->set_config($mailsaobject->{conf});
|
|
|
|
return $self;
|
|
}
|
|
|
|
###########################################################################
|
|
|
|
sub set_config {
|
|
my($self, $conf) = @_;
|
|
my @cmds;
|
|
|
|
=head1 USER SETTINGS
|
|
|
|
=over 4
|
|
|
|
=item welcomelist_from_spf user@example.com
|
|
|
|
Previously whitelist_from_spf which will work interchangeably until 4.1.
|
|
|
|
Works similarly to welcomelist_from, except that in addition to matching a
|
|
sender address, a check against the domain's SPF record must pass. The
|
|
first parameter is an address to welcomelist, and the second is a string to
|
|
match the relay's rDNS.
|
|
|
|
Just like welcomelist_from, multiple addresses per line, separated by
|
|
spaces, are OK. Multiple C<welcomelist_from_spf> lines are also OK.
|
|
|
|
The headers checked for welcomelist_from_spf addresses are the same headers
|
|
used for SPF checks (Envelope-From, Return-Path, X-Envelope-From, etc).
|
|
|
|
Since this welcomelist requires an SPF check to be made, network tests must be
|
|
enabled. It is also required that your trust path be correctly configured.
|
|
See the section on C<trusted_networks> for more info on trust paths.
|
|
|
|
e.g.
|
|
|
|
welcomelist_from_spf joe@example.com fred@example.com
|
|
welcomelist_from_spf *@example.com
|
|
|
|
=item def_welcomelist_from_spf user@example.com
|
|
|
|
Previously def_whitelist_from_spf which will work interchangeably until 4.1.
|
|
|
|
Same as C<welcomelist_from_spf>, but used for the default welcomelist entries
|
|
in the SpamAssassin distribution. The welcomelist score is lower, because
|
|
these are often targets for spammer spoofing.
|
|
|
|
=item unwelcomelist_from_spf user@example.com
|
|
|
|
Previously unwhitelist_from_spf which will work interchangeably until 4.1.
|
|
|
|
Used to remove a C<welcomelist_from_spf> or C<def_welcomelist_from_spf> entry.
|
|
The specified email address has to match exactly the address previously used.
|
|
|
|
Useful for removing undesired default entries from a distributed configuration
|
|
by a local or site-specific configuration or by C<user_prefs>.
|
|
|
|
=cut
|
|
|
|
push (@cmds, {
|
|
setting => 'welcomelist_from_spf',
|
|
aliases => ['whitelist_from_spf'], # removed in 4.1
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
|
|
});
|
|
|
|
push (@cmds, {
|
|
setting => 'def_welcomelist_from_spf',
|
|
aliases => ['def_whitelist_from_spf'], # removed in 4.1
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
|
|
});
|
|
|
|
push (@cmds, {
|
|
setting => 'unwelcomelist_from_spf',
|
|
aliases => ['unwhitelist_from_spf'], # removed in 4.1
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST,
|
|
code => sub {
|
|
my ($self, $key, $value, $line) = @_;
|
|
unless (defined $value && $value !~ /^$/) {
|
|
return $Mail::SpamAssassin::Conf::MISSING_REQUIRED_VALUE;
|
|
}
|
|
unless ($value =~ /^(?:\S+(?:\s+\S+)*)$/) {
|
|
return $Mail::SpamAssassin::Conf::INVALID_VALUE;
|
|
}
|
|
$self->{parser}->remove_from_addrlist('welcomelist_from_spf',
|
|
split (/\s+/, $value));
|
|
$self->{parser}->remove_from_addrlist('def_welcomelist_from_spf',
|
|
split (/\s+/, $value));
|
|
}
|
|
});
|
|
|
|
=back
|
|
|
|
=head1 ADMINISTRATOR SETTINGS
|
|
|
|
=over 4
|
|
|
|
=item spf_timeout n (default: 5)
|
|
|
|
How many seconds to wait for an SPF query to complete, before scanning
|
|
continues without the SPF result. A numeric value is optionally suffixed
|
|
by a time unit (s, m, h, d, w, indicating seconds (default), minutes, hours,
|
|
days, weeks).
|
|
|
|
=cut
|
|
|
|
push (@cmds, {
|
|
setting => 'spf_timeout',
|
|
is_admin => 1,
|
|
default => 5,
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
|
|
});
|
|
|
|
=item ignore_received_spf_header (0|1) (default: 0)
|
|
|
|
By default, to avoid unnecessary DNS lookups, the plugin will try to use the
|
|
SPF results found in any C<Received-SPF> headers it finds in the message that
|
|
could only have been added by an internal relay.
|
|
|
|
Set this option to 1 to ignore any C<Received-SPF> headers present and to have
|
|
the plugin perform the SPF check itself.
|
|
|
|
Note that unless the plugin finds an C<identity=helo>, or some unsupported
|
|
identity, it will assume that the result is a mfrom SPF check result. The
|
|
only identities supported are C<mfrom>, C<mailfrom> and C<helo>.
|
|
|
|
=cut
|
|
|
|
push(@cmds, {
|
|
setting => 'ignore_received_spf_header',
|
|
is_admin => 1,
|
|
default => 0,
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
|
|
});
|
|
|
|
=item use_newest_received_spf_header (0|1) (default: 0)
|
|
|
|
By default, when using C<Received-SPF> headers, the plugin will attempt to use
|
|
the oldest (bottom most) C<Received-SPF> headers, that were added by internal
|
|
relays, that it can parse results from since they are the most likely to be
|
|
accurate. This is done so that if you have an incoming mail setup where one
|
|
of your primary MXes doesn't know about a secondary MX (or your MXes don't
|
|
know about some sort of forwarding relay that SA considers trusted+internal)
|
|
but SA is aware of the actual domain boundary (internal_networks setting) SA
|
|
will use the results that are most accurate.
|
|
|
|
Use this option to start with the newest (top most) C<Received-SPF> headers,
|
|
working downwards until results are successfully parsed.
|
|
|
|
=cut
|
|
|
|
push(@cmds, {
|
|
setting => 'use_newest_received_spf_header',
|
|
is_admin => 1,
|
|
default => 0,
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
|
|
});
|
|
|
|
# Deprecated since 4.0.0, leave for backwards compatibility
|
|
push(@cmds, {
|
|
setting => 'do_not_use_mail_spf',
|
|
is_admin => 1,
|
|
default => 0,
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
|
|
});
|
|
push(@cmds, {
|
|
setting => 'do_not_use_mail_spf_query',
|
|
is_admin => 1,
|
|
default => 1,
|
|
type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
|
|
});
|
|
|
|
$conf->{parser}->register_commands(\@cmds);
|
|
}
|
|
|
|
=item has_check_for_spf_errors
|
|
|
|
Adds capability check for "if can()" for check_for_spf_permerror, check_for_spf_temperror, check_for_spf_helo_permerror and check_for_spf_helo_permerror
|
|
|
|
=cut
|
|
|
|
sub has_check_for_spf_errors { 1 }
|
|
|
|
=item has_check_spf_skipped_noenvfrom
|
|
|
|
Adds capability check for "if can()" for check_spf_skipped_noenvfrom
|
|
|
|
=cut
|
|
|
|
sub has_check_spf_skipped_noenvfrom { 1 }
|
|
|
|
sub parsed_metadata {
|
|
my ($self, $opts) = @_;
|
|
|
|
$self->_get_sender($opts->{permsgstatus});
|
|
|
|
return 1;
|
|
}
|
|
|
|
# SPF support
|
|
sub check_for_spf_pass {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
|
|
return $scanner->{spf_pass} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_neutral {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
|
|
return $scanner->{spf_neutral} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_none {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
|
|
return $scanner->{spf_none} ? 1 : 0;
|
|
}
|
|
|
|
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});
|
|
}
|
|
return $scanner->{spf_fail} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_softfail {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
|
|
return $scanner->{spf_softfail} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_permerror {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
|
|
return $scanner->{spf_permerror} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_temperror {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
|
|
return $scanner->{spf_temperror} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_helo_pass {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
|
|
return $scanner->{spf_helo_pass} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_helo_neutral {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
|
|
return $scanner->{spf_helo_neutral} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_helo_none {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
|
|
return $scanner->{spf_helo_none} ? 1 : 0;
|
|
}
|
|
|
|
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});
|
|
}
|
|
return $scanner->{spf_helo_fail} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_helo_softfail {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
|
|
return $scanner->{spf_helo_softfail} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_helo_permerror {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
|
|
return $scanner->{spf_helo_permerror} ? 1 : 0;
|
|
}
|
|
|
|
sub check_for_spf_helo_temperror {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
|
|
return $scanner->{spf_helo_temperror} ? 1 : 0;
|
|
}
|
|
|
|
=over 4
|
|
|
|
=item check_spf_skipped_noenvfrom
|
|
|
|
Checks if SPF checks have been skipped because EnvelopeFrom cannot be determined.
|
|
|
|
=back
|
|
|
|
=cut
|
|
|
|
sub check_spf_skipped_noenvfrom {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
|
|
if (!exists $scanner->{spf_sender}) {
|
|
return 1;
|
|
} else {
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
sub check_for_spf_welcomelist_from {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_spf_welcomelist($scanner) unless $scanner->{spf_welcomelist_from_checked};
|
|
return $scanner->{spf_welcomelist_from} ? 1 : 0;
|
|
}
|
|
*check_for_spf_whitelist_from = \&check_for_spf_welcomelist_from; # removed in 4.1
|
|
|
|
sub check_for_def_spf_welcomelist_from {
|
|
my ($self, $scanner) = @_;
|
|
$self->_check_def_spf_welcomelist($scanner) unless $scanner->{def_spf_welcomelist_from_checked};
|
|
return $scanner->{def_spf_welcomelist_from} ? 1 : 0;
|
|
}
|
|
*check_for_def_spf_whitelist_from = \&check_for_def_spf_welcomelist_from; # removed in 4.1
|
|
|
|
sub _check_spf {
|
|
my ($self, $scanner, $ishelo) = @_;
|
|
|
|
my $timer = $self->{main}->time_method("check_spf");
|
|
|
|
# we can re-use results from any *INTERNAL* Received-SPF header in the message...
|
|
# we can't use results from trusted but external hosts since (i) spf checks are
|
|
# supposed to be done "on the domain boundary", (ii) even if an external header
|
|
# has a result that matches what we would get, the check was probably done on a
|
|
# different envelope (like the apache.org list servers checking the ORCPT and
|
|
# then using a new envelope to send the mail from the list) and (iii) if the
|
|
# checks are being done right and the envelope isn't being changed it's 99%
|
|
# likely that the trusted+external host really should be defined as part of your
|
|
# internal network
|
|
if ($scanner->{conf}->{ignore_received_spf_header}) {
|
|
dbg("spf: ignoring any Received-SPF headers from internal hosts, by admin setting");
|
|
} elsif ($scanner->{checked_for_received_spf_header}) {
|
|
dbg("spf: already checked for Received-SPF headers, proceeding with DNS based checks");
|
|
} else {
|
|
$scanner->{checked_for_received_spf_header} = 1;
|
|
dbg("spf: checking to see if the message has a Received-SPF header that we can use");
|
|
|
|
my @internal_hdrs = $scanner->get('ALL-INTERNAL');
|
|
unless ($scanner->{conf}->{use_newest_received_spf_header}) {
|
|
# look for the LAST (earliest in time) header, it'll be the most accurate
|
|
@internal_hdrs = reverse(@internal_hdrs);
|
|
} else {
|
|
dbg("spf: starting with the newest Received-SPF headers first");
|
|
}
|
|
|
|
foreach my $hdr (@internal_hdrs) {
|
|
local($1,$2);
|
|
if ($hdr =~ /^received-spf:/i) {
|
|
dbg("spf: found a Received-SPF header added by an internal host: $hdr");
|
|
|
|
# old version:
|
|
# Received-SPF: pass (herse.apache.org: domain of spamassassin@dostech.ca
|
|
# designates 69.61.78.188 as permitted sender)
|
|
|
|
# new version:
|
|
# Received-SPF: pass (dostech.ca: 69.61.78.188 is authorized to use
|
|
# 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched))
|
|
# receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca";
|
|
# helo=smtp.dostech.net; client-ip=69.61.78.188
|
|
|
|
# Received-SPF: pass (dostech.ca: 69.61.78.188 is authorized to use 'dostech.ca'
|
|
# in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=helo;
|
|
# helo=dostech.ca; client-ip=69.61.78.188
|
|
|
|
# http://www.openspf.org/RFC_4408#header-field
|
|
# wtf - for some reason something is sticking an extra space between the header name and field value
|
|
if ($hdr =~ /^received-spf:\s*(pass|neutral|(?:soft)?fail|(?:temp|perm)error|none)\b(?:.*\bidentity=(\S+?);?\b)?/i) {
|
|
my $result = lc($1);
|
|
|
|
my $identity = ''; # we assume it's a mfrom check if we can't tell otherwise
|
|
if (defined $2) {
|
|
$identity = lc($2);
|
|
if ($identity eq 'mfrom' || $identity eq 'mailfrom') {
|
|
next if $scanner->{spf_checked};
|
|
$identity = '';
|
|
} elsif ($identity eq 'helo') {
|
|
next if $scanner->{spf_helo_checked};
|
|
$identity = 'helo_';
|
|
} else {
|
|
dbg("spf: found unknown identity value, cannot use: $identity");
|
|
next; # try the next Received-SPF header, if any
|
|
}
|
|
} else {
|
|
next if $scanner->{spf_checked};
|
|
}
|
|
|
|
# we'd set these if we actually did the check
|
|
$scanner->{"spf_${identity}checked"} = 1;
|
|
$scanner->{"spf_${identity}pass"} = 0;
|
|
$scanner->{"spf_${identity}neutral"} = 0;
|
|
$scanner->{"spf_${identity}none"} = 0;
|
|
$scanner->{"spf_${identity}fail"} = 0;
|
|
$scanner->{"spf_${identity}softfail"} = 0;
|
|
$scanner->{"spf_${identity}temperror"} = 0;
|
|
$scanner->{"spf_${identity}permerror"} = 0;
|
|
$scanner->{"spf_${identity}failure_comment"} = undef;
|
|
|
|
# and the result
|
|
$scanner->{"spf_${identity}${result}"} = 1;
|
|
dbg("spf: re-using %s result from Received-SPF header: %s",
|
|
($identity ? 'helo' : 'mfrom'), $result);
|
|
|
|
# if we've got *both* the mfrom and helo results we're done
|
|
return if ($scanner->{spf_checked} && $scanner->{spf_helo_checked});
|
|
|
|
} else {
|
|
dbg("spf: could not parse result from existing Received-SPF header");
|
|
}
|
|
|
|
} elsif ($hdr =~ /^(?:Arc\-)?Authentication-Results:.*;\s*SPF\s*=\s*([^;]*)/i) {
|
|
dbg("spf: found an Authentication-Results header added by an internal host: $hdr");
|
|
|
|
# RFC 5451 header parser - added by D. Stussy 2010-09-09:
|
|
# Authentication-Results: mail.example.com; SPF=none smtp.mailfrom=example.org (comment)
|
|
|
|
my $tmphdr = $1;
|
|
if ($tmphdr =~ /^(pass|neutral|(?:hard|soft)?fail|(?:temp|perm)error|none)(?:[^;]*?\bsmtp\.(\S+)\s*=[^;]+)?/i) {
|
|
my $result = lc($1);
|
|
$result = 'fail' if $result eq 'hardfail'; # RFC5451 permits this
|
|
|
|
my $identity = ''; # we assume it's a mfrom check if we can't tell otherwise
|
|
if (defined $2) {
|
|
$identity = lc($2);
|
|
if ($identity eq 'mfrom' || $identity eq 'mailfrom') {
|
|
next if $scanner->{spf_checked};
|
|
$identity = '';
|
|
} elsif ($identity eq 'helo') {
|
|
next if $scanner->{spf_helo_checked};
|
|
$identity = 'helo_';
|
|
} else {
|
|
dbg("spf: found unknown identity value, cannot use: $identity");
|
|
next; # try the next Authentication-Results header, if any
|
|
}
|
|
} else {
|
|
next if $scanner->{spf_checked};
|
|
}
|
|
|
|
# we'd set these if we actually did the check
|
|
$scanner->{"spf_${identity}checked"} = 1;
|
|
$scanner->{"spf_${identity}pass"} = 0;
|
|
$scanner->{"spf_${identity}neutral"} = 0;
|
|
$scanner->{"spf_${identity}none"} = 0;
|
|
$scanner->{"spf_${identity}fail"} = 0;
|
|
$scanner->{"spf_${identity}softfail"} = 0;
|
|
$scanner->{"spf_${identity}temperror"} = 0;
|
|
$scanner->{"spf_${identity}permerror"} = 0;
|
|
$scanner->{"spf_${identity}failure_comment"} = undef;
|
|
|
|
# and the result
|
|
$scanner->{"spf_${identity}${result}"} = 1;
|
|
dbg("spf: re-using %s result from Authentication-Results header: %s",
|
|
($identity ? 'helo' : 'mfrom'), $result);
|
|
|
|
# if we've got *both* the mfrom and helo results we're done
|
|
return if ($scanner->{spf_checked} && $scanner->{spf_helo_checked});
|
|
|
|
} else {
|
|
dbg("spf: could not parse result from existing Authentication-Results header");
|
|
}
|
|
}
|
|
}
|
|
# we can return if we've found the one we're being asked to get
|
|
return if ( ($ishelo && $scanner->{spf_helo_checked}) ||
|
|
(!$ishelo && $scanner->{spf_checked}) );
|
|
}
|
|
|
|
# abort if dns or an spf module isn't available
|
|
return unless $scanner->is_dns_available();
|
|
return if $self->{no_spf_module};
|
|
|
|
# select the SPF module we're going to use
|
|
unless (defined $self->{has_mail_spf}) {
|
|
my $eval_stat;
|
|
eval {
|
|
require Mail::SPF;
|
|
if (!defined $Mail::SPF::VERSION || $Mail::SPF::VERSION < 2.001) {
|
|
die "Mail::SPF 2.001 or later required, this is ".
|
|
(defined $Mail::SPF::VERSION ? $Mail::SPF::VERSION : 'unknown')."\n";
|
|
}
|
|
# Mail::SPF::Server can be re-used, and we get to use our own resolver object!
|
|
$self->{spf_server} = Mail::SPF::Server->new(
|
|
hostname => $scanner->get_tag('HOSTNAME'),
|
|
dns_resolver => $self->{main}->{resolver},
|
|
max_dns_interactive_terms => 20);
|
|
# Bug 7112: max_dns_interactive_terms defaults to 10, but even 14 is
|
|
# not enough for ebay.com, setting it to 15 NOTE: raising to 20 per bug 7182
|
|
1;
|
|
} or do {
|
|
$eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
|
|
};
|
|
|
|
if (!defined($eval_stat)) {
|
|
dbg("spf: using Mail::SPF for SPF checks");
|
|
$self->{has_mail_spf} = 1;
|
|
} else {
|
|
dbg("spf: cannot load Mail::SPF: module: $eval_stat");
|
|
dbg("spf: Mail::SPF is required for SPF checks, SPF checks disabled");
|
|
$self->{no_spf_module} = 1;
|
|
return;
|
|
}
|
|
}
|
|
|
|
# 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)
|
|
# TODO: this will only work if the queries are ready before SPF, so never?
|
|
return if $scanner->{sender_host_fail} && $scanner->{sender_host_fail} == 2;
|
|
|
|
if ($ishelo) {
|
|
# SPF HELO-checking variant
|
|
$scanner->{spf_helo_checked} = 1;
|
|
$scanner->{spf_helo_pass} = 0;
|
|
$scanner->{spf_helo_neutral} = 0;
|
|
$scanner->{spf_helo_none} = 0;
|
|
$scanner->{spf_helo_fail} = 0;
|
|
$scanner->{spf_helo_softfail} = 0;
|
|
$scanner->{spf_helo_permerror} = 0;
|
|
$scanner->{spf_helo_temperror} = 0;
|
|
$scanner->{spf_helo_failure_comment} = undef;
|
|
} else {
|
|
# SPF on envelope sender (where possible)
|
|
$scanner->{spf_checked} = 1;
|
|
$scanner->{spf_pass} = 0;
|
|
$scanner->{spf_neutral} = 0;
|
|
$scanner->{spf_none} = 0;
|
|
$scanner->{spf_fail} = 0;
|
|
$scanner->{spf_softfail} = 0;
|
|
$scanner->{spf_permerror} = 0;
|
|
$scanner->{spf_temperror} = 0;
|
|
$scanner->{spf_failure_comment} = undef;
|
|
}
|
|
|
|
my $lasthop = $scanner->{relays_external}->[0];
|
|
if (!defined $lasthop) {
|
|
dbg("spf: no suitable relay for spf use found, skipping SPF%s check",
|
|
$ishelo ? '-helo' : '');
|
|
return;
|
|
}
|
|
|
|
my $ip = $lasthop->{ip}; # always present
|
|
my $helo = $lasthop->{helo}; # could be missing
|
|
|
|
if ($ishelo) {
|
|
unless ($helo) {
|
|
dbg("spf: cannot check HELO, HELO value unknown");
|
|
return;
|
|
}
|
|
dbg("spf: checking HELO (helo=$helo, ip=$ip)");
|
|
} else {
|
|
# TODO: we're supposed to use the helo domain as the sender identity (for
|
|
# mfrom checks) if the sender is the null sender, however determining that
|
|
# it's the null sender, and not just a failure to get the envelope isn't
|
|
# exactly trivial... so for now we'll just skip the check
|
|
|
|
if (!$scanner->{spf_sender}) {
|
|
# we already dbg'd that we couldn't get an Envelope-From and can't do SPF
|
|
return;
|
|
}
|
|
dbg("spf: checking EnvelopeFrom (helo=%s, ip=%s, envfrom=%s)",
|
|
($helo ? $helo : ''), $ip, $scanner->{spf_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 && $scanner->server_failed_to_respond_for_domain($helo)) {
|
|
dbg("spf: we had a previous timeout on '$helo', skipping");
|
|
return;
|
|
}
|
|
|
|
|
|
my ($result, $comment, $text, $err);
|
|
|
|
# TODO: currently we won't get to here for a mfrom check with a null sender
|
|
my $identity = $ishelo ? $helo : ($scanner->{spf_sender}); # || $helo);
|
|
|
|
unless ($identity) {
|
|
dbg("spf: cannot determine %s identity, skipping %s SPF check",
|
|
($ishelo ? 'helo' : 'mfrom'), ($ishelo ? 'helo' : 'mfrom') );
|
|
return;
|
|
}
|
|
$helo ||= 'unknown'; # only used for macro expansion in the mfrom explanation
|
|
|
|
my $request;
|
|
eval {
|
|
$request = Mail::SPF::Request->new( scope => $ishelo ? 'helo' : 'mfrom',
|
|
identity => $identity,
|
|
ip_address => $ip,
|
|
helo_identity => $helo );
|
|
1;
|
|
} or do {
|
|
my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
|
|
dbg("spf: cannot create Mail::SPF::Request object: $eval_stat");
|
|
return;
|
|
};
|
|
|
|
my $timeout = $scanner->{conf}->{spf_timeout};
|
|
|
|
my $timer_spf = Mail::SpamAssassin::Timeout->new(
|
|
{ secs => $timeout, deadline => $scanner->{master_deadline} });
|
|
$err = $timer_spf->run_and_catch(sub {
|
|
my $query = $self->{spf_server}->process($request);
|
|
$result = $query->code;
|
|
$comment = $query->authority_explanation if $query->can("authority_explanation");
|
|
$text = $query->text;
|
|
});
|
|
|
|
if ($err) {
|
|
chomp $err;
|
|
warn("spf: lookup failed: $err\n");
|
|
return 0;
|
|
}
|
|
|
|
$result ||= 'timeout'; # bug 5077
|
|
$comment ||= '';
|
|
$comment =~ s/\s+/ /gs; # no newlines please
|
|
$text ||= '';
|
|
$text =~ s/\s+/ /gs; # no newlines please
|
|
|
|
if ($ishelo) {
|
|
if ($result eq 'pass') { $scanner->{spf_helo_pass} = 1; }
|
|
elsif ($result eq 'neutral') { $scanner->{spf_helo_neutral} = 1; }
|
|
elsif ($result eq 'none') { $scanner->{spf_helo_none} = 1; }
|
|
elsif ($result eq 'fail') { $scanner->{spf_helo_fail} = 1; }
|
|
elsif ($result eq 'softfail') { $scanner->{spf_helo_softfail} = 1; }
|
|
elsif ($result eq 'permerror') { $scanner->{spf_helo_permerror} = 1; }
|
|
elsif ($result eq 'temperror') { $scanner->{spf_helo_temperror} = 1; }
|
|
elsif ($result eq 'error') { $scanner->{spf_helo_temperror} = 1; }
|
|
|
|
if ($result eq 'fail') { # RFC 7208 6.2
|
|
$scanner->{spf_helo_failure_comment} = "SPF failed: $comment";
|
|
}
|
|
} else {
|
|
if ($result eq 'pass') { $scanner->{spf_pass} = 1; }
|
|
elsif ($result eq 'neutral') { $scanner->{spf_neutral} = 1; }
|
|
elsif ($result eq 'none') { $scanner->{spf_none} = 1; }
|
|
elsif ($result eq 'fail') { $scanner->{spf_fail} = 1; }
|
|
elsif ($result eq 'softfail') { $scanner->{spf_softfail} = 1; }
|
|
elsif ($result eq 'permerror') { $scanner->{spf_permerror} = 1; }
|
|
elsif ($result eq 'temperror') { $scanner->{spf_temperror} = 1; }
|
|
elsif ($result eq 'error') { $scanner->{spf_temperror} = 1; }
|
|
|
|
if ($result eq 'fail') { # RFC 7208 6.2
|
|
$scanner->{spf_failure_comment} = "SPF failed: $comment";
|
|
}
|
|
}
|
|
|
|
if ($ishelo) {
|
|
dbg("spf: query for $ip/$helo: result: $result, comment: $comment, text: $text");
|
|
} else {
|
|
dbg("spf: query for $scanner->{spf_sender}/$ip/$helo: result: $result, comment: $comment, text: $text");
|
|
}
|
|
}
|
|
|
|
sub _get_sender {
|
|
my ($self, $scanner) = @_;
|
|
|
|
my $relay = $scanner->{relays_external}->[0];
|
|
if (defined $relay) {
|
|
my $sender = $relay->{envfrom};
|
|
if (defined $sender) {
|
|
dbg("spf: found EnvelopeFrom '$sender' in first external Received header");
|
|
$scanner->{spf_sender} = lc $sender;
|
|
} else {
|
|
dbg("spf: EnvelopeFrom not found in first external Received header");
|
|
}
|
|
}
|
|
|
|
if (!exists $scanner->{spf_sender}) {
|
|
# 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 &&
|
|
!$scanner->{conf}->{always_trust_envelope_sender}) {
|
|
dbg("spf: relayed through one or more trusted relays, ".
|
|
"cannot use header-based EnvelopeFrom");
|
|
} else {
|
|
# we can (apparently) use whatever the current EnvelopeFrom 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.
|
|
my $sender = ($scanner->get("EnvelopeFrom:addr"))[0];
|
|
if (defined $sender) {
|
|
dbg("spf: found EnvelopeFrom '$sender' from header");
|
|
$scanner->{spf_sender} = lc $sender;
|
|
} else {
|
|
dbg("spf: EnvelopeFrom header not found");
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!exists $scanner->{spf_sender}) {
|
|
dbg("spf: cannot get EnvelopeFrom, cannot use SPF by DNS");
|
|
}
|
|
}
|
|
|
|
sub _check_spf_welcomelist {
|
|
my ($self, $scanner) = @_;
|
|
|
|
$scanner->{spf_welcomelist_from_checked} = 1;
|
|
$scanner->{spf_welcomelist_from} = 0;
|
|
|
|
# if we've already checked for an SPF PASS and didn't get it don't waste time
|
|
# checking to see if the sender address is in the spf welcomelist
|
|
if ($scanner->{spf_checked} && !$scanner->{spf_pass}) {
|
|
dbg("spf: welcomelist_from_spf: already checked spf and didn't get pass, skipping welcomelist check");
|
|
return;
|
|
}
|
|
|
|
if (!$scanner->{spf_sender}) {
|
|
dbg("spf: spf_welcomelist_from: no EnvelopeFrom available for welcomelist check");
|
|
return;
|
|
}
|
|
|
|
$scanner->{spf_welcomelist_from} =
|
|
$self->_wlcheck($scanner, 'welcomelist_from_spf') ||
|
|
$self->_wlcheck($scanner, 'welcomelist_auth');
|
|
|
|
# if the message doesn't pass SPF validation, it can't pass an SPF welcomelist
|
|
if ($scanner->{spf_welcomelist_from}) {
|
|
if ($self->check_for_spf_pass($scanner)) {
|
|
dbg("spf: welcomelist_from_spf: $scanner->{spf_sender} is in user's WELCOMELIST_FROM_SPF and passed SPF check");
|
|
} else {
|
|
dbg("spf: welcomelist_from_spf: $scanner->{spf_sender} is in user's WELCOMELIST_FROM_SPF but failed SPF check");
|
|
$scanner->{spf_welcomelist_from} = 0;
|
|
}
|
|
} else {
|
|
dbg("spf: welcomelist_from_spf: $scanner->{spf_sender} is not in user's WELCOMELIST_FROM_SPF");
|
|
}
|
|
}
|
|
|
|
sub _check_def_spf_welcomelist {
|
|
my ($self, $scanner) = @_;
|
|
|
|
$scanner->{def_spf_welcomelist_from_checked} = 1;
|
|
$scanner->{def_spf_welcomelist_from} = 0;
|
|
|
|
# if we've already checked for an SPF PASS and didn't get it don't waste time
|
|
# checking to see if the sender address is in the spf welcomelist
|
|
if ($scanner->{spf_checked} && !$scanner->{spf_pass}) {
|
|
dbg("spf: def_spf_welcomelist_from: already checked spf and didn't get pass, skipping welcomelist check");
|
|
return;
|
|
}
|
|
|
|
if (!$scanner->{spf_sender}) {
|
|
dbg("spf: def_spf_welcomelist_from: could not find usable envelope sender");
|
|
return;
|
|
}
|
|
|
|
$scanner->{def_spf_welcomelist_from} =
|
|
$self->_wlcheck($scanner, 'def_welcomelist_from_spf') ||
|
|
$self->_wlcheck($scanner, 'def_welcomelist_auth');
|
|
|
|
# if the message doesn't pass SPF validation, it can't pass an SPF welcomelist
|
|
if ($scanner->{def_spf_welcomelist_from}) {
|
|
if ($self->check_for_spf_pass($scanner)) {
|
|
dbg("spf: def_welcomelist_from_spf: $scanner->{spf_sender} is in DEF_WELCOMELIST_FROM_SPF and passed SPF check");
|
|
} else {
|
|
dbg("spf: def_welcomelist_from_spf: $scanner->{spf_sender} is in DEF_WELCOMELIST_FROM_SPF but failed SPF check");
|
|
$scanner->{def_spf_welcomelist_from} = 0;
|
|
}
|
|
} else {
|
|
dbg("spf: def_welcomelist_from_spf: $scanner->{spf_sender} is not in DEF_WELCOMELIST_FROM_SPF");
|
|
}
|
|
}
|
|
|
|
sub _wlcheck {
|
|
my ($self, $scanner, $param) = @_;
|
|
if (defined ($scanner->{conf}->{$param}->{$scanner->{spf_sender}})) {
|
|
return 1;
|
|
} else {
|
|
foreach my $regexp (values %{$scanner->{conf}->{$param}}) {
|
|
if ($scanner->{spf_sender} =~ $regexp) {
|
|
return 1;
|
|
}
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
###########################################################################
|
|
|
|
1;
|
|
|
|
=back
|
|
|
|
=cut
|