You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/09/03 03:23:54 UTC

[james-project] 12/13: JAMES-3365 Back-reference resolution at the JSON level

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

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

commit 4c992a5b8f4cec706026243d76c2b917c54b99be
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Aug 24 15:51:55 2020 +0700

    JAMES-3365 Back-reference resolution at the JSON level
---
 .../rfc8621/contract/BackReferenceContract.scala   | 403 +++++++++++++++++++++
 .../rfc8621/memory/MemoryBackReferenceTest.java}   |  31 +-
 .../org/apache/james/jmap/json/Serializer.scala    |  10 +-
 .../org/apache/james/jmap/model/Invocation.scala   |   4 +
 .../apache/james/jmap/model/ProblemDetails.scala   |   1 +
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |  20 +-
 .../james/jmap/routes/ProcessingContext.scala      | 142 +++++++-
 .../apache/james/jmap/json/BackReferenceTest.scala | 160 ++++++++
 8 files changed, 751 insertions(+), 20 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/BackReferenceContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/BackReferenceContract.scala
new file mode 100644
index 0000000..6d18ddf
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/BackReferenceContract.scala
@@ -0,0 +1,403 @@
+/****************************************************************
+ * 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.rfc8621.contract
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured._
+import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture._
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait BackReferenceContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addUser(BOB.asString, BOB_PASSWORD)
+      .addUser(ANDRE.asString, ANDRE_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def backReferenceResolvingShouldWork(): Unit = {
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(
+        s"""{
+           |  "using": [
+           |    "urn:ietf:params:jmap:core",
+           |    "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "ids": null,
+           |       "properties": ["id"]
+           |     },
+           |     "c1"],[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "properties": ["id"],
+           |       "#ids": {
+           |         "resultOf":"c1",
+           |         "name":"Mailbox/get",
+           |         "path":"list/*/id"
+           |       }
+           |     },
+           |     "c2"]]
+           |}""".stripMargin)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .extract()
+      .body()
+      .asString()
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [
+         |        [
+         |            "Mailbox/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {"id": "1"},
+         |                    {"id": "5"},
+         |                    {"id": "2"},
+         |                    {"id": "3"},
+         |                    {"id": "4"},
+         |                    {"id": "6"}
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ],
+         |        [
+         |            "Mailbox/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {"id": "1"},
+         |                    {"id": "5"},
+         |                    {"id": "2"},
+         |                    {"id": "3"},
+         |                    {"id": "4"},
+         |                    {"id": "6"}
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c2"
+         |        ]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def pathShouldBeResolvable(): Unit = {
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(
+        s"""{
+           |  "using": [
+           |    "urn:ietf:params:jmap:core",
+           |    "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "ids": null,
+           |       "properties": ["id"]
+           |     },
+           |     "c1"],[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "properties": ["id"],
+           |       "#ids": {
+           |         "resultOf":"c1",
+           |         "name":"Mailbox/get",
+           |         "path":"unknown/*/id"
+           |       }
+           |     },
+           |     "c2"]]
+           |}""".stripMargin)
+    .when
+      .post.prettyPeek()
+    .`then`
+      .statusCode(SC_OK)
+      .extract()
+      .body()
+      .asString()
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [
+         |        [
+         |            "Mailbox/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {"id": "1"},
+         |                    {"id": "5"},
+         |                    {"id": "2"},
+         |                    {"id": "3"},
+         |                    {"id": "4"},
+         |                    {"id": "6"}
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ],
+         |        [
+         |            "error",
+         |            {
+         |                "type": "invalidResultReference",
+         |                "description": "Failed resolving back-reference: List((,List(JsonValidationError(List(Expected path unknown was missing),ArraySeq()))))"
+         |            },
+         |            "c2"
+         |        ]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def wildcardRequiresAnArray(): Unit = {
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(
+        s"""{
+           |  "using": [
+           |    "urn:ietf:params:jmap:core",
+           |    "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "ids": null,
+           |       "properties": ["id"]
+           |     },
+           |     "c1"],[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "properties": ["id"],
+           |       "#ids": {
+           |         "resultOf":"c1",
+           |         "name":"Mailbox/get",
+           |         "path":"*/*/id"
+           |       }
+           |     },
+           |     "c2"]]
+           |}""".stripMargin)
+    .when
+      .post.prettyPeek()
+    .`then`
+      .statusCode(SC_OK)
+      .extract()
+      .body()
+      .asString()
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [
+         |        [
+         |            "Mailbox/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {"id": "1"},
+         |                    {"id": "5"},
+         |                    {"id": "2"},
+         |                    {"id": "3"},
+         |                    {"id": "4"},
+         |                    {"id": "6"}
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ],
+         |        [
+         |            "error",
+         |            {
+         |                "type": "invalidResultReference",
+         |                "description": "Failed resolving back-reference: List((,List(JsonValidationError(List(Expecting an array),ArraySeq()))))"
+         |            },
+         |            "c2"
+         |        ]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def resolvedBackReferenceShouldHaveTheRightMethodName(): Unit = {
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(
+        s"""{
+           |  "using": [
+           |    "urn:ietf:params:jmap:core",
+           |    "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "ids": null,
+           |       "properties": ["id"]
+           |     },
+           |     "c1"],[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "properties": ["id"],
+           |       "#ids": {
+           |         "resultOf":"c1",
+           |         "name":"Mailbox/set",
+           |         "path":"list/*/id"
+           |       }
+           |     },
+           |     "c2"]]
+           |}""".stripMargin)
+    .when
+      .post.prettyPeek()
+    .`then`
+      .statusCode(SC_OK)
+      .extract()
+      .body()
+      .asString()
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [
+         |        [
+         |            "Mailbox/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {"id": "1"},
+         |                    {"id": "5"},
+         |                    {"id": "2"},
+         |                    {"id": "3"},
+         |                    {"id": "4"},
+         |                    {"id": "6"}
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ],
+         |        [
+         |            "error",
+         |            {
+         |                "type": "invalidResultReference",
+         |                "description": "Failed resolving back-reference: List((,List(JsonValidationError(List(MethodCallId(c1) references a MethodName(Mailbox/get) method),ArraySeq()))))"
+         |            },
+         |            "c2"
+         |        ]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def resolvingAnUnexistingMethodCallIdShouldFail(): Unit = {
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(
+        s"""{
+           |  "using": [
+           |    "urn:ietf:params:jmap:core",
+           |    "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "ids": null,
+           |       "properties": ["id"]
+           |     },
+           |     "c1"],[
+           |     "Mailbox/get",
+           |     {
+           |       "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |       "properties": ["id"],
+           |       "#ids": {
+           |         "resultOf":"c42",
+           |         "name":"Mailbox/get",
+           |         "path":"list/*/id"
+           |       }
+           |     },
+           |     "c2"]]
+           |}""".stripMargin)
+    .when
+      .post.prettyPeek()
+    .`then`
+      .statusCode(SC_OK)
+      .extract()
+      .body()
+      .asString()
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [
+         |        [
+         |            "Mailbox/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {"id": "1"},
+         |                    {"id": "5"},
+         |                    {"id": "2"},
+         |                    {"id": "3"},
+         |                    {"id": "4"},
+         |                    {"id": "6"}
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ],
+         |        [
+         |            "error",
+         |            {
+         |                "type": "invalidResultReference",
+         |                "description": "Failed resolving back-reference: List((,List(JsonValidationError(List(Back reference could not be resolved),ArraySeq()))))"
+         |            },
+         |            "c2"
+         |        ]
+         |    ]
+         |}""".stripMargin)
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ProblemDetails.scala b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryBackReferenceTest.java
similarity index 55%
copy from server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ProblemDetails.scala
copy to server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryBackReferenceTest.java
index 4bfb65c..5be999a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ProblemDetails.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryBackReferenceTest.java
@@ -16,22 +16,23 @@
  * specific language governing permissions and limitations      *
  * under the License.                                           *
  ****************************************************************/
