File Coverage

blib/lib/Mail/SpamAssassin/Plugin/SPF.pm
Criterion Covered Total %
statement 187 388 48.2
branch 57 240 23.7
condition 11 56 19.6
subroutine 24 33 72.7
pod 2 19 10.5
total 281 736 38.1


line stmt bran cond sub pod time code
1             # <@LICENSE>
2             # Licensed to the Apache Software Foundation (ASF) under one or more
3             # contributor license agreements. See the NOTICE file distributed with
4             # this work for additional information regarding copyright ownership.
5             # The ASF licenses this file to you under the Apache License, Version 2.0
6             # (the "License"); you may not use this file except in compliance with
7             # the License. You may obtain a copy of the License at:
8             #
9             # http://www.apache.org/licenses/LICENSE-2.0
10             #
11             # Unless required by applicable law or agreed to in writing, software
12             # distributed under the License is distributed on an "AS IS" BASIS,
13             # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14             # See the License for the specific language governing permissions and
15             # limitations under the License.
16             # </@LICENSE>
17              
18             =head1 NAME
19              
20             Mail::SpamAssassin::Plugin::SPF - perform SPF verification tests
21              
22             =head1 SYNOPSIS
23              
24             loadplugin Mail::SpamAssassin::Plugin::SPF
25              
26             =head1 DESCRIPTION
27              
28             This plugin checks a message against Sender Policy Framework (SPF)
29             records published by the domain owners in DNS to fight email address
30             forgery and make it easier to identify spams.
31              
32             =cut
33              
34             package Mail::SpamAssassin::Plugin::SPF;
35              
36 21     21   161 use Mail::SpamAssassin::Plugin;
  21         48  
  21         704  
37 21     21   125 use Mail::SpamAssassin::Logger;
  21         50  
  21         1451  
38 21     21   156 use Mail::SpamAssassin::Timeout;
  21         45  
  21         677  
39 21     21   119 use strict;
  21         57  
  21         575  
40 21     21   126 use warnings;
  21         40  
  21         813  
41             # use bytes;
42 21     21   124 use re 'taint';
  21         70  
  21         90425  
