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:17 UTC

[james-project] branch master updated (74b7682 -> a44eac6)

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

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


    from 74b7682  JAMES-3359 Add tests to demystify Mailbox/set update parentId data races
     new 0faf47a  JAMES-3439 Include inline attachments
     new c1a920d  JAMES-3439 Wrap inline attachments in multipart/related
     new 4ebd13d  JAMES-3439 Email/set create: Simplify mime structure when possible
     new 71e12dd  JAMES-3439 Various refactorings regarding Email/set create attachments (mixed)
     new 900bc9c  JAMES-3442 Email/set create position multipart/alternative for text/html
     new d2f466c  JAMES-3171 Enhance MailboxGetMethod reactive management
     new 7fa59de  JAMES-3171 Limit concurrency in JMAP methods
     new c9305e3  JAMES-3378 Email/query keywords should be case incentive
     new 87b3678  JAMES-3373 Avoid using hamcrest
     new 433162e  JAMES-3413 Email/set updateValidation message should pass on distributed james
     new 6736651  JAMES-3432 UploadRoutes error handling and schedulers
     new a44eac6  JAMES-3377 Split case sensitivity test case for subject

The 12 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../elasticsearch/MailboxMappingFactory.java       |    1 +
 .../search/AbstractMessageSearchIndexTest.java     |    8 +
 .../distributed/DistributedEmailSetMethodTest.java |    5 +
 .../jmap/rfc8621/contract/DownloadContract.scala   |   15 +-
 .../rfc8621/contract/EmailGetMethodContract.scala  |    4 +-
 .../contract/EmailQueryMethodContract.scala        |   61 +-
 .../rfc8621/contract/EmailSetMethodContract.scala  | 1355 +++++++++++++++++---
 .../rfc8621/memory/MemoryEmailSetMethodTest.java   |    5 +
 .../org/apache/james/jmap/mail/EmailSet.scala      |  122 +-
 .../jmap/method/EmailSetCreatePerformer.scala      |    4 +-
 .../jmap/method/EmailSetUpdatePerformer.scala      |    2 +-
 .../james/jmap/method/MailboxGetMethod.scala       |   30 +-
 .../jmap/method/MailboxSetDeletePerformer.scala    |    3 +-
 .../jmap/method/MailboxSetUpdatePerformer.scala    |    7 +-
 .../apache/james/jmap/routes/UploadRoutes.scala    |   33 +-
 15 files changed, 1430 insertions(+), 225 deletions(-)


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


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

Posted by rc...@apache.org.
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


[james-project] 08/12: JAMES-3378 Email/query keywords should be case incentive

Posted by rc...@apache.org.
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 c9305e37b26079e765ec61ad1b5d3651d3c7174b
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 11:36:25 2020 +0700

    JAMES-3378 Email/query keywords should be case incentive
---
 .../org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java    | 1 +
 1 file changed, 1 insertion(+)

diff --git a/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java b/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java
index aeb6782..944c4a7 100644
--- a/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java
+++ b/mailbox/elasticsearch/src/main/java/org/apache/james/mailbox/elasticsearch/MailboxMappingFactory.java
@@ -147,6 +147,7 @@ public class MailboxMappingFactory {
 
                         .startObject(USER_FLAGS)
                             .field(TYPE, KEYWORD)
+                            .field(NORMALIZER, CASE_INSENSITIVE)
                         .endObject()
 
                         .startObject(MEDIA_TYPE)


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


[james-project] 01/12: JAMES-3439 Include inline attachments

Posted by rc...@apache.org.
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 0faf47a3cd0f8b25ce680795f13cb1aec85a7839
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 11:14:06 2020 +0700

    JAMES-3439 Include inline attachments
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 164 +++++++++++++++++++++
 .../org/apache/james/jmap/mail/EmailSet.scala      |   4 +-
 2 files changed, 166 insertions(+), 2 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 32322e7..78b89ec 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
@@ -1979,6 +1979,170 @@ trait EmailSetMethodContract {
   }
 
   @Test
+  def createShouldSupportInlinedAttachmentsMixedWithRegularAttachmentsAndHtmlBody(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": ["mailboxIds", "subject", "attachments", "htmlBody", "bodyValues"],
+         |        "fetchHTMLBodyValues": true
+         |      },
+         |    "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)
+
+    val messageId = Json.parse(response)
+      .\("methodResponses")
+      .\(1).\(1)
+      .\("list")
+      .\(0)
+      .\("id")
+      .get.asInstanceOf[JsString].value
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[1][1].list[0].id")
+      .inPath(s"methodResponses[1][1].list")
+      .isEqualTo(
+        s"""[{
+           |  "mailboxIds": {
+           |    "${mailboxId.serialize}": true
+           |  },
+           |  "subject": "World domination",
+           |  "attachments": [
+           |    {
+           |      "partId": "3",
+           |      "blobId": "${messageId}_3",
+           |      "size": 11,
+           |      "type": "text/plain",
+           |      "charset": "UTF-8",
+           |      "disposition": "inline",
+           |      "cid": "abc"
+           |    },
+           |    {
+           |      "partId": "4",
+           |      "blobId": "${messageId}_4",
+           |      "size": 11,
+           |      "type": "text/plain",
+           |      "charset": "UTF-8",
+           |      "disposition": "inline",
+           |      "cid": "def"
+           |    },
+           |    {
+           |      "partId": "5",
+           |      "blobId": "${messageId}_5",
+           |      "size": 11,
+           |      "type": "text/plain",
+           |      "charset": "UTF-8",
+           |      "disposition": "attachment"
+           |    }
+           |  ],
+           |  "htmlBody": [
+           |    {
+           |      "partId": "2",
+           |      "blobId": "${messageId}_2",
+           |      "size": 166,
+           |      "type": "text/html",
+           |      "charset": "UTF-8"
+           |    }
+           |  ],
+           |  "bodyValues": {
+           |    "2": {
+           |      "value": "$htmlBody",
+           |      "isEncodingProblem": false,
+           |      "isTruncated": false
+           |    }
+           |  }
+           |}]""".stripMargin)
+  }
+
+  @Test
   def createShouldSupportAttachmentWithName(server: GuiceJamesServer): Unit = {
     val bobPath = MailboxPath.inbox(BOB)
     val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
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 e156674..6533819 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
@@ -29,6 +29,7 @@ import eu.timepit.refined.collection.NonEmpty
 import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.State.State
 import org.apache.james.jmap.core.{AccountId, SetError, UTCDate}
+import org.apache.james.jmap.mail.Disposition.INLINE
 import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId}
 import org.apache.james.jmap.method.WithAccountId
 import org.apache.james.mailbox.exception.AttachmentNotFoundException
