You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/01/28 03:10:59 UTC

[james-project] branch master updated (afb7925 -> d21c297)

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

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


    from afb7925  JAMES-3490 maxUploadSize should come from configuration
     new 571d07f  JAMES-3431 Document DSN setup for the Distributed Server
     new d21c297  JAMES-3491 Transport independant JMAP API

The 2 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 docs/modules/servers/nav.adoc                      |   1 +
 .../servers/pages/distributed/configure/dsn.adoc   | 218 +++++++++++++++++++++
 .../servers/pages/distributed/configure/index.adoc |   3 +-
 .../pages/distributed/configure/smtp-hooks.adoc    |  23 +++
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  96 ++-------
 .../org/apache/james/jmap/routes/JmapApi.scala     | 101 ++++++++++
 .../james/jmap/routes/JMAPApiRoutesTest.scala      |   4 +-
 7 files changed, 361 insertions(+), 85 deletions(-)
 create mode 100644 docs/modules/servers/pages/distributed/configure/dsn.adoc
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JmapApi.scala


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 01/02: JAMES-3431 Document DSN setup for the Distributed Server

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 571d07f58f6b16803ab97eb72da7177405dfd3d1
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Dec 29 17:20:01 2020 +0700

    JAMES-3431 Document DSN setup for the Distributed Server
---
 docs/modules/servers/nav.adoc                      |   1 +
 .../servers/pages/distributed/configure/dsn.adoc   | 218 +++++++++++++++++++++
 .../servers/pages/distributed/configure/index.adoc |   3 +-
 .../pages/distributed/configure/smtp-hooks.adoc    |  23 +++
 4 files changed, 244 insertions(+), 1 deletion(-)

diff --git a/docs/modules/servers/nav.adoc b/docs/modules/servers/nav.adoc
index 7002b26..ccf2860 100644
--- a/docs/modules/servers/nav.adoc
+++ b/docs/modules/servers/nav.adoc
@@ -52,6 +52,7 @@
 ***** xref:distributed/configure/remote-delivery-error-handling.adoc[About RemoteDelivery error handling]
 ***** xref:distributed/configure/collecting-contacts.adoc[Contact collection]
 ***** xref:distributed/configure/collecting-events.adoc[Event collection]
+***** xref:distributed/configure/dsn.adoc[ESMTP DSN support]
 *** xref:distributed/operate/index.adoc[Operate]
 **** xref:distributed/operate/guide.adoc[]
 **** xref:distributed/operate/logging.adoc[]
