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 bt...@apache.org on 2019/12/06 02:34:15 UTC

[james-project] 05/21: JAMES-2992 MessageFastViewFactory implementation

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

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

commit 503e33494d89a4a4c8b3ecdf38e8c553aca21cd7
Author: Tran Tien Duc <dt...@linagora.com>
AuthorDate: Fri Nov 29 17:43:42 2019 +0700

    JAMES-2992 MessageFastViewFactory implementation
---
 .../api/projections/MessageFastViewProjection.java |  20 ++
 .../MessageFastViewProjectionContract.java         |  67 +++++-
 .../methods/SetMessagesCreationProcessor.java      |  19 +-
 .../org/apache/james/jmap/draft/model/Emailer.java |  30 ++-
 .../draft/model/message/view/MessageFastView.java  |   3 +-
 .../model/message/view/MessageFastViewFactory.java | 151 +++++++++++++
 .../model/message/view/MessageFullViewFactory.java |  52 ++---
 .../message/view/MessageHeaderViewFactory.java     |  43 +---
 .../model/message/view/MessageViewFactory.java     |  69 +++---
 .../jmap/draft/methods/MessageSenderTest.java      |   3 +-
 .../message/view/MessageFastViewFactoryTest.java   | 236 +++++++++++++++++++++
 11 files changed, 558 insertions(+), 135 deletions(-)

diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/MessageFastViewProjection.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/MessageFastViewProjection.java
index 9dc4bad..a93f582 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/MessageFastViewProjection.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/projections/MessageFastViewProjection.java
@@ -19,9 +19,19 @@
 
 package org.apache.james.jmap.api.projections;
 
+import java.util.List;
+import java.util.Map;
+
+import org.apache.commons.lang3.tuple.Pair;
 import org.apache.james.mailbox.model.MessageId;
 import org.reactivestreams.Publisher;
 
