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

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

commit 3653e5dc419f41aff0c63e0c64556a8b627a900c
Author: LanKhuat <[email protected]>
AuthorDate: Thu Oct 8 16:51:46 2020 +0700

    JAMES-3410 Email/set destroy error handling
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 478 +++++++++++++++++++--
 .../rfc8621/memory/MemoryEmailSetMethodTest.java   |   9 +
 .../james/jmap/json/EmailSetSerializer.scala       |  19 +-
 .../org/apache/james/jmap/mail/EmailSet.scala      |  30 +-
 .../apache/james/jmap/method/EmailSetMethod.scala  | 120 ++++--
 5 files changed, 564 insertions(+), 92 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 17f5bc3..060d22a 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
@@ -19,8 +19,6 @@
 package org.apache.james.jmap.rfc8621.contract
 
 import java.nio.charset.StandardCharsets
-import java.time.format.DateTimeFormatter
-import java.util.concurrent.TimeUnit
 
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
@@ -28,27 +26,18 @@ import io.restassured.http.ContentType.JSON
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.draft.JmapGuiceProbe
 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
-import org.apache.james.mailbox.model.{MailboxPath, MessageId}
+import org.apache.james.mailbox.model.MailboxACL.Right
+import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, 
MessageId}
 import org.apache.james.mime4j.dom.Message
-import org.apache.james.modules.MailboxProbeImpl
+import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
 import org.apache.james.utils.DataProbeImpl
