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]
