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/10/19 03:24:49 UTC
[james-project] 02/03: JAMES-3413 Email/set update mailboxIds error
handling
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 c4c1030e3d550bf1e2aa3e77bfee0c437d1414d8
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Thu Oct 15 11:29:58 2020 +0700
JAMES-3413 Email/set update mailboxIds error handling
---
.../rfc8621/contract/EmailSetMethodContract.scala | 742 ++++++++++++++++++++-
.../james/jmap/json/EmailSetSerializer.scala | 94 ++-
.../scala/org/apache/james/jmap/mail/Email.scala | 5 +-
.../org/apache/james/jmap/mail/EmailSet.scala | 22 +-
.../apache/james/jmap/method/EmailSetMethod.scala | 214 +++---
5 files changed, 964 insertions(+), 113 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 572f82e..799e907 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
@@ -24,11 +24,9 @@ import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
import io.restassured.RestAssured.{`given`, requestSpecification}
import io.restassured.http.ContentType.JSON
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
-import net.javacrumbs.jsonunit.core.Option
-import net.javacrumbs.jsonunit.core.internal.Options
import org.apache.http.HttpStatus.SC_OK
import org.apache.james.GuiceJamesServer
-import org.apache.james.jmap.draft.JmapGuiceProbe
+import org.apache.james.jmap.draft.{JmapGuiceProbe, MessageIdProbe}
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -37,6 +35,7 @@ import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, Messa
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.junit.jupiter.api.{BeforeEach, Test}
trait EmailSetMethodContract {
@@ -507,7 +506,7 @@ trait EmailSetMethodContract {
}
@Test
- def emailSetShouldUpdateMailboxIds(server: GuiceJamesServer): Unit = {
+ def emailSetMailboxIdResetShouldSucceed(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
val mailboxId2: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "other"))
@@ -586,7 +585,7 @@ trait EmailSetMethodContract {
}
@Test
- def emailSetShouldUpdateMailboxIdsForMultipleMessages(server: GuiceJamesServer): Unit = {
+ def emailSetMailboxIdResetShouldSucceedForMultipleMessages(server: GuiceJamesServer): Unit = {
val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
val mailboxId2: MailboxId = mailboxProbe.createMailbox(MailboxPath.forUser(BOB, "other"))
@@ -645,7 +644,6 @@ trait EmailSetMethodContract {
.asString
assertThatJson(response)
- .withOptions(new Options(Option.IGNORING_ARRAY_ORDER))
.isEqualTo(
s"""{
| "sessionState": "75128aab4b1b",
@@ -681,6 +679,738 @@ trait EmailSetMethodContract {
|}""".stripMargin)
}
+ @Test
+ def emailSetMailboxIdsResetShouldFailWhenNoRightsOnSourceMailbox(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val path = MailboxPath.forUser(ANDRE, "other")
+ mailboxProbe.createMailbox(path)
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(ANDRE.asString, path,
+ AppendCommand.from(buildTestMessage))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId1.serialize}": 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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notUpdated": {
+ | "1": {
+ | "type": "notFound",
+ | "description": "Cannot find message with messageId: 1"
+ | }
+ | }
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetShouldFailWhenForbidden(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ val path = MailboxPath.forUser(ANDRE, "other")
+ val mailboxId2: MailboxId = mailboxProbe.createMailbox(path)
+
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(path, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read))
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId2.serialize}": true
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${messageId.serialize}"],
+ | "properties":["mailboxIds"]
+ | }, "c2"]
+ | ]
+ |}""".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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notUpdated": {
+ | "${messageId.serialize}": {
+ | "type": "notFound",
+ | "description": "Mailbox not found"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId":"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state":"000001",
+ | "list":[
+ | {
+ | "id":"${messageId.serialize}",
+ | "mailboxIds": {
+ | "${mailboxId1.serialize}": true
+ | }
+ | }
+ | ],
+ | "notFound":[]
+ | },"c2"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetShouldFailWhenRemovingMessageFromSourceMailbox(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val path = MailboxPath.forUser(ANDRE, "other")
+ val mailboxId2: MailboxId = mailboxProbe.createMailbox(path)
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(ANDRE.asString, path,
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(path, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read))
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId1.serialize}": true
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${messageId.serialize}"],
+ | "properties":["mailboxIds"]
+ | }, "c2"]
+ | ]
+ |}""".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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notUpdated": {
+ | "1": {
+ | "type": "notFound",
+ | "description": "Mailbox not found"
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId":"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state":"000001",
+ | "list":[
+ | {
+ | "id":"${messageId.serialize}",
+ | "mailboxIds": {
+ | "${mailboxId2.serialize}": true
+ | }
+ | }
+ | ],
+ | "notFound":[]
+ | },"c2"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetShouldSucceedWhenCopyMessage(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val path = MailboxPath.forUser(ANDRE, "other")
+ val mailboxId2: MailboxId = mailboxProbe.createMailbox(path)
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(ANDRE.asString, path,
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(path, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read))
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId1.serialize}": true,
+ | "${mailboxId2.serialize}": true
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${messageId.serialize}"],
+ | "properties":["mailboxIds"]
+ | }, "c2"]
+ | ]
+ |}""".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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "updated": {
+ | "${messageId.serialize}": null
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId":"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state":"000001",
+ | "list":[
+ | {
+ | "id":"${messageId.serialize}",
+ | "mailboxIds": {
+ | "${mailboxId1.serialize}": true,
+ | "${mailboxId2.serialize}": true
+ | }
+ | }
+ | ],
+ | "notFound":[]
+ | },"c2"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetShouldSucceedWhenShareeHasRightOnTargetMailbox(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val path = MailboxPath.forUser(ANDRE, "other")
+ val mailboxId2: MailboxId = mailboxProbe.createMailbox(path)
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(path, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read, Right.Insert))
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId2.serialize}": true
+ | }
+ | }
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${messageId.serialize}"],
+ | "properties":["mailboxIds"]
+ | }, "c2"]
+ | ]
+ |}""".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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "updated": {
+ | "${messageId.serialize}": null
+ | }
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId":"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state":"000001",
+ | "list":[
+ | {
+ | "id":"${messageId.serialize}",
+ | "mailboxIds": {
+ | "${mailboxId2.serialize}": true
+ | }
+ | }
+ | ],
+ | "notFound":[]
+ | },"c2"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetShouldNotAffectMailboxIdFilter(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+
+ val mailboxId1: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+ val path = MailboxPath.forUser(ANDRE, "other")
+ val mailboxId2: MailboxId = mailboxProbe.createMailbox(path)
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(ANDRE.asString, path,
+ AppendCommand.from(buildTestMessage))
+ .getMessageId
+
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(path, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read))
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId1.serialize}": true,
+ | "${mailboxId2.serialize}": 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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "updated": {
+ | "${messageId.serialize}": null
+ | }
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+
+ assertThat(server.getProbe(classOf[MessageIdProbe]).getMessages(messageId, ANDRE)
+ .stream()
+ .map(message => message.getMailboxId))
+ .containsExactly(mailboxId2)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetShouldFailWhenInvalidKey(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "invalid": 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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notUpdated": {
+ | "${messageId.serialize}": {
+ | "type": "invalidPatch",
+ | "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(For input string: \\"invalid\\"),ArraySeq()))))),ArraySeq()))))"
+ | }
+ | }
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetShouldFailWhenInvalidValue(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": "invalid"
+ | }
+ | }
+ | }
+ | }, "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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notUpdated": {
+ | "${messageId.serialize}": {
+ | "type": "invalidPatch",
+ | "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
+ | }
+ | }
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetShouldFailWhenValueIsFalse(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ val mailboxId: MailboxId = mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val messageId: MessageId = mailboxProbe
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId.serialize}": "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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notUpdated": {
+ | "${messageId.serialize}": {
+ | "type": "invalidPatch",
+ | "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
+ | }
+ | }
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def emailSetMailboxIdsResetSuccessAndFailureCanBeMixed(server: GuiceJamesServer): Unit = {
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+
+ val path = MailboxPath.forUser(ANDRE, "other")
+ val mailboxId2: MailboxId = mailboxProbe.createMailbox(path)
+
+ val messageId1: MessageId = mailboxProbe
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ val messageId2: MessageId = mailboxProbe
+ .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
+ AppendCommand.from(
+ buildTestMessage))
+ .getMessageId
+
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(path, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Read, Right.Insert))
+
+ val request =
+ s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "update": {
+ | "${messageId1.serialize}": {
+ | "mailboxIds": {
+ | "${mailboxId2.serialize}": true
+ | }
+ | },
+ | "${messageId2.serialize}": {
+ | "mailboxIds": {
+ | "invalid": 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)
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | ["Email/set", {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "updated": {
+ | "${messageId1.serialize}": null
+ | },
+ | "notUpdated": {
+ | "${messageId2.serialize}": {
+ | "type": "invalidPatch",
+ | "description": "Message ${messageId2.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(For input string: \\"invalid\\"),ArraySeq()))))),ArraySeq()))))"
+ | }
+ | }
+ | }, "c1"]
+ | ]
+ |}""".stripMargin)
+ }
+
private def buildTestMessage = {
Message.Builder
.of
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 396a7f3..2e33585 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
@@ -1,4 +1,4 @@
-/** **************************************************************
+/****************************************************************
* 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 *
@@ -6,16 +6,16 @@
* 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 *
- * *
+ * *
+ * 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
@@ -31,6 +31,75 @@ import scala.util.Try
class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxIdFactory: MailboxId.Factory) {
+ object EmailSetUpdateReads {
+ def reads(jsObject: JsObject): JsResult[EmailSetUpdate] =
+ asEmailSetUpdate(jsObject.value.map {
+ case (property, value) => EntryValidation.from(property, value)
+ }.toSeq)
+
+ private def asEmailSetUpdate(entries: Seq[EntryValidation]): JsResult[EmailSetUpdate] =
+ entries.flatMap(_.asJsError)
+ .headOption
+ .getOrElse({
+ val mailboxReset: Option[MailboxIds] = entries.flatMap {
+ case update: MailboxReset => Some(update)
+ case _ => None
+ }.headOption
+ .map(_.ids)
+
+ val mailboxesToAdd: Option[MailboxIds] = Some(entries
+ .flatMap {
+ case update: MailboxAddition => Some(update)
+ case _ => None
+ }.map(_.id).toList)
+ .filter(_.nonEmpty)
+ .map(MailboxIds)
+
+ val mailboxesToRemove: Option[MailboxIds] = Some(entries
+ .flatMap {
+ case update: MailboxRemoval => Some(update)
+ case _ => None
+ }.map(_.id).toList)
+ .filter(_.nonEmpty)
+ .map(MailboxIds)
+
+ JsSuccess(EmailSetUpdate(mailboxIds = mailboxReset))
+ })
+
+ object EntryValidation {
+ def from(property: String, value: JsValue): EntryValidation = property match {
+ case "mailboxIds" => mailboxIdsReads.reads(value)
+ .fold(
+ e => InvalidPatchEntryValue(property, e.toString()),
+ MailboxReset)
+ case _ => InvalidPatchEntryName(property)
+ }
+ }
+
+ sealed trait EntryValidation {
+ def asJsError: Option[JsError] = None
+ }
+
+ private case class InvalidPatchEntryName(property: String) extends EntryValidation {
+ override def asJsError: Option[JsError] = Some(JsError(s"$property is an invalid entry in an Email/set update patch"))
+ }
+
+ private case class InvalidPatchEntryNameWithDetails(property: String, cause: String) extends EntryValidation {
+ override def asJsError: Option[JsError] = Some(JsError(s"$property is an invalid entry in an Email/set update patch: $cause"))
+ }
+
+ private case class InvalidPatchEntryValue(property: String, cause: String) extends EntryValidation {
+ override def asJsError: Option[JsError] = Some(JsError(s"Value associated with $property is invalid: $cause"))
+ }
+
+ private case class MailboxAddition(id: MailboxId) extends EntryValidation
+
+ private case class MailboxRemoval(id: MailboxId) extends EntryValidation
+
+ private case class MailboxReset(ids: MailboxIds) extends EntryValidation
+
+ }
+
private implicit val messageIdWrites: Writes[MessageId] = messageId => JsString(messageId.serialize)
private implicit val messageIdReads: Reads[MessageId] = {
case JsString(serializedMessageId) => Try(JsSuccess(messageIdFactory.fromString(serializedMessageId)))
@@ -49,10 +118,17 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
private implicit val mailboxIdsReads: Reads[MailboxIds] = jsValue => mailboxIdsMapReads.reads(jsValue).map(
mailboxIdsMap => MailboxIds(mailboxIdsMap.keys.toList))
- private implicit val emailSetUpdateReads: Reads[EmailSetUpdate] = Json.reads[EmailSetUpdate]
+ private implicit val emailSetUpdateReads: Reads[EmailSetUpdate] = {
+ case o: JsObject => EmailSetUpdateReads.reads(o)
+ case _ => JsError("Expecting a JsObject to represent an EmailSetUpdate")
+ }
- private implicit val updatesMapReads: Reads[Map[UnparsedMessageId, EmailSetUpdate]] =
- readMapEntry[UnparsedMessageId, EmailSetUpdate](s => refineV[UnparsedMessageIdConstraint](s), emailSetUpdateReads)
+ private implicit val updatesMapReads: Reads[Map[UnparsedMessageId, JsObject]] =
+ readMapEntry[UnparsedMessageId, JsObject](s => refineV[UnparsedMessageIdConstraint](s),
+ {
+ case o: JsObject => JsSuccess(o)
+ case _ => JsError("Expecting a JsObject as an update entry")
+ })
private implicit val unitWrites: Writes[Unit] = _ => JsNull
private implicit val updatedWrites: Writes[Map[MessageId, Unit]] = mapWrites[MessageId, Unit](_.serialize, unitWrites)
@@ -66,5 +142,7 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
def deserialize(input: JsValue): JsResult[EmailSetRequest] = Json.fromJson[EmailSetRequest](input)
+ def deserializeEmailSetUpdate(input: JsValue): JsResult[EmailSetUpdate] = Json.fromJson[EmailSetUpdate](input)
+
def serialize(response: EmailSetResponse): JsObject = Json.toJsObject(response)
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index 8efb10f..9174ef9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -212,7 +212,10 @@ case class HeaderMessageId(value: String) extends AnyVal
case class Subject(value: String) extends AnyVal
-case class MailboxIds(value: List[MailboxId])
+case class MailboxIds(value: List[MailboxId]) {
+ def ++(ids: MailboxIds) = MailboxIds(value ++ ids.value)
+ def --(ids: MailboxIds) = MailboxIds((value.toSet -- ids.value.toSet).toList)
+}
case class ThreadId(value: String) extends AnyVal
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 85f98ee..ecab680 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
@@ -26,6 +26,7 @@ import org.apache.james.jmap.method.WithAccountId
import org.apache.james.jmap.model.State.State
import org.apache.james.jmap.model.{AccountId, SetError}
import org.apache.james.mailbox.model.MessageId
+import play.api.libs.json.JsObject
import scala.util.Try
@@ -45,15 +46,32 @@ object EmailSet {
case class DestroyIds(value: Seq[UnparsedMessageId])
case class EmailSetRequest(accountId: AccountId,
- update: Option[Map[UnparsedMessageId, EmailSetUpdate]],
+ update: Option[Map[UnparsedMessageId, JsObject]],
destroy: Option[DestroyIds]) extends WithAccountId
case class EmailSetResponse(accountId: AccountId,
newState: State,
updated: Option[Map[MessageId, Unit]],
+ notUpdated: Option[Map[UnparsedMessageId, SetError]],
destroyed: Option[DestroyIds],
notDestroyed: Option[Map[UnparsedMessageId, SetError]])
-case class EmailSetUpdate(mailboxIds: Option[MailboxIds])
+case class EmailSetUpdate(mailboxIds: Option[MailboxIds]) {
+ def validate: Either[IllegalArgumentException, ValidatedEmailSetUpdate] = {
+ val identity: Function[MailboxIds, MailboxIds] = ids => ids
+ val mailboxIdsReset: Function[MailboxIds, MailboxIds] = mailboxIds
+ .map(toReset => (_: MailboxIds) => toReset)
+ .getOrElse(identity)
+ val mailboxIdsTransformation: Function[MailboxIds, MailboxIds] = mailboxIdsReset
+ scala.Right(ValidatedEmailSetUpdate(mailboxIdsTransformation))
+ }
+}
+
+case class ValidatedEmailSetUpdate private (mailboxIdsTransformation: Function[MailboxIds, MailboxIds])
+
+class EmailUpdateValidationException() extends IllegalArgumentException
+case class InvalidEmailPropertyException(property: String, cause: String) extends EmailUpdateValidationException
+case class InvalidEmailUpdateException(property: String, cause: String) extends EmailUpdateValidationException
+
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 36979e0..c7022f8 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
@@ -23,16 +23,17 @@ import javax.inject.Inject
import org.apache.james.jmap.http.SessionSupplier
import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
-import org.apache.james.jmap.mail.{DestroyIds, EmailSet, EmailSetRequest, EmailSetResponse, EmailSetUpdate}
+import org.apache.james.jmap.mail.{DestroyIds, EmailSet, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds, ValidatedEmailSetUpdate}
import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
import org.apache.james.jmap.model.DefaultCapabilities.{CORE_CAPABILITY, MAIL_CAPABILITY}
import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.model.SetError.SetErrorDescription
import org.apache.james.jmap.model.{Capabilities, Invocation, SetError, State}
-import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MailboxId, MessageId}
+import org.apache.james.mailbox.exception.MailboxNotFoundException
+import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MessageId}
import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
import org.apache.james.metrics.api.MetricFactory
-import play.api.libs.json.{JsError, JsSuccess}
+import play.api.libs.json.{JsError, JsObject, JsSuccess}
import reactor.core.scala.publisher.{SFlux, SMono}
import reactor.core.scheduler.Schedulers
@@ -47,81 +48,84 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[EmailSetRequest] {
-case class DestroyResults(results: Seq[DestroyResult]) {
- def destroyed: Option[DestroyIds] = {
- Option(results.flatMap({
- result => result match {
- case result: DestroySuccess => Some(result.messageId)
- case _ => None
- }
- }).map(EmailSet.asUnparsed))
- .filter(_.nonEmpty)
- .map(DestroyIds)
- }
+ case class DestroyResults(results: Seq[DestroyResult]) {
+ def destroyed: Option[DestroyIds] = {
+ Option(results.flatMap({
+ result => result match {
+ case result: DestroySuccess => Some(result.messageId)
+ case _ => None
+ }
+ }).map(EmailSet.asUnparsed))
+ .filter(_.nonEmpty)
+ .map(DestroyIds)
+ }
- def notDestroyed: Option[Map[UnparsedMessageId, SetError]] = {
- Option(results.flatMap({
- result => result match {
- case failure: DestroyFailure => Some(failure)
- case _ => None
- }
- })
- .map(failure => (failure.unparsedMessageId, failure.asMessageSetError))
- .toMap)
- .filter(_.nonEmpty)
+ def notDestroyed: Option[Map[UnparsedMessageId, SetError]] = {
+ Option(results.flatMap({
+ result => result match {
+ case failure: DestroyFailure => Some(failure)
+ case _ => None
+ }
+ })
+ .map(failure => (failure.unparsedMessageId, failure.asMessageSetError))
+ .toMap)
+ .filter(_.nonEmpty)
+ }
}
-}
-object DestroyResult {
- def from(deleteResult: DeleteResult): DestroyResult = {
- val notFound = deleteResult.getNotFound.asScala
+ object DestroyResult {
+ def from(deleteResult: DeleteResult): DestroyResult = {
+ val notFound = deleteResult.getNotFound.asScala
- deleteResult.getDestroyed.asScala
- .headOption
- .map(DestroySuccess)
- .getOrElse(DestroyFailure(EmailSet.asUnparsed(notFound.head), MessageNotFoundExeception(notFound.head)))
- }
-}
-
-trait DestroyResult
-case class DestroySuccess(messageId: MessageId) extends DestroyResult
-case class DestroyFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends DestroyResult {
- def asMessageSetError: SetError = e match {
- case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(s"$unparsedMessageId is not a messageId: ${e.getMessage}"))
- case e: MessageNotFoundExeception => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
- case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
+ deleteResult.getDestroyed.asScala
+ .headOption
+ .map(DestroySuccess)
+ .getOrElse(DestroyFailure(EmailSet.asUnparsed(notFound.head), MessageNotFoundExeception(notFound.head)))
+ }
}
-}
-trait UpdateResult
-case class UpdateSuccess(messageId: MessageId) extends UpdateResult
-case class UpdateFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends UpdateResult {
- def asMessageSetError: SetError = e match {
- case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
+ trait DestroyResult
+ case class DestroySuccess(messageId: MessageId) extends DestroyResult
+ case class DestroyFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends DestroyResult {
+ def asMessageSetError: SetError = e match {
+ case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(s"$unparsedMessageId is not a messageId: ${e.getMessage}"))
+ case e: MessageNotFoundExeception => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
+ case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
+ }
}
-}
-case class UpdateResults(results: Seq[UpdateResult]) {
- def updated: Option[Map[MessageId, Unit]] = {
- Option(results.flatMap({
- result => result match {
- case result: UpdateSuccess => Some(result.messageId, ())
- case _ => None
- }
- }).toMap).filter(_.nonEmpty)
+
+ trait UpdateResult
+ case class UpdateSuccess(messageId: MessageId) extends UpdateResult
+ case class UpdateFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends UpdateResult {
+ def asMessageSetError: SetError = e match {
+ case e: IllegalArgumentException => SetError.invalidPatch(SetErrorDescription(s"Message $unparsedMessageId update is invalid: ${e.getMessage}"))
+ case _: MailboxNotFoundException => SetError.notFound(SetErrorDescription(s"Mailbox not found"))
+ case e: MessageNotFoundExeception => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
+ case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
+ }
}
+ case class UpdateResults(results: Seq[UpdateResult]) {
+ def updated: Option[Map[MessageId, Unit]] = {
+ Option(results.flatMap({
+ result => result match {
+ case result: UpdateSuccess => Some(result.messageId, ())
+ case _ => None
+ }
+ }).toMap).filter(_.nonEmpty)
+ }
- def notUpdated: Option[Map[UnparsedMessageId, SetError]] = {
- Option(results.flatMap({
- result => result match {
- case failure: UpdateFailure => Some(failure)
- case _ => None
- }
- })
- .map(failure => (failure.unparsedMessageId, failure.asMessageSetError))
- .toMap)
- .filter(_.nonEmpty)
+ def notUpdated: Option[Map[UnparsedMessageId, SetError]] = {
+ Option(results.flatMap({
+ result => result match {
+ case failure: UpdateFailure => Some(failure)
+ case _ => None
+ }
+ })
+ .map(failure => (failure.unparsedMessageId, failure.asMessageSetError))
+ .toMap)
+ .filter(_.nonEmpty)
+ }
}
-}
override val methodName: MethodName = MethodName("Email/set")
override val requiredCapabilities: Capabilities = Capabilities(CORE_CAPABILITY, MAIL_CAPABILITY)
@@ -131,16 +135,17 @@ case class UpdateResults(results: Seq[UpdateResult]) {
destroyResults <- destroy(request, mailboxSession)
updateResults <- update(request, mailboxSession)
} yield InvocationWithContext(
- invocation = Invocation(
- methodName = invocation.invocation.methodName,
- arguments = Arguments(serializer.serialize(EmailSetResponse(
- accountId = request.accountId,
- newState = State.INSTANCE,
- updated = updateResults.updated,
- destroyed = destroyResults.destroyed,
- notDestroyed = destroyResults.notDestroyed))),
- methodCallId = invocation.invocation.methodCallId),
- processingContext = invocation.processingContext)
+ invocation = Invocation(
+ methodName = invocation.invocation.methodName,
+ arguments = Arguments(serializer.serialize(EmailSetResponse(
+ accountId = request.accountId,
+ newState = State.INSTANCE,
+ updated = updateResults.updated,
+ notUpdated = updateResults.notUpdated,
+ destroyed = destroyResults.destroyed,
+ notDestroyed = destroyResults.notDestroyed))),
+ methodCallId = invocation.invocation.methodCallId),
+ processingContext = invocation.processingContext)
}
override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailSetRequest] = asEmailSetRequest(invocation.arguments)
@@ -164,13 +169,16 @@ case class UpdateResults(results: Seq[UpdateResult]) {
.getOrElse(SMono.just(UpdateResults(Seq())))
}
- private def update(updates: Map[UnparsedMessageId, EmailSetUpdate], session: MailboxSession): SMono[UpdateResults] = {
+ private def update(updates: Map[UnparsedMessageId, JsObject], session: MailboxSession): SMono[UpdateResults] = {
val validatedUpdates: List[Either[UpdateFailure, (MessageId, EmailSetUpdate)]] = updates
.map({
- case (unparsedMessageId, updatePatch) => EmailSet.parse(messageIdFactory)(unparsedMessageId)
- .map((_, updatePatch))
+ case (unparsedMessageId, json) => EmailSet.parse(messageIdFactory)(unparsedMessageId)
.toEither
.left.map(e => UpdateFailure(unparsedMessageId, e))
+ .flatMap(id => serializer.deserializeEmailSetUpdate(json)
+ .asEither
+ .fold(e => Left(UpdateFailure(unparsedMessageId, new IllegalArgumentException(e.toString()))),
+ (emailSetUpdate: EmailSetUpdate) => Right((id, emailSetUpdate))))
})
.toList
val failures: List[UpdateFailure] = validatedUpdates.flatMap({
@@ -187,7 +195,7 @@ case class UpdateResults(results: Seq[UpdateResult]) {
.collectMultimap(metaData => metaData.getComposedMessageId.getMessageId)
.flatMap(metaData => {
SFlux.fromIterable(validUpdates)
- .concatMap[UpdateResult]({
+ .flatMap[UpdateResult]({
case (messageId, updatePatch) =>
doUpdate(messageId, updatePatch, metaData.get(messageId).toList.flatten, session)
})
@@ -199,23 +207,37 @@ case class UpdateResults(results: Seq[UpdateResult]) {
}
private def doUpdate(messageId: MessageId, update: EmailSetUpdate, storedMetaData: List[ComposedMessageIdWithMetaData], session: MailboxSession): SMono[UpdateResult] = {
- val mailboxIds: List[MailboxId] = storedMetaData.map(metaData => metaData.getComposedMessageId.getMailboxId)
-
- updateMailboxIds(messageId, update, session)
- .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
- .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId)))
+ val mailboxIds: MailboxIds = MailboxIds(storedMetaData.map(metaData => metaData.getComposedMessageId.getMailboxId))
+
+ if (mailboxIds.value.isEmpty) {
+ SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), MessageNotFoundExeception(messageId)))
+ } else {
+ update.validate
+ .fold(
+ e => SMono.just(UpdateFailure(EmailSet.asUnparsed(messageId), e)),
+ validatedUpdate => updateMailboxIds(messageId, validatedUpdate, mailboxIds, session)
+ .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
+ .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId))))
+ }
}
- private def updateMailboxIds(messageId: MessageId, update: EmailSetUpdate, session: MailboxSession): SMono[UpdateResult] =
- SMono.justOrEmpty(update.mailboxIds)
- .flatMap(mailboxIds => SMono.fromCallable(() => messageIdManager.setInMailboxes(messageId, mailboxIds.value.asJava, session))
+ private def updateMailboxIds(messageId: MessageId, update: ValidatedEmailSetUpdate, mailboxIds: MailboxIds, session: MailboxSession): SMono[UpdateResult] = {
+ val targetIds = update.mailboxIdsTransformation.apply(mailboxIds)
+ if (targetIds.equals(mailboxIds)) {
+ SMono.just[UpdateResult](UpdateSuccess(messageId))
+ } else {
+ SMono.fromCallable(() => messageIdManager.setInMailboxes(messageId, targetIds.value.asJava, session))
.subscribeOn(Schedulers.elastic())
- .`then`(SMono.just[UpdateResult](UpdateSuccess(messageId))))
+ .`then`(SMono.just[UpdateResult](UpdateSuccess(messageId)))
+ .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
+ .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId)))
+ }
+ }
private def deleteMessage(destroyId: UnparsedMessageId, mailboxSession: MailboxSession): SMono[DestroyResult] =
EmailSet.parse(messageIdFactory)(destroyId)
- .fold(e => SMono.just(DestroyFailure(destroyId, e)),
- parsedId => SMono.fromCallable(() => DestroyResult.from(messageIdManager.delete(parsedId, mailboxSession)))
- .subscribeOn(Schedulers.elastic)
- .onErrorRecover(e => DestroyFailure(EmailSet.asUnparsed(parsedId), e)))
-}
+ .fold(e => SMono.just(DestroyFailure(destroyId, e)),
+ parsedId => SMono.fromCallable(() => DestroyResult.from(messageIdManager.delete(parsedId, mailboxSession)))
+ .subscribeOn(Schedulers.elastic)
+ .onErrorRecover(e => DestroyFailure(EmailSet.asUnparsed(parsedId), e)))
+}
\ 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