diff --git a/docs/modules/servers/pages/distributed/configure/dsn.adoc b/docs/modules/servers/pages/distributed/configure/dsn.adoc
new file mode 100644
index 0000000..714324b
--- /dev/null
+++ b/docs/modules/servers/pages/distributed/configure/dsn.adoc
@@ -0,0 +1,218 @@
+= Distributed James Server &mdash; Delivery Submission Notifications
+:navtitle: ESMTP DSN setup
+
+DSN introduced in link:https://tools.ietf.org/html/rfc3461[RFC-3461] allows a SMTP sender to demand status messages,
+defined in link:https://tools.ietf.org/html/rfc3464[RFC-3464] to be sent back to the `Return-Path` upon delivery
+progress.
+
+DSN support is not enabled by default, as it needs specific configuration of the
+xref:distributed/configure/mailetcontainer.adoc[mailetcontainer.xml] to be specification compliant.
+
+To enable it you need to:
+
+- Add DSN SMTP hooks as part of the SMTP server stack
+- Configure xref:distributed/configure/mailetcontainer.adoc[mailetcontainer.xml] to generate DSN bounces when needed
+
+== Enabling DSN in SMTP server stack
+
+For this simply add the `DSN hooks` in the handler chain in `smtpserver.xml` :
+
+....
+<smtpserver enabled="true">
+    <...> <!-- The rest of your SMTP configuration, unchanged -->
+    <handlerchain>
+        <handler class="org.apache.james.smtpserver.dsn.DSNEhloHook"/>
+        <handler class="org.apache.james.smtpserver.dsn.DSNMailParameterHook"/>
+        <handler class="org.apache.james.smtpserver.dsn.DSNRcptParameterHook"/>
+        <handler class="org.apache.james.smtpserver.dsn.DSNMessageHook"/>
+        <...> <!-- other handlers, like: -->
+        <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/> <!-- for instance -->
+    </handlerchain>
+</smtpserver>
+....
+
+== Enabling DSN generation as part of mail processing
+
+For the below conditions to be matched we assume you follow
+xref:distributed/configure/remote-delivery-error-handling.adoc[RemoteDelivery error handling for MXs], which is a
+requirement for detailed RemoteDelivery error and delay handling on top of the Distributed server.
+
+Here is a sample xref:distributed/configure/mailetcontainer.adoc[mailetcontainer.xml] achieving the following DSN generation:
+
+- Generate a generic `delivered` notification if LocalDelivery succeeded, if requested
+- Generate a generic `failed` notification in case of local errors, if requested
+- Generate a specific `failed` notification in case of a non existing local user, if requested
+- Generate a specific `failed` notification in case of an address rewriting loop, if requested
+- Generate a `failed` notification in case of remote permanent errors, if requested. We blame the remote server...
+- Generate a `delayed` notification in case of temporary remote errors we are about to retry, if requested. We blame the remote server...
+- Generate a `failed` notification in case of temporary remote errors we are not going to retry (failed too many time), if requested. We blame the remote server...
+
+....
+<mailetcontainer enableJmx="true">
+    <!-- Common processing settings are unchanged -->
+
+    <processors>
+        <processor state="root" enableJmx="true">\
+            <!-- Content of root processor is unchanged -->
+        </processor>
+
+        <processor state="transport" enableJmx="true">
+            <!-- transport processor unchanged -->
+        </processor>
+
+        <processor state="error" enableJmx="true">
+            <mailet match="DSNFailureRequested" class="DSNBounce">
+                <prefix>[FAILED]</prefix>
+                <passThrough>true</passThrough>
+                <messageString>Hi. This is the James mail server at [machine].
+I'm afraid I wasn't able to deliver your message to the following addresses.
+This is a permanent error; I've given up. Sorry it didn't work out.  Below
+I include the list of recipients, and the reason why I was unable to deliver
+your message.</messageString>
+                <action>failed</action>
+                <defaultStatus>5.0.0</defaultStatus>
+            </mailet>
+            <mailet match="All" class="ToRepository">
+                <repositoryPath>cassandra://var/mail/error/</repositoryPath>
+            </mailet>
+        </processor>
+
+        <processor state="local-delivery" enableJmx="true">
+            <!-- Your local-delivery pipeline -->
+            <mailet match="All" class="LocalDelivery">
+                <!-- Do not abort the pipeline yet -->
+                <consume>false</consume>
+            </mailet>
+            <!-- Tell the world we succeeded -->
+            <mailet match="DSNSuccessRequested" class="DSNBounce">
+                <prefix>[SUCCESS]</prefix>
+                <passThrough>true</passThrough>
+                <messageString>Hi. This is the James mail server at [machine].
+I successfully delivered your message to the following addresses.
+Note that it indicates your recipients received the message but do
+not imply they read it.</messageString>
+                <action>delivered</action>
+                <defaultStatus>2.0.0</defaultStatus>
+            </mailet>
+            <mailet match="All" class="Null"/> <!-- ignore people not having requesting a dsn success bounce -->
+        </processor>
+
+        <processor state="relay" enableJmx="true">
+            <!-- Perform at most 5 RemoteDelivery attempts -->
+            <mailet match="AtMost=5" class="RemoteDelivery">
+                <outgoingQueue>outgoing</outgoingQueue>
+                <maxRetries>0</maxRetries>
+                <maxDnsProblemRetries>0</maxDnsProblemRetries>
+                <deliveryThreads>10</deliveryThreads>
+                <sendpartial>true</sendpartial>
+                <!-- Use a custom processor for error handling -->
+                <bounceProcessor>remote-delivery-error</bounceProcessor>
+            </mailet>
+            <!-- When retries are exceeded, consider the mail as a permanent failure -->
+            <mailet match="DSNFailureRequested" class="DSNBounce">
+                <prefix>[FAILED]</prefix>
+                <passThrough>true</passThrough>
+                <messageString>Hi. This is the James mail server at [machine].
+I'm afraid I wasn't able to deliver your message to the following addresses.
+This is a permanent error; I've given up. Sorry it didn't work out.
+The remote server we should relay this mail to keep on failing.
+Below I include the list of recipients, and the reason why I was unable to deliver
+your message.</messageString>
+                <action>failed</action>
+                <defaultStatus>5.0.0</defaultStatus>
+            </mailet>
+            <mailet match="All" class="ToRepository">
+                <repositoryPath>cassandra://var/mail/error/remote-delivery/permanent/</repositoryPath>
+            </mailet>
+        </processor>
+
+        <processor state="remote-delivery-error" enableJmx="true">
+            <matcher name="dsn-permanent" match="org.apache.james.mailetcontainer.impl.matchers.And">
+                <matcher match="IsRemoteDeliveryPermanentError"/>
+                <matcher match="DSNFailureRequested"/>
+            </matcher>
+            <matcher name="dsn-temporary" match="org.apache.james.mailetcontainer.impl.matchers.And">
+                <matcher match="IsRemoteDeliveryTemporaryError"/>
+                <matcher match="DSNDelayRequested"/>
+            </matcher>
+
+            <mailet match="dsn-permanent" class="DSNBounce">
+                <prefix>[FAILED]</prefix>
+                <passThrough>true</passThrough>
+                <messageString>Hi. This is the James mail server at [machine].
+I'm afraid I wasn't able to deliver your message to the following addresses.
+This is a permanent error; I've given up. Sorry it didn't work out.
+The remote server we should relay this mail to returns a permanent error.
+Below I include the list of recipients, and the reason why I was unable to deliver
+your message.</messageString>
+                <action>failed</action>
+                <defaultStatus>5.0.0</defaultStatus>
+            </mailet>
+
+            <mailet match="dsn-temporary" class="DSNBounce">
+                <prefix>[DELAYED]</prefix>
+                <passThrough>true</passThrough>
+                <messageString>Hi. This is the James mail server at [machine].
+I'm afraid I wasn't able to deliver your message to the following addresses yet.
+This is a temporary error: I will keep on trying.
+Below I include the list of recipients, and the reason why I was unable to deliver
+your message.</messageString>
+                <action>delayed</action>
+                <defaultStatus>4.0.0</defaultStatus>
+            </mailet>
+
+            <!-- Error management for remote delivery error handling as described in remote-delivery-error-handling.adoc -->
+        </processor>
+
+        <processor state="local-address-error" enableJmx="true">
+            <mailet match="DSNFailureRequested" class="DSNBounce">
+                <prefix>[FAILED]</prefix>
+                <passThrough>true</passThrough>
+                <messageString>Hi. This is the James mail server at [machine].
+I'm afraid I wasn't able to deliver your message to the following addresses.
+This is a permanent error; I've given up. Sorry it didn't work out.
+The following addresses do not exist here. Sorry.</messageString>
+                <action>failed</action>
+                <defaultStatus>5.0.0</defaultStatus>
+            </mailet>
+            <mailet match="All" class="ToRepository">
+                <repositoryPath>cassandra://var/mail/address-error/</repositoryPath>
+            </mailet>
+        </processor>
+
+        <processor state="relay-denied" enableJmx="true">
+            <!-- This is an abuse, you likely do not want to be polite with these people. we just keep a copy for later audit & replay -->
+            <mailet match="All" class="ToRepository">
+                <repositoryPath>cassandra://var/mail/relay-denied/</repositoryPath>
+                <notice>Warning: You are sending an e-mail to a remote server. You must be authenticated to perform such an operation</notice>
+            </mailet>
+        </processor>
+
+        <processor state="rrt-error" enableJmx="false">
+            <mailet match="All" class="ToRepository">
+                <repositoryPath>cassandra://var/mail/rrt-error/</repositoryPath>
+                <passThrough>true</passThrough>
+            </mailet>
+            <mailet match="IsSenderInRRTLoop" class="Null"/>
+            <mailet match="DSNFailureRequested" class="DSNBounce">
+                <prefix>[FAILED]</prefix>
+                <passThrough>true</passThrough>
+                <messageString>Hi. This is the James mail server at [machine].
+I'm afraid I wasn't able to deliver your message to the following addresses.
+This is a permanent error; I've given up. Sorry it didn't work out.
+The following addresses is caught in a rewriting loop. An admin should come and fix it (you likely want to report it).
+Once resolved the admin should be able to resume the processing of your email.
+Below I include the list of recipients, and the reason why I was unable to deliver
+your message.</messageString>
+                <action>failed</action>
+                <defaultStatus>5.1.6/defaultStatus>
+            </mailet>
+        </processor>
+    </processors>
+</mailetcontainer>
+....
+
+== Limitations
+
+The out of the box tooling do not allow generating `relayed` DSN notification as RemoteDelivery misses a success
+callback.
\ No newline at end of file
diff --git a/docs/modules/servers/pages/distributed/configure/index.adoc b/docs/modules/servers/pages/distributed/configure/index.adoc
index 6a15ada..86fd97b 100644
--- a/docs/modules/servers/pages/distributed/configure/index.adoc
+++ b/docs/modules/servers/pages/distributed/configure/index.adoc
@@ -59,4 +59,5 @@ By omitting these files, no extra behaviour is added.
 ** xref:distributed/configure/spam.adoc[This page] documents Anti-Spam setup with SpamAssassin.
 ** xref:distributed/configure/remote-delivery-error-handling.adoc[This page] proposes a simple strategy for RemoteDelivery error handling.
 ** xref:distributed/configure/collecting-contacts.adoc[This page] documents contact collection
