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 7153ba836fd12f67a161f823b0176b4b3a939761 Author: Benoit TELLIER <[email protected]> AuthorDate: Thu Sep 10 13:01:18 2026 +0200 JAMES-4228 ValidRcptEmailSubmissionSetValidation --- .../org/apache/james/jmap/core/SetError.scala | 4 ++ .../jmap/method/EmailSubmissionSetValidation.scala | 38 +++++++++++ .../ValidRcptEmailSubmissionSetValidation.scala | 77 ++++++++++++++++++++++ 3 files changed, 119 insertions(+) diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/SetError.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/SetError.scala index 4c2bcf4a6f..eaaec60e3f 100644 --- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/SetError.scala +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/SetError.scala @@ -39,6 +39,7 @@ object SetError { val mdnAlreadySentValue: SetErrorType = "mdnAlreadySent" val forbiddenFromValue: SetErrorType = "forbiddenFrom" val tooLargeValue: SetErrorType = "tooLarge" + val invalidRecipientsValue: SetErrorType = "invalidRecipients" def invalidArguments(description: SetErrorDescription, properties: Option[Properties] = None): SetError = SetError(invalidArgumentValue, description, properties) @@ -73,6 +74,9 @@ object SetError { def tooLarge(description: SetErrorDescription): SetError = { SetError(SetError.tooLargeValue, description, None) } + + def invalidRecipients(description: SetErrorDescription, properties: Option[Properties] = None): SetError = + SetError(SetError.invalidRecipientsValue, description, properties) } case class SetError(`type`: SetErrorType, description: SetErrorDescription, properties: Option[Properties]) diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetValidation.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetValidation.scala new file mode 100644 index 0000000000..b65e4139d3 --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetValidation.scala @@ -0,0 +1,38 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.method + +import org.apache.james.jmap.core.SetError +import org.apache.mailet.Mail +import reactor.core.scala.publisher.SMono + +/** + * Extension point allowing to reject an EmailSubmission/set creation before the mail gets spooled, + * the JMAP counterpart of the SMTP `RcptHook` / `MailHook` mechanism. + * + * Returning a `SetError` turns the creation into a `notCreated` entry: the client is told + * synchronously rather than through an asynchronous bounce. + * + * Implementations are expected to emit `None` when the mail is accepted, and `Some(setError)` + * when it is rejected. + */ +trait EmailSubmissionSetValidation { + def validate(mail: Mail): SMono[Option[SetError]] +} diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ValidRcptEmailSubmissionSetValidation.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ValidRcptEmailSubmissionSetValidation.scala new file mode 100644 index 0000000000..a56629978e --- /dev/null +++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/ValidRcptEmailSubmissionSetValidation.scala @@ -0,0 +1,77 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.method + +import eu.timepit.refined.auto._ +import jakarta.inject.Inject +import org.apache.james.core.MailAddress +import org.apache.james.jmap.core.SetError.SetErrorDescription +import org.apache.james.jmap.core.{Properties, SetError} +import org.apache.james.jmap.method.ValidRcptEmailSubmissionSetValidation.LOGGER +import org.apache.james.rrt.api.RecipientValidator +import org.apache.james.util.ReactorUtils +import org.apache.mailet.Mail +import org.slf4j.{Logger, LoggerFactory} +import reactor.core.scala.publisher.{SFlux, SMono} + +import scala.jdk.CollectionConverters._ +import scala.util.{Failure, Success, Try} + +object ValidRcptEmailSubmissionSetValidation { + val LOGGER: Logger = LoggerFactory.getLogger(classOf[ValidRcptEmailSubmissionSetValidation]) +} + +/** + * Rejects submissions carrying recipients that James knows it cannot deliver to: recipients of a + * local domain that neither have a mailbox nor are covered by a RecipientRewriteTable entry. + * + * The JMAP counterpart of the SMTP `ValidRcptHandler`, sharing its logic through + * [[org.apache.james.rrt.api.RecipientValidator]]. Recipients of remote domains are left alone as + * their validity cannot be assessed locally. + */ +class ValidRcptEmailSubmissionSetValidation(recipientValidator: RecipientValidator, + policy: RecipientValidator.Policy) extends EmailSubmissionSetValidation { + @Inject + def this(recipientValidator: RecipientValidator) = this(recipientValidator, RecipientValidator.Policy.DEFAULT) + + override def validate(mail: Mail): SMono[Option[SetError]] = + SFlux.fromIterable(mail.getRecipients.asScala.toSeq) + .filterWhen(recipient => isValid(recipient).map(!_), ReactorUtils.DEFAULT_CONCURRENCY) + .collectSeq() + .map { + case Seq() => None + case invalidRecipients => Some(SetError.invalidRecipients( + SetErrorDescription(s"Invalid recipients: ${invalidRecipients.map(_.asString()).mkString(", ")}"), + Some(Properties("envelope.rcptTo")))) + } + + private def isValid(recipient: MailAddress): SMono[Boolean] = + SMono.fromCallable(() => Try(recipientValidator.isValidRecipient(recipient, policy)) match { + case Success(valid) => valid + // Eg the recipient cannot be turned into a username: such a recipient can never be delivered to. + case Failure(e: IllegalArgumentException) => + LOGGER.info("Encountered an error upon recipient validation ({}), rejecting it", recipient.asString(), e) + false + // Storage failures are left to bubble up: they translate into a serverFail, the JMAP + // counterpart of the SMTP deny-soft, rather than into a definitive rejection. + case Failure(e) => throw e + }) + .subscribeOn(ReactorUtils.BLOCKING_CALL_WRAPPER) +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
