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]

Reply via email to