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/09/08 03:43:06 UTC

[james-project] 12/19: JAMES-3369 Email/get textBody & 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 b2c259a9b20587406b09a2bfaa7bceba56d61ddb
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Sep 7 11:27:27 2020 +0700

    JAMES-3369 Email/get textBody & htmlBody
---
 .../rfc8621/contract/EmailGetMethodContract.scala  | 222 +++++++++++++++++++++
 .../scala/org/apache/james/jmap/mail/Email.scala   |  14 +-
 .../org/apache/james/jmap/mail/EmailBodyPart.scala |  35 ++++
 3 files changed, 267 insertions(+), 4 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/EmailGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
index c076dc8..295ff83 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
@@ -1419,4 +1419,226 @@ trait EmailGetMethodContract {
          |    ]
          |}""".stripMargin)
   }
+
+  @Test
+  def textBodyForSimpleMultipart(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, path, AppendCommand.from(
+        ClassLoader.getSystemResourceAsStream("eml/multipart_simple.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["${messageId.serialize()}"],
+         |      "properties":["textBody"]
+         |    },
+         |    "c1"]]
+         |}""".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).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [[
+         |            "Email/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {
+         |                        "id": "${messageId.serialize()}",
+         |                        "textBody": [
+         |                            {
+         |                                "partId": "2",
+         |                                "blobId": "${messageId.serialize()}_2",
+         |                                "headers": [
+         |                                    {
+         |                                        "name": "Content-Type",
+         |                                        "value": "text/plain; charset=utf-8; format=flowed"
+         |                                    },
+         |                                    {
+         |                                        "name": "Content-Transfer-Encoding",
+         |                                        "value": "7bit"
+         |                                    }
+         |                                ],
+         |                                "size": 97,
+         |                                "type": "text/plain",
+         |                                "charset": "utf-8"
+         |                            }
+         |                        ]
+         |                    }
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ]]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def textBodyForComplexMultipart(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, path, AppendCommand.from(
+        ClassLoader.getSystemResourceAsStream("eml/multipart_complex.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["${messageId.serialize()}"],
+         |      "properties":["textBody"]
+         |    },
+         |    "c1"]]
+         |}""".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).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [[
+         |            "Email/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {
+         |                        "id": "${messageId.serialize()}",
+         |                        "textBody": [
+         |                            {
+         |                                "partId": "3",
+         |                                "blobId": "${messageId.serialize()}_3",
+         |                                "headers": [
+         |                                    {
+         |                                        "name": "Content-Type",
+         |                                        "value": "text/plain; charset=ISO-8859-1; format=flowed"
+         |                                    },
+         |                                    {
+         |                                        "name": "Content-Transfer-Encoding",
+         |                                        "value": "8bit"
+         |                                    }
+         |                                ],
+         |                                "size": 114,
+         |                                "type": "text/plain",
+         |                                "charset": "ISO-8859-1"
+         |                            }
+         |                        ]
+         |                    }
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ]]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def htmlBodyForComplexMultipart(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, path, AppendCommand.from(
+        ClassLoader.getSystemResourceAsStream("eml/multipart_complex.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/get",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "ids": ["${messageId.serialize()}"],
+         |      "properties":["htmlBody"]
+         |    },
+         |    "c1"]]
+         |}""".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).isEqualTo(
+      s"""{
+         |    "sessionState": "75128aab4b1b",
+         |    "methodResponses": [[
+         |            "Email/get",
+         |            {
+         |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |                "state": "000001",
+         |                "list": [
+         |                    {
+         |                        "id": "${messageId.serialize()}",
+         |                        "htmlBody": [
+         |                            {
+         |                                "partId": "4",
+         |                                "blobId": "${messageId.serialize()}_4",
+         |                                "headers": [
+         |                                    {
+         |                                        "name": "Content-Type",
+         |                                        "value": "text/html; charset=ISO-8859-1"
+         |                                    },
+         |                                    {
+         |                                        "name": "Content-Transfer-Encoding",
+         |                                        "value": "7bit"
+         |                                    }
+         |                                ],
+         |                                "size": 108,
+         |                                "type": "text/html",
+         |                                "charset": "ISO-8859-1"
+         |                            }
+         |                        ]
+         |                    }
+         |                ],
+         |                "notFound": []
+         |            },
+         |            "c1"
+         |        ]]
+         |}""".stripMargin)
+  }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index 5f499e9..bf2c2e2 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -29,7 +29,6 @@ import org.apache.james.jmap.mail.Email.Size
 import org.apache.james.jmap.model.Properties
 import org.apache.james.mailbox.model.{MessageId, MessageResult}
 import org.apache.james.mime4j.codec.DecodeMonitor
