You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2020/10/27 02:07:54 UTC

[james-project] 02/04: JAMES-3436 Email/set create - mailboxIds & subject properties

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 339a016f6608f38ce651aa4f0f958135c176d97f
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Oct 23 15:39:45 2020 +0700

    JAMES-3436 Email/set create - mailboxIds & subject properties
---
 .../rfc8621/contract/EmailSetMethodContract.scala  |  58 ++++++++++
 .../james/jmap/json/EmailSetSerializer.scala       |  18 +++-
 .../org/apache/james/jmap/mail/EmailSet.scala      |  22 +++-
 .../apache/james/jmap/method/EmailSetMethod.scala  | 118 +++++++++++++++------
 4 files changed, 179 insertions(+), 37 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 2898b92..d8a3e14 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -175,6 +175,64 @@ trait EmailSetMethodContract {
   }
 
   @Test
+  def createShouldAddAnEmailInTargetMailbox(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "create": {
+         |        "aaaaaa":{
+         |          "mailboxIds": {
+         |             "${mailboxId.serialize}": true
+         |          },
+         |          "subject": "Boredome comes from a boring mind!"
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["#aaaaaa"],
+         |       "properties": ["mailboxIds", "subject"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
+      .inPath("methodResponses[0][1].created.aaaaaa")
+      .isEqualTo("{}".stripMargin)
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[1][1].list[0].id")
+      .inPath(s"methodResponses[1][1].list")
+      .isEqualTo(
+        s"""[{
+          |  "mailboxIds": {
+          |    "${mailboxId.serialize}": true
+          |  },
+          |  "subject": "Boredome comes from a boring mind!"
+          |}]""".stripMargin)
+  }
+
+  @Test
   def shouldNotResetKeywordWhenInvalidKeyword(server: GuiceJamesServer): Unit = {
     val message: Message = Fixture.createTestMessage
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index 2928c03..e36e3ca 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -22,8 +22,9 @@ package org.apache.james.jmap.json
 import cats.implicits._
 import eu.timepit.refined.refineV
 import javax.inject.Inject
-import org.apache.james.jmap.mail.EmailSet.{UnparsedMessageId, UnparsedMessageIdConstraint}
-import org.apache.james.jmap.mail.{DestroyIds, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds}
+import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId, UnparsedMessageIdConstraint}
+import org.apache.james.jmap.mail.{DestroyIds, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds, Subject}
+import org.apache.james.jmap.model.Id.IdConstraint
 import org.apache.james.jmap.model.KeywordsFactory.STRICT_KEYWORDS_FACTORY
 import org.apache.james.jmap.model.{Keyword, Keywords, SetError}
 import org.apache.james.mailbox.model.{MailboxId, MessageId}
@@ -192,6 +193,13 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
         case _ => JsError("Expecting a JsObject as an update entry")
       })
 
+  private implicit val createsMapReads: Reads[Map[EmailCreationId, JsObject]] =
+    readMapEntry[EmailCreationId, JsObject](s => refineV[IdConstraint](s),
+      {
+        case o: JsObject => JsSuccess(o)
+        case _ => JsError("Expecting a JsObject as an update entry")
+      })
+
   private implicit val keywordReads: Reads[Keyword] = {
     case jsString: JsString => Keyword.parse(jsString.value)
       .fold(JsError(_),
@@ -218,10 +226,16 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
   }
   private implicit val destroyIdsWrites: Writes[DestroyIds] = Json.valueWrites[DestroyIds]
   private implicit val emailRequestSetReads: Reads[EmailSetRequest] = Json.reads[EmailSetRequest]
+  private implicit val emailCreationResponseWrites: Writes[EmailCreationResponse] = Json.writes[EmailCreationResponse]
   private implicit val emailResponseSetWrites: OWrites[EmailSetResponse] = Json.writes[EmailSetResponse]
 
+  private implicit val subjectReads: Reads[Subject] = Json.valueReads[Subject]
+  private implicit val emailCreationRequestReads: Reads[EmailCreationRequest] = Json.reads[EmailCreationRequest]
+
   def deserialize(input: JsValue): JsResult[EmailSetRequest] = Json.fromJson[EmailSetRequest](input)
 
+  def deserializeCreationRequest(input: JsValue): JsResult[EmailCreationRequest] = Json.fromJson[EmailCreationRequest](input)
+
   def deserializeEmailSetUpdate(input: JsValue): JsResult[EmailSetUpdate] = Json.fromJson[EmailSetUpdate](input)
 
   def serialize(response: EmailSetResponse): JsObject = Json.toJsObject(response)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index ddeac39..5985147 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -18,19 +18,24 @@
  ****************************************************************/
 package org.apache.james.jmap.mail
 
+import java.nio.charset.StandardCharsets
+
 import eu.timepit.refined
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.collection.NonEmpty
-import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
+import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId}
 import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.jmap.model.Id.Id
 import org.apache.james.jmap.model.State.State
 import org.apache.james.jmap.model.{AccountId, Keywords, SetError}
 import org.apache.james.mailbox.model.MessageId
+import org.apache.james.mime4j.dom.Message
 import play.api.libs.json.JsObject
 
 import scala.util.{Right, Try}
 
 object EmailSet {
+  type EmailCreationId = Id
   type UnparsedMessageIdConstraint = NonEmpty
   type UnparsedMessageId = String Refined UnparsedMessageIdConstraint
 
@@ -43,14 +48,27 @@ object EmailSet {
     Try(messageIdFactory.fromString(unparsed.value))
 }
 
+case class EmailCreationRequest(mailboxIds: MailboxIds,
+                                subject: Option[Subject]) {
+  def toMime4JMessage: Message = {
+    val builder = Message.Builder.of
+    subject.foreach(value => builder.setSubject(value.value))
+    builder.setBody("", StandardCharsets.UTF_8)
+    builder.build()
+  }
+}
+
 case class DestroyIds(value: Seq[UnparsedMessageId])
 
 case class EmailSetRequest(accountId: AccountId,
+                           create: Option[Map[EmailCreationId, JsObject]],
                            update: Option[Map[UnparsedMessageId, JsObject]],
                            destroy: Option[DestroyIds]) extends WithAccountId
 
 case class EmailSetResponse(accountId: AccountId,
                             newState: State,
+                            created: Option[Map[EmailCreationId, EmailCreationResponse]],
+                            notCreated: Option[Map[EmailCreationId, SetError]],
                             updated: Option[Map[MessageId, Unit]],
                             notUpdated: Option[Map[UnparsedMessageId, SetError]],
                             destroyed: Option[DestroyIds],
@@ -118,3 +136,5 @@ class EmailUpdateValidationException() extends IllegalArgumentException
 case class InvalidEmailPropertyException(property: String, cause: String) extends EmailUpdateValidationException
 case class InvalidEmailUpdateException(property: String, cause: String) extends EmailUpdateValidationException
 
+case class EmailCreationResponse(id: MessageId)
+
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index 1d1b519..58f523d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -28,16 +28,20 @@ import org.apache.james.jmap.http.SessionSupplier
 import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
 import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
 import org.apache.james.jmap.mail.{DestroyIds, EmailSet, EmailSetRequest, EmailSetResponse, MailboxIds, ValidatedEmailSetUpdate}
+import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId}
+import org.apache.james.jmap.mail.{DestroyIds, EmailCreationRequest, EmailCreationResponse, EmailSet, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds, ValidatedEmailSetUpdate}
 import org.apache.james.jmap.model.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.model.DefaultCapabilities.{CORE_CAPABILITY, MAIL_CAPABILITY}
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.model.KeywordsFactory.LENIENT_KEYWORDS_FACTORY
 import org.apache.james.jmap.model.SetError.SetErrorDescription
-import org.apache.james.jmap.model.{Capabilities, Invocation, SetError, State}
-import org.apache.james.mailbox.MessageManager.FlagsUpdateMode
+import org.apache.james.jmap.model.{Capabilities, ClientId, Id, Invocation, ServerId, SetError, State}
+import org.apache.james.mailbox.MessageManager.{AppendCommand, FlagsUpdateMode}
 import org.apache.james.mailbox.exception.MailboxNotFoundException
 import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MailboxId, MessageId, MessageRange}
 import org.apache.james.mailbox.{MailboxManager, MailboxSession, MessageIdManager, MessageManager}
+import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MailboxId, MessageId}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession, MessageIdManager}
 import org.apache.james.metrics.api.MetricFactory
 import play.api.libs.json.{JsError, JsObject, JsSuccess}
 import reactor.core.scala.publisher.{SFlux, SMono}
