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 3953dc4ecbf6563b6bd5b997756f3bdff97a22aa Author: Quan Tran <[email protected]> AuthorDate: Sat Oct 31 05:15:18 2026 +0700 [FIX] mx mechanism: clean up its lookup state when nothing matches MXMechanism keeps the MX names left to look up and the addresses found in session attributes, shared by every mx mechanism of the check. It removed them once it compared the addresses, but not when no MX name had an address: the next mx mechanism then took its MX answer for the addresses of the left names, and the check ended as permerror. "v=spf1 mx:a.example.com mx -all" did so when the MX hosts of a.example.com have no address, and with AsynchronousSPFExecutor when a.example.com has no MX record: it delivers no data as an empty list. Such an mx mechanism does not match (RFC 7208 5.4). Remove the attributes when nothing matches too. --- .../org/apache/james/jspf/terms/MXMechanism.java | 3 + .../org/apache/james/jspf/SPFExecutorBaseTest.java | 74 ++++++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/resolver/src/main/java/org/apache/james/jspf/terms/MXMechanism.java b/resolver/src/main/java/org/apache/james/jspf/terms/MXMechanism.java index 20cfe34..f79e968 100644 --- a/resolver/src/main/java/org/apache/james/jspf/terms/MXMechanism.java +++ b/resolver/src/main/java/org/apache/james/jspf/terms/MXMechanism.java @@ -153,6 +153,9 @@ public class MXMechanism extends AMechanism implements DNSServiceEnabled, SPFChe // no mx record found if (mxR == null || mxR.size() == 0) { + // clean up attributes: the next mx mechanism would take its MX names for the addresses of these + spfSession.removeAttribute(ATTRIBUTE_CHECK_RECORDS); + spfSession.removeAttribute(ATTRIBUTE_MX_RECORDS); spfSession.setAttribute(Directive.ATTRIBUTE_MECHANISM_RESULT, Boolean.FALSE); 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 fe7710e..d1dc0a4 100644 --- a/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java +++ b/resolver/src/test/java/org/apache/james/jspf/SPFExecutorBaseTest.java @@ -10,16 +10,21 @@ import org.apache.james.jspf.impl.SPF; import org.apache.james.jspf.policies.local.TrustedForwarderPolicy; import org.junit.Before; import org.junit.Test; +import org.xbill.DNS.ARecord; import org.xbill.DNS.DClass; import org.xbill.DNS.Lookup; +import org.xbill.DNS.MXRecord; import org.xbill.DNS.Name; import org.xbill.DNS.Resolver; import org.xbill.DNS.SPFRecord; import org.xbill.DNS.SimpleResolver; import org.xbill.DNS.TXTRecord; import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; +import org.xbill.DNS.lookup.NoSuchRRSetException; import java.io.IOException; +import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.file.Paths; import java.util.ArrayList; @@ -507,4 +512,73 @@ public abstract class SPFExecutorBaseTest { assertEquals(SPFErrorConstants.PERM_ERROR_CONV, result.getResult()); } + + @Test(timeout = 10000) + public void mxShouldMatchAfterAnMxWhoseHostsHaveNoAddress() throws IOException { + String hostname = "mxAfterMxWithoutAddress.spfexecutor.fake"; + FakeResolver fakeResolver = new FakeResolver(); + fakeResolver.addRecord(new TXTRecord(Name.fromString(hostname + "."), DClass.IN, 30L, + "v=spf1 mx:a." + hostname + " mx:b." + hostname + " -all")); + fakeResolver.addRecord(new MXRecord(Name.fromString("a." + hostname + "."), DClass.IN, 30L, 10, + Name.fromString("mail.a." + hostname + "."))); + fakeResolver.addRecord(new MXRecord(Name.fromString("b." + hostname + "."), DClass.IN, 30L, 10, + Name.fromString("mail.b." + hostname + "."))); + fakeResolver.addRecord(new ARecord(Name.fromString("mail.b." + hostname + "."), DClass.IN, 30L, + InetAddress.getByName("192.0.2.10"))); + + SPFResult result = createCustomSPF(new DNSServiceXBillImpl(fakeResolver)) + .checkSPF("192.0.2.10", "a_user@" + hostname, hostname); + + assertEquals(SPFErrorConstants.PASS_CONV, result.getResult()); + } + + @Test(timeout = 10000) + public void mxShouldMatchAfterAnMxOfADomainWithoutMx() { + DNSService dns = new DNSService() { + public List<String> getRecords(DNSRequest request) { + switch (request.getRecordType()) { + case DNSRequest.TXT: + return request.getHostname().equals("example.com") + ? new ArrayList<>(List.of("v=spf1 mx:nomx.example.com mx -all")) : null; + case DNSRequest.MX: + return request.getHostname().equals("example.com") + ? new ArrayList<>(List.of("mail.example.com")) : null; + case DNSRequest.A: + return request.getHostname().equals("mail.example.com") + ? new ArrayList<>(List.of("192.0.2.10")) : null; + default: + return null; + } + } + + // as DNSServiceXBillImpl: an asynchronous lookup without data fails + public CompletionStage<List<String>> getRecordsAsync(DNSRequest request) { + List<String> records = getRecords(request); + if (records == null) { + CompletableFuture<List<String>> noData = new CompletableFuture<>(); + noData.completeExceptionally(new NoSuchRRSetException(Name.root, Type.MX)); + return noData; + } + return CompletableFuture.completedFuture(records); + } + + public List<String> getLocalDomainNames() { + return List.of("localhost"); + } + + public void setTimeOut(int timeOut) { + } + + public int getRecordLimit() { + return 10; + } + + public void setRecordLimit(int recordLimit) { + } + }; + + SPFResult result = createCustomSPF(dns).checkSPF("192.0.2.10", "[email protected]", "example.com"); + + assertEquals(SPFErrorConstants.PASS_CONV, result.getResult()); + } } --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