-import org.awaitility.Awaitility
-import org.awaitility.Duration.ONE_HUNDRED_MILLISECONDS
 import org.junit.jupiter.api.{BeforeEach, Test}
 
 trait EmailSetMethodContract {
-  private lazy val slowPacedPollInterval = ONE_HUNDRED_MILLISECONDS
-  private lazy val calmlyAwait = Awaitility.`with`
-    .pollInterval(slowPacedPollInterval)
-    .and.`with`.pollDelay(slowPacedPollInterval)
-    .await
-  private lazy val awaitAtMostTenSeconds = calmlyAwait.atMost(10, 
TimeUnit.SECONDS)
-
-  private lazy val UTC_DATE_FORMAT = 
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX")
-
   @BeforeEach
   def setUp(server: GuiceJamesServer): Unit = {
     server.getProbe(classOf[DataProbeImpl])
@@ -62,11 +51,13 @@ trait EmailSetMethodContract {
       .build
   }
 
+  def randomMessageId: MessageId
+
   @Test
   def emailSetShouldDestroyEmail(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
-    val messageId1: MessageId = mailboxProbe
+    val messageId: MessageId = mailboxProbe
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB),
         AppendCommand.from(
           buildTestMessage))
@@ -80,44 +71,437 @@ trait EmailSetMethodContract {
          |  "methodCalls": [
          |    ["Email/set", {
          |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "destroy": ["${messageId1.serialize}"]
+         |      "destroy": ["${messageId.serialize}"]
          |    }, "c1"],
          |    ["Email/get", {
          |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-         |      "ids": ["${messageId1.serialize}"]
+         |      "ids": ["${messageId.serialize}"]
          |    }, "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",
-           |        "destroyed": ["${messageId1.serialize}"]
-           |      }, "c1"],
-           |      ["Email/get", {
-           |        "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-           |        "state": "000001",
-           |        "list": [],
-           |        "notFound": ["${messageId1.serialize}"]
-           |      }, "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",
+         |        "destroyed": ["${messageId.serialize}"]
+         |      }, "c1"],
+         |      ["Email/get", {
+         |        "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |        "state": "000001",
+         |        "list": [],
+         |        "notFound": ["${messageId.serialize}"]
+         |      }, "c2"]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def emailSetDestroyShouldFailWhenInvalidMessageId(server: GuiceJamesServer): 
Unit = {
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["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",
+         |        "notDestroyed": {
+         |          "invalid": {
+         |            "type": "invalidArguments",
+         |            "description": "invalid is not a messageId: For input 
string: \\"invalid\\""
+         |          }
+         |        }
+         |      }, "c1"]]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def emailSetDestroyShouldFailWhenMessageIdNotFound(server: 
GuiceJamesServer): Unit = {
+    val messageId = randomMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["${messageId.serialize}"]
+         |    }, "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",
+         |        "notDestroyed": {
+         |          "${messageId.serialize}": {
+         |            "type": "notFound",
+         |            "description": "Cannot find message with messageId: 
${messageId.serialize}"
+         |          }
+         |        }
+         |      }, "c1"]]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def emailSetDestroyShouldFailWhenMailDoesNotBelongToUser(server: 
GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    mailboxProbe.createMailbox(MailboxPath.inbox(ANDRE))
+
+    val messageId: MessageId = mailboxProbe
+      .appendMessage(ANDRE.asString, MailboxPath.inbox(ANDRE),
+        AppendCommand.from(
+          buildTestMessage))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["${messageId.serialize}"]
+         |    }, "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",
+         |        "notDestroyed": {
+         |          "${messageId.serialize}": {
+         |            "type": "notFound",
+         |            "description": "Cannot find message with messageId: 
${messageId.serialize}"
+         |          }
+         |        }
+         |      }, "c1"]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def emailSetDestroyShouldFailWhenForbidden(server: GuiceJamesServer): Unit = 
{
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+
+    val andreMailbox: String = "andrecustom"
+    val path = MailboxPath.forUser(ANDRE, andreMailbox)
+    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",
+         |      "destroy": ["${messageId.serialize}"]
+         |    }, "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",
+         |        "notDestroyed": {
+         |          "${messageId.serialize}": {
+         |            "type": "notFound",
+         |            "description": "Cannot find message with messageId: 
${messageId.serialize}"
+         |          }
+         |        }
+         |      }, "c1"]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def emailSetDestroyShouldDestroyEmailWhenShareeHasDeleteRight(server: 
GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+
+    val andreMailbox: String = "andrecustom"
+    val path = MailboxPath.forUser(ANDRE, andreMailbox)
+    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, Right.DeleteMessages))
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["${messageId.serialize}"]
+         |    }, "c1"],
+         |    ["Email/get", {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["${messageId.serialize}"]
+         |    }, "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",
+         |        "destroyed": ["${messageId.serialize}"]
+         |      }, "c1"],
+         |      ["Email/get", {
+         |        "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |        "state": "000001",
+         |        "list": [],
+         |        "notFound": ["${messageId.serialize}"]
+         |      }, "c2"]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def emailSetDestroyShouldDestroyEmailWhenMovedIntoAnotherMailbox(server: 
GuiceJamesServer): Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+
+    val andreMailbox: String = "andrecustom"
+    val andrePath = MailboxPath.forUser(ANDRE, andreMailbox)
+    val bobPath = MailboxPath.inbox(BOB)
+    mailboxProbe.createMailbox(andrePath)
+    val mailboxId: MailboxId = mailboxProbe.createMailbox(bobPath)
+
+    val messageId: MessageId = mailboxProbe
+      .appendMessage(ANDRE.asString, andrePath,
+        AppendCommand.from(
+          buildTestMessage))
+      .getMessageId
+
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(andrePath, BOB.asString, new 
MailboxACL.Rfc4314Rights(Right.Insert))
+
+    server.getProbe(classOf[JmapGuiceProbe])
+      .setInMailboxes(messageId, BOB, mailboxId)
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "destroy": ["${messageId.serialize}"]
+         |    }, "c1"],
+         |    ["Email/get", {
+         |      "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["${messageId.serialize}"]
+         |    }, "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",
+         |        "destroyed": ["${messageId.serialize}"]
+         |      }, "c1"],
+         |      ["Email/get", {
+         |        "accountId": 
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |        "state": "000001",
+         |        "list": [],
+         |        "notFound": ["${messageId.serialize}"]
+         |      }, "c2"]
+         |    ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def emailSetDestroySuccessAndFailureCanBeMixed(server: GuiceJamesServer): 
Unit = {
+    val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
+    val path = MailboxPath.inbox(BOB)
+
+    val messageId: MessageId = mailboxProbe
+      .appendMessage(BOB.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",
+         |      "destroy": [
+         |        "${messageId.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",
+         |        "destroyed": ["${messageId.serialize}"],
+         |        "notDestroyed": {
+         |          "invalid": {
+         |            "type": "invalidArguments",
+         |            "description": "invalid is not a messageId: For input 
string: \\"invalid\\""
+         |          }
+         |        }
+         |      }, "c1"]
+         |    ]
+         |}""".stripMargin)
   }
 
   private def buildTestMessage = {
diff --git 
a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
 
b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
index ec7126a..cdf72f4 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
@@ -21,10 +21,14 @@ package org.apache.james.jmap.rfc8621.memory;
 
 import static 
org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
 
+import java.util.concurrent.ThreadLocalRandom;
+
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.JamesServerBuilder;
 import org.apache.james.JamesServerExtension;
 import org.apache.james.jmap.rfc8621.contract.EmailSetMethodContract;
+import org.apache.james.mailbox.inmemory.InMemoryMessageId;
+import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.junit.jupiter.api.extension.RegisterExtension;
 
@@ -36,4 +40,9 @@ public class MemoryEmailSetMethodTest implements 
EmailSetMethodContract {
             .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
             .overrideWith(new TestJMAPServerModule()))
         .build();
+
+    @Override
+    public MessageId randomMessageId() {
+        return 
InMemoryMessageId.of(ThreadLocalRandom.current().nextInt(100000) + 100);
+    }
 }
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 d4ce747..594ceaa 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
@@ -20,21 +20,30 @@
 package org.apache.james.jmap.json
 
 import javax.inject.Inject
+import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
 import org.apache.james.jmap.mail.{DestroyIds, EmailSetRequest, 
EmailSetResponse}
+import org.apache.james.jmap.model.SetError
 import org.apache.james.mailbox.model.MessageId
-import play.api.libs.json.{Format, JsError, JsObject, JsResult, JsString, 
JsSuccess, JsValue, Json, OFormat, Reads, Writes}
+import play.api.libs.json.{Format, JsError, JsObject, JsResult, JsString, 
JsSuccess, JsValue, Json, OWrites, Reads, Writes}
+
+import scala.util.Try
 
 class EmailSetSerializer @Inject() (messageIdFactory: MessageId.Factory) {
 
   private implicit val messageIdWrites: Writes[MessageId] = messageId => 
JsString(messageId.serialize)
   private implicit val messageIdReads: Reads[MessageId] = {
-    case JsString(serializedMessageId) => 
JsSuccess(messageIdFactory.fromString(serializedMessageId))
-    case _ => JsError("Invalid messageId")
+    case JsString(serializedMessageId) => 
Try(JsSuccess(messageIdFactory.fromString(serializedMessageId)))
+      .fold(_ => JsError("Invalid messageId"), messageId => messageId)
+    case _ => JsError("Expecting messageId to be represented by a JsString")
   }
 
-  private implicit val destroyIdsFormat: Format[DestroyIds] = 
Json.valueFormat[DestroyIds]
+  private implicit val notDestroyedWrites: Writes[Map[UnparsedMessageId, 
SetError]] = mapWrites[UnparsedMessageId, SetError](_.value, setErrorWrites)
+  private implicit val destroyIdsReads: Reads[DestroyIds] = {
+    Json.valueFormat[DestroyIds]
+  }
+  private implicit val destroyIdsWrites: Writes[DestroyIds] = 
Json.valueWrites[DestroyIds]
   private implicit val emailRequestSetFormat: Format[EmailSetRequest] = 
Json.format[EmailSetRequest]
-  private implicit val emailResponseSetFormat: OFormat[EmailSetResponse] = 
Json.format[EmailSetResponse]
+  private implicit val emailResponseSetWrites: OWrites[EmailSetResponse] = 
Json.writes[EmailSetResponse]
 
   def deserialize(input: JsValue): JsResult[EmailSetRequest] = 
Json.fromJson[EmailSetRequest](input)
 
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 a47ecff..89813bb 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
@@ -18,16 +18,38 @@
  ****************************************************************/
 package org.apache.james.jmap.mail
 
-import org.apache.james.jmap.model.AccountId
+import eu.timepit.refined
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.collection.NonEmpty
+import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
+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
 
-case class DestroyIds(value: Seq[MessageId])
+import scala.util.Try
+
+object EmailSet {
+  type UnparsedMessageIdConstraint = NonEmpty
+  type UnparsedMessageId = String Refined UnparsedMessageIdConstraint
+
+  def asUnparsed(messageId: MessageId): UnparsedMessageId = 
refined.refineV[UnparsedMessageIdConstraint](messageId.serialize()) match {
+    case Left(e) => throw new IllegalArgumentException(e)
+    case scala.Right(value) => value
+  }
+
+  def parse(messageIdFactory: MessageId.Factory)(unparsed: UnparsedMessageId): 
Try[MessageId] =
+    Try(messageIdFactory.fromString(unparsed.value))
+}
+
+case class DestroyIds(value: Seq[UnparsedMessageId])
+
 case class EmailSetRequest(accountId: AccountId,
-                           destroy: Option[DestroyIds])
+                           destroy: Option[DestroyIds]) extends WithAccountId
 
 case class EmailSetResponse(accountId: AccountId,
                             newState: State,
-                            destroyed: Option[DestroyIds])
+                            destroyed: Option[DestroyIds],
+                            notDestroyed: Option[Map[UnparsedMessageId, 
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 9d086e4..304499a 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
@@ -22,64 +22,112 @@ import eu.timepit.refined.auto._
 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.{DestroyIds, EmailSetRequest, 
EmailSetResponse}
+import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
+import org.apache.james.jmap.mail.{DestroyIds, EmailSet, EmailSetRequest, 
EmailSetResponse}
 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.{Capabilities, Invocation, State}
-import org.apache.james.mailbox.model.DeleteResult
+import org.apache.james.jmap.model.SetError.SetErrorDescription
+import org.apache.james.jmap.model.{Capabilities, Invocation, SetError, State}
+import org.apache.james.mailbox.model.{DeleteResult, MessageId}
 import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
 import org.apache.james.metrics.api.MetricFactory
-import org.reactivestreams.Publisher
 import play.api.libs.json.{JsError, JsSuccess}
-import reactor.core.scala.publisher.SMono
+import reactor.core.scala.publisher.{SFlux, SMono}
 import reactor.core.scheduler.Schedulers
 
 import scala.jdk.CollectionConverters._
 
+case class MessageNotFoundExeception(messageId: MessageId) extends Exception
+
+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)
+  }
+}
+
+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))
+  }
+}
+
 class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
                                messageIdManager: MessageIdManager,
+                               messageIdFactory: MessageId.Factory,
                                val metricFactory: MetricFactory,
-                               val sessionSupplier: SessionSupplier) extends 
Method {
+                               val sessionSupplier: SessionSupplier) extends 
MethodRequiringAccountId[EmailSetRequest] {
 
   override val methodName: MethodName = MethodName("Email/set")
   override val requiredCapabilities: Capabilities = 
Capabilities(CORE_CAPABILITY, MAIL_CAPABILITY)
 
-  override def process(capabilities: Set[CapabilityIdentifier], invocation: 
InvocationWithContext, mailboxSession: MailboxSession): 
Publisher[InvocationWithContext] = {
-    asEmailSetRequest(invocation.invocation.arguments)
-        .flatMap(request => {
-          for {
-            destroyed <- destroy(request, mailboxSession)
-          } yield {
-            InvocationWithContext(
-              invocation = Invocation(
-                methodName = invocation.invocation.methodName,
-                arguments = Arguments(serializer.serialize(EmailSetResponse(
-                  accountId = request.accountId,
-                  newState = State.INSTANCE,
-                  destroyed = destroyed))),
-                methodCallId = invocation.invocation.methodCallId),
-              processingContext = invocation.processingContext
-            )
-          }
-        })
+  override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: 
InvocationWithContext, mailboxSession: MailboxSession, request: 
EmailSetRequest): SMono[InvocationWithContext] = {
+    for {
+      destroyResults <- destroy(request, mailboxSession)
+    } yield InvocationWithContext(
+        invocation = Invocation(
+          methodName = invocation.invocation.methodName,
+          arguments = Arguments(serializer.serialize(EmailSetResponse(
+            accountId = request.accountId,
+            newState = State.INSTANCE,
+            destroyed = destroyResults.destroyed,
+            notDestroyed = destroyResults.notDestroyed))),
+          methodCallId = invocation.invocation.methodCallId),
+        processingContext = invocation.processingContext)
   }
 
-  private def asEmailSetRequest(arguments: Arguments): SMono[EmailSetRequest] 
= {
+  override def getRequest(mailboxSession: MailboxSession, invocation: 
Invocation): SMono[EmailSetRequest] = asEmailSetRequest(invocation.arguments)
+
+  private def asEmailSetRequest(arguments: Arguments): SMono[EmailSetRequest] =
     serializer.deserialize(arguments.value) match {
-      case JsSuccess(request, _) => SMono.just(request)
+      case JsSuccess(emailSetRequest, _) => SMono.just(emailSetRequest)
       case errors: JsError => SMono.raiseError(new 
IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
-  }
 
-  private def destroy(emailSetRequest: EmailSetRequest, mailboxSession: 
MailboxSession): SMono[Option[DestroyIds]] =
-    emailSetRequest.destroy
-      .map(destroyId => deleteMessages(destroyId, mailboxSession))
-      .getOrElse(SMono.just(None))
+  private def destroy(emailSetRequest: EmailSetRequest, mailboxSession: 
MailboxSession): SMono[DestroyResults] =
+    
SFlux.fromIterable(emailSetRequest.destroy.getOrElse(DestroyIds(Seq())).value)
+      .flatMap(id => deleteMessage(id, mailboxSession))
+      .collectSeq()
+      .map(DestroyResults)
 
-  private def deleteMessages(destroyIds: DestroyIds, mailboxSession: 
MailboxSession): SMono[Option[DestroyIds]] = {
-    SMono.fromCallable[DeleteResult](() => 
messageIdManager.delete(destroyIds.value.asJava, mailboxSession))
-      .subscribeOn(Schedulers.elastic)
-      .map(deleteResult => 
Some(DestroyIds(deleteResult.getDestroyed.asScala.toSeq)))
-  }
+  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)))
 }


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

Reply via email to