You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/11/24 01:43:37 UTC

[james-project] 01/08: JAMES-3534 Implement Identity/set create

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 4f357bad39383a397e1ec583bf670fedf72db441
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Tue Nov 16 12:13:58 2021 +0700

    JAMES-3534 Implement Identity/set create
---
 .../jmap/api/identity/CustomIdentityDAO.scala      |   2 +-
 .../james/jmap/json/IdentitySerializer.scala       |  37 +++++--
 .../org/apache/james/jmap/mail/IdentitySet.scala   |  76 +++++++++++++++
 .../jmap/method/IdentitySetCreatePerformer.scala   | 108 +++++++++++++++++++++
 .../james/jmap/method/IdentitySetMethod.scala      |  61 ++++++++++++
 5 files changed, 275 insertions(+), 9 deletions(-)

diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
index 027aea7..bd30740 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
@@ -25,7 +25,7 @@ import java.util.UUID
 import com.google.common.collect.ImmutableList
 import javax.inject.Inject
 import org.apache.james.core.{MailAddress, Username}
-import org.apache.james.jmap.api.model.{EmailAddress, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
+import org.apache.james.jmap.api.model.{EmailAddress, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, PushSubscriptionCreationRequest, TextSignature}
 import org.apache.james.rrt.api.CanSendFrom
 import org.reactivestreams.Publisher
 import reactor.core.scala.publisher.{SFlux, SMono}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala
index 724be33..b1d194c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala
@@ -19,26 +19,45 @@
 
 package org.apache.james.jmap.json
 
+import eu.timepit.refined.refineV
+import org.apache.james.jmap.api.identity.IdentityCreationRequest
 import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
-import org.apache.james.jmap.core.{Properties, UuidState}
+import org.apache.james.jmap.core.Id.IdConstraint
+import org.apache.james.jmap.core.{Properties, SetError, UuidState}
 import org.apache.james.jmap.mail._
-import play.api.libs.json.{Format, JsArray, JsObject, JsResult, JsSuccess, JsValue, Json, OWrites, Reads, Writes, __}
+import play.api.libs.json.{Format, JsArray, JsError, JsObject, JsResult, JsSuccess, JsValue, Json, OWrites, Reads, Writes, __}
 
 object IdentitySerializer {
-  private implicit val emailerNameReads: Writes[EmailerName] = Json.valueWrites[EmailerName]
+  private implicit val emailerNameReads: Format[EmailerName] = Json.valueFormat[EmailerName]
   private implicit val identityIdFormat: Format[IdentityId] = Json.valueFormat[IdentityId]
   private implicit val identityIdUnparsedFormat: Format[UnparsedIdentityId] = Json.valueFormat[UnparsedIdentityId]
   private implicit val identityIdsFormat: Format[IdentityIds] = Json.valueFormat[IdentityIds]
-  private implicit val emailAddressReads: Writes[EmailAddress] = Json.writes[EmailAddress]
-  private implicit val nameWrites: Writes[IdentityName] = Json.valueWrites[IdentityName]
-  private implicit val textSignatureWrites: Writes[TextSignature] = Json.valueWrites[TextSignature]
-  private implicit val htmlSignatureWrites: Writes[HtmlSignature] = Json.valueWrites[HtmlSignature]
+  private implicit val emailAddressReads: Format[EmailAddress] = Json.format[EmailAddress]
+  private implicit val nameWrites: Format[IdentityName] = Json.valueFormat[IdentityName]
+  private implicit val textSignatureWrites: Format[TextSignature] = Json.valueFormat[TextSignature]
+  private implicit val htmlSignatureWrites: Format[HtmlSignature] = Json.valueFormat[HtmlSignature]
   private implicit val mayDeleteWrites: Writes[MayDeleteIdentity] = Json.valueWrites[MayDeleteIdentity]
   private implicit val identityWrites: Writes[Identity] = Json.writes[Identity]
   private implicit val identityGetRequestReads: Reads[IdentityGetRequest] = Json.reads[IdentityGetRequest]
   private implicit val stateWrites: Writes[UuidState] = Json.valueWrites[UuidState]
   private implicit val identityGetResponseWrites: OWrites[IdentityGetResponse] = Json.writes[IdentityGetResponse]
 
+  private implicit val identityCreationIdWrites: Writes[IdentityCreationId] = Json.valueWrites[IdentityCreationId]
+  private implicit val identityCreationResponseWrites: Writes[IdentityCreationResponse] = Json.writes[IdentityCreationResponse]
+  private implicit val identityMapCreationResponseWrites: Writes[Map[IdentityCreationId, IdentityCreationResponse]] =
+    mapWrites[IdentityCreationId, IdentityCreationResponse](id => identityCreationIdWrites.writes(id).as[String], identityCreationResponseWrites)
+  private implicit val identityMapSetErrorForCreationWrites: Writes[Map[IdentityCreationId, SetError]] =
+    mapWrites[IdentityCreationId, SetError](_.serialise, setErrorWrites)
+  private implicit val identitySetResponseWrites: OWrites[IdentitySetResponse] = Json.writes[IdentitySetResponse]
+
+  private implicit val mapCreationRequestByIdentityCreationId: Reads[Map[IdentityCreationId, JsObject]] =
+    Reads.mapReads[IdentityCreationId, JsObject] {string => refineV[IdConstraint](string)
+      .fold(e => JsError(s"identity creationId needs to match id constraints: $e"),
+        id => JsSuccess(IdentityCreationId(id)))
+    }
+  private implicit val identitySetRequestReads: Reads[IdentitySetRequest] = Json.reads[IdentitySetRequest]
+  private implicit val identityCreationRequest: Reads[IdentityCreationRequest] = Json.reads[IdentityCreationRequest]
+
   def serialize(response: IdentityGetResponse, properties: Properties): JsObject = Json.toJsObject(response)
     .transform((__ \ "list").json.update {
       case JsArray(underlying) => JsSuccess(JsArray(underlying.map {
@@ -46,7 +65,9 @@ object IdentitySerializer {
         case jsValue => jsValue
       }))
     }).get
+  def serialize(response: IdentitySetResponse): JsObject = Json.toJsObject(response)
 
   def deserialize(input: JsValue): JsResult[IdentityGetRequest] = Json.fromJson[IdentityGetRequest](input)
-
+  def deserializeIdentitySetRequest(input: JsValue): JsResult[IdentitySetRequest] = Json.fromJson[IdentitySetRequest](input)
+  def deserializeIdentityCreationRequest(input: JsValue): JsResult[IdentityCreationRequest] = Json.fromJson[IdentityCreationRequest](input)
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentitySet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentitySet.scala
new file mode 100644
index 0000000..8fa32ac
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/IdentitySet.scala
@@ -0,0 +1,76 @@
+/****************************************************************
+ * 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.mail
+
+import eu.timepit.refined.collection.NonEmpty
+import eu.timepit.refined.refineV
+import eu.timepit.refined.types.string.NonEmptyString
+import org.apache.james.jmap.api.model.{HtmlSignature, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
+import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{AccountId, Properties, SetError, UuidState}
+import org.apache.james.jmap.method.WithAccountId
+import play.api.libs.json.JsObject
+
+object IdentityCreation {
+  private val serverSetProperty: Set[String] = Set("id", "mayDelete")
+  private val assignableProperties: Set[String] = Set("name", "email", "replyTo", "bcc", "textSignature", "htmlSignature")
+  private val knownProperties: Set[String] = assignableProperties ++ serverSetProperty
+
+  def validateProperties(jsObject: JsObject): Either[IdentityCreationParseException, JsObject] =
+    (jsObject.keys.intersect(serverSetProperty), jsObject.keys.diff(knownProperties)) match {
+      case (_, unknownProperties) if unknownProperties.nonEmpty =>
+        Left(IdentityCreationParseException(SetError.invalidArguments(
+          SetErrorDescription("Some unknown properties were specified"),
+          Some(toProperties(unknownProperties.toSet)))))
+      case (specifiedServerSetProperties, _) if specifiedServerSetProperties.nonEmpty =>
+        Left(IdentityCreationParseException(SetError.invalidArguments(
+          SetErrorDescription("Some server-set properties were specified"),
+          Some(toProperties(specifiedServerSetProperties.toSet)))))
+      case _ => scala.Right(jsObject)
+    }
+
+  private def toProperties(strings: Set[String]): Properties = Properties(strings
+    .flatMap(string => {
+      val refinedValue: Either[String, NonEmptyString] = refineV[NonEmpty](string)
+      refinedValue.fold(_ => None, Some(_))
+    }))
+}
+
+case class IdentitySetRequest(accountId: AccountId,
+                              create: Option[Map[IdentityCreationId, JsObject]]) extends WithAccountId
+
+case class IdentityCreationId(id: Id) {
+  def serialise: String = id.value
+}
+
+case class IdentityCreationResponse(id: IdentityId,
+                                    name: Option[IdentityName],
+                                    textSignature: Option[TextSignature],
+                                    htmlSignature: Option[HtmlSignature],
+                                    mayDelete: MayDeleteIdentity)
+
+case class IdentitySetResponse(accountId: AccountId,
+                               oldState: Option[UuidState],
+                               newState: UuidState,
+                               created: Option[Map[IdentityCreationId, IdentityCreationResponse]],
+                               notCreated: Option[Map[IdentityCreationId, SetError]])
+
+case class IdentityCreationParseException(setError: SetError) extends IllegalArgumentException
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetCreatePerformer.scala
new file mode 100644
index 0000000..13d567a
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetCreatePerformer.scala
@@ -0,0 +1,108 @@
+/****************************************************************
+ * 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.api.identity.{IdentityCreationRequest, IdentityRepository}
+import org.apache.james.jmap.api.model.{ForbiddenSendFromException, HtmlSignature, Identity, IdentityName, TextSignature}
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.{Properties, SetError}
+import org.apache.james.jmap.json.IdentitySerializer
+import org.apache.james.jmap.mail.{IdentityCreation, IdentityCreationId, IdentityCreationParseException, IdentityCreationResponse, IdentitySetRequest}
+import org.apache.james.jmap.method.IdentitySetCreatePerformer.{CreationFailure, CreationResult, CreationResults, CreationSuccess}
+import org.apache.james.mailbox.MailboxSession
+import play.api.libs.json.{JsObject, JsPath, JsonValidationError}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+object IdentitySetCreatePerformer {
+  case class CreationResults(results: Seq[CreationResult]) {
+    def created: Option[Map[IdentityCreationId, IdentityCreationResponse]] =
+      Option(results.flatMap {
+        case result: CreationSuccess => Some((result.clientId, result.response))
+        case _ => None
+      }.toMap)
+        .filter(_.nonEmpty)
+
+    def notCreated: Option[Map[IdentityCreationId, SetError]] =
+      Option(results.flatMap {
+        case failure: CreationFailure => Some((failure.clientId, failure.asMessageSetError))
+        case _ => None
+      }
+        .toMap)
+        .filter(_.nonEmpty)
+  }
+
+  trait CreationResult
+
+  case class CreationSuccess(clientId: IdentityCreationId, response: IdentityCreationResponse) extends CreationResult
+
+  case class CreationFailure(clientId: IdentityCreationId, e: Throwable) extends CreationResult {
+    def asMessageSetError: SetError = e match {
+      case e: IdentityCreationParseException => e.setError
+      case e: ForbiddenSendFromException => SetError.forbiddenFrom(SetErrorDescription(e.getMessage))
+      case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage))
+      case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
+    }
+  }
+}
+
+class IdentitySetCreatePerformer @Inject()(identityRepository: IdentityRepository) {
+  def create(request: IdentitySetRequest, mailboxSession: MailboxSession): SMono[CreationResults] =
+    SFlux.fromIterable(request.create.getOrElse(Map()))
+      .concatMap {
+        case (clientId, json) => parseCreate(json)
+          .fold(e => SMono.just[CreationResult](CreationFailure(clientId, e)),
+            creationRequest => create(clientId, creationRequest, mailboxSession))
+      }.collectSeq()
+      .map(CreationResults)
+
+  private def parseCreate(jsObject: JsObject): Either[Exception, IdentityCreationRequest] = for {
+    validJsObject <- IdentityCreation.validateProperties(jsObject)
+    parsedRequest <- IdentitySerializer.deserializeIdentityCreationRequest(validJsObject).asEither
+      .left.map(errors => IdentityCreationParseException(IdentitySetError(errors)))
+  } yield {
+    parsedRequest
+  }
+
+  private def create(clientId: IdentityCreationId, request: IdentityCreationRequest, mailboxSession: MailboxSession): SMono[CreationResult] =
+    SMono.fromPublisher(identityRepository.save(mailboxSession.getUser, request))
+      .map(identity => CreationSuccess(clientId, evaluateCreationResponse(request, identity)))
+      .onErrorResume(e => SMono.just[CreationResult](CreationFailure(clientId, e)))
+      .subscribeOn(Schedulers.elastic)
+
+  private def evaluateCreationResponse(request: IdentityCreationRequest, identity: Identity): IdentityCreationResponse =
+    IdentityCreationResponse(
+      id = identity.id,
+      name = request.name.fold[Option[IdentityName]](Some(IdentityName.DEFAULT))(_ => None),
+      textSignature = request.textSignature.fold[Option[TextSignature]](Some(TextSignature.DEFAULT))(_ => None),
+      htmlSignature = request.htmlSignature.fold[Option[HtmlSignature]](Some(HtmlSignature.DEFAULT))(_ => None),
+      mayDelete = identity.mayDelete)
+
+  private def IdentitySetError(errors: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): SetError =
+    errors.head match {
+      case (path, Seq()) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in Identity object is not valid"))
+      case (path, Seq(JsonValidationError(Seq("error.path.missing")))) =>
+        SetError.invalidArguments(SetErrorDescription(s"Missing '$path' property in Identity object"), Some(Properties("email")))
+      case (path, Seq(JsonValidationError(Seq(message)))) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in Identity object is not valid: $message"))
+      case (path, _) => SetError.invalidArguments(SetErrorDescription(s"Unknown error on property '$path'"))
+    }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetMethod.scala
new file mode 100644
index 0000000..6138f2a
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetMethod.scala
@@ -0,0 +1,61 @@
+/****************************************************************
+ * 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, EMAIL_SUBMISSION, JMAP_CORE}
+import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
+import org.apache.james.jmap.core.{Invocation, UuidState}
+import org.apache.james.jmap.json.{IdentitySerializer, ResponseSerializer}
+import org.apache.james.jmap.mail.{IdentitySetRequest, IdentitySetResponse}
+import org.apache.james.jmap.routes.SessionSupplier
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.metrics.api.MetricFactory
+import play.api.libs.json.{JsError, JsSuccess}
+import reactor.core.scala.publisher.SMono
+
+class IdentitySetMethod @Inject()(createPerformer: IdentitySetCreatePerformer,
+                                  val metricFactory: MetricFactory,
+                                  val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[IdentitySetRequest] {
+  override val methodName: Invocation.MethodName = MethodName("Identity/set")
+  override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, EMAIL_SUBMISSION)
+
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[Exception, IdentitySetRequest] =
+    IdentitySerializer.deserializeIdentitySetRequest(invocation.arguments.value) match {
+      case JsSuccess(identitySetRequest, _) => Right(identitySetRequest)
+      case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+    }
+
+  override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: IdentitySetRequest): SMono[InvocationWithContext] =
+    for {
+      creationResults <- createPerformer.create(request, mailboxSession)
+    } yield InvocationWithContext(
+      invocation = Invocation(
+        methodName = methodName,
+        arguments = Arguments(IdentitySerializer.serialize(IdentitySetResponse(
+          accountId = request.accountId,
+          oldState = None,
+          newState = UuidState.INSTANCE,
+          created = creationResults.created.filter(_.nonEmpty),
+          notCreated = creationResults.notCreated.filter(_.nonEmpty)))),
+        methodCallId = invocation.invocation.methodCallId),
+      processingContext = invocation.processingContext)
+}

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org