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