qpsmtpd/plugins/sender_permitted_from
Matt Simerson 4e2ae484a2 Merge pull request #15 from msimerson/early
consolidated chunks of code duplicated 4x into log_and_deny and log_and_...
2013-08-05 12:33:42 -07:00

235 lines
7.5 KiB
Perl

#!perl -w
=head1 NAME
SPF - plugin to implement Sender Permitted From
=head1 SYNOPSIS
Prevents email sender address spoofing by checking the SPF policy of the purported senders domain.
=head1 DESCRIPTION
Sender Policy Framework (SPF) is an e-mail validation system designed to prevent spam by addressing source address spoofing. SPF allows administrators to specify which hosts are allowed to send e-mail from a given domain by creating a specific SPF record in the public DNS. Mail exchangers then use the DNS to check that mail from a given domain is being sent by a host sanctioned by that domain's administrators. -- http://en.wikipedia.org/wiki/Sender_Policy_Framework
The results of a SPF query are stored in a transaction note named 'spfquery';
=head1 CONFIGURATION
In config/plugins, add arguments to the sender_permitted_from line.
sender_permitted_from reject 3
=head2 reject
Set to a value between 1 and 6 to enable the following SPF behaviors:
1 annotate-only, add Received-SPF header, no rejections.
2 defer on DNS failures. Assure there's always a meaningful SPF header.
3 rejected if SPF record says 'fail'
4 stricter reject. Also rejects 'softfail'
5 reject 'neutral'
6 reject if no SPF records, or a syntax error
Most sites should start at level 3. It temporarily defers connections (4xx) that have soft SFP failures and only rejects (5xx) messages when the sending domains policy suggests it.
SPF levels above 4 are for crusaders who don't mind rejecting some valid mail when the sending server administrator hasn't dotted his i's and crossed his t's. May the deities bless theirobsessive little hearts.
=head1 SEE ALSO
http://spf.pobox.com/
http://en.wikipedia.org/wiki/Sender_Policy_Framework
=head1 ACKNOWLDGEMENTS
The reject options are modeled after, and aim to match the functionality of those found in the SPF patch for qmail-smtpd.
=head1 AUTHOR
Matt Simerson - 2002 - increased policy options from 3 to 6
Matt Simerson - 2011 - rewrote using Mail::SPF
Matt Sergeant - 2003 - initial plugin
=cut
use strict;
use warnings;
#use Mail::SPF 2.000; # eval'ed in ->register
use Qpsmtpd::Constants;
sub register {
my ($self, $qp, %args) = @_;
eval "use Mail::SPF";
if ( $@ ) {
warn "skip: plugin disabled, could not find Mail::SPF\n";
$self->log(LOGERROR, "skip: plugin disabled, is Mail::SPF installed?");
return;
};
$self->{_args} = { %args };
if ( $self->{_args}{spf_deny} ) {
$self->{_args}{reject} = 3 if $self->{_args}{spf_deny} == 1;
$self->{_args}{reject} = 4 if $self->{_args}{spf_deny} == 2;
};
if ( ! $self->{_args}{reject} && $self->qp->config('spfbehavior') ) {
$self->{_args}{reject} = $self->qp->config('spfbehavior');
};
}
sub hook_mail {
my ($self, $transaction, $sender, %param) = @_;
return (DECLINED) if $self->is_immune();
if ( ! $self->{_args}{reject} ) {
$self->log( LOGINFO, "skip: disabled in config" );
return (DECLINED);
};
my $format = $sender->format;
if ( $format eq '<>' || ! $sender->host || ! $sender->user ) {
$self->log( LOGINFO, "skip: null sender" );
return (DECLINED, "SPF - null sender");
};
if ( $self->is_in_relayclients() ) {
return (DECLINED, "SPF - relaying permitted");
};
my $client_ip = $self->qp->connection->remote_ip;
my $from = $sender->user . '@' . lc($sender->host);
my $helo = $self->qp->connection->hello_host;
my $scope = $from ? 'mfrom' : 'helo';
my %req_params = ( versions => [1, 2], # optional
scope => $scope,
ip_address => $client_ip,
);
if ($scope =~ /^mfrom|pra$/) {
$req_params{identity} = $from;
$req_params{helo_identity} = $helo if $helo;
}
elsif ($scope eq 'helo') {
$req_params{identity} = $helo;
$req_params{helo_identity} = $helo;
}
my $spf_server = Mail::SPF::Server->new();
my $request = Mail::SPF::Request->new(%req_params);
my $result = $spf_server->process($request);
$transaction->notes('spfquery', $result);
$self->log( LOGINFO, $result );
if ( $result->code eq 'pass' ) {
return (OK);
};
return (DECLINED, "SPF - $result->code");
}
sub hook_rcpt {
my ($self, $transaction, $rcpt, %param) = @_;
return DECLINED if $self->is_special_recipient( $rcpt );
my $result = $transaction->notes('spfquery') or return DECLINED;
my $code = $result->code;
my $why = $result->local_explanation;
my $reject = $self->{_args}{reject};
if ( ! $code ) {
return (DENYSOFT, "SPF - no response") if $reject >= 2;
return (DECLINED, "SPF - no response");
};
return (DECLINED, "SPF - $code: $why") if ! $reject;
# SPF result codes: pass fail softfail neutral none error permerror temperror
if ( $code eq 'pass' ) { }
elsif ( $code eq 'fail' ) {
return (DENY, "SPF - forgery: $why") if $reject >= 3;
return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
}
elsif ( $code eq 'softfail' ) {
return (DENY, "SPF - forgery: $why") if $reject >= 4;
return (DENYSOFT, "SPF - $code: $why") if $reject >= 3;
}
elsif ( $code eq 'neutral' ) {
return (DENY, "SPF - forgery: $why") if $reject >= 5;
}
elsif ( $code eq 'none' ) {
return (DENY, "SPF - forgery: $why") if $reject >= 6;
}
elsif ( $code eq 'error' ) {
return (DENY, "SPF - $code: $why") if $reject >= 6;
return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
}
elsif ( $code eq 'permerror' ) {
return (DENY, "SPF - $code: $why") if $reject >= 6;
return (DENYSOFT, "SPF - $code: $why") if $reject >= 3;
}
elsif ( $code eq 'temperror' ) {
return (DENYSOFT, "SPF - $code: $why") if $reject >= 2;
}
$self->log(LOGDEBUG, "result for $rcpt->address was $code: $why");
return (DECLINED, "SPF - $code: $why");
}
sub hook_data_post {
my ($self, $transaction) = @_;
my $result = $transaction->notes('spfquery') or return DECLINED;
$self->log(LOGDEBUG, "result was $result->code");
$transaction->header->add('Received-SPF' => $result->received_spf_header, 0);
return DECLINED;
}
sub is_in_relayclients {
my $self = shift;
# If we are receiving from a relay permitted host, then we are probably
# not the delivery system, and so we shouldn't check
my $client_ip = $self->qp->connection->remote_ip;
my @relay_clients = $self->qp->config('relayclients');
my $more_relay_clients = $self->qp->config('morerelayclients', 'map');
my %relay_clients = map { $_ => 1 } @relay_clients;
while ($client_ip) {
if ( exists $relay_clients{$client_ip} ||
exists $more_relay_clients->{$client_ip} ) {
$self->log( LOGDEBUG, "skip: relaying permitted (config)" );
return 1;
};
$client_ip =~ s/\d+\.?$// or last; # strip off another 8 bits
}
return;
};
sub is_special_recipient {
my ($self, $rcpt) = @_;
if ( ! $rcpt ) {
$self->log(LOGINFO, "skip: missing recipient");
return 1;
};
if ( ! $rcpt->user ) {
$self->log(LOGINFO, "skip: missing user");
return 1;
};
# special addresses don't get SPF-tested.
if ( $rcpt->user =~ /^(?:postmaster|abuse|mailer-daemon|root)$/i ) {
$self->log(LOGINFO, "skip: special user (".$rcpt->user.")");
return 1;
};
return;
};