-package org.apache.james.jmap.model
 
-import eu.timepit.refined.auto._
-import org.apache.http.HttpStatus.SC_BAD_REQUEST
-import org.apache.james.jmap.model.RequestLevelErrorType.ErrorTypeIdentifier
-import org.apache.james.jmap.model.StatusCode.ErrorStatus
+package org.apache.james.jmap.rfc8621.memory;
 
-/**
- * Problem Details for HTTP APIs within the JMAP context
- * https://tools.ietf.org/html/rfc7807
- * see https://jmap.io/spec-core.html#errors
- */
-case class ProblemDetails(`type`: ErrorTypeIdentifier, status: ErrorStatus, limit: Option[String], detail: String)
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
 
-object ProblemDetails {
-  def notRequestProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.NOT_REQUEST, SC_BAD_REQUEST, None, message)
-  def notJSONProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.NOT_JSON, SC_BAD_REQUEST, None, message)
-  def unknownCapabilityProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.UNKNOWN_CAPABILITY, SC_BAD_REQUEST, None, message)
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.BackReferenceContract;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+public class MemoryBackReferenceTest implements BackReferenceContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+        .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+            .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
 }
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 17591a5..f93fc8e 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
@@ -35,6 +35,7 @@ import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodCallId, MethodName}
 import org.apache.james.jmap.model.SetError.SetErrorDescription
 import org.apache.james.jmap.model.{Account, Invocation, Session, _}
