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:56 UTC

[james-project] branch master updated (1f32521 -> 95f8e77)

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

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


    from 1f32521  JAMES-3534 Implement Identity/set update (custom identities) (#754)
     new 4d16282  JAMES-3534 Identity/set destroy
     new 106f528  JAMES-3534 SetError forbidden should not always require Properties
     new 95f8e77  JAMES-3534 Identity/get should not fail when response with empty List[Identity]

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../jmap/api/identity/CustomIdentityDAO.scala      |  18 +-
 .../distributed/DistributedIdentitySetTest.java    |   3 +-
 .../rfc8621/contract/IdentityGetContract.scala     |  50 +++
 .../rfc8621/contract/IdentitySetContract.scala     | 360 ++++++++++++++++++++-
 .../memory/MemoryIdentitySetMethodTests.java       |   3 +-
 .../org/apache/james/jmap/core/SetError.scala      |   4 +-
 .../james/jmap/json/IdentitySerializer.scala       |  11 +-
 .../org/apache/james/jmap/mail/IdentityGet.scala   |   6 +-
 .../org/apache/james/jmap/mail/IdentitySet.scala   |   7 +-
 .../jmap/method/IdentitySetDeletePerformer.scala   |  73 +++++
 .../james/jmap/method/IdentitySetMethod.scala      |   6 +-
 .../jmap/method/MailboxSetCreatePerformer.scala    |   2 +-
 12 files changed, 523 insertions(+), 20 deletions(-)
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetDeletePerformer.scala

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


[james-project] 02/03: JAMES-3534 SetError forbidden should not always require Properties

Posted by rc...@apache.org.
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 106f528535348c8f8cef7ac2e0c76109600c32b1
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Tue Nov 23 09:31:01 2021 +0700

    JAMES-3534 SetError forbidden should not always require Properties
---
 .../james/jmap/rfc8621/contract/IdentitySetContract.scala | 15 +++------------
 .../main/scala/org/apache/james/jmap/core/SetError.scala  |  4 ++--
 .../james/jmap/method/IdentitySetDeletePerformer.scala    |  2 +-
 .../james/jmap/method/MailboxSetCreatePerformer.scala     |  2 +-
 4 files changed, 7 insertions(+), 16 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/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 154b081..f628c06 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
@@ -1298,17 +1298,11 @@ trait IdentitySetContract {
            |				"notDestroyed": {
            |					"$serverIdentitiesId1": {
            |						"type": "forbidden",
-           |						"description": "User do not have permission to delete IdentityId($serverIdentitiesId1)",
-           |						"properties": [
-           |							"id"
-           |						]
+           |						"description": "User do not have permission to delete IdentityId($serverIdentitiesId1)"
            |					},
            |					"$defaultServerSetIdentity": {
            |						"type": "forbidden",
-           |						"description": "User do not have permission to delete IdentityId($defaultServerSetIdentity)",
-           |						"properties": [
-           |							"id"
-           |						]
+           |						"description": "User do not have permission to delete IdentityId($defaultServerSetIdentity)"
            |					}
            |				}
            |			},
@@ -1470,10 +1464,7 @@ trait IdentitySetContract {
            |				"notDestroyed": {
            |					"$defaultServerSetIdentity": {
            |						"type": "forbidden",
-           |						"description": "User do not have permission to delete IdentityId($defaultServerSetIdentity)",
-           |						"properties": [
-           |							"id"
-           |						]
+           |						"description": "User do not have permission to delete IdentityId($defaultServerSetIdentity)"
            |					}
            |				}
            |			},
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/SetError.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/SetError.scala
index 0379b73..8b99e76 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/SetError.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/SetError.scala
@@ -54,8 +54,8 @@ object SetError {
   def invalidPatch(description: SetErrorDescription): SetError =
     SetError(invalidPatchValue, description, None)
 
-  def forbidden(description: SetErrorDescription, properties: Properties): SetError =
-    SetError(forbiddenValue, description, Some(properties))
+  def forbidden(description: SetErrorDescription, properties: Option[Properties] = None): SetError =
+    SetError(forbiddenValue, description, properties)
 
   def stateMismatch(description: SetErrorDescription, properties: Properties): SetError =
     SetError(stateMismatchValue, description, Some(properties))
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
index 72eac97..6233785 100644
--- 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
@@ -35,7 +35,7 @@ object IdentitySetDeletePerformer {
   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: IdentityForbiddenDeleteException => SetError.forbidden(SetErrorDescription(e.getMessage))
       case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(s"${identityId.id} is not a IdentityId: ${e.getMessage}"))
       case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
     }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala
index 87a25f7..e411128 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala
@@ -49,7 +49,7 @@ object MailboxSetCreatePerformer {
       case e: MailboxExistsException => SetError.invalidArguments(SetErrorDescription(e.getMessage), Some(Properties("name")))
       case e: MailboxNameException => SetError.invalidArguments(SetErrorDescription(e.getMessage), Some(Properties("name")))
       case e: MailboxCreationParseException => e.setError
-      case _: InsufficientRightsException => SetError.forbidden(SetErrorDescription("Insufficient rights"), Properties("parentId"))
+      case _: InsufficientRightsException => SetError.forbidden(SetErrorDescription("Insufficient rights"), Some(Properties("parentId")))
       case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
     }
   }

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


