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 rc...@apache.org on 2019/11/25 09:06:54 UTC

[james-project] 11/22: JAMES-2987 Add MessageHeaderViewFactory

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

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

commit d1a8c287a7c6264afeeaa12c336d4d5e342b30b0
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Thu Nov 21 17:45:56 2019 +0700

    JAMES-2987 Add MessageHeaderViewFactory
---
 .../jmap/draft/methods/MIMEMessageConverter.java   |   4 +-
 .../model/message/view/MessageFullViewFactory.java |  86 +------------
 .../model/message/view/MessageHeaderView.java      |   2 +-
 .../message/view/MessageHeaderViewFactory.java     |  94 ++++++++++++++
 .../model/message/view/MessageViewFactory.java     |  72 +++++++++++
 .../message/view/MessageHeaderViewFactoryTest.java | 141 +++++++++++++++++++++
 .../jmap-draft/src/test/resources/fullMessage.eml  |  60 +++++++++
 7 files changed, 376 insertions(+), 83 deletions(-)

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 c9e3dcd..eac8c13 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
@@ -31,7 +31,7 @@ import java.util.stream.Collectors;
 
 import org.apache.james.jmap.draft.model.CreationMessage;
 import org.apache.james.jmap.draft.model.CreationMessage.DraftEmailer;
-import org.apache.james.jmap.draft.model.message.view.MessageFullViewFactory;
+import org.apache.james.jmap.draft.model.message.view.MessageViewFactory;
 import org.apache.james.mailbox.model.MessageAttachment;
 import org.apache.james.mime4j.codec.DecodeMonitor;
 import org.apache.james.mime4j.codec.EncoderUtil;
@@ -176,7 +176,7 @@ public class MIMEMessageConverter {
     }
 
     private void addMultivaluedHeader(Message.Builder messageBuilder, String fieldName, String multipleValues) {
-        Splitter.on(MessageFullViewFactory.JMAP_MULTIVALUED_FIELD_DELIMITER).split(multipleValues)
+        Splitter.on(MessageViewFactory.JMAP_MULTIVALUED_FIELD_DELIMITER).split(multipleValues)
             .forEach(value -> addHeader(messageBuilder, fieldName, value));
     }
 
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFullViewFactory.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFullViewFactory.java
index ebef606..dec75d8 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFullViewFactory.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFullViewFactory.java
@@ -24,19 +24,14 @@ import java.time.Instant;
 import java.util.Collection;
 import java.util.Date;
 import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
 import java.util.Optional;
 import java.util.Set;
-import java.util.function.Function;
-import java.util.stream.Collectors;
 
 import javax.inject.Inject;
 import javax.mail.internet.SharedInputStream;
 
 import org.apache.james.jmap.draft.model.Attachment;
 import org.apache.james.jmap.draft.model.BlobId;
-import org.apache.james.jmap.draft.model.Emailer;
 import org.apache.james.jmap.draft.model.Keywords;
 import org.apache.james.jmap.draft.model.MessagePreviewGenerator;
 import org.apache.james.jmap.draft.utils.HtmlTextExtractor;
@@ -48,31 +43,21 @@ import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MessageAttachment;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.model.MessageResult;
-import org.apache.james.mime4j.dom.address.AddressList;
-import org.apache.james.mime4j.dom.address.Mailbox;
-import org.apache.james.mime4j.dom.address.MailboxList;
-import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.dom.Message;
 import org.apache.james.mime4j.stream.MimeConfig;
-import org.apache.james.mime4j.util.MimeUtil;
 import org.apache.james.util.mime.MessageContentExtractor;
 import org.apache.james.util.mime.MessageContentExtractor.MessageContent;
 
 import com.github.steveash.guavate.Guavate;
 import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableMap;