+import org.apache.james.jmap.routes.{BackReference, JsonPath}
 import org.apache.james.mailbox.Role
 import org.apache.james.mailbox.model.MailboxACL.{Right => JavaRight}
 import org.apache.james.mailbox.model.{MailboxACL, MailboxId}
@@ -231,8 +232,12 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
 
   private implicit val idsRead: Reads[Ids] = Json.valueReads[Ids]
   private implicit val propertiesRead: Reads[Properties] = Json.valueReads[Properties]
+  private implicit val jsonPathReadRead: Reads[JsonPath] = {
+    case JsString(path) => JsSuccess(JsonPath.parse(path))
+    case _ => JsError("JsonPath objects are represented by JsonString")
+  }
   private implicit val mailboxGetRequest: Reads[MailboxGetRequest] = Json.reads[MailboxGetRequest]
-
+  private implicit val backReferenceReads: Reads[BackReference] = Json.reads[BackReference]
 
   private implicit val mailboxRemoveEmailsOnDestroy: Reads[RemoveEmailsOnDestroy] = Json.valueFormat[RemoveEmailsOnDestroy]
   implicit val mailboxCreationRequest: Reads[MailboxCreationRequest] = Json.reads[MailboxCreationRequest]
@@ -390,4 +395,7 @@ class Serializer @Inject() (mailboxIdFactory: MailboxId.Factory) {
   def deserializeVacationResponseGetRequest(input: String): JsResult[VacationResponseGetRequest] = Json.parse(input).validate[VacationResponseGetRequest]
 
   def deserializeVacationResponseGetRequest(input: JsValue): JsResult[VacationResponseGetRequest] = Json.fromJson[VacationResponseGetRequest](input)
+
+  def deserializeBackReference(input: JsValue): JsResult[BackReference] = Json.fromJson[BackReference](input)
+
 }
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
index fd75472..15a3cc2 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
@@ -65,4 +65,8 @@ object ErrorCode {
   case object AccountNotFound extends ErrorCode {
     override def code: String = "accountNotFound"
   }
+
+  case object InvalidResultReference extends ErrorCode {
+    override def code: String = "invalidResultReference"
+  }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ProblemDetails.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ProblemDetails.scala
index 4bfb65c..4e8df2d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ProblemDetails.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/ProblemDetails.scala
@@ -34,4 +34,5 @@ object ProblemDetails {
   def notRequestProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.NOT_REQUEST, SC_BAD_REQUEST, None, message)
   def notJSONProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.NOT_JSON, SC_BAD_REQUEST, None, message)
   def unknownCapabilityProblem(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.UNKNOWN_CAPABILITY, SC_BAD_REQUEST, None, message)