[james-project] 03/03: JAMES-3534 Identity/get should not fail when response with empty List[Identity]

Posted by rc...@apache.org.
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 95f8e7703ac98ab72e80a682d7ca342d3fc856ca
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Wed Nov 24 14:06:23 2021 +0700

    JAMES-3534 Identity/get should not fail when response with empty List[Identity]
    
    Otherwise we have NoSuchElementException when get a None
---
 .../rfc8621/contract/IdentityGetContract.scala     | 50 ++++++++++++++++++++++
 .../org/apache/james/jmap/mail/IdentityGet.scala   |  6 +--
 2 files changed, 53 insertions(+), 3 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/IdentityGetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
index ee6e70d..69219e0 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
@@ -19,6 +19,8 @@
 
 package org.apache.james.jmap.rfc8621.contract
 
+import java.util.UUID
+
 import com.google.inject.AbstractModule
 import com.google.inject.multibindings.Multibinder
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
@@ -516,4 +518,52 @@ trait IdentityGetContract {
           |    ]
           |}""".stripMargin)
   }
+
+  @Test
+  def getRandomIdShouldNotFail(): Unit = {
+    val id = UUID.randomUUID().toString
+    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`
+      .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": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"methodResponses": [
+           |		[
+           |			"Identity/get",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"notFound": [
+           |					"$id"
+           |				],
+           |				"state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |				"list": []
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentityGet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentityGet.scala
index 9047d64..1ab37e5 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentityGet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentityGet.scala
@@ -59,9 +59,9 @@ case class IdentityGetRequest(accountId: AccountId,
                               ids: Option[IdentityIds],
                               properties: Option[Properties]) extends WithAccountId {
   def computeResponse(identities: List[Identity]): IdentityGetResponse = {
-    val list: Option[List[Identity]] = Some(identities.filter(identity => isRequested(identity.id))).filter(_.nonEmpty)
+    val list: List[Identity] = identities.filter(identity => isRequested(identity.id))
     val notFound: Option[IdentityIds] = ids
-      .map(ids => ids.distinct(list.getOrElse(List()).map(_.id)))
+      .map(ids => ids.distinct(list.map(_.id)))
       .filter(_.nonEmpty)
 
     IdentityGetResponse(
@@ -76,5 +76,5 @@ case class IdentityGetRequest(accountId: AccountId,
 
 case class IdentityGetResponse(accountId: AccountId,
                                state: UuidState,
-                               list: Option[List[Identity]],
+                               list: List[Identity],
                                notFound: Option[IdentityIds])

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


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

Posted by rc...@apache.org.
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