<NME> sender_permitted_from
<BEF> #!perl -w

=head1 NAME

SPF - implement Sender Permitted From

=head1 SYNOPSIS

Prevents email sender address spoofing by checking the SPF policy of the purported senders domain.

Sets the transaction note spf_pass_host if the SPF result is pass.

=head1 DESCRIPTION

Sender Policy Framework (SPF) is an email validation system designed to prevent source address spoofing. SPF allows administrators to specify which hosts are allowed to send email from a given domain by creating a specific SPF record in the public DNS. Mail exchangers then use the DNS to verify that mail is being sent by a host sanctioned by a given domain 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 their obsessive little hearts.

=head2 no_dmarc_policy

When used with the dmarc plugin, you don't want sender_permitted_from to reject anything, because dmarc needs to check the sender's domain policy.
So you'll most likely have reject 1.
But then, if the sender's domain has no dmarc policy, you might want to reject solely based on SPF result. This is what this setting is for. A first hook runs at the mail stage and evaluate SPF. Then a second hook runs at the data_post stage (after dmarc), so you have a second chance to reject.

Like reject, you can set a value to indicate how agressive you want to be:

  0 do not reject (default)
  1 reject if SPF record says 'fail'
  2 stricter reject. Also rejects 'softfail'
  3 reject 'neutral'
  4 reject if no SPF records, or a syntax error

Just like reject, the recommanded value is 1. 2 will be a bit more agressive. 3 and 4 will most likely reject some valid emails.

So, for example, you can use something like this:

sender_permetted_from reject 1 no_dmarc_policy 1
dkim reject 0
dmarc reject 1 reporting 1

Note this setting will only have effect if:

  * dmarc plugin is used, and loaded after sender_permetted_from in your plugin's config
  * the reject value is either 1 or 2 (meaning, no reject at the mail stage)
  * dmarc ran with no error
  * the sender's domain has no dmarc policy published (that means, no _dmarc DNS entry)

Note that if a domain has a dmarc "p=none" policy, then this setting has no effect. Only if there's no dmarc policy at all it'll be used.

=head1 SEE ALSO

 http://spf.pobox.com/
 http://en.wikipedia.org/wiki/Sender_Policy_Framework

=head1 TODO

Check the scope of the SPF policy. If it's too broad (ie, the whole internet is valid), apply karma penalty
Examples of too broad: +all,

=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 - 2013 - populate dmarc_spf note with SPF results
 Matt Simerson - 2012 - increased policy options from 3 to 6
 Matt Simerson - 2011 - rewrote using Mail::SPF
 Matt Sergeant - 2003 - initial plugin

=cut

use strict;
use warnings;
        return (DECLINED, "SPF - null sender");
    }

    if ($self->qp->connection->relay_client) {
        $self->log(LOGINFO, "skip, relay_client");
        return (DECLINED, "SPF - relaying permitted");
    }

    if (!$self->{_args}{reject}) {
        $self->log(LOGINFO, "skip, reject disabled");
        return (DECLINED);
    }

    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') {
    if (!$self->{_args}{reject} && $self->qp->config('spfbehavior')) {
        $self->{_args}{reject} = $self->qp->config('spfbehavior');
    }
    $self->{_args}{no_dmarc_policy} ||= 0;
    $self->register_hook('mail', 'evaluate_spf');
    $self->register_hook('data_post_headers', 'add_spf_header');
    $self->register_hook('data_post', 'no_dmarc_policy') if $self->{_args}{no_dmarc_policy} > 0;

}

sub evaluate_spf {
    my ($self, $transaction, $sender, %param) = @_;

    if ( $self->is_immune() ) {
        $transaction->notes('dmarc_spf', {
                domain => $sender->host,
                scope  => 'mfrom',
                result => 'pass',
                } );
        return DECLINED;
        return (DECLINED, "SPF - no response");
    }

    if (!$reject) {
        $self->log(LOGINFO, "fail, no reject policy ($code: $why)");
        return (DECLINED, "SPF - $code: $why");
    }

   # SPF result codes: pass fail softfail neutral none error permerror temperror
    return $self->handle_code_none($reject, $why) if $code eq 'none';
    if ($code eq 'fail') {
        $self->adjust_karma(-1);
        return $self->handle_code_fail($reject, $why);
    }
    elsif ($code eq 'softfail') {
        $self->adjust_karma(-1);
        return $self->handle_code_softfail($reject, $why);
    }
    elsif ($code eq 'pass') {
        $self->adjust_karma(1);
        $transaction->notes('spf_pass_host', lc $sender->host);
        $self->log(LOGINFO, "pass, $code: $why");
        return (DECLINED);
    }
    elsif ($code eq 'neutral') {
        $self->log(LOGINFO, "fail, $code, $why");
        return (DENY, "SPF - $code: $why") if $reject >= 5;
    }
    }
    elsif ($scope eq 'helo') {
        $req_params{identity}      = $helo;
        $req_params{helo_identity} = $helo;
    }

    $transaction->notes('dmarc_spf', {
            domain => $scope eq 'helo' ? $helo : $sender->host,
            scope  => $scope,
            result => 'none',
            } );

    my $spf_server = Mail::SPF::Server->new();
    my $request    = Mail::SPF::Request->new(%req_params);
    my $result     = $spf_server->process($request) or do {
        $self->log(LOGINFO, "fail, no result");
        return DECLINED;
    };

    $transaction->notes('spfquery', $result);

    my $code   = $result->code;
    my $why    = $result->local_explanation;
    my $reject = $self->{_args}{reject};
        return (DENY, "SPF - none: $why");
    }

    $self->log(LOGINFO, "pass, none, $why");
    return DECLINED;
}

