You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2021/11/26 04:34:57 UTC

[james-project] 01/03: JAMES-3534 Identity/set destroy

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 4d1628283b3a282404569b2f306fd24b924b0c22
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Mon Nov 22 15:00:59 2021 +0700

    JAMES-3534 Identity/set destroy
---
 .../jmap/api/identity/CustomIdentityDAO.scala      |  18 +-
 .../distributed/DistributedIdentitySetTest.java    |   3 +-
 .../rfc8621/contract/IdentitySetContract.scala     | 369 ++++++++++++++++++++-
 .../memory/MemoryIdentitySetMethodTests.java       |   3 +-
 .../james/jmap/json/IdentitySerializer.scala       |  11 +-
 .../org/apache/james/jmap/mail/IdentitySet.scala   |   7 +-
 .../jmap/method/IdentitySetDeletePerformer.scala   |  73 ++++
 .../james/jmap/method/IdentitySetMethod.scala      |   6 +-
 8 files changed, 476 insertions(+), 14 deletions(-)

diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
index 155e68e..55e88e9 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
@@ -103,6 +103,9 @@ class DefaultIdentitySupplier @Inject()(canSendFrom: CanSendFrom, usersRepositor
   def userCanSendFrom(username: Username, mailAddress: MailAddress): Boolean =
     canSendFrom.userCanSendFrom(username, usersRepository.getUsername(mailAddress))
 
+  def isServerSetIdentity(username: Username, id: IdentityId): Boolean =
+    listIdentities(username).map(_.id).contains(id)
+
   private def from(address: MailAddress): Option[IdentityId] =
     Try(UUID.nameUUIDFromBytes(address.asString().getBytes(StandardCharsets.UTF_8)))
       .toEither
@@ -128,7 +131,18 @@ class IdentityRepository @Inject()(customIdentityDao: CustomIdentityDAO, identit
 
   def update(user: Username, identityId: IdentityId, identityUpdate: IdentityUpdate): Publisher[Unit] = customIdentityDao.update(user, identityId, identityUpdate)
 
-  def delete(username: Username, ids: Seq[IdentityId]): Publisher[Unit] = customIdentityDao.delete(username, ids)
+  def delete(username: Username, ids: Seq[IdentityId]): Publisher[Unit] =
+    SMono.just(ids)
+      .handle[Seq[IdentityId]]{
+        case (ids, sink) => if (identityFactory.isServerSetIdentity(username, ids.head)) {
+          sink.error(IdentityForbiddenDeleteException(ids.head))
+        } else {
+          sink.next(ids)
+        }
+      }
+      .flatMap(ids => SMono.fromPublisher(customIdentityDao.delete(username, ids)))
+      .subscribeOn(Schedulers.elastic())
 }
 
-case class IdentityNotFoundException(id: IdentityId) extends RuntimeException(s"$id could not be found")
\ No newline at end of file
+case class IdentityNotFoundException(id: IdentityId) extends RuntimeException(s"$id could not be found")
+case class IdentityForbiddenDeleteException(id: IdentityId) extends IllegalArgumentException(s"User do not have permission to delete $id")
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedIdentitySetTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedIdentitySetTest.java
index 6c28c60..9cad99c 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedIdentitySetTest.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedIdentitySetTest.java
@@ -26,6 +26,7 @@ import org.apache.james.DockerElasticSearchExtension;
 import org.apache.james.JamesServerBuilder;
 import org.apache.james.JamesServerExtension;
 import org.apache.james.SearchConfiguration;
+import org.apache.james.jmap.rfc8621.contract.IdentityProbeModule;
 import org.apache.james.jmap.rfc8621.contract.IdentitySetContract;
 import org.apache.james.modules.AwsS3BlobStoreExtension;
 import org.apache.james.modules.RabbitMQExtension;
@@ -51,6 +52,6 @@ public class DistributedIdentitySetTest implements IdentitySetContract {
         .extension(new RabbitMQExtension())
         .extension(new AwsS3BlobStoreExtension())
         .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
-            .overrideWith(new TestJMAPServerModule()))
+            .overrideWith(new TestJMAPServerModule(), new IdentityProbeModule()))
         .build();
 }
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/IdentitySetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
index 2614aa4..154b081 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
@@ -19,6 +19,9 @@
 
 package org.apache.james.jmap.rfc8621.contract
 
