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]