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 080bbc7782b9f17f546d4dd2898fd60f638faff4
Author: Quan Tran <[email protected]>
AuthorDate: Tue Nov 3 15:48:47 2026 +0700

    [FIX] ptr mechanism: compare the domain names whatever their case
    
    ptr matched a validated name against the target domain with equals and
    endsWith, so as text: with the case and the trailing dot of the domain
    as written, and not label by label. For a client whose validated name
    is mail.example.com, "v=spf1 ptr:EXAMPLE.COM -all" failed, and so did
    "v=spf1 ptr -all" for the sender domain EXAMPLE.COM or example.com.
    (with the dot of a fully qualified name), which pass as example.com.
    And a validated name whose first label holds a dot, mail\.example.com,
    a sibling of example.com, passed as one of its subdomains.
    
    Compare them as domain names, label by label, whatever the case and a
    trailing dot (RFC 4343, RFC 7208 5.5). A root target still matches no
    name.
---
 .../org/apache/james/jspf/terms/PTRMechanism.java  | 21 +++++++++-
 .../org/apache/james/jspf/SPFExecutorBaseTest.java | 48 ++++++++++++++++++++++
 2 files changed, 67 insertions(+), 2 deletions(-)

diff --git 
a/resolver/src/main/java/org/apache/james/jspf/terms/PTRMechanism.java 
b/resolver/src/main/java/org/apache/james/jspf/terms/PTRMechanism.java
index 20e9e82..5ac51d3 100644
--- a/resolver/src/main/java/org/apache/james/jspf/terms/PTRMechanism.java
+++ b/resolver/src/main/java/org/apache/james/jspf/terms/PTRMechanism.java
@@ -36,6 +36,8 @@ import org.apache.james.jspf.core.exceptions.NoneException;
 import org.apache.james.jspf.core.exceptions.PermErrorException;
 import org.apache.james.jspf.core.exceptions.TempErrorException;
 import org.apache.james.jspf.core.exceptions.TimeoutException;
+import org.xbill.DNS.Name;
+import org.xbill.DNS.TextParseException;
 
 import java.util.List;
 
@@ -93,6 +95,22 @@ public class PTRMechanism extends GenericMechanism 
implements DNSServiceEnabled,
      */
     public static final String REGEX = "[pP][tT][rR]" + "(?:\\:"
             + SPFTermsRegexps.DOMAIN_SPEC_REGEX + ")?";
+
+    /**
+     * @param validated a validated domain name
+     * @param target the target domain
+     * @return true if the validated name is the target or a subdomain of it, 
label by label and
+     *         whatever the case and a trailing dot (RFC 4343, RFC 7208 5.5)
+     */
+    private static boolean isSubdomain(String validated, String target) {
+        try {
+            Name targetName = Name.fromString(target, Name.root);
+            // the root is no target: every name is under it
+            return !targetName.equals(Name.root) && Name.fromString(validated, 
Name.root).subdomain(targetName);
+        } catch (TextParseException e) {
+            return false;
+        }
+    }
     
     private DNSService dnsService;
 
@@ -159,8 +177,7 @@ public class PTRMechanism extends GenericMechanism 
implements DNSServiceEnabled,
                         // Added the IPAddr parsing/toString to have matching 
