JAMES-1793 Generate MultiPart response when mixed text and html content provided
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/812b9129 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/812b9129 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/812b9129 Branch: refs/heads/master Commit: 812b9129f8d09ade820e8d966285889ab85c8c28 Parents: 16c2593 Author: Raphael Ouazana <[email protected]> Authored: Wed Jul 6 18:45:31 2016 +0200 Committer: Raphael Ouazana <[email protected]> Committed: Wed Jul 6 18:45:31 2016 +0200 ---------------------------------------------------------------------- .../integration/SetMessagesMethodTest.java | 62 ++++++++++++++++++++ .../jmap/methods/MIMEMessageConverter.java | 50 ++++++++++++++-- .../jmap/methods/MIMEMessageConverterTest.java | 48 +++++++++++++-- 3 files changed, 150 insertions(+), 10 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/812b9129/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java index 1b6dab5..f6c18b1 100644 --- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java +++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java @@ -1355,6 +1355,68 @@ public abstract class SetMessagesMethodTest { } @Test + public void setMessagesShouldSendAReadableTextPlusHtmlMessage() throws Exception { + // Sender + jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "sent"); + // Recipient + String recipientAddress = "recipient" + "@" + USERS_DOMAIN; + String password = "password"; + jmapServer.serverProbe().addUser(recipientAddress, password); + jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, recipientAddress, "inbox"); + await(); + AccessToken recipientToken = JmapAuthentication.authenticateJamesUser(recipientAddress, password); + + String messageCreationId = "user|inbox|1"; + String fromAddress = username; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"" + recipientAddress + "\"}]," + + " \"subject\": \"Thank you for joining example.com!\"," + + " \"htmlBody\": \"Hello <b>someone</b>, and thank you for joining example.com!\"," + + " \"textBody\": \"Hello someone, and thank you for joining example.com, text version!\"," + + " \"mailboxIds\": [\"" + getOutboxId() + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + // Given + given() + .header("Authorization", this.accessToken.serialize()) + .body(requestBody) + // When + .when() + .post("/jmap"); + + // Then + calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isTextPlusHtmlMessageReceived(recipientToken)); + } + + private boolean isTextPlusHtmlMessageReceived(AccessToken recipientToken) { + try { + with() + .header("Authorization", recipientToken.serialize()) + .body("[[\"getMessageList\", {\"fetchMessages\": true, \"fetchMessageProperties\": [\"htmlBody\", \"textBody\"]}, \"#0\"]]") + .post("/jmap") + .then() + .statusCode(200) + .body(SECOND_NAME, equalTo("messages")) + .body(SECOND_ARGUMENTS + ".list", hasSize(1)) + .body(SECOND_ARGUMENTS + ".list[0].htmlBody", equalTo("Hello <b>someone</b>, and thank you for joining example.com!")) + .body(SECOND_ARGUMENTS + ".list[0].textBody", equalTo("Hello someone, and thank you for joining example.com, text version!")) + ; + return true; + } catch(AssertionError e) { + return false; + } + } + + @Test public void movingAMessageIsNotSupported() throws Exception { String newMailboxName = "heartFolder"; jmapServer.serverProbe().createMailbox("#private", username, newMailboxName); http://git-wip-us.apache.org/repos/asf/james-project/blob/812b9129/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java index 0240ccc..19e7c63 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java @@ -27,7 +27,6 @@ import java.util.TimeZone; import java.util.function.Consumer; import java.util.stream.Collectors; -import org.apache.commons.lang.NotImplementedException; import org.apache.james.jmap.model.CreationMessage; import org.apache.james.jmap.model.CreationMessage.DraftEmailer; import org.apache.james.jmap.model.CreationMessageId; @@ -35,21 +34,33 @@ import org.apache.james.mime4j.Charsets; import org.apache.james.mime4j.codec.DecodeMonitor; import org.apache.james.mime4j.dom.FieldParser; import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.dom.Multipart; import org.apache.james.mime4j.dom.TextBody; import org.apache.james.mime4j.dom.address.Mailbox; import org.apache.james.mime4j.dom.field.UnstructuredField; import org.apache.james.mime4j.field.UnstructuredFieldImpl; import org.apache.james.mime4j.message.BasicBodyFactory; +import org.apache.james.mime4j.message.BodyPartBuilder; import org.apache.james.mime4j.message.DefaultMessageWriter; import org.apache.james.mime4j.message.MessageBuilder; +import org.apache.james.mime4j.message.MultipartBuilder; import org.apache.james.mime4j.stream.Field; import org.apache.james.mime4j.stream.NameValuePair; import org.apache.james.mime4j.stream.RawField; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Throwables; +import com.google.common.net.MediaType; public class MIMEMessageConverter { + private static final Logger LOGGER = LoggerFactory.getLogger(MIMEMessageConverter.class); + + private static final String PLAIN_TEXT_MEDIA_TYPE = MediaType.PLAIN_TEXT_UTF_8.withoutParameters().toString(); + private static final String HTML_MEDIA_TYPE = MediaType.HTML_UTF_8.withoutParameters().toString(); + private static final NameValuePair UTF_8_CHARSET = new NameValuePair("charset", Charsets.UTF_8.name()); + private static final String MIXED_SUB_TYPE = "mixed"; private final BasicBodyFactory bodyFactory; @@ -75,7 +86,11 @@ public class MIMEMessageConverter { } MessageBuilder messageBuilder = MessageBuilder.create(); - messageBuilder.setBody(createTextBody(creationMessageEntry.getMessage())); + if (mixedTextAndHtml(creationMessageEntry.getMessage())) { + messageBuilder.setBody(createMultipartBody(creationMessageEntry.getMessage())); + } else { + messageBuilder.setBody(createTextBody(creationMessageEntry.getMessage())); + } buildMimeHeaders(messageBuilder, creationMessageEntry.getCreationId(), creationMessageEntry.getMessage()); return messageBuilder.build(); } @@ -106,7 +121,9 @@ public class MIMEMessageConverter { // note that date conversion probably lose milliseconds! messageBuilder.setDate(Date.from(newMessage.getDate().toInstant()), TimeZone.getTimeZone(newMessage.getDate().getZone())); newMessage.getInReplyToMessageId().ifPresent(addInReplyToHeader(messageBuilder::addField)); - newMessage.getHtmlBody().ifPresent(x -> messageBuilder.setContentType("text/html", new NameValuePair("charset", "utf-8"))); + if (!mixedTextAndHtml(newMessage)) { + newMessage.getHtmlBody().ifPresent(x -> messageBuilder.setContentType(HTML_MEDIA_TYPE, UTF_8_CHARSET)); + } } private Consumer<String> addInReplyToHeader(Consumer<Field> headerAppender) { @@ -117,16 +134,37 @@ public class MIMEMessageConverter { }; } + private boolean mixedTextAndHtml(CreationMessage newMessage) { + return newMessage.getTextBody().isPresent() && newMessage.getHtmlBody().isPresent(); + } + private TextBody createTextBody(CreationMessage newMessage) { - if (newMessage.getTextBody().isPresent() && newMessage.getHtmlBody().isPresent()) { - throw new NotImplementedException("Converter can't handle yet htmlBody and textBody in the same message"); - } String body = newMessage.getHtmlBody() .orElse(newMessage.getTextBody() .orElse("")); return bodyFactory.textBody(body, Charsets.UTF_8); } + private Multipart createMultipartBody(CreationMessage newMessage) { + try { + return MultipartBuilder.create(MIXED_SUB_TYPE) + .addBodyPart(BodyPartBuilder.create() + .use(bodyFactory) + .setBody(newMessage.getTextBody().get(), Charsets.UTF_8) + .setContentType(PLAIN_TEXT_MEDIA_TYPE, UTF_8_CHARSET) + .build()) + .addBodyPart(BodyPartBuilder.create() + .use(bodyFactory) + .setBody(newMessage.getHtmlBody().get(), Charsets.UTF_8) + .setContentType(HTML_MEDIA_TYPE, UTF_8_CHARSET) + .build()) + .build(); + } catch (IOException e) { + LOGGER.error("Error while creating textBody \n"+ newMessage.getTextBody().get() +"\n or htmlBody \n" + newMessage.getHtmlBody().get(), e); + throw Throwables.propagate(e); + } + } + private Mailbox convertEmailToMimeHeader(DraftEmailer address) { if (!address.hasValidEmail()) { throw new IllegalArgumentException("address"); http://git-wip-us.apache.org/repos/asf/james-project/blob/812b9129/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/MIMEMessageConverterTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/MIMEMessageConverterTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/MIMEMessageConverterTest.java index c9fb01d..c2cf064 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/MIMEMessageConverterTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/MIMEMessageConverterTest.java @@ -26,12 +26,12 @@ import java.time.Instant; import java.time.ZoneId; import java.time.ZonedDateTime; -import org.apache.commons.lang.NotImplementedException; import org.apache.james.jmap.model.CreationMessage; import org.apache.james.jmap.model.CreationMessage.DraftEmailer; import org.apache.james.jmap.model.CreationMessageId; import org.apache.james.mime4j.Charsets; import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.dom.Multipart; import org.apache.james.mime4j.dom.TextBody; import org.apache.james.mime4j.dom.address.Mailbox; import org.apache.james.mime4j.message.BasicBodyFactory; @@ -176,8 +176,8 @@ public class MIMEMessageConverterTest { assertThat(result.getBody()).isEqualToComparingOnlyGivenFields(expected, "content", "charset"); } - @Test(expected=NotImplementedException.class) - public void convertToMimeShouldFailWhenHtmlBodyAndTxtBodyProvided() { + @Test + public void convertToMimeShouldGenerateMultipartWhenHtmlBodyAndTextBodyProvided() throws Exception { // Given MIMEMessageConverter sut = new MIMEMessageConverter(); @@ -190,8 +190,48 @@ public class MIMEMessageConverterTest { .build(); // When - sut.convertToMime(new MessageWithId.CreationMessageEntry( + Message result = sut.convertToMime(new MessageWithId.CreationMessageEntry( CreationMessageId.of("user|mailbox|1"), testMessage)); + + // Then + assertThat(result.getBody()).isInstanceOf(Multipart.class); + assertThat(result.isMultipart()).isTrue(); + Multipart typedResult = (Multipart)result.getBody(); + assertThat(typedResult.getBodyParts()).hasSize(2); + } + + @Test + public void convertShouldGenerateExpectedMultipartWhenHtmlAndTextBodyProvided() throws Exception { + // Given + MIMEMessageConverter sut = new MIMEMessageConverter(); + + CreationMessage testMessage = CreationMessage.builder() + .mailboxIds(ImmutableList.of("dead-bada55")) + .subject("subject") + .from(DraftEmailer.builder().name("sender").build()) + .textBody("Hello all!") + .htmlBody("Hello <b>all</b>!") + .build(); + + String expectedHeaders = "MIME-Version: 1.0\r\n" + + "Content-Type: multipart/mixed;\r\n" + + " boundary=\"-=Part.0."; + String expectedPart1 = "Content-Type: text/plain; charset=UTF-8\r\n" + + "\r\n" + + "Hello all!\r\n"; + String expectedPart2 = "Content-Type: text/html; charset=UTF-8\r\n" + + "\r\n" + + "Hello <b>all</b>!\r\n"; + + // When + byte[] convert = sut.convert(new MessageWithId.CreationMessageEntry( + CreationMessageId.of("user|mailbox|1"), testMessage)); + + // Then + String actual = new String(convert, Charsets.UTF_8); + assertThat(actual).startsWith(expectedHeaders); + assertThat(actual).contains(expectedPart1); + assertThat(actual).contains(expectedPart2); } @Test --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
