You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@spamassassin.apache.org by fk...@apache.org on 2026/03/15 05:33:23 UTC

svn commit: r1932302 - in spamassassin/trunk: . lib/Mail/SpamAssassin/Plugin

Author: fkento
Date: Sun Mar 15 05:33:23 2026
New Revision: 1932302

Log:
SPF plugin: prefer AuthRes plugin results over direct header parsing

Restructure _check_spf() to use a three-tier priority for obtaining
SPF results:
1. AuthRes plugin ($pms->{authres_parsed}{spf}) - preferred source
   since AuthRes already handles trusted header filtering and proper
   RFC 8601 parsing
2. Received-SPF headers from internal hosts - unchanged fallback
3. DNS lookups via Mail::SPF - last resort

Remove the old manual Authentication-Results header regex parsing from
the SPF plugin, as this is now handled by the AuthRes plugin.  Extract
repeated result-setting code into _set_spf_result() helper.

Update UPGRADE to note that users relying on A-R header parsing need
to enable the AuthRes plugin (v401.pre).

Modified:
   spamassassin/trunk/UPGRADE
   spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm

Modified: spamassassin/trunk/UPGRADE
==============================================================================
--- spamassassin/trunk/UPGRADE	Sun Mar 15 04:20:03 2026	(r1932301)
+++ spamassassin/trunk/UPGRADE	Sun Mar 15 05:33:23 2026	(r1932302)
@@ -1,6 +1,13 @@
 Note for Users Upgrading to SpamAssassin 4.0.3
 ----------------------------------------------
 
+- Mail::SpamAssassin::Plugin::SPF no longer parses Authentication-Results
+  headers directly. It now uses results from
+  Mail::SpamAssassin::Plugin::AuthRes if available, falling back to
+  Received-SPF headers, then DNS lookups as a last resort. Users that
+  previously relied on the SPF plugin to parse Authentication-Results
+  headers will need to enable the AuthRes plugin (v401.pre).
+
 - New Mail::SpamAssassin::Plugin::NeuralNetwork
   This plugin checks messages using Fast Artificial Neural Network library.
 

Modified: spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm
==============================================================================
--- spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm	Sun Mar 15 04:20:03 2026	(r1932301)
+++ spamassassin/trunk/lib/Mail/SpamAssassin/Plugin/SPF.pm	Sun Mar 15 05:33:23 2026	(r1932302)
@@ -29,11 +29,15 @@ This plugin checks a message against Sen
 records published by the domain owners in DNS to fight email address
 forgery and make it easier to identify spams.
 
+SPF results are obtained using the following priority:
+
+1. Results from the AuthRes plugin (if loaded and SPF results available)
+2. Received-SPF headers from internal hosts
+3. DNS lookups via Mail::SPF (last resort)
+
 It's recommended to use MTA filter (pypolicyd-spf / spf-engine etc), so this
-plugin can reuse the Received-SPF and/or Authentication-Results header results as is.
-Otherwise throughput could suffer, DNS lookups done by this plugin are not
-asynchronous.
-Those headers will also help when SpamAssassin is not able to correctly detect EnvelopeFrom.
+plugin can reuse existing results.  Otherwise throughput could suffer, DNS
+lookups done by this plugin are not asynchronous.
 
 =cut
 
