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]
