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

chibenwa 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 a1ec79bdd4 JAMES-3536 Email/set create: reject bodyStructure and 
unknown properties (#3234)
a1ec79bdd4 is described below

commit a1ec79bdd4667c062ab81307e1c2b9ac7d987a08
Author: Benoit TELLIER <[email protected]>
AuthorDate: Mon Oct 5 11:47:43 2026 +0200

    JAMES-3536 Email/set create: reject bodyStructure and unknown properties 
(#3234)
    
    bodyStructure is not supported on Email/set create: it used to be
    silently dropped, storing an email with an empty body. Unknown
    properties were silently ignored as well.
    
    Reject both with an invalidProperties SetError (RFC 8621 section 4.6,
    RFC 8620 section 5.3) listing the offending properties.
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 100 +++++++++++++++++++++
 .../doc/specs/spec/mail/message.mdown              |   3 +-
 .../org/apache/james/jmap/mail/EmailSet.scala      |  32 +++++++
 .../jmap/method/EmailSetCreatePerformer.scala      |   8 +-
 4 files changed, 140 insertions(+), 3 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/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 e33d80215a..9aa0809735 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
@@ -666,6 +666,106 @@ trait EmailSetMethodContract {
            |}""".stripMargin)
   }
 
+  @Test
+  def createShouldRejectBodyStructure(server: GuiceJamesServer): Unit = {
+    val mailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(bobUsername))
+
+    val response = createEmail(
+      s"""{
+         |  "mailboxIds": {"${mailboxId.serialize}": true},
+         |  "subject": "Title",
+         |  "bodyValues": {"t": {"value": "Hello in text"}, "h": {"value": 
"<p>Hello in HTML</p>"}},
+         |  "bodyStructure": {"type": "multipart/alternative", "subParts": [
+         |    {"partId": "t", "type": "text/plain"},
+         |    {"partId": "h", "type": "text/html"}]}
+         |}""".stripMargin)
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |  "accountId": "$bobAccountId",
+           |  "oldState": "$${json-unit.ignore}",
+           |  "newState": "$${json-unit.ignore}",
+           |  "notCreated": {
+           |    "aaaaaa": {
+           |      "type": "invalidProperties",
+           |      "description": "'bodyStructure' is not supported on 
Email/set create, use 'textBody', 'htmlBody' and 'attachments' instead",
+           |      "properties": ["bodyStructure"]
+           |    }
+           |  }
+           |}""".stripMargin)
+  }
+
+  @Test
+  def createShouldRejectBodyStructureCombinedWithHtmlBody(server: 
GuiceJamesServer): Unit = {
+    val mailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(bobUsername))
+
+    val response = createEmail(
+      s"""{
+         |  "mailboxIds": {"${mailboxId.serialize}": true},
+         |  "subject": "Title",
+         |  "bodyValues": {"h": {"value": "<p>Hello in HTML</p>"}},
+         |  "htmlBody": [{"partId": "h", "type": "text/html"}],
+         |  "bodyStructure": {"partId": "h", "type": "text/html"}
+         |}""".stripMargin)
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated.aaaaaa")
+      .isEqualTo(
+        s"""{
+           |  "type": "invalidProperties",
+           |  "description": "'bodyStructure' is not supported on Email/set 
create, use 'textBody', 'htmlBody' and 'attachments' instead",
+           |  "properties": ["bodyStructure"]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def createShouldRejectUnknownProperties(server: GuiceJamesServer): Unit = {
+    val mailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(bobUsername))
+
+    val response = createEmail(
+      s"""{
+         |  "mailboxIds": {"${mailboxId.serialize}": true},
+         |  "subject": "Title",
+         |  "bodyValues": {"t": {"value": "Hello in text"}},
+         |  "textBody": [{"partId": "t", "type": "text/plain"}],
+         |  "notAnEmailProperty": true
+         |}""".stripMargin)
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated.aaaaaa")
+      .isEqualTo(
+        s"""{
+           |  "type": "invalidProperties",
+           |  "description": "Unknown or unsupported properties on Email/set 
create: notAnEmailProperty",
+           |  "properties": ["notAnEmailProperty"]
+           |}""".stripMargin)
+  }
+
+  private def createEmail(creationRequest: String): String =
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", 
"urn:ietf:params:jmap:mail"],
+           |  "methodCalls": [
+           |    ["Email/set", {
+           |      "accountId": "$bobAccountId",
+           |      "create": {
+           |        "aaaaaa": $creationRequest
+           |      }
+           |    }, "c1"]]
+           |}""".stripMargin)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
   @Test
   def shouldNotResetKeywordWhenFalseValue(server: GuiceJamesServer): Unit = {
     val message: Message = Fixture.createTestMessage
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown 
b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
index cb11c2f0a9..969fda963f 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/message.mdown
@@ -832,7 +832,8 @@ Due to the format of the Email object, when creating an 
Email there are a number
   attachments properties.
 
 > :warning:
-> Not implemented. *bodyStructure* property is not supported in Email/set 
create.
+> Not implemented. *bodyStructure* property is not supported in Email/set 
create: such a creation
+> request is rejected with an `invalidProperties` error. So are creation 
requests with unknown properties.
 
 - If given, the bodyStructure EmailBodyPart MUST NOT contain a property
   representing a header field that is already defined on the top-level Email
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index 500c5ba0a5..69eeaf3ac7 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -176,8 +176,40 @@ case class UncheckedAddressesHeaderValue(value: 
List[UncheckedEmailAddress]) {
     .map(l => AddressesHeaderValue(l))
 }
 
+case class InvalidEmailCreationPropertiesException(properties: Set[String], 
description: String) extends IllegalArgumentException(description)
+
 object EmailCreationRequest {
   val KEYWORD_DRAFT: Keyword = org.apache.james.jmap.mail.Keyword("$draft")
+
+  private val SPECIFIC_HEADER_PREFIX: String = "header:"
+  private val BODY_STRUCTURE: String = "bodyStructure"
+  private val SUPPORTED_PROPERTIES: Set[String] = Set("mailboxIds", 
"messageId", "references", "inReplyTo", "from", "to",
+    "cc", "bcc", "sender", "replyTo", "subject", "sentAt", "keywords", 
"receivedAt", "htmlBody", "textBody", "bodyValues",
+    "attachments")
+
+  def validateProperties(creationRequest: JsObject): 
Either[InvalidEmailCreationPropertiesException, JsObject] = {
+    val properties: Set[String] = creationRequest.keys.toSet
+
+    if (properties.contains(BODY_STRUCTURE)) {
+      Left(InvalidEmailCreationPropertiesException(Set(BODY_STRUCTURE),
+        "'bodyStructure' is not supported on Email/set create, use 'textBody', 
'htmlBody' and 'attachments' instead"))
+    } else {
+      validateKnownProperties(creationRequest, properties)
+    }
+  }
+
+  private def validateKnownProperties(creationRequest: JsObject, properties: 
Set[String]): Either[InvalidEmailCreationPropertiesException, JsObject] = {
+    val unknownProperties: Set[String] = properties
+      .filterNot(SUPPORTED_PROPERTIES.contains)
+      .filterNot(_.startsWith(SPECIFIC_HEADER_PREFIX))
+
+    if (unknownProperties.isEmpty) {
+      Right(creationRequest)
+    } else {
+      Left(InvalidEmailCreationPropertiesException(unknownProperties,
+        s"Unknown or unsupported properties on Email/set create: 
${unknownProperties.toList.sorted.mkString(", ")}"))
+    }
+  }
 }
 case class EmailCreationRequest(mailboxIds: MailboxIds,
                                 messageId: Option[MessageIdsHeaderValue],
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
index 62ebe5dcfe..0f5f4a2310 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetCreatePerformer.scala
@@ -30,7 +30,7 @@ import org.apache.james.jmap.api.model.Size.sanitizeSize
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.core.{Properties, SetError, UTCDate}
 import org.apache.james.jmap.json.EmailSetSerializer
-import org.apache.james.jmap.mail.{AttachmentNotFoundException, BlobId, 
EmailCreationId, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, 
ThreadId}
+import org.apache.james.jmap.mail.{AttachmentNotFoundException, BlobId, 
EmailCreationId, EmailCreationRequest, EmailCreationResponse, EmailSetRequest, 
InvalidEmailCreationPropertiesException, ThreadId}
 import org.apache.james.jmap.method.EmailSetCreatePerformer.{CreationFailure, 
CreationResult, CreationResults, CreationSuccess}
 import org.apache.james.jmap.routes.{BlobNotFoundException, BlobResolvers}
 import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -80,6 +80,9 @@ object EmailSetCreatePerformer {
       case e: SizeExceededException =>
         LOGGER.info("Attempt to create too big of a message")
         SetError.tooLarge(SetErrorDescription(e.getMessage))
+      case e: InvalidEmailCreationPropertiesException =>
+        LOGGER.info("Invalid properties in Email/set create: {}", e.getMessage)
+        SetError.invalidProperties(SetErrorDescription(e.getMessage), 
Some(Properties.toProperties(e.properties)))
       case e: IllegalArgumentException =>
         LOGGER.info("Illegal argument in Email/set create", e)
         SetError.invalidArguments(SetErrorDescription(e.getMessage))
@@ -106,7 +109,8 @@ class EmailSetCreatePerformer @Inject()(serializer: 
EmailSetSerializer,
       .concatMap {
         case (clientId, json) => serializer.deserializeCreationRequest(json)
           .fold(e => SMono.just[CreationResult](CreationFailure(clientId, new 
IllegalArgumentException(e.toString))),
-            creationRequest => creationRequest.validateRequest
+            creationRequest => EmailCreationRequest.validateProperties(json)
+              .flatMap(_ => creationRequest.validateRequest)
               .fold(e => SMono.just[CreationResult](CreationFailure(clientId, 
e)),
                 _ => create(clientId, creationRequest, mailboxSession)))
       }.collectSeq()


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

Reply via email to