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 <rkowal...@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:an...@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-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org