+  def invalidResultReference(message: String): ProblemDetails = ProblemDetails(RequestLevelErrorType.UNKNOWN_CAPABILITY, SC_BAD_REQUEST, None, message)
 }
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 a3dc83f..4f4683a 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
@@ -114,13 +114,14 @@ class JMAPApiRoutes (val authenticator: Authenticator,
                       mailboxSession: MailboxSession): SMono[Void] = {
     val processingContext: ProcessingContext = new ProcessingContext
     val unsupportedCapabilities = requestObject.using.toSet -- DefaultCapabilities.SUPPORTED.ids
+    val capabilities: Set[CapabilityIdentifier] = requestObject.using.toSet
 
     if (unsupportedCapabilities.nonEmpty) {
       SMono.raiseError(UnsupportedCapabilitiesException(unsupportedCapabilities))
     } else {
       requestObject
         .methodCalls
-        .map(invocation => this.processMethodWithMatchName(requestObject.using.toSet, invocation, mailboxSession, processingContext))
+        .map(invocation => process(capabilities, mailboxSession, processingContext, invocation))
         .foldLeft(SFlux.empty[Invocation]) { (flux: SFlux[Invocation], mono: SMono[Invocation]) => flux.concatWith(mono) }
         .collectSeq()
         .flatMap((invocations: Seq[Invocation]) =>
@@ -129,12 +130,25 @@ class JMAPApiRoutes (val authenticator: Authenticator,
             .sendString(
               SMono.fromCallable(() =>
                 serializer.serialize(ResponseObject(ResponseObject.SESSION_STATE, invocations)).toString),
-              StandardCharsets.UTF_8
-            ).`then`())
+              StandardCharsets.UTF_8)
+            .`then`())
         )
     }
   }
 
+  private def process(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession, processingContext: ProcessingContext, invocation: Invocation) = {
+    SMono.defer(() => {
+      processingContext.resolveBackReferences(serializer, invocation) match {
+        case Left(e) => SMono.just(Invocation.error(
+          errorCode = ErrorCode.InvalidResultReference,
+          description = s"Failed resolving back-reference: ${e.message}",
+          methodCallId = invocation.methodCallId))
+        case Right(resolvedInvocation) => processMethodWithMatchName(capabilities, resolvedInvocation, mailboxSession, processingContext)
+          .doOnNext(invocationResult => processingContext.recordInvocation(invocationResult))
+      }
+    })
+  }
+
   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, processingContext)))
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
index c216048..64072d8 100644
--- 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
@@ -19,20 +19,160 @@
 
 package org.apache.james.jmap.routes
 
+import org.apache.james.jmap.json.Serializer
 import org.apache.james.jmap.mail.MailboxSetRequest.UnparsedMailboxId
 import org.apache.james.jmap.mail.VacationResponse.{UnparsedVacationResponseId, VACATION_RESPONSE_ID}
 import org.apache.james.jmap.model.Id.Id
-import org.apache.james.jmap.model.{ClientId, Id, ServerId}
+import org.apache.james.jmap.model.Invocation.{Arguments, MethodCallId, MethodName}
+import org.apache.james.jmap.model.{ClientId, Id, Invocation, ServerId}
 import org.apache.james.mailbox.model.MailboxId
+import play.api.libs.json.{JsArray, JsError, JsObject, JsResult, JsSuccess, JsValue, Reads}
 
 import scala.collection.mutable
 
