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 4306950317d246aa8799a9d9ebe2659667ef6920
Author: duc91 <[email protected]>
AuthorDate: Tue Oct 20 11:59:54 2020 +0700

    JAMES-3412 Email/set update keywords partial update: validation
    
    Delegated mailboxes testing, as well as not overwriting stored
    Recent and Deleted flags is already covered as part of reset work.
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 214 ++++++++++++++++++++-
 .../james/jmap/json/EmailSetSerializer.scala       |   5 +-
 2 files changed, 215 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 26316a2..8a92b53 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
@@ -276,7 +276,7 @@ trait EmailSetMethodContract {
         s"""{
            |  "${messageId.serialize}":{
            |      "type":"invalidPatch",
-           |      "description":"Message 1 update is invalid: Does not allow 
to update 'Deleted' or 'Recent' flag"}
+           |      "description":"Message 1 update is invalid: 
List((,List(JsonValidationError(List(Value associated with keywords is invalid: 
List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 
'Recent' flag),ArraySeq()))))),ArraySeq()))))"}
            |  }
            |}"""
           .stripMargin)
@@ -670,6 +670,216 @@ trait EmailSetMethodContract {
   }
 
   @Test
+  def emailSetShouldRejectPartiallyUpdateAndResetKeywordsAtTheSameTime(server: 
GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = FlagsBuilder.builder()
+      .add(Flags.Flag.ANSWERED)
+      .add(Flags.Flag.SEEN)
+      .build()
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = 
server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), 
bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords/music": true,
+         |          "keywords/%s": null,
+         |          "keywords": {
+         |             "movie": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin, "$Seen")
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notUpdated")
+      .isEqualTo(s"""{
+          |  "${messageId.serialize}": {
+          |     "type": "invalidPatch",
+          |     "description": "Message 1 update is invalid: Partial update 
and reset specified for keywords"
+          |   }
+          |}
+      """.stripMargin)
+  }
+
+  @Test
+  def emailSetShouldRejectPartiallyUpdateWhenInvalidKeyword(server: 
GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = 
server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), 
bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords/mus*c": true
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
+      .isEqualTo(
+        """|{
+           |   "type":"invalidPatch",
+           |   "description": "Message 1 update is invalid: 
List((,List(JsonValidationError(List(keywords/mus*c is an invalid entry in an 
Email/set update patch: FlagName must not be null or empty, must have length 
form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or 
{'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))"}"
+           |}""".stripMargin)
+  }
+
+  @Test
+  def emailSetShouldRejectPartiallyUpdateWhenFalseValue(server: 
GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = 
server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), 
bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |           "keywords/music": true,
+         |           "keywords/movie": false
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
+      .isEqualTo(
+        """|{
+          |   "type":"invalidPatch",
+          |   "description": "Message 1 update is invalid: 
List((,List(JsonValidationError(List(Value associated with keywords/movie is 
invalid: Keywords partial updates requires a JsBoolean(true) (set) or a JsNull 
(unset)),ArraySeq()))))"
+          |}""".stripMargin)
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = Array(
+    "$Recent",
+    "$Deleted"
+  ))
+  def partialUpdateShouldRejectNonExposedKeyword(unexposedKeyword: String, 
server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = 
server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), 
bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |           "keywords/music": true,
+         |           "keywords/$unexposedKeyword": true
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notUpdated")
+      .isEqualTo(
+        s"""{
+           |  "${messageId.serialize}":{
+           |      "type":"invalidPatch",
+           |      "description":"Message 1 update is invalid: 
List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 
'Recent' flag),ArraySeq()))))"}
+           |  }
+           |}"""
+          .stripMargin)
+  }
+
+  @Test
   def emailSetShouldDestroyEmail(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
@@ -1322,7 +1532,7 @@ trait EmailSetMethodContract {
       s"""{
          |  "1": {
          |    "type": "invalidPatch",
-         |    "description": "Message 1 update is invalid: Partial update and 
reset specified"
+         |    "description": "Message 1 update is invalid: Partial update and 
reset specified for mailboxIds"
          |  }
          |}""".stripMargin)
   }
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index 4a7f14d..2928c03 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -206,8 +206,9 @@ class EmailSetSerializer @Inject()(messageIdFactory: 
MessageId.Factory, mailboxI
         case JsBoolean(false) => JsError("keyword value can only be true")
         case _ => JsError("Expecting keyword value to be a boolean")
       })
-  private implicit val keywordsReads: Reads[Keywords] = jsValue => 
keywordsMapReads.reads(jsValue).map(
-    keywordsMap => Keywords(keywordsMap.keys.toSet))
+  private implicit val keywordsReads: Reads[Keywords] = jsValue => 
keywordsMapReads.reads(jsValue).flatMap(
+    keywordsMap => STRICT_KEYWORDS_FACTORY.fromSet(keywordsMap.keys.toSet)
+      .fold(e => JsError(e.getMessage), keywords => JsSuccess(keywords)))
 
   private implicit val unitWrites: Writes[Unit] = _ => JsNull
   private implicit val updatedWrites: Writes[Map[MessageId, Unit]] = 
mapWrites[MessageId, Unit](_.serialize, unitWrites)


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to