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 caa3e51be9d2b1a66bbf591b932355f418da67e2 Author: Quan Tran <[email protected]> AuthorDate: Tue Nov 3 10:02:00 2026 +0700 [FIX] DefaultExplanationPolicy: not for an included record The policies run on the included records too, and the default explanation was computed for each one that failed, with its domain as %{d}. ExpModifier skips the explanation inside an include, this policy did not, and its explanation stayed for the rest of the check. With the default explanation "Denied %{d}" and "v=spf1 include:other.example -all", where other.example has "v=spf1 -all": - the check failed with "Denied other.example", not the checked domain; - with "+all" in place of "-all", it passed with that explanation. setUseTrustedForwarder(true) includes the trusted forwarder host in every record ending with -all or ?all, so a fail named that host when its own record failed. Skip it when the session ignores the explanation, as ExpModifier does: RFC 7208 6.2, during the recursion into an include an exp of the target MUST NOT be used, and the explanation is for a fail of the checked domain. --- .../policies/local/DefaultExplanationPolicy.java | 5 ++++ .../org/apache/james/jspf/SPFExecutorBaseTest.java | 35 ++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/resolver/src/main/java/org/apache/james/jspf/policies/local/DefaultExplanationPolicy.java b/resolver/src/main/java/org/apache/james/jspf/policies/local/DefaultExplanationPolicy.java index aa73b48..73a8336 100644 --- a/resolver/src/main/java/org/apache/james/jspf/policies/local/DefaultExplanationPolicy.java +++ b/resolver/src/main/java/org/apache/james/jspf/policies/local/DefaultExplanationPolicy.java @@ -73,6 +73,11 @@ public final class DefaultExplanationPolicy implements PolicyPostFilter { */ public DNSLookupContinuation checkSPF(SPFSession spfData) throws PermErrorException, NoneException, TempErrorException, NeutralException { + // as for exp=, not the explanation of an included record + if (spfData.ignoreExplanation()) { + return null; + } + if (SPF1Constants.FAIL.equals(spfData.getCurrentResult())) { if (spfData.getExplanation()==null || spfData.getExplanation().equals("")) { String explanation; 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 4e66f42..122505a 100644 --- a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java +++ b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java @@ -51,6 +51,16 @@ public abstract class SPFExecutorBaseTest { return new TXTRecord(Name.fromString(hostname + "."), DClass.IN, 30L, strings); } + // hostname includes target.<hostname>, whose record fails, then ends with lastTerm + private static FakeResolver includeOfAFailingRecord(String hostname, String lastTerm) throws IOException { + String target = "target." + hostname; + FakeResolver fakeResolver = new FakeResolver(); + fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), DClass.IN, 30L, + "v=spf1 include:" + target + " " + lastTerm)); + fakeResolver.addRecord(new TXTRecord(Name.fromString(target + "."), DClass.IN, 30L, "v=spf1 -all")); + return fakeResolver; + } + protected abstract SPF createSPF(); protected abstract SPF createCustomSPF(DNSService dnsService); @@ -671,4 +681,29 @@ public abstract class SPFExecutorBaseTest { assertEquals(SPFErrorConstants.PERM_ERROR_CONV, result.getResult()); } + + @Test(timeout = 10000) + public void defaultExplanationShouldNameTheCheckedDomainAfterAFailingInclude() throws IOException { + String hostname = "defaultExplanationAfterFailingInclude.spfexecutor.fake"; + SPF spf = createCustomSPF(new DNSServiceXBillImpl(includeOfAFailingRecord(hostname, "-all"))); + spf.setDefaultExplanation("Denied %{d}"); + + SPFResult result = spf.checkSPF("192.0.2.10", "a_user@" + hostname, hostname); + + assertEquals(SPFErrorConstants.FAIL_CONV, result.getResult()); + assertEquals("Denied " + hostname, result.getExplanation()); + } + + @Test(timeout = 10000) + public void passShouldHaveNoExplanationAfterAFailingInclude() throws IOException { + String hostname = "passAfterFailingInclude.spfexecutor.fake"; + SPF spf = createCustomSPF(new DNSServiceXBillImpl(includeOfAFailingRecord(hostname, "+all"))); + spf.setDefaultExplanation("Denied %{d}"); + + SPFResult result = spf.checkSPF("192.0.2.10", "a_user@" + hostname, hostname); + + assertEquals(SPFErrorConstants.PASS_CONV, result.getResult()); + // SPFResult gives "" when there is no explanation + assertEquals("", result.getExplanation()); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