-import com.google.common.collect.Multimaps;
 import com.google.common.collect.Sets;
 
 public class MessageFullViewFactory implements MessageViewFactory<MessageFullView> {
-    public static final String JMAP_MULTIVALUED_FIELD_DELIMITER = "\n";
-
     private final BlobManager blobManager;
     private final MessagePreviewGenerator messagePreview;
     private final MessageContentExtractor messageContentExtractor;
     private final HtmlTextExtractor htmlTextExtractor;
-    private final Keywords.KeywordsFactory keywordsFactory;
 
     @Inject
     public MessageFullViewFactory(BlobManager blobManager, MessagePreviewGenerator messagePreview, MessageContentExtractor messageContentExtractor,
@@ -81,7 +66,6 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
         this.messagePreview = messagePreview;
         this.messageContentExtractor = messageContentExtractor;
         this.htmlTextExtractor = htmlTextExtractor;
-        this.keywordsFactory = Keywords.lenientFactory();
     }
 
     @Override
@@ -90,7 +74,7 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
     }
 
     public MessageFullView fromMetaDataWithContent(MetaDataWithContent message) throws MailboxException {
-        org.apache.james.mime4j.dom.Message mimeMessage = parse(message);
+        Message mimeMessage = parse(message);
         MessageContent messageContent = extractContent(mimeMessage);
         Optional<String> htmlBody = messageContent.getHtmlBody();
         Optional<String> mainTextContent = mainTextContent(messageContent);
@@ -133,7 +117,7 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
             .build();
     }
 