+import java.nio.charset.StandardCharsets
+import java.util.UUID
+
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.http.ContentType.JSON
@@ -27,15 +30,26 @@ import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
 import net.javacrumbs.jsonunit.core.internal.Options
 import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
+import org.apache.james.core.MailAddress
+import org.apache.james.jmap.api.identity.IdentityCreationRequest
+import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, HtmlSignature, IdentityName, TextSignature}
 import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
 import org.apache.james.jmap.core.UuidState.INSTANCE
 import org.apache.james.jmap.http.UserCredential
 import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.IdentitySetContract.IDENTITY_CREATION_REQUEST
 import org.apache.james.utils.DataProbeImpl
 import org.junit.jupiter.api.{BeforeEach, Test}
+import reactor.core.scala.publisher.SMono
 
-import java.util.UUID
-
+object IdentitySetContract {
+  val IDENTITY_CREATION_REQUEST = IdentityCreationRequest(name = Some(IdentityName("Bob (custom address)")),
+    email = BOB.asMailAddress(),
+    replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss")), new MailAddress("boss@domain.tld")))),
+    bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2")), new MailAddress("boss2@domain.tld")))),
+    textSignature = Some(TextSignature("text signature")),
+    htmlSignature = Some(HtmlSignature("html signature")))
+}
 trait IdentitySetContract {
   @BeforeEach
   def setUp(server: GuiceJamesServer): Unit = {
@@ -91,7 +105,6 @@ trait IdentitySetContract {
          |}""".stripMargin
 
     val response =  `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
     .when
       .post
@@ -349,7 +362,6 @@ trait IdentitySetContract {
 
     val response: String = `given`
       .body(request)
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
     .when
       .post
     .`then`
@@ -406,7 +418,6 @@ trait IdentitySetContract {
 
     val response: String = `given`
       .body(request)
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
     .when
       .post
     .`then`
@@ -469,7 +480,6 @@ trait IdentitySetContract {
 
     val response: String = `given`
       .body(request)
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
     .when
       .post
     .`then`
@@ -1191,4 +1201,351 @@ trait IdentitySetContract {
       .extract()
       .jsonPath()
       .get(s"methodResponses[0][1].created.$clientId.id")
+
+  @Test
+  def destroyShouldSucceedWhenDeleteCustomIdentity(server: GuiceJamesServer): Unit = {
+    val id = SMono(server.getProbe(classOf[IdentityProbe])
+      .save(BOB, IDENTITY_CREATION_REQUEST))
+      .block()
+      .id.id.toString
+
+    val request: String =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"destroy": ["$id"]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Identity/set",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"destroyed": ["$id"]
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def destroyShouldFailWhenDeleteServerSetIdentities(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob-alias", "domain.tld", "bob@domain.tld")
+    val defaultServerSetIdentity = UUID.nameUUIDFromBytes("bob@domain.tld".getBytes(StandardCharsets.UTF_8))
+    val serverIdentitiesId1 = UUID.nameUUIDFromBytes("bob-alias@domain.tld".getBytes(StandardCharsets.UTF_8))
+
+    val request: String =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"destroy": ["$serverIdentitiesId1", "$defaultServerSetIdentity"]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Identity/set",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"notDestroyed": {
+           |					"$serverIdentitiesId1": {
+           |						"type": "forbidden",
+           |						"description": "User do not have permission to delete IdentityId($serverIdentitiesId1)",
+           |						"properties": [
+           |							"id"
+           |						]
+           |					},
+           |					"$defaultServerSetIdentity": {
+           |						"type": "forbidden",
+           |						"description": "User do not have permission to delete IdentityId($defaultServerSetIdentity)",
+           |						"properties": [
+           |							"id"
+           |						]
+           |					}
+           |				}
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def destroyShouldFailWhenInvalidId(): Unit = {
+    val request: String =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"destroy": ["invalid"]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Identity/set",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"notDestroyed": {
+           |					"invalid": {
+           |						"type": "invalidArguments",
+           |						"description": "invalid is not a IdentityId: Invalid UUID string: invalid"
+           |					}
+           |				}
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def destroyShouldNotFailWhenUnknownId(): Unit = {
+    val id = UUID.randomUUID().toString
+
+    val request: String =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"destroy": ["$id"]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Identity/set",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"destroyed": ["$id"]
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def destroyShouldHandleMixedCases(server: GuiceJamesServer): Unit = {
+    val customId1 = SMono(server.getProbe(classOf[IdentityProbe])
+      .save(BOB, IDENTITY_CREATION_REQUEST))
+      .block()
+      .id.id.toString
+    val customId2 = SMono(server.getProbe(classOf[IdentityProbe])
+      .save(BOB, IDENTITY_CREATION_REQUEST))
+      .block()
+      .id.id.toString
+    val defaultServerSetIdentity = UUID.nameUUIDFromBytes("bob@domain.tld".getBytes(StandardCharsets.UTF_8))
+
+    val request: String =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"destroy": ["$customId1", "$customId2", "$defaultServerSetIdentity"]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .when(net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Identity/set",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"destroyed": ["$customId1", "$customId2"],
+           |				"notDestroyed": {
+           |					"$defaultServerSetIdentity": {
+           |						"type": "forbidden",
+           |						"description": "User do not have permission to delete IdentityId($defaultServerSetIdentity)",
+           |						"properties": [
+           |							"id"
+           |						]
+           |					}
+           |				}
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def deletedIdentityShouldNotBeFetchedAnyMore(server: GuiceJamesServer): Unit = {
+    val id = SMono(server.getProbe(classOf[IdentityProbe])
+      .save(BOB, IDENTITY_CREATION_REQUEST))
+      .block()
+      .id.id.toString
+
+    val request1: String =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"destroy": ["$id"]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    `given`
+      .body(request1)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+         |  "methodCalls": [[
+         |    "Identity/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["$id"]
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    val response =  `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"notFound": [
+           |		"$id"
+           |	],
+           |	"state": "${INSTANCE.value}",
+           |	"list": []
+           |}""".stripMargin)
+  }
+
 }
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/MemoryIdentitySetMethodTests.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryIdentitySetMethodTests.java
index 1989a83..4a1aead 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryIdentitySetMethodTests.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryIdentitySetMethodTests.java
@@ -22,6 +22,7 @@ package org.apache.james.jmap.rfc8621.memory;
 import org.apache.james.JamesServerBuilder;
 import org.apache.james.JamesServerExtension;
 import org.apache.james.MemoryJamesServerMain;
