You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/11/03 02:25:03 UTC
[james-project] 07/11: [REFACTORING] Split MailboxSetMethod and
extract create/update/destroy
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 429bcbfb8bf138418ebaccd1f28bfb40ac4f5767
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Nov 2 11:03:05 2020 +0700
[REFACTORING] Split MailboxSetMethod and extract create/update/destroy
---
.../jmap/method/MailboxSetCreatePerformer.scala | 206 ++++++++++
.../jmap/method/MailboxSetDeletePerformer.scala | 109 +++++
.../james/jmap/method/MailboxSetMethod.scala | 443 +--------------------
.../jmap/method/MailboxSetUpdatePerformer.scala | 222 +++++++++++
4 files changed, 556 insertions(+), 424 deletions(-)
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
new file mode 100644
index 0000000..4fe260a
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetCreatePerformer.scala
@@ -0,0 +1,206 @@
+/****************************************************************
+ * 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.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{ClientId, Id, Properties, ServerId, SetError}
+import org.apache.james.jmap.json.MailboxSerializer
+import org.apache.james.jmap.mail.MailboxSetRequest.MailboxCreationId
+import org.apache.james.jmap.mail.{IsSubscribed, MailboxCreationRequest, MailboxCreationResponse, MailboxRights, MailboxSetRequest, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads}
+import org.apache.james.jmap.method.MailboxSetCreatePerformer.{MailboxCreationFailure, MailboxCreationResult, MailboxCreationResults, MailboxCreationSuccess}
+import org.apache.james.jmap.routes.{ProcessingContext, SessionSupplier}
+import org.apache.james.jmap.utils.quotas.QuotaLoaderWithPreloadedDefaultFactory
+import org.apache.james.mailbox.exception.{InsufficientRightsException, MailboxExistsException, MailboxNameException, MailboxNotFoundException}
+import org.apache.james.mailbox.model.{MailboxId, MailboxPath}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession, SubscriptionManager}
+import org.apache.james.metrics.api.MetricFactory
+import play.api.libs.json.{JsError, JsObject, JsPath, JsSuccess, Json, JsonValidationError}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+import scala.util.Try
+
+object MailboxSetCreatePerformer {
+ sealed trait MailboxCreationResult {
+ def mailboxCreationId: MailboxCreationId
+ }
+ case class MailboxCreationSuccess(mailboxCreationId: MailboxCreationId, mailboxCreationResponse: MailboxCreationResponse) extends MailboxCreationResult
+ case class MailboxCreationFailure(mailboxCreationId: MailboxCreationId, exception: Exception) extends MailboxCreationResult {
+ def asMailboxSetError: SetError = exception match {
+ case e: MailboxNotFoundException => SetError.invalidArguments(SetErrorDescription(e.getMessage), Some(Properties("parentId")))
+ 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 _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
+ }
+ }
+ case class MailboxCreationResults(created: Seq[MailboxCreationResult]) {
+ def retrieveCreated: Map[MailboxCreationId, MailboxCreationResponse] = created
+ .flatMap(result => result match {
+ case success: MailboxCreationSuccess => Some(success.mailboxCreationId, success.mailboxCreationResponse)
+ case _ => None
+ })
+ .toMap
+ .map(creation => (creation._1, creation._2))
+
+ def retrieveErrors: Map[MailboxCreationId, SetError] = created
+ .flatMap(result => result match {
+ case failure: MailboxCreationFailure => Some(failure.mailboxCreationId, failure.asMailboxSetError)
+ case _ => None
+ })
+ .toMap
+ }
+}
+
+class MailboxSetCreatePerformer @Inject()(serializer: MailboxSerializer,
+ mailboxManager: MailboxManager,
+ subscriptionManager: SubscriptionManager,
+ mailboxIdFactory: MailboxId.Factory,
+ quotaFactory : QuotaLoaderWithPreloadedDefaultFactory,
+ val metricFactory: MetricFactory,
+ val sessionSupplier: SessionSupplier) {
+
+
+
+ def createMailboxes(mailboxSession: MailboxSession,
+ mailboxSetRequest: MailboxSetRequest,
+ processingContext: ProcessingContext): SMono[(MailboxCreationResults, ProcessingContext)] = {
+ SFlux.fromIterable(mailboxSetRequest.create
+ .getOrElse(Map.empty)
+ .view)
+ .foldLeft((MailboxCreationResults(Nil), processingContext)){
+ (acc : (MailboxCreationResults, ProcessingContext), elem: (MailboxCreationId, JsObject)) => {
+ val (mailboxCreationId, jsObject) = elem
+ val (creationResult, updatedProcessingContext) = createMailbox(mailboxSession, mailboxCreationId, jsObject, acc._2)
+ (MailboxCreationResults(acc._1.created :+ creationResult), updatedProcessingContext)
+ }
+ }
+ .subscribeOn(Schedulers.elastic())
+ }
+
+ private def createMailbox(mailboxSession: MailboxSession,
+ mailboxCreationId: MailboxCreationId,
+ jsObject: JsObject,
+ processingContext: ProcessingContext): (MailboxCreationResult, ProcessingContext) = {
+ parseCreate(jsObject)
+ .flatMap(mailboxCreationRequest => resolvePath(mailboxSession, mailboxCreationRequest)
+ .flatMap(path => createMailbox(mailboxSession = mailboxSession,
+ path = path,
+ mailboxCreationRequest = mailboxCreationRequest)))
+ .flatMap(creationResponse => recordCreationIdInProcessingContext(mailboxCreationId, processingContext, creationResponse.id)
+ .map(context => (creationResponse, context)))
+ .fold(e => (MailboxCreationFailure(mailboxCreationId, e), processingContext),
+ creationResponseWithUpdatedContext => {
+ (MailboxCreationSuccess(mailboxCreationId, creationResponseWithUpdatedContext._1), creationResponseWithUpdatedContext._2)
+ })
+ }
+
+ private def parseCreate(jsObject: JsObject): Either[MailboxCreationParseException, MailboxCreationRequest] =
+ MailboxCreationRequest.validateProperties(jsObject)
+ .flatMap(validJsObject => Json.fromJson(validJsObject)(serializer.mailboxCreationRequest) match {
+ case JsSuccess(creationRequest, _) => Right(creationRequest)
+ case JsError(errors) => Left(MailboxCreationParseException(mailboxSetError(errors)))
+ })
+
+ private def resolvePath(mailboxSession: MailboxSession,
+ mailboxCreationRequest: MailboxCreationRequest): Either[Exception, MailboxPath] = {
+ if (mailboxCreationRequest.name.value.contains(mailboxSession.getPathDelimiter)) {
+ return Left(new MailboxNameException(s"The mailbox '${mailboxCreationRequest.name.value}' contains an illegal character: '${mailboxSession.getPathDelimiter}'"))
+ }
+ mailboxCreationRequest.parentId
+ .map(maybeParentId => for {
+ parentId <- Try(mailboxIdFactory.fromString(maybeParentId.value))
+ .toEither
+ .left
+ .map(e => new IllegalArgumentException(e.getMessage, e))
+ parentPath <- retrievePath(parentId, mailboxSession)
+ } yield {
+ parentPath.child(mailboxCreationRequest.name, mailboxSession.getPathDelimiter)
+ })
+ .getOrElse(Right(MailboxPath.forUser(mailboxSession.getUser, mailboxCreationRequest.name)))
+ }
+
+ private def retrievePath(mailboxId: MailboxId, mailboxSession: MailboxSession): Either[Exception, MailboxPath] = try {
+ Right(mailboxManager.getMailbox(mailboxId, mailboxSession).getMailboxPath)
+ } catch {
+ case e: Exception => Left(e)
+ }
+
+ private def recordCreationIdInProcessingContext(mailboxCreationId: MailboxCreationId,
+ processingContext: ProcessingContext,
+ mailboxId: MailboxId): Either[IllegalArgumentException, ProcessingContext] = {
+ for {
+ creationId <- Id.validate(mailboxCreationId)
+ serverAssignedId <- Id.validate(mailboxId.serialize())
+ } yield {
+ processingContext.recordCreatedId(ClientId(creationId), ServerId(serverAssignedId))
+ }
+ }
+ private def mailboxSetError(errors: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): SetError =
+ errors.head match {
+ case (path, Seq()) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in mailbox object is not valid"))
+ case (path, Seq(JsonValidationError(Seq("error.path.missing")))) => SetError.invalidArguments(SetErrorDescription(s"Missing '$path' property in mailbox object"))
+ case (path, Seq(JsonValidationError(Seq(message)))) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in mailbox object is not valid: $message"))
+ case (path, _) => SetError.invalidArguments(SetErrorDescription(s"Unknown error on property '$path'"))
+ }
+
+ private def createMailbox(mailboxSession: MailboxSession,
+ path: MailboxPath,
+ mailboxCreationRequest: MailboxCreationRequest): Either[Exception, MailboxCreationResponse] = {
+ try {
+ //can safely do a get as the Optional is empty only if the mailbox name is empty which is forbidden by the type constraint on MailboxName
+ val mailboxId = mailboxManager.createMailbox(path, mailboxSession).get()
+
+ val defaultSubscribed = IsSubscribed(true)
+ if (mailboxCreationRequest.isSubscribed.getOrElse(defaultSubscribed).value) {
+ subscriptionManager.subscribe(mailboxSession, path.getName)
+ }
+
+ mailboxCreationRequest.rights
+ .foreach(rights => mailboxManager.setRights(mailboxId, rights.toMailboxAcl.asJava, mailboxSession))
+
+ val quotas = quotaFactory.loadFor(mailboxSession)
+ .flatMap(quotaLoader => quotaLoader.getQuotas(path))
+ .block()
+
+ Right(MailboxCreationResponse(
+ id = mailboxId,
+ sortOrder = SortOrder.defaultSortOrder,
+ role = None,
+ totalEmails = TotalEmails(0L),
+ unreadEmails = UnreadEmails(0L),
+ totalThreads = TotalThreads(0L),
+ unreadThreads = UnreadThreads(0L),
+ myRights = MailboxRights.FULL,
+ quotas = Some(quotas),
+ isSubscribed = if (mailboxCreationRequest.isSubscribed.isEmpty) {
+ Some(defaultSubscribed)
+ } else {
+ None
+ }))
+ } catch {
+ case error: Exception => Left(error)
+ }
+ }
+
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
new file mode 100644
index 0000000..3ccd8f4
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
@@ -0,0 +1,109 @@
+/****************************************************************
+ * 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 javax.inject.Inject
+import org.apache.james.jmap.core.SetError
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.mail.MailboxGet.UnparsedMailboxId
+import org.apache.james.jmap.mail.{MailboxGet, MailboxSetError, MailboxSetRequest, RemoveEmailsOnDestroy}
+import org.apache.james.jmap.method.MailboxSetDeletePerformer.{MailboxDeletionFailure, MailboxDeletionResult, MailboxDeletionResults, MailboxDeletionSuccess}
+import org.apache.james.mailbox.exception.MailboxNotFoundException
+import org.apache.james.mailbox.model.{FetchGroup, MailboxId, MessageRange}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession, MessageManager, Role, SubscriptionManager}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+object MailboxSetDeletePerformer {
+ sealed trait MailboxDeletionResult
+ case class MailboxDeletionSuccess(mailboxId: MailboxId) extends MailboxDeletionResult
+ case class MailboxDeletionFailure(mailboxId: UnparsedMailboxId, exception: Throwable) extends MailboxDeletionResult {
+ def asMailboxSetError: SetError = exception match {
+ case e: MailboxNotFoundException => SetError.notFound(SetErrorDescription(e.getMessage))
+ case e: MailboxHasMailException => MailboxSetError.mailboxHasEmail(SetErrorDescription(s"${e.mailboxId.serialize} is not empty"))
+ case e: MailboxHasChildException => MailboxSetError.mailboxHasChild(SetErrorDescription(s"${e.mailboxId.serialize} has child mailboxes"))
+ case e: SystemMailboxChangeException => SetError.invalidArguments(SetErrorDescription("System mailboxes cannot be destroyed"))
+ case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(s"${mailboxId} is not a mailboxId: ${e.getMessage}"))
+ case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
+ }
+ }
+ case class MailboxDeletionResults(results: Seq[MailboxDeletionResult]) {
+ def destroyed: Seq[MailboxId] =
+ results.flatMap(result => result match {
+ case success: MailboxDeletionSuccess => Some(success)
+ case _ => None
+ }).map(_.mailboxId)
+
+ def retrieveErrors: Map[UnparsedMailboxId, SetError] =
+ results.flatMap(result => result match {
+ case failure: MailboxDeletionFailure => Some(failure.mailboxId, failure.asMailboxSetError)
+ case _ => None
+ })
+ .toMap
+ }
+}
+
+class MailboxSetDeletePerformer @Inject()(mailboxManager: MailboxManager,
+ subscriptionManager: SubscriptionManager,
+ mailboxIdFactory: MailboxId.Factory) {
+
+ def deleteMailboxes(mailboxSession: MailboxSession, mailboxSetRequest: MailboxSetRequest): SMono[MailboxDeletionResults] = {
+ SFlux.fromIterable(mailboxSetRequest.destroy.getOrElse(Seq()))
+ .flatMap(id => delete(mailboxSession, id, mailboxSetRequest.onDestroyRemoveEmails.getOrElse(RemoveEmailsOnDestroy(false)))
+ .onErrorRecover(e => MailboxDeletionFailure(id, e)))
+ .collectSeq()
+ .map(MailboxDeletionResults)
+ }
+
+ private def delete(mailboxSession: MailboxSession, id: UnparsedMailboxId, onDestroy: RemoveEmailsOnDestroy): SMono[MailboxDeletionResult] = {
+ MailboxGet.parse(mailboxIdFactory)(id)
+ .fold(e => SMono.raiseError(e),
+ id => SMono.fromCallable(() => doDelete(mailboxSession, id, onDestroy))
+ .subscribeOn(Schedulers.elastic())
+ .`then`(SMono.just[MailboxDeletionResult](MailboxDeletionSuccess(id))))
+
+ }
+
+ private def doDelete(mailboxSession: MailboxSession, id: MailboxId, onDestroy: RemoveEmailsOnDestroy): Unit = {
+ val mailbox = mailboxManager.getMailbox(id, mailboxSession)
+
+ if (isASystemMailbox(mailbox)) {
+ throw SystemMailboxChangeException(id)
+ }
+
+ if (mailboxManager.hasChildren(mailbox.getMailboxPath, mailboxSession)) {
+ throw MailboxHasChildException(id)
+ }
+
+ if (onDestroy.value) {
+ val deletedMailbox = mailboxManager.deleteMailbox(id, mailboxSession)
+ subscriptionManager.unsubscribe(mailboxSession, deletedMailbox.getName)
+ } else {
+ if (mailbox.getMessages(MessageRange.all(), FetchGroup.MINIMAL, mailboxSession).hasNext) {
+ throw MailboxHasMailException(id)
+ }
+
+ val deletedMailbox = mailboxManager.deleteMailbox(id, mailboxSession)
+ subscriptionManager.unsubscribe(mailboxSession, deletedMailbox.getName)
+ }
+ }
+
+ private def isASystemMailbox(mailbox: MessageManager): Boolean = Role.from(mailbox.getMailboxPath.getName).isPresent
+}
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 e8c4265..723c208 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
@@ -23,25 +23,18 @@ import eu.timepit.refined.auto._
import javax.inject.Inject
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
-import org.apache.james.jmap.core.SetError.SetErrorDescription
-import org.apache.james.jmap.core.{ClientId, Id, Invocation, Properties, ServerId, SetError, State}
+import org.apache.james.jmap.core.{Invocation, SetError, State}
import org.apache.james.jmap.json.{MailboxSerializer, ResponseSerializer}
-import org.apache.james.jmap.mail.MailboxGet.UnparsedMailboxId
-import org.apache.james.jmap.mail.MailboxSetRequest.MailboxCreationId
-import org.apache.james.jmap.mail.{InvalidPatchException, InvalidPropertyException, InvalidUpdateException, IsSubscribed, MailboxCreationRequest, MailboxCreationResponse, MailboxGet, MailboxPatchObject, MailboxRights, MailboxSetError, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, ParentIdUpdate, RemoveEmailsOnDestroy, ServerSetPropertyException, SortOrder, TotalEmails, TotalThreads, UnreadEmails, UnreadThreads, UnsupportedPropertyUpdatedException, ValidatedMai [...]
-import org.apache.james.jmap.routes.{ProcessingContext, SessionSupplier}
-import org.apache.james.jmap.utils.quotas.QuotaLoaderWithPreloadedDefaultFactory
-import org.apache.james.mailbox.MailboxManager.RenameOption
-import org.apache.james.mailbox.exception.{InsufficientRightsException, MailboxExistsException, MailboxNameException, MailboxNotFoundException}
-import org.apache.james.mailbox.model.{FetchGroup, MailboxId, MailboxPath, MessageRange}
-import org.apache.james.mailbox.{MailboxManager, MailboxSession, MessageManager, Role, SubscriptionManager}
+import org.apache.james.jmap.mail.{MailboxSetRequest, MailboxSetResponse}
+import org.apache.james.jmap.method.MailboxSetCreatePerformer.MailboxCreationResults
+import org.apache.james.jmap.method.MailboxSetDeletePerformer.MailboxDeletionResults
+import org.apache.james.jmap.method.MailboxSetUpdatePerformer.MailboxUpdateResults
+import org.apache.james.jmap.routes.SessionSupplier
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.mailbox.model.MailboxId
import org.apache.james.metrics.api.MetricFactory
-import play.api.libs.json.{JsError, JsObject, JsPath, JsSuccess, Json, JsonValidationError}
-import reactor.core.scala.publisher.{SFlux, SMono}
-import reactor.core.scheduler.Schedulers
-
-import scala.jdk.CollectionConverters._
-import scala.util.Try
+import play.api.libs.json.{JsError, JsObject, JsSuccess}
+import reactor.core.scala.publisher.SMono
case class MailboxHasMailException(mailboxId: MailboxId) extends Exception
case class SystemMailboxChangeException(mailboxId: MailboxId) extends Exception
@@ -49,427 +42,29 @@ case class LoopInMailboxGraphException(mailboxId: MailboxId) extends Exception
case class MailboxHasChildException(mailboxId: MailboxId) extends Exception
case class MailboxCreationParseException(setError: SetError) extends Exception
-sealed trait CreationResult {
- def mailboxCreationId: MailboxCreationId
-}
-case class CreationSuccess(mailboxCreationId: MailboxCreationId, mailboxCreationResponse: MailboxCreationResponse) extends CreationResult
-case class CreationFailure(mailboxCreationId: MailboxCreationId, exception: Exception) extends CreationResult {
- def asMailboxSetError: SetError = exception match {
- case e: MailboxNotFoundException => SetError.invalidArguments(SetErrorDescription(e.getMessage), Some(Properties("parentId")))
- 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 _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
- }
-}
-case class CreationResults(created: Seq[CreationResult]) {
- def retrieveCreated: Map[MailboxCreationId, MailboxCreationResponse] = created
- .flatMap(result => result match {
- case success: CreationSuccess => Some(success.mailboxCreationId, success.mailboxCreationResponse)
- case _ => None
- })
- .toMap
- .map(creation => (creation._1, creation._2))
-
- def retrieveErrors: Map[MailboxCreationId, SetError] = created
- .flatMap(result => result match {
- case failure: CreationFailure => Some(failure.mailboxCreationId, failure.asMailboxSetError)
- case _ => None
- })
- .toMap
-}
-
-sealed trait DeletionResult
-case class DeletionSuccess(mailboxId: MailboxId) extends DeletionResult
-case class DeletionFailure(mailboxId: UnparsedMailboxId, exception: Throwable) extends DeletionResult {
- def asMailboxSetError: SetError = exception match {
- case e: MailboxNotFoundException => SetError.notFound(SetErrorDescription(e.getMessage))
- case e: MailboxHasMailException => MailboxSetError.mailboxHasEmail(SetErrorDescription(s"${e.mailboxId.serialize} is not empty"))
- case e: MailboxHasChildException => MailboxSetError.mailboxHasChild(SetErrorDescription(s"${e.mailboxId.serialize} has child mailboxes"))
- case e: SystemMailboxChangeException => SetError.invalidArguments(SetErrorDescription("System mailboxes cannot be destroyed"))
- case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(s"${mailboxId} is not a mailboxId: ${e.getMessage}"))
- case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
- }
-}
-case class DeletionResults(results: Seq[DeletionResult]) {
- def destroyed: Seq[MailboxId] =
- results.flatMap(result => result match {
- case success: DeletionSuccess => Some(success)
- case _ => None
- }).map(_.mailboxId)
-
- def retrieveErrors: Map[UnparsedMailboxId, SetError] =
- results.flatMap(result => result match {
- case failure: DeletionFailure => Some(failure.mailboxId, failure.asMailboxSetError)
- case _ => None
- })
- .toMap
-}
-
-sealed trait UpdateResult
-case class UpdateSuccess(mailboxId: MailboxId) extends UpdateResult
-case class UpdateFailure(mailboxId: UnparsedMailboxId, exception: Throwable, patch: Option[ValidatedMailboxPatchObject]) extends UpdateResult {
- def filter(acceptableProperties: Properties): Option[Properties] = Some(patch
- .map(_.updatedProperties.intersect(acceptableProperties))
- .getOrElse(acceptableProperties))
-
- def asMailboxSetError: SetError = exception match {
- case e: MailboxNotFoundException => SetError.notFound(SetErrorDescription(e.getMessage))
- case e: MailboxNameException => SetError.invalidArguments(SetErrorDescription(e.getMessage), filter(Properties("name", "parentId")))
- case e: MailboxExistsException => SetError.invalidArguments(SetErrorDescription(e.getMessage), filter(Properties("name", "parentId")))
- case e: UnsupportedPropertyUpdatedException => SetError.invalidArguments(SetErrorDescription(s"${e.property} property do not exist thus cannot be updated"), Some(Properties(e.property)))
- case e: InvalidUpdateException => SetError.invalidArguments(SetErrorDescription(s"${e.cause}"), Some(Properties(e.property)))
- case e: ServerSetPropertyException => SetError.invalidArguments(SetErrorDescription("Can not modify server-set properties"), Some(Properties(e.property)))
- case e: InvalidPropertyException => SetError.invalidPatch(SetErrorDescription(s"${e.cause}"))
- case e: InvalidPatchException => SetError.invalidPatch(SetErrorDescription(s"${e.cause}"))
- case e: SystemMailboxChangeException => SetError.invalidArguments(SetErrorDescription("Invalid change to a system mailbox"), filter(Properties("name", "parentId")))
- case e: LoopInMailboxGraphException => SetError.invalidArguments(SetErrorDescription("A mailbox parentId property can not be set to itself or one of its child"), Some(Properties("parentId")))
- case e: InsufficientRightsException => SetError.invalidArguments(SetErrorDescription("Invalid change to a delegated mailbox"))
- case e: MailboxHasChildException => SetError.invalidArguments(SetErrorDescription(s"${e.mailboxId.serialize()} parentId property cannot be updated as this mailbox has child mailboxes"), Some(Properties("parentId")))
- case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage), None)
- case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
- }
-}
-case class UpdateResults(results: Seq[UpdateResult]) {
- def updated: Map[MailboxId, MailboxUpdateResponse] =
- results.flatMap(result => result match {
- case success: UpdateSuccess => Some((success.mailboxId, MailboxSetResponse.empty))
- case _ => None
- }).toMap
- def notUpdated: Map[UnparsedMailboxId, SetError] = results.flatMap(result => result match {
- case failure: UpdateFailure => Some(failure.mailboxId, failure.asMailboxSetError)
- case _ => None
- }).toMap
-}
-
class MailboxSetMethod @Inject()(serializer: MailboxSerializer,
- mailboxManager: MailboxManager,
- subscriptionManager: SubscriptionManager,
- mailboxIdFactory: MailboxId.Factory,
- quotaFactory : QuotaLoaderWithPreloadedDefaultFactory,
+ createPerformer: MailboxSetCreatePerformer,
+ deletePerformer: MailboxSetDeletePerformer,
+ updatePerformer: MailboxSetUpdatePerformer,
val metricFactory: MetricFactory,
val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[MailboxSetRequest] {
override val methodName: MethodName = MethodName("Mailbox/set")
override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: MailboxSetRequest): SMono[InvocationWithContext] = for {
- creationResultsWithUpdatedProcessingContext <- createMailboxes(mailboxSession, request, invocation.processingContext)
- deletionResults <- deleteMailboxes(mailboxSession, request)
- updateResults <- updateMailboxes(mailboxSession, request, invocation.processingContext, capabilities)
+ creationResultsWithUpdatedProcessingContext <- createPerformer.createMailboxes(mailboxSession, request, invocation.processingContext)
+ deletionResults <- deletePerformer.deleteMailboxes(mailboxSession, request)
+ updateResults <- updatePerformer.updateMailboxes(mailboxSession, request, capabilities)
} yield InvocationWithContext(createResponse(capabilities, invocation.invocation, request, creationResultsWithUpdatedProcessingContext._1, deletionResults, updateResults), creationResultsWithUpdatedProcessingContext._2)
override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[MailboxSetRequest] = asMailboxSetRequest(invocation.arguments)
- private def updateMailboxes(mailboxSession: MailboxSession,
- mailboxSetRequest: MailboxSetRequest,
- processingContext: ProcessingContext,
- capabilities: Set[CapabilityIdentifier]): SMono[UpdateResults] = {
- SFlux.fromIterable(mailboxSetRequest.update.getOrElse(Seq()))
- .flatMap({
- case (unparsedMailboxId: UnparsedMailboxId, patch: MailboxPatchObject) =>
- MailboxGet.parse(mailboxIdFactory)(unparsedMailboxId)
- .fold(
- e => SMono.just(UpdateFailure(unparsedMailboxId, e, None)),
- mailboxId => updateMailbox(mailboxSession, mailboxId, unparsedMailboxId, patch, capabilities))
- .onErrorResume(e => SMono.just(UpdateFailure(unparsedMailboxId, e, None)))
- })
- .collectSeq()
- .map(UpdateResults)
- }
-
- private def updateMailbox(mailboxSession: MailboxSession,
- mailboxId: MailboxId,
- unparsedMailboxId: UnparsedMailboxId,
- patch: MailboxPatchObject,
- capabilities: Set[CapabilityIdentifier]): SMono[UpdateResult] = {
- patch.validate(mailboxIdFactory, serializer, capabilities, mailboxSession)
- .fold(e => SMono.raiseError(e), validatedPatch =>
- updateMailboxRights(mailboxId, validatedPatch, mailboxSession)
- .`then`(updateSubscription(mailboxId, validatedPatch, mailboxSession))
- .`then`(updateMailboxPath(mailboxId, unparsedMailboxId, validatedPatch, mailboxSession)))
- }
-
- private def updateSubscription(mailboxId: MailboxId, validatedPatch: ValidatedMailboxPatchObject, mailboxSession: MailboxSession): SMono[UpdateResult] = {
- validatedPatch.isSubscribedUpdate.map(isSubscribedUpdate => {
- SMono.fromCallable(() => {
- val mailbox = mailboxManager.getMailbox(mailboxId, mailboxSession)
- val isOwner = mailbox.getMailboxPath.belongsTo(mailboxSession)
- val shouldSubscribe = isSubscribedUpdate.isSubscribed.map(_.value).getOrElse(isOwner)
-
- if (shouldSubscribe) {
- subscriptionManager.subscribe(mailboxSession, mailbox.getMailboxPath.getName)
- } else {
- subscriptionManager.unsubscribe(mailboxSession, mailbox.getMailboxPath.getName)
- }
- }).`then`(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
- .subscribeOn(Schedulers.elastic())
- })
- .getOrElse(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
- }
-
- private def updateMailboxPath(mailboxId: MailboxId,
- unparsedMailboxId: UnparsedMailboxId,
- validatedPatch: ValidatedMailboxPatchObject,
- mailboxSession: MailboxSession): SMono[UpdateResult] = {
- if (validatedPatch.shouldUpdateMailboxPath) {
- SMono.fromCallable[UpdateResult](() => {
- try {
- val mailbox = mailboxManager.getMailbox(mailboxId, mailboxSession)
- if (isASystemMailbox(mailbox)) {
- throw SystemMailboxChangeException(mailboxId)
- }
- if (validatedPatch.parentIdUpdate.flatMap(_.newId).contains(mailboxId)) {
- throw LoopInMailboxGraphException(mailboxId)
- }
- val oldPath = mailbox.getMailboxPath
- val newPath = applyParentIdUpdate(mailboxId, validatedPatch.parentIdUpdate, mailboxSession)
- .andThen(applyNameUpdate(validatedPatch.nameUpdate, mailboxSession))
- .apply(oldPath)
- if (!oldPath.equals(newPath)) {
- mailboxManager.renameMailbox(mailboxId,
- newPath,
- RenameOption.RENAME_SUBSCRIPTIONS,
- mailboxSession)
- }
- UpdateSuccess(mailboxId)
- } catch {
- case e: Exception => UpdateFailure(unparsedMailboxId, e, Some(validatedPatch))
- }
- })
- .subscribeOn(Schedulers.elastic())
- } else {
- SMono.just[UpdateResult](UpdateSuccess(mailboxId))
- }
- }
-
- private def applyParentIdUpdate(mailboxId: MailboxId, maybeParentIdUpdate: Option[ParentIdUpdate], mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
- maybeParentIdUpdate.map(parentIdUpdate => applyParentIdUpdate(mailboxId, parentIdUpdate, mailboxSession))
- .getOrElse(x => x)
- }
-
- private def applyNameUpdate(maybeNameUpdate: Option[NameUpdate], mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
- originalPath => maybeNameUpdate.map(nameUpdate => {
- val originalParentPath: Option[MailboxPath] = originalPath.getHierarchyLevels(mailboxSession.getPathDelimiter)
- .asScala
- .reverse
- .drop(1)
- .headOption
- originalParentPath.map(_.child(nameUpdate.newName, mailboxSession.getPathDelimiter))
- .getOrElse(MailboxPath.forUser(mailboxSession.getUser, nameUpdate.newName))
- }).getOrElse(originalPath)
- }
-
- private def applyParentIdUpdate(mailboxId: MailboxId, parentIdUpdate: ParentIdUpdate, mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
- originalPath => {
- val currentName = originalPath.getName(mailboxSession.getPathDelimiter)
- parentIdUpdate.newId
- .map(id => {
- if (mailboxManager.hasChildren(originalPath, mailboxSession)) {
- throw MailboxHasChildException(mailboxId)
- }
- val parentPath = mailboxManager.getMailbox(id, mailboxSession).getMailboxPath
- parentPath.child(currentName, mailboxSession.getPathDelimiter)
- })
- .getOrElse(MailboxPath.forUser(originalPath.getUser, currentName))
- }
- }
-
- private def updateMailboxRights(mailboxId: MailboxId,
- validatedPatch: ValidatedMailboxPatchObject,
- mailboxSession: MailboxSession): SMono[UpdateResult] = {
-
- val resetOperation: SMono[Unit] = validatedPatch.rightsReset.map(sharedWithResetUpdate => {
- SMono.fromCallable(() => {
- mailboxManager.setRights(mailboxId, sharedWithResetUpdate.rights.toMailboxAcl.asJava, mailboxSession)
- }).`then`()
- }).getOrElse(SMono.empty)
-
- val partialUpdatesOperation: SMono[Unit] = SFlux.fromIterable(validatedPatch.rightsPartialUpdates)
- .flatMap(partialUpdate => SMono.fromCallable(() => {
- mailboxManager.applyRightsCommand(mailboxId, partialUpdate.asACLCommand(), mailboxSession)
- }))
- .`then`()
-
- SFlux.merge(Seq(resetOperation, partialUpdatesOperation))
- .`then`()
- .`then`(SMono.just[UpdateResult](UpdateSuccess(mailboxId)))
- .subscribeOn(Schedulers.elastic())
-
- }
-
-
- private def deleteMailboxes(mailboxSession: MailboxSession, mailboxSetRequest: MailboxSetRequest): SMono[DeletionResults] = {
- SFlux.fromIterable(mailboxSetRequest.destroy.getOrElse(Seq()))
- .flatMap(id => delete(mailboxSession, id, mailboxSetRequest.onDestroyRemoveEmails.getOrElse(RemoveEmailsOnDestroy(false)))
- .onErrorRecover(e => DeletionFailure(id, e)))
- .collectSeq()
- .map(DeletionResults)
- }
-
- private def delete(mailboxSession: MailboxSession, id: UnparsedMailboxId, onDestroy: RemoveEmailsOnDestroy): SMono[DeletionResult] = {
- MailboxGet.parse(mailboxIdFactory)(id)
- .fold(e => SMono.raiseError(e),
- id => SMono.fromCallable(() => doDelete(mailboxSession, id, onDestroy))
- .subscribeOn(Schedulers.elastic())
- .`then`(SMono.just[DeletionResult](DeletionSuccess(id))))
-
- }
-
- private def doDelete(mailboxSession: MailboxSession, id: MailboxId, onDestroy: RemoveEmailsOnDestroy): Unit = {
- val mailbox = mailboxManager.getMailbox(id, mailboxSession)
-
- if (isASystemMailbox(mailbox)) {
- throw SystemMailboxChangeException(id)
- }
-
- if (mailboxManager.hasChildren(mailbox.getMailboxPath, mailboxSession)) {
- throw MailboxHasChildException(id)
- }
-
- if (onDestroy.value) {
- val deletedMailbox = mailboxManager.deleteMailbox(id, mailboxSession)
- subscriptionManager.unsubscribe(mailboxSession, deletedMailbox.getName)
- } else {
- if (mailbox.getMessages(MessageRange.all(), FetchGroup.MINIMAL, mailboxSession).hasNext) {
- throw MailboxHasMailException(id)
- }
-
- val deletedMailbox = mailboxManager.deleteMailbox(id, mailboxSession)
- subscriptionManager.unsubscribe(mailboxSession, deletedMailbox.getName)
- }
- }
-
- private def isASystemMailbox(mailbox: MessageManager): Boolean = Role.from(mailbox.getMailboxPath.getName).isPresent
-
- private def createMailboxes(mailboxSession: MailboxSession,
- mailboxSetRequest: MailboxSetRequest,
- processingContext: ProcessingContext): SMono[(CreationResults, ProcessingContext)] = {
- SFlux.fromIterable(mailboxSetRequest.create
- .getOrElse(Map.empty)
- .view)
- .foldLeft((CreationResults(Nil), processingContext)){
- (acc : (CreationResults, ProcessingContext), elem: (MailboxCreationId, JsObject)) => {
- val (mailboxCreationId, jsObject) = elem
- val (creationResult, updatedProcessingContext) = createMailbox(mailboxSession, mailboxCreationId, jsObject, acc._2)
- (CreationResults(acc._1.created :+ creationResult), updatedProcessingContext)
- }
- }
- .subscribeOn(Schedulers.elastic())
- }
-
- private def createMailbox(mailboxSession: MailboxSession,
- mailboxCreationId: MailboxCreationId,
- jsObject: JsObject,
- processingContext: ProcessingContext): (CreationResult, ProcessingContext) = {
- parseCreate(jsObject)
- .flatMap(mailboxCreationRequest => resolvePath(mailboxSession, mailboxCreationRequest)
- .flatMap(path => createMailbox(mailboxSession = mailboxSession,
- path = path,
- mailboxCreationRequest = mailboxCreationRequest)))
- .flatMap(creationResponse => recordCreationIdInProcessingContext(mailboxCreationId, processingContext, creationResponse.id)
- .map(context => (creationResponse, context)))
- .fold(e => (CreationFailure(mailboxCreationId, e), processingContext),
- creationResponseWithUpdatedContext => {
- (CreationSuccess(mailboxCreationId, creationResponseWithUpdatedContext._1), creationResponseWithUpdatedContext._2)
- })
- }
-
- private def parseCreate(jsObject: JsObject): Either[MailboxCreationParseException, MailboxCreationRequest] =
- MailboxCreationRequest.validateProperties(jsObject)
- .flatMap(validJsObject => Json.fromJson(validJsObject)(serializer.mailboxCreationRequest) match {
- case JsSuccess(creationRequest, _) => Right(creationRequest)
- case JsError(errors) => Left(MailboxCreationParseException(mailboxSetError(errors)))
- })
-
- private def mailboxSetError(errors: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): SetError =
- errors.head match {
- case (path, Seq()) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in mailbox object is not valid"))
- case (path, Seq(JsonValidationError(Seq("error.path.missing")))) => SetError.invalidArguments(SetErrorDescription(s"Missing '$path' property in mailbox object"))
- case (path, Seq(JsonValidationError(Seq(message)))) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in mailbox object is not valid: $message"))
- case (path, _) => SetError.invalidArguments(SetErrorDescription(s"Unknown error on property '$path'"))
- }
-
- private def createMailbox(mailboxSession: MailboxSession,
- path: MailboxPath,
- mailboxCreationRequest: MailboxCreationRequest): Either[Exception, MailboxCreationResponse] = {
- try {
- //can safely do a get as the Optional is empty only if the mailbox name is empty which is forbidden by the type constraint on MailboxName
- val mailboxId = mailboxManager.createMailbox(path, mailboxSession).get()
-
- val defaultSubscribed = IsSubscribed(true)
- if (mailboxCreationRequest.isSubscribed.getOrElse(defaultSubscribed).value) {
- subscriptionManager.subscribe(mailboxSession, path.getName)
- }
-
- mailboxCreationRequest.rights
- .foreach(rights => mailboxManager.setRights(mailboxId, rights.toMailboxAcl.asJava, mailboxSession))
-
- val quotas = quotaFactory.loadFor(mailboxSession)
- .flatMap(quotaLoader => quotaLoader.getQuotas(path))
- .block()
-
- Right(MailboxCreationResponse(
- id = mailboxId,
- sortOrder = SortOrder.defaultSortOrder,
- role = None,
- totalEmails = TotalEmails(0L),
- unreadEmails = UnreadEmails(0L),
- totalThreads = TotalThreads(0L),
- unreadThreads = UnreadThreads(0L),
- myRights = MailboxRights.FULL,
- quotas = Some(quotas),
- isSubscribed = if (mailboxCreationRequest.isSubscribed.isEmpty) {
- Some(defaultSubscribed)
- } else {
- None
- }))
- } catch {
- case error: Exception => Left(error)
- }
- }
-
- private def recordCreationIdInProcessingContext(mailboxCreationId: MailboxCreationId,
- processingContext: ProcessingContext,
- mailboxId: MailboxId): Either[IllegalArgumentException, ProcessingContext] = {
- for {
- creationId <- Id.validate(mailboxCreationId)
- serverAssignedId <- Id.validate(mailboxId.serialize())
- } yield {
- processingContext.recordCreatedId(ClientId(creationId), ServerId(serverAssignedId))
- }
- }
-
- private def resolvePath(mailboxSession: MailboxSession,
- mailboxCreationRequest: MailboxCreationRequest): Either[Exception, MailboxPath] = {
- if (mailboxCreationRequest.name.value.contains(mailboxSession.getPathDelimiter)) {
- return Left(new MailboxNameException(s"The mailbox '${mailboxCreationRequest.name.value}' contains an illegal character: '${mailboxSession.getPathDelimiter}'"))
- }
- mailboxCreationRequest.parentId
- .map(maybeParentId => for {
- parentId <- Try(mailboxIdFactory.fromString(maybeParentId.value))
- .toEither
- .left
- .map(e => new IllegalArgumentException(e.getMessage, e))
- parentPath <- retrievePath(parentId, mailboxSession)
- } yield {
- parentPath.child(mailboxCreationRequest.name, mailboxSession.getPathDelimiter)
- })
- .getOrElse(Right(MailboxPath.forUser(mailboxSession.getUser, mailboxCreationRequest.name)))
- }
-
- private def retrievePath(mailboxId: MailboxId, mailboxSession: MailboxSession): Either[Exception, MailboxPath] = try {
- Right(mailboxManager.getMailbox(mailboxId, mailboxSession).getMailboxPath)
- } catch {
- case e: Exception => Left(e)
- }
-
private def createResponse(capabilities: Set[CapabilityIdentifier],
invocation: Invocation,
mailboxSetRequest: MailboxSetRequest,
- creationResults: CreationResults,
- deletionResults: DeletionResults,
- updateResults: UpdateResults): Invocation = {
+ creationResults: MailboxCreationResults,
+ deletionResults: MailboxDeletionResults,
+ updateResults: MailboxUpdateResults): Invocation = {
val response = MailboxSetResponse(
mailboxSetRequest.accountId,
oldState = None,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
new file mode 100644
index 0000000..176324f
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
@@ -0,0 +1,222 @@
+/****************************************************************
+ * 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.core.CapabilityIdentifier.CapabilityIdentifier
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{Properties, SetError}
+import org.apache.james.jmap.json.MailboxSerializer
+import org.apache.james.jmap.mail.MailboxGet.UnparsedMailboxId
+import org.apache.james.jmap.mail.{InvalidPatchException, InvalidPropertyException, InvalidUpdateException, MailboxGet, MailboxPatchObject, MailboxSetRequest, MailboxSetResponse, MailboxUpdateResponse, NameUpdate, ParentIdUpdate, ServerSetPropertyException, UnsupportedPropertyUpdatedException, ValidatedMailboxPatchObject}
+import org.apache.james.jmap.method.MailboxSetUpdatePerformer.{MailboxUpdateFailure, MailboxUpdateResult, MailboxUpdateResults, MailboxUpdateSuccess}
+import org.apache.james.mailbox.MailboxManager.RenameOption
+import org.apache.james.mailbox.exception.{InsufficientRightsException, MailboxExistsException, MailboxNameException, MailboxNotFoundException}
+import org.apache.james.mailbox.model.{MailboxId, MailboxPath}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession, MessageManager, Role, SubscriptionManager}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+import scala.jdk.CollectionConverters._
+
+object MailboxSetUpdatePerformer {
+
+ sealed trait MailboxUpdateResult
+ case class MailboxUpdateSuccess(mailboxId: MailboxId) extends MailboxUpdateResult
+ case class MailboxUpdateFailure(mailboxId: UnparsedMailboxId, exception: Throwable, patch: Option[ValidatedMailboxPatchObject]) extends MailboxUpdateResult {
+ def filter(acceptableProperties: Properties): Option[Properties] = Some(patch
+ .map(_.updatedProperties.intersect(acceptableProperties))
+ .getOrElse(acceptableProperties))
+
+ def asMailboxSetError: SetError = exception match {
+ case e: MailboxNotFoundException => SetError.notFound(SetErrorDescription(e.getMessage))
+ case e: MailboxNameException => SetError.invalidArguments(SetErrorDescription(e.getMessage), filter(Properties("name", "parentId")))
+ case e: MailboxExistsException => SetError.invalidArguments(SetErrorDescription(e.getMessage), filter(Properties("name", "parentId")))
+ case e: UnsupportedPropertyUpdatedException => SetError.invalidArguments(SetErrorDescription(s"${e.property} property do not exist thus cannot be updated"), Some(Properties(e.property)))
+ case e: InvalidUpdateException => SetError.invalidArguments(SetErrorDescription(s"${e.cause}"), Some(Properties(e.property)))
+ case e: ServerSetPropertyException => SetError.invalidArguments(SetErrorDescription("Can not modify server-set properties"), Some(Properties(e.property)))
+ case e: InvalidPropertyException => SetError.invalidPatch(SetErrorDescription(s"${e.cause}"))
+ case e: InvalidPatchException => SetError.invalidPatch(SetErrorDescription(s"${e.cause}"))
+ case e: SystemMailboxChangeException => SetError.invalidArguments(SetErrorDescription("Invalid change to a system mailbox"), filter(Properties("name", "parentId")))
+ case e: LoopInMailboxGraphException => SetError.invalidArguments(SetErrorDescription("A mailbox parentId property can not be set to itself or one of its child"), Some(Properties("parentId")))
+ case e: InsufficientRightsException => SetError.invalidArguments(SetErrorDescription("Invalid change to a delegated mailbox"))
+ case e: MailboxHasChildException => SetError.invalidArguments(SetErrorDescription(s"${e.mailboxId.serialize()} parentId property cannot be updated as this mailbox has child mailboxes"), Some(Properties("parentId")))
+ case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage), None)
+ case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
+ }
+ }
+ case class MailboxUpdateResults(results: Seq[MailboxUpdateResult]) {
+ def updated: Map[MailboxId, MailboxUpdateResponse] =
+ results.flatMap(result => result match {
+ case success: MailboxUpdateSuccess => Some((success.mailboxId, MailboxSetResponse.empty))
+ case _ => None
+ }).toMap
+ def notUpdated: Map[UnparsedMailboxId, SetError] = results.flatMap(result => result match {
+ case failure: MailboxUpdateFailure => Some(failure.mailboxId, failure.asMailboxSetError)
+ case _ => None
+ }).toMap
+ }
+}
+
+class MailboxSetUpdatePerformer @Inject()(serializer: MailboxSerializer,
+ mailboxManager: MailboxManager,
+ subscriptionManager: SubscriptionManager,
+ mailboxIdFactory: MailboxId.Factory) {
+
+ def updateMailboxes(mailboxSession: MailboxSession,
+ mailboxSetRequest: MailboxSetRequest,
+ capabilities: Set[CapabilityIdentifier]): SMono[MailboxUpdateResults] = {
+ SFlux.fromIterable(mailboxSetRequest.update.getOrElse(Seq()))
+ .flatMap({
+ case (unparsedMailboxId: UnparsedMailboxId, patch: MailboxPatchObject) =>
+ MailboxGet.parse(mailboxIdFactory)(unparsedMailboxId)
+ .fold(
+ e => SMono.just(MailboxUpdateFailure(unparsedMailboxId, e, None)),
+ mailboxId => updateMailbox(mailboxSession, mailboxId, unparsedMailboxId, patch, capabilities))
+ .onErrorResume(e => SMono.just(MailboxUpdateFailure(unparsedMailboxId, e, None)))
+ })
+ .collectSeq()
+ .map(MailboxUpdateResults)
+ }
+
+ private def updateMailbox(mailboxSession: MailboxSession,
+ mailboxId: MailboxId,
+ unparsedMailboxId: UnparsedMailboxId,
+ patch: MailboxPatchObject,
+ capabilities: Set[CapabilityIdentifier]): SMono[MailboxUpdateResult] = {
+ patch.validate(mailboxIdFactory, serializer, capabilities, mailboxSession)
+ .fold(e => SMono.raiseError(e), validatedPatch =>
+ updateMailboxRights(mailboxId, validatedPatch, mailboxSession)
+ .`then`(updateSubscription(mailboxId, validatedPatch, mailboxSession))
+ .`then`(updateMailboxPath(mailboxId, unparsedMailboxId, validatedPatch, mailboxSession)))
+ }
+
+ private def updateSubscription(mailboxId: MailboxId, validatedPatch: ValidatedMailboxPatchObject, mailboxSession: MailboxSession): SMono[MailboxUpdateResult] = {
+ validatedPatch.isSubscribedUpdate.map(isSubscribedUpdate => {
+ SMono.fromCallable(() => {
+ val mailbox = mailboxManager.getMailbox(mailboxId, mailboxSession)
+ val isOwner = mailbox.getMailboxPath.belongsTo(mailboxSession)
+ val shouldSubscribe = isSubscribedUpdate.isSubscribed.map(_.value).getOrElse(isOwner)
+
+ if (shouldSubscribe) {
+ subscriptionManager.subscribe(mailboxSession, mailbox.getMailboxPath.getName)
+ } else {
+ subscriptionManager.unsubscribe(mailboxSession, mailbox.getMailboxPath.getName)
+ }
+ }).`then`(SMono.just[MailboxUpdateResult](MailboxUpdateSuccess(mailboxId)))
+ .subscribeOn(Schedulers.elastic())
+ })
+ .getOrElse(SMono.just[MailboxUpdateResult](MailboxUpdateSuccess(mailboxId)))
+ }
+
+ private def updateMailboxPath(mailboxId: MailboxId,
+ unparsedMailboxId: UnparsedMailboxId,
+ validatedPatch: ValidatedMailboxPatchObject,
+ mailboxSession: MailboxSession): SMono[MailboxUpdateResult] = {
+ if (validatedPatch.shouldUpdateMailboxPath) {
+ SMono.fromCallable[MailboxUpdateResult](() => {
+ try {
+ val mailbox = mailboxManager.getMailbox(mailboxId, mailboxSession)
+ if (isASystemMailbox(mailbox)) {
+ throw SystemMailboxChangeException(mailboxId)
+ }
+ if (validatedPatch.parentIdUpdate.flatMap(_.newId).contains(mailboxId)) {
+ throw LoopInMailboxGraphException(mailboxId)
+ }
+ val oldPath = mailbox.getMailboxPath
+ val newPath = applyParentIdUpdate(mailboxId, validatedPatch.parentIdUpdate, mailboxSession)
+ .andThen(applyNameUpdate(validatedPatch.nameUpdate, mailboxSession))
+ .apply(oldPath)
+ if (!oldPath.equals(newPath)) {
+ mailboxManager.renameMailbox(mailboxId,
+ newPath,
+ RenameOption.RENAME_SUBSCRIPTIONS,
+ mailboxSession)
+ }
+ MailboxUpdateSuccess(mailboxId)
+ } catch {
+ case e: Exception => MailboxUpdateFailure(unparsedMailboxId, e, Some(validatedPatch))
+ }
+ })
+ .subscribeOn(Schedulers.elastic())
+ } else {
+ SMono.just[MailboxUpdateResult](MailboxUpdateSuccess(mailboxId))
+ }
+ }
+
+ private def applyParentIdUpdate(mailboxId: MailboxId, maybeParentIdUpdate: Option[ParentIdUpdate], mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
+ maybeParentIdUpdate.map(parentIdUpdate => applyParentIdUpdate(mailboxId, parentIdUpdate, mailboxSession))
+ .getOrElse(x => x)
+ }
+
+ private def applyNameUpdate(maybeNameUpdate: Option[NameUpdate], mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
+ originalPath => maybeNameUpdate.map(nameUpdate => {
+ val originalParentPath: Option[MailboxPath] = originalPath.getHierarchyLevels(mailboxSession.getPathDelimiter)
+ .asScala
+ .reverse
+ .drop(1)
+ .headOption
+ originalParentPath.map(_.child(nameUpdate.newName, mailboxSession.getPathDelimiter))
+ .getOrElse(MailboxPath.forUser(mailboxSession.getUser, nameUpdate.newName))
+ }).getOrElse(originalPath)
+ }
+
+ private def applyParentIdUpdate(mailboxId: MailboxId, parentIdUpdate: ParentIdUpdate, mailboxSession: MailboxSession): MailboxPath => MailboxPath = {
+ originalPath => {
+ val currentName = originalPath.getName(mailboxSession.getPathDelimiter)
+ parentIdUpdate.newId
+ .map(id => {
+ if (mailboxManager.hasChildren(originalPath, mailboxSession)) {
+ throw MailboxHasChildException(mailboxId)
+ }
+ val parentPath = mailboxManager.getMailbox(id, mailboxSession).getMailboxPath
+ parentPath.child(currentName, mailboxSession.getPathDelimiter)
+ })
+ .getOrElse(MailboxPath.forUser(originalPath.getUser, currentName))
+ }
+ }
+
+ private def updateMailboxRights(mailboxId: MailboxId,
+ validatedPatch: ValidatedMailboxPatchObject,
+ mailboxSession: MailboxSession): SMono[MailboxUpdateResult] = {
+
+ val resetOperation: SMono[Unit] = validatedPatch.rightsReset.map(sharedWithResetUpdate => {
+ SMono.fromCallable(() => {
+ mailboxManager.setRights(mailboxId, sharedWithResetUpdate.rights.toMailboxAcl.asJava, mailboxSession)
+ }).`then`()
+ }).getOrElse(SMono.empty)
+
+ val partialUpdatesOperation: SMono[Unit] = SFlux.fromIterable(validatedPatch.rightsPartialUpdates)
+ .flatMap(partialUpdate => SMono.fromCallable(() => {
+ mailboxManager.applyRightsCommand(mailboxId, partialUpdate.asACLCommand(), mailboxSession)
+ }))
+ .`then`()
+
+ SFlux.merge(Seq(resetOperation, partialUpdatesOperation))
+ .`then`()
+ .`then`(SMono.just[MailboxUpdateResult](MailboxUpdateSuccess(mailboxId)))
+ .subscribeOn(Schedulers.elastic())
+
+ }
+
+ private def isASystemMailbox(mailbox: MessageManager): Boolean = Role.from(mailbox.getMailboxPath.getName).isPresent
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org