From d1aa042c67c6f26ea5712e2454a5f891ae79659e Mon Sep 17 00:00:00 2001 From: Felix Auringer Date: Wed, 24 Jun 2026 18:23:07 +0200 Subject: [PATCH 1/2] refactor: split mailbox and RRT checks into two methods This enhances the extensibility as only one of them can be overwritten without having to touch the logic of the other. --- .../smtpserver/fastfail/ValidRcptHandler.java | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java index f435f305715..49dec75ad93 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java @@ -24,7 +24,6 @@ import org.apache.commons.configuration2.ex.ConfigurationException; import org.apache.james.core.Domain; import org.apache.james.core.MailAddress; -import org.apache.james.core.Username; import org.apache.james.domainlist.api.DomainList; import org.apache.james.domainlist.api.DomainListException; import org.apache.james.protocols.api.handler.ProtocolHandler; @@ -64,18 +63,21 @@ public void setSupportsRecipientRewriteTable(boolean supportsRecipientRewriteTab @Override protected boolean isValidRecipient(SMTPSession session, MailAddress recipient) throws UsersRepositoryException, RecipientRewriteTableException { - Username username = users.getUsername(recipient); - - if (users.contains(username)) { + // Check existence of mailbox first to use RRT less often. + if (mailboxExists(recipient)) { return true; } else { - return supportsRecipientRewriteTable && isRedirected(recipient, username.asString()); + // Check whether there is a valid RRT entry for the recipient. + return supportsRecipientRewriteTable && hasValidRRTEntry(recipient); } } - private boolean isRedirected(MailAddress recipient, String username) throws RecipientRewriteTableException { - LOGGER.debug("Unknown user {} check if it's an alias", username); + protected boolean mailboxExists(MailAddress recipient) throws UsersRepositoryException { + return users.contains(users.getUsername(recipient)); + } + protected boolean hasValidRRTEntry(MailAddress recipient) throws RecipientRewriteTableException { + LOGGER.debug("Unknown recipient {}, resolving it via RRT", recipient); try { Mappings targetString = recipientRewriteTable.getResolvedMappings(recipient.getLocalPart(), recipient.getDomain()); From 49e5f609613b01376bb880721302063aed750c47 Mon Sep 17 00:00:00 2001 From: Felix Auringer Date: Mon, 29 Jun 2026 10:06:18 +0200 Subject: [PATCH 2/2] feat: optional stricter check of RRT when receiving email Currently, James only checks whether either the recipient exists or any alias for it exists. There is no guarantee that the alias actually points to an existing mailbox. The optional behavior checks whether all resolved mailboxes of the intended recipient actually exists locally. This allows to abort directly after the RCPT command instead of accepting the email and then error because the mailbox could not be found. However, with this additional check enabled, it is not possible anymore to forward emails to other email servers via the RRT. --- .../smtpserver/fastfail/ValidRcptHandler.java | 41 ++++++++++++++++--- 1 file changed, 35 insertions(+), 6 deletions(-) diff --git a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java index 49dec75ad93..31eb91b76c4 100644 --- a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java +++ b/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java @@ -20,6 +20,8 @@ import jakarta.inject.Inject; +import java.util.EnumSet; + import org.apache.commons.configuration2.Configuration; import org.apache.commons.configuration2.ex.ConfigurationException; import org.apache.james.core.Domain; @@ -32,6 +34,7 @@ import org.apache.james.rrt.api.RecipientRewriteTable; import org.apache.james.rrt.api.RecipientRewriteTable.ErrorMappingException; import org.apache.james.rrt.api.RecipientRewriteTableException; +import org.apache.james.rrt.lib.Mapping; import org.apache.james.rrt.lib.Mappings; import org.apache.james.user.api.UsersRepository; import org.apache.james.user.api.UsersRepositoryException; @@ -49,6 +52,7 @@ public class ValidRcptHandler extends AbstractValidRcptHandler implements Protoc private final DomainList domains; private boolean supportsRecipientRewriteTable = true; + private boolean ignoreMappingsWithoutLocalMailbox = false; @Inject public ValidRcptHandler(UsersRepository users, RecipientRewriteTable recipientRewriteTable, DomainList domains) { @@ -61,6 +65,10 @@ public void setSupportsRecipientRewriteTable(boolean supportsRecipientRewriteTab this.supportsRecipientRewriteTable = supportsRecipientRewriteTable; } + public void setIgnoreMappingsWithoutLocalMailbox(boolean ignoreMappingsWithoutLocalMailbox) { + this.ignoreMappingsWithoutLocalMailbox = ignoreMappingsWithoutLocalMailbox; + } + @Override protected boolean isValidRecipient(SMTPSession session, MailAddress recipient) throws UsersRepositoryException, RecipientRewriteTableException { // Check existence of mailbox first to use RRT less often. @@ -79,15 +87,35 @@ protected boolean mailboxExists(MailAddress recipient) throws UsersRepositoryExc protected boolean hasValidRRTEntry(MailAddress recipient) throws RecipientRewriteTableException { LOGGER.debug("Unknown recipient {}, resolving it via RRT", recipient); try { - Mappings targetString = recipientRewriteTable.getResolvedMappings(recipient.getLocalPart(), recipient.getDomain()); - - if (!targetString.isEmpty()) { - return true; + // Error mappings are only used to forbid sending from the source address and can be ignored here. + Mappings mappings = recipientRewriteTable.getResolvedMappings( + recipient.getLocalPart(), + recipient.getDomain(), + EnumSet.complementOf(EnumSet.of(Mapping.Type.Error)) + ); + if (ignoreMappingsWithoutLocalMailbox) { + return !mappings.isEmpty(); + } else { + return allResolvedMailboxesExist(mappings); } } catch (ErrorMappingException e) { - return true; + // As we filter the mappings above, this case should never happen. + LOGGER.error("Unexpexted mapping of type Error: ", e); + return false; } - return false; + } + + private boolean allResolvedMailboxesExist(Mappings mappings) { + return mappings + .asStream() + .flatMap(mapping -> mapping.asMailAddress().stream()) + .allMatch(address -> { + try { + return mailboxExists(address); + } catch (UsersRepositoryException e) { + return false; + } + }); } @Override @@ -98,5 +126,6 @@ protected boolean isLocalDomain(SMTPSession session, Domain domain) throws Domai @Override public void init(Configuration config) throws ConfigurationException { setSupportsRecipientRewriteTable(config.getBoolean("enableRecipientRewriteTable", true)); + setIgnoreMappingsWithoutLocalMailbox(config.getBoolean("ignoreMappingsWithoutLocalMailbox", true)); } }