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 <hqt...@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 <b...@domain.tld>
+To: Alice <al...@domain.tld>
+Subject: My subject
+Message-ID: <20220126112137.wookj26xellphlam@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": 
"al...@domain.tld"
+         |                                     }],
+         |                                     "id": "${messageId.serialize}",
+         |                                     "mailboxIds": {
+         |                                             
"${mailboxId.serialize}": true
+         |                                     },
+         |                                     "from": [{
+         |                                             "name": "Bob",
+         |                                             "email": 
"b...@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": 
"al...@domain.tld"
+         |                                     }],
+         |                                     "id": "${messageId.serialize}",
+         |                                     "mailboxIds": {
+         |                                             
"${mailboxId.serialize}": true
+         |                                     },
+         |                                     "from": [{
+         |                                             "name": "Bob",
+         |                                             "email": 
"b...@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-unsubscr...@james.apache.org
For additional commands, e-mail: notifications-h...@james.apache.org

Reply via email to