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-project.git

commit 3be6d50880d6668fa7f13dbf07ac423d7436d998
Author: Benoit TELLIER <[email protected]>
AuthorDate: Thu Sep 10 12:44:12 2026 +0200

    JAMES-4228 Extract RecipientValidator into data-api
---
 .../apache/james/rrt/api/RecipientValidator.java}  |  89 +++++++-------
 .../smtpserver/fastfail/ValidRcptHandler.java      | 130 +++------------------
 .../james/smtpserver/ValidRcptHandlerTest.java     |   2 +-
 3 files changed, 65 insertions(+), 156 deletions(-)

diff --git 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java
 
b/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientValidator.java
similarity index 67%
copy from 
server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java
copy to 
server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientValidator.java
index 92ddb18f7d..5cd0a0bafe 100644
--- 
a/server/protocols/protocols-smtp/src/main/java/org/apache/james/smtpserver/fastfail/ValidRcptHandler.java
+++ 
b/server/data/data-api/src/main/java/org/apache/james/rrt/api/RecipientValidator.java
@@ -16,7 +16,7 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.smtpserver.fastfail;
+package org.apache.james.rrt.api;
 
 import java.util.EnumSet;
 import java.util.Optional;
@@ -29,12 +29,7 @@ import org.apache.james.core.Domain;
 import org.apache.james.core.MailAddress;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.DomainListException;
-import org.apache.james.protocols.api.handler.ProtocolHandler;
-import org.apache.james.protocols.smtp.SMTPSession;
-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;
@@ -43,68 +38,95 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 /**
- * Handler which reject invalid recipients
+ * Tells whether a recipient can be delivered to: either it has a local 
mailbox, or it is
+ * covered by a {@link RecipientRewriteTable} entry.
+ *
+ * Shared by the protocols willing to reject unknown recipients rather than 
generating a bounce
+ * (SMTP RCPT TO validation, JMAP EmailSubmission/set validation).
  */
