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 dbb0b9a751 JAMES-4172 Fix truncated downloads for non-SingleBody 
EmailBodyPart b… (#2940)
dbb0b9a751 is described below

commit dbb0b9a7518d3ddb6b328d6c7060e5f6fa50f1d9
Author: Ömer Faruk İÇEN <[email protected]>
AuthorDate: Fri Feb 27 11:39:00 2026 +0300

    JAMES-4172 Fix truncated downloads for non-SingleBody EmailBodyPart b… 
(#2940)
    
    In EmailBodyPartBlob, the size and content methods used different
    calculation paths for non-SingleBody parts (e.g. message/rfc822):
    - size relied on SizeUtils.sizeOf(entity) which returns the decoded
      size via SingleBody.size()
    - content used DefaultMessageWriter.writeBody() which re-encodes
      the content (e.g. base64), producing more bytes than the decoded size
    
    This mismatch caused Content-Length to be set smaller than the actual
    response body, resulting in truncated .eml downloads with missing
    attachments and MIME boundaries.
    
    Fix by caching the written body bytes in a lazy val so that both size
    and content use the same data, ensuring Content-Length matches the
    actual stream length.
    
    Co-authored-by: omerfarukicen <[email protected]>
---
 .../contract/EmailParseMethodContract.scala        |  2 +-
 .../apache/james/jmap/routes/DownloadRoutes.scala  | 26 ++++++++++++++++------
 2 files changed, 20 insertions(+), 8 deletions(-)

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/EmailParseMethodContract.scala
 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailParseMethodContract.scala
index 18508647a8..85cf17c59a 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailParseMethodContract.scala
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailParseMethodContract.scala
@@ -327,7 +327,7 @@ trait EmailParseMethodContract {
            |                            }
            |                        ],
            |                        "subject": "test subject",
-           |                        "size": 797,
+           |                        "size": 807,
            |                        "blobId": "${messageId.serialize()}_3",
            |                        "preview": "test body",
            |                        "messageId": [
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
index cd590eed51..ffbd68c5cb 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/DownloadRoutes.scala
@@ -129,17 +129,29 @@ case class AttachmentBlob(attachmentMetadata: 
AttachmentMetadata, fileContent: I
 }
 
 case class EmailBodyPartBlob(blobId: BlobId, part: MinimalEmailBodyPart) 
extends Blob {
-  override def size: Try[Size] = part.size
-
-  override def contentType: ContentType = ContentType.of(part.`type`.value)
-
-  override def content: InputStream = part.entity.getBody match {
-    case body: SingleBody => body.getInputStream
+  private lazy val writtenBodyContent: 
Option[UnsynchronizedByteArrayOutputStream] = part.entity.getBody match {
+    case _: SingleBody => None
     case body =>
       val writer = new DefaultMessageWriter
       val outputStream = new UnsynchronizedByteArrayOutputStream()
       writer.writeBody(body, outputStream)
-      outputStream.toInputStream
+      Some(outputStream)
+  }
+
+  override def size: Try[Size] = writtenBodyContent match {
+    case Some(outputStream) =>
+      refineV[NonNegative](outputStream.size().toLong) match {
+        case Left(e) => Failure(new IllegalArgumentException(e))
+        case Right(s) => Success(s)
+      }
+    case None => part.size
+  }
+
+  override def contentType: ContentType = ContentType.of(part.`type`.value)
+
+  override def content: InputStream = writtenBodyContent match {
+    case Some(outputStream) => outputStream.toInputStream
+    case None => part.entity.getBody.asInstanceOf[SingleBody].getInputStream
   }
 }
 


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to