Skip to content

Commit

Permalink
JAMES-1793 Generate MultiPart response when mixed text and html conte…
Browse files Browse the repository at this point in the history
…nt provided
  • Loading branch information
Raphael Ouazana committed Jul 6, 2016
1 parent 16c2593 commit 812b912
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 10 deletions.
Expand Up @@ -1354,6 +1354,68 @@ private boolean isHtmlMessageReceived(AccessToken recipientToken) {
}
}

@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";
Expand Down
Expand Up @@ -27,29 +27,40 @@
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;
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;

Expand All @@ -75,7 +86,11 @@ public byte[] convert(MessageWithId.CreationMessageEntry creationMessageEntry) {
}

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();
}
Expand Down Expand Up @@ -106,7 +121,9 @@ private void buildMimeHeaders(MessageBuilder messageBuilder, CreationMessageId c
// 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) {
Expand All @@ -117,16 +134,37 @@ private Consumer<String> addInReplyToHeader(Consumer<Field> headerAppender) {
};
}

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");
Expand Down
Expand Up @@ -26,12 +26,12 @@
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;
Expand Down Expand Up @@ -176,8 +176,8 @@ public void convertToMimeShouldSetHtmlBodyWhenProvided() {
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();

Expand All @@ -190,8 +190,48 @@ public void convertToMimeShouldFailWhenHtmlBodyAndTxtBodyProvided() {
.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
Expand Down

0 comments on commit 812b912

Please sign in to comment.