sub handle_code_fail {
    my ($self, $reject, $why) = @_;

    if ($reject >= 2) {
        $self->log(LOGINFO, "fail, $why");
        return (DENY, "SPF - forgery: $why") if $reject >= 3;
        return (DENYSOFT, "SPF - fail: $why");
    }

    $self->log(LOGINFO, "pass, fail tolerated, $why");
    return DECLINED;
}

sub handle_code_softfail {
    my ($self, $reject, $why) = @_;

    if ($reject >= 3) {
        $self->log(LOGINFO, "fail, soft, $why");
        return (DENY,     "SPF - fail: $why") if $reject >= 4;
        return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;
    }

    $self->log(LOGINFO, "pass, softfail tolerated, $why");
    return DECLINED;
}

        $self->log(LOGINFO, "fail, tolerated, $code, $why");
        return DECLINED;
    }
    if ($code =~ /(?:permerror|error)/ ) {
        $self->log(LOGINFO, "fail, $code, $why") if $reject > 3;
        return DENY,     "SPF - $code: $why" if $reject >= 6;
        return DENYSOFT, "SPF - $code: $why" if $reject > 3;
        $self->log(LOGINFO, "fail, tolerated, $code, $why");
        return DECLINED;
    }
    if ($code eq 'temperror') {
        $self->log(LOGINFO, "fail, $code, $why");
        return DENYSOFT, "SPF - $code: $why" if $reject >= 2;
        $self->log(LOGINFO, "fail, tolerated, $code, $why");
        return DECLINED;
    }

    $self->log(LOGINFO, "SPF from $from was $code: $why");
    return DECLINED;
}

sub no_dmarc_policy {
    my ($self, $transaction) = @_;
    return DECLINED if $self->is_immune;
    unless ($self->{_args}{no_dmarc_policy}){
        return DECLINED;
    }
    if ($transaction->notes('spfquery') && $transaction->notes('dmarc_result')){
        my $spf_result  = $transaction->notes('spfquery')->code;
        my $why         = $transaction->notes('spfquery')->local_explanation;
        my $dmarc_dispo = $transaction->notes('dmarc_result')->disposition;
        return DECLINED unless $dmarc_dispo eq 'none';
        my $comment = '';
        if ($transaction->notes('dmarc_result')->reason &&
            $transaction->notes('dmarc_result')->reason->[0] &&
            $transaction->notes('dmarc_result')->reason->[0]->comment){
            $comment = $transaction->notes('dmarc_result')->reason->[0]->comment;
        }
        return DECLINED unless $comment eq 'no policy';
        # No SPF or syntaxe error: reject if no_dmarc_policy is at least 4
        if ((!$spf_result || $spf_result =~ m/(?:permerror|error|none)/) && $self->{_args}{no_dmarc_policy} >= 4){
            $self->log(LOGINFO, "fail, $spf_result, $why");
            return DENY, "SPF - $spf_result: $why";
        }
        # All other reject levels require an SPF code
        return DECLINED unless $spf_result;
        # Neutral
        if ($spf_result eq 'neutral' && $self->{_args}{no_dmarc_policy} >= 3){
            $self->log(LOGINFO, "fail, $spf_result, $why");
            return DENY, "SPF - $spf_result: $why";
        }
        # Softfail
        if ($spf_result eq 'softfail' && $self->{_args}{no_dmarc_policy} >= 2){
            $self->log(LOGINFO, "fail, $spf_result, $why");
            return DENY, "SPF - $spf_result: $why";
        }
        # Fail
        if ($spf_result eq 'fail' && $self->{_args}{no_dmarc_policy} >= 1){
            $self->log(LOGINFO, "fail, $spf_result, $why");
            return DENY, "SPF - $spf_result: $why";
        }
    }
    $self->log(LOGINFO, 'pass');
    return DECLINED;
}

sub handle_code_none {
    my ($self, $reject, $why) = @_;

    if ($reject >= 6) {
        $self->log(LOGINFO, "fail, none, $why");
        return DENY, "SPF - none: $why";
    }

    $self->log(LOGINFO, "skip, tolerated, none, $why");
    return DECLINED;
}

sub handle_code_fail {
    my ($self, $reject, $why) = @_;

    $self->adjust_karma(-1);

    if ($reject >= 2) {
        $self->log(LOGINFO, "fail, $why");
        return DENY, "SPF - forgery: $why" if $reject >= 3;
        return DENYSOFT, "SPF - fail: $why";
    }

    $self->log(LOGINFO, "fail, tolerated, $why");
    return DECLINED;
}

sub handle_code_softfail {
    my ($self, $reject, $why) = @_;

    $self->adjust_karma(-1);

    if ($reject >= 3) {
        $self->log(LOGINFO, "fail, soft, $why");
        return DENY,     "SPF - fail: $why" if $reject >= 4;
        return DENYSOFT, "SPF - fail: $why" if $reject >= 3;
    }

    $self->log(LOGINFO, "fail, tolerated, soft, $why");
    return DECLINED;
}