-import org.apache.james.mime4j.dom.Message
 import org.apache.james.mime4j.message.DefaultMessageBuilder
 import org.apache.james.mime4j.stream.MimeConfig
 import org.slf4j.{Logger, LoggerFactory}
@@ -43,7 +42,7 @@ object Email {
   type UnparsedEmailId = String Refined UnparsedEmailIdConstraint
 
   val defaultProperties: Properties = Properties("id", "size")
-  val allowedProperties: Properties = Properties("id", "size", "bodyStructure")
+  val allowedProperties: Properties = Properties("id", "size", "bodyStructure", "textBody", "htmlBody")
   val idProperty: Properties = Properties("id")
 
   def asUnparsed(messageId: MessageId): Try[UnparsedEmailId] =
@@ -79,11 +78,18 @@ object Email {
       mime4JMessage <- Try(defaultMessageBuilder.parseMessage(firstMessage.getFullContent.getInputStream))
       bodyStructure <- EmailBodyPart.of(messageId, mime4JMessage)
     } yield {
-      Email(messageId, sanitizeSize(firstMessage.getSize), bodyStructure)
+      Email(
+        id = messageId,
+        size = sanitizeSize(firstMessage.getSize),
+        bodyStructure = bodyStructure,
+        textBody = bodyStructure.textBody,
+        htmlBody = bodyStructure.htmlBody)
     }
   }
 }
 
 case class Email(id: MessageId,
                  size: Size,
-                 bodyStructure: EmailBodyPart)
+                 bodyStructure: EmailBodyPart,
+                 textBody: List[EmailBodyPart],
+                 htmlBody: List[EmailBodyPart])
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
index 0606275..608a045 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
@@ -28,6 +28,7 @@ import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.NonNegative
 import eu.timepit.refined.refineV
 import org.apache.james.jmap.mail.Email.Size
+import org.apache.james.jmap.mail.EmailBodyPart.{MULTIPART_ALTERNATIVE, TEXT_HTML, TEXT_PLAIN}
 import org.apache.james.jmap.mail.PartId.PartIdValue
 import org.apache.james.mailbox.model.{Cid, MessageId}
 import org.apache.james.mime4j.dom.{Entity, Message, Multipart}
@@ -51,6 +52,10 @@ case class PartId(value: PartIdValue) {
 }
 
 object EmailBodyPart {
+  val TEXT_PLAIN: String = "text/plain"
+  val TEXT_HTML: String = "text/html"
+  val MULTIPART_ALTERNATIVE: String = "multipart/alternative"
+
   def of(messageId: MessageId, message: Message): Try[EmailBodyPart] =
     of(messageId, PartId(1), message).map(_._1)
 
@@ -154,4 +159,34 @@ case class EmailBodyPart(partId: PartId,
                          location: Option[Location],
                          subParts: Option[List[EmailBodyPart]]) {
 
+  def textBody: List[EmailBodyPart] = selfBody ++ textBodyOfMultipart
+
+  def htmlBody: List[EmailBodyPart] = selfBody ++ htmlBodyOfMultipart
+
+  private def selfBody: List[EmailBodyPart] = if (shouldBeDisplayedAsBody) {
+    List(this)
+  } else {
+    Nil
+  }
+
+  private val hasTextMediaType: Boolean = `type`.equals(TEXT_PLAIN) || `type`.equals(TEXT_HTML)
+  private val shouldBeDisplayedAsBody: Boolean = hasTextMediaType && disposition.isEmpty && cid.isEmpty
+
+  private def textBodyOfMultipart: List[EmailBodyPart] = `type` match {
+    case MULTIPART_ALTERNATIVE => textPlainSubparts
+    case _ => subParts.getOrElse(Nil)
+      .flatMap(subPart => subPart.textBody)
+  }
+
+  private def htmlBodyOfMultipart: List[EmailBodyPart] = `type` match {
+    case MULTIPART_ALTERNATIVE => textHtmlSubparts
+    case _ => subParts.getOrElse(Nil)
+      .flatMap(subPart => subPart.htmlBody)
+  }
+
+  private def textPlainSubparts: List[EmailBodyPart] = subParts.getOrElse(Nil)
+    .filter(subPart => subPart.`type`.equals(TEXT_PLAIN))
+
+  private def textHtmlSubparts: List[EmailBodyPart] = subParts.getOrElse(Nil)
+    .filter(subPart => subPart.`type`.equals(TEXT_HTML))
 }


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