This is an automated email from the ASF dual-hosted git repository.

chibenwa pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-jspf.git

commit 6e573fb5ffbb3b73fe81199a71d80bbe44eb4ede
Author: Quan Tran <[email protected]>
AuthorDate: Thu Nov 5 10:19:58 2026 +0700

    [FIX] SPFRetriever: look up the TXT record first
    
    The record of a domain was looked up as the obsolete SPF type first,
    and as TXT only when the domain had none, as RFC 4408 4.4 allowed. A
    domain whose SPF type record went stale, after its TXT record changed,
    was checked against the stale one, where RFC 7208 4.4 looks the record
    up as "type TXT only" (3.1: the SPF type is discontinued).
    
    Look up TXT first, and the SPF type only when no TXT record is an SPF
    record: a domain still publishing that type only keeps its result, as
    do the YAML suites whose zones publish it only. A DNS error of the TXT
    lookup is a temperror (RFC 7208 4.4), now also for a domain that has a
    record of the SPF type, whose TXT records were not looked up before; a
    DNS error of the SPF type then means no record. A domain with a TXT
    record needs one lookup instead of two. Strict mode
    (setSPFMustEqualsTXT) still compares both.
---
 docs/modules/ROOT/pages/usage.adoc                 |  3 +-
 .../apache/james/jspf/policies/SPFRetriever.java   | 45 +++++++----------
 .../org/apache/james/jspf/SPFExecutorBaseTest.java | 57 ++++++++++++++++++++--
 3 files changed, 72 insertions(+), 33 deletions(-)

diff --git a/docs/modules/ROOT/pages/usage.adoc 
b/docs/modules/ROOT/pages/usage.adoc
index 5cc3c07..de53c7d 100644
--- a/docs/modules/ROOT/pages/usage.adoc
+++ b/docs/modules/ROOT/pages/usage.adoc
@@ -144,7 +144,8 @@ spf.setSPFMustEqualsTXT(true);                          // 
<4>
 <3> When no SPF record is found, evaluate `v=spf1 a/24 mx/24 ptr ?all` instead 
of
 returning `none`. Only `pass` or `neutral` can result from a best guess. 
Defaults to false.
 <4> Return a `permerror` when a domain publishes both an SPF-type and a 
TXT-type record
-and the two disagree. Defaults to false.
+and the two disagree. Defaults to false: the TXT record is then read (RFC 7208 
4.4), and a
+record of the obsolete SPF type only when no TXT record is an SPF record.
 
 Two policy objects let you supply records jSPF would not otherwise see:
 
