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 2021/01/14 03:26:08 UTC

[james-project] 03/11: JAMES-3474 Email/set should handle state property

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 ac22a265774b9962272d42f9a91548157b2557c3
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Fri Jan 8 11:21:55 2021 +0700

    JAMES-3474 Email/set should handle state property
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 246 ++++++++++++++++++++-
 .../org/apache/james/jmap/mail/EmailSet.scala      |   1 +
 .../apache/james/jmap/method/EmailSetMethod.scala  |  21 +-
 3 files changed, 264 insertions(+), 4 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/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index a4cff0d..2277986 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -25,7 +25,7 @@ import java.time.format.DateTimeFormatter
 import java.util.Date
 
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
-import io.restassured.RestAssured.{`given`, requestSpecification}
+import io.restassured.RestAssured.{`given`, `with`, requestSpecification}
 import io.restassured.builder.ResponseSpecBuilder
 import io.restassured.http.ContentType.JSON
 import javax.mail.Flags
@@ -48,6 +48,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.assertThat
+import org.hamcrest.Matchers.{equalTo, not}
 import org.junit.jupiter.api.{BeforeEach, Test}
 import org.junit.jupiter.params.ParameterizedTest
 import org.junit.jupiter.params.provider.ValueSource
@@ -6289,6 +6290,249 @@ trait EmailSetMethodContract {
            |}""".stripMargin)
   }
 
+  @Test
+  def newStateShouldBeUpToDate(server: GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+
+    val request =
+      s"""
+         |{
+         |   "using": [
+         |     "urn:ietf:params:jmap:core",
+         |     "urn:ietf:params:jmap:mail"],
+         |   "methodCalls": [
+         |       ["Email/set", {
+         |         "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |         "create": {
+         |           "aaaaaa": {
+         |             "mailboxIds": {
+         |               "${mailboxId.serialize()}": true
+         |             }
+         |           }
+         |         }
+         |       }, "c1"],
+         |       ["Email/changes", {
+         |         "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |         "#sinceState": {
+         |           "resultOf":"c1",
+         |           "name":"Email/set",
+         |           "path":"newState"
+         |         }
+         |       }, "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)
+      .withOptions(new Options(Option.IGNORING_ARRAY_ORDER))
+      .whenIgnoringPaths("methodResponses[1][1].oldState",
+        "methodResponses[1][1].newState")
+      .inPath("methodResponses[1][1]")
+      .isEqualTo(
+        s"""{
+           |  "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |  "hasMoreChanges": false,
+           |  "created": [],
+           |  "updated": [],
+           |  "destroyed": []
+           |}""".stripMargin)
+  }
+
+  @Test
+  def oldStateShouldIncludeSetChanges(server: GuiceJamesServer): Unit = {
+    val path: MailboxPath = MailboxPath.forUser(BOB, "mailbox")
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+
+    val message: Message = Fixture.createTestMessage
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString(), path,
+        AppendCommand.builder()
+          .build(message))
+      .getMessageId
+
+    val request =
+      s"""
+         |{
+         |   "using": [
+         |     "urn:ietf:params:jmap:core",
+         |     "urn:ietf:params:jmap:mail"],
+         |   "methodCalls": [
+         |       ["Email/set", {
+         |         "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |         "update": {
+         |           "${messageId.serialize}": {
+         |             "keywords": {
+         |               "music": true
+         |             }
+         |           }
+         |         }
+         |       }, "c1"],
+         |       ["Email/changes", {
+         |         "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |         "#sinceState": {
+         |            "resultOf":"c1",
+         |            "name":"Email/set",
+         |            "path":"oldState"
+         |          }
+         |       }, "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)
+      .withOptions(new Options(Option.IGNORING_ARRAY_ORDER))
+      .whenIgnoringPaths("methodResponses[1][1].oldState", "methodResponses[1][1].newState")
+      .inPath("methodResponses[1][1]")
+      .isEqualTo(
+        s"""{
+           |  "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |  "hasMoreChanges": false,
+           |  "created": [],
+           |  "updated": ["${messageId.serialize}"],
+           |  "destroyed": []
+           |}""".stripMargin)
+  }
+
+  @Test
+  def stateShouldNotTakeIntoAccountDelegationWhenNoCapability(server: GuiceJamesServer): Unit = {
+    val state: String = `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(
+        s"""{
+           |  "using": [
+           |    "urn:ietf:params:jmap:core",
+           |    "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [
+           |    ["Email/get", {
+           |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |      "ids": []
+           |    }, "c1"]
+           |  ]
+           |}""".stripMargin)
+      .post
+    .`then`()
+      .extract()
+      .jsonPath()
+      .get("methodResponses[0][1].state")
+
+    val sharedMailboxName = "AndreShared"
+    val andreMailboxPath = MailboxPath.forUser(ANDRE, sharedMailboxName)
+    server.getProbe(classOf[MailboxProbeImpl])
+      .createMailbox(andreMailboxPath)
+
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(andreMailboxPath, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Lookup))
+
+    val message: Message = Fixture.createTestMessage
+    server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(ANDRE.asString(), andreMailboxPath,
+        AppendCommand.builder()
+          .build(message))
+      .getMessageId
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(s"""{
+               |  "using": [
+               |    "urn:ietf:params:jmap:core",
+               |    "urn:ietf:params:jmap:mail"],
+               |  "methodCalls": [
+               |    ["Email/set", {
+               |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6"
+               |    }, "c1"]
+               |  ]
+               |}""".stripMargin)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .body("methodResponses[0][1].oldState", equalTo(state))
+  }
+
+  @Test
+  def stateShouldTakeIntoAccountDelegationWhenCapability(server: GuiceJamesServer): Unit = {
+    val state: String = `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(
+        s"""{
+           |  "using": [
+           |  "urn:ietf:params:jmap:core",
+           |  "urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [
+           |    ["Email/get", {
+           |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |      "ids":[]
+           |    }, "c1"]
+           |  ]
+           |}""".stripMargin)
+      .post
+    .`then`()
+      .extract()
+      .jsonPath()
+      .get("methodResponses[0][1].state")
+
+    val sharedMailboxName = "AndreShared"
+    val andreMailboxPath = MailboxPath.forUser(ANDRE, sharedMailboxName)
+    server.getProbe(classOf[MailboxProbeImpl])
+      .createMailbox(andreMailboxPath)
+
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(andreMailboxPath, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Lookup))
+
+    val message: Message = Fixture.createTestMessage
+    server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(ANDRE.asString(), andreMailboxPath,
+        AppendCommand.builder()
+          .build(message))
+      .getMessageId
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(s"""{
+               |  "using": [
+               |    "urn:ietf:params:jmap:core",
+               |    "urn:ietf:params:jmap:mail",
+               |    "urn:apache:james:params:jmap:mail:shares"],
+               |  "methodCalls": [
+               |    ["Email/set", {
+               |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6"
+               |    }, "c1"]
+               |  ]
+               |}""".stripMargin)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .body("methodResponses[0][1].oldState", not(equalTo(state)))
+  }
+
   private def buildTestMessage = {
     Message.Builder
       .of
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index f56f5ba..a9fb1e9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -338,6 +338,7 @@ case class EmailSetRequest(accountId: AccountId,
                            destroy: Option[DestroyIds]) extends WithAccountId
 
 case class EmailSetResponse(accountId: AccountId,
+                            oldState: Option[State],
                             newState: State,
                             created: Option[Map[EmailCreationId, EmailCreationResponse]],
                             notCreated: Option[Map[EmailCreationId, SetError]],
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index 4a69b2c..bd52c8b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -20,7 +20,9 @@ package org.apache.james.jmap.method
 
 import eu.timepit.refined.auto._
 import javax.inject.Inject
-import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
+import org.apache.james.jmap.api.change.EmailChangeRepository
+import org.apache.james.jmap.api.model.{AccountId => JavaAccountId}
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JAMES_SHARES, JMAP_CORE, JMAP_MAIL}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.core.{ClientId, Id, Invocation, ServerId, State}
 import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
@@ -39,21 +41,25 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
                                val sessionSupplier: SessionSupplier,
                                createPerformer: EmailSetCreatePerformer,
                                deletePerformer: EmailSetDeletePerformer,
-                               updatePerformer: EmailSetUpdatePerformer) extends MethodRequiringAccountId[EmailSetRequest] {
+                               updatePerformer: EmailSetUpdatePerformer,
+                               emailChangeRepository: EmailChangeRepository) extends MethodRequiringAccountId[EmailSetRequest] {
   override val methodName: MethodName = MethodName("Email/set")
   override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
 
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSetRequest): SMono[InvocationWithContext] = {
     for {
+      oldState <- retrieveState(capabilities, mailboxSession)
       destroyResults <- deletePerformer.destroy(request, mailboxSession)
       updateResults <- updatePerformer.update(request, mailboxSession)
       created <- createPerformer.create(request, mailboxSession)
+      newState <- retrieveState(capabilities, mailboxSession)
     } yield InvocationWithContext(
       invocation = Invocation(
         methodName = methodName,
         arguments = Arguments(serializer.serialize(EmailSetResponse(
           accountId = request.accountId,
-          newState = State.INSTANCE,
+          oldState = Some(oldState),
+          newState = newState,
           created = created.created,
           notCreated = created.notCreated,
           updated = updateResults.updated,
@@ -75,4 +81,13 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
       case JsSuccess(emailSetRequest, _) => Right(emailSetRequest)
       case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
+
+  private def retrieveState(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession): SMono[State] =
+    if (capabilities.contains(JAMES_SHARES)) {
+      SMono(emailChangeRepository.getLatestStateWithDelegation(JavaAccountId.fromUsername(mailboxSession.getUser)))
+        .map(State.fromJava)
+    } else {
+      SMono(emailChangeRepository.getLatestState(JavaAccountId.fromUsername(mailboxSession.getUser)))
+        .map(State.fromJava)
+    }
 }
\ No newline at end of file


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