@@ -56,28 +60,20 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
 
 
   case class DestroyResults(results: Seq[DestroyResult]) {
-    def destroyed: Option[DestroyIds] = {
-      Option(results.flatMap({
-        result => result match {
+    def destroyed: Option[DestroyIds] =
+      Option(results.flatMap{
           case result: DestroySuccess => Some(result.messageId)
           case _ => None
-        }
-      }).map(EmailSet.asUnparsed))
+        }.map(EmailSet.asUnparsed))
         .filter(_.nonEmpty)
         .map(DestroyIds)
-    }
 
-    def notDestroyed: Option[Map[UnparsedMessageId, SetError]] = {
-      Option(results.flatMap({
-        result => result match {
-          case failure: DestroyFailure => Some(failure)
+    def notDestroyed: Option[Map[UnparsedMessageId, SetError]] =
+      Option(results.flatMap{
+          case failure: DestroyFailure => Some((failure.unparsedMessageId, failure.asMessageSetError))
           case _ => None
-        }
-      })
-        .map(failure => (failure.unparsedMessageId, failure.asMessageSetError))
-        .toMap)
+        }.toMap)
         .filter(_.nonEmpty)
-    }
   }
 
   object DestroyResult {
@@ -101,6 +97,31 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
     }
   }
 
+  case class CreationResults(results: Seq[CreationResult]) {
+    def created: Option[Map[EmailCreationId, EmailCreationResponse]] =
+      Option(results.flatMap{
+          case result: CreationSuccess => Some((result.clientId, result.response))
+          case _ => None
+        }.toMap)
+        .filter(_.nonEmpty)
+
+    def notCreated: Option[Map[EmailCreationId, SetError]] = {
+      Option(results.flatMap{
+          case failure: CreationFailure => Some((failure.clientId, failure.asMessageSetError))
+          case _ => None
+        }
+        .toMap)
+        .filter(_.nonEmpty)
+    }
+  }
+  trait CreationResult
+  case class CreationSuccess(clientId: EmailCreationId, response: EmailCreationResponse) extends CreationResult
+  case class CreationFailure(clientId: EmailCreationId, e: Throwable) extends CreationResult {
+    def asMessageSetError: SetError = e match {
+      case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
+    }
+  }
+
   trait UpdateResult
   case class UpdateSuccess(messageId: MessageId) extends UpdateResult
   case class UpdateFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends UpdateResult {
@@ -112,26 +133,19 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
     }
   }
   case class UpdateResults(results: Seq[UpdateResult]) {
-    def updated: Option[Map[MessageId, Unit]] = {
-      Option(results.flatMap({
-        result => result match {
+    def updated: Option[Map[MessageId, Unit]] =
+      Option(results.flatMap{
           case result: UpdateSuccess => Some(result.messageId, ())
           case _ => None
-        }
-      }).toMap).filter(_.nonEmpty)
-    }
+        }.toMap)
+        .filter(_.nonEmpty)
 
-    def notUpdated: Option[Map[UnparsedMessageId, SetError]] = {
-      Option(results.flatMap({
-        result => result match {
-          case failure: UpdateFailure => Some(failure)
+    def notUpdated: Option[Map[UnparsedMessageId, SetError]] =
+      Option(results.flatMap{
+          case failure: UpdateFailure => Some((failure.unparsedMessageId, failure.asMessageSetError))
           case _ => None
-        }
-      })
-        .map(failure => (failure.unparsedMessageId, failure.asMessageSetError))
-        .toMap)
+        }.toMap)
         .filter(_.nonEmpty)
-    }
   }
 
   override val methodName: MethodName = MethodName("Email/set")
