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]
