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 6a83667b6c818e1cc186181f663964e0e05d5fcd Author: Quan Tran <[email protected]> AuthorDate: Wed Nov 4 21:41:49 2026 +0700 [FIX] SPFSession: trim only spaces and tabs, then parse MAIL FROM SPFSession trimmed MAIL FROM and HELO with String.trim, which also removes control characters: a HELO "mail.example.com\u0001" was checked as mail.example.com, a domain the client did not give, instead of a malformed domain (RFC 7208 4.3). And it parsed MAIL FROM before trimming it: "[email protected]\t" checked the domain "example.com\t", and a MAIL FROM of spaces was not read as the null sender, whose identity is postmaster at the HELO domain (RFC 7208 2.4). Trim only spaces and tabs from MAIL FROM and HELO, and parse the trimmed MAIL FROM. The client address is still trimmed with String.trim: it holds no control character, so that gives the same address. --- .../org/apache/james/jspf/core/SPFSession.java | 30 ++++++++++++++---- .../org/apache/james/jspf/core/SPFSessionTest.java | 36 ++++++++++++++++++++++ 2 files changed, 60 insertions(+), 6 deletions(-) diff --git a/resolver/src/main/java/org/apache/james/jspf/core/SPFSession.java b/resolver/src/main/java/org/apache/james/jspf/core/SPFSession.java index 97d8381..d00b8ee 100644 --- a/resolver/src/main/java/org/apache/james/jspf/core/SPFSession.java +++ b/resolver/src/main/java/org/apache/james/jspf/core/SPFSession.java @@ -36,6 +36,23 @@ import java.util.function.Predicate; public class SPFSession implements MacroData { + /** + * @param s an identity of the SMTP session + * @return the identity without its leading and trailing spaces and tabs: unlike String.trim, a + * control character stays, so that it makes a malformed domain instead of another one + */ + private static String trimSpaces(String s) { + int start = 0; + int end = s.length(); + while (start < end && (s.charAt(start) == ' ' || s.charAt(start) == '\t')) { + start++; + } + while (end > start && (s.charAt(end - 1) == ' ' || s.charAt(end - 1) == '\t')) { + end--; + } + return s.substring(start, end); + } + private String ipAddress = ""; // also used for (i)<sending-host> private String mailFrom = ""; // (s)<responsible-sender> @@ -94,10 +111,11 @@ public class SPFSession implements MacroData { if (containsCRLF(heloDomain)) { throw new IllegalArgumentException("heloDomain must not contain CR or LF characters"); } - this.mailFrom = mailFrom.trim(); - this.hostName = heloDomain.trim(); + this.mailFrom = trimSpaces(mailFrom); + this.hostName = trimSpaces(heloDomain); try { + // an address holds no control character: String.trim gives the same address this.ipAddress = IPAddr.getProperIpAddress(clientIP.trim()); // get the in Address, of the address checked: trimmed, and IPv4 when IPv4-mapped this.inAddress = IPAddr.getInAddress(this.ipAddress); @@ -107,22 +125,22 @@ public class SPFSession implements MacroData { } // if nullsender is used postmaster@helo will be used as email - if (mailFrom.equals("")) { + if (this.mailFrom.equals("")) { this.currentSenderPart = "postmaster"; this.senderDomain = hostName; this.mailFrom = currentSenderPart + "@" + hostName; } else { - String[] fromParts = mailFrom.split("@"); + String[] fromParts = this.mailFrom.split("@"); // What to do when mailFrom is "@example.com" ? if (fromParts.length > 1) { this.senderDomain = fromParts[fromParts.length -1]; - this.currentSenderPart = mailFrom.substring(0, mailFrom.length() - senderDomain.length() - 1); + this.currentSenderPart = this.mailFrom.substring(0, this.mailFrom.length() - senderDomain.length() - 1); if (this.currentSenderPart.length() == 0) { this.currentSenderPart = "postmaster"; } } else { this.currentSenderPart = "postmaster"; - this.senderDomain = mailFrom; + this.senderDomain = this.mailFrom; } } this.currentDomain = this.senderDomain; diff --git a/resolver/src/test/java/org/apache/james/jspf/core/SPFSessionTest.java b/resolver/src/test/java/org/apache/james/jspf/core/SPFSessionTest.java index 7f91325..aa30d6d 100644 --- a/resolver/src/test/java/org/apache/james/jspf/core/SPFSessionTest.java +++ b/resolver/src/test/java/org/apache/james/jspf/core/SPFSessionTest.java @@ -52,6 +52,42 @@ public class SPFSessionTest { assertEquals("ip6", spfSession.getInAddress()); } + @Test + public void theSenderDomainShouldBeThatOfTheTrimmedMailFrom() { + SPFSession spfSession = new SPFSession(" [email protected]\t", "mail.example.org", "192.0.2.1"); + + assertEquals("example.com", spfSession.getSenderDomain()); + assertEquals("foo", spfSession.getCurrentSenderPart()); + assertEquals("[email protected]", spfSession.getMailFrom()); + } + + @Test + public void aMailFromOfSpacesShouldBeTheNullSender() { + SPFSession spfSession = new SPFSession(" ", "mail.example.org", "192.0.2.1"); + + // RFC 7208 2.4: "postmaster@" and the HELO identity + assertEquals("mail.example.org", spfSession.getSenderDomain()); + assertEquals("[email protected]", spfSession.getMailFrom()); + } + + @Test + public void theClientAddressShouldBeTrimmedOfControlCharacters() { + // as read from a line of a protocol + SPFSession spfSession = new SPFSession("[email protected]", "mail.example.org", "192.0.2.10\n"); + + assertEquals("192.0.2.10", spfSession.getIpAddress()); + assertNull(spfSession.getCurrentResultExpanded()); + } + + @Test + public void controlCharactersShouldStayInTheIdentities() { + SPFSession spfSession = new SPFSession("[email protected]\u0002", "mail.example.org\u0001", "192.0.2.1"); + + // only spaces and tabs are trimmed: a control character makes a malformed domain, not another one + assertEquals("example.com\u0002", spfSession.getSenderDomain()); + assertEquals("mail.example.org\u0001", spfSession.getHostName()); + } + @Test public void shouldReturnIPv4Mapped() { SPFSession spfSession = new SPFSession("", "", "::ffff:192.168.1.1"); --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
