You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2020/08/19 07:38:31 UTC

[james-project] 02/11: JAMES-3356 Allow the use of MailboxCreationId as arguments of destroy

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 8b360fa6a6b63c245327c45ce52f9dd426f135d8
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Aug 13 17:47:02 2020 +0700

    JAMES-3356 Allow the use of MailboxCreationId as arguments of destroy
    
    JMAP mailbox ids prefixed by # references previously created records within the same request. The server needs
    to resolve that mailboxId, and replace the given creationId with the mailboxId it did allocate.
    
    In order to do so, we need to record as part of the processing the ids allocated to the creationId.
---
 .../contract/MailboxSetMethodContract.scala        | 229 ++++++++++++++++++++-
 .../org/apache/james/jmap/json/Serializer.scala    |   7 +
 .../apache/james/jmap/method/CoreEchoMethod.scala  |   3 +-
 .../james/jmap/method/MailboxGetMethod.scala       |   3 +-
 .../james/jmap/method/MailboxSetMethod.scala       |  91 +++++---
 .../org/apache/james/jmap/method/Method.scala      |   3 +-
 .../scala/org/apache/james/jmap/model/Id.scala     |  11 +-
 .../apache/james/jmap/model/RequestObject.scala    |  11 +-
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |   9 +-
 .../james/jmap/routes/ProcessingContext.scala      |  52 +++++
 .../james/jmap/method/CoreEchoMethodTest.scala     |   5 +-
 .../james/jmap/routes/JMAPApiRoutesTest.scala      |   2 +-
 12 files changed, 387 insertions(+), 39 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
index 8a67f7a..becc612 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
@@ -38,6 +38,7 @@ import org.apache.james.mime4j.dom.Message
 import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
 import org.apache.james.utils.DataProbeImpl
 import org.assertj.core.api.Assertions
