This is an automated email from the ASF dual-hosted git repository.

quantranhong1999 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 04ba203834 [ENHANCEMENT] Remove bidi control chars from attachment 
names
04ba203834 is described below

commit 04ba2038348314c33c620e84815aedf86dc8fa81
Author: Benoit TELLIER <[email protected]>
AuthorDate: Sat Sep 26 20:27:09 2026 +0200

    [ENHANCEMENT] Remove bidi control chars from attachment names
---
 .../scala/org/apache/james/jmap/mail/EmailBodyPart.scala     | 12 +++++++++---
 .../scala/org/apache/james/jmap/routes/DownloadRoutes.scala  |  6 ++++--
 2 files changed, 13 insertions(+), 5 deletions(-)

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 94238ce54c..00a114d3ca 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
@@ -87,7 +87,7 @@ object EmailBodyPart {
       blobId = BlobId.of(attachment.getAttachmentId.getId).toOption,
       headers = List(),
       size = Size.sanitizeSize(attachment.getAttachment.getSize),
-      name = attachment.getName.map(Name(_)).toScala,
+      name = attachment.getName.map(Name.of(_)).toScala,
       `type` = Type(attachment.getAttachment.getType.mimeType().asString()),
       charset = attachment.getAttachment.getType.charset().map(charset => 
Charset(charset.name().toUpperCase(Locale.US))).toScala,
       disposition = parseDisposition(attachment),
@@ -183,13 +183,19 @@ object Name {
       case contentTypeField: ContentTypeField => 
Option(contentTypeField.getParameter(FILENAME_PREFIX))
           .map(DecoderUtil.decodeEncodedWords(_, DecodeMonitor.SILENT))
       case _ => None
-    }.map(Name(_))
+    }.map(Name.of(_))
     .orElse(Option(entity.getHeader.getField(FieldName.CONTENT_DISPOSITION))
       .flatMap {
         case contentDispositionField: ContentDispositionField => 
Option(contentDispositionField.getFilename)
           .map(DecoderUtil.decodeEncodedWords(_, DecodeMonitor.SILENT))
         case _ => None
-      }.map(Name(_)))
+      }.map(Name.of(_)))
+
+  private val BIDI_CONTROL_CHARACTERS = 
"[\u200E\u200F\u202A-\u202E\u2066-\u2069]"
+
+  def normalize(value: String): String = 
value.replaceAll(BIDI_CONTROL_CHARACTERS, "")
+
+  def of(value: String): Name = Name(normalize(value))
 }
 
 case class Name(value: String) extends AnyVal
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 8481609322..b7129d0295 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
@@ -45,7 +45,7 @@ import org.apache.james.jmap.exceptions.UnauthorizedException
 import org.apache.james.jmap.http.Authenticator
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.json.ResponseSerializer
-import org.apache.james.jmap.mail.{BlobId, MinimalEmailBodyPart}
+import org.apache.james.jmap.mail.{BlobId, MinimalEmailBodyPart, Name}
 import org.apache.james.jmap.method.{AccountNotFoundException, ZoneIdProvider}
 import org.apache.james.jmap.routes.DownloadRoutes.{BUFFER_SIZE, LOGGER}
 import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
@@ -419,7 +419,8 @@ class DownloadRoutes 
@Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
   private def addCacheControlHeader(): HttpServerResponse => 
HttpServerResponse =
     resp => resp.header(HttpHeaderNames.CACHE_CONTROL, "private, immutable, 
max-age=31536000")
 
-  private def addContentDispositionHeaderRegardingEncoding(name: String, resp: 
HttpServerResponse): HttpServerResponse =
+  private def addContentDispositionHeaderRegardingEncoding(rawName: String, 
resp: HttpServerResponse): HttpServerResponse = {
+    val name = Name.normalize(rawName)
     if (CharMatcher.ascii.matchesAllOf(name)) {
       Try(resp.header("Content-Disposition", "attachment; filename=\"" + name 
+ "\""))
         // Can fail if the file name contains valid ascii character that are 
invalid in a contentDisposition header
@@ -427,6 +428,7 @@ class DownloadRoutes 
@Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
     } else {
       resp.header("Content-Disposition", encodedFileName(name))
     }
+  }
 
   private def encodedFileName(name: String) = "attachment; filename*=\"" + 
EncoderUtil.encodeEncodedWord(name, Usage.TEXT_TOKEN) + "\""
 


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

Reply via email to