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]