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