+import com.google.common.base.Preconditions;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
 public interface MessageFastViewProjection {
 
     Publisher<Void> store(MessageId messageId, MessageFastViewPrecomputedProperties preview);
@@ -29,4 +39,14 @@ public interface MessageFastViewProjection {
     Publisher<MessageFastViewPrecomputedProperties> retrieve(MessageId messageId);
 
     Publisher<Void> delete(MessageId messageId);
+
+    default Publisher<Map<MessageId, MessageFastViewPrecomputedProperties>> retrieve(List<MessageId> messageIds) {
+        Preconditions.checkNotNull(messageIds);
+
+        return Flux.fromIterable(messageIds)
+            .flatMap(messageId -> Mono.from(this.retrieve(messageId))
+                .map(preview -> Pair.of(messageId, preview)))
+            .collectMap(Pair::getLeft, Pair::getRight)
+            .subscribeOn(Schedulers.boundedElastic());
+    }
 }
diff --git a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/MessageFastViewProjectionContract.java b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/MessageFastViewProjectionContract.java
index d2dfdfe..7c11d05 100644
--- a/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/MessageFastViewProjectionContract.java
+++ b/server/data/data-jmap/src/test/java/org/apache/james/jmap/api/projections/MessageFastViewProjectionContract.java
@@ -28,11 +28,16 @@ import java.util.concurrent.ConcurrentHashMap;
 import java.util.stream.IntStream;
 
 import org.apache.james.jmap.api.model.Preview;
+import java.util.List;
+
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.util.concurrency.ConcurrentTestRunner;
 import org.assertj.core.api.SoftAssertions;
 import org.junit.jupiter.api.Test;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+
 import reactor.core.publisher.Mono;
 
 public interface MessageFastViewProjectionContract {
@@ -54,7 +59,7 @@ public interface MessageFastViewProjectionContract {
 
     @Test
     default void retrieveShouldThrowWhenNullMessageId() {
-        assertThatThrownBy(() -> Mono.from(testee().retrieve(null)).block())
+        assertThatThrownBy(() -> Mono.from(testee().retrieve((MessageId) null)).block())
             .isInstanceOf(NullPointerException.class);
     }
 
@@ -97,6 +102,66 @@ public interface MessageFastViewProjectionContract {
     }
 
     @Test
+    default void retrieveShouldThrowWhenNullMessageIds() {
+        assertThatThrownBy(() -> Mono.from(testee().retrieve((List<MessageId>) null)).block())
+            .isInstanceOf(NullPointerException.class);
+    }
+
+    @Test
+    default void retrieveShouldReturnEmptyWhenEmptyMessageIds() {
+        assertThat(Mono.from(testee().retrieve(ImmutableList.of())).block())
+            .isEmpty();
+    }
+
+    @Test
+    default void retrieveShouldReturnAMapContainingStoredPreviews() {
+        MessageId messageId1 = newMessageId();
+        MessageId messageId2 = newMessageId();
+        Mono.from(testee().store(messageId1, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_1))
+            .block();
+        Mono.from(testee().store(messageId2, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_2))
+            .block();
+
+        assertThat(Mono.from(testee().retrieve(ImmutableList.of(messageId1, messageId2))).block())
+            .isEqualTo(ImmutableMap.builder()
+                .put(messageId1, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_1)
+                .put(messageId2, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_2)
+                .build());
+    }
+
+    @Test
+    default void retrieveShouldReturnOnlyPreviewsAvailableInTheStore() {
+        MessageId messageId1 = newMessageId();
+        MessageId messageId2 = newMessageId();
+        MessageId messageId3 = newMessageId();
+        Mono.from(testee().store(messageId1, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_1))
+            .block();
+        Mono.from(testee().store(messageId2, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_2))
+            .block();
+
+        assertThat(Mono.from(testee().retrieve(ImmutableList.of(messageId1, messageId2, messageId3))).block())
+            .isEqualTo(ImmutableMap.builder()
+                .put(messageId1, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_1)
+                .put(messageId2, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_2)
+                .build());
+    }
+
+    @Test
+    default void retrieveShouldReturnOnlyPreviewsByAskedMessageIds() {
+        MessageId messageId1 = newMessageId();
+        MessageId messageId2 = newMessageId();
+        Mono.from(testee().store(messageId1, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_1))
+            .block();
+        Mono.from(testee().store(messageId2, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_2))
+            .block();
+
+        assertThat(Mono.from(testee().retrieve(ImmutableList.of(messageId1))).block())
+            .isEqualTo(ImmutableMap.builder()
+                .put(messageId1, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_1)
+                .build());
+    }
+
+    @Test
     default void storeShouldThrowWhenNullMessageId() {
         assertThatThrownBy(() -> Mono.from(testee().store(null, MESSAGE_FAST_VIEW_PRECOMPUTED_PROPERTIES_1)).block())
             .isInstanceOf(NullPointerException.class);
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessor.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessor.java
index aa99deb..b51c831 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessor.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/methods/SetMessagesCreationProcessor.java
@@ -21,6 +21,7 @@ package org.apache.james.jmap.draft.methods;
 
 import static org.apache.james.jmap.draft.methods.Method.JMAP_PREFIX;
 
+import java.io.IOException;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -195,7 +196,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
                     .description(e.getMessage())
                     .build());
 
-        } catch (MailboxException | MessagingException e) {
+        } catch (MailboxException | MessagingException | IOException e) {
             LOG.error("Unexpected error while creating message", e);
             responseBuilder.notCreated(create.getCreationId(), 
                     SetError.builder()
@@ -213,7 +214,9 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
             .collect(Guavate.toImmutableList());
     }
 
-    private void performCreate(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session) throws MailboxException, InvalidMailboxForCreationException, MessagingException, AttachmentsNotFoundException {
+    private void performCreate(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session)
+        throws MailboxException, InvalidMailboxForCreationException, MessagingException, AttachmentsNotFoundException, IOException {
+
         if (isAppendToMailboxWithRole(Role.OUTBOX, entry.getValue(), session)) {
             sendMailViaOutbox(entry, responseBuilder, session);
         } else if (entry.getValue().isDraft()) {
@@ -239,13 +242,17 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
         }
     }
 
-    private void sendMailViaOutbox(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session) throws AttachmentsNotFoundException, MailboxException, MessagingException {
+    private void sendMailViaOutbox(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session)
+        throws AttachmentsNotFoundException, MailboxException, MessagingException, IOException {
+
         validateArguments(entry, session);
         MessageWithId created = handleOutboxMessages(entry, session);
         responseBuilder.created(created.getCreationId(), created.getValue());
     }
 
-    private void saveDraft(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session) throws AttachmentsNotFoundException, MailboxException, MessagingException {
+    private void saveDraft(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session)
+        throws AttachmentsNotFoundException, MailboxException, MessagingException, IOException {
+
         attachmentChecker.assertAttachmentsExist(entry, session);
         MessageWithId created = handleDraftMessages(entry, session);
         responseBuilder.created(created.getCreationId(), created.getValue());
@@ -273,7 +280,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
             .allMatch(path -> path.belongsTo(session));
     }
 
-    private MessageWithId handleOutboxMessages(CreationMessageEntry entry, MailboxSession session) throws MailboxException, MessagingException {
+    private MessageWithId handleOutboxMessages(CreationMessageEntry entry, MailboxSession session) throws MailboxException, MessagingException, IOException {
         assertUserIsSender(session, entry.getValue().getFrom());
         MetaDataWithContent newMessage = messageAppender.appendMessageInMailboxes(entry, toMailboxIds(entry), session);
         MessageFullView jmapMessage = messageFullViewFactory.fromMetaDataWithContent(newMessage);
@@ -292,7 +299,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
         }
     }
 
-    private MessageWithId handleDraftMessages(CreationMessageEntry entry, MailboxSession session) throws MailboxException, MessagingException {
+    private MessageWithId handleDraftMessages(CreationMessageEntry entry, MailboxSession session) throws MailboxException, MessagingException, IOException {
         MetaDataWithContent newMessage = messageAppender.appendMessageInMailboxes(entry, toMailboxIds(entry), session);
         MessageFullView jmapMessage = messageFullViewFactory.fromMetaDataWithContent(newMessage);
         return new ValueWithId.MessageWithId(entry.getCreationId(), jmapMessage);
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/Emailer.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/Emailer.java
index 9060c76..819831c 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/Emailer.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/Emailer.java
@@ -48,22 +48,20 @@ public class Emailer {
     private static final Logger LOGGER = LoggerFactory.getLogger(Emailer.class);
 
     public static List<Emailer> fromAddressList(AddressList list) {
-        if (list == null) {
-            return ImmutableList.of();
-        }
-        return list.flatten()
-            .stream()
-            .map(Emailer::fromMailbox)
-            .collect(Guavate.toImmutableList());
+        return Optional.ofNullable(list)
+            .map(addresses -> addresses.flatten()
+                .stream()
+                .map(Emailer::fromMailbox)
+                .collect(Guavate.toImmutableList()))
+            .orElse(ImmutableList.of());
     }
 
     public static Emailer firstFromMailboxList(MailboxList list) {
-        if (list == null) {
-            return null;
-        }
-        return list.stream()
-            .map(Emailer::fromMailbox)
-            .findFirst()
+        return Optional.ofNullable(list)
+            .map(mailboxes -> mailboxes.stream()
+                .map(Emailer::fromMailbox)
+                .findFirst()
+                .orElse(null))
             .orElse(null);
     }
 
@@ -76,10 +74,8 @@ public class Emailer {
     }
 
     private static String getNameOrAddress(Mailbox mailbox) {
-        if (mailbox.getName() != null) {
-            return mailbox.getName();
-        }
-        return mailbox.getAddress();
+        return Optional.ofNullable(mailbox.getName())
+            .orElseGet(mailbox::getAddress);
     }
 
 
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFastView.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFastView.java
index dfa6ce6..3143ba1 100644
--- a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFastView.java
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFastView.java
@@ -57,6 +57,7 @@ public class MessageFastView extends MessageHeaderView {
 
         protected Builder() {
             super();
+            this.preview = Optional.empty();
         }
 
         public S preview(Preview preview) {
@@ -77,13 +78,13 @@ public class MessageFastView extends MessageHeaderView {
                 keywords.orElse(Keywords.DEFAULT_VALUE));
         }
 
+        @Override
         public void checkState() {
             super.checkState();
             Preconditions.checkState(preview != null, "'preview' is mandatory");
         }
     }
 
-
     private final PreviewDTO preview;
 
     @VisibleForTesting
diff --git a/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFastViewFactory.java b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFastViewFactory.java
new file mode 100644
index 0000000..edc43b7
--- /dev/null
+++ b/server/protocols/jmap-draft/src/main/java/org/apache/james/jmap/draft/model/message/view/MessageFastViewFactory.java
@@ -0,0 +1,151 @@
+/****************************************************************
+ * 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.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import javax.inject.Inject;
+
+import org.apache.james.jmap.api.projections.MessageFastViewPrecomputedProperties;
+import org.apache.james.jmap.api.projections.MessageFastViewProjection;
+import org.apache.james.jmap.draft.model.BlobId;
+import org.apache.james.jmap.draft.model.Emailer;
+import org.apache.james.mailbox.BlobManager;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.FetchGroup;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.MessageResult;
+import org.apache.james.mime4j.dom.Message;
+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.Preconditions;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+import reactor.core.scheduler.Schedulers;
+
+public class MessageFastViewFactory implements MessageViewFactory<MessageFastView> {
+
+    private static class FromMessageResultAndPreview implements Helpers.FromMessageResult<MessageFastView> {
+
+        private final BlobManager blobManager;
+        private final Map<MessageId, MessageFastViewPrecomputedProperties> fastProjections;
+
+        private FromMessageResultAndPreview(BlobManager blobManager,
+                                            Map<MessageId, MessageFastViewPrecomputedProperties> fastProjections) {
+            this.blobManager = blobManager;
+            this.fastProjections = fastProjections;
+        }
+
+        @Override
+        public MessageFastView fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException, IOException {
+            Helpers.assertOneMessageId(messageResults);
+            MessageResult firstMessageResult = messageResults.iterator().next();
+            Preconditions.checkArgument(fastProjections.containsKey(firstMessageResult.getMessageId()),
+                "FromMessageResultAndPreview usage requires a precomputed preview");
+
+            List<MailboxId> mailboxIds = Helpers.getMailboxIds(messageResults);
+
+            Message mimeMessage = Helpers.parse(firstMessageResult.getFullContent().getInputStream());
+
+            return MessageFastView.builder()
+                .id(firstMessageResult.getMessageId())
+                .mailboxIds(mailboxIds)
+                .blobId(BlobId.of(blobManager.toBlobId(firstMessageResult.getMessageId())))
+                .threadId(firstMessageResult.getMessageId().serialize())
+                .keywords(Helpers.getKeywords(messageResults))
+                .size(firstMessageResult.getSize())
+                .inReplyToMessageId(Helpers.getHeaderValue(mimeMessage, "in-reply-to"))
+                .subject(Strings.nullToEmpty(mimeMessage.getSubject()).trim())
+                .headers(Helpers.toHeaderMap(mimeMessage.getHeader().getFields()))
+                .from(Emailer.firstFromMailboxList(mimeMessage.getFrom()))
+                .to(Emailer.fromAddressList(mimeMessage.getTo()))
+                .cc(Emailer.fromAddressList(mimeMessage.getCc()))
+                .bcc(Emailer.fromAddressList(mimeMessage.getBcc()))
+                .replyTo(Emailer.fromAddressList(mimeMessage.getReplyTo()))
+                .date(Helpers.getDateFromHeaderOrInternalDateOtherwise(mimeMessage, firstMessageResult))
+                .preview(fastProjections.get(firstMessageResult.getMessageId()).getPreview())
+                .build();
+        }
+    }
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(MessageFastViewFactory.class);
+
+    private final BlobManager blobManager;
+    private final MessageIdManager messageIdManager;
+    private final MessageFastViewProjection fastViewProjection;
+    private final MessageFullViewFactory messageFullViewFactory;
+
+    @Inject
+    @VisibleForTesting
+    MessageFastViewFactory(BlobManager blobManager, MessageIdManager messageIdManager, MessageFastViewProjection fastViewProjection,
+                           MessageFullViewFactory messageFullViewFactory) {
+        this.blobManager = blobManager;
+        this.messageIdManager = messageIdManager;
+        this.fastViewProjection = fastViewProjection;
+        this.messageFullViewFactory = messageFullViewFactory;
+    }
+
+    @Override
+    public List<MessageFastView> fromMessageIds(List<MessageId> messageIds, MailboxSession mailboxSession) {
+        return Mono.from(fastViewProjection.retrieve(messageIds))
+            .flatMapMany(fastProjections -> gatherMessageViews(messageIds, mailboxSession, fastProjections))
+            .collectList()
+            .subscribeOn(Schedulers.boundedElastic())
+            .block();
+    }
+
+    private Flux<MessageFastView> gatherMessageViews(List<MessageId> messageIds, MailboxSession mailboxSession,
+                                                     Map<MessageId, MessageFastViewPrecomputedProperties> fastProjections) {
+        return Flux.merge(
+                fetch(ImmutableList.copyOf(fastProjections.keySet()), FetchGroup.HEADERS, mailboxSession)
+                    .map(messageResults -> Helpers.toMessageViews(messageResults, new FromMessageResultAndPreview(blobManager, fastProjections))),
+                fetch(withoutPreviews(messageIds, fastProjections), FetchGroup.FULL_CONTENT, mailboxSession)
+                    .map(messageResults -> Helpers.toMessageViews(messageResults, messageFullViewFactory::fromMessageResults)))
+            .flatMap(Flux::fromIterable);
+    }
+
+    private List<MessageId> withoutPreviews(List<MessageId> messageIds, Map<MessageId, MessageFastViewPrecomputedProperties> fastProjections) {
+        return ImmutableList.copyOf(Sets.difference(
+            ImmutableSet.copyOf(messageIds),
+            fastProjections.keySet()));
+    }
+
+    private Mono<List<MessageResult>> fetch(List<MessageId> messageIds, FetchGroup fetchGroup, MailboxSession mailboxSession) {
+        return Mono.fromCallable(() -> messageIdManager.getMessages(messageIds, fetchGroup, mailboxSession))
+            .onErrorResume(MailboxException.class, ex -> {
+                LOGGER.error("cannot read messages {}", messageIds, ex);
+                return Mono.just(ImmutableList.of());
+            });
+    }
+}
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 b5b20c1..7cbd989 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
@@ -33,6 +33,7 @@ import javax.mail.internet.SharedInputStream;
 import org.apache.james.jmap.api.model.Preview;
 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.MessageProperties;
 import org.apache.james.jmap.draft.utils.HtmlTextExtractor;
@@ -47,7 +48,6 @@ 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.Message;
-import org.apache.james.mime4j.stream.MimeConfig;
 import org.apache.james.util.mime.MessageContentExtractor;
 import org.apache.james.util.mime.MessageContentExtractor.MessageContent;
 
@@ -77,8 +77,8 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
         return Helpers.toMessageViews(messages, this::fromMessageResults);
     }
 
-    public MessageFullView fromMetaDataWithContent(MetaDataWithContent message) throws MailboxException {
-        Message mimeMessage = parse(message);
+    public MessageFullView fromMetaDataWithContent(MetaDataWithContent message) throws MailboxException, IOException {
+        Message mimeMessage = Helpers.parse(message.getContent());
         MessageContent messageContent = extractContent(mimeMessage);
         Optional<String> htmlBody = messageContent.getHtmlBody();
         Optional<String> mainTextContent = mainTextContent(messageContent);
@@ -91,15 +91,15 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
                 .blobId(BlobId.of(blobManager.toBlobId(message.getMessageId())))
                 .threadId(message.getMessageId().serialize())
                 .mailboxIds(message.getMailboxIds())
-                .inReplyToMessageId(Helpers.getHeader(mimeMessage, "in-reply-to"))
+                .inReplyToMessageId(Helpers.getHeaderValue(mimeMessage, "in-reply-to"))
                 .keywords(message.getKeywords())
                 .subject(Strings.nullToEmpty(mimeMessage.getSubject()).trim())
-                .headers(Helpers.toMap(mimeMessage.getHeader().getFields()))
-                .from(Helpers.firstFromMailboxList(mimeMessage.getFrom()))
-                .to(Helpers.fromAddressList(mimeMessage.getTo()))
-                .cc(Helpers.fromAddressList(mimeMessage.getCc()))
-                .bcc(Helpers.fromAddressList(mimeMessage.getBcc()))
-                .replyTo(Helpers.fromAddressList(mimeMessage.getReplyTo()))
+                .headers(Helpers.toHeaderMap(mimeMessage.getHeader().getFields()))
+                .from(Emailer.firstFromMailboxList(mimeMessage.getFrom()))
+                .to(Emailer.fromAddressList(mimeMessage.getTo()))
+                .cc(Emailer.fromAddressList(mimeMessage.getCc()))
+                .bcc(Emailer.fromAddressList(mimeMessage.getBcc()))
+                .replyTo(Emailer.fromAddressList(mimeMessage.getReplyTo()))
                 .size(message.getSize())
                 .date(getDateFromHeaderOrInternalDateOtherwise(mimeMessage, message))
                 .textBody(textBody)
@@ -109,11 +109,17 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
                 .build();
     }
 
-    private MessageFullView fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException {
+    private Instant getDateFromHeaderOrInternalDateOtherwise(Message mimeMessage, MessageFullViewFactory.MetaDataWithContent message) {
+        return Optional.ofNullable(mimeMessage.getDate())
+            .map(Date::toInstant)
+            .orElse(message.getInternalDate());
+    }
+
+    MessageFullView fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException, IOException {
         return fromMetaDataWithContent(toMetaDataWithContent(messageResults));
     }
 
-    private MetaDataWithContent toMetaDataWithContent(Collection<MessageResult> messageResults) throws MailboxException {
+    MetaDataWithContent toMetaDataWithContent(Collection<MessageResult> messageResults) throws MailboxException {
         Helpers.assertOneMessageId(messageResults);
 
         MessageResult firstMessageResult = messageResults.iterator().next();
@@ -127,19 +133,13 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
             .build();
     }
 
-    private Instant getDateFromHeaderOrInternalDateOtherwise(Message mimeMessage, MetaDataWithContent message) {
-        return Optional.ofNullable(mimeMessage.getDate())
-            .map(Date::toInstant)
-            .orElse(message.getInternalDate());
-    }
-
     private Optional<String> computeTextBodyIfNeeded(MessageContent messageContent, Optional<String> mainTextContent) {
         return messageContent.getTextBody()
             .map(Optional::of)
             .orElse(mainTextContent);
     }
 
-    private Optional<String> mainTextContent(MessageContent messageContent) {
+    Optional<String> mainTextContent(MessageContent messageContent) {
         return messageContent.getHtmlBody()
             .map(htmlTextExtractor::toPlainText)
             .filter(s -> !Strings.isNullOrEmpty(s))
@@ -147,19 +147,7 @@ public class MessageFullViewFactory implements MessageViewFactory<MessageFullVie
             .orElse(messageContent.getTextBody());
     }
 
-    private Message parse(MetaDataWithContent message) throws MailboxException {
-        try {
-            return Message.Builder
-                    .of()
-                    .use(MimeConfig.PERMISSIVE)
-                    .parse(message.getContent())
-                    .build();
-        } catch (IOException e) {
-            throw new MailboxException("Unable to parse message: " + e.getMessage(), e);
-        }
-    }
-
-    private MessageContent extractContent(Message mimeMessage) throws MailboxException {
+    MessageContent extractContent(Message mimeMessage) throws MailboxException {
         try {
             return messageContentExtractor.extract(mimeMessage);
         } catch (IOException e) {
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
index e840d2d..b519b87 100644
--- 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
@@ -20,15 +20,13 @@
 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.jmap.draft.model.Emailer;
 import org.apache.james.jmap.draft.model.MessageProperties;
 import org.apache.james.mailbox.BlobManager;
 import org.apache.james.mailbox.MailboxSession;
@@ -38,7 +36,6 @@ import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MessageId;
 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.annotations.VisibleForTesting;
 import com.google.common.base.Strings;
@@ -60,13 +57,13 @@ public class MessageHeaderViewFactory implements MessageViewFactory<MessageHeade
         return Helpers.toMessageViews(messages, this::fromMessageResults);
     }
 
-    private MessageHeaderView fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException {
+    private MessageHeaderView fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException, IOException {
         Helpers.assertOneMessageId(messageResults);
 
         MessageResult firstMessageResult = messageResults.iterator().next();
         List<MailboxId> mailboxIds = Helpers.getMailboxIds(messageResults);
 
-        Message mimeMessage = parse(firstMessageResult);
+        Message mimeMessage = Helpers.parse(firstMessageResult.getFullContent().getInputStream());
 
         return MessageHeaderView.messageHeaderBuilder()
             .id(firstMessageResult.getMessageId())
@@ -75,33 +72,15 @@ public class MessageHeaderViewFactory implements MessageViewFactory<MessageHeade
             .threadId(firstMessageResult.getMessageId().serialize())
             .keywords(Helpers.getKeywords(messageResults))
             .size(firstMessageResult.getSize())
-            .inReplyToMessageId(Helpers.getHeader(mimeMessage, "in-reply-to"))
+            .inReplyToMessageId(Helpers.getHeaderValue(mimeMessage, "in-reply-to"))
             .subject(Strings.nullToEmpty(mimeMessage.getSubject()).trim())
-            .headers(Helpers.toMap(mimeMessage.getHeader().getFields()))
-            .from(Helpers.firstFromMailboxList(mimeMessage.getFrom()))
-            .to(Helpers.fromAddressList(mimeMessage.getTo()))
-            .cc(Helpers.fromAddressList(mimeMessage.getCc()))
-            .bcc(Helpers.fromAddressList(mimeMessage.getBcc()))
-            .replyTo(Helpers.fromAddressList(mimeMessage.getReplyTo()))
-            .date(getDateFromHeaderOrInternalDateOtherwise(mimeMessage, firstMessageResult))
+            .headers(Helpers.toHeaderMap(mimeMessage.getHeader().getFields()))
+            .from(Emailer.firstFromMailboxList(mimeMessage.getFrom()))
+            .to(Emailer.fromAddressList(mimeMessage.getTo()))
+            .cc(Emailer.fromAddressList(mimeMessage.getCc()))
+            .bcc(Emailer.fromAddressList(mimeMessage.getBcc()))
+            .replyTo(Emailer.fromAddressList(mimeMessage.getReplyTo()))
+            .date(Helpers.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 1a08705..0025f19 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
@@ -19,14 +19,18 @@
 
 package org.apache.james.jmap.draft.model.message.view;
 
+import java.io.IOException;
+import java.io.InputStream;
+import java.time.Instant;
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
-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.MailboxSession;
@@ -34,17 +38,15 @@ import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.MailboxId;
 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.dom.Message;
 import org.apache.james.mime4j.stream.Field;
+import org.apache.james.mime4j.stream.MimeConfig;
 import org.apache.james.mime4j.util.MimeUtil;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 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;
 
@@ -60,7 +62,7 @@ public interface MessageViewFactory<T extends MessageView> {
 
     class Helpers {
         interface FromMessageResult<T extends MessageView> {
-            T fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException;
+            T fromMessageResults(Collection<MessageResult> messageResults) throws MailboxException, IOException;
         }
 
         static void assertOneMessageId(Collection<MessageResult> messageResults) {
@@ -68,7 +70,7 @@ public interface MessageViewFactory<T extends MessageView> {
             Preconditions.checkArgument(hasOnlyOneMessageId(messageResults), "MessageResults need to share the same messageId");
         }
 
-        static boolean hasOnlyOneMessageId(Collection<MessageResult> messageResults) {
+        private static boolean hasOnlyOneMessageId(Collection<MessageResult> messageResults) {
             return messageResults
                 .stream()
                 .map(MessageResult::getMessageId)
@@ -91,7 +93,7 @@ public interface MessageViewFactory<T extends MessageView> {
                 .get();
         }
 
-        static String getHeader(org.apache.james.mime4j.dom.Message message, String header) {
+        static String getHeaderValue(org.apache.james.mime4j.dom.Message message, String header) {
             Field field = message.getHeader().getField(header);
             if (field == null) {
                 return null;
@@ -99,7 +101,7 @@ public interface MessageViewFactory<T extends MessageView> {
             return field.getBody();
         }
 
-        static ImmutableMap<String, String> toMap(List<Field> fields) {
+        static ImmutableMap<String, String> toHeaderMap(List<Field> fields) {
             Function<Map.Entry<String, Collection<Field>>, String> bodyConcatenator = fieldListEntry -> fieldListEntry.getValue()
                 .stream()
                 .map(Field::getBody)
@@ -115,41 +117,6 @@ public interface MessageViewFactory<T extends MessageView> {
                 .collect(Guavate.toImmutableMap(Map.Entry::getKey, bodyConcatenator));
         }
 
-        static Emailer firstFromMailboxList(MailboxList list) {
-            if (list == null) {
-                return null;
-            }
-            return list.stream()
-                .map(Helpers::fromMailbox)
-                .findFirst()
-                .orElse(null);
-        }
-
-        static Emailer fromMailbox(Mailbox mailbox) {
-            return Emailer.builder()
-                .name(getNameOrAddress(mailbox))
-                .email(mailbox.getAddress())
-                .allowInvalid()
-                .build();
-        }
-
-        static String getNameOrAddress(Mailbox mailbox) {
-            if (mailbox.getName() != null) {
-                return mailbox.getName();
-            }
-            return mailbox.getAddress();
-        }
-
-        static ImmutableList<Emailer> fromAddressList(AddressList list) {
-            if (list == null) {
-                return ImmutableList.of();
-            }
-            return list.flatten()
-                .stream()
-                .map(Helpers::fromMailbox)
-                .collect(Guavate.toImmutableList());
-        }
-
         static <T extends MessageView>  Function<Collection<MessageResult>, Stream<T>> toMessageViews(FromMessageResult<T> converter) {
             return messageResults -> {
                 try {
@@ -171,5 +138,19 @@ public interface MessageViewFactory<T extends MessageView> {
                 .flatMap(toMessageViews(converter))
                 .collect(Guavate.toImmutableList());
         }
+
+        static Instant getDateFromHeaderOrInternalDateOtherwise(Message mimeMessage, MessageResult message) {
+            return Optional.ofNullable(mimeMessage.getDate())
+                .map(Date::toInstant)
+                .orElse(message.getInternalDate().toInstant());
+        }
+
+        static Message parse(InputStream messageContent) throws IOException {
+            return Message.Builder
+                .of()
+                .use(MimeConfig.PERMISSIVE)
+                .parse(messageContent)
+                .build();
+        }
     }
 }
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/MessageSenderTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/MessageSenderTest.java
index 3791de6..f8f768c 100644
--- a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/MessageSenderTest.java
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/methods/MessageSenderTest.java
@@ -43,7 +43,6 @@ import org.apache.james.jmap.draft.utils.HtmlTextExtractor;
 import org.apache.james.mailbox.BlobManager;
 import org.apache.james.mailbox.MessageIdManager;
 import org.apache.james.mailbox.MessageUid;
-import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.inmemory.InMemoryId;
 import org.apache.james.mailbox.model.BlobId;
 import org.apache.james.mailbox.model.MessageId;
@@ -64,7 +63,7 @@ class MessageSenderTest {
     private MessageFullView jmapMessage;
 
     @BeforeEach
-    void setup() throws MailboxException {
+    void setup() throws Exception {
         String headers = "From: me@example.com\n"
             + "To: 1@example.com\n"
             + "Cc: 2@example.com, 3@example.com\n"
diff --git a/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/model/message/view/MessageFastViewFactoryTest.java b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/model/message/view/MessageFastViewFactoryTest.java
new file mode 100644
index 0000000..044bcaf
--- /dev/null
+++ b/server/protocols/jmap-draft/src/test/java/org/apache/james/jmap/draft/model/message/view/MessageFastViewFactoryTest.java
@@ -0,0 +1,236 @@
+/****************************************************************
+ * 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 static org.apache.james.jmap.draft.model.message.view.MessageViewFixture.ALICE_EMAIL;
+import static org.apache.james.jmap.draft.model.message.view.MessageViewFixture.BOB;
+import static org.apache.james.jmap.draft.model.message.view.MessageViewFixture.BOB_EMAIL;
+import static org.apache.james.jmap.draft.model.message.view.MessageViewFixture.HEADERS_MAP;
+import static org.apache.james.jmap.draft.model.message.view.MessageViewFixture.JACK_EMAIL;
+import static org.apache.james.jmap.draft.model.message.view.MessageViewFixture.JACOB_EMAIL;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.spy;
+
+import java.util.List;
+import java.util.Optional;
+
+import javax.mail.Flags;
+
+import org.apache.james.jmap.api.model.Preview;
+import org.apache.james.jmap.api.projections.MessageFastViewPrecomputedProperties;
+import org.apache.james.jmap.draft.model.BlobId;
+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.jmap.draft.model.PreviewDTO;
+import org.apache.james.jmap.draft.utils.HtmlTextExtractor;
+import org.apache.james.jmap.draft.utils.JsoupHtmlTextExtractor;
+import org.apache.james.jmap.memory.projections.MemoryMessageFastViewProjection;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.exception.MailboxException;
+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.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.store.StoreBlobManager;
+import org.apache.james.util.ClassLoaderUtils;
+import org.apache.james.util.mime.MessageContentExtractor;
+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 reactor.core.publisher.Mono;
+
+class MessageFastViewFactoryTest {
+
+    private static final String PREVIEW_1_STRING = "preview 1";
+    private static final Preview PREVIEW_1 = Preview.from(PREVIEW_1_STRING);
+    private static final String PREVIEW_3_STRING = "preview 3";
+    private static final Preview PREVIEW_3 = Preview.from(PREVIEW_3_STRING);
+    private static final String PREVIEW_4_STRING = "preview 4";
+    private static final Preview PREVIEW_4 = Preview.from(PREVIEW_4_STRING);
+    private static final String DEFAULT_PREVIEW_STRING = "blabla bloblo";
+    private static final MessageFastViewPrecomputedProperties PROJECTION_1 = MessageFastViewPrecomputedProperties
+        .builder()
+        .preview(PREVIEW_1)
+        .hasAttachment()
+        .build();
+    private static final MessageFastViewPrecomputedProperties PROJECTION_3 = MessageFastViewPrecomputedProperties
+        .builder()
+        .preview(PREVIEW_3)
+        .noAttachments()
+        .build();
+    private static final MessageFastViewPrecomputedProperties PROJECTION_4 = MessageFastViewPrecomputedProperties
+        .builder()
+        .preview(PREVIEW_4)
+        .noAttachments()
+        .build();
+
+    private MessageIdManager messageIdManager;
+    private MailboxSession session;
+    private MessageManager bobInbox;
+    private ComposedMessageId previewComputedMessage1;
+    private ComposedMessageId missingPreviewComputedMessage1;
+    private ComposedMessageId previewComputedMessage2;
+    private ComposedMessageId previewComputedMessage3;
+    private MessageFastViewFactory messageFastViewFactory;
+    private MemoryMessageFastViewProjection fastViewProjection;
+    private StoreBlobManager blobManager;
+    private MessageFullViewFactory messageFullViewFactory;
+
+    @BeforeEach
+    void setUp() throws Exception {
+        HtmlTextExtractor htmlTextExtractor = new JsoupHtmlTextExtractor();
+
+        MessageContentExtractor messageContentExtractor = new MessageContentExtractor();
+
+        InMemoryIntegrationResources resources = InMemoryIntegrationResources.defaultResources();
+        messageIdManager = spy(resources.getMessageIdManager());
+        InMemoryMailboxManager mailboxManager = resources.getMailboxManager();
+
+        session = mailboxManager.createSystemSession(BOB);
+        MailboxId bobInboxId = mailboxManager.createMailbox(MailboxPath.inbox(session), session).get();
+
+        bobInbox = mailboxManager.getMailbox(bobInboxId, session);
+
+        previewComputedMessage1 = bobInbox.appendMessage(MessageManager.AppendCommand.builder()
+                .withFlags(new Flags(Flags.Flag.SEEN))
+                .build(ClassLoaderUtils.getSystemResourceAsSharedStream("fullMessage.eml")),
+            session);
+        missingPreviewComputedMessage1 = bobInbox.appendMessage(MessageManager.AppendCommand.builder()
+                .withFlags(new Flags(Flags.Flag.SEEN))
+                .build(ClassLoaderUtils.getSystemResourceAsSharedStream("fullMessage.eml")),
+            session);
+        previewComputedMessage2 = bobInbox.appendMessage(MessageManager.AppendCommand.builder()
+                .withFlags(new Flags(Flags.Flag.SEEN))
+                .build(ClassLoaderUtils.getSystemResourceAsSharedStream("fullMessage.eml")),
+            session);
+        previewComputedMessage3 = bobInbox.appendMessage(MessageManager.AppendCommand.builder()
+                .withFlags(new Flags(Flags.Flag.SEEN))
+                .build(ClassLoaderUtils.getSystemResourceAsSharedStream("fullMessage.eml")),
+            session);
+
+        fastViewProjection = new MemoryMessageFastViewProjection();
+
+        Mono.from(fastViewProjection.store(previewComputedMessage1.getMessageId(), PROJECTION_1))
+            .block();
+        Mono.from(fastViewProjection.store(previewComputedMessage2.getMessageId(), PROJECTION_3))
+            .block();
+        Mono.from(fastViewProjection.store(previewComputedMessage3.getMessageId(), PROJECTION_4))
+            .block();
+
+        blobManager = resources.getBlobManager();
+        messageFullViewFactory = new MessageFullViewFactory(blobManager, messageContentExtractor, htmlTextExtractor, messageIdManager);
+        messageFastViewFactory = new MessageFastViewFactory(blobManager, messageIdManager, fastViewProjection, messageFullViewFactory);
+    }
+
+    @Test
+    void fromMessageIdsShouldReturnAMessageWithPreviewInThePreviewStore() throws Exception {
+        MessageFastView actual = messageFastViewFactory.fromMessageIds(ImmutableList.of(previewComputedMessage1.getMessageId()), session).get(0);
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(actual.getId()).isEqualTo(previewComputedMessage1.getMessageId());
+            softly.assertThat(actual.getMailboxIds()).containsExactly(bobInbox.getId());
+            softly.assertThat(actual.getThreadId()).isEqualTo(previewComputedMessage1.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(previewComputedMessage1.getMessageId().serialize()));
+            softly.assertThat(actual.getInReplyToMessageId()).isEqualTo(Optional.of(BOB.asString()));
+            softly.assertThat(actual.getHeaders()).isEqualTo(HEADERS_MAP);
+            softly.assertThat(actual.getFrom()).isEqualTo(Optional.of(ALICE_EMAIL));
+            softly.assertThat(actual.getTo()).isEqualTo(ImmutableList.of(BOB_EMAIL));
+            softly.assertThat(actual.getCc()).isEqualTo(ImmutableList.of(JACK_EMAIL, JACOB_EMAIL));
+            softly.assertThat(actual.getBcc()).isEqualTo(ImmutableList.of(ALICE_EMAIL));
+            softly.assertThat(actual.getReplyTo()).isEqualTo(ImmutableList.of(ALICE_EMAIL));
+            softly.assertThat(actual.getSubject()).isEqualTo("Full message");
+            softly.assertThat(actual.getDate()).isEqualTo("2016-06-07T14:23:37Z");
+
+            softly.assertThat(actual.getPreview()).isEqualTo(PreviewDTO.of(PREVIEW_1_STRING));
+        });
+    }
+
+    @Test
+    void fromMessageIdsShouldReturnAMessageWithPreviewComputedFromFullMessageWhenNotInThePreviewStore() throws Exception {
+        MessageFastView actual = messageFastViewFactory.fromMessageIds(ImmutableList.of(missingPreviewComputedMessage1.getMessageId()), session).get(0);
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(actual.getId()).isEqualTo(missingPreviewComputedMessage1.getMessageId());
+            softly.assertThat(actual.getMailboxIds()).containsExactly(bobInbox.getId());
+            softly.assertThat(actual.getThreadId()).isEqualTo(missingPreviewComputedMessage1.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(missingPreviewComputedMessage1.getMessageId().serialize()));
+            softly.assertThat(actual.getInReplyToMessageId()).isEqualTo(Optional.of(BOB.asString()));
+            softly.assertThat(actual.getHeaders()).isEqualTo(HEADERS_MAP);
+            softly.assertThat(actual.getFrom()).isEqualTo(Optional.of(ALICE_EMAIL));
+            softly.assertThat(actual.getTo()).isEqualTo(ImmutableList.of(BOB_EMAIL));
+            softly.assertThat(actual.getCc()).isEqualTo(ImmutableList.of(JACK_EMAIL, JACOB_EMAIL));
+            softly.assertThat(actual.getBcc()).isEqualTo(ImmutableList.of(ALICE_EMAIL));
+            softly.assertThat(actual.getReplyTo()).isEqualTo(ImmutableList.of(ALICE_EMAIL));
+            softly.assertThat(actual.getSubject()).isEqualTo("Full message");
+            softly.assertThat(actual.getDate()).isEqualTo("2016-06-07T14:23:37Z");
+
+            softly.assertThat(actual.getPreview()).isEqualTo(PreviewDTO.of(DEFAULT_PREVIEW_STRING));
+        });
+    }
+
+    @Test
+    void fromMessageIdsShouldReturnMessagesWithPreviews() throws Exception {
+        List<MessageFastView> actual = messageFastViewFactory
+            .fromMessageIds(ImmutableList.of(
+                    previewComputedMessage2.getMessageId(),
+                    previewComputedMessage3.getMessageId(),
+                    missingPreviewComputedMessage1.getMessageId(),
+                    previewComputedMessage1.getMessageId()),
+                session);
+
+        assertThat(actual)
+            .hasSize(4)
+            .extracting(MessageFastView::getPreview)
+            .containsOnly(
+                PreviewDTO.of(PREVIEW_3_STRING),
+                PreviewDTO.of(PREVIEW_4_STRING),
+                PreviewDTO.of(DEFAULT_PREVIEW_STRING),
+                PreviewDTO.of(PREVIEW_1_STRING));
+    }
+
+    @Test
+    void fromMessageIdsShouldKeepProcessingEvenWhenFetchingFail() throws Exception {
+        doThrow(new MailboxException("mock exception"))
+            .doCallRealMethod()
+            .when(messageIdManager).getMessages(any(), any(), any());
+
+        List<MessageFastView> actual = messageFastViewFactory
+            .fromMessageIds(ImmutableList.of(
+                    missingPreviewComputedMessage1.getMessageId(),
+                    previewComputedMessage1.getMessageId()),
+                session);
+
+        assertThat(actual)
+            .hasSize(1)
+            .extracting(MessageFastView::getId)
+            .containsAnyOf(missingPreviewComputedMessage1.getMessageId(), previewComputedMessage1.getMessageId());
+    }
+}
\ No newline at end of file


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