-    private Instant getDateFromHeaderOrInternalDateOtherwise(org.apache.james.mime4j.dom.Message mimeMessage, MetaDataWithContent message) {
+    private Instant getDateFromHeaderOrInternalDateOtherwise(Message mimeMessage, MetaDataWithContent message) {
         return Optional.ofNullable(mimeMessage.getDate())
             .map(Date::toInstant)
             .orElse(message.getInternalDate());
@@ -153,9 +137,9 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
             .orElse(messageContent.getTextBody());
     }
 
-    private org.apache.james.mime4j.dom.Message parse(MetaDataWithContent message) throws MailboxException {
+    private Message parse(MetaDataWithContent message) throws MailboxException {
         try {
-            return org.apache.james.mime4j.dom.Message.Builder
+            return Message.Builder
                     .of()
                     .use(MimeConfig.PERMISSIVE)
                     .parse(message.getContent())
@@ -165,71 +149,13 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
         }
     }
 
-    private MessageContent extractContent(org.apache.james.mime4j.dom.Message mimeMessage) throws MailboxException {
+    private MessageContent extractContent(Message mimeMessage) throws MailboxException {
         try {
             return messageContentExtractor.extract(mimeMessage);
         } catch (IOException e) {
             throw new MailboxException("Unable to extract content: " + e.getMessage(), e);
         }
     }
-
-    private Emailer firstFromMailboxList(MailboxList list) {
-        if (list == null) {
-            return null;
-        }
-        return list.stream()
-                .map(this::fromMailbox)
-                .findFirst()
-                .orElse(null);
-    }
-    
-    private ImmutableList<Emailer> fromAddressList(AddressList list) {
-        if (list == null) {
-            return ImmutableList.of();
-        }
-        return list.flatten()
-            .stream()
-            .map(this::fromMailbox)
-            .collect(Guavate.toImmutableList());
-    }
-    
-    private Emailer fromMailbox(Mailbox mailbox) {
-        return Emailer.builder()
-            .name(getNameOrAddress(mailbox))
-            .email(mailbox.getAddress())
-            .allowInvalid()
-            .build();
-    }
-
-    private String getNameOrAddress(Mailbox mailbox) {
-        if (mailbox.getName() != null) {
-            return mailbox.getName();
-        }
-        return mailbox.getAddress();
-    }
-
-    private ImmutableMap<String, String> toMap(List<Field> fields) {
-        Function<Entry<String, Collection<Field>>, String> bodyConcatenator = fieldListEntry -> fieldListEntry.getValue()
-                .stream()
-                .map(Field::getBody)
-                .map(MimeUtil::unscrambleHeaderValue)
-                .collect(Collectors.toList())
-                .stream()
-                .collect(Collectors.joining(JMAP_MULTIVALUED_FIELD_DELIMITER));
-        return Multimaps.index(fields, Field::getName)
-                .asMap()
-                .entrySet()
-                .stream()
-                .collect(Guavate.toImmutableMap(Map.Entry::getKey, bodyConcatenator));
-    }
-    
-    private String getHeader(org.apache.james.mime4j.dom.Message message, String header) {
-        Field field = message.getHeader().getField(header);
-        if (field == null) {
-            return null;
-        }
-        return field.getBody();
-    }
     
     private List<Attachment> getAttachments(List<MessageAttachment> attachments) {
         return attachments.stream()
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderView.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderView.java
index abb3a31..9ad4610 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderView.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderView.java
@@ -39,7 +39,7 @@ import com.google.common.collect.ImmutableMap;
 
 public class MessageHeaderView extends MessageMetadataView {
 
-    public static Builder messageHeaderBuilder() {
+    public static MessageHeaderView.Builder<? extends MessageHeaderView.Builder> messageHeaderBuilder() {
         return new Builder();
     }
 
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderViewFactory.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderViewFactory.java
new file mode 100644
index 0000000..6995b44
--- /dev/null
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderViewFactory.java
@@ -0,0 +1,94 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.draft.model.message.view;
+
+import java.io.IOException;
+import java.time.Instant;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.jmap.draft.model.BlobId;
+import org.apache.james.mailbox.BlobManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MessageResult;
+import org.apache.james.mime4j.dom.Message;
+import org.apache.james.mime4j.stream.MimeConfig;
+
+import com.google.common.base.Strings;
+
+public class MessageHeaderViewFactory implements MessageViewFactory<MessageHeaderView> {
+    private final BlobManager blobManager;
+
+    @Inject
+    MessageHeaderViewFactory(BlobManager blobManager) {
+        this.blobManager = blobManager;
+    }
+
+    @Override
+    public MessageHeaderView fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException {
+        assertOneMessageId(messageResults);
+
+        MessageResult firstMessageResult = messageResults.iterator().next();
+        List<MailboxId> mailboxIds = getMailboxIds(messageResults);
+
+        Message mimeMessage = parse(firstMessageResult);
+
+        return MessageHeaderView.messageHeaderBuilder()
+            .id(firstMessageResult.getMessageId())
+            .mailboxIds(mailboxIds)
+            .blobId(BlobId.of(blobManager.toBlobId(firstMessageResult.getMessageId())))
+            .threadId(firstMessageResult.getMessageId().serialize())
+            .keywords(getKeywords(messageResults))
+            .size(firstMessageResult.getSize())
+            .inReplyToMessageId(getHeader(mimeMessage, "in-reply-to"))
+            .subject(Strings.nullToEmpty(mimeMessage.getSubject()).trim())
+            .headers(toMap(mimeMessage.getHeader().getFields()))
+            .from(firstFromMailboxList(mimeMessage.getFrom()))
+            .to(fromAddressList(mimeMessage.getTo()))
+            .cc(fromAddressList(mimeMessage.getCc()))
+            .bcc(fromAddressList(mimeMessage.getBcc()))
+            .replyTo(fromAddressList(mimeMessage.getReplyTo()))
+            .date(getDateFromHeaderOrInternalDateOtherwise(mimeMessage, firstMessageResult))
+            .build();
+    }
+
+    private Message parse(MessageResult message) throws MailboxException {
+        try {
+            return Message.Builder
+                .of()
+                .use(MimeConfig.PERMISSIVE)
+                .parse(message.getFullContent().getInputStream())
+                .build();
+        } catch (IOException e) {
+            throw new MailboxException("Unable to parse message: " + e.getMessage(), e);
+        }
+    }
+
+    private Instant getDateFromHeaderOrInternalDateOtherwise(Message mimeMessage, MessageResult message) {
+        return Optional.ofNullable(mimeMessage.getDate())
+            .map(Date::toInstant)
+            .orElse(message.getInternalDate().toInstant());
+    }
+}
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageViewFactory.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageViewFactory.java
index 5339c13..4afb117 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageViewFactory.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageViewFactory.java
@@ -21,19 +21,32 @@ package org.apache.james.jmap.draft.model.message.view;
 
 import java.util.Collection;
 import java.util.List;
+import java.util.Map;
+import java.util.function.Function;
+import java.util.stream.Collectors;
 
+import org.apache.james.jmap.draft.model.Emailer;
 import org.apache.james.jmap.draft.model.Keywords;
 import org.apache.james.jmap.draft.utils.KeywordsCombiner;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MessageResult;
+import org.apache.james.mime4j.dom.address.AddressList;
+import org.apache.james.mime4j.dom.address.Mailbox;
+import org.apache.james.mime4j.dom.address.MailboxList;
+import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.util.MimeUtil;
 
 import com.github.steveash.guavate.Guavate;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Multimaps;
 
 public interface MessageViewFactory<T extends MessageView> {
     KeywordsCombiner KEYWORDS_COMBINER = new KeywordsCombiner();
     Keywords.KeywordsFactory KEYWORDS_FACTORY = Keywords.lenientFactory();
+    String JMAP_MULTIVALUED_FIELD_DELIMITER = "\n";
 
     T fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException;
 
@@ -64,4 +77,63 @@ public interface MessageViewFactory<T extends MessageView> {
                 .reduce(KEYWORDS_COMBINER)
                 .get();
     }
+
+    default String getHeader(org.apache.james.mime4j.dom.Message message, String header) {
+        Field field = message.getHeader().getField(header);
+        if (field == null) {
+            return null;
+        }
+        return field.getBody();
+    }
+
+    default ImmutableMap<String, String> toMap(List<Field> fields) {
+        Function<Map.Entry<String, Collection<Field>>, String> bodyConcatenator = fieldListEntry -> fieldListEntry.getValue()
+            .stream()
+            .map(Field::getBody)
+            .map(MimeUtil::unscrambleHeaderValue)
+            .collect(Collectors.toList())
+            .stream()
+            .collect(Collectors.joining(JMAP_MULTIVALUED_FIELD_DELIMITER));
+
+        return Multimaps.index(fields, Field::getName)
+            .asMap()
+            .entrySet()
+            .stream()
+            .collect(Guavate.toImmutableMap(Map.Entry::getKey, bodyConcatenator));
+    }
+
+    default Emailer firstFromMailboxList(MailboxList list) {
+        if (list == null) {
+            return null;
+        }
+        return list.stream()
+            .map(this::fromMailbox)
+            .findFirst()
+            .orElse(null);
+    }
+
+    default Emailer fromMailbox(Mailbox mailbox) {
+        return Emailer.builder()
+            .name(getNameOrAddress(mailbox))
+            .email(mailbox.getAddress())
+            .allowInvalid()
+            .build();
+    }
+
+    default String getNameOrAddress(Mailbox mailbox) {
+        if (mailbox.getName() != null) {
+            return mailbox.getName();
+        }
+        return mailbox.getAddress();
+    }
+
+    default ImmutableList<Emailer> fromAddressList(AddressList list) {
+        if (list == null) {
+            return ImmutableList.of();
+        }
+        return list.flatten()
+            .stream()
+            .map(this::fromMailbox)
+            .collect(Guavate.toImmutableList());
+    }
 }
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderViewFactoryTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderViewFactoryTest.java
new file mode 100644
index 0000000..a60c17f
--- /dev/null
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/model/message/view/MessageHeaderViewFactoryTest.java
@@ -0,0 +1,141 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.draft.model.message.view;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.mail.Flags;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.draft.model.BlobId;
+import org.apache.james.jmap.draft.model.Emailer;
+import org.apache.james.jmap.draft.model.Keyword;
+import org.apache.james.jmap.draft.model.Keywords;
+import org.apache.james.jmap.draft.model.Number;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
+import org.apache.james.mailbox.model.ComposedMessageId;
+import org.apache.james.mailbox.model.FetchGroupImpl;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.MessageRange;
+import org.apache.james.mailbox.model.MessageResult;
+import org.apache.james.util.ClassLoaderUtils;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
+class MessageHeaderViewFactoryTest {
+    private static final Username BOB = Username.of("bob@local");
+
+    private MessageIdManager messageIdManager;
+    private MessageHeaderViewFactory testee;
+    private MailboxSession session;
+    private MessageManager bobInbox;
+    private MessageManager bobMailbox;
+    private ComposedMessageId message1;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        InMemoryIntegrationResources resources = InMemoryIntegrationResources.defaultResources();
+        messageIdManager = resources.getMessageIdManager();
+        InMemoryMailboxManager mailboxManager = resources.getMailboxManager();
+
+        session = mailboxManager.createSystemSession(BOB);
+        MailboxId bobInboxId = mailboxManager.createMailbox(MailboxPath.inbox(session), session).get();
+        MailboxId bobMailboxId = mailboxManager.createMailbox(MailboxPath.forUser(BOB, "anotherMailbox"), session).get();
+
+        bobInbox = mailboxManager.getMailbox(bobInboxId, session);
+        bobMailbox = mailboxManager.getMailbox(bobMailboxId, session);
+
+        message1 = bobInbox.appendMessage(MessageManager.AppendCommand.builder()
+                .withFlags(new Flags(Flags.Flag.SEEN))
+                .build(ClassLoaderUtils.getSystemResourceAsSharedStream("fullMessage.eml")),
+            session);
+
+        testee = new MessageHeaderViewFactory(resources.getBlobManager());
+    }
+
+    @Test
+    void fromMessageResultsShouldReturnCorrectView() throws Exception {
+        List<MessageResult> messages = messageIdManager
+            .getMessages(ImmutableList.of(message1.getMessageId()), FetchGroupImpl.MINIMAL, session);
+
+        Emailer bobEmail = Emailer.builder().name(BOB.getLocalPart()).email(BOB.asString()).build();
+        Emailer aliceEmail = Emailer.builder().name("alice").email("alice@local").build();
+        Emailer jackEmail = Emailer.builder().name("jack").email("jack@local").build();
+        Emailer jacobEmail = Emailer.builder().name("jacob").email("jacob@local").build();
+
+        ImmutableMap<String, String> headersMap = ImmutableMap.<String, String>builder()
+            .put("Content-Type", "multipart/mixed; boundary=\"------------7AF1D14DE1DFA16229726B54\"")
+            .put("Date", "Tue, 7 Jun 2016 16:23:37 +0200")
+            .put("From", "alice <al...@local>")
+            .put("To", "bob <bo...@local>")
+            .put("Subject", "Full message")
+            .put("Mime-Version", "1.0")
+            .put("Message-ID", "<1c...@open-paas.org>")
+            .put("Cc", "jack <ja...@local>, jacob <ja...@local>")
+            .put("Bcc", "alice <al...@local>")
+            .put("Reply-to", "alice <al...@local>")
+            .put("In-reply-to", "bob@local")
+            .build();
+
+        MessageHeaderView actual = testee.fromMessageResults(messages);
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(actual.getId()).isEqualTo(message1.getMessageId());
+            softly.assertThat(actual.getMailboxIds()).containsExactly(bobInbox.getId());
+            softly.assertThat(actual.getThreadId()).isEqualTo(message1.getMessageId().serialize());
+            softly.assertThat(actual.getSize()).isEqualTo(Number.fromLong(2255));
+            softly.assertThat(actual.getKeywords()).isEqualTo(Keywords.strictFactory().from(Keyword.SEEN).asMap());
+            softly.assertThat(actual.getBlobId()).isEqualTo(BlobId.of(message1.getMessageId().serialize()));
+            softly.assertThat(actual.getInReplyToMessageId()).isEqualTo(Optional.of(BOB.asString()));
+            softly.assertThat(actual.getHeaders()).isEqualTo(headersMap);
+            softly.assertThat(actual.getFrom()).isEqualTo(Optional.of(aliceEmail));
+            softly.assertThat(actual.getTo()).isEqualTo(ImmutableList.of(bobEmail));
+            softly.assertThat(actual.getCc()).isEqualTo(ImmutableList.of(jackEmail, jacobEmail));
+            softly.assertThat(actual.getBcc()).isEqualTo(ImmutableList.of(aliceEmail));
+            softly.assertThat(actual.getReplyTo()).isEqualTo(ImmutableList.of(aliceEmail));
+            softly.assertThat(actual.getSubject()).isEqualTo("Full message");
+            softly.assertThat(actual.getDate()).isEqualTo("2016-06-07T14:23:37Z");
+        });
+    }
+
+    @Test
+    void fromMessageResultsShouldCombineKeywords() throws Exception {
+        messageIdManager.setInMailboxes(message1.getMessageId(), ImmutableList.of(bobInbox.getId(), bobMailbox.getId()), session);
+        bobMailbox.setFlags(new Flags(Flags.Flag.FLAGGED), MessageManager.FlagsUpdateMode.REPLACE, MessageRange.all(), session);
+
+        List<MessageResult> messages = messageIdManager
+            .getMessages(ImmutableList.of(message1.getMessageId()), FetchGroupImpl.MINIMAL, session);
+
+        MessageHeaderView actual = testee.fromMessageResults(messages);
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(actual.getId()).isEqualTo(message1.getMessageId());
+            softly.assertThat(actual.getKeywords()).isEqualTo(Keywords.strictFactory().from(Keyword.SEEN, Keyword.FLAGGED).asMap());
+        });
+    }
+}
diff --git a/server/protocols/jmap-draft/src/test/resources/fullMessage.eml b/server/protocols/jmap-draft/src/test/resources/fullMessage.eml
new file mode 100644
index 0000000..daf28d3
--- /dev/null
+++ b/server/protocols/jmap-draft/src/test/resources/fullMessage.eml
@@ -0,0 +1,60 @@
+Reply-to: alice <al...@local>
+In-reply-to: bob@local
+Mime-Version: 1.0
+To: bob <bo...@local>
+From: alice <al...@local>
+Cc: jack <ja...@local>, jacob <ja...@local>
+Bcc: alice <al...@local>
+Subject: Full message
+Message-ID: <1c...@open-paas.org>
+Date: Tue, 7 Jun 2016 16:23:37 +0200
+Content-Type: multipart/mixed;
+ boundary="------------7AF1D14DE1DFA16229726B54"
+
+This is a multi-part message in MIME format.
+--------------7AF1D14DE1DFA16229726B54
+Content-Type: multipart/alternative;
+ boundary="------------172F9470CFA3BF3417835D92"
+
+
+--------------172F9470CFA3BF3417835D92
+Content-Type: text/plain; charset=utf-8; format=flowed
+Content-Transfer-Encoding: 7bit
+
+/blabla/
+*bloblo*
+
+--------------172F9470CFA3BF3417835D92
+Content-Type: text/html; charset=utf-8
+Content-Transfer-Encoding: 7bit
+
+<i>blabla</i>
+<b>bloblo</b>
+
+--------------172F9470CFA3BF3417835D92--
+
+--------------7AF1D14DE1DFA16229726B54
+Content-Type: image/jpeg;
+ name="4037_014.jpg"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+ filename="4037_014.jpg"
+
+/9j/4X2cRXhpZgAASUkqAAgAAAANAA8BAgAKAAAAqgAAABABAgAJAAAAtAAAABIBAwABAAAA
+AQAAABoBBQABAAAAvgAAABsBBQABAAAAxgAAACgBAwABAAAAAgAAADEBAgAKAAAAzgAAADIB
+AgAUAAAA2AAAABMCAwABAAAAAgAAAGmHBAABAAAAfAIAAKXEBwDQAAAA7AAAANLGBwBAAAAA
+vAEAANPGBwCAAAAA/AEAAEwqAABQYW5hc29uaWMARE1DLUZaNDUAALQAAAABAAAAtAAAAAEA
+AABWZXIuMS4wICAAMjAxNDowMjoyNSAxMDozMjowOQBQcmludElNADAyNTAAAA4AAQAWABYA
+AgAAAAAAAwBkAAAABwAAAAAACAAAAAAACQAAAAAACgAAAAAACwCsAAAADAAAAAAADQAAAAAA
+DgDEAAAAAAEFAAAAAQEBAAAAEAGAAAAACREAABAnAAALDwAAECcAAJcFAAAQJwAAsAgAABAn
+AAABHAAAECcAAF4CAAAQJwAAiwAAABAnAADLAwAAECcAAOUbAAAQJwAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
+
+--------------7AF1D14DE1DFA16229726B54--


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