-public class ValidRcptHandler extends AbstractValidRcptHandler implements 
ProtocolHandler {
-    private static final Logger LOGGER = 
LoggerFactory.getLogger(ValidRcptHandler.class);
+public class RecipientValidator {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(RecipientValidator.class);
+
+    private static final String ENABLE_RECIPIENT_REWRITE_TABLE_PROPERTY = 
"enableRecipientRewriteTable";
+    private static final String RECIPIENT_REWRITE_TABLE_CHECK_PROPERTY = 
"recipientRewriteTableCheck";
 
     public enum RecipientRewriteTableCheck {
         MAPPING_EXISTS,
         ANY_TARGET_HAS_LOCAL_MAILBOX,
         ALL_TARGETS_HAVE_LOCAL_MAILBOX;
 
-        private static RecipientRewriteTableCheck parse(String value) throws 
ConfigurationException {
+        public 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 + "'");
+                default -> throw new 
ConfigurationException("RecipientValidator.RecipientRewriteTableCheck: 
unsupported value '" + value + "'");
             };
         }
     }
 
+    /**
+     * How strict the validation should be. Supplied by each caller as 
validation strictness is
+     * configured per protocol.
+     */
+    public record Policy(boolean supportsRecipientRewriteTable, 
RecipientRewriteTableCheck recipientRewriteTableCheck) {
+        public static final Policy DEFAULT = new Policy(true, 
RecipientRewriteTableCheck.MAPPING_EXISTS);
+
+        public static Policy from(Configuration config) throws 
ConfigurationException {
+            return new Policy(
+                config.getBoolean(ENABLE_RECIPIENT_REWRITE_TABLE_PROPERTY, 
true),
+                
RecipientRewriteTableCheck.parse(config.getString(RECIPIENT_REWRITE_TABLE_CHECK_PROPERTY,
 "mappingExists")));
+        }
+    }
+
     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) {
+    public RecipientValidator(UsersRepository users, RecipientRewriteTable 
recipientRewriteTable, DomainList domains) {
         this.users = users;
         this.recipientRewriteTable = recipientRewriteTable;
         this.domains = domains;
     }
 
-    public void setSupportsRecipientRewriteTable(boolean 
supportsRecipientRewriteTable) {
-        this.supportsRecipientRewriteTable = supportsRecipientRewriteTable;
-    }
-
-    public void setRecipientRewriteTableCheck(RecipientRewriteTableCheck 
recipientRewriteTableCheck) {
-        this.recipientRewriteTableCheck = recipientRewriteTableCheck;
+    /**
+     * Return true if email for the given recipient should get accepted. 
Recipients of non local
+     * domains are accepted as their validity cannot be assessed locally.
+     */
+    public boolean isValidRecipient(MailAddress recipient, Policy policy) 
throws DomainListException, UsersRepositoryException, 
RecipientRewriteTableException {
+        return !isLocalDomain(recipient.getDomain()) || 
isValidLocalRecipient(recipient, policy);
     }
 
-    @Override
-    protected boolean isValidRecipient(SMTPSession session, MailAddress 
recipient) throws UsersRepositoryException, RecipientRewriteTableException {
+    /**
+     * Return true if email for the given recipient of a local domain should 
get accepted
+     */
+    public boolean isValidLocalRecipient(MailAddress recipient, Policy policy) 
throws UsersRepositoryException, RecipientRewriteTableException {
         // Check existence of mailbox first to use RRT less often.
         if (mailboxExists(recipient)) {
             return true;
         } else {
             // Check whether there is a valid RRT entry for the recipient.
-            return supportsRecipientRewriteTable && 
hasValidRRTEntry(recipient);
+            return policy.supportsRecipientRewriteTable() && 
hasValidRRTEntry(recipient, policy.recipientRewriteTableCheck());
         }
     }
 
-    protected boolean mailboxExists(MailAddress recipient) throws 
UsersRepositoryException {
+    /**
+     * Return true if the domain is local
+     */
+    public boolean isLocalDomain(Domain domain) throws DomainListException {
+        return domains.containsDomain(domain);
+    }
+
+    public boolean mailboxExists(MailAddress recipient) throws 
UsersRepositoryException {
         return users.contains(users.getUsername(recipient));
     }
 
-    protected boolean hasValidRRTEntry(MailAddress recipient) throws 
RecipientRewriteTableException, UsersRepositoryException {
+    public boolean hasValidRRTEntry(MailAddress recipient, 
RecipientRewriteTableCheck check) throws RecipientRewriteTableException, 
UsersRepositoryException {
         LOGGER.debug("Unknown recipient {}, resolving it via RRT", recipient);
 
         try {
-            return switch (this.recipientRewriteTableCheck) {
+            return switch (check) {
                 case MAPPING_EXISTS -> {
                     Mappings mappings = 
recipientRewriteTable.getResolvedMappings(recipient.getLocalPart(), 
recipient.getDomain());
                     yield !mappings.isEmpty();
@@ -127,7 +149,7 @@ public class ValidRcptHandler extends 
AbstractValidRcptHandler implements Protoc
         } 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) {
+            return switch (check) {
                 case MAPPING_EXISTS -> {
                     // An error during mapping means that a mapping exists.
                     LOGGER.info("Error while resolving recipient via RRT, 
allowing recipient {}: ", recipient, e);
@@ -166,15 +188,4 @@ public class ValidRcptHandler extends 
AbstractValidRcptHandler implements Protoc
         }
         return !mappings.isEmpty();
     }
-
-    @Override
-    protected boolean isLocalDomain(SMTPSession session, Domain domain) throws 
DomainListException {
-        return domains.containsDomain(domain);
-    }
-
-    @Override
-    public void init(Configuration config) throws ConfigurationException {
-        
setSupportsRecipientRewriteTable(config.getBoolean("enableRecipientRewriteTable",
 true));
-        
setRecipientRewriteTableCheck(RecipientRewriteTableCheck.parse(config.getString("recipientRewriteTableCheck",
 "mappingExists")));
-    }
 }
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 92ddb18f7d..3fc9f901cf 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,9 +18,6 @@
  ****************************************************************/
 package org.apache.james.smtpserver.fastfail;
 
-import java.util.EnumSet;
-import java.util.Optional;
-
 import jakarta.inject.Inject;
 
 import org.apache.commons.configuration2.Configuration;
@@ -33,148 +30,49 @@ import 
org.apache.james.protocols.api.handler.ProtocolHandler;
 import org.apache.james.protocols.smtp.SMTPSession;
 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.rrt.api.RecipientValidator;
+import org.apache.james.rrt.api.RecipientValidator.RecipientRewriteTableCheck;
 import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.api.UsersRepositoryException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 /**
  * Handler which reject invalid recipients
  */
 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 RecipientValidator recipientValidator;
 
-    private final UsersRepository users;
-    private final RecipientRewriteTable recipientRewriteTable;
-    private final DomainList domains;
-
-    private boolean supportsRecipientRewriteTable = true;
-    private RecipientRewriteTableCheck recipientRewriteTableCheck = 
RecipientRewriteTableCheck.MAPPING_EXISTS;
+    private RecipientValidator.Policy policy = 
RecipientValidator.Policy.DEFAULT;
 
     @Inject
     public ValidRcptHandler(UsersRepository users, RecipientRewriteTable 
recipientRewriteTable, DomainList domains) {
-        this.users = users;
-        this.recipientRewriteTable = recipientRewriteTable;
-        this.domains = domains;
+        this(new RecipientValidator(users, recipientRewriteTable, domains));
+    }
+
+    public ValidRcptHandler(RecipientValidator recipientValidator) {
+        this.recipientValidator = recipientValidator;
     }
 
     public void setSupportsRecipientRewriteTable(boolean 
supportsRecipientRewriteTable) {
-        this.supportsRecipientRewriteTable = supportsRecipientRewriteTable;
+        this.policy = new 
RecipientValidator.Policy(supportsRecipientRewriteTable, 
policy.recipientRewriteTableCheck());
     }
 
     public void setRecipientRewriteTableCheck(RecipientRewriteTableCheck 
recipientRewriteTableCheck) {
-        this.recipientRewriteTableCheck = recipientRewriteTableCheck;
+        this.policy = new 
RecipientValidator.Policy(policy.supportsRecipientRewriteTable(), 
recipientRewriteTableCheck);
     }
 
     @Override
     protected boolean isValidRecipient(SMTPSession session, MailAddress 
recipient) throws UsersRepositoryException, RecipientRewriteTableException {
-        // Check existence of mailbox first to use RRT less often.
-        if (mailboxExists(recipient)) {
-            return true;
-        } else {
-            // Check whether there is a valid RRT entry for the recipient.
-            return supportsRecipientRewriteTable && 
hasValidRRTEntry(recipient);
-        }
-    }
-
-    protected boolean mailboxExists(MailAddress recipient) throws 
UsersRepositoryException {
-        return users.contains(users.getUsername(recipient));
-    }
-
-    protected boolean hasValidRRTEntry(MailAddress recipient) throws 
RecipientRewriteTableException, UsersRepositoryException {
-        LOGGER.debug("Unknown recipient {}, resolving it via RRT", recipient);
-
-        try {
-            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;
-                }
-            };
-        }
-    }
-
-    private boolean anyResolvedMailboxExists(Mappings mappings) throws 
UsersRepositoryException {
-        for (Mapping mapping : mappings) {
-            Optional<MailAddress> email = mapping.asMailAddress();
-            if (email.isPresent() && mailboxExists(email.get())) {
-                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();
+        return recipientValidator.isValidLocalRecipient(recipient, policy);
     }
 
     @Override
     protected boolean isLocalDomain(SMTPSession session, Domain domain) throws 
DomainListException {
-        return domains.containsDomain(domain);
+        return recipientValidator.isLocalDomain(domain);
     }
 
     @Override
     public void init(Configuration config) throws ConfigurationException {
-        
setSupportsRecipientRewriteTable(config.getBoolean("enableRecipientRewriteTable",
 true));
-        
setRecipientRewriteTableCheck(RecipientRewriteTableCheck.parse(config.getString("recipientRewriteTableCheck",
 "mappingExists")));
+        this.policy = RecipientValidator.Policy.from(config);
     }
 }
diff --git 
a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/ValidRcptHandlerTest.java
 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/ValidRcptHandlerTest.java
index 26e38574ae..dad16339e6 100644
--- 
a/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/ValidRcptHandlerTest.java
+++ 
b/server/protocols/protocols-smtp/src/test/java/org/apache/james/smtpserver/ValidRcptHandlerTest.java
@@ -41,10 +41,10 @@ import org.apache.james.protocols.smtp.SMTPSession;
 import org.apache.james.protocols.smtp.hook.HookReturnCode;
 import org.apache.james.protocols.smtp.utils.BaseFakeSMTPSession;
 import org.apache.james.rrt.api.RecipientRewriteTableConfiguration;
+import org.apache.james.rrt.api.RecipientValidator.RecipientRewriteTableCheck;
 import org.apache.james.rrt.lib.MappingSource;
 import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
 import org.apache.james.smtpserver.fastfail.ValidRcptHandler;
-import 
org.apache.james.smtpserver.fastfail.ValidRcptHandler.RecipientRewriteTableCheck;
 import org.apache.james.user.api.UsersRepository;
 import org.apache.james.user.api.UsersRepositoryException;
 import org.apache.james.user.memory.MemoryUsersRepository;


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

Reply via email to