You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2023/01/04 02:31:19 UTC

[james-project] branch master updated (0659f18f8d -> a7ae09187f)

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

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


    from 0659f18f8d JAMES-3870 Group IMAP response line within TCP packets (#1364)
     new 5cb1ed5546 JAMES-3872 Add a FetchType that support getting the attachments' metadata without fetching the body content
     new de44d8b5ab JAMES-3872 Add a JMAP read level that get preview of mail with attachments' metadata without getting body content
     new a7ae09187f JAMES-3872 Fix failed JpaMessageMapperTest

The 3 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../org/apache/james/mailbox/model/FetchGroup.java |   2 +
 .../mailbox/cassandra/mail/AttachmentLoader.java   |   2 +-
 .../cassandra/mail/CassandraMessageDAO.java        |   1 +
 .../cassandra/mail/CassandraMessageDAOV3.java      |   1 +
 .../cassandra/mail/CassandraMessageMapperTest.java |  49 +++
 .../store/search/SimpleMessageSearchIndexTest.java |   2 +-
 .../org/apache/james/mailbox/store/BatchSizes.java |   3 +-
 .../apache/james/mailbox/store/ResultUtils.java    |   3 +-
 .../mailbox/store/mail/FetchGroupConverter.java    |   8 +-
 .../james/mailbox/store/mail/MessageMapper.java    |   9 +-
 .../store/mail/FetchGroupConverterTest.java        |   1 +
 .../store/mail/model/MessageMapperTest.java        |   2 +-
 .../resources/eml/inlined-single-attachment.eml    |  11 -
 .../rfc8621/contract/EmailGetMethodContract.scala  | 353 ++++++++++++++++++++-
 .../james/jmap/json/EmailGetSerializer.scala       |   9 +-
 .../scala/org/apache/james/jmap/mail/Email.scala   | 130 +++++++-
 .../org/apache/james/jmap/mail/EmailBodyPart.scala |  30 +-
 17 files changed, 586 insertions(+), 30 deletions(-)
 copy mailbox/opensearch/src/test/resources/eml/inlined-mixed.eml => server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/inlined-single-attachment.eml (72%)


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


[james-project] 03/03: JAMES-3872 Fix failed JpaMessageMapperTest

Posted by bt...@apache.org.
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 a7ae09187f16bc9484c1500d84257d9a1229f856
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Tue Jan 3 23:04:11 2023 +0700

    JAMES-3872 Fix failed JpaMessageMapperTest
---
 .../james/mailbox/cassandra/mail/CassandraMessageMapperTest.java       | 2 ++
 .../org/apache/james/mailbox/store/mail/model/MessageMapperTest.java   | 3 ---
 2 files changed, 2 insertions(+), 3 deletions(-)

diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageMapperTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageMapperTest.java
index 028d4cf0d0..e6bc711bdf 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageMapperTest.java
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageMapperTest.java
@@ -51,6 +51,7 @@ import org.apache.james.mailbox.model.MessageRange;
 import org.apache.james.mailbox.model.ParsedAttachment;
 import org.apache.james.mailbox.model.ThreadId;
 import org.apache.james.mailbox.store.FlagsUpdateCalculator;
+import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper.FetchType;
 import org.apache.james.mailbox.store.mail.model.MailboxMessage;
@@ -445,6 +446,7 @@ class CassandraMessageMapperTest extends MessageMapperTest {
 
     @Test
     void messagesRetrievedUsingFetchTypeAttachmentsMetadataShouldHaveAttachmentsMetadataLoaded() throws MailboxException {
+        AttachmentMapper attachmentMapper = mapperProvider.createAttachmentMapper();
         MessageId messageId = mapperProvider.generateMessageId();
         String content = "Subject: Test1 \n\nBody1\n.\n";
         ParsedAttachment attachment1 = ParsedAttachment.builder()
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/MessageMapperTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/MessageMapperTest.java
index 2a49fc1af6..1efac851ea 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/MessageMapperTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/MessageMapperTest.java
@@ -53,7 +53,6 @@ import org.apache.james.mailbox.model.ThreadId;
 import org.apache.james.mailbox.model.UidValidity;
 import org.apache.james.mailbox.model.UpdatedFlags;
 import org.apache.james.mailbox.store.FlagsUpdateCalculator;
-import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper.FetchType;
@@ -85,7 +84,6 @@ public abstract class MessageMapperTest {
     protected MapperProvider mapperProvider;
     protected MessageMapper messageMapper;
     private MailboxMapper mailboxMapper;
-    protected AttachmentMapper attachmentMapper;
 
     protected Mailbox benwaInboxMailbox;
     protected Mailbox benwaWorkMailbox;
@@ -109,7 +107,6 @@ public abstract class MessageMapperTest {
 
         this.messageMapper = mapperProvider.createMessageMapper();
         this.mailboxMapper = mapperProvider.createMailboxMapper();
-        this.attachmentMapper = mapperProvider.createAttachmentMapper();
 
         initData();
     }


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


[james-project] 01/03: JAMES-3872 Add a FetchType that support getting the attachments' metadata without fetching the body content

Posted by bt...@apache.org.
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 5cb1ed5546d4840afca1d22cd3bb011e0553465d
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Wed Dec 28 18:01:03 2022 +0700

    JAMES-3872 Add a FetchType that support getting the attachments' metadata without fetching the body content
---
 .../org/apache/james/mailbox/model/FetchGroup.java |  2 +
 .../mailbox/cassandra/mail/AttachmentLoader.java   |  2 +-
 .../cassandra/mail/CassandraMessageDAO.java        |  1 +
 .../cassandra/mail/CassandraMessageDAOV3.java      |  1 +
 .../cassandra/mail/CassandraMessageMapperTest.java | 47 ++++++++++++++++++++++
 .../store/search/SimpleMessageSearchIndexTest.java |  2 +-
 .../org/apache/james/mailbox/store/BatchSizes.java |  3 +-
 .../apache/james/mailbox/store/ResultUtils.java    |  3 +-
 .../mailbox/store/mail/FetchGroupConverter.java    |  8 +++-
 .../james/mailbox/store/mail/MessageMapper.java    |  9 ++++-
 .../store/mail/FetchGroupConverterTest.java        |  1 +
 .../store/mail/model/MessageMapperTest.java        |  5 ++-
 12 files changed, 77 insertions(+), 7 deletions(-)

diff --git a/mailbox/api/src/main/java/org/apache/james/mailbox/model/FetchGroup.java b/mailbox/api/src/main/java/org/apache/james/mailbox/model/FetchGroup.java
index fef49c09ea..396dbf5d16 100644
--- a/mailbox/api/src/main/java/org/apache/james/mailbox/model/FetchGroup.java
+++ b/mailbox/api/src/main/java/org/apache/james/mailbox/model/FetchGroup.java
@@ -36,6 +36,7 @@ public class FetchGroup extends Profiles<FetchGroup> {
     public enum Profile {
         MIME_DESCRIPTOR,
         HEADERS,
+        HEADERS_WITH_ATTACHMENTS_METADATA,
         FULL_CONTENT,
         BODY_CONTENT,
         MIME_HEADERS,
@@ -48,6 +49,7 @@ public class FetchGroup extends Profiles<FetchGroup> {
      */
     public static final FetchGroup MINIMAL = new FetchGroup(EnumSet.noneOf(Profile.class));
     public static final FetchGroup HEADERS = new FetchGroup(EnumSet.of(Profile.HEADERS));
+    public static final FetchGroup HEADERS_WITH_ATTACHMENTS_METADATA = new FetchGroup(EnumSet.of(Profile.HEADERS_WITH_ATTACHMENTS_METADATA));
     public static final FetchGroup FULL_CONTENT = new FetchGroup(EnumSet.of(Profile.FULL_CONTENT));
     public static final FetchGroup BODY_CONTENT = new FetchGroup(EnumSet.of(Profile.BODY_CONTENT));
 
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/AttachmentLoader.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/AttachmentLoader.java
index f37267caa2..c91d12b7b1 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/AttachmentLoader.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/AttachmentLoader.java
@@ -53,7 +53,7 @@ public class AttachmentLoader {
     }
 
     private Mono<List<MessageAttachmentMetadata>> loadAttachments(Stream<MessageAttachmentRepresentation> messageAttachmentRepresentations, MessageMapper.FetchType fetchType) {
-        if (fetchType == MessageMapper.FetchType.FULL) {
+        if (fetchType == MessageMapper.FetchType.FULL || fetchType == MessageMapper.FetchType.ATTACHMENTS_METADATA) {
             return getAttachments(messageAttachmentRepresentations.collect(ImmutableList.toImmutableList()));
         } else {
             return Mono.just(ImmutableList.of());
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAO.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAO.java
index 040b2b29b5..6c1ecec6d1 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAO.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAO.java
@@ -324,6 +324,7 @@ public class CassandraMessageDAO {
         switch (fetchType) {
             case FULL:
                 return getFullContent(headerId, bodyId);
+            case ATTACHMENTS_METADATA:
             case HEADERS:
                 return getContent(headerId, SIZE_BASED);
             case METADATA:
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3.java
index a7bf3669c4..575c8852fd 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3.java
@@ -397,6 +397,7 @@ public class CassandraMessageDAOV3 {
         switch (fetchType) {
             case FULL:
                 return getFullContent(headerId, bodyId);
+            case ATTACHMENTS_METADATA:
             case HEADERS:
                 return getContent(headerId, SIZE_BASED)
                     .map(ByteContent::new);
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageMapperTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageMapperTest.java
index a459de58ff..028d4cf0d0 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageMapperTest.java
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageMapperTest.java
@@ -23,8 +23,12 @@ import static org.apache.james.backends.cassandra.Scenario.Builder.fail;
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.awaitility.Durations.ONE_SECOND;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
 import java.time.Duration;
+import java.util.Date;
 import java.util.Iterator;
+import java.util.List;
 import java.util.Optional;
 
 import javax.mail.Flags;
@@ -39,12 +43,22 @@ import org.apache.james.mailbox.MessageManager;
 import org.apache.james.mailbox.cassandra.ids.CassandraId;
 import org.apache.james.mailbox.cassandra.ids.CassandraMessageId;
 import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.ByteContent;
+import org.apache.james.mailbox.model.Cid;
+import org.apache.james.mailbox.model.MessageAttachmentMetadata;
+import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.model.MessageRange;
+import org.apache.james.mailbox.model.ParsedAttachment;
+import org.apache.james.mailbox.model.ThreadId;
 import org.apache.james.mailbox.store.FlagsUpdateCalculator;
+import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper.FetchType;
 import org.apache.james.mailbox.store.mail.model.MailboxMessage;
 import org.apache.james.mailbox.store.mail.model.MapperProvider;
+import org.apache.james.mailbox.store.mail.model.MessageAssert;
 import org.apache.james.mailbox.store.mail.model.MessageMapperTest;
+import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
+import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage;
 import org.apache.james.util.streams.Limit;
 import org.apache.james.utils.UpdatableTickingClock;
 import org.assertj.core.api.SoftAssertions;
@@ -55,6 +69,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 
 import com.github.fge.lambdas.Throwing;
 import com.google.common.collect.ImmutableList;
+import com.google.common.io.ByteSource;
 
 class CassandraMessageMapperTest extends MessageMapperTest {
     @RegisterExtension
@@ -418,4 +433,36 @@ class CassandraMessageMapperTest extends MessageMapperTest {
             assertThat(messageMapper.getMailboxCounters(benwaInboxMailbox).getUnseen()).isEqualTo(4);
         }
     }
+
+    @Test
+    void messagesRetrievedUsingFetchTypeAttachmentsMetadataShouldNotHaveBodyDataLoaded() throws MailboxException, IOException {
+        saveMessages();
+        MessageMapper.FetchType fetchType = FetchType.ATTACHMENTS_METADATA;
+        MailboxMessage retrievedMessage = messageMapper.findInMailbox(benwaInboxMailbox, MessageRange.one(message1.getUid()), fetchType, 1).next();
+        MessageAssert.assertThat(retrievedMessage).isEqualToWithoutUid(message1, fetchType);
+        assertThat(retrievedMessage.getBodyContent().readAllBytes()).isEmpty();
+    }
+
+    @Test
+    void messagesRetrievedUsingFetchTypeAttachmentsMetadataShouldHaveAttachmentsMetadataLoaded() throws MailboxException {
+        MessageId messageId = mapperProvider.generateMessageId();
+        String content = "Subject: Test1 \n\nBody1\n.\n";
+        ParsedAttachment attachment1 = ParsedAttachment.builder()
+            .contentType("content")
+            .content(ByteSource.wrap("attachment".getBytes(StandardCharsets.UTF_8)))
+            .noName()
+            .cid(Cid.from("cid"))
+            .inline();
+        List<MessageAttachmentMetadata> messageAttachments = attachmentMapper.storeAttachments(ImmutableList.of(attachment1), messageId);
+        MailboxMessage message = new SimpleMailboxMessage(messageId, ThreadId.fromBaseMessageId(messageId), new Date(), content.length(), 16,
+            new ByteContent(content.getBytes()), new Flags(), new PropertyBuilder().build(), benwaInboxMailbox.getMailboxId(),
+            messageAttachments, Optional.empty());
+        messageMapper.add(benwaInboxMailbox, message);
+        message.setModSeq(messageMapper.getHighestModSeq(benwaInboxMailbox));
+
+        MessageMapper.FetchType fetchType = FetchType.ATTACHMENTS_METADATA;
+        MailboxMessage retrievedMessage = messageMapper.findInMailbox(benwaInboxMailbox, MessageRange.one(message.getUid()), fetchType, 1).next();
+
+        assertThat(retrievedMessage.getAttachments()).isEqualTo(message.getAttachments());
+    }
 }
diff --git a/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SimpleMessageSearchIndexTest.java b/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SimpleMessageSearchIndexTest.java
index 5e9c62d7de..0e36139300 100644
--- a/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SimpleMessageSearchIndexTest.java
+++ b/mailbox/scanning-search/src/test/java/org/apache/james/mailbox/store/search/SimpleMessageSearchIndexTest.java
@@ -239,7 +239,7 @@ class SimpleMessageSearchIndexTest extends AbstractMessageSearchIndexTest {
     
     @Test
     public void canCompareFetchTypes() {
-        assertThat(FetchType.values()).containsExactly(FetchType.METADATA, FetchType.HEADERS, FetchType.FULL);
+        assertThat(FetchType.values()).containsExactly(FetchType.METADATA, FetchType.HEADERS, FetchType.ATTACHMENTS_METADATA, FetchType.FULL);
         
         assertThat(SimpleMessageSearchIndex.maxFetchType(FetchType.METADATA, FetchType.METADATA)).isEqualTo(FetchType.METADATA);
         assertThat(SimpleMessageSearchIndex.maxFetchType(FetchType.METADATA, FetchType.HEADERS)).isEqualTo(FetchType.HEADERS);
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/BatchSizes.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/BatchSizes.java
index fd8b5648a3..07e4b0edcf 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/BatchSizes.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/BatchSizes.java
@@ -134,12 +134,13 @@ public class BatchSizes {
         switch (fetchType) {
             case METADATA:
                 return fetchMetadata;
+            case ATTACHMENTS_METADATA:
             case HEADERS:
                 return fetchHeaders;
             case FULL:
                 return fetchFull;
         }
-        throw new RuntimeException("Unknown fetchTpe: " + fetchType);
+        throw new RuntimeException("Unknown fetchType: " + fetchType);
     }
 
     public Optional<Integer> getCopyBatchSize() {
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/ResultUtils.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/ResultUtils.java
index 9846b159fa..f5fee1ddae 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/ResultUtils.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/ResultUtils.java
@@ -51,6 +51,7 @@ import com.google.common.annotations.VisibleForTesting;
 public class ResultUtils {
     private static final EnumSet<FetchGroup.Profile> SUPPORTED_GROUPS = EnumSet.of(
         FetchGroup.Profile.HEADERS,
+        FetchGroup.Profile.HEADERS_WITH_ATTACHMENTS_METADATA,
         FetchGroup.Profile.BODY_CONTENT,
         FetchGroup.Profile.FULL_CONTENT,
         FetchGroup.Profile.MIME_DESCRIPTOR);
@@ -205,7 +206,7 @@ public class ResultUtils {
         if (profiles.contains(FetchGroup.Profile.MIME_CONTENT)) {
             addMimeBodyContent(message, messageResult, mimePath);
         }
-        if (profiles.contains(FetchGroup.Profile.HEADERS)) {
+        if (profiles.contains(FetchGroup.Profile.HEADERS) || profiles.contains(FetchGroup.Profile.HEADERS_WITH_ATTACHMENTS_METADATA)) {
             addHeaders(message, messageResult, mimePath);
         }
         if (profiles.contains(FetchGroup.Profile.MIME_HEADERS)) {
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/FetchGroupConverter.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/FetchGroupConverter.java
index fb08d13858..dd4065cfb1 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/FetchGroupConverter.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/FetchGroupConverter.java
@@ -51,13 +51,17 @@ public class FetchGroupConverter {
         return reduce(fetchTypes);
     }
 
-    public static MessageMapper.FetchType reduce(Collection<MessageMapper.FetchType> fetchTypes) {
+    private static MessageMapper.FetchType reduce(Collection<MessageMapper.FetchType> fetchTypes) {
         boolean full = fetchTypes.contains(MessageMapper.FetchType.FULL);
         boolean headers = fetchTypes.contains(MessageMapper.FetchType.HEADERS);
+        boolean headersWithAttachmentsMetadata = fetchTypes.contains(MessageMapper.FetchType.ATTACHMENTS_METADATA);
 
         if (full) {
             return MessageMapper.FetchType.FULL;
         }
+        if (headersWithAttachmentsMetadata) {
+            return MessageMapper.FetchType.ATTACHMENTS_METADATA;
+        }
         if (headers) {
             return MessageMapper.FetchType.HEADERS;
         }
@@ -68,6 +72,8 @@ public class FetchGroupConverter {
         switch (profile) {
             case HEADERS:
                 return MessageMapper.FetchType.HEADERS;
+            case HEADERS_WITH_ATTACHMENTS_METADATA:
+                return MessageMapper.FetchType.ATTACHMENTS_METADATA;
             case BODY_CONTENT:
             case FULL_CONTENT:
             case MIME_CONTENT:
diff --git a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/MessageMapper.java b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/MessageMapper.java
index 3d25e3507f..f37068826f 100644
--- a/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/MessageMapper.java
+++ b/mailbox/store/src/main/java/org/apache/james/mailbox/store/mail/MessageMapper.java
@@ -307,7 +307,14 @@ public interface MessageMapper extends Mapper {
          * </p>
          */
         HEADERS,
-        
+        /**
+         * Fetch the {@link #HEADERS}, {@link Property}'s and the {@link #ATTACHMENTS_METADATA}'s for the {@link MailboxMessage}. This includes:
+         *
+         * <p>
+         * {@link MailboxMessage#getAttachments()}
+         * </p>
+         */
+        ATTACHMENTS_METADATA,
         /**
          * Fetch the complete {@link MailboxMessage}
          * 
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/FetchGroupConverterTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/FetchGroupConverterTest.java
index 9898c64ba0..d69459d8a5 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/FetchGroupConverterTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/FetchGroupConverterTest.java
@@ -39,6 +39,7 @@ class FetchGroupConverterTest {
         return Stream.of(
             Arguments.arguments(FetchGroup.MINIMAL, FetchType.METADATA),
             Arguments.arguments(FetchGroup.HEADERS, FetchType.HEADERS),
+            Arguments.arguments(FetchGroup.HEADERS_WITH_ATTACHMENTS_METADATA, FetchType.ATTACHMENTS_METADATA),
             Arguments.arguments(FetchGroup.BODY_CONTENT, FetchType.FULL),
             Arguments.arguments(FetchGroup.FULL_CONTENT, FetchType.FULL),
             Arguments.arguments(FetchGroup.BODY_CONTENT.with(Profile.HEADERS), FetchType.FULL),
diff --git a/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/MessageMapperTest.java b/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/MessageMapperTest.java
index f2b478d744..2a49fc1af6 100644
--- a/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/MessageMapperTest.java
+++ b/mailbox/store/src/test/java/org/apache/james/mailbox/store/mail/model/MessageMapperTest.java
@@ -53,6 +53,7 @@ import org.apache.james.mailbox.model.ThreadId;
 import org.apache.james.mailbox.model.UidValidity;
 import org.apache.james.mailbox.model.UpdatedFlags;
 import org.apache.james.mailbox.store.FlagsUpdateCalculator;
+import org.apache.james.mailbox.store.mail.AttachmentMapper;
 import org.apache.james.mailbox.store.mail.MailboxMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.MessageMapper.FetchType;
@@ -81,9 +82,10 @@ public abstract class MessageMapperTest {
 
     protected static final String USER_FLAG = "userFlag";
 
-    private MapperProvider mapperProvider;
+    protected MapperProvider mapperProvider;
     protected MessageMapper messageMapper;
     private MailboxMapper mailboxMapper;
+    protected AttachmentMapper attachmentMapper;
 
     protected Mailbox benwaInboxMailbox;
     protected Mailbox benwaWorkMailbox;
@@ -107,6 +109,7 @@ public abstract class MessageMapperTest {
 
         this.messageMapper = mapperProvider.createMessageMapper();
         this.mailboxMapper = mapperProvider.createMailboxMapper();
+        this.attachmentMapper = mapperProvider.createAttachmentMapper();
 
         initData();
     }


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


[james-project] 02/03: JAMES-3872 Add a JMAP read level that get preview of mail with attachments' metadata without getting body content

Posted by bt...@apache.org.
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 de44d8b5abffff1e4260799bfe2c0ac4fc110ac6
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Wed Dec 28 18:02:08 2022 +0700

    JAMES-3872 Add a JMAP read level that get preview of mail with attachments' metadata without getting body content
    
    fixup! Add a JMAP read level that get preview of mail with attachments' metadata without getting body content
---
 .../resources/eml/inlined-single-attachment.eml    |  30 ++
 .../rfc8621/contract/EmailGetMethodContract.scala  | 353 ++++++++++++++++++++-
 .../james/jmap/json/EmailGetSerializer.scala       |   9 +-
 .../scala/org/apache/james/jmap/mail/Email.scala   | 130 +++++++-
 .../org/apache/james/jmap/mail/EmailBodyPart.scala |  30 +-
 5 files changed, 540 insertions(+), 12 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/inlined-single-attachment.eml b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/inlined-single-attachment.eml
new file mode 100644
index 0000000000..82684c56b2
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/resources/eml/inlined-single-attachment.eml
@@ -0,0 +1,30 @@
+Date: Wed, 26 Jan 2022 12:21:37 +0100
+From: Bob <bo...@domain.tld>
+To: Alice <al...@domain.tld>
+Subject: My subject
+Message-ID: <20...@W0248292>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="7f4cfz6rtfqdbqxn"
+Content-Disposition: inline
+Content-Transfer-Encoding: 8bit
+
+--7f4cfz6rtfqdbqxn
+Content-Type: text/plain; charset=utf-8
+Content-Transfer-Encoding: 8bit
+
+Main test message...
+
+--7f4cfz6rtfqdbqxn
+Content-Type: application/json; charset=us-ascii
+Content-Disposition: attachment; filename="yyy.txt"
+Content-Transfer-Encoding: quoted-printable
+
+[
+    {
+        "Id": "2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+    }
+]
+
+--7f4cfz6rtfqdbqxn
+
+
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
index 2da1b0acd9..54191b1891 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailGetMethodContract.scala
@@ -19,9 +19,16 @@
 
 package org.apache.james.jmap.rfc8621.contract
 
+import java.io.ByteArrayInputStream
+import java.nio.charset.StandardCharsets
+import java.time.{Duration, ZonedDateTime}
+import java.util.Date
+import java.util.concurrent.TimeUnit
+
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.http.ContentType.JSON
+import javax.mail.Flags
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import net.javacrumbs.jsonunit.core.Option
 import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
@@ -49,12 +56,6 @@ import org.assertj.core.api.Assertions.assertThat
 import org.awaitility.Awaitility
 import org.junit.jupiter.api.{BeforeEach, Test}
 
-import java.nio.charset.StandardCharsets
-import java.time.{Duration, ZonedDateTime}
-import java.util.Date
-import java.util.concurrent.TimeUnit
-import javax.mail.Flags
-
 object EmailGetMethodContract {
   private def createTestMessage: Message = Message.Builder
       .of
@@ -4488,6 +4489,346 @@ trait EmailGetMethodContract {
          |}""".stripMargin)
   }
 
+  @Test
+  def shouldUseFullViewReaderWhenFetchAllBodyProperties(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(BOB)
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, path, AppendCommand.from(
+        ClassLoaderUtils.getSystemResourceAsSharedStream("eml/inlined-mixed.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |	"using": [
+         |		"urn:ietf:params:jmap:core",
+         |		"urn:ietf:params:jmap:mail"
+         |	],
+         |	"methodCalls": [
+         |		[
+         |			"Email/get",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"ids": ["${messageId.serialize}"],
+         |				"properties": [
+         |					"id",
+         |					"subject",
+         |					"from",
+         |					"to",
+         |					"cc",
+         |					"bcc",
+         |					"keywords",
+         |					"size",
+         |					"receivedAt",
+         |					"sentAt",
+         |					"preview",
+         |					"hasAttachment",
+         |					"attachments",
+         |					"replyTo",
+         |					"mailboxIds"
+         |				],
+         |				"fetchTextBodyValues": true
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].state")
+      .isEqualTo(
+      s"""{
+         |	"sessionState": "${SESSION_STATE.value}",
+         |	"methodResponses": [
+         |		[
+         |			"Email/get",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"notFound": [],
+         |				"list": [{
+         |					"preview": "Main test message...",
+         |					"to": [{
+         |						"name": "Alice",
+         |						"email": "alice@domain.tld"
+         |					}],
+         |					"id": "${messageId.serialize}",
+         |					"mailboxIds": {
+         |						"${mailboxId.serialize}": true
+         |					},
+         |					"from": [{
+         |						"name": "Bob",
+         |						"email": "bob@domain.tld"
+         |					}],
+         |					"keywords": {
+         |
+         |					},
+         |					"receivedAt": "$${json-unit.ignore}",
+         |					"sentAt": "$${json-unit.ignore}",
+         |					"hasAttachment": true,
+         |					"attachments": [{
+         |							"charset": "us-ascii",
+         |							"disposition": "attachment",
+         |							"size": 102,
+         |							"partId": "3",
+         |							"blobId": "${messageId.serialize}_3",
+         |							"name": "yyy.txt",
+         |							"type": "application/json"
+         |						},
+         |						{
+         |							"charset": "us-ascii",
+         |							"disposition": "attachment",
+         |							"size": 102,
+         |							"partId": "4",
+         |							"blobId": "${messageId.serialize}_4",
+         |							"name": "xxx.txt",
+         |							"type": "application/json"
+         |						}
+         |					],
+         |					"subject": "My subject",
+         |					"size": 970
+         |				}]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def shouldUseFastViewWithAttachmentMetadataWhenSupportedBodyProperties(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(BOB)
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, path, AppendCommand.from(
+        ClassLoaderUtils.getSystemResourceAsSharedStream("eml/inlined-mixed.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |	"using": [
+         |		"urn:ietf:params:jmap:core",
+         |		"urn:ietf:params:jmap:mail"
+         |	],
+         |	"methodCalls": [
+         |		[
+         |			"Email/get",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"ids": ["${messageId.serialize}"],
+         |				"properties": [
+         |					"id",
+         |					"subject",
+         |					"from",
+         |					"to",
+         |					"cc",
+         |					"bcc",
+         |					"keywords",
+         |					"size",
+         |					"receivedAt",
+         |					"sentAt",
+         |					"preview",
+         |					"hasAttachment",
+         |					"attachments",
+         |					"replyTo",
+         |					"mailboxIds"
+         |				],
+         |				"fetchTextBodyValues": true,
+         |				"bodyProperties": ["partId", "blobId", "size", "name", "type", "charset", "disposition", "cid", "headers"]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .whenIgnoringPaths("methodResponses[0][1].state")
+      .isEqualTo(
+      s"""{
+         |	"sessionState": "${SESSION_STATE.value}",
+         |	"methodResponses": [
+         |		[
+         |			"Email/get",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"notFound": [],
+         |				"list": [{
+         |					"preview": "Main test message...",
+         |					"to": [{
+         |						"name": "Alice",
+         |						"email": "alice@domain.tld"
+         |					}],
+         |					"id": "${messageId.serialize}",
+         |					"mailboxIds": {
+         |						"${mailboxId.serialize}": true
+         |					},
+         |					"from": [{
+         |						"name": "Bob",
+         |						"email": "bob@domain.tld"
+         |					}],
+         |					"keywords": {
+         |
+         |					},
+         |					"receivedAt": "$${json-unit.ignore}",
+         |					"sentAt": "$${json-unit.ignore}",
+         |					"hasAttachment": true,
+         |					"attachments": [{
+         |							"charset": "us-ascii",
+         |							"headers": [{
+         |									"name": "Content-Type",
+         |									"value": " application/json; charset=us-ascii"
+         |								},
+         |								{
+         |									"name": "Content-Disposition",
+         |									"value": "$${json-unit.ignore}"
+         |								},
+         |								{
+         |									"name": "Content-Transfer-Encoding",
+         |									"value": " quoted-printable"
+         |								}
+         |							],
+         |							"disposition": "attachment",
+         |							"size": 102,
+         |							"partId": "3",
+         |							"blobId": "${messageId.serialize}_3",
+         |							"name": "yyy.txt",
+         |							"type": "application/json"
+         |						},
+         |						{
+         |							"charset": "us-ascii",
+         |							"headers": [{
+         |									"name": "Content-Type",
+         |									"value": " application/json; charset=us-ascii"
+         |								},
+         |								{
+         |									"name": "Content-Disposition",
+         |									"value": "$${json-unit.ignore}"
+         |								},
+         |								{
+         |									"name": "Content-Transfer-Encoding",
+         |									"value": " quoted-printable"
+         |								}
+         |							],
+         |							"disposition": "attachment",
+         |							"size": 102,
+         |							"partId": "4",
+         |							"blobId": "${messageId.serialize}_4",
+         |							"name": "xxx.txt",
+         |							"type": "application/json"
+         |						}
+         |					],
+         |					"subject": "My subject",
+         |					"size": 970
+         |				}]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def shouldBeAbleToDownloadAttachmentBaseOnFastViewWithAttachmentsMetadataResult(server: GuiceJamesServer): Unit = {
+    val path = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(path)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(BOB.asString, path, AppendCommand.from(
+        ClassLoaderUtils.getSystemResourceAsSharedStream("eml/inlined-single-attachment.eml")))
+      .getMessageId
+
+    val request =
+      s"""{
+         |	"using": [
+         |		"urn:ietf:params:jmap:core",
+         |		"urn:ietf:params:jmap:mail"
+         |	],
+         |	"methodCalls": [
+         |		[
+         |			"Email/get",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"ids": ["${messageId.serialize}"],
+         |				"properties": [
+         |					"id",
+         |					"subject",
+         |					"from",
+         |					"to",
+         |					"cc",
+         |					"bcc",
+         |					"keywords",
+         |					"size",
+         |					"receivedAt",
+         |					"sentAt",
+         |					"preview",
+         |					"hasAttachment",
+         |					"attachments",
+         |					"replyTo",
+         |					"mailboxIds"
+         |				],
+         |				"fetchTextBodyValues": true,
+         |				"bodyProperties": ["blobId", "size", "name", "type", "charset", "disposition", "cid"]
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val blobId = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .jsonPath()
+      .getString("methodResponses[0][1].list[0].attachments[0].blobId")
+
+    val blob = `given`
+      .basePath("")
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .get(s"/download/29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6/$blobId")
+    .`then`
+      .statusCode(SC_OK)
+      .contentType("application/json")
+      .extract
+      .body
+      .asString
+
+    val expectedBlob: String =
+      """[
+        |    {
+        |        "Id": "2xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
+        |    }
+        |]""".stripMargin
+
+    assertThat(new ByteArrayInputStream(blob.getBytes(StandardCharsets.UTF_8)))
+      .hasContent(expectedBlob)
+  }
+
   @Test
   def textBodyValuesForComplexMultipart(server: GuiceJamesServer): Unit = {
     val path = MailboxPath.inbox(BOB)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
index a247daa1e2..4bff99d640 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailGetSerializer.scala
@@ -25,7 +25,7 @@ import org.apache.james.jmap.api.model.Size.Size
 import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, Preview}
 import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.{Properties, UuidState}
-import org.apache.james.jmap.mail.{AddressesHeaderValue, BlobId, Charset, DateHeaderValue, Disposition, EmailAddressGroup, EmailBody, EmailBodyMetadata, EmailBodyPart, EmailBodyValue, EmailChangesRequest, EmailChangesResponse, EmailFastView, EmailFullView, EmailGetRequest, EmailGetResponse, EmailHeader, EmailHeaderName, EmailHeaderValue, EmailHeaderView, EmailHeaders, EmailIds, EmailMetadata, EmailMetadataView, EmailNotFound, EmailView, FetchAllBodyValues, FetchHTMLBodyValues, FetchTextB [...]
+import org.apache.james.jmap.mail._
 import org.apache.james.mailbox.model.{Cid, MailboxId, MessageId}
 import play.api.libs.functional.syntax._
 import play.api.libs.json._
@@ -152,12 +152,18 @@ object EmailGetSerializer {
 
   private implicit val emailMetadataWrites: OWrites[EmailMetadata] = Json.writes[EmailMetadata]
   private implicit val emailHeadersWrites: Writes[EmailHeaders] = Json.writes[EmailHeaders]
+  private implicit val attachmentsMetadataWrites: Writes[AttachmentsMetadata] = Json.writes[AttachmentsMetadata]
   private implicit val emailBodyMetadataWrites: Writes[EmailBodyMetadata] = Json.writes[EmailBodyMetadata]
 
   private val emailFastViewWrites: OWrites[EmailFastView] = (JsPath.write[EmailMetadata] and
     JsPath.write[EmailHeaders] and
     JsPath.write[EmailBodyMetadata] and
     JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailFastView.unapply))
+  private val emailFastViewWithAttachmentsWrites: OWrites[EmailFastViewWithAttachments] = (JsPath.write[EmailMetadata] and
+    JsPath.write[EmailHeaders] and
+    JsPath.write[AttachmentsMetadata] and
+    JsPath.write[EmailBodyMetadata] and
+    JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailFastViewWithAttachments.unapply))
   private val emailHeaderViewWrites: OWrites[EmailHeaderView] = (JsPath.write[EmailMetadata] and
     JsPath.write[EmailHeaders] and
     JsPath.write[Map[String, Option[EmailHeaderValue]]]) (unlift(EmailHeaderView.unapply))
@@ -172,6 +178,7 @@ object EmailGetSerializer {
     case view: EmailMetadataView => emailMetadataViewWrites.writes(view)
     case view: EmailHeaderView => emailHeaderViewWrites.writes(view)
     case view: EmailFastView => emailFastViewWrites.writes(view)
+    case view: EmailFastViewWithAttachments => emailFastViewWithAttachmentsWrites.writes(view)
     case view: EmailFullView => emailFullViewWrites.writes(view)
   }
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
index 6650dce10d..bc6c039658 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Email.scala
@@ -35,9 +35,10 @@ import org.apache.james.jmap.core.Id.{Id, IdConstraint}
 import org.apache.james.jmap.core.{Properties, UTCDate}
 import org.apache.james.jmap.mail.BracketHeader.sanitize
 import org.apache.james.jmap.mail.EmailHeaderName.{ADDRESSES_NAMES, DATE, MESSAGE_ID_NAMES}
+import org.apache.james.jmap.mail.FastViewWithAttachmentsMetadataReadLevel.supportedByFastViewWithAttachments
 import org.apache.james.jmap.mail.KeywordsFactory.LENIENT_KEYWORDS_FACTORY
 import org.apache.james.jmap.method.ZoneIdProvider
-import org.apache.james.mailbox.model.FetchGroup.{FULL_CONTENT, HEADERS, MINIMAL}
+import org.apache.james.mailbox.model.FetchGroup.{FULL_CONTENT, HEADERS, HEADERS_WITH_ATTACHMENTS_METADATA, MINIMAL}
 import org.apache.james.mailbox.model.{FetchGroup, MailboxId, MessageId, MessageResult, ThreadId => JavaThreadId}
 import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
 import org.apache.james.mime4j.codec.DecodeMonitor
@@ -104,14 +105,16 @@ object ReadLevel {
   private val metadataProperty: Seq[NonEmptyString] = Seq("id", "size", "mailboxIds",
     "mailboxIds", "blobId", "threadId", "receivedAt")
   private val fastViewProperty: Seq[NonEmptyString] = Seq("preview", "hasAttachment")
-  private val fullProperty: Seq[NonEmptyString] = Seq("bodyStructure", "textBody", "htmlBody",
-    "attachments", "bodyValues")
+  private val attachmentsMetadataViewProperty: Seq[NonEmptyString] = Seq("attachments")
+  private val fullProperty: Seq[NonEmptyString] = Seq("bodyStructure", "textBody", "htmlBody", "bodyValues")
 
   def of(property: NonEmptyString): ReadLevel = if (metadataProperty.contains(property)) {
     MetadataReadLevel
   } else if (fastViewProperty.contains(property)) {
     FastViewReadLevel
-  }  else if (fullProperty.contains(property)) {
+  } else if (attachmentsMetadataViewProperty.contains(property)) {
+    FastViewWithAttachmentsMetadataReadLevel
+  } else if (fullProperty.contains(property)) {
     FullReadLevel
   } else {
     HeaderReadLevel
@@ -122,11 +125,13 @@ object ReadLevel {
     case FullReadLevel => FullReadLevel
     case HeaderReadLevel => readLevel2 match {
       case FullReadLevel => FullReadLevel
+      case FastViewWithAttachmentsMetadataReadLevel => FastViewWithAttachmentsMetadataReadLevel
       case FastViewReadLevel => FastViewReadLevel
       case _ => HeaderReadLevel
     }
     case FastViewReadLevel => readLevel2 match {
       case FullReadLevel => FullReadLevel
+      case FastViewWithAttachmentsMetadataReadLevel => FastViewWithAttachmentsMetadataReadLevel
       case _ => FastViewReadLevel
     }
   }
@@ -136,6 +141,17 @@ sealed trait ReadLevel
 case object MetadataReadLevel extends ReadLevel
 case object HeaderReadLevel extends ReadLevel
 case object FastViewReadLevel extends ReadLevel
+case object FastViewWithAttachmentsMetadataReadLevel extends ReadLevel {
+  private val availableFetchingBodyPropertiesForFastViewWithAttachments = Seq("partId", "blobId", "size", "name", "type", "charset", "disposition", "cid", "headers")
+
+  def supportedByFastViewWithAttachments(bodyProperties: Option[Properties]): Boolean =
+    bodyProperties.exists(supportedByFastViewWithAttachments)
+
+  private def supportedByFastViewWithAttachments(properties: Properties): Boolean =
+    properties.value
+      .map(availableFetchingBodyPropertiesForFastViewWithAttachments.contains)
+      .reduce(_&&_)
+}
 case object FullReadLevel extends ReadLevel
 
 object HeaderMessageId {
@@ -356,10 +372,18 @@ case class EmailFastView(metadata: EmailMetadata,
                          bodyMetadata: EmailBodyMetadata,
                          specificHeaders: Map[String, Option[EmailHeaderValue]]) extends EmailView
 
+case class EmailFastViewWithAttachments(metadata: EmailMetadata,
+                                        header: EmailHeaders,
+                                        attachments: AttachmentsMetadata,
+                                        bodyMetadata: EmailBodyMetadata,
+                                        specificHeaders: Map[String, Option[EmailHeaderValue]]) extends EmailView
+
+case class AttachmentsMetadata(attachments: List[EmailBodyPart])
 
 class EmailViewReaderFactory @Inject() (metadataReader: EmailMetadataViewReader,
                                         headerReader: EmailHeaderViewReader,
                                         fastViewReader: EmailFastViewReader,
+                                        fastViewWithAttachmentsMetadataReader: EmailFastViewWithAttachmentsMetadataReader,
                                         fullReader: EmailFullViewReader) {
   def selectReader(request: EmailGetRequest): EmailViewReader[EmailView] = {
     val readLevel: ReadLevel = request.properties
@@ -373,6 +397,12 @@ class EmailViewReaderFactory @Inject() (metadataReader: EmailMetadataViewReader,
       case MetadataReadLevel => metadataReader
       case HeaderReadLevel => headerReader
       case FastViewReadLevel => fastViewReader
+      case FastViewWithAttachmentsMetadataReadLevel =>
+        if (supportedByFastViewWithAttachments(request.bodyProperties)) {
+          fastViewWithAttachmentsMetadataReader
+        } else {
+          fullReader
+        }
       case FullReadLevel => fullReader
     }
   }
@@ -667,3 +697,95 @@ private class EmailFastViewReader @Inject()(messageIdManager: MessageIdManager,
     }
   }
 }
+
+private class EmailFastViewWithAttachmentsMetadataReader @Inject()(messageIdManager: MessageIdManager,
+                                                                   messageFastViewProjection: MessageFastViewProjection,
+                                                                   htmlTextExtractor: HtmlTextExtractor,
+                                                                   zoneIdProvider: ZoneIdProvider,
+                                                                   fullViewFactory: EmailFullViewFactory) extends EmailViewReader[EmailView] {
+  private val fullReader: GenericEmailViewReader[EmailFullView] = new GenericEmailViewReader[EmailFullView](messageIdManager, FULL_CONTENT, htmlTextExtractor, fullViewFactory)
+
+  override def read[T >: EmailView](ids: Seq[MessageId], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] =
+    SMono.fromPublisher(messageFastViewProjection.retrieve(ids.asJava))
+      .map(_.asScala.toMap)
+      .map(fastViews => ids.map(id => fastViews.get(id)
+        .map(FastViewAvailable(id, _))
+        .getOrElse(FastViewUnavailable(id))))
+      .flatMapMany(results => toEmailViews(results, request, mailboxSession))
+
+  private def toEmailViews[T >: EmailView](results: Seq[FastViewResult], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[T] = {
+    val availables: Seq[FastViewAvailable] = results.flatMap {
+      case available: FastViewAvailable => Some(available)
+      case _ => None
+    }
+    val unavailables: Seq[FastViewUnavailable] = results.flatMap {
+      case unavailable: FastViewUnavailable => Some(unavailable)
+      case _ => None
+    }
+
+    SFlux.merge(Seq(
+      toFastViews(availables, request, mailboxSession),
+      fullReader.read(unavailables.map(_.id), request, mailboxSession)
+        .doOnNext(storeOnCacheMisses)))
+  }
+
+  private def storeOnCacheMisses(fullView: EmailFullView) = {
+    SMono.fromPublisher(messageFastViewProjection.store(
+      fullView.metadata.id,
+      MessageFastViewPrecomputedProperties.builder()
+        .preview(fullView.bodyMetadata.preview)
+        .hasAttachment(fullView.bodyMetadata.hasAttachment.value)
+        .build()))
+      .doOnError(e => EmailFastViewReader.logger.error(s"Cannot store the projection to MessageFastViewProjection for ${fullView.metadata.id}", e))
+      .subscribeOn(Schedulers.parallel())
+      .subscribe()
+  }
+
+  private def toFastViews(fastViews: Seq[FastViewAvailable], request: EmailGetRequest, mailboxSession: MailboxSession): SFlux[EmailView] ={
+    val fastViewsAsMap: Map[MessageId, MessageFastViewPrecomputedProperties] = fastViews.map(e => (e.id, e.fastView)).toMap
+    val ids: Seq[MessageId] = fastViews.map(_.id)
+
+    SFlux.fromPublisher(messageIdManager.getMessagesReactive(ids.asJava, HEADERS_WITH_ATTACHMENTS_METADATA, mailboxSession))
+      .collectSeq()
+      .flatMapIterable(messages => messages.groupBy(_.getMessageId).toSet)
+      .map(x => toEmail(request)(x, fastViewsAsMap(x._1)))
+      .handle[EmailView]((aTry, sink) => aTry match {
+        case Success(value) => sink.next(value)
+        case Failure(e) => sink.error(e)
+      })
+  }
+
+  private def toEmail(request: EmailGetRequest)(message: (MessageId, Seq[MessageResult]), fastView: MessageFastViewPrecomputedProperties): Try[EmailView] = {
+    val messageId: MessageId = message._1
+    val mailboxIds: MailboxIds = MailboxIds(message._2
+      .map(_.getMailboxId)
+      .toList)
+    val threadId: ThreadId = ThreadId(message._2.head.getThreadId.serialize())
+
+    for {
+      firstMessage <- message._2
+        .headOption
+        .map(Success(_))
+        .getOrElse(Failure(new IllegalArgumentException("No message supplied")))
+      mime4JMessage <- Email.parseAsMime4JMessage(firstMessage)
+      blobId <- BlobId.of(messageId)
+      keywords <- LENIENT_KEYWORDS_FACTORY.fromFlags(firstMessage.getFlags)
+    } yield {
+      EmailFastViewWithAttachments(
+        metadata = EmailMetadata(
+          id = messageId,
+          blobId = blobId,
+          threadId = threadId,
+          mailboxIds = mailboxIds,
+          receivedAt = UTCDate.from(firstMessage.getInternalDate, zoneIdProvider.get()),
+          size = sanitizeSize(firstMessage.getSize),
+          keywords = keywords),
+        bodyMetadata = EmailBodyMetadata(
+          hasAttachment = HasAttachment(fastView.hasAttachment),
+          preview = fastView.getPreview),
+        header = EmailHeaders.from(zoneIdProvider.get())(mime4JMessage),
+        specificHeaders = EmailHeaders.extractSpecificHeaders(request.properties)(zoneIdProvider.get(), mime4JMessage),
+        attachments = AttachmentsMetadata(firstMessage.getLoadedAttachments.asScala.toList.map(EmailBodyPart.fromAttachment(_, mime4JMessage))))
+    }
+  }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
index 0824cf5fbe..f3af9e778f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailBodyPart.scala
@@ -28,11 +28,12 @@ import eu.timepit.refined.auto._
 import eu.timepit.refined.numeric.NonNegative
 import eu.timepit.refined.refineV
 import org.apache.commons.io.IOUtils
+import org.apache.james.jmap.api.model.Size
 import org.apache.james.jmap.api.model.Size.Size
 import org.apache.james.jmap.core.Properties
 import org.apache.james.jmap.mail.EmailBodyPart.{FILENAME_PREFIX, MULTIPART_ALTERNATIVE, TEXT_HTML, TEXT_PLAIN}
 import org.apache.james.jmap.mail.PartId.PartIdValue
-import org.apache.james.mailbox.model.{Cid, MessageId, MessageResult}
+import org.apache.james.mailbox.model.{Cid, MessageAttachmentMetadata, MessageId, MessageResult}
 import org.apache.james.mime4j.codec.{DecodeMonitor, DecoderUtil}
 import org.apache.james.mime4j.dom.field.{ContentDispositionField, ContentLanguageField, ContentTypeField, FieldName}
 import org.apache.james.mime4j.dom.{Entity, Message, Multipart, TextBody => Mime4JTextBody}
@@ -41,6 +42,7 @@ import org.apache.james.mime4j.stream.{Field, MimeConfig, RawField}
 import org.apache.james.util.html.HtmlTextExtractor
 
 import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
 import scala.util.{Failure, Success, Try}
 
 object PartId {
@@ -80,6 +82,32 @@ object EmailBodyPart {
     mime4JMessage.flatMap(of(messageId, _))
   }
 
+  def fromAttachment(attachment: MessageAttachmentMetadata, entity: Message): EmailBodyPart = {
+    def parseDisposition(attachment: MessageAttachmentMetadata): Option[Disposition] =
+      if (attachment.isInline) {
+        Option(Disposition.INLINE)
+      } else {
+        Option(Disposition.ATTACHMENT)
+      }
+
+    def parsePartIdFromBlobId(blobId: String): PartId =
+      PartId(blobId.substring(blobId.lastIndexOf("_") + 1).asInstanceOf[PartIdValue])
+
+    EmailBodyPart(partId = parsePartIdFromBlobId(attachment.getAttachmentId.getId),
+      blobId = BlobId.of(attachment.getAttachmentId.getId).toOption,
+      headers = entity.getHeader.getFields.asScala.toList.map(EmailHeader(_)),
+      size = Size.sanitizeSize(attachment.getAttachment.getSize),
+      name = attachment.getName.map(Name(_)).toScala,
+      `type` = Type(attachment.getAttachment.getType.mimeType().asString()),
+      charset = attachment.getAttachment.getType.charset().map(charset => Charset(charset.name())).toScala,
+      disposition = parseDisposition(attachment),
+      cid = attachment.getCid.toScala,
+      language = Option.empty,
+      location = Option.empty,
+      subParts = Option.empty,
+      entity = entity)
+  }
+
   def of(messageId: MessageId, message: Message): Try[EmailBodyPart] =
     of(messageId, PartId(1), message).map(_._1)
 


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