+import org.hamcrest.Matchers.{equalTo, hasSize}
 import org.junit.jupiter.api.{BeforeEach, Disabled, Test}
 
 trait MailboxSetMethodContract {
@@ -1132,6 +1133,7 @@ trait MailboxSetMethodContract {
         .body
         .asString
 
+    val message: String = "invalid is not a mailboxId: For input string: \\\"invalid\\\""
     assertThatJson(response).isEqualTo(
       s"""{
          |  "sessionState": "75128aab4b1b",
@@ -1143,11 +1145,236 @@ trait MailboxSetMethodContract {
          |      "notDestroyed": {
          |        "invalid": {
          |          "type": "invalidArguments",
-         |          "description": "invalid is not a mailboxId"
+         |          "description": "$message"
          |        }
          |      }
          |    },
          |    "c1"]]
          |}""".stripMargin)
   }
+
+  @Test
+  def deleteShouldAcceptCreationIdsWithinTheSameRequest(): Unit = {
+    val request =
+      s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |       ["Mailbox/set",
+        |           {
+        |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "create": {
+        |                    "C42": {
+        |                      "name": "myMailbox"
+        |                    }
+        |                }
+        |           },
+        |    "c1"],
+        |       ["Mailbox/set",
+        |           {
+        |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "destroy": ["#C42"]
+        |           },
+        |    "c2"]
+        |   ]
+        |}
+        |""".stripMargin
+
+     `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(request)
+      .when
+        .post
+      .`then`
+        .log().ifValidationFails()
+        .statusCode(SC_OK)
+        .contentType(JSON)
+       // We need to limit ourself to simple body assertions in order not to infer id allocation
+       .body("methodResponses[0][1].created.C42.totalThreads", equalTo(0))
+       .body("methodResponses[1][1].destroyed", hasSize(1))
+  }
+
+  @Test
+  def creationIdReferencesShouldFailWhenWrongOrder(server: GuiceJamesServer): Unit = {
+    val request =
+      s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |      ["Mailbox/set",
+        |          {
+        |               "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |               "destroy": ["#C42"]
+        |          },
+        |   "c2"],
+        |       ["Mailbox/set",
+        |           {
+        |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |                "create": {
+        |                    "C42": {
+        |                      "name": "myMailbox"
+        |                    }
+        |                }
+        |           },
+        |    "c1"]
+        |   ]
+        |}
+        |""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    val mailboxId: String = server.getProbe(classOf[MailboxProbeImpl])
+      .getMailboxId("#private", BOB.asString(), "myMailbox")
+      .serialize()
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |	"sessionState": "75128aab4b1b",
+         |	"methodResponses": [
+         |		["Mailbox/set", {
+         |			"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |			"newState": "000001",
+         |			"notDestroyed": {
+         |				"#C42": {
+         |					"type": "invalidArguments",
+         |					"description": "#C42 is not a mailboxId: ClientId(#C42) was not used in previously defined creationIds"
+         |				}
+         |			}
+         |		}, "c2"],
+         |		["Mailbox/set", {
+         |			"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |			"newState": "000001",
+         |			"created": {
+         |				"C42": {
+         |					"id": "$mailboxId",
+         |					"totalEmails": 0,
+         |					"unreadEmails": 0,
+         |					"totalThreads": 0,
+         |					"unreadThreads": 0,
+         |					"myRights": {
+         |						"mayReadItems": true,
+         |						"mayAddItems": true,
+         |						"mayRemoveItems": true,
+         |						"maySetSeen": true,
+         |						"maySetKeywords": true,
+         |						"mayCreateChild": true,
+         |						"mayRename": true,
+         |						"mayDelete": true,
+         |						"maySubmit": true
+         |					},
+         |					"isSubscribed": true
+         |				}
+         |			}
+         |		}, "c1"]
+         |	]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def creationIdReferencesShouldFailWhenNone(server: GuiceJamesServer): Unit = {
+    val request =
+      s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |      ["Mailbox/set",
+        |          {
+        |               "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |               "destroy": ["#C42"]
+        |          },
+        |   "c2"]
+        |   ]
+        |}
+        |""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |	"sessionState": "75128aab4b1b",
+         |	"methodResponses": [
+         |		["Mailbox/set", {
+         |			"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |			"newState": "000001",
+         |			"notDestroyed": {
+         |				"#C42": {
+         |					"type": "invalidArguments",
+         |					"description": "#C42 is not a mailboxId: ClientId(#C42) was not used in previously defined creationIds"
+         |				}
+         |			}
+         |		}, "c2"]
+         |	]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def emptyCreationIdReferencesShouldFail(): Unit = {
+    val request =
+      s"""
+        |{
+        |   "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+        |   "methodCalls": [
+        |      ["Mailbox/set",
+        |          {
+        |               "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |               "destroy": ["#"]
+        |          },
+        |   "c2"]
+        |   ]
+        |}
+        |""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    val message = "# is not a mailboxId: Left predicate of ((!(0 < 1) && !(0 > 255)) && \\\"\\\".matches(\\\"^[#a-zA-Z0-9-_]*$\\\")) failed: Predicate taking size() = 0 failed: Left predicate of (!(0 < 1) && !(0 > 255)) failed: Predicate (0 < 1) did not fail."
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |	"sessionState": "75128aab4b1b",
+         |	"methodResponses": [
+         |		["Mailbox/set", {
+         |			"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |			"newState": "000001",
+         |			"notDestroyed": {
+         |				"#": {
+         |					"type": "invalidArguments",
+         |					"description": "$message"
+         |				}
+         |			}
+         |		}, "c2"]
+         |	]
+         |}""".stripMargin)
+  }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
index f15f1ce..5ba43fb 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/Serializer.scala
@@ -284,6 +284,13 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
         jsObject.+(mailboxId.serialize(), mailboxSetErrorWrites.writes(mailboxSetError))
       })
     }
+  private implicit def mailboxMapSetErrorWritesByClientId: Writes[Map[ClientId, MailboxSetError]] =
+    (m: Map[ClientId, MailboxSetError]) => {
+      m.foldLeft(JsObject.empty)((jsObject, kv) => {
+        val (clientId: ClientId, mailboxSetError: MailboxSetError) = kv
+        jsObject.+(clientId.value, mailboxSetErrorWrites.writes(mailboxSetError))
+      })
+    }
 
   private implicit def mailboxMapCreationResponseWrites: Writes[Map[MailboxCreationId, MailboxCreationResponse]] =
     (m: Map[MailboxCreationId, MailboxCreationResponse]) => {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/CoreEchoMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/CoreEchoMethod.scala
index d1dd286..9e9c432 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/CoreEchoMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/CoreEchoMethod.scala
@@ -23,6 +23,7 @@ import eu.timepit.refined.auto._
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation
 import org.apache.james.jmap.model.Invocation.MethodName
+import org.apache.james.jmap.routes.ProcessingContext
 import org.apache.james.mailbox.MailboxSession
 import org.reactivestreams.Publisher
 import reactor.core.scala.publisher.SMono
@@ -30,5 +31,5 @@ import reactor.core.scala.publisher.SMono
 class CoreEchoMethod extends Method {
   override val methodName = MethodName("Core/echo")
 
-  override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession): Publisher[Invocation] = SMono.just(invocation)
+  override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession, processingContext: ProcessingContext): Publisher[Invocation] = SMono.just(invocation)
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
index ed582f4..7e66b5e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
@@ -27,6 +27,7 @@ import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.model.State.INSTANCE
 import org.apache.james.jmap.model.{CapabilityIdentifier, ErrorCode, Invocation, MailboxFactory}
+import org.apache.james.jmap.routes.ProcessingContext
 import org.apache.james.jmap.utils.quotas.{QuotaLoader, QuotaLoaderWithPreloadedDefaultFactory}
 import org.apache.james.mailbox.exception.MailboxNotFoundException
 import org.apache.james.mailbox.model.search.MailboxQuery
@@ -54,7 +55,7 @@ class MailboxGetMethod @Inject() (serializer: Serializer,
     def merge(other: MailboxGetResults): MailboxGetResults = MailboxGetResults(this.mailboxes ++ other.mailboxes, this.notFound.merge(other.notFound))
   }
 
-  override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession): Publisher[Invocation] = {
+  override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession, processingContext: ProcessingContext): Publisher[Invocation] = {
     metricFactory.decoratePublisherWithTimerMetricLogP99(JMAP_RFC8621_PREFIX + methodName.value,
       asMailboxGetRequest(invocation.arguments)
         .flatMap(mailboxGetRequest => {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
index 44a7a76..ae3bec0 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
@@ -26,7 +26,8 @@ import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, Unparsed
 import org.apache.james.jmap.mail.{IsSubscribed, MailboxCreationRequest, MailboxCreationResponse, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, Properties, SetErrorDescription, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads}
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
-import org.apache.james.jmap.model.{Invocation, State}
+import org.apache.james.jmap.model.{ClientId, Id, Invocation, ServerId, State}
+import org.apache.james.jmap.routes.ProcessingContext
 import org.apache.james.mailbox.exception.{InsufficientRightsException, MailboxExistsException, MailboxNameException, MailboxNotFoundException}
 import org.apache.james.mailbox.model.{FetchGroup, Mailbox, MailboxId, MailboxPath, MessageRange}
 import org.apache.james.mailbox.{MailboxManager, MailboxSession}
@@ -77,7 +78,7 @@ case class DeletionFailure(mailboxId: UnparsedMailboxId, exception: Throwable) e
     case e: MailboxNotFoundException => MailboxSetError.notFound(Some(SetErrorDescription(e.getMessage)))
     case e: MailboxHasMailException => MailboxSetError.mailboxHasEmail(Some(SetErrorDescription(s"${e.mailboxId.serialize} is not empty")))
     case e: MailboxHasChildException => MailboxSetError.mailboxHasChild(Some(SetErrorDescription(s"${e.mailboxId.serialize} has child mailboxes")))
-    case e: IllegalArgumentException => MailboxSetError.invalidArgument(Some(SetErrorDescription(s"${mailboxId} is not a mailboxId")), None)
+    case e: IllegalArgumentException => MailboxSetError.invalidArgument(Some(SetErrorDescription(s"${mailboxId} is not a mailboxId: ${e.getMessage}")), None)
     case _ => MailboxSetError.serverFail(Some(SetErrorDescription(exception.getMessage)), None)
   }
 }
@@ -102,15 +103,14 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
                                  metricFactory: MetricFactory) extends Method {
   override val methodName: MethodName = MethodName("Mailbox/set")
 
-
-  override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession): Publisher[Invocation] = {
+  override def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession, processingContext: ProcessingContext): Publisher[Invocation] = {
     metricFactory.decoratePublisherWithTimerMetricLogP99(JMAP_RFC8621_PREFIX + methodName.value,
       asMailboxSetRequest(invocation.arguments)
         .flatMap(mailboxSetRequest => {
           val (unparsableCreateRequests, createRequests) = parseCreateRequests(mailboxSetRequest)
           for {
-            creationResults <- createMailboxes(mailboxSession, createRequests)
-            deletionResults <- deleteMailboxes(mailboxSession, mailboxSetRequest.destroy.getOrElse(Seq()))
+            creationResults <- createMailboxes(mailboxSession, createRequests, processingContext)
+            deletionResults <- deleteMailboxes(mailboxSession, mailboxSetRequest.destroy.getOrElse(Seq()), processingContext)
           } yield createResponse(invocation, mailboxSetRequest, unparsableCreateRequests, creationResults, deletionResults)
         }))
   }
@@ -136,18 +136,23 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
       case (path, _) => MailboxSetError.invalidArgument(Some(SetErrorDescription(s"Unknown error on property '$path'")), None)
     }
 
-  private def deleteMailboxes(mailboxSession: MailboxSession, deleteRequests: immutable.Iterable[UnparsedMailboxId]): SMono[DeletionResults] = {
+  private def deleteMailboxes(mailboxSession: MailboxSession, deleteRequests: immutable.Iterable[UnparsedMailboxId], processingContext: ProcessingContext): SMono[DeletionResults] = {
     SFlux.fromIterable(deleteRequests)
-      .flatMap(id => SMono.just(id)
-        .map(id => mailboxIdFactory.fromString(id))
-        .flatMap(mailboxId => SMono.fromCallable(() => delete(mailboxSession, mailboxId))
-          .subscribeOn(Schedulers.elastic())
-          .`then`(SMono.just[DeletionResult](DeletionSuccess(mailboxId))))
+      .flatMap(id => delete(mailboxSession, processingContext, id)
         .onErrorRecover(e => DeletionFailure(id, e)))
       .collectSeq()
       .map(DeletionResults)
   }
 
+  private def delete(mailboxSession: MailboxSession, processingContext: ProcessingContext, id: UnparsedMailboxId): SMono[DeletionResult] = {
+    processingContext.resolveMailboxId(id, mailboxIdFactory) match {
+      case Right(mailboxId) => SMono.fromCallable(() => delete(mailboxSession, mailboxId))
+        .subscribeOn(Schedulers.elastic())
+        .`then`(SMono.just[DeletionResult](DeletionSuccess(mailboxId)))
+      case Left(e) => SMono.raiseError(e)
+    }
+  }
+
   private def delete(mailboxSession: MailboxSession, id: MailboxId): Mailbox = {
     val mailbox = mailboxManager.getMailbox(id, mailboxSession)
     if (mailbox.getMessages(MessageRange.all(), FetchGroup.MINIMAL, mailboxSession).hasNext) {
@@ -159,34 +164,70 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
     mailboxManager.deleteMailbox(id, mailboxSession)
   }
 
-  private def createMailboxes(mailboxSession: MailboxSession, createRequests: immutable.Iterable[(MailboxCreationId, MailboxCreationRequest)]): SMono[CreationResults] = {
+  private def createMailboxes(mailboxSession: MailboxSession,
+                              createRequests: immutable.Iterable[(MailboxCreationId, MailboxCreationRequest)],
+                              processingContext: ProcessingContext): SMono[CreationResults] = {
     SFlux.fromIterable(createRequests).flatMap {
-      case (mailboxCreationId: MailboxCreationId, mailboxCreationRequest: MailboxCreationRequest) => {
+      case (mailboxCreationId: MailboxCreationId, mailboxCreationRequest: MailboxCreationRequest) =>
         SMono.fromCallable(() => {
-          createMailbox(mailboxSession, mailboxCreationId, mailboxCreationRequest)
+          createMailbox(mailboxSession, mailboxCreationId, mailboxCreationRequest, processingContext)
         }).subscribeOn(Schedulers.elastic())
-      }
     }
       .collectSeq()
       .map(CreationResults)
   }
 
-  private def createMailbox(mailboxSession: MailboxSession, mailboxCreationId: MailboxCreationId, mailboxCreationRequest: MailboxCreationRequest): CreationResult = {
+  private def createMailbox(mailboxSession: MailboxSession,
+                            mailboxCreationId: MailboxCreationId,
+                            mailboxCreationRequest: MailboxCreationRequest,
+                            processingContext: ProcessingContext): CreationResult = {
+    resolvePath(mailboxSession, mailboxCreationRequest)
+      .map(path => createMailbox(mailboxSession, mailboxCreationId, processingContext, path))
+      .fold(e => CreationFailure(mailboxCreationId, e), r => r)
+  }
+
+  private def createMailbox(mailboxSession: MailboxSession,
+                            mailboxCreationId: MailboxCreationId,
+                            processingContext: ProcessingContext,
+                            path: MailboxPath): CreationResult = {
     try {
-      val path: MailboxPath = if (mailboxCreationRequest.parentId.isEmpty) {
-        MailboxPath.forUser(mailboxSession.getUser, mailboxCreationRequest.name)
-      } else {
-        val parentId: MailboxId = mailboxCreationRequest.parentId.get
-        val parentPath: MailboxPath = mailboxManager.getMailbox(parentId, mailboxSession).getMailboxPath
-        parentPath.child(mailboxCreationRequest.name, mailboxSession.getPathDelimiter)
-      }
       //can safely do a get as the Optional is empty only if the mailbox name is empty which is forbidden by the type constraint on MailboxName
-      CreationSuccess(mailboxCreationId, mailboxManager.createMailbox(path, mailboxSession).get())
+      val mailboxId = mailboxManager.createMailbox(path, mailboxSession).get()
+      recordCreationIdInProcessingContext(mailboxCreationId, processingContext, mailboxId)
+      CreationSuccess(mailboxCreationId, mailboxId)
     } catch {
       case error: Exception => CreationFailure(mailboxCreationId, error)
     }
   }
 
+  private def recordCreationIdInProcessingContext(mailboxCreationId: MailboxCreationId,
+                                                  processingContext: ProcessingContext,
+                                                  mailboxId: MailboxId): Unit = {
+    for {
+      creationId <- Id.validate(mailboxCreationId)
+      serverAssignedId <- Id.validate(mailboxId.serialize())
+    } yield {
+      processingContext.recordCreatedId(ClientId(creationId), ServerId(serverAssignedId))
+    }
+  }
+
+  private def resolvePath(mailboxSession: MailboxSession,
+                          mailboxCreationRequest: MailboxCreationRequest): Either[Exception, MailboxPath] = {
+    mailboxCreationRequest.parentId
+      .map(parentId => for {
+        parentPath <- retrievePath(parentId, mailboxSession)
+      } yield {
+        parentPath.child(mailboxCreationRequest.name, mailboxSession.getPathDelimiter)
+      })
+      .getOrElse(Right(MailboxPath.forUser(mailboxSession.getUser, mailboxCreationRequest.name)))
+  }
+
+  private def retrievePath(mailboxId: MailboxId, mailboxSession: MailboxSession): Either[Exception, MailboxPath] = try {
+      Right(mailboxManager.getMailbox(mailboxId, mailboxSession).getMailboxPath)
+    } catch {
+      case e: Exception => Left(e)
+    }
+
   private def createResponse(invocation: Invocation, mailboxSetRequest: MailboxSetRequest,
                              unparsableCreateRequests: immutable.Iterable[(MailboxCreationId, MailboxSetError)],
                              creationResults: CreationResults, deletionResults: DeletionResults): Invocation = {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
index 078929e..937112b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
@@ -22,6 +22,7 @@ package org.apache.james.jmap.method
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation
 import org.apache.james.jmap.model.Invocation.MethodName
+import org.apache.james.jmap.routes.ProcessingContext
 import org.apache.james.mailbox.MailboxSession
 import org.reactivestreams.Publisher
 
@@ -30,6 +31,6 @@ trait Method {
 
   val methodName: MethodName
 
-  def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession): Publisher[Invocation]
+  def process(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession, processingContext: ProcessingContext): Publisher[Invocation]
 }
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Id.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Id.scala
index f1178ae..f532fd8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Id.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Id.scala
@@ -19,6 +19,7 @@
 
 package org.apache.james.jmap.model
 
+import eu.timepit.refined
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.boolean.And
 import eu.timepit.refined.collection.Size
@@ -26,7 +27,13 @@ import eu.timepit.refined.numeric.Interval
 import eu.timepit.refined.string.MatchesRegex
 
 object Id {
-  type Id = String Refined And[
+  type IdConstraint = And[
     Size[Interval.Closed[1, 255]],
-    MatchesRegex["^[a-zA-Z0-9-_]*$"]]
+    MatchesRegex["^[#a-zA-Z0-9-_]*$"]]
+  type Id = String Refined IdConstraint
+
+  def validate(string: String): Either[IllegalArgumentException, Id] =
+    refined.refineV[IdConstraint](string)
+      .left
+      .map(new IllegalArgumentException(_))
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/RequestObject.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/RequestObject.scala
index 12e2223..f356663 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/RequestObject.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/RequestObject.scala
@@ -22,7 +22,16 @@ package org.apache.james.jmap.model
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Id.Id
 
-final case class ClientId(value: Id)
+final case class ClientId(value: Id) {
+  def referencesPreviousCreationId: Boolean = value.value.startsWith("#")
+  def retrieveOriginalClientId: Option[Either[IllegalArgumentException, ClientId]] =
+    if (referencesPreviousCreationId) {
+      Some(Id.validate(value.value.substring(1, value.value.length))
+        .map(ClientId))
+    } else {
+      None
+    }
+}
 
 final case class ServerId(value: Id)
 
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 cfdbc2d..a3dc83f 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
@@ -112,6 +112,7 @@ class JMAPApiRoutes (val authenticator: Authenticator,
   private def process(requestObject: RequestObject,
                       httpServerResponse: HttpServerResponse,
                       mailboxSession: MailboxSession): SMono[Void] = {
+    val processingContext: ProcessingContext = new ProcessingContext
     val unsupportedCapabilities = requestObject.using.toSet -- DefaultCapabilities.SUPPORTED.ids
 
     if (unsupportedCapabilities.nonEmpty) {
@@ -119,8 +120,8 @@ class JMAPApiRoutes (val authenticator: Authenticator,
     } else {
       requestObject
         .methodCalls
-        .map(invocation => this.processMethodWithMatchName(requestObject.using.toSet, invocation, mailboxSession))
-        .foldLeft(SFlux.empty[Invocation]) { (flux: SFlux[Invocation], mono: SMono[Invocation]) => flux.mergeWith(mono) }
+        .map(invocation => this.processMethodWithMatchName(requestObject.using.toSet, invocation, mailboxSession, processingContext))
+        .foldLeft(SFlux.empty[Invocation]) { (flux: SFlux[Invocation], mono: SMono[Invocation]) => flux.concatWith(mono) }
         .collectSeq()
         .flatMap((invocations: Seq[Invocation]) =>
           SMono.fromPublisher(httpServerResponse.status(OK)
@@ -134,9 +135,9 @@ class JMAPApiRoutes (val authenticator: Authenticator,
     }
   }
 
-  private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession): SMono[Invocation] =
+  private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: Invocation, mailboxSession: MailboxSession, processingContext: ProcessingContext): SMono[Invocation] =
     SMono.justOrEmpty(methodsByName.get(invocation.methodName))
-      .flatMap(method => SMono.fromPublisher(method.process(capabilities, invocation, mailboxSession)))
+      .flatMap(method => SMono.fromPublisher(method.process(capabilities, invocation, mailboxSession, processingContext)))
       .onErrorResume(throwable => SMono.just(Invocation.error(ErrorCode.ServerFail, throwable.getMessage, invocation.methodCallId)))
       .switchIfEmpty(SMono.just(Invocation.error(ErrorCode.UnknownMethod, invocation.methodCallId)))
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/ProcessingContext.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/ProcessingContext.scala
new file mode 100644
index 0000000..e2e0e9a
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/ProcessingContext.scala
@@ -0,0 +1,52 @@
+/****************************************************************
+ * 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 org.apache.james.jmap.mail.MailboxSetRequest.UnparsedMailboxId
+import org.apache.james.jmap.model.{ClientId, Id, ServerId}
+import org.apache.james.mailbox.model.MailboxId
+
+import scala.collection.mutable
+
+class ProcessingContext {
+ private val creationIds: mutable.Map[ClientId, ServerId] = mutable.Map()
+
+ def recordCreatedId(clientId: ClientId, serverId: ServerId): Unit = creationIds.put(clientId, serverId)
+ private def retrieveServerId(clientId: ClientId): Option[ServerId] = creationIds.get(clientId)
+
+ def resolveMailboxId(unparsedMailboxId: UnparsedMailboxId, mailboxIdFactory: MailboxId.Factory): Either[IllegalArgumentException, MailboxId] =
+  Id.validate(unparsedMailboxId.value)
+      .flatMap(id => resolveServerId(ClientId(id)))
+      .flatMap(serverId => parseMailboxId(mailboxIdFactory, serverId))
+
+ private def parseMailboxId(mailboxIdFactory: MailboxId.Factory, serverId: ServerId) =
+  try {
+   Right(mailboxIdFactory.fromString(serverId.value.value))
+  } catch {
+   case e: IllegalArgumentException => Left(e)
+  }
+
+ private def resolveServerId(id: ClientId): Either[IllegalArgumentException, ServerId] =
+  id.retrieveOriginalClientId
+    .map(maybePreviousClientId => maybePreviousClientId.flatMap(previousClientId => retrieveServerId(previousClientId)
+      .map(serverId => Right(serverId))
+      .getOrElse(Left[IllegalArgumentException, ServerId](new IllegalArgumentException(s"$id was not used in previously defined creationIds")))))
+    .getOrElse(Right(ServerId(id.value)))
+}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/method/CoreEchoMethodTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/method/CoreEchoMethodTest.scala
index 9415c16..1f7d859 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/method/CoreEchoMethodTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/method/CoreEchoMethodTest.scala
@@ -21,6 +21,7 @@ package org.apache.james.jmap.method
 import org.apache.james.jmap.json.Fixture.{invocation1, invocation2}
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.{CapabilityIdentifier, Invocation}
+import org.apache.james.jmap.routes.ProcessingContext
 import org.apache.james.mailbox.MailboxSession
 import org.mockito.Mockito.mock
 import org.scalatest.matchers.should.Matchers
@@ -36,14 +37,14 @@ class CoreEchoMethodTest extends AnyWordSpec with Matchers {
     "Process" should {
       "success and return the same with parameters as the invocation request" in {
         val expectedResponse: Invocation = invocation1
-        val dataResponse = SMono.fromPublisher(echoMethod.process(capabilities, invocation1, mockedSession)).block()
+        val dataResponse = SMono.fromPublisher(echoMethod.process(capabilities, invocation1, mockedSession, new ProcessingContext)).block()
 
         dataResponse shouldBe expectedResponse
       }
 
       "success and not return anything else different than the original invocation" in {
         val wrongExpected: Invocation = invocation2
-        val dataResponse = SMono.fromPublisher(echoMethod.process(capabilities, invocation1, mockedSession)).block()
+        val dataResponse = SMono.fromPublisher(echoMethod.process(capabilities, invocation1, mockedSession, new ProcessingContext)).block()
         
         dataResponse should not be(wrongExpected)
       }
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 8c2f9b8..28a1640 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
@@ -439,7 +439,7 @@ class JMAPApiRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
     doThrow(new RuntimeException("Unexpected Exception occur, the others method may proceed normally"))
       .doCallRealMethod()
       .when(mockCoreEchoMethod)
-      .process(any[Set[CapabilityIdentifier]], any(), any())
+      .process(any[Set[CapabilityIdentifier]], any(), any(), any())
 
     when(mockCoreEchoMethod.methodName).thenReturn(MethodName("Core/echo"))
 


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