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