This is an automated email from the ASF dual-hosted git repository.

Arsnael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit c20a917d444fd0bcfa9fe22ef8827e1f7075026e
Author: Felix Auringer <[email protected]>
AuthorDate: Mon Jun 29 10:06:18 2026 +0200

    feat: optional stricter checks 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 any or all resolved mailboxes
    of the intended recipient actually exist 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      | 87 ++++++++++++++++++++--
 1 file changed, 82 insertions(+), 5 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 c4f6110eed..92ddb18f7d 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
@@ -18,6 +18,9 @@
  ****************************************************************/
 package org.apache.james.smtpserver.fastfail;
 
+import java.util.EnumSet;
+import java.util.Optional;
+
 import jakarta.inject.Inject;
 
 import org.apache.commons.configuration2.Configuration;
@@ -32,6 +35,7 @@ import 
org.apache.james.protocols.smtp.core.fastfail.AbstractValidRcptHandler;
 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;
@@ -44,11 +48,27 @@ import org.slf4j.LoggerFactory;
 public class ValidRcptHandler extends AbstractValidRcptHandler implements 
ProtocolHandler {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(ValidRcptHandler.class);
 
+    public enum RecipientRewriteTableCheck {
+        MAPPING_EXISTS,
+        ANY_TARGET_HAS_LOCAL_MAILBOX,
+        ALL_TARGETS_HAVE_LOCAL_MAILBOX;
+
+        private static RecipientRewriteTableCheck parse(String value) throws 
ConfigurationException {
+            return switch (value) {
+                case "mappingExists" -> MAPPING_EXISTS;
+                case "anyMappingValid" -> ANY_TARGET_HAS_LOCAL_MAILBOX;
+                case "allMappingsValid" -> ALL_TARGETS_HAVE_LOCAL_MAILBOX;
+                default -> throw new 
ConfigurationException("ValidRcptHandler.RecipientRewriteTableCheck: 
unsupported value '" + value + "'");
+            };
+        }
+    }
+
     private final UsersRepository users;
     private final RecipientRewriteTable recipientRewriteTable;
     private final DomainList domains;
 
     private boolean supportsRecipientRewriteTable = true;
+    private RecipientRewriteTableCheck recipientRewriteTableCheck = 
RecipientRewriteTableCheck.MAPPING_EXISTS;
 
     @Inject
     public ValidRcptHandler(UsersRepository users, RecipientRewriteTable 
recipientRewriteTable, DomainList domains) {
@@ -61,6 +81,10 @@ public class ValidRcptHandler extends 
AbstractValidRcptHandler implements Protoc
         this.supportsRecipientRewriteTable = supportsRecipientRewriteTable;
     }
 
+    public void setRecipientRewriteTableCheck(RecipientRewriteTableCheck 
recipientRewriteTableCheck) {
+        this.recipientRewriteTableCheck = recipientRewriteTableCheck;
+    }
+
     @Override
     protected boolean isValidRecipient(SMTPSession session, MailAddress 
recipient) throws UsersRepositoryException, RecipientRewriteTableException {
         // Check existence of mailbox first to use RRT less often.
@@ -76,21 +100,73 @@ public class ValidRcptHandler extends 
AbstractValidRcptHandler implements Protoc
         return users.contains(users.getUsername(recipient));
     }
 
-    protected boolean hasValidRRTEntry(MailAddress recipient) throws 
RecipientRewriteTableException {
+    protected boolean hasValidRRTEntry(MailAddress recipient) throws 
RecipientRewriteTableException, UsersRepositoryException {
         LOGGER.debug("Unknown recipient {}, resolving it via RRT", recipient);
 
         try {
-            Mappings targetString = 
recipientRewriteTable.getResolvedMappings(recipient.getLocalPart(), 
recipient.getDomain());
+            return switch (this.recipientRewriteTableCheck) {
+                case MAPPING_EXISTS -> {
+                    Mappings mappings = 
recipientRewriteTable.getResolvedMappings(recipient.getLocalPart(), 
recipient.getDomain());
+                    yield !mappings.isEmpty();
+                }
+                case ANY_TARGET_HAS_LOCAL_MAILBOX -> {
+                    // As long as there is any mapping to a local mailbox, 
this check passes.
+                    // Error mappings are therefore irrelevant.
+                    Mappings mappings = 
recipientRewriteTable.getResolvedMappings(
+                        recipient.getLocalPart(),
+                        recipient.getDomain(),
+                        EnumSet.complementOf(EnumSet.of(Mapping.Type.Error))
+                    );
+                    yield anyResolvedMailboxExists(mappings);
+                }
+                case ALL_TARGETS_HAVE_LOCAL_MAILBOX -> {
+                    Mappings mappings = 
recipientRewriteTable.getResolvedMappings(recipient.getLocalPart(), 
recipient.getDomain());
+                    yield allResolvedMailboxesExist(mappings);
+                }
+            };
+        } catch (ErrorMappingException e) {
+            // Either an error mapping was encountered (ErrorMappingException) 
or the limit for recursively
+            // resolving a mapping was reached (TooManyMappingException).
+            return switch (this.recipientRewriteTableCheck) {
+                case MAPPING_EXISTS -> {
+                    // An error during mapping means that a mapping exists.
+                    LOGGER.info("Error while resolving recipient via RRT, 
allowing recipient {}: ", recipient, e);
+                    yield true;
+                }
+                case ANY_TARGET_HAS_LOCAL_MAILBOX -> {
+                    // It is unclear whether at least one mapping is valid.
+                    LOGGER.info("Error while resolving recipient via RRT, 
refusing recipient {}: ", recipient, e);
+                    yield false;
+                }
+                case ALL_TARGETS_HAVE_LOCAL_MAILBOX -> {
+                    // It is unclear whether all mappings are valid.
+                    LOGGER.info("Error while resolving recipient via RRT, 
refusing recipient {}: ", recipient, e);
+                    yield false;
+                }
+            };
+        }
+    }
 
-            if (!targetString.isEmpty()) {
+    private boolean anyResolvedMailboxExists(Mappings mappings) throws 
UsersRepositoryException {
+        for (Mapping mapping : mappings) {
+            Optional<MailAddress> email = mapping.asMailAddress();
+            if (email.isPresent() && mailboxExists(email.get())) {
                 return true;
             }
-        } catch (ErrorMappingException e) {
-            return true;
         }
         return false;
     }
 
+    private boolean allResolvedMailboxesExist(Mappings mappings) throws 
UsersRepositoryException {
+        for (Mapping mapping : mappings) {
+            Optional<MailAddress> email = mapping.asMailAddress();
+            if (email.isEmpty() || !mailboxExists(email.get())) {
+                return false;
+            }
+        }
+        return !mappings.isEmpty();
+    }
+
     @Override
     protected boolean isLocalDomain(SMTPSession session, Domain domain) throws 
DomainListException {
         return domains.containsDomain(domain);
@@ -99,5 +175,6 @@ public class ValidRcptHandler extends 
AbstractValidRcptHandler implements Protoc
     @Override
     public void init(Configuration config) throws ConfigurationException {
         
setSupportsRecipientRewriteTable(config.getBoolean("enableRecipientRewriteTable",
 true));
+        
setRecipientRewriteTableCheck(RecipientRewriteTableCheck.parse(config.getString("recipientRewriteTableCheck",
 "mappingExists")));
     }
 }


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to