@@ -96,7 +97,7 @@ case class Attachment(blobId: BlobId,
                       location: Option[Location],
                       cid: Option[ClientCid]) {
 
-  def isInline: Boolean = disposition.contains("inline")
+  def isInline: Boolean = disposition.contains(INLINE)
 }
 
 case class EmailCreationRequest(mailboxIds: MailboxIds,
@@ -154,7 +155,6 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
 
     val maybeAttachments: Either[Exception, List[(Attachment, AttachmentMetadata, Array[Byte])]] =
       attachments
-        .filter(!_.isInline)
         .map(attachment => getAttachmentMetadata(attachment, attachmentManager, mailboxSession))
         .map(attachmentMetadataList => attachmentMetadataList
           .flatMap(attachmentAndMetadata => loadAttachment(attachmentAndMetadata._1, attachmentAndMetadata._2, attachmentContentLoader, mailboxSession)))


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


[james-project] 03/12: JAMES-3439 Email/set create: Simplify mime structure when possible

Posted by rc...@apache.org.
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 4ebd13dba7ad1773f76f06275f529cdf179f4756
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 12:02:22 2020 +0700

    JAMES-3439 Email/set create: Simplify mime structure when possible
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 446 ++++++++++++++++++++-
 .../org/apache/james/jmap/mail/EmailSet.scala      |  63 ++-
 2 files changed, 480 insertions(+), 29 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 248680f..bdf41cc 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": "4",
+           |      "partId": "3",
            |      "blobId": "$blobIdToDownload",
            |      "size": 11,
            |      "type": "text/plain",
@@ -1951,8 +1951,8 @@ trait EmailSetMethodContract {
            |  "subject": "World domination",
            |  "attachments": [
            |    {
-           |      "partId": "4",
-           |      "blobId": "${messageId}_4",
+           |      "partId": "3",
+           |      "blobId": "${messageId}_3",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -1961,15 +1961,15 @@ trait EmailSetMethodContract {
            |  ],
            |  "htmlBody": [
            |    {
-           |      "partId": "3",
-           |      "blobId": "${messageId}_3",
+           |      "partId": "2",
+           |      "blobId": "${messageId}_2",
            |      "size": 166,
            |      "type": "text/html",
            |      "charset": "UTF-8"
            |    }
            |  ],
            |  "bodyValues": {
-           |    "3": {
+           |    "2": {
            |      "value": "$htmlBody",
            |      "isEncodingProblem": false,
            |      "isTruncated": false
@@ -2154,9 +2154,9 @@ trait EmailSetMethodContract {
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .contentType("text/plain")
       .body(payload)
-    .when
+      .when
       .post(s"/upload/$ACCOUNT_ID/")
-    .`then`
+      .`then`
       .statusCode(SC_CREATED)
       .extract
       .body
@@ -2228,9 +2228,9 @@ trait EmailSetMethodContract {
     val response = `given`
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
-    .when
+      .when
       .post
-    .`then`
+      .`then`
       .statusCode(SC_OK)
       .contentType(JSON)
       .extract
@@ -2314,6 +2314,430 @@ trait EmailSetMethodContract {
   }
 
   @Test
+  def htmlBodyPartWithOnlyNormalAttachmentsShouldNotBeWrappedInARelatedMultipart(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": "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": "text/html"
+           |                                },
+           |                                {
+           |                                    "type": "text/plain",
+           |                                    "disposition": "attachment"
+           |                                }
+           |                            ]
+           |                        }
+           |                    }
+           |                ],
+           |                "notFound": []
+           |            },
+           |            "c2"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def inlinedAttachmentsOnlyShouldNotBeWrappedInAMixedMultipart(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"
+         |            }
+         |          ],
+         |          "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/related",
+           |                                 "subParts": [
+           |                                    {
+           |                                        "type": "text/html"
+           |                                    },
+           |                                    {
+           |                                        "type": "text/plain",
+           |                                        "disposition": "inline",
+           |                                        "cid": "abc"
+           |                                    },
+           |                                    {
+           |                                        "type": "text/plain",
+           |                                        "disposition": "inline",
+           |                                        "cid": "def"
+           |                                    }
+           |                                ]
+           |                        }
+           |                    }
+           |                ],
+           |                "notFound": []
+           |            },
+           |            "c2"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def htmlBodyOnlyShouldNotBeWrappedInMultiparts(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": [],
+         |          "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": "text/html"
+           |                        }
+           |                    }
+           |                ],
+           |                "notFound": []
+           |            },
+           |            "c2"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
   def createShouldSupportAttachmentWithName(server: GuiceJamesServer): Unit = {
     val bobPath = MailboxPath.inbox(BOB)
     val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
@@ -2410,7 +2834,7 @@ trait EmailSetMethodContract {
            |  "attachments": [
            |    {
            |      "name": "myAttachment",
-           |      "partId": "4",
+           |      "partId": "3",
            |      "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 c607f8b..a1a72a8 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
@@ -137,7 +137,7 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
         validateSpecificHeaders(builder)
           .flatMap(_ => {
             specificHeaders.map(_.asField).foreach(builder.addField)
-            attachments.map(attachments =>
+            attachments.filter(_.nonEmpty).map(attachments =>
               createMultipartWithAttachments(maybeHtmlBody, attachments, attachmentManager, attachmentContentLoader, mailboxSession)
                 .map(multipartBuilder => {
                   builder.setBody(multipartBuilder)
@@ -160,28 +160,55 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
         .sequence
 
     maybeAttachments.map(list => {
-      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)) =>
-          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
+      (list.filter(_._1.isInline), list.filter(!_._1.isInline)) match {
+        case (Nil, normalAttachments) => createMixedBody(maybeHtmlBody, normalAttachments)
+        case (inlineAttachments, Nil) => createRelatedBody(maybeHtmlBody, inlineAttachments)
+        case (inlineAttachments, normalAttachments) => createMixedRelatedBody(maybeHtmlBody, inlineAttachments, normalAttachments)
       }
     })
   }
 
+  private def createMixedRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+    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)) =>
+        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 createMixedBody(maybeHtmlBody: Option[String], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+    val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
+    mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+    normalAttachments.foldLeft(mixedMultipartBuilder) {
+      case (acc, (attachment, storedMetadata, content)) =>
+        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+        acc
+    }
+  }
+
+  private def createRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+    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)) =>
+        acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
+        acc
+    }
+    relatedMultipartBuilder
+  }
+
   private def toBodypartBuilder(attachment: Attachment, storedMetadata: AttachmentMetadata, content: Array[Byte]) = {
     val bodypartBuilder = BodyPartBuilder.create()
     bodypartBuilder.setBody(content, attachment.`type`.value)


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


[james-project] 11/12: JAMES-3432 UploadRoutes error handling and schedulers

Posted by rc...@apache.org.
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 6736651799a5dd6350770753cd853057c9c2ed8c
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 11:58:27 2020 +0700

    JAMES-3432 UploadRoutes error handling and schedulers
    
    Required to pass on Distributed James
---
 .../apache/james/jmap/routes/UploadRoutes.scala    | 33 ++++++++++------------
 1 file changed, 15 insertions(+), 18 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
index 9bb9f78..2f084c6 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/UploadRoutes.scala
@@ -19,7 +19,7 @@
 
 package org.apache.james.jmap.routes
 
-import java.io.{IOException, InputStream, UncheckedIOException}
+import java.io.InputStream
 import java.util.stream
 import java.util.stream.Stream
 
@@ -51,6 +51,8 @@ import reactor.core.scala.publisher.SMono
 import reactor.core.scheduler.Schedulers
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
 
+case class TooBigUploadException() extends RuntimeException
+
 object UploadRoutes {
   val LOGGER: Logger = LoggerFactory.getLogger(classOf[DownloadRoutes])
 
@@ -96,23 +98,23 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
   def post(request: HttpServerRequest, response: HttpServerResponse): Mono[Void] = {
     request.requestHeaders.get(CONTENT_TYPE) match {
       case contentType => SMono.fromPublisher(
-          authenticator.authenticate(request))
-          .flatMap(session => post(request, response, ContentType.of(contentType), session))
-          .onErrorResume {
-            case e: UnauthorizedException => SMono.fromPublisher(handleAuthenticationFailure(response, LOGGER, e))
-            case e: Throwable => SMono.fromPublisher(handleInternalError(response, LOGGER, e))
-          }
-          .asJava().`then`()
+        authenticator.authenticate(request))
+        .flatMap(session => post(request, response, ContentType.of(contentType), session))
+        .onErrorResume {
+          case e: TooBigUploadException => SMono.fromPublisher(response.status(BAD_REQUEST).sendString(SMono.just("Attempt to upload exceed max size")).`then`())
+          case e: UnauthorizedException => SMono.fromPublisher(handleAuthenticationFailure(response, LOGGER, e))
+          case e: Throwable => SMono.fromPublisher(handleInternalError(response, LOGGER, e))
+        }
+        .asJava()
+        .subscribeOn(Schedulers.elastic())
+        .`then`()
       case _ => response.status(BAD_REQUEST).send
     }
   }
 
-  private def handleUncheckedIOException(response: HttpServerResponse, e: UncheckedIOException) =
-    response.status(BAD_REQUEST).sendString(SMono.just(e.getMessage))
-
   def post(request: HttpServerRequest, response: HttpServerResponse, contentType: ContentType, session: MailboxSession): SMono[Void] = {
     Id.validate(request.param(accountIdParam)) match {
-      case Right(id: Id) => {
+      case Right(id: Id) =>
         val targetAccountId: AccountId = AccountId(id)
         AccountId.from(session.getUser).map(accountId => accountId.equals(targetAccountId))
           .fold[SMono[Void]](
@@ -120,11 +122,9 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
             value => if (value) {
               SMono.fromCallable(() => ReactorUtils.toInputStream(request.receive.asByteBuffer))
               .flatMap(content => handle(targetAccountId, contentType, content, session, response))
-              .subscribeOn(Schedulers.elastic())
             } else {
               SMono.raiseError(new UnauthorizedException("Attempt to upload in another account"))
             })
-      }
 
       case Left(throwable: Throwable) => SMono.raiseError(throwable)
     }
@@ -135,16 +135,13 @@ class UploadRoutes @Inject()(@Named(InjectionKeys.RFC_8621) val authenticator: A
 
     SMono.fromCallable(() => new LimitedInputStream(content, maxSize) {
       override def raiseError(max: Long, count: Long): Unit = if (count > max) {
-        throw new IOException("Attempt to upload exceed max size")
+        throw TooBigUploadException()
       }})
       .flatMap(uploadContent(accountId, contentType, _, mailboxSession))
       .flatMap(uploadResponse => SMono.fromPublisher(response
               .header(CONTENT_TYPE, uploadResponse.`type`.asString())
               .status(CREATED)
               .sendString(SMono.just(serializer.serialize(uploadResponse).toString()))))
-      .onErrorResume {
-        case e: UncheckedIOException => SMono.fromPublisher(handleUncheckedIOException(response, e))
-      }
   }
 
 


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


[james-project] 04/12: JAMES-3439 Various refactorings regarding Email/set create attachments (mixed)

Posted by rc...@apache.org.
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 71e12dd04df9c3342a429ecc71921aa13e424b1c
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Tue Nov 10 17:35:20 2020 +0700

    JAMES-3439 Various refactorings regarding Email/set create attachments (mixed)
---
 .../james/jmap/rfc8621/contract/EmailSetMethodContract.scala      | 8 ++++----
 .../src/main/scala/org/apache/james/jmap/mail/EmailSet.scala      | 6 +++---
 2 files changed, 7 insertions(+), 7 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 bdf41cc..378fbbb 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
@@ -2154,9 +2154,9 @@ trait EmailSetMethodContract {
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .contentType("text/plain")
       .body(payload)
-      .when
+    .when
       .post(s"/upload/$ACCOUNT_ID/")
-      .`then`
+    .`then`
       .statusCode(SC_CREATED)
       .extract
       .body
@@ -2228,9 +2228,9 @@ trait EmailSetMethodContract {
     val response = `given`
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
-      .when
+    .when
       .post
-      .`then`
+    .`then`
       .statusCode(SC_OK)
       .contentType(JSON)
       .extract
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 a1a72a8..464be6d 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
@@ -33,7 +33,7 @@ import org.apache.james.jmap.mail.Disposition.INLINE
 import org.apache.james.jmap.mail.EmailSet.{EmailCreationId, UnparsedMessageId}
 import org.apache.james.jmap.method.WithAccountId
 import org.apache.james.mailbox.exception.AttachmentNotFoundException
-import org.apache.james.mailbox.model.{AttachmentId, AttachmentMetadata, MessageId, Cid => MailboxCid}
+import org.apache.james.mailbox.model.{AttachmentId, AttachmentMetadata, Cid, MessageId}
 import org.apache.james.mailbox.{AttachmentContentLoader, AttachmentManager, MailboxSession}
 import org.apache.james.mime4j.codec.EncoderUtil
 import org.apache.james.mime4j.codec.EncoderUtil.Usage
@@ -77,10 +77,10 @@ case class ClientEmailBodyValue(value: String,
                                 isTruncated: Option[IsTruncated])
 
 object ClientCid {
-  def of(entity: Entity): Option[MailboxCid] =
+  def of(entity: Entity): Option[Cid] =
     Option(entity.getHeader.getField(FieldName.CONTENT_ID))
       .flatMap {
-        case contentIdField: ContentIdField => MailboxCid.parser().relaxed().unwrap().parse(contentIdField.getId).toScala
+        case contentIdField: ContentIdField => Cid.parser().relaxed().unwrap().parse(contentIdField.getId).toScala
         case _ => None
       }
 }


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


[james-project] 07/12: JAMES-3171 Limit concurrency in JMAP methods

Posted by rc...@apache.org.
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 7fa59ded0afb5621195683de91bdccbda1cdd971
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 14:13:53 2020 +0700

    JAMES-3171 Limit concurrency in JMAP methods
---
 .../main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala | 3 ++-
 .../org/apache/james/jmap/method/MailboxSetDeletePerformer.scala   | 3 ++-
 .../org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala   | 7 +++----
 3 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
index c13bf3a..a7adf77 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
@@ -108,7 +108,8 @@ class MailboxGetMethod @Inject() (serializer: MailboxSerializer,
       case Some(ids) => SFlux.fromIterable(ids.value)
         .flatMap(id => Try(mailboxIdFactory.fromString(id.value))
           .fold(e => SMono.just(MailboxGetResults.notFound(id)),
-            mailboxId => getMailboxResultById(capabilities, mailboxId, mailboxSession)))
+            mailboxId => getMailboxResultById(capabilities, mailboxId, mailboxSession)),
+          maxConcurrency = 5)
     }
 
   private def getMailboxResultById(capabilities: Set[CapabilityIdentifier],
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
index 3ccd8f4..de5d4ea 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetDeletePerformer.scala
@@ -67,7 +67,8 @@ class MailboxSetDeletePerformer @Inject()(mailboxManager: MailboxManager,
   def deleteMailboxes(mailboxSession: MailboxSession, mailboxSetRequest: MailboxSetRequest): SMono[MailboxDeletionResults] = {
     SFlux.fromIterable(mailboxSetRequest.destroy.getOrElse(Seq()))
       .flatMap(id => delete(mailboxSession, id, mailboxSetRequest.onDestroyRemoveEmails.getOrElse(RemoveEmailsOnDestroy(false)))
-        .onErrorRecover(e => MailboxDeletionFailure(id, e)))
+        .onErrorRecover(e => MailboxDeletionFailure(id, e)),
+        maxConcurrency = 5)
       .collectSeq()
       .map(MailboxDeletionResults)
   }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
index f208989..c82e5c3 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxSetUpdatePerformer.scala
@@ -92,7 +92,7 @@ class MailboxSetUpdatePerformer @Inject()(serializer: MailboxSerializer,
               e => SMono.just(MailboxUpdateFailure(unparsedMailboxId, e, None)),
               mailboxId => updateMailbox(mailboxSession, mailboxId, unparsedMailboxId, patch, capabilities))
             .onErrorResume(e => SMono.just(MailboxUpdateFailure(unparsedMailboxId, e, None)))
-      })
+      }, maxConcurrency = 5)
       .collectSeq()
       .map(MailboxUpdateResults)
   }
