You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/11/12 04:42:19 UTC

[james-project] 02/12: JAMES-3439 Wrap inline attachments in multipart/related

This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit c1a920dcd83ce0638b9c26ef1a46a49f347ab8ce
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 11:39:36 2020 +0700

    JAMES-3439 Wrap inline attachments in multipart/related
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 205 +++++++++++++++++++--
 .../org/apache/james/jmap/mail/EmailSet.scala      |  41 +++--
 2 files changed, 216 insertions(+), 30 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 78b89ec..248680f 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
@@ -1818,7 +1818,7 @@ trait EmailSetMethodContract {
            |  "subject": "World domination",
            |  "attachments": [
            |    {
-           |      "partId": "3",
+           |      "partId": "4",
            |      "blobId": "$blobIdToDownload",
            |      "size": 11,
            |      "type": "text/plain",
@@ -1951,8 +1951,8 @@ trait EmailSetMethodContract {
            |  "subject": "World domination",
            |  "attachments": [
            |    {
-           |      "partId": "3",
-           |      "blobId": "${messageId}_3",
+           |      "partId": "4",
+           |      "blobId": "${messageId}_4",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -1961,15 +1961,15 @@ trait EmailSetMethodContract {
            |  ],
            |  "htmlBody": [
            |    {
-           |      "partId": "2",
-           |      "blobId": "${messageId}_2",
+           |      "partId": "3",
+           |      "blobId": "${messageId}_3",
            |      "size": 166,
            |      "type": "text/html",
            |      "charset": "UTF-8"
            |    }
            |  ],
            |  "bodyValues": {
-           |    "2": {
+           |    "3": {
            |      "value": "$htmlBody",
            |      "isEncodingProblem": false,
            |      "isTruncated": false
@@ -2065,7 +2065,7 @@ trait EmailSetMethodContract {
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
     .when
-      .post.prettyPeek()
+      .post
     .`then`
       .statusCode(SC_OK)
       .contentType(JSON)
@@ -2097,8 +2097,8 @@ trait EmailSetMethodContract {
            |  "subject": "World domination",
            |  "attachments": [
            |    {
-           |      "partId": "3",
-           |      "blobId": "${messageId}_3",
+           |      "partId": "4",
+           |      "blobId": "${messageId}_4",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -2106,8 +2106,8 @@ trait EmailSetMethodContract {
            |      "cid": "abc"
            |    },
            |    {
-           |      "partId": "4",
-           |      "blobId": "${messageId}_4",
+           |      "partId": "5",
+           |      "blobId": "${messageId}_5",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -2115,8 +2115,8 @@ trait EmailSetMethodContract {
            |      "cid": "def"
            |    },
            |    {
-           |      "partId": "5",
-           |      "blobId": "${messageId}_5",
+           |      "partId": "6",
+           |      "blobId": "${messageId}_6",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -2125,15 +2125,15 @@ trait EmailSetMethodContract {
            |  ],
            |  "htmlBody": [
            |    {
-           |      "partId": "2",
-           |      "blobId": "${messageId}_2",
+           |      "partId": "3",
+           |      "blobId": "${messageId}_3",
            |      "size": 166,
            |      "type": "text/html",
            |      "charset": "UTF-8"
            |    }
            |  ],
            |  "bodyValues": {
-           |    "2": {
+           |    "3": {
            |      "value": "$htmlBody",
            |      "isEncodingProblem": false,
            |      "isTruncated": false
@@ -2143,6 +2143,177 @@ trait EmailSetMethodContract {
   }
 
   @Test
+  def inlinedAttachmentsShouldBeWrappedInRelatedMultipart(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val payload = "123456789\r\n".getBytes(StandardCharsets.UTF_8)
+    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 uploadResponse: String = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .contentType("text/plain")
+      .body(payload)
+    .when
+      .post(s"/upload/$ACCOUNT_ID/")
+    .`then`
+      .statusCode(SC_CREATED)
+      .extract
+      .body
+      .asString
+
+    val blobId: String = Json.parse(uploadResponse).\("blobId").get.asInstanceOf[JsString].value
+
+    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",
+         |          "attachments": [
+         |            {
+         |              "blobId": "$blobId",
+         |              "type":"text/plain",
+         |              "charset":"UTF-8",
+         |              "disposition": "inline",
+         |              "cid": "abc"
+         |            },
+         |            {
+         |              "blobId": "$blobId",
+         |              "type":"text/plain",
+         |              "charset":"UTF-8",
+         |              "disposition": "inline",
+         |              "cid": "def"
+         |            },
+         |            {
+         |              "blobId": "$blobId",
+         |              "type":"text/plain",
+         |              "charset":"UTF-8",
+         |              "disposition": "attachment"
+         |            }
+         |          ],
+         |          "htmlBody": [
+         |            {
+         |              "partId": "a49d",
+         |              "type": "text/html"
+         |            }
+         |          ],
+         |          "bodyValues": {
+         |            "a49d": {
+         |              "value": "$htmlBody",
+         |              "isTruncated": false,
+         |              "isEncodingProblem": false
+         |            }
+         |          }
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |      {
+         |        "accountId": "$ACCOUNT_ID",
+         |        "ids": ["#aaaaaa"],
+         |        "properties": ["bodyStructure"],
+         |        "bodyProperties": ["type", "disposition", "cid", "subParts"]
+         |      },
+         |    "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)
+
+    val messageId = Json.parse(response)
+      .\("methodResponses")
+      .\(1).\(1)
+      .\("list")
+      .\(0)
+      .\("id")
+      .get.asInstanceOf[JsString].value
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "75128aab4b1b",
+           |    "methodResponses": [
+           |        [
+           |            "Email/set",
+           |            {
+           |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |                "newState": "000001",
+           |                "created": {
+           |                    "aaaaaa": {
+           |                        "id": "$messageId"
+           |                    }
+           |                }
+           |            },
+           |            "c1"
+           |        ],
+           |        [
+           |            "Email/get",
+           |            {
+           |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |                "state": "000001",
+           |                "list": [
+           |                    {
+           |                        "id": "$messageId",
+           |                        "bodyStructure": {
+           |                            "type": "multipart/mixed",
+           |                            "subParts": [
+           |                                {
+           |                                    "type": "multipart/related",
+           |                                    "subParts": [
+           |                                        {
+           |                                            "type": "text/html"
+           |                                        },
+           |                                        {
+           |                                            "type": "text/plain",
+           |                                            "disposition": "inline",
+           |                                            "cid": "abc"
+           |                                        },
+           |                                        {
+           |                                            "type": "text/plain",
+           |                                            "disposition": "inline",
+           |                                            "cid": "def"
+           |                                        }
+           |                                    ]
+           |                                },
+           |                                {
+           |                                    "type": "text/plain",
+           |                                    "disposition": "attachment"
+           |                                }
+           |                            ]
+           |                        }
+           |                    }
+           |                ],
+           |                "notFound": []
+           |            },
+           |            "c2"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
   def createShouldSupportAttachmentWithName(server: GuiceJamesServer): Unit = {
     val bobPath = MailboxPath.inbox(BOB)
     val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
@@ -2239,7 +2410,7 @@ trait EmailSetMethodContract {
            |  "attachments": [
            |    {
            |      "name": "myAttachment",
-           |      "partId": "3",
+           |      "partId": "4",
            |      "blobId": "$blobIdToDownload",
            |      "size": 11,
            |      "type": "text/plain",
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 6533819..c607f8b 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
@@ -65,6 +65,7 @@ object EmailSet {
 object SubType {
   val HTML_SUBTYPE = "html"
   val MIXED_SUBTYPE = "mixed"
+  val RELATED_SUBTYPE = "related"
 }
 
 case class ClientPartId(id: Id)
@@ -151,8 +152,6 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
                                              attachmentManager: AttachmentManager,
                                              attachmentContentLoader: AttachmentContentLoader,
                                              mailboxSession: MailboxSession): Either[Exception, MultipartBuilder] = {
-    val multipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
-
     val maybeAttachments: Either[Exception, List[(Attachment, AttachmentMetadata, Array[Byte])]] =
       attachments
         .map(attachment => getAttachmentMetadata(attachment, attachmentManager, mailboxSession))
@@ -160,24 +159,40 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
           .flatMap(attachmentAndMetadata => loadAttachment(attachmentAndMetadata._1, attachmentAndMetadata._2, attachmentContentLoader, mailboxSession)))
         .sequence
 
-    multipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
     maybeAttachments.map(list => {
-      list.foldLeft(multipartBuilder) {
+      val inlineAttachments = list.filter(_._1.isInline)
+      val normalAttachments = list.filter(!_._1.isInline)
+
+      val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
+      val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
+      relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+      inlineAttachments.foldLeft(relatedMultipartBuilder) {
         case (acc, (attachment, storedMetadata, content)) =>
-          val bodypartBuilder = BodyPartBuilder.create()
-          bodypartBuilder.setBody(content, attachment.`type`.value)
-            .setField(contentTypeField(attachment, storedMetadata))
-            .setContentDisposition(attachment.disposition.getOrElse(Disposition.ATTACHMENT).value)
-          attachment.cid.map(_.asField).foreach(bodypartBuilder.addField)
-          attachment.location.map(_.asField).foreach(bodypartBuilder.addField)
-          attachment.language.map(_.asField).foreach(bodypartBuilder.addField)
-
-          acc.addBodyPart(bodypartBuilder)
+          acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+          acc
+      }
+
+      mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(relatedMultipartBuilder.build))
+
+      normalAttachments.foldLeft(mixedMultipartBuilder) {
+        case (acc, (attachment, storedMetadata, content)) =>
+          acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
           acc
       }
     })
   }
 
+  private def toBodypartBuilder(attachment: Attachment, storedMetadata: AttachmentMetadata, content: Array[Byte]) = {
+    val bodypartBuilder = BodyPartBuilder.create()
+    bodypartBuilder.setBody(content, attachment.`type`.value)
+      .setField(contentTypeField(attachment, storedMetadata))
+      .setContentDisposition(attachment.disposition.getOrElse(Disposition.ATTACHMENT).value)
+    attachment.cid.map(_.asField).foreach(bodypartBuilder.addField)
+    attachment.location.map(_.asField).foreach(bodypartBuilder.addField)
+    attachment.language.map(_.asField).foreach(bodypartBuilder.addField)
+    bodypartBuilder
+  }
+
   private def contentTypeField(attachment: Attachment, attachmentMetadata: AttachmentMetadata): ContentTypeField = {
     val typeAsField: ContentTypeField = attachmentMetadata.getType.asMime4J
     if (attachment.name.isDefined) {


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