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 ma...@apache.org on 2017/09/27 08:00:13 UTC
[4/6] james-project git commit: JAMES-2160 Handle JMAP headers
provided at creation time
JAMES-2160 Handle JMAP headers provided at creation time
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/93cef1dd
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/93cef1dd
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/93cef1dd
Branch: refs/heads/master
Commit: 93cef1ddd0ef1deb8092710f473cacc2e5328e50
Parents: 0e5872c
Author: Raphael Ouazana <ra...@linagora.com>
Authored: Mon Sep 25 15:22:13 2017 +0200
Committer: Matthieu Baechler <ma...@apache.org>
Committed: Wed Sep 27 09:58:16 2017 +0200
----------------------------------------------------------------------
.../integration/SetMessagesMethodTest.java | 151 +++++++++++++++++++
.../jmap/methods/MIMEMessageConverter.java | 40 ++++-
.../jmap/methods/MIMEMessageConverterTest.java | 138 +++++++++++++++++
3 files changed, 321 insertions(+), 8 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/james-project/blob/93cef1dd/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 f22b2de..f22472b 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
@@ -3375,6 +3375,157 @@ public abstract class SetMessagesMethodTest {
}
@Test
+ public void setMessagesShouldSetUserAddedHeaders() throws Exception {
+ String messageCreationId = "creationId1337";
+ String requestBody = "[" +
+ " [" +
+ " \"setMessages\","+
+ " {" +
+ " \"create\": { \"" + messageCreationId + "\" : {" +
+ " \"from\": { \"name\": \"Me\", \"email\": \"" + USERNAME + "\"}," +
+ " \"to\": [{ \"name\": \"Me\", \"email\": \"" + USERNAME + "\"}]," +
+ " \"headers\": { \"X-MY-SPECIAL-HEADER\": \"first header value\", \"OTHER-HEADER\": \"other value\"}," +
+ " \"subject\": \"Thank you for joining example.com!\"," +
+ " \"textBody\": \"Hello someone, and thank you for joining example.com!\"," +
+ " \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+ " }}" +
+ " }," +
+ " \"#0\"" +
+ " ]" +
+ "]";
+
+ String messageId = given()
+ .header("Authorization", accessToken.serialize())
+ .body(requestBody)
+ // When
+ .post("/jmap")
+ .then()
+ .extract()
+ .body()
+ .<String>path(ARGUMENTS + ".created."+ messageCreationId +".id");
+
+ calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isAnyMessageFoundInInbox(accessToken));
+
+ String message = ARGUMENTS + ".list[0]";
+
+ with()
+ .header("Authorization", accessToken.serialize())
+ .body("[[\"getMessages\", {\"ids\": [\"" + messageId + "\"]}, \"#0\"]]")
+ .when()
+ .post("/jmap")
+ .then()
+ .statusCode(200)
+ .log().ifValidationFails()
+ .body(NAME, equalTo("messages"))
+ .body(ARGUMENTS + ".list", hasSize(1))
+ .body(message + ".headers", Matchers.allOf(
+ hasEntry("X-MY-SPECIAL-HEADER", "first header value"),
+ hasEntry("OTHER-HEADER", "other value")));
+ }
+
+ @Test
+ public void setMessagesShouldSetUserAddedHeadersInSent() throws Exception {
+ String toUsername = "username1@" + USERS_DOMAIN;
+ String password = "password";
+ dataProbe.addUser(toUsername, password);
+ mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, toUsername, DefaultMailboxes.INBOX);
+
+ String messageCreationId = "creationId1337";
+ String fromAddress = USERNAME;
+ String requestBody = "[" +
+ " [" +
+ " \"setMessages\","+
+ " {" +
+ " \"create\": { \"" + messageCreationId + "\" : {" +
+ " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+ " \"to\": [{ \"name\": \"BOB\", \"email\": \"" + toUsername + "\"}]," +
+ " \"headers\": { \"X-MY-SPECIAL-HEADER\": \"first header value\", \"OTHER-HEADER\": \"other value\"}," +
+ " \"subject\": \"Thank you for joining example.com!\"," +
+ " \"textBody\": \"Hello someone, and thank you for joining example.com!\"," +
+ " \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+ " }}" +
+ " }," +
+ " \"#0\"" +
+ " ]" +
+ "]";
+
+ given()
+ .header("Authorization", accessToken.serialize())
+ .body(requestBody)
+ .post("/jmap");
+
+ String sentMailboxId = getMailboxId(accessToken, Role.SENT);
+
+ calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> messageHasBeenMovedToSentBox(sentMailboxId));
+
+ String message = SECOND_ARGUMENTS + ".list[0]";
+ with()
+ .header("Authorization", this.accessToken.serialize())
+ .body("[[\"getMessageList\", {\"fetchMessages\":true, \"fetchMessageProperties\": [\"headers\"], \"filter\":{\"inMailboxes\":[\"" + sentMailboxId + "\"]}}, \"#0\"]]")
+ .when()
+ .post("/jmap")
+ .then()
+ .log().ifValidationFails()
+ .statusCode(200)
+ .body(SECOND_NAME, equalTo("messages"))
+ .body(SECOND_ARGUMENTS + ".list", hasSize(1))
+ .body(message + ".headers", Matchers.allOf(
+ hasEntry("X-MY-SPECIAL-HEADER", "first header value"),
+ hasEntry("OTHER-HEADER", "other value")));
+ }
+
+ @Test
+ public void setMessagesShouldFilterComputedHeadersFromUserAddedHeaders() throws Exception {
+ String messageCreationId = "creationId1337";
+ String requestBody = "[" +
+ " [" +
+ " \"setMessages\","+
+ " {" +
+ " \"create\": { \"" + messageCreationId + "\" : {" +
+ " \"from\": { \"name\": \"Me\", \"email\": \"" + USERNAME + "\"}," +
+ " \"to\": [{ \"name\": \"Me\", \"email\": \"" + USERNAME + "\"}]," +
+ " \"headers\": { \"From\": \"hacker@example.com\", \"X-MY-SPECIAL-HEADER\": \"first header value\", \"OTHER-HEADER\": \"other value\"}," +
+ " \"subject\": \"Thank you for joining example.com!\"," +
+ " \"textBody\": \"Hello someone, and thank you for joining example.com!\"," +
+ " \"mailboxIds\": [\"" + getOutboxId(accessToken) + "\"]" +
+ " }}" +
+ " }," +
+ " \"#0\"" +
+ " ]" +
+ "]";
+
+ String messageId = with()
+ .header("Authorization", accessToken.serialize())
+ .body(requestBody)
+ // When
+ .post("/jmap")
+ .then()
+ .extract()
+ .body()
+ .<String>path(ARGUMENTS + ".created."+ messageCreationId +".id");
+
+ calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isAnyMessageFoundInInbox(accessToken));
+
+ String message = ARGUMENTS + ".list[0]";
+
+ given()
+ .header("Authorization", accessToken.serialize())
+ .body("[[\"getMessages\", {\"ids\": [\"" + messageId + "\"]}, \"#0\"]]")
+ .when()
+ .post("/jmap")
+ .then()
+ .statusCode(200)
+ .log().ifValidationFails()
+ .body(NAME, equalTo("messages"))
+ .body(ARGUMENTS + ".list", hasSize(1))
+ .body(message + ".headers", Matchers.allOf(
+ hasEntry("X-MY-SPECIAL-HEADER", "first header value"),
+ hasEntry("OTHER-HEADER", "other value"),
+ not(hasEntry("From", "hacker@example.com")),
+ hasEntry("From", "Me <" + USERNAME + ">")));
+ }
+
+ @Test
public void setMessagesShouldCreateMessageWhenSendingMessageWithNonIndexableAttachment() throws Exception {
Attachment nonIndexableAttachment = Attachment.builder()
.bytes(IOUtils.toByteArray(ClassLoader.getSystemResourceAsStream("attachment/nonIndexableAttachment.html")))
http://git-wip-us.apache.org/repos/asf/james-project/blob/93cef1dd/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 4fdecf1..c964c7d 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
@@ -22,6 +22,8 @@ package org.apache.james.jmap.methods;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Date;
+import java.util.List;
+import java.util.Locale;
import java.util.Optional;
import java.util.TimeZone;
import java.util.function.Consumer;
@@ -42,6 +44,7 @@ import org.apache.james.mime4j.dom.TextBody;
import org.apache.james.mime4j.dom.address.Mailbox;
import org.apache.james.mime4j.dom.field.ContentDispositionField;
import org.apache.james.mime4j.dom.field.ContentTypeField;
+import org.apache.james.mime4j.dom.field.FieldName;
import org.apache.james.mime4j.dom.field.UnstructuredField;
import org.apache.james.mime4j.field.Fields;
import org.apache.james.mime4j.field.UnstructuredFieldImpl;
@@ -50,12 +53,12 @@ import org.apache.james.mime4j.message.BodyPart;
import org.apache.james.mime4j.message.BodyPartBuilder;
import org.apache.james.mime4j.message.DefaultMessageWriter;
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.github.steveash.guavate.Guavate;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Splitter;
import com.google.common.base.Throwables;
@@ -77,6 +80,24 @@ public class MIMEMessageConverter {
private static final String FIELD_PARAMETERS_SEPARATOR = ";";
private static final String QUOTED_PRINTABLE = "quoted-printable";
private static final String BASE64 = "base64";
+ private static final String IN_REPLY_TO_HEADER = "In-Reply-To";
+ private static final List<String> COMPUTED_HEADERS = ImmutableList.of(
+ FieldName.FROM,
+ FieldName.SENDER,
+ FieldName.REPLY_TO,
+ FieldName.TO,
+ FieldName.CC,
+ FieldName.BCC,
+ FieldName.SUBJECT,
+ FieldName.MESSAGE_ID,
+ FieldName.DATE,
+ IN_REPLY_TO_HEADER,
+ FieldName.CONTENT_TYPE,
+ FieldName.MIME_VERSION,
+ FieldName.CONTENT_TRANSFER_ENCODING);
+ private static final List<String> LOWERCASED_COMPUTED_HEADERS = COMPUTED_HEADERS.stream()
+ .map(s -> s.toLowerCase(Locale.ENGLISH))
+ .collect(Guavate.toImmutableList());
private final BasicBodyFactory bodyFactory;
@@ -137,18 +158,21 @@ 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.getInReplyToMessageId()
+ .ifPresent(id -> addHeader(messageBuilder, IN_REPLY_TO_HEADER, id));
if (!isMultipart(newMessage, messageAttachments)) {
newMessage.getHtmlBody().ifPresent(x -> messageBuilder.setContentType(HTML_MEDIA_TYPE, UTF_8_CHARSET));
}
+ newMessage.getHeaders().entrySet().stream()
+ .filter(header -> ! header.getKey().trim().isEmpty())
+ .filter(header -> ! LOWERCASED_COMPUTED_HEADERS.contains(header.getKey().toLowerCase(Locale.ENGLISH)))
+ .forEach(header -> addHeader(messageBuilder, header.getKey(), header.getValue()));
}
- private Consumer<String> addInReplyToHeader(Consumer<Field> headerAppender) {
- return msgId -> {
- FieldParser<UnstructuredField> parser = UnstructuredFieldImpl.PARSER;
- RawField rawField = new RawField("In-Reply-To", msgId);
- headerAppender.accept(parser.parse(rawField, DecodeMonitor.SILENT));
- };
+ private void addHeader(Message.Builder messageBuilder, String fieldName, String value) {
+ FieldParser<UnstructuredField> parser = UnstructuredFieldImpl.PARSER;
+ RawField rawField = new RawField(fieldName, value);
+ messageBuilder.addField(parser.parse(rawField, DecodeMonitor.SILENT));
}
private boolean isMultipart(CreationMessage newMessage, ImmutableList<MessageAttachment> messageAttachments) {
http://git-wip-us.apache.org/repos/asf/james-project/blob/93cef1dd/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 6fbb98d..9e9a2cf 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
@@ -47,6 +47,7 @@ import org.apache.james.mime4j.stream.Field;
import org.junit.Test;
import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
public class MIMEMessageConverterTest {
@Test
@@ -71,6 +72,143 @@ public class MIMEMessageConverterTest {
.containsOnly(matchingMessageId);
}
+ @Test
+ public void convertToMimeShouldAddHeaderWhenProvided() {
+ // Given
+ MIMEMessageConverter sut = new MIMEMessageConverter();
+
+ CreationMessage messageHavingInReplyTo = CreationMessage.builder()
+ .from(DraftEmailer.builder().name("sender").build())
+ .headers(ImmutableMap.of("FIRST", "first value"))
+ .mailboxIds(ImmutableList.of("dead-beef-1337"))
+ .subject("subject")
+ .build();
+
+ // When
+ Message result = sut.convertToMime(new ValueWithId.CreationMessageEntry(
+ CreationMessageId.of("user|mailbox|1"), messageHavingInReplyTo), ImmutableList.of());
+
+ // Then
+ assertThat(result.getHeader().getFields("FIRST")).extracting(Field::getBody)
+ .containsOnly("first value");
+ }
+
+ @Test
+ public void convertToMimeShouldAddHeadersWhenProvided() {
+ // Given
+ MIMEMessageConverter sut = new MIMEMessageConverter();
+
+ CreationMessage messageHavingInReplyTo = CreationMessage.builder()
+ .from(DraftEmailer.builder().name("sender").build())
+ .headers(ImmutableMap.of("FIRST", "first value", "SECOND", "second value"))
+ .mailboxIds(ImmutableList.of("dead-beef-1337"))
+ .subject("subject")
+ .build();
+
+ // When
+ Message result = sut.convertToMime(new ValueWithId.CreationMessageEntry(
+ CreationMessageId.of("user|mailbox|1"), messageHavingInReplyTo), ImmutableList.of());
+
+ // Then
+ assertThat(result.getHeader().getFields("FIRST")).extracting(Field::getBody)
+ .containsOnly("first value");
+ assertThat(result.getHeader().getFields("SECOND")).extracting(Field::getBody)
+ .containsOnly("second value");
+ }
+
+ @Test
+ public void convertToMimeShouldFilterGeneratedHeadersWhenProvided() {
+ // Given
+ MIMEMessageConverter sut = new MIMEMessageConverter();
+
+ String joesEmail = "joe@example.com";
+ CreationMessage messageHavingInReplyTo = CreationMessage.builder()
+ .from(DraftEmailer.builder().email(joesEmail).name("joe").build())
+ .headers(ImmutableMap.of("From", "hacker@example.com", "VALID", "valid header value"))
+ .mailboxIds(ImmutableList.of("dead-beef-1337"))
+ .subject("subject")
+ .build();
+
+ // When
+ Message result = sut.convertToMime(new ValueWithId.CreationMessageEntry(
+ CreationMessageId.of("user|mailbox|1"), messageHavingInReplyTo), ImmutableList.of());
+
+ // Then
+ assertThat(result.getFrom()).extracting(Mailbox::getAddress)
+ .allMatch(f -> f.equals(joesEmail));
+ assertThat(result.getHeader().getFields("VALID")).extracting(Field::getBody)
+ .containsOnly("valid header value");
+ assertThat(result.getHeader().getFields("From")).extracting(Field::getBody)
+ .containsOnly("joe <jo...@example.com>");
+ }
+
+ @Test
+ public void convertToMimeShouldFilterGeneratedHeadersRegardlessOfCaseWhenProvided() {
+ // Given
+ MIMEMessageConverter sut = new MIMEMessageConverter();
+
+ String joesEmail = "joe@example.com";
+ CreationMessage messageHavingInReplyTo = CreationMessage.builder()
+ .from(DraftEmailer.builder().email(joesEmail).name("joe").build())
+ .headers(ImmutableMap.of("frOM", "hacker@example.com", "VALID", "valid header value"))
+ .mailboxIds(ImmutableList.of("dead-beef-1337"))
+ .subject("subject")
+ .build();
+
+ // When
+ Message result = sut.convertToMime(new ValueWithId.CreationMessageEntry(
+ CreationMessageId.of("user|mailbox|1"), messageHavingInReplyTo), ImmutableList.of());
+
+ // Then
+ assertThat(result.getFrom()).extracting(Mailbox::getAddress)
+ .allMatch(f -> f.equals(joesEmail));
+ assertThat(result.getHeader().getFields("VALID")).extracting(Field::getBody)
+ .containsOnly("valid header value");
+ assertThat(result.getHeader().getFields("From")).extracting(Field::getBody)
+ .containsOnly("joe <jo...@example.com>");
+ }
+
+ @Test
+ public void convertToMimeShouldFilterEmptyHeaderNames() {
+ // Given
+ MIMEMessageConverter sut = new MIMEMessageConverter();
+
+ CreationMessage messageHavingInReplyTo = CreationMessage.builder()
+ .from(DraftEmailer.builder().name("joe").build())
+ .headers(ImmutableMap.of("", "empty header name value"))
+ .mailboxIds(ImmutableList.of("dead-beef-1337"))
+ .subject("subject")
+ .build();
+
+ // When
+ Message result = sut.convertToMime(new ValueWithId.CreationMessageEntry(
+ CreationMessageId.of("user|mailbox|1"), messageHavingInReplyTo), ImmutableList.of());
+
+ // Then
+ assertThat(result.getHeader().getFields("")).isEmpty();
+ }
+
+ @Test
+ public void convertToMimeShouldFilterWhiteSpacesOnlyHeaderNames() {
+ // Given
+ MIMEMessageConverter sut = new MIMEMessageConverter();
+
+ CreationMessage messageHavingInReplyTo = CreationMessage.builder()
+ .from(DraftEmailer.builder().name("joe").build())
+ .headers(ImmutableMap.of(" ", "only spaces header name values"))
+ .mailboxIds(ImmutableList.of("dead-beef-1337"))
+ .subject("subject")
+ .build();
+
+ // When
+ Message result = sut.convertToMime(new ValueWithId.CreationMessageEntry(
+ CreationMessageId.of("user|mailbox|1"), messageHavingInReplyTo), ImmutableList.of());
+
+ // Then
+ assertThat(result.getHeader().getFields(" ")).isEmpty();
+ assertThat(result.getHeader().getFields("")).isEmpty();
+ }
+
@Test(expected = IllegalArgumentException.class)
public void convertToMimeShouldThrowWhenMessageIsNull() {
MIMEMessageConverter sut = new MIMEMessageConverter();
---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org