@@ -216,9 +216,8 @@ class MailboxSetUpdatePerformer @Inject()(serializer: MailboxSerializer,
     }).getOrElse(SMono.empty)
 
     val partialUpdatesOperation: SMono[Unit] = SFlux.fromIterable(validatedPatch.rightsPartialUpdates)
-      .flatMap(partialUpdate => SMono.fromCallable(() => {
-        mailboxManager.applyRightsCommand(mailboxId, partialUpdate.asACLCommand(), mailboxSession)
-      }))
+      .flatMap(partialUpdate => SMono.fromCallable(() => mailboxManager.applyRightsCommand(mailboxId, partialUpdate.asACLCommand(), mailboxSession)),
+        maxConcurrency = 5)
       .`then`()
 
     SFlux.merge(Seq(resetOperation, partialUpdatesOperation))


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


[james-project] 12/12: JAMES-3377 Split case sensitivity test case for subject

Posted by rc...@apache.org.
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 a44eac63c2aa3939c0ce6c47997451eb3b81842a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 12:03:42 2020 +0700

    JAMES-3377 Split case sensitivity test case for subject
---
 .../search/AbstractMessageSearchIndexTest.java     |  8 +++
 .../contract/EmailQueryMethodContract.scala        | 61 +++++++++++++++++++++-
 2 files changed, 68 insertions(+), 1 deletion(-)

diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
index 5e3d2e9..b0500ef 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/search/AbstractMessageSearchIndexTest.java
@@ -827,6 +827,14 @@ public abstract class AbstractMessageSearchIndexTest {
     }
 
     @Test