@@ -407,9 +411,51 @@ sub _check_spf {
 
   my $timer = $self->{main}->time_method("check_spf");
 
-  # we can re-use results from any *INTERNAL* Received-SPF header in the message...
+  # Try to re-use SPF results from the AuthRes plugin if available.
+  # This is the preferred source since AuthRes already handles trusted
+  # header filtering and proper RFC 8601 parsing.
+  if ($scanner->{authres_parsed} && $scanner->{authres_parsed}{spf}
+      && @{$scanner->{authres_parsed}{spf}}
+      && !$scanner->{checked_for_spf_authres})
+  {
+    $scanner->{checked_for_spf_authres} = 1;
+    dbg("spf: checking for AuthRes plugin SPF results");
+
+    foreach my $spf_result (@{$scanner->{authres_parsed}{spf}}) {
+      my $result = lc($spf_result->{result} || '');
+      next unless $result =~ /^(pass|neutral|(?:soft)?fail|(?:temp|perm)error|none)$/;
+      $result = 'fail' if $result eq 'hardfail';  # RFC 5451 permits this
+
+      # determine identity from smtp properties
+      my $identity = '';  # assume mfrom if we can't tell
+      my $props = $spf_result->{properties} || {};
+      if ($props->{smtp}) {
+        if (exists $props->{smtp}{helo} && !exists $props->{smtp}{mailfrom}
+            && !exists $props->{smtp}{mfrom})
+        {
+          next if $scanner->{spf_helo_checked};
+          $identity = 'helo_';
+        } else {
+          next if $scanner->{spf_checked};
+        }
+      } else {
+        next if $scanner->{spf_checked};
+      }
+
+      $self->_set_spf_result($scanner, $identity, $result);
+      dbg("spf: re-using %s result from AuthRes plugin: %s",
+          ($identity ? 'helo' : 'mfrom'), $result);
+
+      return if ($scanner->{spf_checked} && $scanner->{spf_helo_checked});
+    }
+    # return if we've found the one we're being asked to get
+    return if ( ($ishelo && $scanner->{spf_helo_checked}) ||
+                (!$ishelo && $scanner->{spf_checked}) );
+  }
+
+  # Next, try re-using results from any *INTERNAL* Received-SPF header...
   # we can't use results from trusted but external hosts since (i) spf checks are
-  # supposed to be done "on the domain boundary", (ii) even if an external header 
+  # supposed to be done "on the domain boundary", (ii) even if an external header
   # has a result that matches what we would get, the check was probably done on a
   # different envelope (like the apache.org list servers checking the ORCPT and
   # then using a new envelope to send the mail from the list) and (iii) if the
@@ -437,22 +483,7 @@ sub _check_spf {
       if ($hdr =~ /^received-spf:/i) {
 	dbg("spf: found a Received-SPF header added by an internal host: $hdr");
 
-	# old version:
-	# Received-SPF: pass (herse.apache.org: domain of spamassassin@dostech.ca
-	# 	designates 69.61.78.188 as permitted sender)
-
-	# new version:
-	# Received-SPF: pass (dostech.ca: 69.61.78.188 is authorized to use
-	# 	'spamassassin@dostech.ca' in 'mfrom' identity (mechanism 'mx' matched))
-	# 	receiver=FC5-VPC; identity=mfrom; envelope-from="spamassassin@dostech.ca";
-	# 	helo=smtp.dostech.net; client-ip=69.61.78.188
-
-	# Received-SPF: pass (dostech.ca: 69.61.78.188 is authorized to use 'dostech.ca'
-	# 	in 'helo' identity (mechanism 'mx' matched)) receiver=FC5-VPC; identity=helo;
-	# 	helo=dostech.ca; client-ip=69.61.78.188
-
 	# http://www.openspf.org/RFC_4408#header-field
-	# wtf - for some reason something is sticking an extra space between the header name and field value
 	if ($hdr =~ /^received-spf:\s*(pass|neutral|(?:soft)?fail|(?:temp|perm)error|none)\b(?:.*\bidentity=(\S+?);?\b)?/i) {
 	  my $result = lc($1);
 
@@ -473,19 +504,7 @@ sub _check_spf {
 	    next if $scanner->{spf_checked};
 	  }
 
-	  # we'd set these if we actually did the check
-	  $scanner->{"spf_${identity}checked"} = 1;
-	  $scanner->{"spf_${identity}pass"} = 0;
-	  $scanner->{"spf_${identity}neutral"} = 0;
-	  $scanner->{"spf_${identity}none"} = 0;
-	  $scanner->{"spf_${identity}fail"} = 0;
-	  $scanner->{"spf_${identity}softfail"} = 0;
-	  $scanner->{"spf_${identity}temperror"} = 0;
-	  $scanner->{"spf_${identity}permerror"} = 0;
-	  $scanner->{"spf_${identity}failure_comment"} = undef;
-
-	  # and the result
-	  $scanner->{"spf_${identity}${result}"} = 1;
+	  $self->_set_spf_result($scanner, $identity, $result);
 	  dbg("spf: re-using %s result from Received-SPF header: %s",
               ($identity ? 'helo' : 'mfrom'), $result);
 
@@ -495,57 +514,6 @@ sub _check_spf {
 	} else {
 	  dbg("spf: could not parse result from existing Received-SPF header");
 	}
-
-      } elsif ($hdr =~ /^(?:Arc\-)?Authentication-Results:.*;\s*SPF\s*=\s*([^;]*)/i) {
-        dbg("spf: found an Authentication-Results header added by an internal host: $hdr");
-
-        # RFC 5451 header parser - added by D. Stussy 2010-09-09:
-        # Authentication-Results: mail.example.com; SPF=none smtp.mailfrom=example.org (comment)
-
-        my $tmphdr = $1;
-        if ($tmphdr =~ /^(pass|neutral|(?:hard|soft)?fail|(?:temp|perm)error|none)(?:[^;]*?\bsmtp\.(\S+)\s*=[^;]+)?/i) {
-          my $result = lc($1);
-          $result = 'fail'  if $result eq 'hardfail';  # RFC5451 permits this
-
-          my $identity = '';    # we assume it's a mfrom check if we can't tell otherwise
-          if (defined $2) {
-            $identity = lc($2);
-            if ($identity eq 'mfrom' || $identity eq 'mailfrom') {
-              next if $scanner->{spf_checked};
-              $identity = '';
-            } elsif ($identity eq 'helo') {
-              next if $scanner->{spf_helo_checked};
-              $identity = 'helo_';
-            } else {
-              dbg("spf: found unknown identity value, cannot use: $identity");
-              next;     # try the next Authentication-Results header, if any
-            }
-          } else {
-            next if $scanner->{spf_checked};
-          }
-
-          # we'd set these if we actually did the check
-          $scanner->{"spf_${identity}checked"} = 1;
-          $scanner->{"spf_${identity}pass"} = 0;
-          $scanner->{"spf_${identity}neutral"} = 0;
-          $scanner->{"spf_${identity}none"} = 0;
-          $scanner->{"spf_${identity}fail"} = 0;
-          $scanner->{"spf_${identity}softfail"} = 0;
-          $scanner->{"spf_${identity}temperror"} = 0;
-          $scanner->{"spf_${identity}permerror"} = 0;
-          $scanner->{"spf_${identity}failure_comment"} = undef;
-
-          # and the result
-          $scanner->{"spf_${identity}${result}"} = 1;
-          dbg("spf: re-using %s result from Authentication-Results header: %s",
-               ($identity ? 'helo' : 'mfrom'), $result);
-
-          # if we've got *both* the mfrom and helo results we're done
-          return if ($scanner->{spf_checked} && $scanner->{spf_helo_checked});
-
-        } else {
-          dbg("spf: could not parse result from existing Authentication-Results header");
-        }
       }
     }
     # we can return if we've found the one we're being asked to get
@@ -745,6 +713,21 @@ sub _check_spf {
   }
 }
 
+sub _set_spf_result {
+  my ($self, $scanner, $identity, $result) = @_;
+
+  $scanner->{"spf_${identity}checked"} = 1;
+  $scanner->{"spf_${identity}pass"} = 0;
+  $scanner->{"spf_${identity}neutral"} = 0;
+  $scanner->{"spf_${identity}none"} = 0;
+  $scanner->{"spf_${identity}fail"} = 0;
+  $scanner->{"spf_${identity}softfail"} = 0;
+  $scanner->{"spf_${identity}temperror"} = 0;
+  $scanner->{"spf_${identity}permerror"} = 0;
+  $scanner->{"spf_${identity}failure_comment"} = undef;
+  $scanner->{"spf_${identity}${result}"} = 1;
+}
+
 sub _get_sender {
   my ($self, $scanner) = @_;