+sealed trait JsonPathPart
+
+case class WildcardPart() extends JsonPathPart
+
+case class PlainPart(name: String) extends JsonPathPart {
+  def read(jsValue: JsValue): JsResult[JsValue] = jsValue match {
+    case JsObject(underlying) => underlying.get(name).map(JsSuccess(_)).getOrElse(JsError(s"Expected path $name was missing"))
+    case _ => JsError("Expecting a JsObject but got a different structure")
+  }
+}
+
+object JsonPath {
+  def parse(string: String): JsonPath = JsonPath(string.split('/').toList
+    .flatMap {
+      case "" => None
+      case "*" => Some(WildcardPart())
+      case part => Some(PlainPart(part))
+    })
+}
+
+case class JsonPath(parts: List[JsonPathPart]) {
+  def evaluate(jsValue: JsValue): JsResult[JsValue] = parts match {
+    case Nil => JsSuccess(jsValue)
+    case head :: tail =>
+      val tailAsJsonPath = JsonPath(tail)
+      head match {
+        case part: PlainPart => part.read(jsValue).flatMap(subPart => tailAsJsonPath.evaluate(subPart))
+        case _: WildcardPart => tailAsJsonPath.readWildcard(jsValue)
+      }
+  }
+
+  private def readWildcard(jsValue: JsValue) = jsValue match {
+    case JsArray(arrayItems) =>
+      val evaluationResults: List[JsResult[JsValue]] = arrayItems.toList.map(evaluate)
+
+      evaluationResults.find(x => x.isInstanceOf[JsError])
+        .getOrElse(JsSuccess(expendArray(evaluationResults)))
+    case _ => JsError("Expecting an array")
+  }
+
+  private def expendArray(evaluationResults: List[JsResult[JsValue]]): JsArray = {
+    JsArray(evaluationResults
+      .map(_.get)
+      .flatMap({
+        case JsArray(nestedArray) => nestedArray
+        case other: JsValue => List(other)
+      }))
+  }
+}
+
+case class BackReference(name: MethodName, path: JsonPath, resultOf: MethodCallId) {
+  def resolve(invocation: Invocation): JsResult[JsValue] = if (!(invocation.methodName equals name)) {
+    JsError(s"$resultOf references a ${invocation.methodName} method")
+  } else {
+    path.evaluate(invocation.arguments.value)
+  }
+}
+
+case class InvalidResultReferenceException(message: String) extends IllegalArgumentException
+
 class ProcessingContext {
  private val creationIds: mutable.Map[ClientId, ServerId] = mutable.Map()
+ private val invocations: mutable.Map[MethodCallId, Invocation] = mutable.Map()
 
  def recordCreatedId(clientId: ClientId, serverId: ServerId): Unit = creationIds.put(clientId, serverId)
  private def retrieveServerId(clientId: ClientId): Option[ServerId] = creationIds.get(clientId)
 
+  def recordInvocation(invocation: Invocation): Unit = invocations.put(invocation.methodCallId, invocation)
+
+  def resolveBackReferences(serializer: Serializer, invocation: Invocation): Either[InvalidResultReferenceException, Invocation] =
+    backReferenceResolver(serializer).reads(invocation.arguments.value) match {
+      case JsError(e) => Left(InvalidResultReferenceException(e.toString()))
+      case JsSuccess(JsObject(underlying), _) => Right(Invocation(methodName = invocation.methodName,
+        methodCallId = invocation.methodCallId,
+        arguments = Arguments(JsObject(underlying))))
+      case others: JsSuccess[JsValue] => Left(InvalidResultReferenceException(s"Unexpected value $others"))
+    }
+
+  private def backReferenceResolver(serializer: Serializer): Reads[JsValue] = {
+    case JsArray(value) => resolveBackReferences(serializer, value)
+    case JsObject(underlying) => resolveBackReference(serializer, underlying)
+    case others: JsValue => JsSuccess(others)
+  }
+
+  private def resolveBackReferences(serializer: Serializer, array: collection.IndexedSeq[JsValue]): JsResult[JsValue] = {
+    val resolver: Reads[JsValue] = backReferenceResolver(serializer)
+    val results: Seq[JsResult[JsValue]] = array.map(resolver.reads).toSeq
+    results.find(_.isError)
+      .getOrElse(JsSuccess(JsArray(results.map(_.get))))
+  }
+
+  private def resolveBackReference(serializer: Serializer, underlying: collection.Map[String, JsValue]): JsResult[JsObject] = {
+    val resolutions = underlying.map(resolveBackReference(serializer))
+
+    val firstError = resolutions.flatMap({
+      case Left(jsError) => Some(jsError)
+      case _ => None
+    }).headOption
+
+    val transformedMap = resolutions.flatMap({
+      case Right((entry, value)) => Some((entry, value))
+      case _ => None
+    }).toMap
+
+    firstError.getOrElse(JsSuccess(JsObject(transformedMap)))
+  }
+
+  private def resolveBackReference(serializer: Serializer)(entry: (String, JsValue)): Either[JsError, (String, JsValue)] = {
+    if (entry._1.startsWith("#")) {
+      val newEntry: String = entry._1.substring(1)
+
+      serializer.deserializeBackReference(entry._2) match {
+        case JsSuccess(backReference, _) => resolveBackReference(newEntry, backReference)
+        // If the JSON object is not a back-reference continue parsing (it could be a creationId)
+        case JsError(_) => propagateBackReferenceResolution(serializer, entry)
+      }
+    } else {
+      propagateBackReferenceResolution(serializer, entry)
+    }
+  }
+
+  private def resolveBackReference(newEntry: String, backReference: BackReference): Either[JsError, (String, JsValue)] = {
+    resolve(backReference) match {
+      case JsError(e) => Left(JsError(e))
+      case JsSuccess(resolvedBackReference, _) => Right((newEntry, resolvedBackReference))
+    }
+  }
+
+  private def propagateBackReferenceResolution(serializer: Serializer, entry: (String, JsValue)): Either[JsError, (String, JsValue)] = {
+    val entryPayload: JsResult[JsValue] = backReferenceResolver(serializer).reads(entry._2)
+
+    entryPayload match {
+      case JsError(e) => Left(JsError(e))
+      case JsSuccess(newValue, _) => Right((entry._1, newValue))
+    }
+  }
+
+  private def retrieveInvocation(callId: MethodCallId): Option[Invocation] = invocations.get(callId)
+
+  private def resolve(backReference: BackReference): JsResult[JsValue] = retrieveInvocation(backReference.resultOf)
+    .map(backReference.resolve)
+    .getOrElse(JsError("Back reference could not be resolved"))
+
  def resolveMailboxId(unparsedMailboxId: UnparsedMailboxId, mailboxIdFactory: MailboxId.Factory): Either[IllegalArgumentException, MailboxId] =
   Id.validate(unparsedMailboxId.value)
    .flatMap(id => resolveServerId(ClientId(id)))
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/BackReferenceTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/BackReferenceTest.scala
new file mode 100644
index 0000000..f51c5fc
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/json/BackReferenceTest.scala
@@ -0,0 +1,160 @@
+/****************************************************************
+ * 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.json
+
+import eu.timepit.refined.auto._
+import org.apache.james.jmap.model.Invocation.{MethodCallId, MethodName}
+import org.apache.james.jmap.routes.{BackReference, JsonPath}
+import org.apache.james.mailbox.model.TestId
+import org.scalatest.matchers.should.Matchers
+import org.scalatest.wordspec.AnyWordSpec
+import play.api.libs.json.{JsError, JsSuccess, Json}
+
+
+object BackReferenceTest {
+  val SERIALIZER: Serializer = new Serializer(new TestId.Factory)
+}
+
+class BackReferenceTest extends AnyWordSpec with Matchers {
+  "Deserialize backReference" should {
+    "succeed" in {
+      val jsonPath = JsonPath.parse("list/*/id")
+      val expectedBackReference = BackReference(
+        name = MethodName("Mailbox/get"),
+        resultOf = MethodCallId("c1"),
+        path = jsonPath)
+
+      BackReferenceTest.SERIALIZER.deserializeBackReference(
+        Json.parse("""{
+          |  "resultOf":"c1",
+          |  "name":"Mailbox/get",
+          |  "path":"list/*/id"
+          |}""".stripMargin)) should equal(JsSuccess(expectedBackReference))
+    }
+  }
+
+  "JsonPath evaluation" should {
+    "noop when empty" in {
+      val jsonPath = JsonPath.parse("")
+      val json = Json.parse("""["array"]""")
+      val expected = Json.parse("""["array"]""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "succeed when single path part is present" in {
+      val jsonPath = JsonPath.parse("path")
+      val json = Json.parse("""{"path" : ["array"]}""")
+      val expected = Json.parse("""["array"]""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "succeed when array part is present" in {
+      val jsonPath = JsonPath.parse("path/*")
+      val json = Json.parse("""{"path" : ["1", "2"]}""")
+      val expected = Json.parse("""["1", "2"]""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "fail when not an array" in {
+      val jsonPath = JsonPath.parse("path/*")
+      val json = Json.parse("""{"path" : {"key": "value"}}""")
+
+      jsonPath.evaluate(json) shouldBe a[JsError]
+    }
+    "allow simple resolution within an array" in {
+      val jsonPath = JsonPath.parse("path/*/key")
+      val json = Json.parse(
+        """{"path" : [
+          |  {"key": "1"},
+          |  {"key": "2"}
+          |]}""".stripMargin)
+      val expected = Json.parse("""["1", "2"]""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "allow recursive array resolution" in {
+      val jsonPath = JsonPath.parse("path/*/key/*")
+      val json = Json.parse(
+        """{"path" : [
+          |  {"key": ["1", "2"]},
+          |  {"key": ["3"]}
+          |]}""".stripMargin)
+      val expected = Json.parse("""["1", "2", "3"]""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "succeed when double path part is present" in {
+      val jsonPath = JsonPath.parse("path/second")
+      val json = Json.parse(
+        """{
+          |  "path" : {
+          |    "second" : {"key": "value"}
+          |  }
+          |}""".stripMargin)
+      val expected = Json.parse("""{"key": "value"}""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "succeed double delimiter" in {
+      val jsonPath = JsonPath.parse("path//second")
+      val json = Json.parse(
+        """{
+          |  "path" : {
+          |    "second" : {"key": "value"}
+          |  }
+          |}""".stripMargin)
+      val expected = Json.parse("""{"key": "value"}""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "accept starting delimiter" in {
+      val jsonPath = JsonPath.parse("/path")
+      val json = Json.parse("""{"path" : ["array"]}""")
+      val expected = Json.parse("""["array"]""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "accept ending delimiter" in {
+      val jsonPath = JsonPath.parse("path/")
+      val json = Json.parse("""{"path" : ["array"]}""")
+      val expected = Json.parse("""["array"]""")
+
+      jsonPath.evaluate(json) should equal(JsSuccess(expected))
+    }
+    "fail when single path part is missing" in {
+      val jsonPath = JsonPath.parse("other")
+      val json = Json.parse("""{"path" : ["array"]}""")
+
+      jsonPath.evaluate(json) shouldBe a[JsError]
+    }
+    "fail when single path part is present in an array" in {
+      val jsonPath = JsonPath.parse("other")
+      val json = Json.parse("""[{"path" : "array"}]""")
+
+      jsonPath.evaluate(json) shouldBe a[JsError]
+    }
+    "fail when single path part requested on a value" in {
+      val jsonPath = JsonPath.parse("other")
+      val json = Json.parse(""""value"""")
+
+      jsonPath.evaluate(json) shouldBe a[JsError]
+    }
+  }
+}


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