+import org.apache.james.jmap.rfc8621.contract.IdentityProbeModule;
 import org.apache.james.jmap.rfc8621.contract.IdentitySetContract;
 import org.apache.james.modules.TestJMAPServerModule;
 import org.junit.jupiter.api.extension.RegisterExtension;
@@ -30,6 +31,6 @@ public class MemoryIdentitySetMethodTests implements IdentitySetContract {
     @RegisterExtension
     static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
         .server(configuration -> MemoryJamesServerMain.createServer(configuration)
-            .overrideWith(new TestJMAPServerModule()))
+            .overrideWith(new TestJMAPServerModule(), new IdentityProbeModule()))
         .build();
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala
index 171c7df..b2dc894 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala
@@ -19,6 +19,7 @@
 
 package org.apache.james.jmap.json
 
+import eu.timepit.refined
 import eu.timepit.refined.refineV
 import org.apache.james.jmap.api.identity.{IdentityBccUpdate, IdentityCreationRequest, IdentityHtmlSignatureUpdate, IdentityNameUpdate, IdentityReplyToUpdate, IdentityTextSignatureUpdate, IdentityUpdateRequest}
 import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
@@ -26,11 +27,19 @@ import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.{Properties, SetError, UuidState}
 import org.apache.james.jmap.mail._
 import org.apache.james.jmap.method.IdentitySetUpdatePerformer.IdentitySetUpdateResponse
