File: sender_permitted_from

package info (click to toggle)
qpsmtpd 0.94-8
  • links: PTS, VCS
  • area: main
  • in suites: sid, trixie
  • size: 2,340 kB
  • sloc: perl: 17,176; sh: 543; makefile: 186; sql: 100
file content (286 lines) | stat: -rw-r--r-- 8,954 bytes parent folder | download | duplicates (4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
#!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.

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

#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, is Mail::SPF installed?\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');
    }
    $self->register_hook('mail',      'mail_handler');
    $self->register_hook('data_post', 'data_post_handler');
}

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

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

    my $format = $sender->format;
    if ($format eq '<>' || !$sender->host || !$sender->user) {
        $self->log(LOGINFO, "skip, null sender");
        $transaction->notes('dmarc_spf', {
                scope  => 'helo',
                result => 'none',
                } );
        return (DECLINED, "SPF - null sender");
    }

    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 => $self->qp->connection->remote_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;
    }

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

    if (!$code) {
        $self->log(LOGINFO, "fail, no response");
        return (DENYSOFT, "SPF - no response") if $reject >= 2;
        return (DECLINED, "SPF - no response");
    }

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

    $self->store_auth_results("spf=$code smtp.mailfrom=".$sender->host);

    if ($code eq 'pass') {
        $self->adjust_karma(1);
        $transaction->notes('spf_pass_host', lc $sender->host);
        $self->log(LOGINFO, "pass, $why");
        return (DECLINED);
    }

    if (!$reject) {
        $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';
    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') {
        if ($reject >= 5 ) {
            $self->log(LOGINFO, "fail, $code, $why");
            return (DENY, "SPF - $code: $why");
        };
        $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 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 data_post_handler {
    my ($self, $transaction) = @_;

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

    # if we skipped processing in mail_handler, 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;
}