@@ -140,19 +154,28 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
   override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSetRequest): SMono[InvocationWithContext] = {
     for {
       destroyResults <- destroy(request, mailboxSession)
-      updateResults <- update(request, mailboxSession).doOnError(e => e.printStackTrace())
+      updateResults <- update(request, mailboxSession)
+      created <- create(request, mailboxSession)
     } yield InvocationWithContext(
       invocation = Invocation(
         methodName = invocation.invocation.methodName,
         arguments = Arguments(serializer.serialize(EmailSetResponse(
           accountId = request.accountId,
           newState = State.INSTANCE,
+          created = created.created,
+          notCreated = created.notCreated,
           updated = updateResults.updated,
           notUpdated = updateResults.notUpdated,
           destroyed = destroyResults.destroyed,
           notDestroyed = destroyResults.notDestroyed))),
         methodCallId = invocation.invocation.methodCallId),
-      processingContext = invocation.processingContext)
+      processingContext = created.created.getOrElse(Map())
+          .foldLeft(invocation.processingContext)({
+            case (processingContext, (clientId, response)) =>
+              Id.validate(response.id.serialize)
+                  .fold(_ => processingContext,
+                    serverId => processingContext.recordCreatedId(ClientId(clientId), ServerId(serverId)))
+          }))
   }
 
   override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailSetRequest] = asEmailSetRequest(invocation.arguments)
@@ -188,6 +211,33 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
     }
   }
 
+  private def create(request: EmailSetRequest, mailboxSession: MailboxSession): SMono[CreationResults] =
+    SFlux.fromIterable(request.create.getOrElse(Map()))
+        .concatMap {
+          case (clientId, json) => serializer.deserializeCreationRequest(json)
+            .fold(e => SMono.just[CreationResult](CreationFailure(clientId, new IllegalArgumentException(e.toString))),
+              creationRequest => create(clientId, creationRequest, mailboxSession))
+        }.collectSeq()
+        .map(CreationResults)
+
+  private def create(clientId: EmailCreationId, request: EmailCreationRequest, mailboxSession: MailboxSession): SMono[CreationResult] = {
+    if (request.mailboxIds.value.size != 1) {
+      SMono.just(CreationFailure(clientId, new IllegalArgumentException("mailboxIds need to have size 1")))
+    } else {
+      SMono.fromCallable[CreationResult](() => {
+        val mailboxId: MailboxId = request.mailboxIds.value.headOption.get
+        val appendResult = mailboxManager.getMailbox(mailboxId, mailboxSession)
+          .appendMessage(AppendCommand.builder()
+            .recent()
+            .build(request.toMime4JMessage),
+            mailboxSession)
+        CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId))
+      })
+        .subscribeOn(Schedulers.elastic())
+        .onErrorResume(e => SMono.just[CreationResult](CreationFailure(clientId, e)))
+    }
+  }
+
   private def update(emailSetRequest: EmailSetRequest, mailboxSession: MailboxSession): SMono[UpdateResults] = {
     emailSetRequest.update
       .filter(_.nonEmpty)


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