-import play.api.libs.json.{Format, JsArray, JsError, JsObject, JsResult, JsSuccess, JsValue, Json, OWrites, Reads, Writes, __}
+import play.api.libs.json.{Format, JsArray, JsError, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes, __}
 
 object IdentitySerializer {
   private implicit val emailerNameReads: Format[EmailerName] = Json.valueFormat[EmailerName]
   private implicit val identityIdFormat: Format[IdentityId] = Json.valueFormat[IdentityId]
+  private implicit val unparsedIdentityIdReads: Reads[UnparsedIdentityId] = {
+    case JsString(string) =>
+      refined.refineV[IdConstraint](string)
+        .fold(
+          e => JsError(s"identityId does not match Id constraints: $e"),
+          id => JsSuccess(UnparsedIdentityId(id)))
+    case _ => JsError("identityId needs to be represented by a JsString")
+  }
   private implicit val identityIdUnparsedFormat: Format[UnparsedIdentityId] = Json.valueFormat[UnparsedIdentityId]
   private implicit val identityIdsFormat: Format[IdentityIds] = Json.valueFormat[IdentityIds]
   private implicit val emailAddressReads: Format[EmailAddress] = Json.format[EmailAddress]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentitySet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentitySet.scala
index 799c0a2..33359b7 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentitySet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentitySet.scala
@@ -50,7 +50,8 @@ object IdentityCreation {
 
 case class IdentitySetRequest(accountId: AccountId,
                               create: Option[Map[IdentityCreationId, JsObject]],
-                              update: Option[Map[UnparsedIdentityId, JsObject]]) extends WithAccountId
+                              update: Option[Map[UnparsedIdentityId, JsObject]],
+                              destroy: Option[Seq[UnparsedIdentityId]]) extends WithAccountId
 
 case class IdentityCreationId(id: Id) {
   def serialise: String = id.value
@@ -68,7 +69,9 @@ case class IdentitySetResponse(accountId: AccountId,
                                created: Option[Map[IdentityCreationId, IdentityCreationResponse]],
                                notCreated: Option[Map[IdentityCreationId, SetError]],
                                updated: Option[Map[IdentityId, IdentitySetUpdateResponse]],
-                               notUpdated: Option[Map[UnparsedIdentityId, SetError]])
+                               notUpdated: Option[Map[UnparsedIdentityId, SetError]],
+                               destroyed: Option[Seq[IdentityId]],
+                               notDestroyed: Option[Map[UnparsedIdentityId, SetError]])
 
 case class IdentitySetParseException(setError: SetError) extends IllegalArgumentException
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetDeletePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetDeletePerformer.scala
new file mode 100644
index 0000000..72eac97
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetDeletePerformer.scala
@@ -0,0 +1,73 @@
+/****************************************************************
+ * 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        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * 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                 *
+ *                                                              *
+ * 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.method
+
+import eu.timepit.refined.auto._
+import javax.inject.Inject
+import org.apache.james.jmap.api.identity.{IdentityForbiddenDeleteException, IdentityRepository}
+import org.apache.james.jmap.api.model.IdentityId
+import org.apache.james.jmap.core.{Properties, SetError}
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.mail.{IdentitySetRequest, UnparsedIdentityId}
+import org.apache.james.jmap.method.IdentitySetDeletePerformer.{IdentityDeletionFailure, IdentityDeletionResult, IdentityDeletionResults, IdentityDeletionSuccess}
+import org.apache.james.mailbox.MailboxSession
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+object IdentitySetDeletePerformer {
+  sealed trait IdentityDeletionResult
+  case class IdentityDeletionSuccess(identityId: IdentityId) extends IdentityDeletionResult
+  case class IdentityDeletionFailure(identityId: UnparsedIdentityId, exception: Throwable) extends IdentityDeletionResult {
+    def asIdentitySetError: SetError = exception match {
+      case e: IdentityForbiddenDeleteException => SetError.forbidden(SetErrorDescription(e.getMessage), Properties("id"))
+      case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(s"${identityId.id} is not a IdentityId: ${e.getMessage}"))
+      case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
+    }
+  }
+  case class IdentityDeletionResults(results: Seq[IdentityDeletionResult]) {
+    def destroyed: Seq[IdentityId] =
+      results.flatMap(result => result match {
+        case success: IdentityDeletionSuccess => Some(success)
+        case _ => None
+      }).map(_.identityId)
+
+    def retrieveErrors: Map[UnparsedIdentityId, SetError] =
+      results.flatMap(result => result match {
+        case failure: IdentityDeletionFailure => Some(failure.identityId, failure.asIdentitySetError)
+        case _ => None
+      })
+        .toMap
+  }
+}
+
+class IdentitySetDeletePerformer @Inject()(identityRepository: IdentityRepository) {
+  def destroy(identitySetRequest: IdentitySetRequest, mailboxSession: MailboxSession): SMono[IdentityDeletionResults] =
+    SFlux.fromIterable(identitySetRequest.destroy.getOrElse(Seq()))
+      .flatMap(unparsedId => delete(unparsedId, mailboxSession)
+        .onErrorRecover(e => IdentityDeletionFailure(unparsedId, e)),
+        maxConcurrency = 5)
+      .collectSeq()
+      .map(IdentityDeletionResults)
+
+  private def delete(unparsedId: UnparsedIdentityId, mailboxSession: MailboxSession): SMono[IdentityDeletionResult] =
+    unparsedId.validate
+      .fold(e => SMono.error(e),
+        id => SMono.fromPublisher(identityRepository.delete(mailboxSession.getUser, Seq(id)))
+          .`then`(SMono.just[IdentityDeletionResult](IdentityDeletionSuccess(id))))
+}
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetMethod.scala
index 11f2e44..8f9fd64 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetMethod.scala
@@ -34,6 +34,7 @@ import reactor.core.scala.publisher.SMono
 
 class IdentitySetMethod @Inject()(createPerformer: IdentitySetCreatePerformer,
                                   updatePerformer: IdentitySetUpdatePerformer,
+                                  deletePerformer: IdentitySetDeletePerformer,
                                   val metricFactory: MetricFactory,
                                   val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[IdentitySetRequest] {
   override val methodName: Invocation.MethodName = MethodName("Identity/set")
@@ -49,6 +50,7 @@ class IdentitySetMethod @Inject()(createPerformer: IdentitySetCreatePerformer,
     for {
       creationResults <- createPerformer.create(request, mailboxSession)
       updatedResults <- updatePerformer.update(request, mailboxSession)
+      destroyResults <- deletePerformer.destroy(request, mailboxSession)
     } yield InvocationWithContext(
       invocation = Invocation(
         methodName = methodName,
@@ -59,7 +61,9 @@ class IdentitySetMethod @Inject()(createPerformer: IdentitySetCreatePerformer,
           created = creationResults.created.filter(_.nonEmpty),
           notCreated = creationResults.notCreated.filter(_.nonEmpty),
           updated = Some(updatedResults.updated).filter(_.nonEmpty),
-          notUpdated = Some(updatedResults.notUpdated).filter(_.nonEmpty)))),
+          notUpdated = Some(updatedResults.notUpdated).filter(_.nonEmpty),
+          destroyed = Some(destroyResults.destroyed).filter(_.nonEmpty),
+          notDestroyed = Some(destroyResults.retrieveErrors).filter(_.nonEmpty)))),
         methodCallId = invocation.invocation.methodCallId),
       processingContext = creationResults.created.getOrElse(Map())
         .foldLeft(invocation.processingContext)({

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