in IPV6 multiple ways to 
                         if (IPAddr.getAddress((String) 
aList.get(j)).getIPAddress().equals(IPAddr.getAddress(spfSession.getIpAddress()).getIPAddress()))
 {
                             
-                            if (compareDomain.equals(host)
-                                    || compareDomain.endsWith("." + host)) {
+                            if (isSubdomain(compareDomain, host)) {
                                 
spfSession.setAttribute(Directive.ATTRIBUTE_MECHANISM_RESULT, Boolean.TRUE);
                                 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 11cc622..613a99e 100644
--- a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java
+++ b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java
@@ -16,6 +16,7 @@ import org.xbill.DNS.DClass;
 import org.xbill.DNS.Lookup;
 import org.xbill.DNS.MXRecord;
 import org.xbill.DNS.Name;
+import org.xbill.DNS.PTRRecord;
 import org.xbill.DNS.Resolver;
 import org.xbill.DNS.SPFRecord;
 import org.xbill.DNS.SimpleResolver;
@@ -31,6 +32,7 @@ import java.nio.file.Paths;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Locale;
 import java.util.concurrent.CompletableFuture;
 import java.util.concurrent.CompletionStage;
 import java.util.concurrent.ThreadLocalRandom;
@@ -61,6 +63,18 @@ public abstract class SPFExecutorBaseTest {
         return fakeResolver;
     }
 
+    // hostname has "v=spf1 <ptrTerm> -all", and 1.2.3.4 the validated name 
<ptrName>
+    private static FakeResolver ptrOfTheClient(String hostname, String 
ptrTerm, String ptrName) throws IOException {
+        FakeResolver fakeResolver = new FakeResolver();
+        fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), 
DClass.IN, 30L,
+            "v=spf1 " + ptrTerm + " -all"));
+        fakeResolver.addRecord(new 
PTRRecord(Name.fromString("4.3.2.1.in-addr.arpa."), DClass.IN, 30L,
+            Name.fromString(ptrName)));
+        fakeResolver.addRecord(new ARecord(Name.fromString(ptrName), 
DClass.IN, 30L,
+            InetAddress.getByName("1.2.3.4")));
+        return fakeResolver;
+    }
+
     protected abstract SPF createSPF();
 
     protected abstract SPF createCustomSPF(DNSService dnsService);
@@ -618,6 +632,40 @@ public abstract class SPFExecutorBaseTest {
         assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult());
     }
 
+    @Test(timeout = 10000)
+    public void ptrShouldMatchADomainInUpperCase() throws IOException {
+        String hostname = "ptrcase.spfexecutor.fake";
+        FakeResolver fakeResolver = ptrOfTheClient(hostname, "ptr:" + 
hostname.toUpperCase(Locale.ROOT),
+            "mail." + hostname + ".");
+        SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver));
+
+        SPFResult result = spf.checkSPF("1.2.3.4", "a_user@" + hostname, 
hostname);
+
+        assertEquals(SPFErrorConstants.PASS_CONV, result.getResult());
+    }
+
+    @Test(timeout = 10000)
+    public void ptrShouldMatchASenderDomainWithATrailingDot() throws 
IOException {
+        String hostname = "ptrdot.spfexecutor.fake";
+        SPF spf = createCustomSPF(new 
DNSServiceXBillImpl(ptrOfTheClient(hostname, "ptr", "mail." + hostname + ".")));
+
+        SPFResult result = spf.checkSPF("1.2.3.4", "a_user@" + hostname + ".", 
hostname);
+
+        assertEquals(SPFErrorConstants.PASS_CONV, result.getResult());
+    }
+
+    @Test(timeout = 10000)
+    public void ptrShouldNotMatchALabelHoldingADotAsASubdomain() throws 
IOException {
+        String hostname = "ptrlabel.spfexecutor.fake";
+        // the labels "mail.ptrlabel", "spfexecutor" and "fake": a sibling of 
hostname, not a subdomain
+        FakeResolver fakeResolver = ptrOfTheClient(hostname, "ptr", "mail\\." 
+ hostname + ".");
+        SPF spf = createCustomSPF(new DNSServiceXBillImpl(fakeResolver));
+
+        SPFResult result = spf.checkSPF("1.2.3.4", "a_user@" + hostname, 
hostname);
+
+        assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult());
+    }
+
     @Test(timeout = 10000)
     public void shouldReturnPermErrorOnAIp4CidrOverflowingAnInt() throws 
IOException {
         SPFResult result = checkRecord("aIp4CidrOverflow.spfexecutor.fake", 
"v=spf1 a/4294967296 -all", "1.2.3.4");


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

Reply via email to