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 <[email protected]>
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: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to