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]