-** xref:distributed/configure/collecting-events.adoc[This page] documents event collection
\ No newline at end of file
+** xref:distributed/configure/collecting-events.adoc[This page] documents event collection
+** xref:distributed/configure/dsn.adoc[this page] specified how to support SMTP Delivery Submission Notification (link:https://tools.ietf.org/html/rfc3461[RFC-3461])
diff --git a/docs/modules/servers/pages/distributed/configure/smtp-hooks.adoc b/docs/modules/servers/pages/distributed/configure/smtp-hooks.adoc
index cca17f6..441a703 100644
--- a/docs/modules/servers/pages/distributed/configure/smtp-hooks.adoc
+++ b/docs/modules/servers/pages/distributed/configure/smtp-hooks.adoc
@@ -57,6 +57,29 @@ Example configuration:
 </handlerchain>
 ....
 
+== DSN hooks
+
+The Distributed server has optional support for DSN (link:https://tools.ietf.org/html/rfc3461[RFC-3461])
+
+Please read carefully xref:distributed/configure/dsn.adoc[this page].
+
+....
+<smtpserver enabled="true">
+    <...> <!-- The rest of your SMTP configuration, unchanged -->
+    <handlerchain>
+        <handler class="org.apache.james.smtpserver.dsn.DSNEhloHook"/>
+        <handler class="org.apache.james.smtpserver.dsn.DSNMailParameterHook"/>
+        <handler class="org.apache.james.smtpserver.dsn.DSNRcptParameterHook"/>
+        <handler class="org.apache.james.smtpserver.dsn.DSNMessageHook"/>
+        <...> <!-- other handlers, like: -->
+        <handler class="org.apache.james.smtpserver.CoreCmdHandlerLoader"/> <!-- for instance -->
+    </handlerchain>
+</smtpserver>
+....
+
+Note that a specific configuration of xref:distributed/configure/mailetcontainer.adoc[mailetcontainer.xml] is
+required as well to be spec compliant.
+
 == MailPriorityHandler
 
 This handler can add a hint to the mail which tells the MailQueue which email should get processed first.


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org


[james-project] 02/02: JAMES-3491 Transport independant JMAP API

Posted by bt...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit d21c2975f27ff5ac79a2829c3e56701cd085b4ab
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Jan 26 14:30:01 2021 +0700

    JAMES-3491 Transport independant JMAP API
    
    Allows reuse over HTTP or WebSocket transport
---
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  96 +++-----------------
 .../org/apache/james/jmap/routes/JmapApi.scala     | 101 +++++++++++++++++++++
 .../james/jmap/routes/JMAPApiRoutesTest.scala      |   4 +-
 3 files changed, 117 insertions(+), 84 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index faf7742..2bc48bb 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -31,43 +31,29 @@ import javax.inject.{Inject, Named}
 import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE
 import org.apache.james.jmap.JMAPUrls.JMAP
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
-import org.apache.james.jmap.core.Invocation.MethodName
 import org.apache.james.jmap.core.ProblemDetails.{notJSONProblem, notRequestProblem, unknownCapabilityProblem}
-import org.apache.james.jmap.core.{Capability, ErrorCode, Invocation, MissingCapabilityException, ProblemDetails, RequestObject, ResponseObject}
+import org.apache.james.jmap.core.{ProblemDetails, RequestObject}
 import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.http.{Authenticator, UserProvisioning}
 import org.apache.james.jmap.json.ResponseSerializer
-import org.apache.james.jmap.method.{InvocationWithContext, Method}
 import org.apache.james.jmap.routes.DownloadRoutes.LOGGER
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.mailbox.MailboxSession
 import org.slf4j.{Logger, LoggerFactory}
 import play.api.libs.json.{JsError, JsSuccess}
-import reactor.core.publisher.{Flux, Mono}
-import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.publisher.Mono
+import reactor.core.scala.publisher.SMono
 import reactor.core.scheduler.Schedulers
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
 
-import scala.jdk.CollectionConverters._
-
 object JMAPApiRoutes {
   val LOGGER: Logger = LoggerFactory.getLogger(classOf[JMAPApiRoutes])
 }
 
-class JMAPApiRoutes (val authenticator: Authenticator,
+class JMAPApiRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator: Authenticator,
                      userProvisioner: UserProvisioning,
-                     methods: Set[Method],
-                     defaultCapabilities: Set[Capability]) extends JMAPRoutes {
-
-  private val methodsByName: Map[MethodName, Method] = methods.map(method => method.methodName -> method).toMap
-
-  @Inject
-  def this(@Named(InjectionKeys.RFC_8621) authenticator: Authenticator,
-           userProvisioner: UserProvisioning,
-           javaMethods: java.util.Set[Method], supportedCapabilities: java.util.Set[Capability]) {
-    this(authenticator, userProvisioner, javaMethods.asScala.toSet, supportedCapabilities.asScala.toSet)
-  }
+                     jmapApi: JMAPApi) extends JMAPRoutes {
 
   override def routes(): stream.Stream[JMAPRoute] = Stream.of(
     JMAPRoute.builder
@@ -108,69 +94,15 @@ class JMAPApiRoutes (val authenticator: Authenticator,
 
   private def process(requestObject: RequestObject,
                       httpServerResponse: HttpServerResponse,
-                      mailboxSession: MailboxSession): SMono[Void] = {
-    val processingContext: ProcessingContext = ProcessingContext(Map.empty, Map.empty)
-    val unsupportedCapabilities = requestObject.using.toSet -- defaultCapabilities.map(_.identifier())
-    val capabilities: Set[CapabilityIdentifier] = requestObject.using.toSet
-
-    if (unsupportedCapabilities.nonEmpty) {
-      SMono.raiseError(UnsupportedCapabilitiesException(unsupportedCapabilities))
-    } else {
-      processSequentiallyAndUpdateContext(requestObject, mailboxSession, processingContext, capabilities)
-        .flatMap((invocations : Seq[InvocationWithContext]) =>
-          SMono.fromPublisher(httpServerResponse.status(OK)
-            .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
-            .sendString(
-              SMono.fromCallable(() =>
-                ResponseSerializer.serialize(ResponseObject(ResponseObject.SESSION_STATE, invocations.map(_.invocation))).toString),
-              StandardCharsets.UTF_8)
-            .`then`())
-        )
-    }
-  }
-
-  private def processSequentiallyAndUpdateContext(requestObject: RequestObject, mailboxSession: MailboxSession, processingContext: ProcessingContext, capabilities: Set[CapabilityIdentifier]): SMono[Seq[(InvocationWithContext)]] = {
-    SFlux.fromIterable(requestObject.methodCalls)
-      .foldLeft(List[SFlux[InvocationWithContext]]())((acc, elem) => {
-        val lastProcessingContext: SMono[ProcessingContext] = acc.headOption
-          .map(last => SMono.fromPublisher(Flux.from(last.map(_.processingContext)).last()))
-          .getOrElse(SMono.just(processingContext))
-        val invocation: SFlux[InvocationWithContext] = lastProcessingContext.flatMapMany(context => process(capabilities, mailboxSession, InvocationWithContext(elem, context)))
-        invocation.cache() :: acc
-      })
-      .map(_.reverse)
-      .flatMap(list => SFlux.fromIterable(list)
-        .concatMap(e => e)
-        .collectSeq())
-  }
-
-  private def process(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession, invocation: InvocationWithContext) : SFlux[InvocationWithContext] =
-    SFlux.fromPublisher(
-      invocation.processingContext.resolveBackReferences(invocation.invocation) match {
-        case Left(e) => SFlux.just[InvocationWithContext](InvocationWithContext(Invocation.error(
-          errorCode = ErrorCode.InvalidResultReference,
-          description = s"Failed resolving back-reference: ${e.message}",
-          methodCallId = invocation.invocation.methodCallId), invocation.processingContext))
-        case Right(resolvedInvocation) => processMethodWithMatchName(capabilities, InvocationWithContext(resolvedInvocation, invocation.processingContext), mailboxSession)
-          .map(_.recordInvocation)
-      })
-
-  private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): SFlux[InvocationWithContext] =
-    methodsByName.get(invocation.invocation.methodName)
-      .map(method => validateCapabilities(capabilities, method.requiredCapabilities)
-        .fold(e => SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, e.description, invocation.invocation.methodCallId), invocation.processingContext)),
-          _ => SFlux.fromPublisher(method.process(capabilities, invocation, mailboxSession))))
-      .getOrElse(SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, invocation.invocation.methodCallId), invocation.processingContext)))
-      .onErrorResume(throwable => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.ServerFail, throwable.getMessage, invocation.invocation.methodCallId), invocation.processingContext)))
-
-  private def validateCapabilities(capabilities: Set[CapabilityIdentifier], requiredCapabilities: Set[CapabilityIdentifier]): Either[MissingCapabilityException, Unit] = {
-    val missingCapabilities = requiredCapabilities -- capabilities
-    if (missingCapabilities.nonEmpty) {
-      Left(MissingCapabilityException(s"Missing capability(ies): ${missingCapabilities.mkString(", ")}"))
-    } else {
-      Right()
-    }
-  }
+                      mailboxSession: MailboxSession): SMono[Void] =
+    jmapApi.process(requestObject, mailboxSession)
+      .flatMap(responseObject => SMono.fromPublisher(httpServerResponse.status(OK)
+        .header(CONTENT_TYPE, JSON_CONTENT_TYPE)
+        .sendString(
+          SMono.fromCallable(() =>
+            ResponseSerializer.serialize(responseObject).toString),
+          StandardCharsets.UTF_8)
+        .`then`()))
 
   private def handleError(throwable: Throwable, response: HttpServerResponse): SMono[Void] = throwable match {
     case exception: IllegalArgumentException => respondDetails(response,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JmapApi.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JmapApi.scala
new file mode 100644
index 0000000..a8ca359
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JmapApi.scala
@@ -0,0 +1,101 @@
+/****************************************************************
+ * 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.routes
+
+import javax.inject.Inject
+import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
+import org.apache.james.jmap.core.Invocation.MethodName
+import org.apache.james.jmap.core.{Capability, DefaultCapabilities, ErrorCode, Invocation, MissingCapabilityException, RequestObject, ResponseObject}
+import org.apache.james.jmap.method.{InvocationWithContext, Method}
+import org.apache.james.mailbox.MailboxSession
+import org.slf4j.{Logger, LoggerFactory}
+import reactor.core.publisher.Flux
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.jdk.CollectionConverters._
+
+object JMAPApi {
+  val LOGGER: Logger = LoggerFactory.getLogger(classOf[JMAPApi])
+}
+
+class JMAPApi (methods: Set[Method], defaultCapabilities: Set[Capability]) {
+
+  private val methodsByName: Map[MethodName, Method] = methods.map(method => method.methodName -> method).toMap
+
+  @Inject
+  def this(javaMethods: java.util.Set[Method], supportedCapabilities: java.util.Set[Capability]) {
+    this(javaMethods.asScala.toSet, supportedCapabilities.asScala.toSet)
+  }
+
+  def process(requestObject: RequestObject,
+              mailboxSession: MailboxSession): SMono[ResponseObject] = {
+    val processingContext: ProcessingContext = ProcessingContext(Map.empty, Map.empty)
+    val unsupportedCapabilities = requestObject.using.toSet -- defaultCapabilities.map(_.identifier())
+    val capabilities: Set[CapabilityIdentifier] = requestObject.using.toSet
+
+    if (unsupportedCapabilities.nonEmpty) {
+      SMono.raiseError(UnsupportedCapabilitiesException(unsupportedCapabilities))
+    } else {
+      processSequentiallyAndUpdateContext(requestObject, mailboxSession, processingContext, capabilities)
+        .map(invocations => ResponseObject(ResponseObject.SESSION_STATE, invocations.map(_.invocation)))
+    }
+  }
+
+  private def processSequentiallyAndUpdateContext(requestObject: RequestObject, mailboxSession: MailboxSession, processingContext: ProcessingContext, capabilities: Set[CapabilityIdentifier]): SMono[Seq[(InvocationWithContext)]] =
+    SFlux.fromIterable(requestObject.methodCalls)
+      .foldLeft(List[SFlux[InvocationWithContext]]())((acc, elem) => {
+        val lastProcessingContext: SMono[ProcessingContext] = acc.headOption
+          .map(last => SMono.fromPublisher(Flux.from(last.map(_.processingContext)).last()))
+          .getOrElse(SMono.just(processingContext))
+        val invocation: SFlux[InvocationWithContext] = lastProcessingContext.flatMapMany(context => process(capabilities, mailboxSession, InvocationWithContext(elem, context)))
+        invocation.cache() :: acc
+      })
+      .map(_.reverse)
+      .flatMap(list => SFlux.fromIterable(list)
+        .concatMap(e => e)
+        .collectSeq())
+
+  private def process(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession, invocation: InvocationWithContext) : SFlux[InvocationWithContext] =
+    SFlux.fromPublisher(
+      invocation.processingContext.resolveBackReferences(invocation.invocation) match {
+        case Left(e) => SFlux.just[InvocationWithContext](InvocationWithContext(Invocation.error(
+          errorCode = ErrorCode.InvalidResultReference,
+          description = s"Failed resolving back-reference: ${e.message}",
+          methodCallId = invocation.invocation.methodCallId), invocation.processingContext))
+        case Right(resolvedInvocation) => processMethodWithMatchName(capabilities, InvocationWithContext(resolvedInvocation, invocation.processingContext), mailboxSession)
+          .map(_.recordInvocation)
+      })
+
+  private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): SFlux[InvocationWithContext] =
+    methodsByName.get(invocation.invocation.methodName)
+      .map(method => validateCapabilities(capabilities, method.requiredCapabilities)
+        .fold(e => SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, e.description, invocation.invocation.methodCallId), invocation.processingContext)),
+          _ => SFlux.fromPublisher(method.process(capabilities, invocation, mailboxSession))))
+      .getOrElse(SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, invocation.invocation.methodCallId), invocation.processingContext)))
+      .onErrorResume(throwable => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.ServerFail, throwable.getMessage, invocation.invocation.methodCallId), invocation.processingContext)))
+
+  private def validateCapabilities(capabilities: Set[CapabilityIdentifier], requiredCapabilities: Set[CapabilityIdentifier]): Either[MissingCapabilityException, Unit] = {
+    val missingCapabilities = requiredCapabilities -- capabilities
+    if (missingCapabilities.nonEmpty) {
+      Left(MissingCapabilityException(s"Missing capability(ies): ${missingCapabilities.mkString(", ")}"))
+    } else {
+      Right()
+    }
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
index fece846..61faf1b 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/JMAPApiRoutesTest.scala
@@ -78,7 +78,7 @@ object JMAPApiRoutesTest {
   private val userProvisionner: UserProvisioning = new UserProvisioning(usersRepository, new RecordingMetricFactory)
   private val JMAP_METHODS: Set[Method] = Set(new CoreEchoMethod)
 
-  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, JMAP_METHODS, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet)
+  private val JMAP_API_ROUTE: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, new JMAPApi(JMAP_METHODS, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet))
   private val ROUTES_HANDLER: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, JMAP_API_ROUTE))
 
   private val userBase64String: String = Base64.getEncoder.encodeToString("user1:password".getBytes(StandardCharsets.UTF_8))
@@ -442,7 +442,7 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
     when(mockCoreEchoMethod.requiredCapabilities).thenReturn(Set(JMAP_CORE))
 
     val methods: Set[Method] = Set(mockCoreEchoMethod)
-    val apiRoute: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, methods, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet)
+    val apiRoute: JMAPApiRoutes = new JMAPApiRoutes(AUTHENTICATOR, userProvisionner, new JMAPApi(methods, DefaultCapabilities.supported(JmapRfc8621Configuration.UPLOAD_LIMIT_30_MB).capabilities.toSet))
     val routesHandler: ImmutableSet[JMAPRoutesHandler] = ImmutableSet.of(new JMAPRoutesHandler(Version.RFC8621, apiRoute))
 
     val versionParser: VersionParser = new VersionParser(SUPPORTED_VERSIONS, JMAPConfiguration.DEFAULT)


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org