You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2020/08/19 07:38:30 UTC
[james-project] 01/11: JAMES-3355 Mailbox/set destroy implementation
This is an automated email from the ASF dual-hosted git repository.
btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit a0534dacbf5250687986963ebeb291f58ecfdb6a
Author: RĂ©mi Kowalski <rk...@linagora.com>
AuthorDate: Wed Jul 29 17:03:49 2020 +0200
JAMES-3355 Mailbox/set destroy implementation
---
.../contract/MailboxSetMethodContract.scala | 485 ++++++++++++++++++++-
.../org/apache/james/jmap/mail/MailboxSet.scala | 13 +-
.../james/jmap/method/MailboxSetMethod.scala | 72 ++-
.../org/apache/james/jmap/model/Invocation.scala | 1 -
.../apache/james/jmap/routes/JMAPApiRoutes.scala | 1 -
5 files changed, 555 insertions(+), 17 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/MailboxSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
index d248288..8a67f7a 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxSetMethodContract.scala
@@ -19,16 +19,22 @@
package org.apache.james.jmap.rfc8621.contract
+import java.nio.charset.StandardCharsets
+
import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
import io.restassured.RestAssured._
import io.restassured.http.ContentType.JSON
import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import net.javacrumbs.jsonunit.core.Option
+import net.javacrumbs.jsonunit.core.internal.Options
import org.apache.http.HttpStatus.SC_OK
import org.apache.james.GuiceJamesServer
import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture._
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.mailbox.MessageManager.AppendCommand
import org.apache.james.mailbox.model.MailboxACL.Right
import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath}
+import org.apache.james.mime4j.dom.Message
import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
import org.apache.james.utils.DataProbeImpl
import org.assertj.core.api.Assertions
@@ -438,7 +444,7 @@ trait MailboxSetMethodContract {
}
@Test
- def mailboxSetShouldNotCreateMailboxWhenParentIdNotFound(server: GuiceJamesServer): Unit = {
+ def mailboxSetShouldNotCreateMailboxWhenParentIdNotFound(): Unit = {
val mailboxId: MailboxId = randomMailboxId
val request=
s"""
@@ -669,4 +675,479 @@ trait MailboxSetMethodContract {
| "c1"]]
|}""".stripMargin)
}
+
+ @Test
+ def deleteShouldSucceedWhenMailboxExists(server: GuiceJamesServer): Unit = {
+ val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "destroy": ["${mailboxId.serialize}"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "destroyed": ["${mailboxId.serialize}"]
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def deleteShouldRemoveExistingMailbox(server: GuiceJamesServer): Unit = {
+ val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "mailbox"))
+
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "destroy": ["${mailboxId.serialize}"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail",
+ | "urn:apache:james:params:jmap:mail:quota"],
+ | "methodCalls": [[
+ | "Mailbox/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${mailboxId.serialize()}"]
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`()
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "Mailbox/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ |
+ | ],
+ | "notFound": [
+ | "${mailboxId.serialize()}"
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def deleteShouldRemoveExistingMailboxes(server: GuiceJamesServer): Unit = {
+ val mailboxId1: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
+ val mailboxId2: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "mailbox2"))
+
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "destroy": ["${mailboxId1.serialize}", "${mailboxId2.serialize}"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(s"""{
+ | "using": [
+ | "urn:ietf:params:jmap:core",
+ | "urn:ietf:params:jmap:mail",
+ | "urn:apache:james:params:jmap:mail:quota"],
+ | "methodCalls": [[
+ | "Mailbox/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": ["${mailboxId1.serialize()}", "${mailboxId2.serialize()}"]
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ .when
+ .post
+ .`then`()
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .withOptions(new Options(Option.IGNORING_ARRAY_ORDER))
+ .isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "Mailbox/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ |
+ | ],
+ | "notFound": [
+ | "${mailboxId1.serialize()}", "${mailboxId2.serialize()}"
+ | ]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def deleteShouldFailWhenMailboxDoesNotExist(): Unit = {
+ val mailboxId = randomMailboxId
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "destroy": ["${mailboxId.serialize()}"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notDestroyed": {
+ | "${mailboxId.serialize()}": {
+ | "type": "notFound",
+ | "description": "${mailboxId.serialize()} can not be found"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def deleteShouldFailWhenMailboxIsNotEmpty(server: GuiceJamesServer): Unit = {
+ val message: Message = Message.Builder
+ .of
+ .setSubject("test")
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+
+ val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
+ server.getProbe(classOf[MailboxProbeImpl])
+ .appendMessage(BOB.asString, MailboxPath.forUser(BOB, "mailbox1"), AppendCommand.from(message))
+
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "destroy": ["${mailboxId.serialize()}"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notDestroyed": {
+ | "${mailboxId.serialize()}": {
+ | "type": "mailboxHasEmail",
+ | "description": "${mailboxId.serialize()} is not empty"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def deleteShouldFailWhenMailboxHasChild(server: GuiceJamesServer): Unit = {
+ val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "mailbox1"))
+ server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.forUser(BOB, "mailbox1.mailbox2"))
+
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "destroy": ["${mailboxId.serialize()}"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notDestroyed": {
+ | "${mailboxId.serialize()}": {
+ | "type": "mailboxHasChild",
+ | "description": "${mailboxId.serialize()} has child mailboxes"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def deleteShouldFailWhenNotEnoughRights(server: GuiceJamesServer): Unit = {
+ val path = MailboxPath.forUser(ANDRE, "mailbox")
+ val mailboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+ server.getProbe(classOf[ACLProbeImpl])
+ .replaceRights(path, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read, Right.CreateMailbox))
+
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "destroy": ["${mailboxId.serialize()}"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notDestroyed": {
+ | "${mailboxId.serialize()}": {
+ | "type": "notFound",
+ | "description": "#private:andre@domain.tld:mailbox"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def deleteShouldHandleInvalidMailboxId(): Unit = {
+ val request =
+ s"""
+ |{
+ | "using": [ "urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail" ],
+ | "methodCalls": [
+ | [
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "destroy": ["invalid"]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}
+ |""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .log().ifValidationFails()
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response).isEqualTo(
+ s"""{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [[
+ | "Mailbox/set",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "newState": "000001",
+ | "notDestroyed": {
+ | "invalid": {
+ | "type": "invalidArguments",
+ | "description": "invalid is not a mailboxId"
+ | }
+ | }
+ | },
+ | "c1"]]
+ |}""".stripMargin)
+ }
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
index a55a8a3..2b2fc13 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/MailboxSet.scala
@@ -23,7 +23,7 @@ import eu.timepit.refined.api.Refined
import eu.timepit.refined.auto._
import eu.timepit.refined.collection.NonEmpty
import org.apache.james.jmap.mail.MailboxName.MailboxName
-import org.apache.james.jmap.mail.MailboxSetRequest.MailboxCreationId
+import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, UnparsedMailboxId}
import org.apache.james.jmap.model.AccountId
import org.apache.james.jmap.model.State.State
import org.apache.james.mailbox.Role
@@ -34,11 +34,12 @@ case class MailboxSetRequest(accountId: AccountId,
ifInState: Option[State],
create: Option[Map[MailboxCreationId, JsObject]],
update: Option[Map[MailboxId, MailboxPatchObject]],
- destroy: Option[Seq[MailboxId]],
+ destroy: Option[Seq[UnparsedMailboxId]],
onDestroyRemoveEmails: Option[RemoveEmailsOnDestroy])
object MailboxSetRequest {
type MailboxCreationId = String Refined NonEmpty
+ type UnparsedMailboxId = String Refined NonEmpty
}
case class RemoveEmailsOnDestroy(value: Boolean) extends AnyVal
@@ -54,15 +55,21 @@ case class MailboxSetResponse(accountId: AccountId,
destroyed: Option[Seq[MailboxId]],
notCreated: Option[Map[MailboxCreationId, MailboxSetError]],
notUpdated: Option[Map[MailboxId, MailboxSetError]],
- notDestroyed: Option[Map[MailboxId, MailboxSetError]])
+ notDestroyed: Option[Map[UnparsedMailboxId, MailboxSetError]])
object MailboxSetError {
val invalidArgumentValue: SetErrorType = "invalidArguments"
val serverFailValue: SetErrorType = "serverFail"
+ val notFoundValue: SetErrorType = "notFound"
+ val mailboxHasEmailValue: SetErrorType = "mailboxHasEmail"
+ val mailboxHasChildValue: SetErrorType = "mailboxHasChild"
val forbiddenValue: SetErrorType = "forbidden"
def invalidArgument(description: Option[SetErrorDescription], properties: Option[Properties]) = MailboxSetError(invalidArgumentValue, description, properties)
def serverFail(description: Option[SetErrorDescription], properties: Option[Properties]) = MailboxSetError(serverFailValue, description, properties)
+ def notFound(description: Option[SetErrorDescription]) = MailboxSetError(notFoundValue, description, None)
+ def mailboxHasEmail(description: Option[SetErrorDescription]) = MailboxSetError(mailboxHasEmailValue, description, None)
+ def mailboxHasChild(description: Option[SetErrorDescription]) = MailboxSetError(mailboxHasChildValue, description, None)
def forbidden(description: Option[SetErrorDescription], properties: Option[Properties]) = MailboxSetError(forbiddenValue, description, properties)
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
index c2fcd51..44a7a76 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetMethod.scala
@@ -22,13 +22,13 @@ package org.apache.james.jmap.method
import eu.timepit.refined.auto._
import javax.inject.Inject
import org.apache.james.jmap.json.Serializer
-import org.apache.james.jmap.mail.MailboxSetRequest.MailboxCreationId
+import org.apache.james.jmap.mail.MailboxSetRequest.{MailboxCreationId, UnparsedMailboxId}
import org.apache.james.jmap.mail.{IsSubscribed, MailboxCreationRequest, MailboxCreationResponse, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, Properties, SetErrorDescription, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads}
import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.model.{Invocation, State}
import org.apache.james.mailbox.exception.{InsufficientRightsException, MailboxExistsException, MailboxNameException, MailboxNotFoundException}
-import org.apache.james.mailbox.model.{MailboxId, MailboxPath}
+import org.apache.james.mailbox.model.{FetchGroup, Mailbox, MailboxId, MailboxPath, MessageRange}
import org.apache.james.mailbox.{MailboxManager, MailboxSession}
import org.apache.james.metrics.api.MetricFactory
import org.reactivestreams.Publisher
@@ -38,12 +38,13 @@ import reactor.core.scheduler.Schedulers
import scala.collection.immutable
+case class MailboxHasMailException(mailboxId: MailboxId) extends Exception
+case class MailboxHasChildException(mailboxId: MailboxId) extends Exception
+
sealed trait CreationResult {
def mailboxCreationId: MailboxCreationId
}
-
case class CreationSuccess(mailboxCreationId: MailboxCreationId, mailboxId: MailboxId) extends CreationResult
-
case class CreationFailure(mailboxCreationId: MailboxCreationId, exception: Exception) extends CreationResult {
def asMailboxSetError: MailboxSetError = exception match {
case e: MailboxNotFoundException => MailboxSetError.invalidArgument(Some(SetErrorDescription(e.getMessage)), Some(Properties(List("parentId"))))
@@ -53,7 +54,6 @@ case class CreationFailure(mailboxCreationId: MailboxCreationId, exception: Exce
case _ => MailboxSetError.serverFail(Some(SetErrorDescription(exception.getMessage)), None)
}
}
-
case class CreationResults(created: Seq[CreationResult]) {
def retrieveCreated: Map[MailboxCreationId, MailboxId] = created
.flatMap(result => result match {
@@ -70,8 +70,35 @@ case class CreationResults(created: Seq[CreationResult]) {
.toMap
}
+sealed trait DeletionResult
+case class DeletionSuccess(mailboxId: MailboxId) extends DeletionResult
+case class DeletionFailure(mailboxId: UnparsedMailboxId, exception: Throwable) extends DeletionResult {
+ def asMailboxSetError: MailboxSetError = exception match {
+ case e: MailboxNotFoundException => MailboxSetError.notFound(Some(SetErrorDescription(e.getMessage)))
+ case e: MailboxHasMailException => MailboxSetError.mailboxHasEmail(Some(SetErrorDescription(s"${e.mailboxId.serialize} is not empty")))
+ case e: MailboxHasChildException => MailboxSetError.mailboxHasChild(Some(SetErrorDescription(s"${e.mailboxId.serialize} has child mailboxes")))
+ case e: IllegalArgumentException => MailboxSetError.invalidArgument(Some(SetErrorDescription(s"${mailboxId} is not a mailboxId")), None)
+ case _ => MailboxSetError.serverFail(Some(SetErrorDescription(exception.getMessage)), None)
+ }
+}
+case class DeletionResults(results: Seq[DeletionResult]) {
+ def destroyed: Seq[DeletionSuccess] =
+ results.flatMap(result => result match {
+ case success: DeletionSuccess => Some(success)
+ case _ => None
+ })
+
+ def retrieveErrors: Map[UnparsedMailboxId, MailboxSetError] =
+ results.flatMap(result => result match {
+ case failure: DeletionFailure => Some(failure.mailboxId, failure.asMailboxSetError)
+ case _ => None
+ })
+ .toMap
+}
+
class MailboxSetMethod @Inject()(serializer: Serializer,
mailboxManager: MailboxManager,
+ mailboxIdFactory: MailboxId.Factory,
metricFactory: MetricFactory) extends Method {
override val methodName: MethodName = MethodName("Mailbox/set")
@@ -83,7 +110,8 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
val (unparsableCreateRequests, createRequests) = parseCreateRequests(mailboxSetRequest)
for {
creationResults <- createMailboxes(mailboxSession, createRequests)
- } yield createResponse(invocation, mailboxSetRequest, unparsableCreateRequests, creationResults)
+ deletionResults <- deleteMailboxes(mailboxSession, mailboxSetRequest.destroy.getOrElse(Seq()))
+ } yield createResponse(invocation, mailboxSetRequest, unparsableCreateRequests, creationResults, deletionResults)
}))
}
@@ -108,6 +136,29 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
case (path, _) => MailboxSetError.invalidArgument(Some(SetErrorDescription(s"Unknown error on property '$path'")), None)
}
+ private def deleteMailboxes(mailboxSession: MailboxSession, deleteRequests: immutable.Iterable[UnparsedMailboxId]): SMono[DeletionResults] = {
+ SFlux.fromIterable(deleteRequests)
+ .flatMap(id => SMono.just(id)
+ .map(id => mailboxIdFactory.fromString(id))
+ .flatMap(mailboxId => SMono.fromCallable(() => delete(mailboxSession, mailboxId))
+ .subscribeOn(Schedulers.elastic())
+ .`then`(SMono.just[DeletionResult](DeletionSuccess(mailboxId))))
+ .onErrorRecover(e => DeletionFailure(id, e)))
+ .collectSeq()
+ .map(DeletionResults)
+ }
+
+ private def delete(mailboxSession: MailboxSession, id: MailboxId): Mailbox = {
+ val mailbox = mailboxManager.getMailbox(id, mailboxSession)
+ if (mailbox.getMessages(MessageRange.all(), FetchGroup.MINIMAL, mailboxSession).hasNext) {
+ throw MailboxHasMailException(id)
+ }
+ if (mailboxManager.hasChildren(mailbox.getMailboxPath, mailboxSession)) {
+ throw MailboxHasChildException(id)
+ }
+ mailboxManager.deleteMailbox(id, mailboxSession)
+ }
+
private def createMailboxes(mailboxSession: MailboxSession, createRequests: immutable.Iterable[(MailboxCreationId, MailboxCreationRequest)]): SMono[CreationResults] = {
SFlux.fromIterable(createRequests).flatMap {
case (mailboxCreationId: MailboxCreationId, mailboxCreationRequest: MailboxCreationRequest) => {
@@ -136,13 +187,16 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
}
}
- private def createResponse(invocation: Invocation, mailboxSetRequest: MailboxSetRequest, unparsableCreateRequests: immutable.Iterable[(MailboxCreationId, MailboxSetError)], creationResults: CreationResults): Invocation = {
+ private def createResponse(invocation: Invocation, mailboxSetRequest: MailboxSetRequest,
+ unparsableCreateRequests: immutable.Iterable[(MailboxCreationId, MailboxSetError)],
+ creationResults: CreationResults, deletionResults: DeletionResults): Invocation = {
val created: Map[MailboxCreationId, MailboxId] = creationResults.retrieveCreated
Invocation(methodName, Arguments(serializer.serialize(MailboxSetResponse(
mailboxSetRequest.accountId,
oldState = None,
newState = State.INSTANCE,
+ destroyed = Some(deletionResults.destroyed.map(_.mailboxId)).filter(_.nonEmpty),
created = Some(created.map(creation => (creation._1, MailboxCreationResponse(
id = creation._2,
role = None,
@@ -155,13 +209,11 @@ class MailboxSetMethod @Inject()(serializer: Serializer,
namespace = None,
quotas = None,
isSubscribed = IsSubscribed(true)
-
)))).filter(_.nonEmpty),
notCreated = Some(unparsableCreateRequests.toMap ++ creationResults.retrieveErrors).filter(_.nonEmpty),
updated = None,
notUpdated = None,
- destroyed = None,
- notDestroyed = None
+ notDestroyed = Some(deletionResults.retrieveErrors).filter(_.nonEmpty)
)).as[JsObject]), invocation.methodCallId)
}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
index 1072310..fd75472 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/model/Invocation.scala
@@ -34,7 +34,6 @@ object Invocation {
case class Arguments(value: JsObject) extends AnyVal
case class MethodCallId(value: NonEmptyString)
-
def error(errorCode: ErrorCode, description: String, methodCallId: MethodCallId): Invocation =
Invocation(MethodName("error"),
Arguments(JsObject(Seq("type" -> JsString(errorCode.code), "description" -> JsString(description)))),
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index 972bccf..cfdbc2d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -24,7 +24,6 @@ import java.util.stream
import java.util.stream.Stream
import com.fasterxml.jackson.core.JsonParseException
-import eu.timepit.refined.auto._
import io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
import io.netty.handler.codec.http.HttpMethod
import io.netty.handler.codec.http.HttpResponseStatus.OK
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org