43              
44             our @ISA = qw(Mail::SpamAssassin::Plugin);
45              
46             # constructor: register the eval rule
47             sub new {
48 62     62 1 240 my $class = shift;
49 62         156 my $mailsaobject = shift;
50              
51             # some boilerplate...
52 62   33     481 $class = ref($class) || $class;
53 62         362 my $self = $class->SUPER::new($mailsaobject);
54 62         261 bless ($self, $class);
55              
56 62         365 $self->register_eval_rule ("check_for_spf_pass");
57 62         212 $self->register_eval_rule ("check_for_spf_neutral");
58 62         231 $self->register_eval_rule ("check_for_spf_none");
59 62         212 $self->register_eval_rule ("check_for_spf_fail");
60 62         204 $self->register_eval_rule ("check_for_spf_softfail");
61 62         201 $self->register_eval_rule ("check_for_spf_permerror");
62 62         204 $self->register_eval_rule ("check_for_spf_temperror");
63 62         210 $self->register_eval_rule ("check_for_spf_helo_pass");
64 62         185 $self->register_eval_rule ("check_for_spf_helo_neutral");
65 62         202 $self->register_eval_rule ("check_for_spf_helo_none");
66 62         207 $self->register_eval_rule ("check_for_spf_helo_fail");
67 62         194 $self->register_eval_rule ("check_for_spf_helo_softfail");
68 62         207 $self->register_eval_rule ("check_for_spf_helo_permerror");
69 62         241 $self->register_eval_rule ("check_for_spf_helo_temperror");
70 62         198 $self->register_eval_rule ("check_for_spf_whitelist_from");
71 62         221 $self->register_eval_rule ("check_for_def_spf_whitelist_from");
72              
73 62         323 $self->set_config($mailsaobject->{conf});
74              
75 62         691 return $self;
76             }
77              
78             ###########################################################################
79              
80             sub set_config {
81 62     62 0 185 my($self, $conf) = @_;
82 62         161 my @cmds;
83              
84             =head1 USER SETTINGS
85              
86             =over 4
87              
88             =item whitelist_from_spf user@example.com
89              
90             Works similarly to whitelist_from, except that in addition to matching
91             a sender address, a check against the domain's SPF record must pass.
92             The first parameter is an address to whitelist, and the second is a string
93             to match the relay's rDNS.
94              
95             Just like whitelist_from, multiple addresses per line, separated by spaces,
96             are OK. Multiple C<whitelist_from_spf> lines are also OK.
97              
98             The headers checked for whitelist_from_spf addresses are the same headers
99             used for SPF checks (Envelope-From, Return-Path, X-Envelope-From, etc).
100              
101             Since this whitelist requires an SPF check to be made, network tests must be
102             enabled. It is also required that your trust path be correctly configured.
103             See the section on C<trusted_networks> for more info on trust paths.
104              
105             e.g.
106              
107             whitelist_from_spf joe@example.com fred@example.com
108             whitelist_from_spf *@example.com
109              
110             =item def_whitelist_from_spf user@example.com
111              
112             Same as C<whitelist_from_spf>, but used for the default whitelist entries
113             in the SpamAssassin distribution. The whitelist score is lower, because
114             these are often targets for spammer spoofing.
115              
116             =cut
117              
118 62         331 push (@cmds, {
119             setting => 'whitelist_from_spf',
120             type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
121             });
122              
123 62         241 push (@cmds, {
124             setting => 'def_whitelist_from_spf',
125             type => $Mail::SpamAssassin::Conf::CONF_TYPE_ADDRLIST
126             });
127              
128             =back
129              
130             =head1 ADMINISTRATOR OPTIONS
131              
132             =over 4
133              
134             =item spf_timeout n (default: 5)
135              
136             How many seconds to wait for an SPF query to complete, before scanning
137             continues without the SPF result. A numeric value is optionally suffixed
138             by a time unit (s, m, h, d, w, indicating seconds (default), minutes, hours,
139             days, weeks).
140              
141             =cut
142              
143 62         275 push (@cmds, {
144             setting => 'spf_timeout',
145             is_admin => 1,
146             default => 5,
147             type => $Mail::SpamAssassin::Conf::CONF_TYPE_DURATION
148             });
149              
150             =item do_not_use_mail_spf (0|1) (default: 0)
151              
152             By default the plugin will try to use the Mail::SPF module for SPF checks if
153             it can be loaded. If Mail::SPF cannot be used the plugin will fall back to
154             using the legacy Mail::SPF::Query module if it can be loaded.
155              
156             Use this option to stop the plugin from using Mail::SPF and cause it to try to
157             use Mail::SPF::Query instead.
158              
159             =cut
160              
161 62         343 push(@cmds, {
162             setting => 'do_not_use_mail_spf',
163             is_admin => 1,
164             default => 0,
165             type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
166             });
167              
168             =item do_not_use_mail_spf_query (0|1) (default: 0)
169              
170             As above, but instead stop the plugin from trying to use Mail::SPF::Query and
171             cause it to only try to use Mail::SPF.
172              
173             =cut
174              
175 62         340 push(@cmds, {
176             setting => 'do_not_use_mail_spf_query',
177             is_admin => 1,
178             default => 0,
179             type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
180             });
181              
182             =item ignore_received_spf_header (0|1) (default: 0)
183              
184             By default, to avoid unnecessary DNS lookups, the plugin will try to use the
185             SPF results found in any C<Received-SPF> headers it finds in the message that
186             could only have been added by an internal relay.
187              
188             Set this option to 1 to ignore any C<Received-SPF> headers present and to have
189             the plugin perform the SPF check itself.
190              
191             Note that unless the plugin finds an C<identity=helo>, or some unsupported
192             identity, it will assume that the result is a mfrom SPF check result. The
193             only identities supported are C<mfrom>, C<mailfrom> and C<helo>.
194              
195             =cut
196              
197 62         294 push(@cmds, {
198             setting => 'ignore_received_spf_header',
199             is_admin => 1,
200             default => 0,
201             type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
202             });
203              
204             =item use_newest_received_spf_header (0|1) (default: 0)
205              
206             By default, when using C<Received-SPF> headers, the plugin will attempt to use
207             the oldest (bottom most) C<Received-SPF> headers, that were added by internal
208             relays, that it can parse results from since they are the most likely to be
209             accurate. This is done so that if you have an incoming mail setup where one
210             of your primary MXes doesn't know about a secondary MX (or your MXes don't
211             know about some sort of forwarding relay that SA considers trusted+internal)
212             but SA is aware of the actual domain boundary (internal_networks setting) SA
213             will use the results that are most accurate.
214              
215             Use this option to start with the newest (top most) C<Received-SPF> headers,
216             working downwards until results are successfully parsed.
217              
218             =cut
219              
220 62         262 push(@cmds, {
221             setting => 'use_newest_received_spf_header',
222             is_admin => 1,
223             default => 0,
224             type => $Mail::SpamAssassin::Conf::CONF_TYPE_BOOL,
225             });
226              
227 62         318 $conf->{parser}->register_commands(\@cmds);
228             }
229              
230              
231             =item has_check_for_spf_errors
232              
233             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
234              
235             =cut
236              
237 0     0 1 0 sub has_check_for_spf_errors { 1 }
238              
239             # SPF support
240             sub check_for_spf_pass {
241 4     4 0 11 my ($self, $scanner) = @_;
242 4 50       12 $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
243 4         125 $scanner->{spf_pass};
244             }
245              
246             sub check_for_spf_neutral {
247 4     4 0 14 my ($self, $scanner) = @_;
248 4 50       31 $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
249 4         106 $scanner->{spf_neutral};
250             }
251              
252             sub check_for_spf_none {
253 0     0 0 0 my ($self, $scanner) = @_;
254 0 0       0 $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
255 0         0 $scanner->{spf_none};
256             }
257              
258             sub check_for_spf_fail {
259 4     4 0 14 my ($self, $scanner) = @_;
260 4 50       15 $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
261 4 50       28 if ($scanner->{spf_failure_comment}) {
262 0         0 $scanner->test_log ($scanner->{spf_failure_comment});
263             }
264 4         71 $scanner->{spf_fail};
265             }
266              
267             sub check_for_spf_softfail {
268 4     4 0 14 my ($self, $scanner) = @_;
269 4 50       17 $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
270 4         66 $scanner->{spf_softfail};
271             }
272              
273             sub check_for_spf_permerror {
274 0     0 0 0 my ($self, $scanner) = @_;
275 0 0       0 $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
276 0         0 $scanner->{spf_permerror};
277             }
278              
279             sub check_for_spf_temperror {
280 0     0 0 0 my ($self, $scanner) = @_;
281 0 0       0 $self->_check_spf ($scanner, 0) unless $scanner->{spf_checked};
282 0         0 $scanner->{spf_temperror};
283             }
284              
285             sub check_for_spf_helo_pass {
286 4     4 0 11 my ($self, $scanner) = @_;
287 4 50       15 $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
288 4         70 $scanner->{spf_helo_pass};
289             }
290              
291             sub check_for_spf_helo_neutral {
292 4     4 0 16 my ($self, $scanner) = @_;
293 4 50       29 $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
294 4         92 $scanner->{spf_helo_neutral};
295             }
296              
297             sub check_for_spf_helo_none {
298 0     0 0 0 my ($self, $scanner) = @_;
299 0 0       0 $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
300 0         0 $scanner->{spf_helo_none};
301             }
302              
303             sub check_for_spf_helo_fail {
304 4     4 0 11 my ($self, $scanner) = @_;
305 4 50       16 $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
306 4 50       150 if ($scanner->{spf_helo_failure_comment}) {
307 0         0 $scanner->test_log ($scanner->{spf_helo_failure_comment});
308             }
309 4         74 $scanner->{spf_helo_fail};
310             }
311              
312             sub check_for_spf_helo_softfail {
313 4     4 0 14 my ($self, $scanner) = @_;
314 4 50       12 $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
315 4         66 $scanner->{spf_helo_softfail};
316             }
317              
318             sub check_for_spf_helo_permerror {
319 0     0 0 0 my ($self, $scanner) = @_;
320 0 0       0 $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
321 0         0 $scanner->{spf_helo_permerror};
322             }
323              
324             sub check_for_spf_helo_temperror {
325 0     0 0 0 my ($self, $scanner) = @_;
326 0 0       0 $self->_check_spf ($scanner, 1) unless $scanner->{spf_helo_checked};
327 0         0 $scanner->{spf_helo_temperror};
328             }
329              
330             sub check_for_spf_whitelist_from {
331 81     81 0 238 my ($self, $scanner) = @_;
332 81 50       535 $self->_check_spf_whitelist($scanner) unless $scanner->{spf_whitelist_from_checked};
333 81         1470 $scanner->{spf_whitelist_from};
334             }
335              
336             sub check_for_def_spf_whitelist_from {
337 81     81 0 263 my ($self, $scanner) = @_;
338 81 50       543 $self->_check_def_spf_whitelist($scanner) unless $scanner->{def_spf_whitelist_from_checked};
339 81         1519 $scanner->{def_spf_whitelist_from};
340             }
341              
342             sub _check_spf {
343 8     8   20 my ($self, $scanner, $ishelo) = @_;
344              
345 8         36 my $timer = $self->{main}->time_method("check_spf");
346              
347             # we can re-use results from any *INTERNAL* Received-SPF header in the message...
348             # we can't use results from trusted but external hosts since (i) spf checks are
349             # supposed to be done "on the domain boundary", (ii) even if an external header
350             # has a result that matches what we would get, the check was probably done on a
351             # different envelope (like the apache.org list servers checking the ORCPT and
352             # then using a new envelope to send the mail from the list) and (iii) if the
353             # checks are being done right and the envelope isn't being changed it's 99%
354             # likely that the trusted+external host really should be defined as part of your
355             # internal network
356 8 50       43 if ($scanner->{conf}->{ignore_received_spf_header}) {
    100          
357 0         0 dbg("spf: ignoring any Received-SPF headers from internal hosts, by admin setting");
358             } elsif ($scanner->{checked_for_received_spf_header}) {
359 4         16 dbg("spf: already checked for Received-SPF headers, proceeding with DNS based checks");
360             } else {
361 4         11 $scanner->{checked_for_received_spf_header} = 1;
362 4         14 dbg("spf: checking to see if the message has a Received-SPF header that we can use");
363              
364 4         16 my @internal_hdrs = split("\n", $scanner->get('ALL-INTERNAL'));
365 4 50       21 unless ($scanner->{conf}->{use_newest_received_spf_header}) {
366             # look for the LAST (earliest in time) header, it'll be the most accurate
367 4         13 @internal_hdrs = reverse(@internal_hdrs);
368             } else {
369 0         0 dbg("spf: starting with the newest Received-SPF headers first");
370             }
371              
372 4         13 foreach my $hdr (@internal_hdrs) {
373 17         40 local($1,$2);
374 17 50       89 if ($hdr =~ /^received-spf:/i) {
    50          
375 0         0 dbg("spf: found a Received-SPF header added by an internal host: $hdr");
376              
377             # old version:
378             # Received-SPF: pass (herse.apache.org: domain of spamassassin@dostech.ca
379             # designates 69.61.78.188 as permitted sender)
380              
381             # new version:
382             # Received-SPF: pass (dostech.ca: 69.61.78.188 is authorized to use
383             # 'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched))
384             # receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca";
385             # helo=smtp.dostech.net; client-ip=69.61.78.188
386              
387             # Received-SPF: pass (dostech.ca: 69.61.78.188 is authorized to use 'dostech.ca'
388             # in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=helo;
389             # helo=dostech.ca; client-ip=69.61.78.188
390              
391             # http://www.openspf.org/RFC_4408#header-field
392             # wtf - for some reason something is sticking an extra space between the header name and field value
393 0 0       0 if ($hdr =~ /^received-spf:\s*(pass|neutral|(?:soft)?fail|(?:temp|perm)error|none)\b(?:.*\bidentity=(\S+?);?\b)?/i) {
394 0         0 my $result = lc($1);
395              
396 0         0 my $identity = ''; # we assume it's a mfrom check if we can't tell otherwise
397 0 0       0 if (defined $2) {
398 0         0 $identity = lc($2);
399 0 0 0     0 if ($identity eq 'mfrom' || $identity eq 'mailfrom') {
    0          
400 0 0       0 next if $scanner->{spf_checked};
401 0         0 $identity = '';
402             } elsif ($identity eq 'helo') {
403 0 0       0 next if $scanner->{spf_helo_checked};
404 0         0 $identity = 'helo_';
405             } else {
406 0         0 dbg("spf: found unknown identity value, cannot use: $identity");
407 0         0 next; # try the next Received-SPF header, if any
408             }
409             } else {
410 0 0       0 next if $scanner->{spf_checked};
411             }
412              
413             # we'd set these if we actually did the check
414 0         0 $scanner->{"spf_${identity}checked"} = 1;
415 0         0 $scanner->{"spf_${identity}pass"} = 0;
416 0         0 $scanner->{"spf_${identity}neutral"} = 0;
417 0         0 $scanner->{"spf_${identity}none"} = 0;
418 0         0 $scanner->{"spf_${identity}fail"} = 0;
419 0         0 $scanner->{"spf_${identity}softfail"} = 0;
420 0         0 $scanner->{"spf_${identity}temperror"} = 0;
421 0         0 $scanner->{"spf_${identity}permerror"} = 0;
422 0         0 $scanner->{"spf_${identity}failure_comment"} = undef;
423              
424             # and the result
425 0         0 $scanner->{"spf_${identity}${result}"} = 1;
426 0 0       0 dbg("spf: re-using %s result from Received-SPF header: %s",
427             ($identity ? 'helo' : 'mfrom'), $result);
428              
429             # if we've got *both* the mfrom and helo results we're done
430 0 0 0     0 return if ($scanner->{spf_checked} && $scanner->{spf_helo_checked});
431              
432             } else {
433 0         0 dbg("spf: could not parse result from existing Received-SPF header");
434             }
435              
436             } elsif ($hdr =~ /^Authentication-Results:.*;\s*SPF\s*=\s*([^;]*)/i) {
437 0         0 dbg("spf: found an Authentication-Results header added by an internal host: $hdr");
438              
439             # RFC 5451 header parser - added by D. Stussy 2010-09-09:
440             # Authentication-Results: mail.example.com; SPF=none smtp.mailfrom=example.org (comment)
441              
442 0         0 my $tmphdr = $1;
443 0 0       0 if ($tmphdr =~ /^(pass|neutral|(?:hard|soft)?fail|(?:temp|perm)error|none)(?:[^;]*?\bsmtp\.(\S+)\s*=[^;]+)?/i) {
444 0         0 my $result = lc($1);
445 0 0       0 $result = 'fail' if $result eq 'hardfail'; # RFC5451 permits this
446              
447 0         0 my $identity = ''; # we assume it's a mfrom check if we can't tell otherwise
448 0 0       0 if (defined $2) {
449 0         0 $identity = lc($2);
450 0 0 0     0 if ($identity eq 'mfrom' || $identity eq 'mailfrom') {
    0          
451 0 0       0 next if $scanner->{spf_checked};
452 0         0 $identity = '';
453             } elsif ($identity eq 'helo') {
454 0 0       0 next if $scanner->{spf_helo_checked};
455 0         0 $identity = 'helo_';
456             } else {
457 0         0 dbg("spf: found unknown identity value, cannot use: $identity");
458 0         0 next; # try the next Authentication-Results header, if any
459             }
460             } else {
461 0 0       0 next if $scanner->{spf_checked};
462             }
463              
464             # we'd set these if we actually did the check
465 0         0 $scanner->{"spf_${identity}checked"} = 1;
466 0         0 $scanner->{"spf_${identity}pass"} = 0;
467 0         0 $scanner->{"spf_${identity}neutral"} = 0;
468 0         0 $scanner->{"spf_${identity}none"} = 0;
469 0         0 $scanner->{"spf_${identity}fail"} = 0;
470 0         0 $scanner->{"spf_${identity}softfail"} = 0;
471 0         0 $scanner->{"spf_${identity}temperror"} = 0;
472 0         0 $scanner->{"spf_${identity}permerror"} = 0;
473 0         0 $scanner->{"spf_${identity}failure_comment"} = undef;
474              
475             # and the result
476 0         0 $scanner->{"spf_${identity}${result}"} = 1;
477 0 0       0 dbg("spf: re-using %s result from Authentication-Results header: %s",
478             ($identity ? 'helo' : 'mfrom'), $result);
479              
480             # if we've got *both* the mfrom and helo results we're done
481 0 0 0     0 return if ($scanner->{spf_checked} && $scanner->{spf_helo_checked});
482              
483             } else {
484 0         0 dbg("spf: could not parse result from existing Authentication-Results header");
485             }
486             }
487             }
488             # we can return if we've found the one we're being asked to get
489             return if ( ($ishelo && $scanner->{spf_helo_checked}) ||
490 4 50 33     55 (!$ishelo && $scanner->{spf_checked}) );
      33        
      33        
491             }
492              
493             # abort if dns or an spf module isn't available
494 8 50       37 return unless $scanner->is_dns_available();
495 8 50       30 return if $self->{no_spf_module};
496              
497             # select the SPF module we're going to use
498 8 100       25 unless (defined $self->{has_mail_spf}) {
499 1         2 my $eval_stat;
500             eval {
501 1 50       5 die("Mail::SPF disabled by admin setting\n") if $scanner->{conf}->{do_not_use_mail_spf};
502              
503 1         569 require Mail::SPF;
504 1 50 33     54944 if (!defined $Mail::SPF::VERSION || $Mail::SPF::VERSION < 2.001) {
505 0 0       0 die "Mail::SPF 2.001 or later required, this is ".
506             (defined $Mail::SPF::VERSION ? $Mail::SPF::VERSION : 'unknown')."\n";
507             }
508             # Mail::SPF::Server can be re-used, and we get to use our own resolver object!
509             $self->{spf_server} = Mail::SPF::Server->new(
510             hostname => $scanner->get_tag('HOSTNAME'),
511             dns_resolver => $self->{main}->{resolver},
512 1         10 max_dns_interactive_terms => 20);
513             # Bug 7112: max_dns_interactive_terms defaults to 10, but even 14 is
514             # not enough for ebay.com, setting it to 15 NOTE: raising to 20 per bug 7182
515 1         251 1;
516 1 50       2 } or do {
517 0 0       0 $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
518             };
519              
520 1 50       4 if (!defined($eval_stat)) {
521 1         5 dbg("spf: using Mail::SPF for SPF checks");
522 1         3 $self->{has_mail_spf} = 1;
523             } else {
524             # strip the @INC paths... users are going to see it and think there's a problem even though
525             # we're going to fall back to Mail::SPF::Query (which will display the same paths if it fails)
526 0         0 $eval_stat =~ s#^Can't locate Mail/SPFd.pm in \@INC .*#Can't locate Mail/SPFd.pm#;
527 0         0 dbg("spf: cannot load Mail::SPF module or create Mail::SPF::Server object: $eval_stat");
528 0         0 dbg("spf: attempting to use legacy Mail::SPF::Query module instead");
529              
530 0         0 undef $eval_stat;
531             eval {
532 0 0       0 die("Mail::SPF::Query disabled by admin setting\n") if $scanner->{conf}->{do_not_use_mail_spf_query};
533              
534 0         0 require Mail::SPF::Query;
535 0 0 0     0 if (!defined $Mail::SPF::Query::VERSION || $Mail::SPF::Query::VERSION < 1.996) {
536 0 0       0 die "Mail::SPF::Query 1.996 or later required, this is ".
537             (defined $Mail::SPF::Query::VERSION ? $Mail::SPF::Query::VERSION : 'unknown')."\n";
538             }
539 0         0 1;
540 0 0       0 } or do {
541 0 0       0 $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
542             };
543              
544 0 0       0 if (!defined($eval_stat)) {
545 0         0 dbg("spf: using Mail::SPF::Query for SPF checks");
546 0         0 $self->{has_mail_spf} = 0;
547             } else {
548 0         0 dbg("spf: cannot load Mail::SPF::Query module: $eval_stat");
549 0         0 dbg("spf: one of Mail::SPF or Mail::SPF::Query is required for SPF checks, SPF checks disabled");
550 0         0 $self->{no_spf_module} = 1;
551 0         0 return;
552             }
553             }
554             }
555              
556              
557             # skip SPF checks if the A/MX records are nonexistent for the From
558             # domain, anyway, to avoid crappy messages from slowing us down
559             # (bug 3016)
560 8 50       35 return if $scanner->check_for_from_dns();
561              
562 8 100       29 if ($ishelo) {
563             # SPF HELO-checking variant
564 4         16 $scanner->{spf_helo_checked} = 1;
565 4         11 $scanner->{spf_helo_pass} = 0;
566 4         12 $scanner->{spf_helo_neutral} = 0;
567 4         11 $scanner->{spf_helo_none} = 0;
568 4         11 $scanner->{spf_helo_fail} = 0;
569 4         10 $scanner->{spf_helo_softfail} = 0;
570 4         9 $scanner->{spf_helo_permerror} = 0;
571 4         11 $scanner->{spf_helo_temperror} = 0;
572 4         13 $scanner->{spf_helo_failure_comment} = undef;
573             } else {
574             # SPF on envelope sender (where possible)
575 4         13 $scanner->{spf_checked} = 1;
576 4         9 $scanner->{spf_pass} = 0;
577 4         13 $scanner->{spf_neutral} = 0;
578 4         12 $scanner->{spf_none} = 0;
579 4         72 $scanner->{spf_fail} = 0;
580 4         12 $scanner->{spf_softfail} = 0;
581 4         9 $scanner->{spf_permerror} = 0;
582 4         12 $scanner->{spf_temperror} = 0;
583 4         11 $scanner->{spf_failure_comment} = undef;
584             }
585              
586 8         30 my $lasthop = $self->_get_relay($scanner);
587 8 50       20 if (!defined $lasthop) {
588 8 100       35 dbg("spf: no suitable relay for spf use found, skipping SPF%s check",
589             $ishelo ? '-helo' : '');
590 8         20 return;
591             }
592              
593 0         0 my $ip = $lasthop->{ip}; # always present
594 0         0 my $helo = $lasthop->{helo}; # could be missing
595 0 0       0 $scanner->{sender} = '' unless $scanner->{sender_got};
596              
597 0 0       0 if ($ishelo) {
598 0 0       0 unless ($helo) {
599 0         0 dbg("spf: cannot check HELO, HELO value unknown");
600 0         0 return;
601             }
602 0         0 dbg("spf: checking HELO (helo=$helo, ip=$ip)");
603             } else {
604 0 0       0 $self->_get_sender($scanner) unless $scanner->{sender_got};
605              
606             # TODO: we're supposed to use the helo domain as the sender identity (for
607             # mfrom checks) if the sender is the null sender, however determining that
608             # it's the null sender, and not just a failure to get the envelope isn't
609             # exactly trivial... so for now we'll just skip the check
610              
611 0 0       0 if (!$scanner->{sender}) {
612             # we already dbg'd that we couldn't get an Envelope-From and can't do SPF
613 0         0 return;
614             }
615             dbg("spf: checking EnvelopeFrom (helo=%s, ip=%s, envfrom=%s)",
616 0 0       0 ($helo ? $helo : ''), $ip, $scanner->{sender});
617             }
618              
619             # this test could probably stand to be more strict, but try to test
620             # any invalid HELO hostname formats with a header rule
621 0 0 0     0 if ($ishelo && ($helo =~ /^[\[!]?\d+\.\d+\.\d+\.\d+[\]!]?$/ || $helo =~ /^[^.]+$/)) {
      0        
622 0         0 dbg("spf: cannot check HELO of '$helo', skipping");
623 0         0 return;
624             }
625              
626 0 0 0     0 if ($helo && $scanner->server_failed_to_respond_for_domain($helo)) {
627 0         0 dbg("spf: we had a previous timeout on '$helo', skipping");
628 0         0 return;
629             }
630              
631              
632 0         0 my ($result, $comment, $text, $err);
633              
634             # use Mail::SPF if it was available, otherwise use the legacy Mail::SPF::Query
635 0 0       0 if ($self->{has_mail_spf}) {
636              
637             # TODO: currently we won't get to here for a mfrom check with a null sender
638 0 0       0 my $identity = $ishelo ? $helo : ($scanner->{sender}); # || $helo);
639              
640 0 0       0 unless ($identity) {
641 0 0       0 dbg("spf: cannot determine %s identity, skipping %s SPF check",
    0          
642             ($ishelo ? 'helo' : 'mfrom'), ($ishelo ? 'helo' : 'mfrom') );
643 0         0 return;
644             }
645 0   0     0 $helo ||= 'unknown'; # only used for macro expansion in the mfrom explanation
646              
647 0         0 my $request;
648             eval {
649 0 0       0 $request = Mail::SPF::Request->new( scope => $ishelo ? 'helo' : 'mfrom',
650             identity => $identity,
651             ip_address => $ip,
652             helo_identity => $helo );
653 0         0 1;
654 0 0       0 } or do {
655 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
656 0         0 dbg("spf: cannot create Mail::SPF::Request object: $eval_stat");
657 0         0 return;
658             };
659              
660 0         0 my $timeout = $scanner->{conf}->{spf_timeout};
661              
662             my $timer = Mail::SpamAssassin::Timeout->new(
663 0         0 { secs => $timeout, deadline => $scanner->{master_deadline} });
664             $err = $timer->run_and_catch(sub {
665              
666 0     0   0 my $query = $self->{spf_server}->process($request);
667              
668 0         0 $result = $query->code;
669 0 0       0 $comment = $query->authority_explanation if $query->can("authority_explanation");
670 0         0 $text = $query->text;
671              
672 0         0 });
673              
674              
675             } else {
676              
677 0 0       0 if (!$helo) {
678 0         0 dbg("spf: cannot get HELO, cannot use Mail::SPF::Query, consider installing Mail::SPF");
679 0         0 return;
680             }
681              
682             # TODO: if we start doing checks on the null sender using the helo domain
683             # be sure to fix this so that it uses the correct sender identity
684 0         0 my $query;
685             eval {
686             $query = Mail::SPF::Query->new (ip => $ip,
687             sender => $scanner->{sender},
688 0         0 helo => $helo,
689             debug => 0,
690             trusted => 0);
691 0         0 1;
692 0 0       0 } or do {
693 0 0       0 my $eval_stat = $@ ne '' ? $@ : "errno=$!"; chomp $eval_stat;
  0         0  
694 0         0 dbg("spf: cannot create Mail::SPF::Query object: $eval_stat");
695 0         0 return;
696             };
697              
698 0         0 my $timeout = $scanner->{conf}->{spf_timeout};
699              
700             my $timer = Mail::SpamAssassin::Timeout->new(
701 0         0 { secs => $timeout, deadline => $scanner->{master_deadline} });
702             $err = $timer->run_and_catch(sub {
703              
704 0     0   0 ($result, $comment) = $query->result();
705              
706 0         0 });
707              
708             } # end of differences between Mail::SPF and Mail::SPF::Query
709              
710 0 0       0 if ($err) {
711 0         0 chomp $err;
712 0         0 warn("spf: lookup failed: $err\n");
713 0         0 return 0;
714             }
715              
716              
717 0   0     0 $result ||= 'timeout'; # bug 5077
718 0   0     0 $comment ||= '';
719 0         0 $comment =~ s/\s+/ /gs; # no newlines please
720 0   0     0 $text ||= '';
721 0         0 $text =~ s/\s+/ /gs; # no newlines please
722              
723 0 0       0 if ($ishelo) {
724 0 0       0 if ($result eq 'pass') { $scanner->{spf_helo_pass} = 1; }
  0 0       0  
    0          
    0          
    0          
    0          
    0          
    0          
725 0         0 elsif ($result eq 'neutral') { $scanner->{spf_helo_neutral} = 1; }
726 0         0 elsif ($result eq 'none') { $scanner->{spf_helo_none} = 1; }
727 0         0 elsif ($result eq 'fail') { $scanner->{spf_helo_fail} = 1; }
728 0         0 elsif ($result eq 'softfail') { $scanner->{spf_helo_softfail} = 1; }
729 0         0 elsif ($result eq 'permerror') { $scanner->{spf_helo_permerror} = 1; }
730 0         0 elsif ($result eq 'temperror') { $scanner->{spf_helo_temperror} = 1; }
731 0         0 elsif ($result eq 'error') { $scanner->{spf_helo_temperror} = 1; }
732              
733 0 0       0 if ($result eq 'fail') { # RFC 7208 6.2
734 0         0 $scanner->{spf_helo_failure_comment} = "SPF failed: $comment";
735             }
736             } else {
737 0 0       0 if ($result eq 'pass') { $scanner->{spf_pass} = 1; }
  0 0       0  
    0          
    0          
    0          
    0          
    0          
    0          
738 0         0 elsif ($result eq 'neutral') { $scanner->{spf_neutral} = 1; }
739 0         0 elsif ($result eq 'none') { $scanner->{spf_none} = 1; }
740 0         0 elsif ($result eq 'fail') { $scanner->{spf_fail} = 1; }
741 0         0 elsif ($result eq 'softfail') { $scanner->{spf_softfail} = 1; }
742 0         0 elsif ($result eq 'permerror') { $scanner->{spf_permerror} = 1; }
743 0         0 elsif ($result eq 'temperror') { $scanner->{spf_temperror} = 1; }
744 0         0 elsif ($result eq 'error') { $scanner->{spf_temperror} = 1; }
745              
746 0 0       0 if ($result eq 'fail') { # RFC 7208 6.2
747 0         0 $scanner->{spf_failure_comment} = "SPF failed: $comment";
748             }
749             }
750              
751 0         0 dbg("spf: query for $scanner->{sender}/$ip/$helo: result: $result, comment: $comment, text: $text");
752             }
753              
754             sub _get_relay {
755 85     85   228 my ($self, $scanner) = @_;
756              
757             # dos: first external relay, not first untrusted
758 85         268 return $scanner->{relays_external}->[0];
759             }
760              
761             sub _get_sender {
762 77     77   218 my ($self, $scanner) = @_;
763 77         164 my $sender;
764              
765 77         221 $scanner->{sender_got} = 1;
766 77         213 $scanner->{sender} = '';
767              
768 77         280 my $relay = $self->_get_relay($scanner);
769 77 100       243 if (defined $relay) {
770 14         47 $sender = $relay->{envfrom};
771             }
772              
773 77 50       264 if ($sender) {
774 0         0 dbg("spf: found Envelope-From in first external Received header");
775             }
776             else {
777             # We cannot use the env-from data, since it went through 1 or more relays
778             # since the untrusted sender and they may have rewritten it.
779 77 100 66     407 if ($scanner->{num_relays_trusted} > 0 && !$scanner->{conf}->{always_trust_envelope_sender}) {
780 28         141 dbg("spf: relayed through one or more trusted relays, cannot use header-based Envelope-From, skipping");
781 28         70 return;
782             }
783              
784             # we can (apparently) use whatever the current Envelope-From was,
785             # from the Return-Path, X-Envelope-From, or whatever header.
786             # it's better to get it from Received though, as that is updated
787             # hop-by-hop.
788 49         185 $sender = $scanner->get("EnvelopeFrom:addr");
789             }
790              
791 49 100       197 if (!$sender) {
792 48         199 dbg("spf: cannot get Envelope-From, cannot use SPF");
793 48         126 return; # avoid setting $scanner->{sender} to undef
794             }
795              
796 1         4 return $scanner->{sender} = lc $sender;
797             }
798              
799             sub _check_spf_whitelist {
800 81     81   200 my ($self, $scanner) = @_;
801              
802 81         243 $scanner->{spf_whitelist_from_checked} = 1;
803 81         246 $scanner->{spf_whitelist_from} = 0;
804              
805             # if we've already checked for an SPF PASS and didn't get it don't waste time
806             # checking to see if the sender address is in the spf whitelist
807 81 100 66     306 if ($scanner->{spf_checked} && !$scanner->{spf_pass}) {
808 4         18 dbg("spf: whitelist_from_spf: already checked spf and didn't get pass, skipping whitelist check");
809 4         13 return;
810             }
811              
812 77 100       337 $self->_get_sender($scanner) unless $scanner->{sender_got};
813              
814 77 100       251 unless ($scanner->{sender}) {
815 76         296 dbg("spf: spf_whitelist_from: could not find usable envelope sender");
816 76         176 return;
817             }
818              
819 1         3 $scanner->{spf_whitelist_from} = $self->_wlcheck($scanner,'whitelist_from_spf');
820 1 50       3 if (!$scanner->{spf_whitelist_from}) {
821 1         3 $scanner->{spf_whitelist_from} = $self->_wlcheck($scanner, 'whitelist_auth');
822             }
823              
824             # if the message doesn't pass SPF validation, it can't pass an SPF whitelist
825 1 50       3 if ($scanner->{spf_whitelist_from}) {
826 0 0       0 if ($self->check_for_spf_pass($scanner)) {
827 0         0 dbg("spf: whitelist_from_spf: $scanner->{sender} is in user's WHITELIST_FROM_SPF and passed SPF check");
828             } else {
829 0         0 dbg("spf: whitelist_from_spf: $scanner->{sender} is in user's WHITELIST_FROM_SPF but failed SPF check");
830 0         0 $scanner->{spf_whitelist_from} = 0;
831             }
832             } else {
833 1         7 dbg("spf: whitelist_from_spf: $scanner->{sender} is not in user's WHITELIST_FROM_SPF");
834             }
835             }
836              
837             sub _check_def_spf_whitelist {
838 81     81   231 my ($self, $scanner) = @_;
839              
840 81         318 $scanner->{def_spf_whitelist_from_checked} = 1;
841 81         251 $scanner->{def_spf_whitelist_from} = 0;
842              
843             # if we've already checked for an SPF PASS and didn't get it don't waste time
844             # checking to see if the sender address is in the spf whitelist
845 81 100 66     330 if ($scanner->{spf_checked} && !$scanner->{spf_pass}) {
846 4         19 dbg("spf: def_spf_whitelist_from: already checked spf and didn't get pass, skipping whitelist check");
847 4         9 return;
848             }
849              
850 77 100       466 $self->_get_sender($scanner) unless $scanner->{sender_got};
851              
852 77 100       245 unless ($scanner->{sender}) {
853 76         269 dbg("spf: def_spf_whitelist_from: could not find usable envelope sender");
854 76         144 return;
855             }
856              
857 1         4 $scanner->{def_spf_whitelist_from} = $self->_wlcheck($scanner,'def_whitelist_from_spf');
858 1 50       3 if (!$scanner->{def_spf_whitelist_from}) {
859 1         2 $scanner->{def_spf_whitelist_from} = $self->_wlcheck($scanner, 'def_whitelist_auth');
860             }
861              
862             # if the message doesn't pass SPF validation, it can't pass an SPF whitelist
863 1 50       3 if ($scanner->{def_spf_whitelist_from}) {
864 0 0       0 if ($self->check_for_spf_pass($scanner)) {
865 0         0 dbg("spf: def_whitelist_from_spf: $scanner->{sender} is in DEF_WHITELIST_FROM_SPF and passed SPF check");
866             } else {
867 0         0 dbg("spf: def_whitelist_from_spf: $scanner->{sender} is in DEF_WHITELIST_FROM_SPF but failed SPF check");
868 0         0 $scanner->{def_spf_whitelist_from} = 0;
869             }
870             } else {
871 1         15 dbg("spf: def_whitelist_from_spf: $scanner->{sender} is not in DEF_WHITELIST_FROM_SPF");
872             }
873             }
874              
875             sub _wlcheck {
876 4     4   8 my ($self, $scanner, $param) = @_;
877 4 50       15 if (defined ($scanner->{conf}->{$param}->{$scanner->{sender}})) {
878 0         0 return 1;
879             } else {
880 4         7 study $scanner->{sender}; # study is a no-op since perl 5.16.0
881 4         5 foreach my $regexp (values %{$scanner->{conf}->{$param}}) {
  4         11  
882 0 0       0 if ($scanner->{sender} =~ qr/$regexp/i) {
883 0         0 return 1;
884             }
885             }
886             }
887 4         10 return 0;
888             }
889              
890             ###########################################################################
891              
892             1;
893              
894             =back
895              
896             =cut