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