sub add_spf_header {
    my ($self, $transaction) = @_;

    my $result = $transaction->notes('spfquery') or return DECLINED;

    # if we skipped processing in evaluate_spf, we should skip here too
    return DECLINED if $self->is_immune();

    $self->log(LOGDEBUG, "result was $result->code");

    if (!$transaction->header) {
        $self->log(LOGERROR, "missing headers!");
        return DECLINED;
    }

    $transaction->header->add('Received-SPF', $result->received_spf_header, 0);

    return DECLINED;
}

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;
}

<MSG> SPF: arrage flow so if a pass result is possible,

we will get it and set the note for DMARC plugin

<DFF> @@ -96,28 +96,17 @@ sub mail_handler {
         return (DECLINED, "SPF - null sender");
     }
 
-    if ($self->qp->connection->relay_client) {
-        $self->log(LOGINFO, "skip, relay_client");
-        return (DECLINED, "SPF - relaying permitted");
-    }
-
-    if (!$self->{_args}{reject}) {
-        $self->log(LOGINFO, "skip, reject disabled");
-        return (DECLINED);
-    }
-
-    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,
+        ip_address => $self->qp->connection->remote_ip,
                      );
 
     if ($scope =~ /^mfrom|pra$/) {
-        $req_params{identity} = $from;
+        $req_params{identity}      = $from;
         $req_params{helo_identity} = $helo if $helo;
     }
     elsif ($scope eq 'helo') {
@@ -144,28 +133,24 @@ sub mail_handler {
         return (DECLINED, "SPF - no response");
     }
 
+    if ($code eq 'pass') {
+        $self->adjust_karma(1);
+        $transaction->notes('spf_pass_host', lc $sender->host);
+        $self->log(LOGINFO, "pass, $code: $why");
+        return (DECLINED);
+    }
+
     if (!$reject) {
-        $self->log(LOGINFO, "fail, no reject policy ($code: $why)");
+        $self->log(LOGINFO, "skip, tolerated ($code: $why)");
         return (DECLINED, "SPF - $code: $why");
     }
 
    # SPF result codes: pass fail softfail neutral none error permerror temperror
     return $self->handle_code_none($reject, $why) if $code eq 'none';
-    if ($code eq 'fail') {
-        $self->adjust_karma(-1);
-        return $self->handle_code_fail($reject, $why);
-    }
-    elsif ($code eq 'softfail') {
-        $self->adjust_karma(-1);
-        return $self->handle_code_softfail($reject, $why);
-    }
-    elsif ($code eq 'pass') {
-        $self->adjust_karma(1);
-        $transaction->notes('spf_pass_host', lc $sender->host);
-        $self->log(LOGINFO, "pass, $code: $why");
-        return (DECLINED);
-    }
-    elsif ($code eq 'neutral') {
+    return $self->handle_code_fail($reject, $why) if $code eq 'fail';
+    return $self->handle_code_softfail($reject, $why) if $code eq 'softfail';
+
+    if ($code eq 'neutral') {
         $self->log(LOGINFO, "fail, $code, $why");
         return (DENY, "SPF - $code: $why") if $reject >= 5;
     }
@@ -196,33 +181,37 @@ sub handle_code_none {
         return (DENY, "SPF - none: $why");
     }
 
-    $self->log(LOGINFO, "pass, none, $why");
+    $self->log(LOGINFO, "skip, tolerated, none, $why");
     return DECLINED;
 }
 
 sub handle_code_fail {
     my ($self, $reject, $why) = @_;
 
+    $self->adjust_karma(-1);
+
     if ($reject >= 2) {
         $self->log(LOGINFO, "fail, $why");
         return (DENY, "SPF - forgery: $why") if $reject >= 3;
         return (DENYSOFT, "SPF - fail: $why");
     }
 
-    $self->log(LOGINFO, "pass, fail tolerated, $why");
+    $self->log(LOGINFO, "fail, tolerated, $why");
     return DECLINED;
 }
 
 sub handle_code_softfail {
     my ($self, $reject, $why) = @_;
 
+    $self->adjust_karma(-1);
+
     if ($reject >= 3) {
         $self->log(LOGINFO, "fail, soft, $why");
         return (DENY,     "SPF - fail: $why") if $reject >= 4;
         return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;
     }
 
-    $self->log(LOGINFO, "pass, softfail tolerated, $why");
+    $self->log(LOGINFO, "fail, soft, tolerated, $why");
     return DECLINED;
 }
 

{'commit': 'diff --git a/plugins/dmarc b/plugins/dmarc\nindex 60db367..6f41234 100644\n--- a/plugins/dmarc\n+++ b/plugins/dmarc\n@@ -296,9 +296,16 @@ sub get_organizational_domain {\n \n         # $self->log( LOGINFO, "i: $i, $tld" );\n         #warn "i: $i -  tld: $tld\\n";\n-        if (grep /$tld/, $self->qp->config(\'public_suffix_list\')) {\n+        if (grep /^$tld/, $self->qp->config(\'public_suffix_list\')) {\n             $greatest = $i + 1;\n+            next;\n         }\n+\n+        # check for wildcards (ex: *.uk should match co.uk)\n+        $tld = join \'.\', \'\\*\', reverse((@labels)[0 .. $i-1]);\n+        if (grep /^$tld/, $self->qp->config(\'public_suffix_list\')) {\n+            $greatest = $i + 1;\n+        };\n     }\n \n     return $from_host if $greatest == scalar @labels;    # same\n@@ -327,7 +334,16 @@ sub exists_in_dns {\n # Since this search gets repeated for the Organizational Name, if it\n # fails for the O.N., there\'s no delegation from the TLD.\n     my $res = $self->init_resolver(8);\n-    my $query = $res->query($domain, \'NS\') or do {\n+    return 1 if $self->host_has_rr(\'NS\', $res, $domain);\n+    return 1 if $self->host_has_rr(\'MX\', $res, $domain);\n+    return 1 if $self->host_has_rr(\'A\',  $res, $domain);\n+    return 1 if $self->host_has_rr(\'AAAA\', $res, $domain);\n+}\n+\n+sub host_has_rr {\n+    my ($self, $type, $res, $domain) = @_;\n+\n+    my $query = $res->query($domain, $type) or do {\n         if ($res->errorstring eq \'NXDOMAIN\') {\n             $self->log(LOGDEBUG, "fail, non-existent domain: $domain");\n             return;\n@@ -337,14 +353,14 @@ sub exists_in_dns {\n     };\n     my $matches = 0;\n     for my $rr ($query->answer) {\n-        next if $rr->type !~ /(?:NS|MX|A|AAAA)/;\n+        next if $rr->type ne $type;\n         $matches++;\n     }\n     if (0 == $matches) {\n-        $self->log(LOGDEBUG, "fail, no records for $domain");\n+        $self->log(LOGDEBUG, "no $type records for $domain");\n     }\n     return $matches;\n-}\n+};\n \n sub fetch_dmarc_record {\n     my ($self, $zone) = @_;\ndiff --git a/plugins/sender_permitted_from b/plugins/sender_permitted_from\nindex e9a1f9e..87d418d 100644\n--- a/plugins/sender_permitted_from\n+++ b/plugins/sender_permitted_from\n@@ -96,28 +96,17 @@ sub mail_handler {\n         return (DECLINED, "SPF - null sender");\n     }\n \n-    if ($self->qp->connection->relay_client) {\n-        $self->log(LOGINFO, "skip, relay_client");\n-        return (DECLINED, "SPF - relaying permitted");\n-    }\n-\n-    if (!$self->{_args}{reject}) {\n-        $self->log(LOGINFO, "skip, reject disabled");\n-        return (DECLINED);\n-    }\n-\n-    my $client_ip = $self->qp->connection->remote_ip;\n     my $from      = $sender->user . \'@\' . lc($sender->host);\n     my $helo      = $self->qp->connection->hello_host;\n     my $scope     = $from ? \'mfrom\' : \'helo\';\n     my %req_params = (\n         versions => [1, 2],    # optional\n         scope => $scope,\n-        ip_address => $client_ip,\n+        ip_address => $self->qp->connection->remote_ip,\n                      );\n \n     if ($scope =~ /^mfrom|pra$/) {\n-        $req_params{identity} = $from;\n+        $req_params{identity}      = $from;\n         $req_params{helo_identity} = $helo if $helo;\n     }\n     elsif ($scope eq \'helo\') {\n@@ -144,28 +133,24 @@ sub mail_handler {\n         return (DECLINED, "SPF - no response");\n     }\n \n+    if ($code eq \'pass\') {\n+        $self->adjust_karma(1);\n+        $transaction->notes(\'spf_pass_host\', lc $sender->host);\n+        $self->log(LOGINFO, "pass, $code: $why");\n+        return (DECLINED);\n+    }\n+\n     if (!$reject) {\n-        $self->log(LOGINFO, "fail, no reject policy ($code: $why)");\n+        $self->log(LOGINFO, "skip, tolerated ($code: $why)");\n         return (DECLINED, "SPF - $code: $why");\n     }\n \n    # SPF result codes: pass fail softfail neutral none error permerror temperror\n     return $self->handle_code_none($reject, $why) if $code eq \'none\';\n-    if ($code eq \'fail\') {\n-        $self->adjust_karma(-1);\n-        return $self->handle_code_fail($reject, $why);\n-    }\n-    elsif ($code eq \'softfail\') {\n-        $self->adjust_karma(-1);\n-        return $self->handle_code_softfail($reject, $why);\n-    }\n-    elsif ($code eq \'pass\') {\n-        $self->adjust_karma(1);\n-        $transaction->notes(\'spf_pass_host\', lc $sender->host);\n-        $self->log(LOGINFO, "pass, $code: $why");\n-        return (DECLINED);\n-    }\n-    elsif ($code eq \'neutral\') {\n+    return $self->handle_code_fail($reject, $why) if $code eq \'fail\';\n+    return $self->handle_code_softfail($reject, $why) if $code eq \'softfail\';\n+\n+    if ($code eq \'neutral\') {\n         $self->log(LOGINFO, "fail, $code, $why");\n         return (DENY, "SPF - $code: $why") if $reject >= 5;\n     }\n@@ -196,33 +181,37 @@ sub handle_code_none {\n         return (DENY, "SPF - none: $why");\n     }\n \n-    $self->log(LOGINFO, "pass, none, $why");\n+    $self->log(LOGINFO, "skip, tolerated, none, $why");\n     return DECLINED;\n }\n \n sub handle_code_fail {\n     my ($self, $reject, $why) = @_;\n \n+    $self->adjust_karma(-1);\n+\n     if ($reject >= 2) {\n         $self->log(LOGINFO, "fail, $why");\n         return (DENY, "SPF - forgery: $why") if $reject >= 3;\n         return (DENYSOFT, "SPF - fail: $why");\n     }\n \n-    $self->log(LOGINFO, "pass, fail tolerated, $why");\n+    $self->log(LOGINFO, "fail, tolerated, $why");\n     return DECLINED;\n }\n \n sub handle_code_softfail {\n     my ($self, $reject, $why) = @_;\n \n+    $self->adjust_karma(-1);\n+\n     if ($reject >= 3) {\n         $self->log(LOGINFO, "fail, soft, $why");\n         return (DENY,     "SPF - fail: $why") if $reject >= 4;\n         return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;\n     }\n \n-    $self->log(LOGINFO, "pass, softfail tolerated, $why");\n+    $self->log(LOGINFO, "fail, soft, tolerated, $why");\n     return DECLINED;\n }\n \n', 'commit_subject': 'SPF: arrage flow so if a pass result is possible,', 'repo_name': 'smtpd/qpsmtpd', 'committer': {'name': 'Matt Simerson', 'email': '1fa2ef4755a9226cb9a0a4840bd89b158ac71391@tnpi.net', 'time_sec': '1366787342', 'tz_offset': '-240', 'date': {'seconds': '1366787342'}}, 'commit_tree': '6077eb5782c8f736baf623c40d46339d94f6e820', 'commit_message': 'SPF: arrage flow so if a pass result is possible,\n\nwe will get it and set the note for DMARC plugin\n', 'language_name': 'Shell', 'license': 'mit', 'hunks': '@@ -96,28 +96,17 @@ sub mail_handler {\n         return (DECLINED, "SPF - null sender");\n     }\n \n-    if ($self->qp->connection->relay_client) {\n-        $self->log(LOGINFO, "skip, relay_client");\n-        return (DECLINED, "SPF - relaying permitted");\n-    }\n-\n-    if (!$self->{_args}{reject}) {\n-        $self->log(LOGINFO, "skip, reject disabled");\n-        return (DECLINED);\n-    }\n-\n-    my $client_ip = $self->qp->connection->remote_ip;\n     my $from      = $sender->user . \'@\' . lc($sender->host);\n     my $helo      = $self->qp->connection->hello_host;\n     my $scope     = $from ? \'mfrom\' : \'helo\';\n     my %req_params = (\n         versions => [1, 2],    # optional\n         scope => $scope,\n-        ip_address => $client_ip,\n+        ip_address => $self->qp->connection->remote_ip,\n                      );\n \n     if ($scope =~ /^mfrom|pra$/) {\n-        $req_params{identity} = $from;\n+        $req_params{identity}      = $from;\n         $req_params{helo_identity} = $helo if $helo;\n     }\n     elsif ($scope eq \'helo\') {\n@@ -144,28 +133,24 @@ sub mail_handler {\n         return (DECLINED, "SPF - no response");\n     }\n \n+    if ($code eq \'pass\') {\n+        $self->adjust_karma(1);\n+        $transaction->notes(\'spf_pass_host\', lc $sender->host);\n+        $self->log(LOGINFO, "pass, $code: $why");\n+        return (DECLINED);\n+    }\n+\n     if (!$reject) {\n-        $self->log(LOGINFO, "fail, no reject policy ($code: $why)");\n+        $self->log(LOGINFO, "skip, tolerated ($code: $why)");\n         return (DECLINED, "SPF - $code: $why");\n     }\n \n    # SPF result codes: pass fail softfail neutral none error permerror temperror\n     return $self->handle_code_none($reject, $why) if $code eq \'none\';\n-    if ($code eq \'fail\') {\n-        $self->adjust_karma(-1);\n-        return $self->handle_code_fail($reject, $why);\n-    }\n-    elsif ($code eq \'softfail\') {\n-        $self->adjust_karma(-1);\n-        return $self->handle_code_softfail($reject, $why);\n-    }\n-    elsif ($code eq \'pass\') {\n-        $self->adjust_karma(1);\n-        $transaction->notes(\'spf_pass_host\', lc $sender->host);\n-        $self->log(LOGINFO, "pass, $code: $why");\n-        return (DECLINED);\n-    }\n-    elsif ($code eq \'neutral\') {\n+    return $self->handle_code_fail($reject, $why) if $code eq \'fail\';\n+    return $self->handle_code_softfail($reject, $why) if $code eq \'softfail\';\n+\n+    if ($code eq \'neutral\') {\n         $self->log(LOGINFO, "fail, $code, $why");\n         return (DENY, "SPF - $code: $why") if $reject >= 5;\n     }\n@@ -196,33 +181,37 @@ sub handle_code_none {\n         return (DENY, "SPF - none: $why");\n     }\n \n-    $self->log(LOGINFO, "pass, none, $why");\n+    $self->log(LOGINFO, "skip, tolerated, none, $why");\n     return DECLINED;\n }\n \n sub handle_code_fail {\n     my ($self, $reject, $why) = @_;\n \n+    $self->adjust_karma(-1);\n+\n     if ($reject >= 2) {\n         $self->log(LOGINFO, "fail, $why");\n         return (DENY, "SPF - forgery: $why") if $reject >= 3;\n         return (DENYSOFT, "SPF - fail: $why");\n     }\n \n-    $self->log(LOGINFO, "pass, fail tolerated, $why");\n+    $self->log(LOGINFO, "fail, tolerated, $why");\n     return DECLINED;\n }\n \n sub handle_code_softfail {\n     my ($self, $reject, $why) = @_;\n \n+    $self->adjust_karma(-1);\n+\n     if ($reject >= 3) {\n         $self->log(LOGINFO, "fail, soft, $why");\n         return (DENY,     "SPF - fail: $why") if $reject >= 4;\n         return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;\n     }\n \n-    $self->log(LOGINFO, "pass, softfail tolerated, $why");\n+    $self->log(LOGINFO, "fail, soft, tolerated, $why");\n     return DECLINED;\n }\n \n', 'addition_count': 21, 'deletion_count': 32, 'src_file': 'a/plugins/sender_permitted_from', 'tgt_file': 'b/plugins/sender_permitted_from', 'file_extension': '', 'before_file': '#!perl -w\n\n=head1 NAME\n\nSPF - implement Sender Permitted From\n\n=head1 SYNOPSIS\n\nPrevents email sender address spoofing by checking the SPF policy of the purported senders domain.\n\nSets the transaction note spf_pass_host if the SPF result is pass.\n\n=head1 DESCRIPTION\n\nSender Policy Framework (SPF) is an email validation system designed to prevent source address spoofing. SPF allows administrators to specify which hosts are allowed to send email from a given domain by creating a specific SPF record in the public DNS. Mail exchangers then use the DNS to verify that mail is being sent by a host sanctioned by a given domain administrators. -- http://en.wikipedia.org/wiki/Sender_Policy_Framework\n\nThe results of a SPF query are stored in a transaction note named \'spfquery\';\n\n=head1 CONFIGURATION\n\nIn config/plugins, add arguments to the sender_permitted_from line.\n\n  sender_permitted_from reject 3\n\n=head2 reject\n\nSet to a value between 1 and 6 to enable the following SPF behaviors:\n\n 1 annotate-only, add Received-SPF header, no rejections.\n 2 defer on DNS failures. Assure there\'s always a meaningful SPF header.\n 3 rejected if SPF record says \'fail\'\n 4 stricter reject. Also rejects \'softfail\'\n 5 reject \'neutral\'\n 6 reject if no SPF records, or a syntax error\n\nMost 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.\n\nSPF 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 their obsessive little hearts.\n\n=head2 no_dmarc_policy\n\nWhen used with the dmarc plugin, you don\'t want sender_permitted_from to reject anything, because dmarc needs to check the sender\'s domain policy.\nSo you\'ll most likely have reject 1.\nBut then, if the sender\'s domain has no dmarc policy, you might want to reject solely based on SPF result. This is what this setting is for. A first hook runs at the mail stage and evaluate SPF. Then a second hook runs at the data_post stage (after dmarc), so you have a second chance to reject.\n\nLike reject, you can set a value to indicate how agressive you want to be:\n\n  0 do not reject (default)\n  1 reject if SPF record says \'fail\'\n  2 stricter reject. Also rejects \'softfail\'\n  3 reject \'neutral\'\n  4 reject if no SPF records, or a syntax error\n\nJust like reject, the recommanded value is 1. 2 will be a bit more agressive. 3 and 4 will most likely reject some valid emails.\n\nSo, for example, you can use something like this:\n\nsender_permetted_from reject 1 no_dmarc_policy 1\ndkim reject 0\ndmarc reject 1 reporting 1\n\nNote this setting will only have effect if:\n\n  * dmarc plugin is used, and loaded after sender_permetted_from in your plugin\'s config\n  * the reject value is either 1 or 2 (meaning, no reject at the mail stage)\n  * dmarc ran with no error\n  * the sender\'s domain has no dmarc policy published (that means, no _dmarc DNS entry)\n\nNote that if a domain has a dmarc "p=none" policy, then this setting has no effect. Only if there\'s no dmarc policy at all it\'ll be used.\n\n=head1 SEE ALSO\n\n http://spf.pobox.com/\n http://en.wikipedia.org/wiki/Sender_Policy_Framework\n\n=head1 TODO\n\nCheck the scope of the SPF policy. If it\'s too broad (ie, the whole internet is valid), apply karma penalty\nExamples of too broad: +all,\n\n=head1 ACKNOWLDGEMENTS\n\nThe reject options are modeled after, and aim to match the functionality of those found in the SPF patch for qmail-smtpd.\n\n=head1 AUTHOR\n\n Matt Simerson - 2013 - populate dmarc_spf note with SPF results\n Matt Simerson - 2012 - increased policy options from 3 to 6\n Matt Simerson - 2011 - rewrote using Mail::SPF\n Matt Sergeant - 2003 - initial plugin\n\n=cut\n\nuse strict;\nuse warnings;\n        return (DECLINED, "SPF - null sender");\n    }\n\n    if ($self->qp->connection->relay_client) {\n        $self->log(LOGINFO, "skip, relay_client");\n        return (DECLINED, "SPF - relaying permitted");\n    }\n\n    if (!$self->{_args}{reject}) {\n        $self->log(LOGINFO, "skip, reject disabled");\n        return (DECLINED);\n    }\n\n    my $client_ip = $self->qp->connection->remote_ip;\n    my $from      = $sender->user . \'@\' . lc($sender->host);\n    my $helo      = $self->qp->connection->hello_host;\n    my $scope     = $from ? \'mfrom\' : \'helo\';\n    my %req_params = (\n        versions => [1, 2],    # optional\n        scope => $scope,\n        ip_address => $client_ip,\n                     );\n\n    if ($scope =~ /^mfrom|pra$/) {\n        $req_params{identity} = $from;\n        $req_params{helo_identity} = $helo if $helo;\n    }\n    elsif ($scope eq \'helo\') {\n    if (!$self->{_args}{reject} && $self->qp->config(\'spfbehavior\')) {\n        $self->{_args}{reject} = $self->qp->config(\'spfbehavior\');\n    }\n    $self->{_args}{no_dmarc_policy} ||= 0;\n    $self->register_hook(\'mail\', \'evaluate_spf\');\n    $self->register_hook(\'data_post_headers\', \'add_spf_header\');\n    $self->register_hook(\'data_post\', \'no_dmarc_policy\') if $self->{_args}{no_dmarc_policy} > 0;\n\n}\n\nsub evaluate_spf {\n    my ($self, $transaction, $sender, %param) = @_;\n\n    if ( $self->is_immune() ) {\n        $transaction->notes(\'dmarc_spf\', {\n                domain => $sender->host,\n                scope  => \'mfrom\',\n                result => \'pass\',\n                } );\n        return DECLINED;\n        return (DECLINED, "SPF - no response");\n    }\n\n    if (!$reject) {\n        $self->log(LOGINFO, "fail, no reject policy ($code: $why)");\n        return (DECLINED, "SPF - $code: $why");\n    }\n\n   # SPF result codes: pass fail softfail neutral none error permerror temperror\n    return $self->handle_code_none($reject, $why) if $code eq \'none\';\n    if ($code eq \'fail\') {\n        $self->adjust_karma(-1);\n        return $self->handle_code_fail($reject, $why);\n    }\n    elsif ($code eq \'softfail\') {\n        $self->adjust_karma(-1);\n        return $self->handle_code_softfail($reject, $why);\n    }\n    elsif ($code eq \'pass\') {\n        $self->adjust_karma(1);\n        $transaction->notes(\'spf_pass_host\', lc $sender->host);\n        $self->log(LOGINFO, "pass, $code: $why");\n        return (DECLINED);\n    }\n    elsif ($code eq \'neutral\') {\n        $self->log(LOGINFO, "fail, $code, $why");\n        return (DENY, "SPF - $code: $why") if $reject >= 5;\n    }\n    }\n    elsif ($scope eq \'helo\') {\n        $req_params{identity}      = $helo;\n        $req_params{helo_identity} = $helo;\n    }\n\n    $transaction->notes(\'dmarc_spf\', {\n            domain => $scope eq \'helo\' ? $helo : $sender->host,\n            scope  => $scope,\n            result => \'none\',\n            } );\n\n    my $spf_server = Mail::SPF::Server->new();\n    my $request    = Mail::SPF::Request->new(%req_params);\n    my $result     = $spf_server->process($request) or do {\n        $self->log(LOGINFO, "fail, no result");\n        return DECLINED;\n    };\n\n    $transaction->notes(\'spfquery\', $result);\n\n    my $code   = $result->code;\n    my $why    = $result->local_explanation;\n    my $reject = $self->{_args}{reject};\n        return (DENY, "SPF - none: $why");\n    }\n\n    $self->log(LOGINFO, "pass, none, $why");\n    return DECLINED;\n}\n\nsub handle_code_fail {\n    my ($self, $reject, $why) = @_;\n\n    if ($reject >= 2) {\n        $self->log(LOGINFO, "fail, $why");\n        return (DENY, "SPF - forgery: $why") if $reject >= 3;\n        return (DENYSOFT, "SPF - fail: $why");\n    }\n\n    $self->log(LOGINFO, "pass, fail tolerated, $why");\n    return DECLINED;\n}\n\nsub handle_code_softfail {\n    my ($self, $reject, $why) = @_;\n\n    if ($reject >= 3) {\n        $self->log(LOGINFO, "fail, soft, $why");\n        return (DENY,     "SPF - fail: $why") if $reject >= 4;\n        return (DENYSOFT, "SPF - fail: $why") if $reject >= 3;\n    }\n\n    $self->log(LOGINFO, "pass, softfail tolerated, $why");\n    return DECLINED;\n}\n\n        $self->log(LOGINFO, "fail, tolerated, $code, $why");\n        return DECLINED;\n    }\n    if ($code =~ /(?:permerror|error)/ ) {\n        $self->log(LOGINFO, "fail, $code, $why") if $reject > 3;\n        return DENY,     "SPF - $code: $why" if $reject >= 6;\n        return DENYSOFT, "SPF - $code: $why" if $reject > 3;\n        $self->log(LOGINFO, "fail, tolerated, $code, $why");\n        return DECLINED;\n    }\n    if ($code eq \'temperror\') {\n        $self->log(LOGINFO, "fail, $code, $why");\n        return DENYSOFT, "SPF - $code: $why" if $reject >= 2;\n        $self->log(LOGINFO, "fail, tolerated, $code, $why");\n        return DECLINED;\n    }\n\n    $self->log(LOGINFO, "SPF from $from was $code: $why");\n    return DECLINED;\n}\n\nsub no_dmarc_policy {\n    my ($self, $transaction) = @_;\n    return DECLINED if $self->is_immune;\n    unless ($self->{_args}{no_dmarc_policy}){\n        return DECLINED;\n    }\n    if ($transaction->notes(\'spfquery\') && $transaction->notes(\'dmarc_result\')){\n        my $spf_result  = $transaction->notes(\'spfquery\')->code;\n        my $why         = $transaction->notes(\'spfquery\')->local_explanation;\n        my $dmarc_dispo = $transaction->notes(\'dmarc_result\')->disposition;\n        return DECLINED unless $dmarc_dispo eq \'none\';\n        my $comment = \'\';\n        if ($transaction->notes(\'dmarc_result\')->reason &&\n            $transaction->notes(\'dmarc_result\')->reason->[0] &&\n            $transaction->notes(\'dmarc_result\')->reason->[0]->comment){\n            $comment = $transaction->notes(\'dmarc_result\')->reason->[0]->comment;\n        }\n        return DECLINED unless $comment eq \'no policy\';\n        # No SPF or syntaxe error: reject if no_dmarc_policy is at least 4\n        if ((!$spf_result || $spf_result =~ m/(?:permerror|error|none)/) && $self->{_args}{no_dmarc_policy} >= 4){\n            $self->log(LOGINFO, "fail, $spf_result, $why");\n            return DENY, "SPF - $spf_result: $why";\n        }\n        # All other reject levels require an SPF code\n        return DECLINED unless $spf_result;\n        # Neutral\n        if ($spf_result eq \'neutral\' && $self->{_args}{no_dmarc_policy} >= 3){\n            $self->log(LOGINFO, "fail, $spf_result, $why");\n            return DENY, "SPF - $spf_result: $why";\n        }\n        # Softfail\n        if ($spf_result eq \'softfail\' && $self->{_args}{no_dmarc_policy} >= 2){\n            $self->log(LOGINFO, "fail, $spf_result, $why");\n            return DENY, "SPF - $spf_result: $why";\n        }\n        # Fail\n        if ($spf_result eq \'fail\' && $self->{_args}{no_dmarc_policy} >= 1){\n            $self->log(LOGINFO, "fail, $spf_result, $why");\n            return DENY, "SPF - $spf_result: $why";\n        }\n    }\n    $self->log(LOGINFO, \'pass\');\n    return DECLINED;\n}\n\nsub handle_code_none {\n    my ($self, $reject, $why) = @_;\n\n    if ($reject >= 6) {\n        $self->log(LOGINFO, "fail, none, $why");\n        return DENY, "SPF - none: $why";\n    }\n\n    $self->log(LOGINFO, "skip, tolerated, none, $why");\n    return DECLINED;\n}\n\nsub handle_code_fail {\n    my ($self, $reject, $why) = @_;\n\n    $self->adjust_karma(-1);\n\n    if ($reject >= 2) {\n        $self->log(LOGINFO, "fail, $why");\n        return DENY, "SPF - forgery: $why" if $reject >= 3;\n        return DENYSOFT, "SPF - fail: $why";\n    }\n\n    $self->log(LOGINFO, "fail, tolerated, $why");\n    return DECLINED;\n}\n\nsub handle_code_softfail {\n    my ($self, $reject, $why) = @_;\n\n    $self->adjust_karma(-1);\n\n    if ($reject >= 3) {\n        $self->log(LOGINFO, "fail, soft, $why");\n        return DENY,     "SPF - fail: $why" if $reject >= 4;\n        return DENYSOFT, "SPF - fail: $why" if $reject >= 3;\n    }\n\n    $self->log(LOGINFO, "fail, tolerated, soft, $why");\n    return DECLINED;\n}\n\nsub add_spf_header {\n    my ($self, $transaction) = @_;\n\n    my $result = $transaction->notes(\'spfquery\') or return DECLINED;\n\n    # if we skipped processing in evaluate_spf, we should skip here too\n    return DECLINED if $self->is_immune();\n\n    $self->log(LOGDEBUG, "result was $result->code");\n\n    if (!$transaction->header) {\n        $self->log(LOGERROR, "missing headers!");\n        return DECLINED;\n    }\n\n    $transaction->header->add(\'Received-SPF\', $result->received_spf_header, 0);\n\n    return DECLINED;\n}\n\nsub is_special_recipient {\n    my ($self, $rcpt) = @_;\n\n    if (!$rcpt) {\n        $self->log(LOGINFO, "skip: missing recipient");\n        return 1;\n    }\n    if (!$rcpt->user) {\n        $self->log(LOGINFO, "skip: missing user");\n        return 1;\n    }\n\n    # special addresses don\'t get SPF-tested.\n    if ($rcpt->user =~ /^(?:postmaster|abuse|mailer-daemon|root)$/i) {\n        $self->log(LOGINFO, "skip: special user (" . $rcpt->user . ")");\n        return 1;\n    }\n\n    return;\n}\n', 'message': 'SPF: arrage flow so if a pass result is possible,\n\nwe will get it and set the note for DMARC plugin\n', 'author_name': 'Matt Simerson', 'lang': 'sender_permitted_from'}