+    void headerContainsShouldBeCaseInsensitive() throws Exception {
+        SearchQuery searchQuery = SearchQuery.of(SearchQuery.headerContains("Precedence", "LiSt"));
+
+        assertThat(messageSearchIndex.search(session, mailbox, searchQuery).toStream())
+            .containsOnly(m1.getUid(), m6.getUid(), m8.getUid(), m9.getUid());
+    }
+
+    @Test
     void headerExistsShouldReturnUidsOfMessageHavingThisHeader() throws Exception {
         SearchQuery searchQuery = SearchQuery.of(SearchQuery.headerExists("Precedence"));
 
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/EmailQueryMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
index 9007184..9f9078a 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailQueryMethodContract.scala
@@ -4682,10 +4682,69 @@ trait EmailQueryMethodContract {
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder.build))
       .getMessageId
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |      "filter" : {
+         |        "subject": "paradise"
+         |      }
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      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)
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .inPath("$.methodResponses[0][1].ids")
+        .isEqualTo(s"""["${messageId1.serialize}"]""")
+    }
+  }
+
+  @Test
+  def subjectShouldBeCaseInsensitive(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    def messageBuilder = Message.Builder
+      .of
+      .setBody("testmail", StandardCharsets.UTF_8)
+    val messageId1 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
+        messageBuilder
+          .setSubject("Yet another day in paradise")
+          .build))
+      .getMessageId
+    val messageId2 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
+        messageBuilder
+          .setSubject("Welcome to hell")
+          .build))
+      .getMessageId
+    val messageId3 = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
+        messageBuilder.build))
+      .getMessageId
     val messageId4 = server.getProbe(classOf[MailboxProbeImpl])
       .appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder().build(
         messageBuilder
-          .setSubject("Yet another day in PaRaDiSeS")
+          .setSubject("Yet another day in PaRaDiSe")
           .build))
       .getMessageId
 


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


[james-project] 06/12: JAMES-3171 Enhance MailboxGetMethod reactive management

Posted by rc...@apache.org.
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 d2f466c443c61d62e569e4abe4b4d5719cd999a1
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 14:06:12 2020 +0700

    JAMES-3171 Enhance MailboxGetMethod reactive management
    
     - Chain SMonos with `.zip`
     - Remove unnecessary intermediate flatMaps
---
 .../james/jmap/method/MailboxGetMethod.scala       | 27 ++++++++++------------
 1 file changed, 12 insertions(+), 15 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
index c433d17..c13bf3a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/MailboxGetMethod.scala
@@ -29,7 +29,7 @@ import org.apache.james.jmap.json.{MailboxSerializer, ResponseSerializer}
 import org.apache.james.jmap.mail.MailboxGet.UnparsedMailboxId
 import org.apache.james.jmap.mail.{Mailbox, MailboxFactory, MailboxGet, MailboxGetRequest, MailboxGetResponse, NotFound, PersonalNamespace, Subscriptions}
 import org.apache.james.jmap.routes.SessionSupplier
-import org.apache.james.jmap.utils.quotas.{QuotaLoader, QuotaLoaderWithPreloadedDefaultFactory}
+import org.apache.james.jmap.utils.quotas.{QuotaLoaderWithPreloadedDefault, QuotaLoaderWithPreloadedDefaultFactory}
 import org.apache.james.mailbox.exception.MailboxNotFoundException
 import org.apache.james.mailbox.model.search.MailboxQuery
 import org.apache.james.mailbox.model.{MailboxId, MailboxMetaData}
@@ -138,23 +138,20 @@ class MailboxGetMethod @Inject() (serializer: MailboxSerializer,
     val subscriptions: SMono[Subscriptions] = SMono.fromCallable(() =>
       Subscriptions(subscriptionManager.subscriptions(mailboxSession).asScala.toSet))
 
-    quotaFactory.loadFor(mailboxSession)
-      .flatMap(quotaLoader => subscriptions.map[(QuotaLoader, Subscriptions)](subscriptions => (quotaLoader, subscriptions)))
+    SMono.zip(array => (array(0).asInstanceOf[Seq[MailboxMetaData]],
+          array(1).asInstanceOf[QuotaLoaderWithPreloadedDefault],
+          array(2).asInstanceOf[Subscriptions]),
+        getAllMailboxesMetaData(capabilities, mailboxSession),
+        quotaFactory.loadFor(mailboxSession),
+        subscriptions)
       .subscribeOn(Schedulers.elastic)
-      .flatMap {
-        case (quotaLoader, subscriptions) => getAllMailboxesMetaData(capabilities, mailboxSession)
-          .map((_, quotaLoader, subscriptions))
-      }
       .flatMapMany {
         case (mailboxes, quotaLoader, subscriptions) => SFlux.fromIterable(mailboxes)
-          .map(mailbox => (mailboxes, mailbox, quotaLoader, subscriptions))
-      }
-      .flatMap {
-        case (mailboxes, mailbox, quotaLoader, subs) => mailboxFactory.create(mailboxMetaData = mailbox,
-          mailboxSession = mailboxSession,
-          subscriptions = subs,
-          allMailboxesMetadata = mailboxes,
-          quotaLoader = quotaLoader)
+          .flatMap(mailbox => mailboxFactory.create(mailboxMetaData = mailbox,
+            mailboxSession = mailboxSession,
+            subscriptions = subscriptions,
+            allMailboxesMetadata = mailboxes,
+            quotaLoader = quotaLoader))
       }
   }
 


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


[james-project] 10/12: JAMES-3413 Email/set updateValidation message should pass on distributed james

Posted by rc...@apache.org.
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 433162e9405555d85f45f7a93c51652ccf8c1bc7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 11:54:56 2020 +0700

    JAMES-3413 Email/set updateValidation message should pass on distributed james
---
 .../distributed/DistributedEmailSetMethodTest.java |  5 ++
 .../rfc8621/contract/EmailGetMethodContract.scala  |  4 +-
 .../rfc8621/contract/EmailSetMethodContract.scala  | 60 +++++++++++-----------
 .../rfc8621/memory/MemoryEmailSetMethodTest.java   |  5 ++
 .../jmap/method/EmailSetUpdatePerformer.scala      |  2 +-
 5 files changed, 44 insertions(+), 32 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSetMethodTest.java
index 20a16f1..ec07ea6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSetMethodTest.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSetMethodTest.java
@@ -62,4 +62,9 @@ public class DistributedEmailSetMethodTest implements EmailSetMethodContract {
     public MessageId randomMessageId() {
         return MESSAGE_ID_FACTORY.of(UUIDs.timeBased());
     }
+
+    @Override
+    public String invalidMessageIdMessage(String invalid) {
+        return String.format("Invalid UUID string: %s", invalid);
+    }
 }
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 20a1800..347c4ea 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
@@ -2417,7 +2417,7 @@ trait EmailGetMethodContract {
          |                        "threadId": "${messageId.serialize}",
          |                        "size": 2695,
          |                        "keywords": {},
-         |                        "blobId": "1",
+         |                        "blobId": "${messageId.serialize}",
          |                        "mailboxIds": {"${mailboxId.serialize}": true},
          |                        "id": "${messageId.serialize}",
          |                        "receivedAt": "2014-10-30T14:12:00Z",
@@ -5885,7 +5885,7 @@ trait EmailGetMethodContract {
       .inPath("methodResponses[0][1].list[0]")
       .isEqualTo(
         s"""{
-           |    "id": "1",
+           |    "id": "${messageId.serialize}",
            |    "header:Bcc:asRaw": " \\"user3\\" user3@domain.tld",
            |    "header:MessageId:asRaw": null,
            |    "header:ReplyTo:asRaw": " \\"user1\\" user1@domain.tld",
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 af77d0b..8bd5a9e 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
@@ -71,6 +71,8 @@ trait EmailSetMethodContract {
 
   def randomMessageId: MessageId
 
+  def invalidMessageIdMessage(invalid: String): String
+
   @Test
   def shouldResetKeywords(server: GuiceJamesServer): Unit = {
     val message: Message = Fixture.createTestMessage
@@ -426,9 +428,9 @@ trait EmailSetMethodContract {
     assertThatJson(response)
       .inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
       .isEqualTo(
-        """|{
+        s"""|{
           |   "type":"invalidPatch",
-          |   "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(keyword value can only be true),ArraySeq()))))),ArraySeq()))))"
+          |   "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(keyword value can only be true),ArraySeq()))))),ArraySeq()))))"
           |}""".stripMargin)
   }
 
@@ -962,7 +964,7 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].notCreated.aaaaaa")
       .isEqualTo(
         s"""{
-          |  "description": "List((/mailboxIds,List(JsonValidationError(List(For input string: \\"invalid\\"),ArraySeq()))))",
+          |  "description": "List((/mailboxIds,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))",
           |  "type": "invalidArguments"
           |}""".stripMargin)
   }
