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]
