You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by bt...@apache.org on 2020/04/27 02:19:37 UTC

[james-project] 01/14: JAMES-2997 MessageParser & MIMEMessageConverter should preserve charset

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

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

commit 411e330e8c7a84d8c46396e20acab6722fe96cd0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Sun Apr 19 18:50:42 2020 +0700

    JAMES-2997 MessageParser & MIMEMessageConverter should preserve charset
    
    Previous implementations where not preserving charset upon:
     - composing a message via JMAP
     - storing an attachment related to a mailbox
    
    Note that JMAP attachment upload was preserving charset.
    
    **master** is also affected by this issue but the problem was hidden as
    SetMessages was returning the expected attachment metadata and not the
    actual one.
---
 .../store/mail/model/impl/MessageParser.java       | 21 ++++-
 .../store/mail/model/impl/MessageParserTest.java   | 24 +++++-
 mailbox/store/src/test/resources/eml/charset.eml   | 39 +++++++++
 mailbox/store/src/test/resources/eml/charset2.eml  | 52 ++++++++++++
 .../methods/integration/SetMessagesMethodTest.java | 96 +++++++++++++++++++---
 .../test/resources/cucumber/GetMessages.feature    | 10 +--
 .../jmap/draft/methods/MIMEMessageConverter.java   | 20 ++---
 .../draft/methods/MIMEMessageConverterTest.java    | 48 +++++++++++
 8 files changed, 277 insertions(+), 33 deletions(-)

diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/model/impl/MessageParser.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/model/impl/MessageParser.java
index 33d3406..53649fb 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/model/impl/MessageParser.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/model/impl/MessageParser.java
@@ -19,11 +19,14 @@
 
 package org.apache.james.mailbox.store.mail.model.impl;
 
+import static org.apache.james.mime4j.dom.field.ContentTypeField.PARAM_CHARSET;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Stream;
 
@@ -38,6 +41,7 @@ import org.apache.james.mime4j.dom.field.ContentDispositionField;
 import org.apache.james.mime4j.dom.field.ContentIdField;
 import org.apache.james.mime4j.dom.field.ContentTypeField;
 import org.apache.james.mime4j.dom.field.ParsedField;
+import org.apache.james.mime4j.field.Fields;
 import org.apache.james.mime4j.message.DefaultMessageBuilder;
 import org.apache.james.mime4j.message.DefaultMessageWriter;
 import org.apache.james.mime4j.stream.Field;
@@ -156,7 +160,22 @@ public class MessageParser {
     }
 
     private Optional<String> contentType(Optional<ContentTypeField> contentTypeField) {
-        return contentTypeField.map(ContentTypeField::getMimeType);
+        return contentTypeField.map(this::contentTypePreserveCharset);
+    }
+
+    private String contentTypePreserveCharset(ContentTypeField contentTypeField) {
+        Map<String, String> params = contentTypeField.getParameters()
+            .entrySet()
+            .stream()
+            .filter(param -> param.getKey().equals(PARAM_CHARSET))
+            .collect(Guavate.toImmutableMap(Map.Entry::getKey, Map.Entry::getValue));
+
+        try {
+            return Fields.contentType(contentTypeField.getMimeType(), params)
+                .getBody();
+        } catch (IllegalArgumentException e) {
+            return contentTypeField.getMimeType();
+        }
     }
 
     private Optional<String> name(Optional<ContentTypeField> contentTypeField, Optional<ContentDispositionField> contentDispositionField) {
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/impl/MessageParserTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/impl/MessageParserTest.java
index 721b8b7..8fe2009 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/impl/MessageParserTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/impl/MessageParserTest.java
@@ -264,13 +264,33 @@ class MessageParserTest {
     }
 
     @Test
+    void getAttachmentsShouldRetrieveCharset() throws Exception {
+        List<ParsedAttachment> attachments = testee.retrieveAttachments(
+            ClassLoader.getSystemResourceAsStream("eml/charset.eml"));
+
+        assertThat(attachments).hasSize(1)
+            .first()
+            .satisfies(attachment -> assertThat(attachment.getContentType()).isEqualTo("text/calendar; charset=iso-8859-1"));
+    }
+
+    @Test
+    void getAttachmentsShouldRetrieveAllPartsCharset() throws Exception {
+        List<ParsedAttachment> attachments = testee.retrieveAttachments(
+            ClassLoader.getSystemResourceAsStream("eml/charset2.eml"));
+
+        assertThat(attachments).hasSize(2)
+            .extracting(ParsedAttachment::getContentType)
+            .containsOnly("text/calendar; charset=iso-8859-1", "text/calendar; charset=iso-4444-5");
+    }
+
+    @Test
     void getAttachmentsShouldConsiderICSAsAttachments() throws Exception {
         List<ParsedAttachment> attachments = testee.retrieveAttachments(
             ClassLoader.getSystemResourceAsStream("eml/calendar.eml"));
 
         assertThat(attachments)
             .hasSize(1)
-            .allMatch(messageAttachment -> messageAttachment.getContentType().equals("text/calendar"));
+            .allMatch(messageAttachment -> messageAttachment.getContentType().equals("text/calendar; charset=utf-8"));
     }
 
     @Test
@@ -306,6 +326,6 @@ class MessageParserTest {
 
         List<ParsedAttachment> result = testee.retrieveAttachments(new ByteArrayInputStream(DefaultMessageWriter.asBytes(message)));
         assertThat(result).hasSize(1)
-            .allMatch(attachment -> attachment.getContentType().equals(MDN.DISPOSITION_CONTENT_TYPE));
+            .allMatch(attachment -> attachment.getContentType().equals("message/disposition-notification; charset=UTF-8"));
     }
 }
