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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to