@@ -3310,7 +3312,7 @@ trait EmailSetMethodContract {
       .isEqualTo(
         """|{
            |   "type":"invalidPatch",
-           |   "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))),ArraySeq()))))"
+           |   "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))),ArraySeq()))))"
            |}""".stripMargin)
   }
 
@@ -3366,7 +3368,7 @@ trait EmailSetMethodContract {
         s"""{
            |  "${messageId.serialize}":{
            |      "type":"invalidPatch",
-           |      "description":"Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 'Recent' flag),ArraySeq()))))),ArraySeq()))))"}
+           |      "description":"Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 'Recent' flag),ArraySeq()))))),ArraySeq()))))"}
            |  }
            |}"""
           .stripMargin)
@@ -3507,10 +3509,10 @@ trait EmailSetMethodContract {
 
     assertThatJson(response)
      .inPath("methodResponses[0][1].notUpdated")
-     .isEqualTo("""{
+     .isEqualTo(s"""{
         | "invalid": {
         |     "type":"invalidPatch",
-        |     "description":"Message invalid update is invalid: For input string: \"invalid\""
+        |     "description":"Message update is invalid: ${invalidMessageIdMessage("invalid")}"
         | }
         |}""".stripMargin)
   }
@@ -4122,7 +4124,7 @@ trait EmailSetMethodContract {
       .isEqualTo(s"""{
           |  "${messageId.serialize}": {
           |     "type": "invalidPatch",
-          |     "description": "Message 1 update is invalid: Partial update and reset specified for keywords"
+          |     "description": "Message update is invalid: Partial update and reset specified for keywords"
           |   }
           |}
       """.stripMargin)
@@ -4172,7 +4174,7 @@ trait EmailSetMethodContract {
       .isEqualTo(
         """|{
            |   "type":"invalidPatch",
-           |   "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(keywords/mus*c is an invalid entry in an Email/set update patch: FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))"}"
+           |   "description": "Message update is invalid: List((,List(JsonValidationError(List(keywords/mus*c is an invalid entry in an Email/set update patch: FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))"}"
            |}""".stripMargin)
   }
 
@@ -4219,9 +4221,9 @@ trait EmailSetMethodContract {
     assertThatJson(response)
       .inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
       .isEqualTo(
-        """|{
+        s"""|{
           |   "type":"invalidPatch",
-          |   "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords/movie is invalid: Keywords partial updates requires a JsBoolean(true) (set) or a JsNull (unset)),ArraySeq()))))"
+          |   "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with keywords/movie is invalid: Keywords partial updates requires a JsBoolean(true) (set) or a JsNull (unset)),ArraySeq()))))"
           |}""".stripMargin)
   }
 
@@ -4275,7 +4277,7 @@ trait EmailSetMethodContract {
         s"""{
            |  "${messageId.serialize}":{
            |      "type":"invalidPatch",
-           |      "description":"Message 1 update is invalid: List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 'Recent' flag),ArraySeq()))))"}
+           |      "description":"Message update is invalid: List((,List(JsonValidationError(List(Does not allow to update 'Deleted' or 'Recent' flag),ArraySeq()))))"}
            |  }
            |}"""
           .stripMargin)
@@ -4375,7 +4377,7 @@ trait EmailSetMethodContract {
          |        "notDestroyed": {
          |          "invalid": {
          |            "type": "invalidArguments",
-         |            "description": "invalid is not a messageId: For input string: \\"invalid\\""
+         |            "description": "invalid is not a messageId: ${invalidMessageIdMessage("invalid")}"
          |          }
          |        }
          |      }, "c1"]]
@@ -4782,9 +4784,9 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].notUpdated")
       .isEqualTo(
       s"""{
-         |  "1": {
+         |  "${messageId.serialize}": {
          |    "type": "invalidPatch",
-         |    "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(invalid is an invalid entry in an Email/set update patch),ArraySeq()))))"
+         |    "description": "Message update is invalid: List((,List(JsonValidationError(List(invalid is an invalid entry in an Email/set update patch),ArraySeq()))))"
          |  }
          |}""".stripMargin)
   }
@@ -4831,9 +4833,9 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].notUpdated")
       .isEqualTo(
       s"""{
-         |  "1": {
+         |  "${messageId.serialize}": {
          |    "type": "invalidPatch",
-         |    "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(mailboxIds/invalid is an invalid entry in an Email/set update patch: For input string: \\"invalid\\"),ArraySeq()))))"
+         |    "description": "Message update is invalid: List((,List(JsonValidationError(List(mailboxIds/invalid is an invalid entry in an Email/set update patch: ${invalidMessageIdMessage("invalid")}),ArraySeq()))))"
          |  }
          |}""".stripMargin)
   }
@@ -4880,9 +4882,9 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].notUpdated")
       .isEqualTo(
       s"""{
-         |  "1": {
+         |  "${messageId.serialize}": {
          |    "type": "invalidPatch",
-         |    "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds/1 is invalid: MailboxId partial updates requires a JsBoolean(true) (set) or a JsNull (unset)),ArraySeq()))))"
+         |    "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds/${mailboxId1.serialize} is invalid: MailboxId partial updates requires a JsBoolean(true) (set) or a JsNull (unset)),ArraySeq()))))"
          |  }
          |}""".stripMargin)
   }
@@ -4932,9 +4934,9 @@ trait EmailSetMethodContract {
       .inPath("methodResponses[0][1].notUpdated")
       .isEqualTo(
       s"""{
-         |  "1": {
+         |  "${messageId.serialize}": {
          |    "type": "invalidPatch",
-         |    "description": "Message 1 update is invalid: Partial update and reset specified for mailboxIds"
+         |    "description": "Message update is invalid: Partial update and reset specified for mailboxIds"
          |  }
          |}""".stripMargin)
   }
@@ -4989,7 +4991,7 @@ trait EmailSetMethodContract {
          |        "notDestroyed": {
          |          "invalid": {
          |            "type": "invalidArguments",
-         |            "description": "invalid is not a messageId: For input string: \\"invalid\\""
+         |            "description": "invalid is not a messageId: ${invalidMessageIdMessage("invalid")}"
          |          }
          |        }
          |      }, "c1"]
@@ -5224,9 +5226,9 @@ trait EmailSetMethodContract {
            |          "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |          "newState": "000001",
            |          "notUpdated": {
-           |            "1": {
+           |            "${messageId.serialize}": {
            |              "type": "notFound",
-           |              "description": "Cannot find message with messageId: 1"
+           |              "description": "Cannot find message with messageId: ${messageId.serialize}"
            |            }
            |          }
            |        }, "c1"]
@@ -5381,7 +5383,7 @@ trait EmailSetMethodContract {
            |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
            |        "newState": "000001",
            |        "notUpdated": {
-           |          "1": {
+           |          "${messageId.serialize}": {
            |            "type": "notFound",
            |            "description": "Mailbox not found"
            |          }
@@ -5692,7 +5694,7 @@ trait EmailSetMethodContract {
            |        "notUpdated": {
            |          "${messageId.serialize}": {
            |            "type": "invalidPatch",
-           |            "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(For input string: \\"invalid\\"),ArraySeq()))))),ArraySeq()))))"
+           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))),ArraySeq()))))"
            |          }
            |        }
            |      }, "c1"]
@@ -5753,7 +5755,7 @@ trait EmailSetMethodContract {
            |        "notUpdated": {
            |          "${messageId.serialize}": {
            |            "type": "invalidPatch",
-           |            "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
+           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
            |          }
            |        }
            |      }, "c1"]