diff --git 
a/resolver/src/main/java/org/apache/james/jspf/policies/SPFRetriever.java 
b/resolver/src/main/java/org/apache/james/jspf/policies/SPFRetriever.java
index 0c6629e..765f15e 100644
--- a/resolver/src/main/java/org/apache/james/jspf/policies/SPFRetriever.java
+++ b/resolver/src/main/java/org/apache/james/jspf/policies/SPFRetriever.java
@@ -42,6 +42,10 @@ import java.util.List;
  */
 public class SPFRetriever implements SPFChecker {
     
+    /**
+     * The TXT records of the domain, where RFC 7208 4.4 looks the record up 
("type TXT only"): when
+     * none is an SPF record, the records of the obsolete SPF type are read, 
as RFC 4408 4.4 allowed
+     */
     private static final class SPFRecordHandlerDNSResponseListener implements 
SPFCheckerDNSResponseListener {
 
         /**
@@ -59,6 +63,9 @@ public class SPFRetriever implements SPFChecker {
                 String record = extractSPFRecord(spfR);
                 if (record != null) {
                     session.setAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD, new 
SPF1Record(record));
+                } else if (!CHECK_ONLY_TXT_RECORDS) {
+                    String currentDomain = session.getCurrentDomain();
+                    return new DNSLookupContinuation(new 
DNSRequest(currentDomain, DNSRequest.SPF), new 
SPFRetrieverDNSResponseListener());
                 }
             } catch (TimeoutException e) {
                 throw new TempErrorException("Timeout querying dns");
@@ -69,6 +76,9 @@ public class SPFRetriever implements SPFChecker {
         
     }
 
+    /**
+     * The records of the obsolete SPF type, of a domain whose TXT records 
hold no SPF record
+     */
     private static final class SPFRetrieverDNSResponseListener implements 
SPFCheckerDNSResponseListener {
 
         /**
@@ -79,36 +89,22 @@ public class SPFRetriever implements SPFChecker {
                 throws PermErrorException, NoneException,
                 TempErrorException, NeutralException {
             try {
-                List<String> spfR = response.getResponse();
-                
-                if (spfR == null || spfR.isEmpty()) {
-                    
-                    String currentDomain = session.getCurrentDomain();
-                    return new DNSLookupContinuation(new 
DNSRequest(currentDomain, DNSRequest.TXT), new 
SPFRecordHandlerDNSResponseListener());
-
-                } else {
-                    
-                    String record = extractSPFRecord(spfR);
-                    if (record != null) {
-                        session.setAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD, 
new SPF1Record(record));
-                    }
-                    
+                String record = extractSPFRecord(response.getResponse());
+                if (record != null) {
+                    session.setAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD, new 
SPF1Record(record));
                 }
-                
-                return null;
-                
             } catch (TimeoutException e) {
                 // RFC 7208 4.4: the record is looked up as "type TXT only": a 
DNS error of the obsolete SPF type is not one
-                String currentDomain = session.getCurrentDomain();
-                return new DNSLookupContinuation(new DNSRequest(currentDomain, 
DNSRequest.TXT), new SPFRecordHandlerDNSResponseListener());
             }
+            return null;
         }
         
     }
 
        /**
-        * This is used for testing purpose. Setting this to true will skip the 
initial
-        * lookups for SPF records and instead will simply check the TXT 
records.
+        * This is used for testing purpose. Setting this to true will skip the 
lookup of
+        * the obsolete SPF type, when no TXT record is an SPF record, and will 
simply check
+        * the TXT records.
         */
        private static final boolean CHECK_ONLY_TXT_RECORDS = false;
     
@@ -154,12 +150,7 @@ public class SPFRetriever implements SPFChecker {
         SPF1Record res = (SPF1Record) 
spfData.getAttribute(SPF1Utils.ATTRIBUTE_SPF1_RECORD);
         if (res == null) {
             String currentDomain = spfData.getCurrentDomain();
-            if (CHECK_ONLY_TXT_RECORDS) {
-                return new DNSLookupContinuation(new DNSRequest(currentDomain, 
DNSRequest.TXT), new SPFRecordHandlerDNSResponseListener());
-            } else {
-                return new DNSLookupContinuation(new DNSRequest(currentDomain, 
DNSRequest.SPF), new SPFRetrieverDNSResponseListener());
-            }
-            
+            return new DNSLookupContinuation(new DNSRequest(currentDomain, 
DNSRequest.TXT), new SPFRecordHandlerDNSResponseListener());
         }
         return null;
     }
diff --git 
a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java 
b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java
index 31294d1..98057a5 100644
--- a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java
+++ b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java
@@ -23,6 +23,7 @@ import org.xbill.DNS.Message;
 import org.xbill.DNS.Name;
 import org.xbill.DNS.PTRRecord;
 import org.xbill.DNS.Rcode;
+import org.xbill.DNS.Record;
 import org.xbill.DNS.Resolver;
 import org.xbill.DNS.SPFRecord;
 import org.xbill.DNS.Section;
@@ -82,14 +83,14 @@ public abstract class SPFExecutorBaseTest {
         return fakeResolver;
     }
 
-    // hostname publishes "v=spf1 -all" as TXT, on a server failing the 
lookups of the obsolete SPF type
-    private static DNSService dnsFailingTheSpfType(String hostname) throws 
IOException {
+    // a server publishing the record, failing the lookups of the given 
DNSRequest type
+    private static DNSService dnsFailing(int failedType, Record record) throws 
IOException {
         FakeResolver fakeResolver = new FakeResolver();
-        fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), 
DClass.IN, 30L, "v=spf1 -all"));
+        fakeResolver.addRecord(record);
         return new DNSServiceXBillImpl(fakeResolver) {
             @Override
             public List<String> getRecords(DNSRequest request) throws 
TimeoutException {
-                if (request.getRecordType() == DNSRequest.SPF) {
+                if (request.getRecordType() == failedType) {
                     throw new TimeoutException("SERVFAIL");
                 }
                 return super.getRecords(request);
@@ -97,7 +98,7 @@ public abstract class SPFExecutorBaseTest {
 
             @Override
             public CompletionStage<List<String>> getRecordsAsync(DNSRequest 
request) {
-                if (request.getRecordType() == DNSRequest.SPF) {
+                if (request.getRecordType() == failedType) {
                     return CompletableFuture.failedFuture(new 
TimeoutException("SERVFAIL"));
                 }
                 return super.getRecordsAsync(request);
@@ -105,6 +106,11 @@ public abstract class SPFExecutorBaseTest {
         };
     }
 
+    // hostname publishes "v=spf1 -all" as TXT, on a server failing the 
lookups of the obsolete SPF type
+    private static DNSService dnsFailingTheSpfType(String hostname) throws 
IOException {
+        return dnsFailing(DNSRequest.SPF, new 
TXTRecord(Name.fromString(hostname + "."), DClass.IN, 30L, "v=spf1 -all"));
+    }
+
     protected abstract SPF createSPF();
 
     protected abstract SPF createCustomSPF(DNSService dnsService);
@@ -897,6 +903,47 @@ public abstract class SPFExecutorBaseTest {
         assertEquals(SPFErrorConstants.PASS_CONV, result.getResult());
     }
 
+    @Test(timeout = 10000)
+    public void shouldPreferTheTxtRecordToARecordOfTheSpfType() throws 
IOException {
+        String hostname = "txtAndSpfType.spfexecutor.fake";
+        FakeResolver fakeResolver = new FakeResolver();
+        fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), 
DClass.IN, 30L, "v=spf1 +all"));
+        // a stale record of the obsolete SPF type
+        fakeResolver.addRecord(new SPFRecord(Name.fromString(hostname + "."), 
DClass.IN, 30L, "v=spf1 -all"));
+        SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver));
+
+        SPFResult result = spf.checkSPF("192.0.2.10", "a_user@" + hostname, 
hostname);
+
+        // RFC 7208 4.4: the record is looked up as "type TXT only"
+        assertEquals(SPFErrorConstants.PASS_CONV, result.getResult());
+    }
+
+    @Test(timeout = 10000)
+    public void shouldReadTheSpfTypeWhenNoTxtRecordIsAnSpfRecord() throws 
IOException {
+        String hostname = "spfTypeOnly.spfexecutor.fake";
+        FakeResolver fakeResolver = new FakeResolver();
+        fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), 
DClass.IN, 30L, "site-verification=abc"));
+        fakeResolver.addRecord(new SPFRecord(Name.fromString(hostname + "."), 
DClass.IN, 30L, "v=spf1 -all"));
+        SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver));
+
+        SPFResult result = spf.checkSPF("192.0.2.10", "a_user@" + hostname, 
hostname);
+
+        // as before RFC 7208, for a domain publishing its record as the 
obsolete SPF type only
+        assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult());
+    }
+
+    @Test(timeout = 10000)
+    public void 
shouldReturnTempErrorWhenTheTxtLookupFailsAlthoughTheSpfTypeHasARecord() throws 
IOException {
+        String hostname = "txtservfail.spfexecutor.fake";
+        DNSService dns = dnsFailing(DNSRequest.TXT,
+            new SPFRecord(Name.fromString(hostname + "."), DClass.IN, 30L, 
"v=spf1 -all"));
+
+        SPFResult result = createCustomSPF(dns).checkSPF("192.0.2.10", 
"a_user@" + hostname, hostname);
+
+        // RFC 7208 4.4: the record is looked up as "type TXT only", a DNS 
error of which is a temperror
+        assertEquals(SPFErrorConstants.TEMP_ERROR_CONV, result.getResult());
+    }
+
     @Test(timeout = 10000)
     public void shouldCheckTheTxtRecordWhenTheLookupOfTheSpfTypeFails() throws 
IOException {
         String hostname = "spftypeservfail.spfexecutor.fake";


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to