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
The following commit(s) were added to refs/heads/master by this push:
new 094f441ea3 JAMES-3872 JMAP Email/get: Fix and test attachment read
level (#2868)
094f441ea3 is described below
commit 094f441ea32daabde8dcf30bfae814fb1dc69d91
Author: Benoit TELLIER <[email protected]>
AuthorDate: Sat Nov 29 07:20:46 2025 +0100
JAMES-3872 JMAP Email/get: Fix and test attachment read level (#2868)
---
.../jmap/rfc8621/distributed/ReadLevelTest.java | 34 +++-
.../rfc8621/contract/EmailGetMethodContract.scala | 194 +++++++++++++++++++++
.../rfc8621/contract/EmailSetMethodContract.scala | 2 +-
.../scala/org/apache/james/jmap/mail/Email.scala | 5 +-
.../org/apache/james/jmap/mail/EmailBodyPart.scala | 5 +-
5 files changed, 232 insertions(+), 8 deletions(-)
diff --git
a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/ReadLevelTest.java
b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/ReadLevelTest.java
index 67cd602b8c..197302f0a2 100644
---
a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/ReadLevelTest.java
+++
b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/ReadLevelTest.java
@@ -155,7 +155,7 @@ class ReadLevelTest {
" \"accountId\":
\"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6\"," +
" \"ids\": [\"" + messageId.serialize() + "\"]," +
" \"properties\": [\"id\", \"size\", \"mailboxIds\",
\"mailboxIds\", \"blobId\", " +
- " \"threadId\", \"receivedAt\"]" +
+ " \"threadId\", \"receivedAt\",
\"keywords\"]" +
" }," +
" \"c1\"]]" +
"} ";
@@ -230,6 +230,38 @@ class ReadLevelTest {
.hasSize(1);
}
+ @Test
+ void gettingAttachmentDetailsShouldReadBlobOnce(GuiceJamesServer server) {
+ StatementRecorder statementRecorder =
server.getProbe(TestingSessionProbe.class)
+ .getTestingSession()
+ .recordStatements();
+
+ String request = "{" +
+ " \"using\": [\"urn:ietf:params:jmap:core\",
\"urn:ietf:params:jmap:mail\"]," +
+ " \"methodCalls\": [[" +
+ " \"Email/get\"," +
+ " {" +
+ " \"accountId\":
\"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6\"," +
+ " \"ids\": [\"" + messageId.serialize() + "\"]," +
+ " \"bodyProperties\":[\"blobId\", \"name\", \"type\"]," +
+ " \"properties\": [\"id\", \"size\", \"mailboxIds\",
\"mailboxIds\", \"blobId\", " +
+ " \"threadId\", \"receivedAt\",
\"messageId\", \"inReplyTo\", " +
+ " \"references\", \"to\", \"cc\", \"bcc\",
\"from\", \"sender\", " +
+ " \"replyTo\", \"subject\", \"headers\",
\"header:anything\", " +
+ " \"preview\", \"hasAttachment\",
\"attachments\"]" +
+ " }," +
+ " \"c1\"]]" +
+ "} ";
+ with()
+ .header(HttpHeaderNames.ACCEPT.toString(),
Fixture.ACCEPT_RFC8621_VERSION_HEADER())
+ .body(request)
+ .post();
+
+ assertThat(statementRecorder.listExecutedStatements(
+ StatementRecorder.Selector.preparedStatementStartingWith("SELECT *
FROM blobs")))
+ .hasSize(1);
+ }
+
@Test
void
gettingEmailFastViewShouldReadBlobTwiceUponCacheMisses(GuiceJamesServer server)
{
server.getProbe(JmapGuiceProbe.class).clearMessageFastViewProjection();
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 2d98222a51..0503e12a81 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
@@ -5085,6 +5085,119 @@ trait EmailGetMethodContract {
|}""".stripMargin)
}
+ @Test
+ def
shouldUseFastViewWithAttachmentMetadataWhenSupportedBodyPropertiesAtAttachmentReadLevel(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": ["size", "name",
"type", "charset", "disposition", "cid"]
+ | },
+ | "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",
"methodResponses[0][1].list[0].attachments[0].blobId",
"methodResponses[0][1].list[0].attachments[1].blobId")
+ .isEqualTo(
+ s"""{
+ | "sessionState": "${SESSION_STATE.value}",
+ | "methodResponses": [
+ | [
+ | "Email/get",
+ | {
+ | "accountId":
"29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "notFound": [],
+ | "list": [{
+ | "preview": "Main test
message...",
+ | "to": [{
+ | "name": "Alice",
+ | "email":
"[email protected]"
+ | }],
+ | "id": "${messageId.serialize}",
+ | "mailboxIds": {
+ |
"${mailboxId.serialize}": true
+ | },
+ | "from": [{
+ | "name": "Bob",
+ | "email":
"[email protected]"
+ | }],
+ | "keywords": {
+ |
+ | },
+ | "receivedAt":
"$${json-unit.ignore}",
+ | "sentAt":
"$${json-unit.ignore}",
+ | "hasAttachment": true,
+ | "attachments": [{
+ | "charset":
"US-ASCII",
+ | "disposition":
"attachment",
+ | "size": 102,
+ | "name":
"yyy.txt",
+ | "type":
"application/json"
+ | },
+ | {
+ | "charset":
"US-ASCII",
+ | "disposition":
"attachment",
+ | "size": 102,
+ | "name":
"xxx.txt",
+ | "type":
"application/json"
+ | }
+ | ],
+ | "subject": "My subject",
+ | "size": 1011
+ | }]
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
@Test
def
shouldBeAbleToDownloadAttachmentBaseOnFastViewWithAttachmentsMetadataResult(server:
GuiceJamesServer): Unit = {
val path = MailboxPath.inbox(BOB)
@@ -5145,7 +5258,88 @@ trait EmailGetMethodContract {
val blob = `given`
.basePath("")
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER).log().all()
+ .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
shouldBeAbleToDownloadAttachmentBaseOnFastViewWithAttachmentsMetadataResultWithReadLevelFll(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", "partId",
"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).log().all()
.when
.get(s"/download/29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6/$blobId")
.`then`
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/EmailSetMethodContract.scala
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index fb494af4e8..0a2047e40c 100644
---
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -4992,7 +4992,7 @@ trait EmailSetMethodContract {
|}""".stripMargin)
assertThatJson(response)
- .inPath(s"methodResponses[1][1].list[0]")
+ .inPath("methodResponses[1][1].list[0]")
.isEqualTo(
s"""{
| "id": "$messageId",
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 a0de324aea..0a77ab425e 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
@@ -163,7 +163,7 @@ case class UnparsedEmailId(id: Id)
object ReadLevel {
private val metadataProperty: Seq[NonEmptyString] = Seq("id", "size",
"mailboxIds",
- "mailboxIds", "blobId", "threadId", "receivedAt")
+ "mailboxIds", "blobId", "threadId", "receivedAt", "keywords")
private val fastViewProperty: Seq[NonEmptyString] = Seq("preview",
"hasAttachment")
private val attachmentsMetadataViewProperty: Seq[NonEmptyString] =
Seq("attachments")
private val fullProperty: Seq[NonEmptyString] = Seq("bodyStructure",
"textBody", "htmlBody", "bodyValues")
@@ -203,13 +203,14 @@ 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")
+ private val availableFetchingBodyPropertiesForFastViewWithAttachments =
Seq("blobId", "size", "name", "type", "charset", "disposition", "cid")
def supportedByFastViewWithAttachments(bodyProperties: Option[Properties]):
Boolean =
bodyProperties.exists(supportedByFastViewWithAttachments)
private def supportedByFastViewWithAttachments(properties: Properties):
Boolean =
properties.value
+ .map(s => s.value)
.map(availableFetchingBodyPropertiesForFastViewWithAttachments.contains)
.reduce(_&&_)
}
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 b885e079fd..750667014b 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
@@ -82,10 +82,7 @@ object EmailBodyPart {
Option(Disposition.ATTACHMENT)
}
- def parsePartIdFromBlobId(blobId: String): PartId =
- PartId(blobId.substring(blobId.lastIndexOf("_") +
1).asInstanceOf[PartIdValue])
-
- EmailBodyPart(partId =
parsePartIdFromBlobId(attachment.getAttachmentId.getId),
+ EmailBodyPart(partId = PartId.parse("1").get,
blobId = BlobId.of(attachment.getAttachmentId.getId).toOption,
headers = entity.getHeader.getFields.asScala.toList.map(EmailHeader(_)),
size = Size.sanitizeSize(attachment.getAttachment.getSize),
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]