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/30 07:15:51 UTC

[james-project] 01/02: JAMES-3438 Email/set create htmlBody

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 18a9f40fd6e5058f9461ccda5b96653e704fac12
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Oct 28 18:35:27 2020 +0700

    JAMES-3438 Email/set create htmlBody
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 78 +++++++++++++++++++
 .../james/jmap/json/EmailSetSerializer.scala       | 15 +++-
 .../org/apache/james/jmap/mail/EmailSet.scala      | 50 +++++++++----
 .../apache/james/jmap/method/EmailSetMethod.scala  | 87 ++++++++++++----------
 4 files changed, 171 insertions(+), 59 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 0e2f4bd..59de94d 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
@@ -851,6 +851,84 @@ trait EmailSetMethodContract {
   }
 
   @Test
+  def createShouldSupportHtmlBody(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    val htmlBody: String = "<!DOCTYPE html><html><head><title></title></head><body><div>I have the most <b>brilliant</b> plan. Let me tell you all about it. What we do is, we</div></body></html>"
+
+    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": "World domination",
+         |          "htmlBody": [
+         |            {
+         |              "partId": "a49d",
+         |              "type": "text/html"
+         |            }
+         |          ],
+         |          "bodyValues": {
+         |            "a49d": {
+         |              "value": "$htmlBody",
+         |              "isTruncated": false
+         |            }
+         |          }
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["#aaaaaa"],
+         |       "properties": ["mailboxIds", "subject", "bodyValues"]
+         |     },
+         |     "c2"]
+         |    ]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post.prettyPeek()
+    .`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": "World domination",
+           |  "bodyValues": {
+           |    "1": {
+           |      "value": "$htmlBody"
+           |    }
+           |  }
+           |}]""".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 caacce8..5c76a58 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
@@ -23,10 +23,10 @@ import cats.implicits._
 import eu.timepit.refined.refineV
 import javax.inject.Inject
 import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId, UnparsedMessageIdConstraint}
-import org.apache.james.jmap.mail.{AddressesHeaderValue, DestroyIds, EmailAddress, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, EmailSetResponse, EmailSetUpdate, EmailerName, HeaderMessageId, MailboxIds, MessageIdsHeaderValue, Subject}
+import org.apache.james.jmap.mail.{AddressesHeaderValue, ClientEmailBodyValue, ClientHtmlBody, ClientPartId, DestroyIds, EmailAddress, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, EmailSetResponse, EmailSetUpdate, EmailerName, HeaderMessageId, IsEncodingProblem, IsTruncated, MailboxIds, MessageIdsHeaderValue, Subject, Type}
 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.jmap.model.{Id, Keyword, Keywords, SetError}
 import org.apache.james.mailbox.model.{MailboxId, MessageId}
 import play.api.libs.json.{JsArray, JsBoolean, JsError, JsNull, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes}
 
@@ -242,6 +242,17 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
       .fold(e => JsError(e),
         ids => JsSuccess(MessageIdsHeaderValue(Some(ids).filter(_.nonEmpty))))
   }
+
+  private implicit val isTruncatedReads: Reads[IsTruncated] = Json.valueReads[IsTruncated]
+  private implicit val isEncodingProblemReads: Reads[IsEncodingProblem] = Json.valueReads[IsEncodingProblem]
+  private implicit val clientEmailBodyValueReads: Reads[ClientEmailBodyValue] = Json.reads[ClientEmailBodyValue]
+  private implicit val typeReads: Reads[Type] = Json.valueReads[Type]
+  private implicit val clientPartIdReads: Reads[ClientPartId] = Json.valueReads[ClientPartId]
+  private implicit val clientHtmlBodyReads: Reads[ClientHtmlBody] = Json.reads[ClientHtmlBody]
+  private implicit val bodyValuesReads: Reads[Map[ClientPartId, ClientEmailBodyValue]] =
+    readMapEntry[ClientPartId, ClientEmailBodyValue](s => Id.validate(s).fold(e => Left(e.getMessage), partId => Right(ClientPartId(partId))),
+      clientEmailBodyValueReads)
+
   private implicit val emailCreationRequestReads: Reads[EmailCreationRequest] = Json.reads[EmailCreationRequest]
 
   def deserialize(input: JsValue): JsResult[EmailSetRequest] = Json.fromJson[EmailSetRequest](input)
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 767f021..e5b31b3 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
@@ -53,6 +53,14 @@ object EmailSet {
     Try(messageIdFactory.fromString(unparsed.value))
 }
 
+case class ClientPartId(id: Id)
+
+case class ClientHtmlBody(partId: ClientPartId, `type`: Type)
+
+case class ClientEmailBodyValue(value: String,
+                                isEncodingProblem: Option[IsEncodingProblem],
+                                isTruncated: Option[IsTruncated])
+
 case class EmailCreationRequest(mailboxIds: MailboxIds,
                                 messageId: Option[MessageIdsHeaderValue],
                                 references: Option[MessageIdsHeaderValue],
@@ -66,22 +74,32 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
                                 subject: Option[Subject],
                                 sentAt: Option[UTCDate],
                                 keywords: Option[Keywords],
-                                receivedAt: Option[UTCDate]) {
-  def toMime4JMessage: Message = {
-    val builder = Message.Builder.of
-    references.flatMap(_.asString).map(new RawField("References", _)).foreach(builder.setField)
-    inReplyTo.flatMap(_.asString).map(new RawField("In-Reply-To", _)).foreach(builder.setField)
-    messageId.flatMap(_.asString).map(new RawField(FieldName.MESSAGE_ID, _)).foreach(builder.setField)
-    subject.foreach(value => builder.setSubject(value.value))
-    from.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setFrom)
-    to.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setTo)
-    cc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setCc)
-    bcc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setBcc)
-    sender.flatMap(_.asMime4JMailboxList).map(_.asJava).map(Fields.addressList(FieldName.SENDER, _)).foreach(builder.setField)
-    replyTo.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setReplyTo)
-    sentAt.map(_.asUTC).map(_.toInstant).map(Date.from).foreach(builder.setDate)
-    builder.setBody("", StandardCharsets.UTF_8)
-    builder.build()
+                                receivedAt: Option[UTCDate],
+                                htmlBody: Option[List[ClientHtmlBody]],
+                                bodyValues: Option[Map[ClientPartId, ClientEmailBodyValue]]) {
+  def toMime4JMessage: Either[IllegalArgumentException, Message] =
+    validateHtmlBody.map(maybeHtmlBody => {
+      val builder = Message.Builder.of
+      references.flatMap(_.asString).map(new RawField("References", _)).foreach(builder.setField)
+      inReplyTo.flatMap(_.asString).map(new RawField("In-Reply-To", _)).foreach(builder.setField)
+      messageId.flatMap(_.asString).map(new RawField(FieldName.MESSAGE_ID, _)).foreach(builder.setField)
+      subject.foreach(value => builder.setSubject(value.value))
+      from.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setFrom)
+      to.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setTo)
+      cc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setCc)
+      bcc.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setBcc)
+      sender.flatMap(_.asMime4JMailboxList).map(_.asJava).map(Fields.addressList(FieldName.SENDER, _)).foreach(builder.setField)
+      replyTo.flatMap(_.asMime4JMailboxList).map(_.asJava).foreach(builder.setReplyTo)
+      sentAt.map(_.asUTC).map(_.toInstant).map(Date.from).foreach(builder.setDate)
+      builder.setBody(maybeHtmlBody.getOrElse(""), "html", StandardCharsets.UTF_8)
+      builder.build()
+    })
+
+  def validateHtmlBody: Either[IllegalArgumentException, Option[String]] = htmlBody match {
+    case None => Right(None)
+    case Some(html :: Nil) => bodyValues.getOrElse(Map()).get(html.partId)
+      .map(part => Right(Some(part.value)))
+      .getOrElse(Left(new IllegalArgumentException("todo")))
   }
 }
 
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 8686751..cf36efc 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
@@ -59,17 +59,17 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
   case class DestroyResults(results: Seq[DestroyResult]) {
     def destroyed: Option[DestroyIds] =
       Option(results.flatMap{
-          case result: DestroySuccess => Some(result.messageId)
-          case _ => None
-        }.map(EmailSet.asUnparsed))
+        case result: DestroySuccess => Some(result.messageId)
+        case _ => None
+      }.map(EmailSet.asUnparsed))
         .filter(_.nonEmpty)
         .map(DestroyIds)
 
     def notDestroyed: Option[Map[UnparsedMessageId, SetError]] =
       Option(results.flatMap{
-          case failure: DestroyFailure => Some((failure.unparsedMessageId, failure.asMessageSetError))
-          case _ => None
-        }.toMap)
+        case failure: DestroyFailure => Some((failure.unparsedMessageId, failure.asMessageSetError))
+        case _ => None
+      }.toMap)
         .filter(_.nonEmpty)
   }
 
@@ -78,7 +78,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
       val success: Seq[DestroySuccess] = deleteResult.getDestroyed.asScala.toSeq
         .map(DestroySuccess)
       val notFound: Seq[DestroyResult] = deleteResult.getNotFound.asScala.toSeq
-          .map(id => DestroyFailure(EmailSet.asUnparsed(id), MessageNotFoundExeception(id)))
+        .map(id => DestroyFailure(EmailSet.asUnparsed(id), MessageNotFoundExeception(id)))
 
       success ++ notFound
     }
@@ -97,16 +97,16 @@ 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)
+        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
-        }
+        case failure: CreationFailure => Some((failure.clientId, failure.asMessageSetError))
+        case _ => None
+      }
         .toMap)
         .filter(_.nonEmpty)
     }
@@ -134,16 +134,16 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
   case class UpdateResults(results: Seq[UpdateResult]) {
     def updated: Option[Map[MessageId, Unit]] =
       Option(results.flatMap{
-          case result: UpdateSuccess => Some(result.messageId, ())
-          case _ => None
-        }.toMap)
+        case result: UpdateSuccess => Some(result.messageId, ())
+        case _ => None
+      }.toMap)
         .filter(_.nonEmpty)
 
     def notUpdated: Option[Map[UnparsedMessageId, SetError]] =
       Option(results.flatMap{
-          case failure: UpdateFailure => Some((failure.unparsedMessageId, failure.asMessageSetError))
-          case _ => None
-        }.toMap)
+        case failure: UpdateFailure => Some((failure.unparsedMessageId, failure.asMessageSetError))
+        case _ => None
+      }.toMap)
         .filter(_.nonEmpty)
   }
 
@@ -169,12 +169,12 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
           notDestroyed = destroyResults.notDestroyed))),
         methodCallId = invocation.invocation.methodCallId),
       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)))
-          }))
+        .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)
@@ -212,27 +212,32 @@ 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)
+      .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()
-            .withFlags(request.keywords.map(_.asFlags).getOrElse(new Flags()))
-              .withInternalDate(Date.from(request.receivedAt.getOrElse(UTCDate(ZonedDateTime.now())).asUTC.toInstant))
-            .build(request.toMime4JMessage),
-            mailboxSession)
-        CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId))
+        request.toMime4JMessage
+          .fold(e => CreationFailure(clientId, e),
+            message => {
+              val mailboxId: MailboxId = request.mailboxIds.value.headOption.get
+              val appendResult = mailboxManager.getMailbox(mailboxId, mailboxSession)
+                .appendMessage(AppendCommand.builder()
+                  .recent()
+                  .withFlags(request.keywords.map(_.asFlags).getOrElse(new Flags()))
+                  .withInternalDate(Date.from(request.receivedAt.getOrElse(UTCDate(ZonedDateTime.now())).asUTC.toInstant))
+                  .build(message),
+                  mailboxSession)
+              CreationSuccess(clientId, EmailCreationResponse(appendResult.getId.getMessageId))
+            }
+          )
       })
         .subscribeOn(Schedulers.elastic())
         .onErrorResume(e => SMono.just[CreationResult](CreationFailure(clientId, e)))
@@ -405,7 +410,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
     if (newFlags.equals(originalFlags)) {
       SMono.just[UpdateResult](UpdateSuccess(messageId))
     } else {
-    SMono.fromCallable(() =>
+      SMono.fromCallable(() =>
         messageIdManager.setFlags(newFlags, FlagsUpdateMode.REPLACE, messageId, ImmutableList.copyOf(mailboxIds.value.asJavaCollection), session))
         .subscribeOn(Schedulers.elastic())
         .`then`(SMono.just[UpdateResult](UpdateSuccess(messageId)))


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