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:21 UTC
[james-project] 02/03: JAMES-3872 Add a JMAP read level that get preview of mail with attachments' metadata without getting body content
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