diff --git a/mailbox/store/src/test/resources/eml/charset.eml b/mailbox/store/src/test/resources/eml/charset.eml
new file mode 100644
index 0000000..957b4f1
--- /dev/null
+++ b/mailbox/store/src/test/resources/eml/charset.eml
@@ -0,0 +1,39 @@
+From: Test 1 <te...@linagora.com>
+To: Test 2 <te...@linagora.com>
+Subject: New Time Proposed: New event from Test: lole
+Date: Tue, 13 Mar 2018 14:36:08 +0000
+Message-ID: <AM...@AM5P190MB0354.EURP190.PROD.OUTLOOK.COM>
+Accept-Language: en-US
+Content-Language: en-US
+Content-Type: multipart/alternative;
+	boundary="_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_"
+MIME-Version: 1.0
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+Will be better for me, thx
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_
+Content-Type: text/html; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+ <p>Will be better for me, thx</p>
+</html>
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_
+Content-Type: text/calendar; charset="iso-8859-1"; method=COUNTER
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSDQpNRVRIT0Q6Q09VTlRFUg0KUFJPRElEOk1pY3Jvc29mdCBFeGNoYW5n
+ZSBTZXJ2ZXIgMjAxMA0KVkVSU0lPTjoyLjANCkJFR0lOOlZUSU1FWk9ORQ0KVFpJRDpFdXJvcGUv
+QmVybGluDQpCRUdJTjpTVEFOREFSRA0KRFRTVEFSVDoxNjAxMDEwMVQwMzAwMDANClRaT0ZGU0VU
+RlJPTTorMDIwMA0KVFpPRkZTRVRUTzorMDEwMA0KUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9
+MTtCWURBWT0tMVNVO0JZTU9OVEg9MTANCkVORDpTVEFOREFSRA0KQkVHSU46REFZTElHSFQNCkRU
+U0VRVUVOQ0U6MA0KRFRTVEFNUDoyMDE4MDMxM1QxNDM2MDdaDQpDT01NRU5UO0xBTkdVQUdFPWVu
+LVVTOldpbGwgYmUgYmV0dGVyIGZvciBtZVwsIHRoeFxuDQpFTkQ6VkVWRU5UDQpFTkQ6VkNBTEVO
+REFSDQo=
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_--
\ No newline at end of file
diff --git a/mailbox/store/src/test/resources/eml/charset2.eml b/mailbox/store/src/test/resources/eml/charset2.eml
new file mode 100644
index 0000000..7919d8c
--- /dev/null
+++ b/mailbox/store/src/test/resources/eml/charset2.eml
@@ -0,0 +1,52 @@
+From: Test 1 <te...@linagora.com>
+To: Test 2 <te...@linagora.com>
+Subject: New Time Proposed: New event from Test: lole
+Date: Tue, 13 Mar 2018 14:36:08 +0000
+Message-ID: <AM...@AM5P190MB0354.EURP190.PROD.OUTLOOK.COM>
+Accept-Language: en-US
+Content-Language: en-US
+Content-Type: multipart/alternative;
+	boundary="_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_"
+MIME-Version: 1.0
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_
+Content-Type: text/plain; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+Will be better for me, thx
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_
+Content-Type: text/html; charset="iso-8859-1"
+Content-Transfer-Encoding: quoted-printable
+
+<html>
+ <p>Will be better for me, thx</p>
+</html>
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_
+Content-Type: text/calendar; charset="iso-8859-1"; method=COUNTER
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSDQpNRVRIT0Q6Q09VTlRFUg0KUFJPRElEOk1pY3Jvc29mdCBFeGNoYW5n
+ZSBTZXJ2ZXIgMjAxMA0KVkVSU0lPTjoyLjANCkJFR0lOOlZUSU1FWk9ORQ0KVFpJRDpFdXJvcGUv
+QmVybGluDQpCRUdJTjpTVEFOREFSRA0KRFRTVEFSVDoxNjAxMDEwMVQwMzAwMDANClRaT0ZGU0VU
+RlJPTTorMDIwMA0KVFpPRkZTRVRUTzorMDEwMA0KUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9
+MTtCWURBWT0tMVNVO0JZTU9OVEg9MTANCkVORDpTVEFOREFSRA0KQkVHSU46REFZTElHSFQNCkRU
+U0VRVUVOQ0U6MA0KRFRTVEFNUDoyMDE4MDMxM1QxNDM2MDdaDQpDT01NRU5UO0xBTkdVQUdFPWVu
+LVVTOldpbGwgYmUgYmV0dGVyIGZvciBtZVwsIHRoeFxuDQpFTkQ6VkVWRU5UDQpFTkQ6VkNBTEVO
+REFSDQo=
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_
+Content-Type: text/calendar; charset="iso-4444-5"; method=COUNTER
+Content-Transfer-Encoding: base64
+
+QkVHSU46VkNBTEVOREFSDQpNRVRIT0Q6Q09VTlRFUg0KUFJPRElEOk1pY3Jvc29mdCBFeGNoYW5n
+ZSBTZXJ2ZXIgMjAxMA0KVkVSU0lPTjoyLjANCkJFR0lOOlZUSU1FWk9ORQ0KVFpJRDpFdXJvcGUv
+QmVybGluDQpCRUdJTjpTVEFOREFSRA0KRFRTVEFSVDoxNjAxMDEwMVQwMzAwMDANClRaT0ZGU0VU
+RlJPTTorMDIwMA0KVFpPRkZTRVRUTzorMDEwMA0KUlJVTEU6RlJFUT1ZRUFSTFk7SU5URVJWQUw9
+MTtCWURBWT0tMVNVO0JZTU9OVEg9MTANCkVORDpTVEFOREFSRA0KQkVHSU46REFZTElHSFQNCkRU
+U0VRVUVOQ0U6MA0KRFRTVEFNUDoyMDE4MDMxM1QxNDM2MDdaDQpDT01NRU5UO0xBTkdVQUdFPWVu
+LVVTOldpbGwgYmUgYmV0dGVyIGZvciBtZVwsIHRoeFxuDQpFTkQ6VkVWRU5UDQpFTkQ6VkNBTEVO
+REFSDQo=
+
+--_000_AM5P190MB03542A58E344F2C68475A951AFD20AM5P190MB0354EURP_--
\ No newline at end of file
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
index bf18630..c65bcda 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/java/org/apache/james/jmap/draft/methods/integration/SetMessagesMethodTest.java
@@ -153,6 +153,7 @@ public abstract class SetMessagesMethodTest {
     private static final String NOT_UPDATED = ARGUMENTS + ".notUpdated";
     private static final int BIG_MESSAGE_SIZE = 20 * 1024 * 1024;
     public static final String OCTET_CONTENT_TYPE = "application/octet-stream";
+    public static final String OCTET_CONTENT_TYPE_UTF8 = "application/octet-stream; charset=UTF-8";
 
     private AccessToken bobAccessToken;
 
@@ -4016,12 +4017,81 @@ public abstract class SetMessagesMethodTest {
                 createdPath + ".attachments[0].inlinedWithCid", createdPath + ".attachments[1].inlinedWithCid")
             .inPath(createdPath + ".attachments")
             .isEqualTo("[{" +
-                "  \"type\":\"application/octet-stream\"," +
+                "  \"type\":\"application/octet-stream; charset=UTF-8\"," +
                 "  \"size\":" + bytes1.length() + "," +
                 "  \"cid\":null," +
                 "  \"isInline\":false" +
                 "}, {" +
-                "  \"type\":\"application/octet-stream\"," +
+                "  \"type\":\"application/octet-stream; charset=UTF-8\"," +
+                "  \"size\":" + bytes2.length() + "," +
+                "  \"cid\":\"123456789\"," +
+                "  \"isInline\":true" +
+                "}]");
+    }
+
+    @Test
+    public void setMessagesShouldPreserveCharsetOfAttachment() throws Exception {
+        String bytes1 = "attachment";
+        String bytes2 = "attachment2";
+        AttachmentMetadata uploadedAttachment1 = uploadAttachment(OCTET_CONTENT_TYPE_UTF8, bytes1.getBytes(StandardCharsets.UTF_8));
+        AttachmentMetadata uploadedAttachment2 = uploadAttachment(OCTET_CONTENT_TYPE_UTF8, bytes2.getBytes(StandardCharsets.UTF_8));
+
+        String messageCreationId = "creationId";
+        String fromAddress = USERNAME.asString();
+        String outboxId = getOutboxId(accessToken);
+        String requestBody = "[" +
+            "  [" +
+            "    \"setMessages\"," +
+            "    {" +
+            "      \"create\": { \"" + messageCreationId  + "\" : {" +
+            "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+            "        \"to\": [{ \"name\": \"BOB\", \"email\": \"someone@example.com\"}]," +
+            "        \"subject\": \"Message with two attachments\"," +
+            "        \"textBody\": \"Test body\"," +
+            "        \"mailboxIds\": [\"" + outboxId + "\"], " +
+            "        \"attachments\": [" +
+            "               {\"blobId\" : \"" + uploadedAttachment1.getAttachmentId().getId() + "\", " +
+            "               \"type\" : \"" + uploadedAttachment1.getType() + "\", " +
+            "               \"size\" : " + uploadedAttachment1.getSize() + "}," +
+            "               {\"blobId\" : \"" + uploadedAttachment2.getAttachmentId().getId() + "\", " +
+            "               \"type\" : \"" + uploadedAttachment2.getType() + "\", " +
+            "               \"size\" : " + uploadedAttachment2.getSize() + ", " +
+            "               \"cid\" : \"123456789\", " +
+            "               \"isInline\" : true }" +
+            "           ]" +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+        String createdPath = ARGUMENTS + ".created[\"" + messageCreationId + "\"]";
+
+        String json = given()
+            .header("Authorization", accessToken.asString())
+            .body(requestBody)
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notCreated", aMapWithSize(0))
+            .body(ARGUMENTS + ".created", aMapWithSize(1))
+            .body(createdPath + ".attachments", hasSize(2))
+            .extract().asString();
+
+        assertThatJson(json)
+            .withOptions(new Options(Option.TREATING_NULL_AS_ABSENT, Option.IGNORING_ARRAY_ORDER, Option.IGNORING_EXTRA_FIELDS))
+            .whenIgnoringPaths(createdPath + ".attachments[0].blobId", createdPath + ".attachments[1].blobId",
+                createdPath + ".attachments[0].inlinedWithCid", createdPath + ".attachments[1].inlinedWithCid")
+            .inPath(createdPath + ".attachments")
+            .isEqualTo("[{" +
+                "  \"type\":\"application/octet-stream; charset=UTF-8\"," +
+                "  \"size\":" + bytes1.length() + "," +
+                "  \"cid\":null," +
+                "  \"isInline\":false" +
+                "}, {" +
+                "  \"type\":\"application/octet-stream; charset=UTF-8\"," +
                 "  \"size\":" + bytes2.length() + "," +
                 "  \"cid\":\"123456789\"," +
                 "  \"isInline\":true" +
@@ -4302,7 +4372,7 @@ public abstract class SetMessagesMethodTest {
             .body(NAME, equalTo("messages"))
             .body(ARGUMENTS + ".list", hasSize(1))
             .body(firstMessage + ".attachments", hasSize(1))
-            .body(firstAttachment + ".type", equalTo(OCTET_CONTENT_TYPE))
+            .body(firstAttachment + ".type", equalTo(OCTET_CONTENT_TYPE_UTF8))
             .body(firstAttachment + ".size", equalTo(rawBytes.length))
             .body(firstAttachment + ".cid", equalTo("123456789"))
             .body(firstAttachment + ".isInline", equalTo(true))
@@ -4375,7 +4445,7 @@ public abstract class SetMessagesMethodTest {
             .body(NAME, equalTo("messages"))
             .body(ARGUMENTS + ".list", hasSize(1))
             .body(firstMessage + ".attachments", hasSize(1))
-            .body(firstAttachment + ".type", equalTo(OCTET_CONTENT_TYPE))
+            .body(firstAttachment + ".type", equalTo(OCTET_CONTENT_TYPE_UTF8))
             .body(firstAttachment + ".size", equalTo(rawBytes.length))
             .body(firstAttachment + ".cid", equalTo("123456789"))
             .body(firstAttachment + ".isInline", equalTo(true))
@@ -4472,7 +4542,7 @@ public abstract class SetMessagesMethodTest {
             .body(firstMessage + ".textBody", equalTo("Test body, plain text version"))
             .body(firstMessage + ".htmlBody", equalTo("Test <b>body</b>, HTML version"))
             .body(firstMessage + ".attachments", hasSize(1))
-            .body(firstAttachment + ".type", equalTo("text/html"))
+            .body(firstAttachment + ".type", equalTo("text/html; charset=UTF-8"))
             .body(firstAttachment + ".size", equalTo(text.length()))
             .extract()
             .jsonPath()
@@ -4547,7 +4617,7 @@ public abstract class SetMessagesMethodTest {
             .body(firstMessage + ".textBody", equalTo("Test body, plain text version"))
             .body(firstMessage + ".htmlBody", isEmptyOrNullString())
             .body(firstMessage + ".attachments", hasSize(1))
-            .body(firstAttachment + ".type", equalTo("text/html"))
+            .body(firstAttachment + ".type", equalTo("text/html; charset=UTF-8"))
             .body(firstAttachment + ".size", equalTo((int) uploadedAttachment.getSize()))
             .extract()
             .jsonPath()
@@ -4632,7 +4702,7 @@ public abstract class SetMessagesMethodTest {
             .body(firstMessage + ".textBody", isEmptyOrNullString())
             .body(firstMessage + ".htmlBody", isEmptyOrNullString())
             .body(firstMessage + ".attachments", hasSize(1))
-            .body(firstAttachment + ".type", equalTo("text/plain"))
+            .body(firstAttachment + ".type", equalTo("text/plain; charset=UTF-8"))
             .body(firstAttachment + ".size", equalTo((int) uploadedAttachment.getSize()))
             .extract()
             .jsonPath()
@@ -5374,7 +5444,7 @@ public abstract class SetMessagesMethodTest {
             .body(ARGUMENTS + ".notCreated", aMapWithSize(0))
             .body(ARGUMENTS + ".created", aMapWithSize(1))
             .body(createdPath + ".attachments", hasSize(1))
-            .body(singleAttachment + ".type", equalTo("text/html"))
+            .body(singleAttachment + ".type", equalTo("text/html; charset=UTF-8"))
             .body(singleAttachment + ".size", equalTo((int) uploadedAttachment.getSize()));
     }
 
@@ -5493,9 +5563,9 @@ public abstract class SetMessagesMethodTest {
     @Test
     public void setMessagesShouldReturnAttachmentsWhenMessageHasInlinedAttachmentButNoCid() throws Exception {
         String bytes = "attachment";
-        AttachmentMetadata uploadedAttachment1 = uploadAttachment(OCTET_CONTENT_TYPE, bytes.getBytes(StandardCharsets.UTF_8));
+        AttachmentMetadata uploadedAttachment1 = uploadAttachment(OCTET_CONTENT_TYPE_UTF8, bytes.getBytes(StandardCharsets.UTF_8));
         String bytes2 = "attachment2";
-        AttachmentMetadata uploadedAttachment2 = uploadAttachment(OCTET_CONTENT_TYPE, bytes2.getBytes(StandardCharsets.UTF_8));
+        AttachmentMetadata uploadedAttachment2 = uploadAttachment(OCTET_CONTENT_TYPE_UTF8, bytes2.getBytes(StandardCharsets.UTF_8));
 
         String messageCreationId = "creationId";
         String fromAddress = USERNAME.asString();
@@ -5545,11 +5615,11 @@ public abstract class SetMessagesMethodTest {
             .withOptions(new Options(Option.TREATING_NULL_AS_ABSENT, Option.IGNORING_ARRAY_ORDER, Option.IGNORING_EXTRA_FIELDS))
             .inPath(createdPath + ".attachments")
             .isEqualTo("[{" +
-                "  \"type\":\"application/octet-stream\"," +
+                "  \"type\":\"application/octet-stream; charset=UTF-8\"," +
                 "  \"size\":" + bytes2.length() + "," +
                 "  \"isInline\":false" +
                 "}, {" +
-                "  \"type\":\"application/octet-stream\"," +
+                "  \"type\":\"application/octet-stream; charset=UTF-8\"," +
                 "  \"size\":" + bytes.length() + "," +
                 "  \"isInline\":false" + // See JAMES-2258 inline should be false in case of no Content-ID for inlined attachment
                 // Stored attachment will not be considered as having an inlined attachment.
@@ -5684,7 +5754,7 @@ public abstract class SetMessagesMethodTest {
             .body(NAME, equalTo("messages"))
             .body(ARGUMENTS + ".list", hasSize(1))
             .body(message + ".attachments", hasSize(1))
-            .body(firstAttachment + ".type", equalTo("text/calendar"))
+            .body(firstAttachment + ".type", equalTo("text/calendar; charset=UTF-8"))
             .body(firstAttachment + ".blobId", not(isEmptyOrNullString()));
     }
 
diff --git a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/resources/cucumber/GetMessages.feature b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/resources/cucumber/GetMessages.feature
index d58c6a1..d65a73e 100644
--- a/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/resources/cucumber/GetMessages.feature
+++ b/server/protocols/jmap-draft-integration-testing/jmap-draft-integration-testing-common/src/test/resources/cucumber/GetMessages.feature
@@ -445,8 +445,8 @@ Feature: GetMessages method
     And the hasAttachment of the message is "true"
     And the list of attachments of the message contains 1 attachments
     And the first attachment is:
-    |key      | value                        |
-    |type     |"text/calendar"               |
-    |size     |1096                          |
-    |name     |"event.ics"                   |
-    |isInline |false                         |
+    |key      | value                         |
+    |type     |"text/calendar; charset=UTF-8" |
+    |size     |1096                           |
+    |name     |"event.ics"                    |
+    |isInline |false                          |
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/MIMEMessageConverter.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/MIMEMessageConverter.java
index d5690f8..4864f05 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/MIMEMessageConverter.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/MIMEMessageConverter.java
@@ -69,7 +69,6 @@ import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.Splitter;
 import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.ImmutableMap.Builder;
 import com.google.common.io.ByteStreams;
 import com.google.common.net.MediaType;
 
@@ -329,25 +328,22 @@ public class MIMEMessageConverter {
     }
 
     private ContentTypeField contentTypeField(MessageAttachmentMetadata att) {
-        Builder<String, String> parameters = ImmutableMap.builder();
-        if (att.getName().isPresent()) {
-            parameters.put("name", encode(att.getName().get()));
-        }
         String type = att.getAttachment().getType();
-        if (type.contains(FIELD_PARAMETERS_SEPARATOR)) {
-            return Fields.contentType(contentTypeWithoutParameters(type), parameters.build());
+        ContentTypeField typeAsField = Fields.contentType(type);
+        if (att.getName().isPresent()) {
+            return Fields.contentType(typeAsField.getMimeType(),
+                ImmutableMap.<String, String>builder()
+                    .putAll(typeAsField.getParameters())
+                    .put("name", encode(att.getName().get()))
+                    .build());
         }
-        return Fields.contentType(type, parameters.build());
+        return typeAsField;
     }
 
     private String encode(String name) {
         return EncoderUtil.encodeEncodedWord(name, Usage.TEXT_TOKEN);
     }
 
-    private String contentTypeWithoutParameters(String type) {
-        return Splitter.on(FIELD_PARAMETERS_SEPARATOR).splitToList(type).get(0);
-    }
-
     private ContentDispositionField contentDispositionField(boolean isInline) {
         if (isInline) {
             return Fields.contentDisposition("inline");
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/MIMEMessageConverterTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/MIMEMessageConverterTest.java
index efca590..6cff89c 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/MIMEMessageConverterTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/MIMEMessageConverterTest.java
@@ -646,6 +646,54 @@ class MIMEMessageConverterTest {
         }
 
         @Test
+        void convertToMimeShouldPreservePartCharset() throws Exception {
+            // Given
+            MIMEMessageConverter sut = new MIMEMessageConverter(attachmentContentLoader);
+
+            CreationMessage testMessage = CreationMessage.builder()
+                .mailboxId("dead-bada55")
+                .subject("subject")
+                .from(DraftEmailer.builder().name("sender").build())
+                .htmlBody("Hello <b>all<b>!")
+                .build();
+
+            String expectedCID = "cid";
+            String expectedMimeType = "text/calendar; charset=\"iso-8859-1\"";
+            String text = "123456";
+            TextBody expectedBody = new BasicBodyFactory().textBody(text.getBytes(), StandardCharsets.UTF_8);
+            AttachmentId blodId = AttachmentId.from("blodId");
+            MessageAttachmentMetadata attachment = MessageAttachmentMetadata.builder()
+                .attachment(AttachmentMetadata.builder()
+                    .attachmentId(blodId)
+                    .size(text.getBytes().length)
+                    .type(expectedMimeType)
+                    .build())
+                .cid(Cid.from(expectedCID))
+                .isInline(true)
+                .build();
+            when(attachmentContentLoader.load(attachment.getAttachment(), session))
+                .thenReturn(new ByteArrayInputStream(text.getBytes()));
+
+            // When
+            Message result = sut.convertToMime(new ValueWithId.CreationMessageEntry(
+                CreationMessageId.of("user|mailbox|1"), testMessage), ImmutableList.of(attachment), session);
+            Multipart typedResult = (Multipart)result.getBody();
+
+            assertThat(typedResult.getBodyParts())
+                .hasSize(1)
+                .extracting(entity -> (Multipart) entity.getBody())
+                .flatExtracting(Multipart::getBodyParts)
+                .anySatisfy(part -> {
+                    assertThat(part.getBody()).isEqualToComparingOnlyGivenFields(expectedBody, "content");
+                    assertThat(part.getDispositionType()).isEqualTo("inline");
+                    assertThat(part.getMimeType()).isEqualTo("text/calendar");
+                    assertThat(part.getCharset()).isEqualTo("iso-8859-1");
+                    assertThat(part.getHeader().getField("Content-ID").getBody()).isEqualTo(expectedCID);
+                    assertThat(part.getContentTransferEncoding()).isEqualTo("base64");
+                });
+        }
+
+        @Test
         void convertToMimeShouldAddAttachmentAndMultipartAlternativeWhenOneAttachementAndTextAndHtmlBody() throws Exception {
             // Given
             MIMEMessageConverter sut = new MIMEMessageConverter(attachmentContentLoader);


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