@@ -5814,7 +5816,7 @@ trait EmailSetMethodContract {
            |        "notUpdated": {
            |          "${messageId.serialize}": {
            |            "type": "invalidPatch",
-           |            "description": "Message ${messageId.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
+           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(Expecting mailboxId value to be a boolean),ArraySeq()))))),ArraySeq()))))"
            |          }
            |        }
            |      }, "c1"]
@@ -5895,7 +5897,7 @@ trait EmailSetMethodContract {
            |        "notUpdated": {
            |          "${messageId2.serialize}": {
            |            "type": "invalidPatch",
-           |            "description": "Message ${messageId2.serialize} update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(For input string: \\"invalid\\"),ArraySeq()))))),ArraySeq()))))"
+           |            "description": "Message update is invalid: List((,List(JsonValidationError(List(Value associated with mailboxIds is invalid: List((,List(JsonValidationError(List(${invalidMessageIdMessage("invalid")}),ArraySeq()))))),ArraySeq()))))"
            |          }
            |        }
            |      }, "c1"]
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
index cdf72f4..b67b80c 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSetMethodTest.java
@@ -45,4 +45,9 @@ public class MemoryEmailSetMethodTest implements EmailSetMethodContract {
     public MessageId randomMessageId() {
         return InMemoryMessageId.of(ThreadLocalRandom.current().nextInt(100000) + 100);
     }
+
+    @Override
+    public String invalidMessageIdMessage(String invalid) {
+        return String.format("For input string: \\\"%s\\\"", invalid);
+    }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
index 094f823..c5a71b9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
@@ -46,7 +46,7 @@ object EmailSetUpdatePerformer {
   case class EmailUpdateSuccess(messageId: MessageId) extends EmailUpdateResult
   case class EmailUpdateFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends EmailUpdateResult {
     def asMessageSetError: SetError = e match {
-      case e: IllegalArgumentException => SetError.invalidPatch(SetErrorDescription(s"Message $unparsedMessageId update is invalid: ${e.getMessage}"))
+      case e: IllegalArgumentException => SetError.invalidPatch(SetErrorDescription(s"Message update is invalid: ${e.getMessage}"))
       case _: MailboxNotFoundException => SetError.notFound(SetErrorDescription(s"Mailbox not found"))
       case e: MessageNotFoundException => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
       case _ => SetError.serverFail(SetErrorDescription(e.getMessage))


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


[james-project] 05/12: JAMES-3442 Email/set create position multipart/alternative for text/html

Posted by rc...@apache.org.
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 900bc9c3311eb86f7527286864399d3dcc46958d
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Nov 11 16:10:16 2020 +0700

    JAMES-3442 Email/set create position multipart/alternative for text/html
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 682 +++++++++++++++------
 .../org/apache/james/jmap/mail/EmailSet.scala      |  56 +-
 .../jmap/method/EmailSetCreatePerformer.scala      |   4 +-
 3 files changed, 533 insertions(+), 209 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 378fbbb..af77d0b 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
@@ -1175,7 +1175,7 @@ trait EmailSetMethodContract {
            |  },
            |  "subject": "World domination",
            |  "bodyValues": {
-           |    "1": {
+           |    "2": {
            |      "value": "$htmlBody",
            |      "isEncodingProblem": false,
            |      "isTruncated": false
@@ -1254,7 +1254,7 @@ trait EmailSetMethodContract {
            |  },
            |  "subject": "World domination",
            |  "bodyValues": {
-           |    "1": {
+           |    "2": {
            |      "value": "$htmlBody",
            |      "isEncodingProblem": false,
            |      "isTruncated": false
@@ -1818,7 +1818,7 @@ trait EmailSetMethodContract {
            |  "subject": "World domination",
            |  "attachments": [
            |    {
-           |      "partId": "3",
+           |      "partId": "5",
            |      "blobId": "$blobIdToDownload",
            |      "size": 11,
            |      "type": "text/plain",
@@ -1951,8 +1951,8 @@ trait EmailSetMethodContract {
            |  "subject": "World domination",
            |  "attachments": [
            |    {
-           |      "partId": "3",
-           |      "blobId": "${messageId}_3",
+           |      "partId": "5",
+           |      "blobId": "${messageId}_5",
            |      "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
@@ -2097,8 +2097,8 @@ trait EmailSetMethodContract {
            |  "subject": "World domination",
            |  "attachments": [
            |    {
-           |      "partId": "4",
-           |      "blobId": "${messageId}_4",
+           |      "partId": "6",
+           |      "blobId": "${messageId}_6",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -2106,8 +2106,8 @@ trait EmailSetMethodContract {
            |      "cid": "abc"
            |    },
            |    {
-           |      "partId": "5",
-           |      "blobId": "${messageId}_5",
+           |      "partId": "7",
+           |      "blobId": "${messageId}_7",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -2115,8 +2115,8 @@ trait EmailSetMethodContract {
            |      "cid": "def"
            |    },
            |    {
-           |      "partId": "6",
-           |      "blobId": "${messageId}_6",
+           |      "partId": "8",
+           |      "blobId": "${messageId}_8",
            |      "size": 11,
            |      "type": "text/plain",
            |      "charset": "UTF-8",
@@ -2125,15 +2125,15 @@ trait EmailSetMethodContract {
            |  ],
            |  "htmlBody": [
            |    {
-           |      "partId": "3",
-           |      "blobId": "${messageId}_3",
+           |      "partId": "4",
+           |      "blobId": "${messageId}_4",
            |      "size": 166,
            |      "type": "text/html",
            |      "charset": "UTF-8"
            |    }
            |  ],
            |  "bodyValues": {
-           |    "3": {
+           |    "4": {
            |      "value": "$htmlBody",
            |      "isEncodingProblem": false,
            |      "isTruncated": false
@@ -2253,63 +2253,63 @@ trait EmailSetMethodContract {
     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"
-           |        ]
-           |    ]
+           |  "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": "multipart/alternative",
+           |                    "subParts": [
+           |                      {
+           |                        "type": "text/html"
+           |                      },
+           |                      {
+           |                        "type": "text/plain"
+           |                      }
+           |                    ]
+           |                  },
+           |                  {
+           |                    "type": "text/plain",
+           |                    "disposition": "inline",
+           |                    "cid": "abc"
+           |                  },
+           |                  {
+           |                    "type": "text/plain",
+           |                    "disposition": "inline",
+           |                    "cid": "def"
+           |                  }
+           |                ]
+           |              },
+           |              {
+           |                "type": "text/plain",
+           |                "disposition": "attachment"
+           |              }
+           |            ]
+           |          }
+           |        }
+           |      ],
+           |      "notFound": []
+           |    }, "c2"]
+           |  ]
            |}""".stripMargin)
   }
 
@@ -2410,48 +2410,48 @@ trait EmailSetMethodContract {
     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": "text/html"
-           |                                },
-           |                                {
-           |                                    "type": "text/plain",
-           |                                    "disposition": "attachment"
-           |                                }
-           |                            ]
-           |                        }
-           |                    }
-           |                ],
-           |                "notFound": []
-           |            },
-           |            "c2"
-           |        ]
-           |    ]
+           |  "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/alternative",
+           |                "subParts": [
+           |                  {
+           |                    "type":"text/html"
+           |                  },
+           |                  {
+           |                    "type":"text/plain"
+           |                  }
+           |                ]
+           |              },
+           |              {
+           |                "type": "text/plain",
+           |                "disposition": "attachment"
+           |              }
+           |            ]
+           |          }
+           |        }
+           |      ],
+           |      "notFound": []
+           |    }, "c2"]
+           |  ]
            |}""".stripMargin)
   }
 
@@ -2560,54 +2560,54 @@ trait EmailSetMethodContract {
     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/related",
-           |                                 "subParts": [
-           |                                    {
-           |                                        "type": "text/html"
-           |                                    },
-           |                                    {
-           |                                        "type": "text/plain",
-           |                                        "disposition": "inline",
-           |                                        "cid": "abc"
-           |                                    },
-           |                                    {
-           |                                        "type": "text/plain",
-           |                                        "disposition": "inline",
-           |                                        "cid": "def"
-           |                                    }
-           |                                ]
-           |                        }
-           |                    }
-           |                ],
-           |                "notFound": []
-           |            },
-           |            "c2"
-           |        ]
-           |    ]
+           |  "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/related",
+           |            "subParts": [
+           |              {
+           |                "type":"multipart/alternative",
+           |                "subParts": [
+           |                  {
+           |                    "type":"text/html"
+           |                  },
+           |                  {
+           |                    "type":"text/plain"
+           |                  }
+           |                ]
+           |              },
+           |              {
+           |                "type": "text/plain",
+           |                "disposition": "inline",
+           |                "cid": "abc"
+           |              },
+           |              {
+           |                "type": "text/plain",
+           |                "disposition": "inline",
+           |                "cid": "def"
+           |              }
+           |            ]
+           |          }
+           |        }
+           |      ],
+           |      "notFound": []
+           |    }, "c2"]
+           |  ]
            |}""".stripMargin)
   }
 
@@ -2631,6 +2631,254 @@ trait EmailSetMethodContract {
       .body
       .asString
 
+    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": [],
+         |          "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/alternative",
+           |            "subParts": [
+           |              {
+           |                "type":"text/html"
+           |              },
+           |              {
+           |                "type":"text/plain"
+           |              }
+           |            ]
+           |          }
+           |        }
+           |      ], "notFound": []
+           |    }, "c2"]
+           |  ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def createShouldSupportHtmlAndTextBody(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,
+         |              "isEncodingProblem": false
+         |            }
+         |          }
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |      {
+         |        "accountId": "$ACCOUNT_ID",
+         |        "ids": ["#aaaaaa"],
+         |        "properties": ["bodyStructure", "bodyValues"],
+         |        "bodyProperties": ["type", "disposition", "cid", "subParts", "charset"],
+         |        "fetchAllBodyValues": true
+         |      },
+         |    "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/alternative",
+           |            "charset": "us-ascii",
+           |            "subParts": [
+           |              {
+           |                "type": "text/html",
+           |                "charset": "UTF-8"
+           |              },
+           |              {
+           |                "type": "text/plain",
+           |                "charset": "UTF-8"
+           |              }
+           |            ]
+           |          },
+           |          "bodyValues": {
+           |            "2": {
+           |              "value": "$htmlBody",
+           |              "isEncodingProblem": false,
+           |              "isTruncated": false
+           |            },
+           |            "3": {
+           |              "value": "I have the most brilliant plan. Let me tell you all about it. What we do is, we",
+           |              "isEncodingProblem": false,
+           |              "isTruncated": false
+           |            }
+           |          }
+           |        }
+           |      ],
+           |      "notFound": []
+           |    }, "c2"]
+           |  ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def createShouldWrapInlineBodyWithAlternativeMultipart(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 =
@@ -2645,7 +2893,28 @@ trait EmailSetMethodContract {
          |             "${mailboxId.serialize}": true
          |          },
          |          "subject": "World domination",
-         |          "attachments": [],
+         |          "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",
@@ -2699,41 +2968,66 @@ trait EmailSetMethodContract {
       .get.asInstanceOf[JsString].value
 
     assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].created.aaaaaa.id")
       .isEqualTo(
         s"""{
-           |    "sessionState": "75128aab4b1b",
-           |    "methodResponses": [
-           |        [
-           |            "Email/set",
+           |  "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": [
            |            {
-           |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-           |                "newState": "000001",
-           |                "created": {
-           |                    "aaaaaa": {
-           |                        "id": "$messageId"
+           |              "type": "multipart/related",
+           |              "subParts": [
+           |                {
+           |                  "type": "multipart/alternative",
+           |                  "subParts": [
+           |                    {
+           |                      "type": "text/html"
+           |                    },
+           |                    {
+           |                      "type": "text/plain"
            |                    }
+           |                  ]
+           |                },
+           |                {
+           |                  "type": "text/plain",
+           |                  "disposition": "inline",
+           |                  "cid": "abc"
+           |                },
+           |                {
+           |                  "type": "text/plain",
+           |                  "disposition": "inline",
+           |                  "cid": "def"
            |                }
+           |              ]
            |            },
-           |            "c1"
-           |        ],
-           |        [
-           |            "Email/get",
            |            {
-           |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
-           |                "state": "000001",
-           |                "list": [
-           |                    {
-           |                        "id": "$messageId",
-           |                        "bodyStructure": {
-           |                            "type": "text/html"
-           |                        }
-           |                    }
-           |                ],
-           |                "notFound": []
-           |            },
-           |            "c2"
-           |        ]
-           |    ]
+           |              "type": "text/plain",
+           |              "disposition": "attachment"
+           |             }
+           |           ]
+           |         }
+           |       }
+           |     ],
+           |     "notFound": []
+           |    }, "c2"]
+           |  ]
            |}""".stripMargin)
   }
 
@@ -2834,7 +3128,7 @@ trait EmailSetMethodContract {
            |  "attachments": [
            |    {
            |      "name": "myAttachment",
-           |      "partId": "3",
+           |      "partId": "5",
            |      "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 464be6d..ca1af42 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
@@ -19,10 +19,12 @@
 package org.apache.james.jmap.mail
 
 import java.io.IOException
-import java.nio.charset.StandardCharsets
+import java.nio.charset.{StandardCharsets, Charset => NioCharset}
 import java.util.Date
 
 import cats.implicits._
+import com.google.common.net.MediaType
+import com.google.common.net.MediaType.{HTML_UTF_8, PLAIN_TEXT_UTF_8}
 import eu.timepit.refined
 import eu.timepit.refined.api.Refined
 import eu.timepit.refined.collection.NonEmpty
@@ -41,7 +43,8 @@ import org.apache.james.mime4j.dom.field.{ContentIdField, ContentTypeField, Fiel
 import org.apache.james.mime4j.dom.{Entity, Message}
 import org.apache.james.mime4j.field.Fields
 import org.apache.james.mime4j.message.{BodyPartBuilder, MultipartBuilder}
-import org.apache.james.mime4j.stream.{Field, RawField}
+import org.apache.james.mime4j.stream.{Field, NameValuePair, RawField}
+import org.apache.james.util.html.HtmlTextExtractor
 import play.api.libs.json.JsObject
 
 import scala.jdk.CollectionConverters._
@@ -66,6 +69,7 @@ object SubType {
   val HTML_SUBTYPE = "html"
   val MIXED_SUBTYPE = "mixed"
   val RELATED_SUBTYPE = "related"
+  val ALTERNATIVE_SUBTYPE = "alternative"
 }
 
 case class ClientPartId(id: Id)
@@ -119,7 +123,10 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
                                 bodyValues: Option[Map[ClientPartId, ClientEmailBodyValue]],
                                 specificHeaders: List[EmailHeader],
                                 attachments: Option[List[Attachment]]) {
-  def toMime4JMessage(attachmentManager: AttachmentManager, attachmentContentLoader: AttachmentContentLoader, mailboxSession: MailboxSession): Either[Exception, Message] =
+  def toMime4JMessage(attachmentManager: AttachmentManager,
+                      attachmentContentLoader: AttachmentContentLoader,
+                      htmlTextExtractor: HtmlTextExtractor,
+                      mailboxSession: MailboxSession): Either[Exception, Message] =
     validateHtmlBody
       .flatMap(maybeHtmlBody => {
         val builder = Message.Builder.of
@@ -138,19 +145,37 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
           .flatMap(_ => {
             specificHeaders.map(_.asField).foreach(builder.addField)
             attachments.filter(_.nonEmpty).map(attachments =>
-              createMultipartWithAttachments(maybeHtmlBody, attachments, attachmentManager, attachmentContentLoader, mailboxSession)
+              createMultipartWithAttachments(maybeHtmlBody, attachments, attachmentManager, attachmentContentLoader, htmlTextExtractor, mailboxSession)
                 .map(multipartBuilder => {
                   builder.setBody(multipartBuilder)
                   builder.build
                 }))
-              .getOrElse(Right(builder.setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build))
+              .getOrElse({
+                builder.setBody(createAlternativeBody(maybeHtmlBody, htmlTextExtractor))
+                Right(builder.build)
+              })
           })
       })
 
+  private def createAlternativeBody(htmlBody: Option[String], htmlTextExtractor: HtmlTextExtractor): MultipartBuilder = {
+    val alternativeBuilder: MultipartBuilder = MultipartBuilder.create(SubType.ALTERNATIVE_SUBTYPE)
+    addBodypart(alternativeBuilder, htmlBody.getOrElse(""), HTML_UTF_8, StandardCharsets.UTF_8)
+    addBodypart(alternativeBuilder, htmlTextExtractor.toPlainText(htmlBody.getOrElse("")), PLAIN_TEXT_UTF_8, StandardCharsets.UTF_8)
+
+    alternativeBuilder
+  }
+
+  private def addBodypart(multipartBuilder: MultipartBuilder, body: String, mediaType: MediaType, charset: NioCharset): MultipartBuilder =
+    multipartBuilder.addBodyPart(
+      BodyPartBuilder.create.setBody(body, charset)
+      .setContentType(mediaType.withoutParameters().toString, new NameValuePair("charset", charset.name))
+      .setContentTransferEncoding("quoted-printable"))
+
   private def createMultipartWithAttachments(maybeHtmlBody: Option[String],
                                              attachments: List[Attachment],
                                              attachmentManager: AttachmentManager,
                                              attachmentContentLoader: AttachmentContentLoader,
+                                             htmlTextExtractor: HtmlTextExtractor,
                                              mailboxSession: MailboxSession): Either[Exception, MultipartBuilder] = {
     val maybeAttachments: Either[Exception, List[(Attachment, AttachmentMetadata, Array[Byte])]] =
       attachments
@@ -162,17 +187,20 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
     maybeAttachments.map(list => {
 
       (list.filter(_._1.isInline), list.filter(!_._1.isInline)) match {
-        case (Nil, normalAttachments) => createMixedBody(maybeHtmlBody, normalAttachments)
-        case (inlineAttachments, Nil) => createRelatedBody(maybeHtmlBody, inlineAttachments)
-        case (inlineAttachments, normalAttachments) => createMixedRelatedBody(maybeHtmlBody, inlineAttachments, normalAttachments)
+        case (Nil, normalAttachments) => createMixedBody(maybeHtmlBody, normalAttachments, htmlTextExtractor)
+        case (inlineAttachments, Nil) => createRelatedBody(maybeHtmlBody, inlineAttachments, htmlTextExtractor)
+        case (inlineAttachments, normalAttachments) => createMixedRelatedBody(maybeHtmlBody, inlineAttachments, normalAttachments, htmlTextExtractor)
       }
     })
   }
 
-  private def createMixedRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+  private def createMixedRelatedBody(maybeHtmlBody: Option[String],
+                                     inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])],
+                                     normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])],
+                                     htmlTextExtractor: HtmlTextExtractor) = {
     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)
+    relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, htmlTextExtractor).build))
     inlineAttachments.foldLeft(relatedMultipartBuilder) {
       case (acc, (attachment, storedMetadata, content)) =>
         acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
@@ -188,9 +216,9 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
     }
   }
 
-  private def createMixedBody(maybeHtmlBody: Option[String], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+  private def createMixedBody(maybeHtmlBody: Option[String], normalAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
     val mixedMultipartBuilder = MultipartBuilder.create(SubType.MIXED_SUBTYPE)
-    mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+    mixedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, htmlTextExtractor).build))
     normalAttachments.foldLeft(mixedMultipartBuilder) {
       case (acc, (attachment, storedMetadata, content)) =>
         acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
@@ -198,9 +226,9 @@ case class EmailCreationRequest(mailboxIds: MailboxIds,
     }
   }
 
-  private def createRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])]) = {
+  private def createRelatedBody(maybeHtmlBody: Option[String], inlineAttachments: List[(Attachment, AttachmentMetadata, Array[Byte])], htmlTextExtractor: HtmlTextExtractor) = {
     val relatedMultipartBuilder = MultipartBuilder.create(SubType.RELATED_SUBTYPE)
-    relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(maybeHtmlBody.getOrElse(""), SubType.HTML_SUBTYPE, StandardCharsets.UTF_8).build)
+    relatedMultipartBuilder.addBodyPart(BodyPartBuilder.create().setBody(createAlternativeBody(maybeHtmlBody, htmlTextExtractor).build))
     inlineAttachments.foldLeft(relatedMultipartBuilder) {
       case (acc, (attachment, storedMetadata, content)) =>
         acc.addBodyPart(toBodypartBuilder(attachment, storedMetadata, content))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
index 1fcac8c..662c750 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
@@ -35,6 +35,7 @@ import org.apache.james.mailbox.MessageManager.AppendCommand
 import org.apache.james.mailbox.exception.{AttachmentNotFoundException, MailboxNotFoundException}
 import org.apache.james.mailbox.model.MailboxId
 import org.apache.james.mailbox.{AttachmentContentLoader, AttachmentManager, MailboxManager, MailboxSession}
+import org.apache.james.util.html.HtmlTextExtractor
 import reactor.core.scala.publisher.{SFlux, SMono}
 import reactor.core.scheduler.Schedulers
 
@@ -71,6 +72,7 @@ object EmailSetCreatePerformer {
 class EmailSetCreatePerformer @Inject()(serializer: EmailSetSerializer,
                                         attachmentManager: AttachmentManager,
                                         attachmentContentLoader: AttachmentContentLoader,
+                                        htmlTextExtractor: HtmlTextExtractor,
                                         mailboxManager: MailboxManager) {
 
   def create(request: EmailSetRequest, mailboxSession: MailboxSession): SMono[CreationResults] =
@@ -87,7 +89,7 @@ class EmailSetCreatePerformer @Inject()(serializer: EmailSetSerializer,
     if (mailboxIds.size != 1) {
       SMono.just(CreationFailure(clientId, new IllegalArgumentException("mailboxIds need to have size 1")))
     } else {
-      request.toMime4JMessage(attachmentManager, attachmentContentLoader, mailboxSession)
+      request.toMime4JMessage(attachmentManager, attachmentContentLoader, htmlTextExtractor, mailboxSession)
         .fold(e => SMono.just(CreationFailure(clientId, e)),
           message => SMono.fromCallable[CreationResult](() => {
             val appendResult = mailboxManager.getMailbox(mailboxIds.head, mailboxSession)


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


[james-project] 09/12: JAMES-3373 Avoid using hamcrest

Posted by rc...@apache.org.
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 87b3678f88f424d29eb91da91e0b009eae29beaa
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Wed Nov 11 11:39:05 2020 +0700

    JAMES-3373 Avoid using hamcrest
---
 .../james/jmap/rfc8621/contract/DownloadContract.scala    | 15 ++++++++++-----
 1 file changed, 10 insertions(+), 5 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/DownloadContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
index 20579b9..f10ef4d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/DownloadContract.scala
@@ -36,7 +36,7 @@ import org.apache.james.mailbox.model.{MailboxACL, MailboxPath, MessageId}
 import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
 import org.apache.james.utils.DataProbeImpl
 import org.assertj.core.api.Assertions.assertThat
-import org.hamcrest.Matchers.{containsString, emptyOrNullString}
+import org.hamcrest.Matchers.containsString
 import org.junit.jupiter.api.{BeforeEach, Test}
 
 object DownloadContract {
@@ -303,14 +303,17 @@ trait DownloadContract {
         ClassLoader.getSystemResourceAsStream("eml/multipart_simple.eml")))
       .getMessageId
 
-    `given`
+    val contentDisposition = `given`
       .basePath("")
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
     .when
       .get(s"/download/$accountId/${messageId.serialize()}_3")
     .`then`
       .statusCode(SC_OK)
-      .header("Content-Disposition",  emptyOrNullString())
+      .extract()
+      .header("Content-Disposition")
+
+    assertThat(contentDisposition).isNullOrEmpty()
   }
 
   @Test
@@ -342,14 +345,16 @@ trait DownloadContract {
         ClassLoader.getSystemResourceAsStream("eml/multipart_simple.eml")))
       .getMessageId
 
-    `given`
+    val contentDisposition = `given`
       .basePath("")
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
     .when
       .get(s"/download/$accountId/${messageId.serialize()}")
     .`then`
       .statusCode(SC_OK)
-      .header("Content-Disposition", emptyOrNullString())
+      .extract().header("Content-Disposition")
+
+    assertThat(contentDisposition).isNullOrEmpty()
   }
 
   @Test


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