You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2026/10/19 14:56:18 UTC
(james-project) branch master updated (c1d1ca7357 -> 7025ec5dd9)
This is an automated email from the ASF dual-hosted git repository.
Arsnael pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
from c1d1ca7357 JAMES-4236: Fix EmailSubmission/set sender validation errors
new 9defe2c89f JAMES-4236 Test fixtures send an identityId with EmailSubmission/set
new 942a18ac7a JAMES-4236 EmailSubmission/set answer an unknown emailId with invalidProperties
new 1fdb381d4e JAMES-4236 EmailSubmission/set reject an unknown or missing identityId
new c45bd5fbbf JAMES-4236 EmailSubmission/set accept a missing identityId
new f5139952ed JAMES-4236 EmailSubmission/set reject a missing identityId unless the compatibility property is set
new 7025ec5dd9 JAMES-4236 EmailSubmission/set accept a missing identityId by default
The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
Summary of changes:
docs/modules/servers/partials/configure/jvm.adoc | 14 +
.../sample-configuration/jvm.properties | 3 +
.../rfc8621/contract/EmailSetMethodContract.scala | 3 +-
.../EmailSubmissionSetMethodContract.scala | 328 ++++++++++++++++++++-
...lSubmissionSetMethodFutureReleaseContract.scala | 23 +-
.../EmailSubmissionSetValidateRcptContract.scala | 3 +-
.../rfc8621/contract/EventSourceContract.scala | 3 +-
.../james/jmap/rfc8621/contract/Fixture.scala | 2 +
.../jmap/rfc8621/contract/QuotaMailingTest.scala | 3 +-
.../rfc8621/contract/VacationIntegrationTest.scala | 3 +-
.../jmap/rfc8621/contract/WebPushContract.scala | 3 +-
...EmailSubmissionSetExtraValidationContract.scala | 3 +-
.../doc/specs/spec/mail/messagesubmission.mdown | 4 +-
.../jmap/method/EmailSubmissionSetMethod.scala | 35 ++-
...ewProjectionHealthCheckIntegrationContract.java | 1 +
.../integration/ForwardIntegrationTest.java | 1 +
.../vault/DeletedMessageVaultIntegrationTest.java | 2 +
.../apache/james/jmap/JmapRFCCommonRequests.java | 5 +
.../james/spamassassin/SpamAssassinContract.java | 2 +
upgrade-instructions.md | 16 +
20 files changed, 439 insertions(+), 18 deletions(-)
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
(james-project) 06/06: JAMES-4236 EmailSubmission/set accept a missing identityId by default
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Arsnael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 7025ec5dd928849dff2a14052a4e6693c612ff0c
Author: Min Nguyen <mi...@gmail.com>
AuthorDate: Mon Oct 19 14:50:07 2026 +0700
JAMES-4236 EmailSubmission/set accept a missing identityId by default
---
docs/modules/servers/partials/configure/jvm.adoc | 12 +++----
.../sample-configuration/jvm.properties | 4 +--
.../EmailSubmissionSetMethodContract.scala | 42 +++++++++++-----------
.../doc/specs/spec/mail/messagesubmission.mdown | 2 +-
.../jmap/method/EmailSubmissionSetMethod.scala | 4 +--
upgrade-instructions.md | 17 ++++-----
6 files changed, 41 insertions(+), 40 deletions(-)
diff --git a/docs/modules/servers/partials/configure/jvm.adoc b/docs/modules/servers/partials/configure/jvm.adoc
index 56d83db23f..6460382bab 100644
--- a/docs/modules/servers/partials/configure/jvm.adoc
+++ b/docs/modules/servers/partials/configure/jvm.adoc
@@ -179,17 +179,17 @@ To enable the compatibility.
== JMAP EmailSubmission without identityId
-RFC 8621 requires `EmailSubmission/set` creations to carry an `identityId`, and James rejects those without one. The
-property `james.jmap.email.submission.missing.identity.compatibility` allows to accept them for clients that omit it.
-An `identityId` that is not one of the user's identities is rejected either way.
+RFC 8621 requires `EmailSubmission/set` creations to carry an `identityId`. Some clients omit it, so James accepts
+those creations by default. The property `james.jmap.email.submission.missing.identity.compatibility` set to `false`
+rejects them. An `identityId` that is not one of the user's identities is rejected either way.
-Optional. Boolean. Default to false.
+Optional. Boolean. Default to true.
Ex in `jvm.properties`
----
-james.jmap.email.submission.missing.identity.compatibility=true
+james.jmap.email.submission.missing.identity.compatibility=false
----
-To accept submissions without identityId.
+To reject submissions without identityId.
== Enable Reactor Netty metrics for JMAP server
diff --git a/server/apps/distributed-app/sample-configuration/jvm.properties b/server/apps/distributed-app/sample-configuration/jvm.properties
index 41a766aa62..ebc9659897 100644
--- a/server/apps/distributed-app/sample-configuration/jvm.properties
+++ b/server/apps/distributed-app/sample-configuration/jvm.properties
@@ -116,8 +116,8 @@ jmx.remote.x.mlet.allow.getMBeansFromURL=false
# From which point should range operation be used? Defaults to 3.
# james.jmap.email.set.range.threshold=3
-# Accept EmailSubmission/set creations without identityId, for clients that omit it. Defaults to false.
-# james.jmap.email.submission.missing.identity.compatibility=false
+# Accept EmailSubmission/set creations without identityId, for clients that omit it. Defaults to true.
+# james.jmap.email.submission.missing.identity.compatibility=true
# Allow users to have rights for shares of different domain. Defaults to false.
#james.rights.crossdomain.allow=false
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/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index 7c33496112..e950dc6e05 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -1043,40 +1043,40 @@ trait EmailSubmissionSetMethodContract {
@Test
@Category(Array(classOf[BasicFeature]))
- def setShouldRejectMissingIdentityId(server: GuiceJamesServer): Unit = {
+ def setShouldAcceptMissingIdentityId(server: GuiceJamesServer): Unit = {
val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(andreUsername))
- val rejectedMessageId: MessageId = appendBobDraft(server, "rejected")
- val acceptedMessageId: MessageId = appendBobDraft(server, "accepted")
+ val messageId: MessageId = appendBobDraft(server, "accepted")
- val response = submit(
- "rejected" -> (rejectedMessageId, None),
- "accepted" -> (acceptedMessageId, Some(identityIdOf(bobUsername))))
+ val response = submit("accepted" -> (messageId, None))
assertThatJson(response)
- .inPath("methodResponses[0][1].notCreated")
- .isEqualTo("""{
- | "rejected": {
- | "type": "invalidProperties",
- | "description": "Missing 'identityId' property",
- | "properties": ["identityId"]
- | }
- |}""".stripMargin)
+ .whenIgnoringPaths("methodResponses[0][1].created.accepted")
+ .inPath("methodResponses[0][1].created")
+ .isEqualTo("""{"accepted": {}}""")
andreInboxShouldOnlyContain(server, andreInboxId, "accepted")
}
@Test
- def setShouldAcceptMissingIdentityIdWhenCompatibilityIsEnabled(server: GuiceJamesServer): Unit = {
- System.setProperty(EmailSubmissionSetMethod.MISSING_IDENTITY_COMPATIBILITY_PROPERTY, "true")
+ def setShouldRejectMissingIdentityIdWhenCompatibilityIsDisabled(server: GuiceJamesServer): Unit = {
+ System.setProperty(EmailSubmissionSetMethod.MISSING_IDENTITY_COMPATIBILITY_PROPERTY, "false")
try {
val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(andreUsername))
- val messageId: MessageId = appendBobDraft(server, "accepted")
+ val rejectedMessageId: MessageId = appendBobDraft(server, "rejected")
+ val acceptedMessageId: MessageId = appendBobDraft(server, "accepted")
- val response = submit("accepted" -> (messageId, None))
+ val response = submit(
+ "rejected" -> (rejectedMessageId, None),
+ "accepted" -> (acceptedMessageId, Some(identityIdOf(bobUsername))))
assertThatJson(response)
- .whenIgnoringPaths("methodResponses[0][1].created.accepted")
- .inPath("methodResponses[0][1].created")
- .isEqualTo("""{"accepted": {}}""")
+ .inPath("methodResponses[0][1].notCreated")
+ .isEqualTo("""{
+ | "rejected": {
+ | "type": "invalidProperties",
+ | "description": "Missing 'identityId' property",
+ | "properties": ["identityId"]
+ | }
+ |}""".stripMargin)
andreInboxShouldOnlyContain(server, andreInboxId, "accepted")
} finally {
System.clearProperty(EmailSubmissionSetMethod.MISSING_IDENTITY_COMPATIBILITY_PROPERTY)
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
index 53bf6601bf..5d76cc7f59 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
@@ -11,7 +11,7 @@ An **EmailSubmission** object represents the submission of an Email for delivery
The id of the Identity to associate with this submission.
> :information_source:
-> James requires this field. A submission whose identityId is missing, or is not one of the user's identities, is rejected with an `invalidProperties` SetError. Setting the `james.jmap.email.submission.missing.identity.compatibility` JVM property to `true` accepts a submission without identityId.
+> James validates this field. A submission whose identityId is not one of the user's identities is rejected with an `invalidProperties` SetError. A submission without identityId is accepted, unless the `james.jmap.email.submission.missing.identity.compatibility` JVM property is set to `false`.
- **emailId**: `Id` (immutable)
The id of the Email to send. The Email being sent does not have to be a draft, for example, when "redirecting" an existing Email to a different address.
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index cbda9e51ff..5135771050 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -367,7 +367,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
EmailSubmissionCreationResponse(submissionId, sendAt) -> request.emailId
}
- // A missing identityId is only tolerated when the compatibility property is enabled, as some clients omit it
+ // A missing identityId is tolerated unless the compatibility property is disabled, as some clients omit it
private def validateIdentity(mailboxSession: MailboxSession, maybeIdentityId: Option[Id]): SMono[Option[Identity]] =
maybeIdentityId match {
case Some(id) => UnparsedIdentityId(id).validate
@@ -380,7 +380,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
}
private def missingIdentityCompatibility: Boolean =
- Option(System.getProperty(EmailSubmissionSetMethod.MISSING_IDENTITY_COMPATIBILITY_PROPERTY)).exists(_.toBoolean)
+ Option(System.getProperty(EmailSubmissionSetMethod.MISSING_IDENTITY_COMPATIBILITY_PROPERTY)).forall(_.toBoolean)
/**
* Runs the pluggable validations against the mail about to be spooled. The first rejection wins,
diff --git a/upgrade-instructions.md b/upgrade-instructions.md
index c467f5df68..307920a7a9 100644
--- a/upgrade-instructions.md
+++ b/upgrade-instructions.md
@@ -26,21 +26,22 @@ Change list:
- [Dropping unneeded Cassandra schema columns](#dropping-unneeded-cassandra-schema-columns)
- [Cassandra schema version 16: mandatory message denormalization migration](#cassandra-schema-version-16-mandatory-message-denormalization-migration)
- [Migration of embedded mail queue broker from ActiveMQ Classic to ActiveMQ Artemis](#migration-of-embedded-mail-queue-broker-from-activemq-classic-to-activemq-artemis)
- - [JAMES-4236 JMAP EmailSubmission/set requires identityId](#james-4236-jmap-emailsubmissionset-requires-identityid)
+ - [JAMES-4236 JMAP EmailSubmission/set validates identityId](#james-4236-jmap-emailsubmissionset-validates-identityid)
-### JAMES-4236 JMAP EmailSubmission/set requires identityId
+### JAMES-4236 JMAP EmailSubmission/set validates identityId
Date: 14/10/2026
Concerned products: all products exposing JMAP (RFC 8621)
-`EmailSubmission/set` now validates `identityId`, as required by RFC 8621. A creation without `identityId`, or with
-an id that is not one of the user's identities (see `Identity/get`), is rejected with an `invalidProperties` SetError
-on `identityId`, and the email is not sent.
+`EmailSubmission/set` now validates `identityId`, as required by RFC 8621. A creation with an id that is not one of
+the user's identities (see `Identity/get`) is rejected with an `invalidProperties` SetError on `identityId`, and the
+email is not sent.
-JMAP clients must send an `identityId` listed by `Identity/get`. To keep accepting clients that omit it, set
-`james.jmap.email.submission.missing.identity.compatibility=true` in `jvm.properties`. A wrong `identityId` is
-rejected either way.
+JMAP clients that send a stale or foreign `identityId` must be updated to send one listed by `Identity/get`.
+
+A creation without `identityId` is still accepted by default. Set
+`james.jmap.email.submission.missing.identity.compatibility=false` in `jvm.properties` to reject it as well.
### Migration of embedded mail queue broker from ActiveMQ Classic to ActiveMQ Artemis
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
(james-project) 01/06: JAMES-4236 Test fixtures send an identityId with EmailSubmission/set
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Arsnael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 9defe2c89f93aceb5f28107f9111b05bb5ef2a31
Author: Min Nguyen <mi...@gmail.com>
AuthorDate: Thu Oct 15 09:36:02 2026 +0700
JAMES-4236 Test fixtures send an identityId with EmailSubmission/set
---
.../rfc8621/contract/EmailSetMethodContract.scala | 3 +-
.../EmailSubmissionSetMethodContract.scala | 40 ++++++++++++++++++++--
...lSubmissionSetMethodFutureReleaseContract.scala | 23 ++++++++++++-
.../EmailSubmissionSetValidateRcptContract.scala | 3 +-
.../rfc8621/contract/EventSourceContract.scala | 3 +-
.../james/jmap/rfc8621/contract/Fixture.scala | 2 ++
.../jmap/rfc8621/contract/QuotaMailingTest.scala | 3 +-
.../rfc8621/contract/VacationIntegrationTest.scala | 3 +-
.../jmap/rfc8621/contract/WebPushContract.scala | 3 +-
...EmailSubmissionSetExtraValidationContract.scala | 3 +-
...ewProjectionHealthCheckIntegrationContract.java | 1 +
.../integration/ForwardIntegrationTest.java | 1 +
.../vault/DeletedMessageVaultIntegrationTest.java | 2 ++
.../apache/james/jmap/JmapRFCCommonRequests.java | 5 +++
.../james/spamassassin/SpamAssassinContract.java | 2 ++
15 files changed, 87 insertions(+), 10 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 9aa0809735..481388ade2 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
@@ -45,7 +45,7 @@ import org.apache.james.jmap.core.UTCDate
import org.apache.james.jmap.core.UuidState.INSTANCE
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.EmailSetMethodContract.TestContext
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbe
import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
import org.apache.james.jmap.{JmapGuiceProbe, MessageIdProbe}
@@ -8815,6 +8815,7 @@ trait EmailSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "#e1526",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
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/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index f1f26b3c2b..9aaf09ff9a 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -39,7 +39,7 @@ import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
import org.apache.james.jmap.core.UuidState.INSTANCE
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.EmailSubmissionSetMethodContract.TestContext
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.mailbox.DefaultMailboxes
import org.apache.james.mailbox.MessageManager.AppendCommand
import org.apache.james.mailbox.model.MailboxACL.Right
@@ -123,6 +123,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -203,6 +204,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -283,6 +285,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}"
| }
| }
@@ -357,6 +360,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -435,6 +439,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -513,6 +518,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -591,6 +597,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "bob.alias@${DOMAIN.asString}"},
@@ -670,6 +677,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -773,6 +781,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "#e1526",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -848,6 +857,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -902,8 +912,8 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
- | "identityId": "$bobAccountId",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
| "rcptTo": [{"email": "${andreUsername.asString}"}]
@@ -957,6 +967,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1065,6 +1076,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1133,6 +1145,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1201,6 +1214,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1298,6 +1312,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1362,6 +1377,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1427,6 +1443,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$otherAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1483,6 +1500,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1527,6 +1545,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${randomMessageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1585,6 +1604,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1622,6 +1642,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${randomMessageId.serialize}",
| "extra": true,
| "envelope": {
@@ -1681,6 +1702,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1742,6 +1764,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1817,6 +1840,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1875,6 +1899,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1931,6 +1956,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -1989,6 +2015,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${andreUsername.asString}"},
@@ -2112,6 +2139,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -2173,6 +2201,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -2232,6 +2261,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${randomMessageId.serialize}",
| "extra": true,
| "envelope": {
@@ -2295,6 +2325,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${randomMessageId.serialize}",
| "extra": true,
| "envelope": {
@@ -2397,6 +2428,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "#e1526",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -2526,6 +2558,7 @@ trait EmailSubmissionSetMethodContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
@@ -2593,6 +2626,7 @@ trait EmailSubmissionSetMethodContract {
| "create": {
| "k1490": {
| "emailId": "${messageId.serialize}",
+ | "identityId": "${identityIdOf(bobUsername)}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}", "parameters": $parameters},
| "rcptTo": [{"email": "${andreUsername.asString}"}]
@@ -2679,6 +2713,7 @@ trait EmailSubmissionSetMethodContract {
| "create": {
| "k1490": {
| "emailId": "${messageId.serialize}",
+ | "identityId": "${identityIdOf(bobUsername)}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
| "rcptTo": [{"email": "${andreUsername.asString}"}]
@@ -2741,6 +2776,7 @@ trait EmailSubmissionSetMethodContract {
| "create": {
| "k1490": {
| "emailId": "${messageId.serialize}",
+ | "identityId": "${identityIdOf(bobUsername)}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
| "rcptTo": [{"email": "${andreUsername.asString}"}]
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/EmailSubmissionSetMethodFutureReleaseContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala
index e895a18715..35d0de5e74 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala
@@ -35,7 +35,7 @@ import org.apache.james.GuiceJamesServer
import org.apache.james.core.Username
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.EmailSubmissionSetMethodFutureReleaseContract.{DATE, TestContext, futureReleaseSessionObject}
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
import org.apache.james.mailbox.DefaultMailboxes
import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -252,6 +252,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -308,6 +309,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -395,6 +397,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -503,6 +506,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -558,6 +562,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -613,6 +618,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -695,6 +701,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -751,6 +758,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -837,6 +845,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -944,6 +953,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -997,6 +1007,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1050,6 +1061,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1103,6 +1115,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1157,6 +1170,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1212,6 +1226,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1270,6 +1285,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1321,6 +1337,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1372,6 +1389,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1425,6 +1443,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1480,6 +1499,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
@@ -1563,6 +1583,7 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {
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/EmailSubmissionSetValidateRcptContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetValidateRcptContract.scala
index 7ed6dc58eb..7ef669622e 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetValidateRcptContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetValidateRcptContract.scala
@@ -34,7 +34,7 @@ import org.apache.james.core.Username
import org.apache.james.jmap.core.JmapRfc8621Configuration
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.EmailSubmissionSetValidateRcptContract.TestContext
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.mailbox.DefaultMailboxes
import org.apache.james.mailbox.MessageManager.AppendCommand
import org.apache.james.mailbox.model.MailboxPath
@@ -160,6 +160,7 @@ trait EmailSubmissionSetValidateRcptContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bob)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bob.asString}"},
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/EventSourceContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EventSourceContract.scala
index a23c6a5564..2016dba86c 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EventSourceContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EventSourceContract.scala
@@ -33,7 +33,7 @@ import org.apache.james.GuiceJamesServer
import org.apache.james.core.Username
import org.apache.james.jmap.JmapGuiceProbe
import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbe
import org.apache.james.mailbox.DefaultMailboxes
import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -451,6 +451,7 @@ trait EventSourceContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
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/Fixture.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
index 22fb18ca33..1210084e5f 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
@@ -94,6 +94,8 @@ object Fixture {
val DAVID_IDENTITY_ID: String = UUID.nameUUIDFromBytes(ANDRE.asString().getBytes(StandardCharsets.UTF_8)).toString
val ANDRE_IDENTITY_ID: String = "1d684cf0-101b-300b-82c0-8e17f8d464bc"
val IDENTITY_ID: String = UUID.nameUUIDFromBytes(BOB.asString().getBytes(StandardCharsets.UTF_8)).toString
+
+ def identityIdOf(username: Username): String = UUID.nameUUIDFromBytes(username.asString().getBytes(StandardCharsets.UTF_8)).toString
val BOB_PASSWORD: String = "bobpassword"
val ANDRE_PASSWORD: String = "andrepassword"
val ALICE_PASSWORD: String = "alicepassword"
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/QuotaMailingTest.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaMailingTest.scala
index 7291e88295..7eca3dce9c 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaMailingTest.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/QuotaMailingTest.scala
@@ -35,7 +35,7 @@ import org.apache.james.GuiceJamesServer
import org.apache.james.core.Username
import org.apache.james.core.quota.QuotaSizeLimit
import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.junit.categories.BasicFeature
import org.apache.james.mailbox.DefaultMailboxes
import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -203,6 +203,7 @@ trait QuotaMailingTest {
| "accountId": "$andreAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(andreUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${andreUsername.asString}"},
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/VacationIntegrationTest.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/VacationIntegrationTest.scala
index f6dcd1602b..8f362cd99f 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/VacationIntegrationTest.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/VacationIntegrationTest.scala
@@ -37,7 +37,7 @@ import org.apache.james.jmap.JMAPTestingConstants.DOMAIN
import org.apache.james.jmap.JmapGuiceProbe
import org.apache.james.jmap.api.model.AccountId
import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.jmap.rfc8621.contract.VacationIntegrationTest.{html_reason, original_message_text_body, reason}
import org.apache.james.junit.categories.BasicFeature
import org.apache.james.mailbox.DefaultMailboxes
@@ -360,6 +360,7 @@ trait VacationIntegrationTest {
| "accountId": "$andreAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(andreUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${andreUsername.asString}"},
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/WebPushContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebPushContract.scala
index c6a897e54d..61ecd28c4e 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebPushContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebPushContract.scala
@@ -43,7 +43,7 @@ import org.apache.james.GuiceJamesServer
import org.apache.james.core.Username
import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.jmap.rfc8621.contract.tags.CategoryTags
import org.apache.james.mailbox.DefaultMailboxes
import org.apache.james.mailbox.MessageManager.AppendCommand
@@ -359,6 +359,7 @@ trait WebPushContract {
| "accountId": "$bobAccountId",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${bobUsername.asString}"},
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/custom/emailsubmission/EmailSubmissionSetExtraValidationContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/custom/emailsubmission/EmailSubmissionSetExtraValidationContract.scala
index 4ab949525b..668c4abffa 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/custom/emailsubmission/EmailSubmissionSetExtraValidationContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/custom/emailsubmission/EmailSubmissionSetExtraValidationContract.scala
@@ -33,7 +33,7 @@ import org.apache.http.HttpStatus.SC_OK
import org.apache.james.GuiceJamesServer
import org.apache.james.jmap.core.JmapRfc8621Configuration
import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.mailbox.DefaultMailboxes
import org.apache.james.mailbox.MessageManager.AppendCommand
import org.apache.james.mailbox.model.MailboxPath
@@ -151,6 +151,7 @@ trait EmailSubmissionSetExtraValidationContract {
| "accountId": "$ACCOUNT_ID",
| "create": {
| "k1490": {
+ | "identityId": "${identityIdOf(BOB)}",
| "emailId": "${messageId.serialize}",
| "envelope": {
| "mailFrom": {"email": "${BOB.asString}"},
diff --git a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/FastViewProjectionHealthCheckIntegrationContract.java b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/FastViewProjectionHealthCheckIntegrationContract.java
index e014de3064..74f0f135e0 100644
--- a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/FastViewProjectionHealthCheckIntegrationContract.java
+++ b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/FastViewProjectionHealthCheckIntegrationContract.java
@@ -183,6 +183,7 @@ public abstract class FastViewProjectionHealthCheckIntegrationContract {
" \"accountId\": \"" + bobCredential.accountId() + "\"," +
" \"create\": {" +
" \"k1490\": {" +
+ " \"identityId\": \"" + bobCredential.identityId() + "\"," +
" \"emailId\": \"#e1526\"," +
" \"envelope\": {" +
" \"mailFrom\": {\"email\": \"" + BOB.asString() + "\"}," +
diff --git a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/ForwardIntegrationTest.java b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/ForwardIntegrationTest.java
index 12098ffae4..e56799e581 100644
--- a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/ForwardIntegrationTest.java
+++ b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/ForwardIntegrationTest.java
@@ -213,6 +213,7 @@ public abstract class ForwardIntegrationTest {
" \"accountId\": \"" + cedricCredential.accountId() + "\"," +
" \"create\": {" +
" \"k1490\": {" +
+ " \"identityId\": \"" + cedricCredential.identityId() + "\"," +
" \"emailId\": \"#e1526\"," +
" \"envelope\": {" +
" \"mailFrom\": {\"email\": \"" + CEDRIC.asString() + "\"}," +
diff --git a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/vault/DeletedMessageVaultIntegrationTest.java b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/vault/DeletedMessageVaultIntegrationTest.java
index 24478dfa83..7db6617b66 100644
--- a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/vault/DeletedMessageVaultIntegrationTest.java
+++ b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/vault/DeletedMessageVaultIntegrationTest.java
@@ -1142,6 +1142,7 @@ public abstract class DeletedMessageVaultIntegrationTest {
" \"accountId\": \"" + bartCredential.accountId() + "\"," +
" \"create\": {" +
" \"k1490\": {" +
+ " \"identityId\": \"" + bartCredential.identityId() + "\"," +
" \"emailId\": \"#e1526\"," +
" \"envelope\": {" +
" \"mailFrom\": {\"email\": \"" + bart + "\"}," +
@@ -1202,6 +1203,7 @@ public abstract class DeletedMessageVaultIntegrationTest {
" \"accountId\": \"" + bartCredential.accountId() + "\"," +
" \"create\": {" +
" \"k1490\": {" +
+ " \"identityId\": \"" + bartCredential.identityId() + "\"," +
" \"emailId\": \"#e1526\"," +
" \"envelope\": {" +
" \"mailFrom\": {\"email\": \"" + bart + "\"}," +
diff --git a/server/testing/src/main/java/org/apache/james/jmap/JmapRFCCommonRequests.java b/server/testing/src/main/java/org/apache/james/jmap/JmapRFCCommonRequests.java
index e6ea38007b..f22197434d 100644
--- a/server/testing/src/main/java/org/apache/james/jmap/JmapRFCCommonRequests.java
+++ b/server/testing/src/main/java/org/apache/james/jmap/JmapRFCCommonRequests.java
@@ -22,8 +22,10 @@ package org.apache.james.jmap;
import static io.restassured.RestAssured.with;
import static io.restassured.http.ContentType.JSON;
+import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
+import java.util.UUID;
import java.util.stream.Collectors;
import org.apache.james.core.Username;
@@ -36,6 +38,9 @@ import io.restassured.path.json.JsonPath;
public class JmapRFCCommonRequests {
public record UserCredential(Username username, String password, String accountId) {
+ public String identityId() {
+ return UUID.nameUUIDFromBytes(username.asString().getBytes(StandardCharsets.UTF_8)).toString();
+ }
}
public static final Header ACCEPT_JMAP_RFC_HEADER = new Header("accept", "application/json; jmapVersion=rfc-8621");
diff --git a/third-party/spamassassin/src/test/java/org/apache/james/spamassassin/SpamAssassinContract.java b/third-party/spamassassin/src/test/java/org/apache/james/spamassassin/SpamAssassinContract.java
index e79f18609f..2fad606e29 100644
--- a/third-party/spamassassin/src/test/java/org/apache/james/spamassassin/SpamAssassinContract.java
+++ b/third-party/spamassassin/src/test/java/org/apache/james/spamassassin/SpamAssassinContract.java
@@ -455,6 +455,7 @@ public interface SpamAssassinContract {
" \"accountId\": \"" + bobCredential.accountId() + "\"," +
" \"create\": {" +
" \"k1490\": {" +
+ " \"identityId\": \"" + bobCredential.identityId() + "\"," +
" \"emailId\": \"#e1526\"," +
" \"envelope\": {" +
" \"mailFrom\": {\"email\": \"" + BOB + "\"}," +
@@ -523,6 +524,7 @@ public interface SpamAssassinContract {
" \"accountId\": \"" + bobCredential.accountId() + "\"," +
" \"create\": {" +
" \"k1490\": {" +
+ " \"identityId\": \"" + bobCredential.identityId() + "\"," +
" \"emailId\": \"#e1526\"," +
" \"envelope\": {" +
" \"mailFrom\": {\"email\": \"" + BOB + "\"}," +
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
(james-project) 02/06: JAMES-4236 EmailSubmission/set answer an unknown emailId with invalidProperties
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Arsnael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 942a18ac7a1ba08da1ca2eaf1ba43bb8004e73fd
Author: Min Nguyen <mi...@gmail.com>
AuthorDate: Thu Oct 15 09:37:24 2026 +0700
JAMES-4236 EmailSubmission/set answer an unknown emailId with invalidProperties
---
.../EmailSubmissionSetMethodContract.scala | 48 +++++++++++++++++++++-
.../jmap/method/EmailSubmissionSetMethod.scala | 9 ++--
2 files changed, 52 insertions(+), 5 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/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index 9aaf09ff9a..b10a1f198f 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -1572,13 +1572,57 @@ trait EmailSubmissionSetMethodContract {
.inPath("methodResponses[0][1].notCreated")
.isEqualTo("""{
| "k1490": {
- | "type": "invalidArguments",
+ | "type": "invalidProperties",
| "description": "The email to be sent cannot be found",
| "properties": ["emailId"]
| }
|}""".stripMargin)
}
+ @Test
+ def setShouldRejectUnparsableEmailId(): Unit = {
+ val requestBob =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
+ | "methodCalls": [
+ | ["EmailSubmission/set", {
+ | "accountId": "$bobAccountId",
+ | "create": {
+ | "k1490": {
+ | "identityId": "${identityIdOf(bobUsername)}",
+ | "emailId": "Munknown0000",
+ | "envelope": {
+ | "mailFrom": {"email": "${bobUsername.asString}"},
+ | "rcptTo": [{"email": "${andreUsername.asString}"}]
+ | }
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(requestBob)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].notCreated.k1490.description")
+ .inPath("methodResponses[0][1].notCreated")
+ .isEqualTo("""{
+ | "k1490": {
+ | "type": "invalidProperties",
+ | "properties": ["emailId"]
+ | }
+ |}""".stripMargin)
+ }
+
@Test
def implicitSetShouldNotBeAttemptedWhenNotSpecified(server: GuiceJamesServer): Unit = {
val message: Message = Message.Builder
@@ -1729,7 +1773,7 @@ trait EmailSubmissionSetMethodContract {
.inPath("methodResponses[0][1].notCreated")
.isEqualTo("""{
| "k1490": {
- | "type": "invalidArguments",
+ | "type": "invalidProperties",
| "description": "The email to be sent cannot be found",
| "properties": ["emailId"]
| }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index fdb6cb9913..15f5593470 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -102,8 +102,7 @@ object EmailSubmissionSetMethod {
error
case _: MessageNotFoundException =>
LOGGER.info(" EmailSubmission/set failed as the underlying email could not be found")
- SetError(SetError.invalidArgumentValue,
- SetErrorDescription("The email to be sent cannot be found"),
+ SetError.invalidProperties(SetErrorDescription("The email to be sent cannot be found"),
Some(Properties("emailId")))
case e: DateTimeParseException =>
LOGGER.info("Failed to parse date time", e)
@@ -319,7 +318,11 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
})
private def emailSubmissionSetError(errors: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): SetError =
- standardError(errors)
+ errors.head match {
+ case (path, _) if path == JsPath \ "emailId" =>
+ SetError.invalidProperties(SetErrorDescription(standardErrorMessage(errors)), Some(Properties("emailId")))
+ case _ => standardError(errors)
+ }
private def sendEmail(mailboxSession: MailboxSession,
request: EmailSubmissionCreationRequest): SMono[(EmailSubmissionCreationResponse, MessageId)] =
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
(james-project) 05/06: JAMES-4236 EmailSubmission/set reject a missing identityId unless the compatibility property is set
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Arsnael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit f5139952edc3ba1d30921425afdc4e0bceeeffb8
Author: Min Nguyen <mi...@gmail.com>
AuthorDate: Mon Oct 19 13:47:25 2026 +0700
JAMES-4236 EmailSubmission/set reject a missing identityId unless the compatibility property is set
---
docs/modules/servers/partials/configure/jvm.adoc | 14 ++++++++
.../sample-configuration/jvm.properties | 3 ++
.../EmailSubmissionSetMethodContract.scala | 40 ++++++++++++++++++----
.../doc/specs/spec/mail/messagesubmission.mdown | 2 +-
.../jmap/method/EmailSubmissionSetMethod.scala | 16 ++++++---
upgrade-instructions.md | 14 ++++----
6 files changed, 71 insertions(+), 18 deletions(-)
diff --git a/docs/modules/servers/partials/configure/jvm.adoc b/docs/modules/servers/partials/configure/jvm.adoc
index 59b2c1fe01..56d83db23f 100644
--- a/docs/modules/servers/partials/configure/jvm.adoc
+++ b/docs/modules/servers/partials/configure/jvm.adoc
@@ -177,6 +177,20 @@ james.jmap.quota.draft.compatibility=true
----
To enable the compatibility.
+== JMAP EmailSubmission without identityId
+
+RFC 8621 requires `EmailSubmission/set` creations to carry an `identityId`, and James rejects those without one. The
+property `james.jmap.email.submission.missing.identity.compatibility` allows to accept them for clients that omit it.
+An `identityId` that is not one of the user's identities is rejected either way.
+
+Optional. Boolean. Default to false.
+
+Ex in `jvm.properties`
+----
+james.jmap.email.submission.missing.identity.compatibility=true
+----
+To accept submissions without identityId.
+
== Enable Reactor Netty metrics for JMAP server
Allow to enable https://projectreactor.io/docs/netty/1.1.19/reference/index.html#_metrics_4[Reactor Netty metrics] for JMAP server which would provide useful debug information.
diff --git a/server/apps/distributed-app/sample-configuration/jvm.properties b/server/apps/distributed-app/sample-configuration/jvm.properties
index dabfc953ae..41a766aa62 100644
--- a/server/apps/distributed-app/sample-configuration/jvm.properties
+++ b/server/apps/distributed-app/sample-configuration/jvm.properties
@@ -116,6 +116,9 @@ jmx.remote.x.mlet.allow.getMBeansFromURL=false
# From which point should range operation be used? Defaults to 3.
# james.jmap.email.set.range.threshold=3
+# Accept EmailSubmission/set creations without identityId, for clients that omit it. Defaults to false.
+# james.jmap.email.submission.missing.identity.compatibility=false
+
# Allow users to have rights for shares of different domain. Defaults to false.
#james.rights.crossdomain.allow=false
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/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index 2c0e2e055e..7c33496112 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -38,6 +38,7 @@ import org.apache.james.core.Username
import org.apache.james.jmap.core.ResponseObject.SESSION_STATE
import org.apache.james.jmap.core.UuidState.INSTANCE
import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.method.EmailSubmissionSetMethod
import org.apache.james.jmap.rfc8621.contract.EmailSubmissionSetMethodContract.TestContext
import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
import org.apache.james.junit.categories.BasicFeature
@@ -1042,19 +1043,46 @@ trait EmailSubmissionSetMethodContract {
@Test
@Category(Array(classOf[BasicFeature]))
- def setShouldAcceptMissingIdentityId(server: GuiceJamesServer): Unit = {
+ def setShouldRejectMissingIdentityId(server: GuiceJamesServer): Unit = {
val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(andreUsername))
- val messageId: MessageId = appendBobDraft(server, "accepted")
+ val rejectedMessageId: MessageId = appendBobDraft(server, "rejected")
+ val acceptedMessageId: MessageId = appendBobDraft(server, "accepted")
- val response = submit("accepted" -> (messageId, None))
+ val response = submit(
+ "rejected" -> (rejectedMessageId, None),
+ "accepted" -> (acceptedMessageId, Some(identityIdOf(bobUsername))))
assertThatJson(response)
- .whenIgnoringPaths("methodResponses[0][1].created.accepted")
- .inPath("methodResponses[0][1].created")
- .isEqualTo("""{"accepted": {}}""")
+ .inPath("methodResponses[0][1].notCreated")
+ .isEqualTo("""{
+ | "rejected": {
+ | "type": "invalidProperties",
+ | "description": "Missing 'identityId' property",
+ | "properties": ["identityId"]
+ | }
+ |}""".stripMargin)
andreInboxShouldOnlyContain(server, andreInboxId, "accepted")
}
+ @Test
+ def setShouldAcceptMissingIdentityIdWhenCompatibilityIsEnabled(server: GuiceJamesServer): Unit = {
+ System.setProperty(EmailSubmissionSetMethod.MISSING_IDENTITY_COMPATIBILITY_PROPERTY, "true")
+ try {
+ val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(andreUsername))
+ val messageId: MessageId = appendBobDraft(server, "accepted")
+
+ val response = submit("accepted" -> (messageId, None))
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.accepted")
+ .inPath("methodResponses[0][1].created")
+ .isEqualTo("""{"accepted": {}}""")
+ andreInboxShouldOnlyContain(server, andreInboxId, "accepted")
+ } finally {
+ System.clearProperty(EmailSubmissionSetMethod.MISSING_IDENTITY_COMPATIBILITY_PROPERTY)
+ }
+ }
+
private def appendBobDraft(server: GuiceJamesServer, subject: String): MessageId = {
val message: Message = Message.Builder
.of
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
index b40c6f1ab9..53bf6601bf 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
@@ -11,7 +11,7 @@ An **EmailSubmission** object represents the submission of an Email for delivery
The id of the Identity to associate with this submission.
> :information_source:
-> James validates this field when present. A submission whose identityId is not one of the user's identities is rejected with an `invalidProperties` SetError. A submission without identityId is accepted.
+> James requires this field. A submission whose identityId is missing, or is not one of the user's identities, is rejected with an `invalidProperties` SetError. Setting the `james.jmap.email.submission.missing.identity.compatibility` JVM property to `true` accepts a submission without identityId.
- **emailId**: `Id` (immutable)
The id of the Email to send. The Email being sent does not have to be a draft, for example, when "redirecting" an existing Email to a different address.
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index bfee67329d..cbda9e51ff 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -75,6 +75,7 @@ object EmailSubmissionSetMethod {
val formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("Z"))
val VALID_PARAMETER_NAME_SET: Set[ParameterName] = Set(ParameterName.holdFor, ParameterName.holdUntil)
val NO_DELAY: Duration = Duration.ZERO
+ val MISSING_IDENTITY_COMPATIBILITY_PROPERTY: String = "james.jmap.email.submission.missing.identity.compatibility"
def validateFromHeader(mimeMessage: MimeMessage): SMono[MimeMessage] =
if (mimeMessage.getHeader("From") != null && Option(mimeMessage.getFrom).exists(_.nonEmpty)) {
@@ -366,15 +367,20 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
EmailSubmissionCreationResponse(submissionId, sendAt) -> request.emailId
}
- // A missing identityId is tolerated as some clients omit it; a wrong one is rejected
+ // A missing identityId is only tolerated when the compatibility property is enabled, as some clients omit it
private def validateIdentity(mailboxSession: MailboxSession, maybeIdentityId: Option[Id]): SMono[Option[Identity]] =
- maybeIdentityId
- .map(id => UnparsedIdentityId(id).validate
+ maybeIdentityId match {
+ case Some(id) => UnparsedIdentityId(id).validate
.fold(_ => SMono.empty[Identity],
identityId => identityResolver.resolveIdentityId(identityId, mailboxSession).flatMap(SMono.justOrEmpty(_)))
.switchIfEmpty(SMono.error(IdentityNotFoundException("The identity to be used cannot be found")))
- .map(Option(_)))
- .getOrElse(SMono.just(None))
+ .map(Option(_))
+ case None if missingIdentityCompatibility => SMono.just(None)
+ case None => SMono.error(IdentityNotFoundException("Missing 'identityId' property"))
+ }
+
+ private def missingIdentityCompatibility: Boolean =
+ Option(System.getProperty(EmailSubmissionSetMethod.MISSING_IDENTITY_COMPATIBILITY_PROPERTY)).exists(_.toBoolean)
/**
* Runs the pluggable validations against the mail about to be spooled. The first rejection wins,
diff --git a/upgrade-instructions.md b/upgrade-instructions.md
index 399a554081..c467f5df68 100644
--- a/upgrade-instructions.md
+++ b/upgrade-instructions.md
@@ -26,19 +26,21 @@ Change list:
- [Dropping unneeded Cassandra schema columns](#dropping-unneeded-cassandra-schema-columns)
- [Cassandra schema version 16: mandatory message denormalization migration](#cassandra-schema-version-16-mandatory-message-denormalization-migration)
- [Migration of embedded mail queue broker from ActiveMQ Classic to ActiveMQ Artemis](#migration-of-embedded-mail-queue-broker-from-activemq-classic-to-activemq-artemis)
- - [JAMES-4236 JMAP EmailSubmission/set validates identityId](#james-4236-jmap-emailsubmissionset-validates-identityid)
+ - [JAMES-4236 JMAP EmailSubmission/set requires identityId](#james-4236-jmap-emailsubmissionset-requires-identityid)
-### JAMES-4236 JMAP EmailSubmission/set validates identityId
+### JAMES-4236 JMAP EmailSubmission/set requires identityId
Date: 14/10/2026
Concerned products: all products exposing JMAP (RFC 8621)
-`EmailSubmission/set` now validates `identityId`, as required by RFC 8621. A creation with an id that is not one of
-the user's identities (see `Identity/get`) is rejected with an `invalidProperties` SetError on `identityId`, and the
-email is not sent. A creation without `identityId` is still accepted.
+`EmailSubmission/set` now validates `identityId`, as required by RFC 8621. A creation without `identityId`, or with
+an id that is not one of the user's identities (see `Identity/get`), is rejected with an `invalidProperties` SetError
+on `identityId`, and the email is not sent.
-JMAP clients that send a stale or foreign `identityId` must be updated to send one listed by `Identity/get`.
+JMAP clients must send an `identityId` listed by `Identity/get`. To keep accepting clients that omit it, set
+`james.jmap.email.submission.missing.identity.compatibility=true` in `jvm.properties`. A wrong `identityId` is
+rejected either way.
### Migration of embedded mail queue broker from ActiveMQ Classic to ActiveMQ Artemis
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
(james-project) 03/06: JAMES-4236 EmailSubmission/set reject an unknown or missing identityId
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Arsnael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit 1fdb381d4e43c862c7ab1099bd1e3b5f5369dec0
Author: Min Nguyen <mi...@gmail.com>
AuthorDate: Thu Oct 15 09:37:24 2026 +0700
JAMES-4236 EmailSubmission/set reject an unknown or missing identityId
---
.../EmailSubmissionSetMethodContract.scala | 220 +++++++++++++++++++++
.../doc/specs/spec/mail/messagesubmission.mdown | 4 +-
.../jmap/method/EmailSubmissionSetMethod.scala | 18 +-
upgrade-instructions.md | 13 ++
4 files changed, 252 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/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index b10a1f198f..c21224d00b 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -40,6 +40,7 @@ import org.apache.james.jmap.core.UuidState.INSTANCE
import org.apache.james.jmap.http.UserCredential
import org.apache.james.jmap.rfc8621.contract.EmailSubmissionSetMethodContract.TestContext
import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE_PASSWORD, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder, identityIdOf}
+import org.apache.james.junit.categories.BasicFeature
import org.apache.james.mailbox.DefaultMailboxes
import org.apache.james.mailbox.MessageManager.AppendCommand
import org.apache.james.mailbox.model.MailboxACL.Right
@@ -49,6 +50,7 @@ import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
import org.apache.james.utils.DataProbeImpl
import org.awaitility.Awaitility
import org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS
+import org.junit.experimental.categories.Category
import org.junit.jupiter.api.{BeforeEach, Test}
import play.api.libs.json.{JsObject, JsString, Json}
@@ -942,6 +944,224 @@ trait EmailSubmissionSetMethodContract {
.isEqualTo("""{"k1490": {}}""")
}
+ @Test
+ def emailSubmissionSetCreateShouldAcceptCustomIdentityId(server: GuiceJamesServer): Unit = {
+ val messageId: MessageId = appendBobDraft(server, "test")
+
+ val identityCreation =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+ | "methodCalls": [
+ | ["Identity/set", {
+ | "accountId": "$bobAccountId",
+ | "create": {
+ | "i1": {
+ | "name": "Bob custom",
+ | "email": "${bobUsername.asString}"
+ | }
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ val customIdentityId: String = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(identityCreation)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .jsonPath()
+ .getString("methodResponses[0][1].created.i1.id")
+
+ val response = submit(messageId, Some(customIdentityId))
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[0][1].created.k1490")
+ .inPath("methodResponses[0][1].created")
+ .isEqualTo("""{"k1490": {}}""")
+ }
+
+ @Test
+ def setShouldRejectUnparsableIdentityId(server: GuiceJamesServer): Unit = {
+ val messageId: MessageId = appendBobDraft(server, "test")
+
+ val response = submit(messageId, Some("Iunknown0000"))
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].notCreated")
+ .isEqualTo("""{
+ | "k1490": {
+ | "type": "invalidProperties",
+ | "description": "The identity to be used cannot be found",
+ | "properties": ["identityId"]
+ | }
+ |}""".stripMargin)
+ }
+
+ @Test
+ def setShouldRejectUnknownIdentityId(server: GuiceJamesServer): Unit = {
+ val messageId: MessageId = appendBobDraft(server, "test")
+
+ val response = submit(messageId, Some(UUID.randomUUID().toString))
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].notCreated")
+ .isEqualTo("""{
+ | "k1490": {
+ | "type": "invalidProperties",
+ | "description": "The identity to be used cannot be found",
+ | "properties": ["identityId"]
+ | }
+ |}""".stripMargin)
+ }
+
+ @Test
+ @Category(Array(classOf[BasicFeature]))
+ def setShouldRejectIdentityIdOfOtherUsers(server: GuiceJamesServer): Unit = {
+ val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(andreUsername))
+ val rejectedMessageId: MessageId = appendBobDraft(server, "rejected")
+ val acceptedMessageId: MessageId = appendBobDraft(server, "accepted")
+
+ val response = submit(
+ "rejected" -> (rejectedMessageId, Some(identityIdOf(andreUsername))),
+ "accepted" -> (acceptedMessageId, Some(identityIdOf(bobUsername))))
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].notCreated")
+ .isEqualTo("""{
+ | "rejected": {
+ | "type": "invalidProperties",
+ | "description": "The identity to be used cannot be found",
+ | "properties": ["identityId"]
+ | }
+ |}""".stripMargin)
+ andreInboxShouldOnlyContain(server, andreInboxId, "accepted")
+ }
+
+ @Test
+ @Category(Array(classOf[BasicFeature]))
+ def setShouldRejectMissingIdentityId(server: GuiceJamesServer): Unit = {
+ val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(andreUsername))
+ val rejectedMessageId: MessageId = appendBobDraft(server, "rejected")
+ val acceptedMessageId: MessageId = appendBobDraft(server, "accepted")
+
+ val response = submit(
+ "rejected" -> (rejectedMessageId, None),
+ "accepted" -> (acceptedMessageId, Some(identityIdOf(bobUsername))))
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1].notCreated")
+ .isEqualTo("""{
+ | "rejected": {
+ | "type": "invalidProperties",
+ | "description": "Missing 'identityId' property",
+ | "properties": ["identityId"]
+ | }
+ |}""".stripMargin)
+ andreInboxShouldOnlyContain(server, andreInboxId, "accepted")
+ }
+
+ private def appendBobDraft(server: GuiceJamesServer, subject: String): MessageId = {
+ val message: Message = Message.Builder
+ .of
+ .setSubject(subject)
+ .setSender(bobUsername.asString)
+ .setFrom(bobUsername.asString)
+ .setTo(andreUsername.asString)
+ .setBody("testmail", StandardCharsets.UTF_8)
+ .build
+
+ val bobDraftsPath = MailboxPath.forUser(bobUsername, DefaultMailboxes.DRAFTS)
+ val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+ if (!mailboxProbe.listUserMailboxes(bobUsername.asString).contains(DefaultMailboxes.DRAFTS)) {
+ mailboxProbe.createMailbox(bobDraftsPath)
+ }
+ mailboxProbe.appendMessage(bobUsername.asString(), bobDraftsPath, AppendCommand.builder()
+ .build(message))
+ .getMessageId
+ }
+
+ private def submit(messageId: MessageId, identityId: Option[String]): String =
+ submit("k1490" -> (messageId, identityId))
+
+ private def submit(creations: (String, (MessageId, Option[String]))*): String = {
+ val create: String = creations.map {
+ case (creationId, (messageId, identityId)) =>
+ s""" "$creationId": {
+ | ${identityId.map(id => s""""identityId": "$id",""").getOrElse("")}
+ | "emailId": "${messageId.serialize}",
+ | "envelope": {
+ | "mailFrom": {"email": "${bobUsername.asString}"},
+ | "rcptTo": [{"email": "${andreUsername.asString}"}]
+ | }
+ | }""".stripMargin
+ }.mkString(",\n")
+
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
+ | "methodCalls": [
+ | ["EmailSubmission/set", {
+ | "accountId": "$bobAccountId",
+ | "create": {
+ |$create
+ | }
+ | }, "c1"]]
+ |}""".stripMargin
+
+ `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+ }
+
+ private def andreInboxShouldOnlyContain(server: GuiceJamesServer, andreInboxId: MailboxId, subject: String): Unit = {
+ val requestAndre =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+ | "methodCalls": [
+ | ["Email/query", {
+ | "accountId": "$andreAccountId",
+ | "filter": {"inMailbox": "${andreInboxId.serialize}"}
+ | }, "c1"],
+ | ["Email/get", {
+ | "accountId": "$andreAccountId",
+ | "#ids": {"resultOf": "c1", "name": "Email/query", "path": "ids/*"},
+ | "properties": ["subject"]
+ | }, "c2"]]
+ |}""".stripMargin
+
+ awaitAtMostTenSeconds.untilAsserted { () =>
+ val response = `given`(
+ baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(andreUsername, ANDRE_PASSWORD)))
+ .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .setBody(requestAndre)
+ .build, new ResponseSpecBuilder().build)
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .whenIgnoringPaths("methodResponses[1][1].list[*].id")
+ .inPath("methodResponses[1][1].list")
+ .isEqualTo(s"""[{"subject": "$subject"}]""")
+ }
+ }
+
@Test
def onSuccessUpdateEmailShouldTriggerAnImplicitEmailSetCall(server: GuiceJamesServer): Unit = {
val message: Message = Message.Builder
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
index 0a45fd1794..04422c0069 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
@@ -10,8 +10,8 @@ An **EmailSubmission** object represents the submission of an Email for delivery
- **identityId**: `Id` (immutable)
The id of the Identity to associate with this submission.
-> :warning:
-> Not implemented. Submissions cannot be created with identityId. This field should be omitted.
+> :information_source:
+> James requires this field. A submission whose identityId is missing, or is not one of the user's identities, is rejected with an `invalidProperties` SetError.
- **emailId**: `Id` (immutable)
The id of the Email to send. The Email being sent does not have to be a draft, for example, when "redirecting" an existing Email to a different address.
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index 15f5593470..619250e6a0 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -34,13 +34,14 @@ import jakarta.mail.Message.RecipientType
import jakarta.mail.internet.{InternetAddress, MimeMessage}
import org.apache.commons.lang3.StringUtils
import org.apache.james.core.{MailAddress, Username}
+import org.apache.james.jmap.api.model.Identity
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, EMAIL_SUBMISSION, JMAP_CORE}
import org.apache.james.jmap.core.Id.{Id, IdConstraint}
import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.core.SetError.{SetErrorDescription, SetErrorType}
import org.apache.james.jmap.core.{ClientId, Invocation, JmapRfc8621Configuration, Properties, ServerId, SessionTranslator, SetError, SubmissionCapabilityFactory, UTCDate, UuidState}
import org.apache.james.jmap.json.EmailSubmissionSetSerializer
-import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreationId, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope, ParameterName, ParameterValue, UnparsedEmailSubmissionId}
+import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreationId, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope, ParameterName, ParameterValue, UnparsedEmailSubmissionId, UnparsedIdentityId}
import org.apache.james.jmap.method.EmailSubmissionSetMethod.{CreationFailure, CreationResult, CreationResults, CreationSuccess, LOGGER, NO_DELAY, VALID_PARAMETER_NAME_SET, formatter}
import org.apache.james.jmap.routes.{ProcessingContext, SessionSupplier}
import org.apache.james.lifecycle.api.{LifecycleUtil, Startable}
@@ -104,6 +105,10 @@ object EmailSubmissionSetMethod {
LOGGER.info(" EmailSubmission/set failed as the underlying email could not be found")
SetError.invalidProperties(SetErrorDescription("The email to be sent cannot be found"),
Some(Properties("emailId")))
+ case e: IdentityNotFoundException =>
+ LOGGER.info("EmailSubmission/set failed: {}", e.description)
+ SetError.invalidProperties(SetErrorDescription(e.description),
+ Some(Properties("identityId")))
case e: DateTimeParseException =>
LOGGER.info("Failed to parse date time", e)
SetError.invalidArguments(SetErrorDescription(e.getMessage))
@@ -190,6 +195,7 @@ case class MissingFromHeaderException() extends EmailSubmissionException {
SetErrorDescription("Attempt to send a mail whose MimeMessage From is missing"), Some(Properties("from")))
}
case class InvalidDelayException(message: String) extends IllegalArgumentException(message)
+case class IdentityNotFoundException(description: String) extends Exception
case class MessageMimeMessageSource(id: String, message: MessageResult) extends MimeMessageSource {
override def getSourceId: String = id
@@ -205,6 +211,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
mailQueueFactory: MailQueueFactory[_ <: MailQueue],
canSendFrom: CanSendFrom,
emailSetMethod: EmailSetMethod,
+ identityResolver: IdentityResolver,
clock: Clock,
javaValidations: java.util.Set[EmailSubmissionSetValidation],
@Named("supportsDelaySends") supportsDelaySends: Boolean,
@@ -327,6 +334,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
private def sendEmail(mailboxSession: MailboxSession,
request: EmailSubmissionCreationRequest): SMono[(EmailSubmissionCreationResponse, MessageId)] =
for {
+ _ <- validateIdentity(mailboxSession, request.identityId)
message <- SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, FetchGroup.FULL_CONTENT, mailboxSession))
.next
.switchIfEmpty(SMono.error(MessageNotFoundException(request.emailId)))
@@ -358,6 +366,14 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
EmailSubmissionCreationResponse(submissionId, sendAt) -> request.emailId
}
+ private def validateIdentity(mailboxSession: MailboxSession, maybeIdentityId: Option[Id]): SMono[Identity] =
+ maybeIdentityId
+ .map(id => UnparsedIdentityId(id).validate
+ .fold(_ => SMono.empty[Identity],
+ identityId => identityResolver.resolveIdentityId(identityId, mailboxSession).flatMap(SMono.justOrEmpty(_)))
+ .switchIfEmpty(SMono.error(IdentityNotFoundException("The identity to be used cannot be found"))))
+ .getOrElse(SMono.error(IdentityNotFoundException("Missing 'identityId' property")))
+
/**
* Runs the pluggable validations against the mail about to be spooled. The first rejection wins,
* subsequent validations are not evaluated. As the mail holds the message content, it needs
diff --git a/upgrade-instructions.md b/upgrade-instructions.md
index 360b7c5326..d3438d3509 100644
--- a/upgrade-instructions.md
+++ b/upgrade-instructions.md
@@ -26,6 +26,19 @@ Change list:
- [Dropping unneeded Cassandra schema columns](#dropping-unneeded-cassandra-schema-columns)
- [Cassandra schema version 16: mandatory message denormalization migration](#cassandra-schema-version-16-mandatory-message-denormalization-migration)
- [Migration of embedded mail queue broker from ActiveMQ Classic to ActiveMQ Artemis](#migration-of-embedded-mail-queue-broker-from-activemq-classic-to-activemq-artemis)
+ - [JAMES-4236 JMAP EmailSubmission/set requires identityId](#james-4236-jmap-emailsubmissionset-requires-identityid)
+
+### JAMES-4236 JMAP EmailSubmission/set requires identityId
+
+Date: 14/10/2026
+
+Concerned products: all products exposing JMAP (RFC 8621)
+
+`EmailSubmission/set` now validates `identityId`, as required by RFC 8621. A creation without `identityId`, or with
+an id that is not one of the user's identities (see `Identity/get`), is rejected with an `invalidProperties` SetError
+on `identityId`, and the email is not sent.
+
+JMAP clients that omit `identityId` must be updated to send it before upgrading.
### Migration of embedded mail queue broker from ActiveMQ Classic to ActiveMQ Artemis
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org
(james-project) 04/06: JAMES-4236 EmailSubmission/set accept a missing identityId
Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
Arsnael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git
commit c45bd5fbbfd8186db38a71565fa922cee4efb124
Author: Min Nguyen <mi...@gmail.com>
AuthorDate: Mon Oct 19 11:01:03 2026 +0700
JAMES-4236 EmailSubmission/set accept a missing identityId
---
.../contract/EmailSubmissionSetMethodContract.scala | 20 ++++++--------------
.../doc/specs/spec/mail/messagesubmission.mdown | 2 +-
.../james/jmap/method/EmailSubmissionSetMethod.scala | 8 +++++---
upgrade-instructions.md | 12 ++++++------
4 files changed, 18 insertions(+), 24 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/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index c21224d00b..2c0e2e055e 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -1042,24 +1042,16 @@ trait EmailSubmissionSetMethodContract {
@Test
@Category(Array(classOf[BasicFeature]))
- def setShouldRejectMissingIdentityId(server: GuiceJamesServer): Unit = {
+ def setShouldAcceptMissingIdentityId(server: GuiceJamesServer): Unit = {
val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(andreUsername))
- val rejectedMessageId: MessageId = appendBobDraft(server, "rejected")
- val acceptedMessageId: MessageId = appendBobDraft(server, "accepted")
+ val messageId: MessageId = appendBobDraft(server, "accepted")
- val response = submit(
- "rejected" -> (rejectedMessageId, None),
- "accepted" -> (acceptedMessageId, Some(identityIdOf(bobUsername))))
+ val response = submit("accepted" -> (messageId, None))
assertThatJson(response)
- .inPath("methodResponses[0][1].notCreated")
- .isEqualTo("""{
- | "rejected": {
- | "type": "invalidProperties",
- | "description": "Missing 'identityId' property",
- | "properties": ["identityId"]
- | }
- |}""".stripMargin)
+ .whenIgnoringPaths("methodResponses[0][1].created.accepted")
+ .inPath("methodResponses[0][1].created")
+ .isEqualTo("""{"accepted": {}}""")
andreInboxShouldOnlyContain(server, andreInboxId, "accepted")
}
diff --git a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
index 04422c0069..b40c6f1ab9 100644
--- a/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
+++ b/server/protocols/jmap-rfc-8621/doc/specs/spec/mail/messagesubmission.mdown
@@ -11,7 +11,7 @@ An **EmailSubmission** object represents the submission of an Email for delivery
The id of the Identity to associate with this submission.
> :information_source:
-> James requires this field. A submission whose identityId is missing, or is not one of the user's identities, is rejected with an `invalidProperties` SetError.
+> James validates this field when present. A submission whose identityId is not one of the user's identities is rejected with an `invalidProperties` SetError. A submission without identityId is accepted.
- **emailId**: `Id` (immutable)
The id of the Email to send. The Email being sent does not have to be a draft, for example, when "redirecting" an existing Email to a different address.
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index 619250e6a0..bfee67329d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -366,13 +366,15 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
EmailSubmissionCreationResponse(submissionId, sendAt) -> request.emailId
}
- private def validateIdentity(mailboxSession: MailboxSession, maybeIdentityId: Option[Id]): SMono[Identity] =
+ // A missing identityId is tolerated as some clients omit it; a wrong one is rejected
+ private def validateIdentity(mailboxSession: MailboxSession, maybeIdentityId: Option[Id]): SMono[Option[Identity]] =
maybeIdentityId
.map(id => UnparsedIdentityId(id).validate
.fold(_ => SMono.empty[Identity],
identityId => identityResolver.resolveIdentityId(identityId, mailboxSession).flatMap(SMono.justOrEmpty(_)))
- .switchIfEmpty(SMono.error(IdentityNotFoundException("The identity to be used cannot be found"))))
- .getOrElse(SMono.error(IdentityNotFoundException("Missing 'identityId' property")))
+ .switchIfEmpty(SMono.error(IdentityNotFoundException("The identity to be used cannot be found")))
+ .map(Option(_)))
+ .getOrElse(SMono.just(None))
/**
* Runs the pluggable validations against the mail about to be spooled. The first rejection wins,
diff --git a/upgrade-instructions.md b/upgrade-instructions.md
index d3438d3509..399a554081 100644
--- a/upgrade-instructions.md
+++ b/upgrade-instructions.md
@@ -26,19 +26,19 @@ Change list:
- [Dropping unneeded Cassandra schema columns](#dropping-unneeded-cassandra-schema-columns)
- [Cassandra schema version 16: mandatory message denormalization migration](#cassandra-schema-version-16-mandatory-message-denormalization-migration)
- [Migration of embedded mail queue broker from ActiveMQ Classic to ActiveMQ Artemis](#migration-of-embedded-mail-queue-broker-from-activemq-classic-to-activemq-artemis)
- - [JAMES-4236 JMAP EmailSubmission/set requires identityId](#james-4236-jmap-emailsubmissionset-requires-identityid)
+ - [JAMES-4236 JMAP EmailSubmission/set validates identityId](#james-4236-jmap-emailsubmissionset-validates-identityid)
-### JAMES-4236 JMAP EmailSubmission/set requires identityId
+### JAMES-4236 JMAP EmailSubmission/set validates identityId
Date: 14/10/2026
Concerned products: all products exposing JMAP (RFC 8621)
-`EmailSubmission/set` now validates `identityId`, as required by RFC 8621. A creation without `identityId`, or with
-an id that is not one of the user's identities (see `Identity/get`), is rejected with an `invalidProperties` SetError
-on `identityId`, and the email is not sent.
+`EmailSubmission/set` now validates `identityId`, as required by RFC 8621. A creation with an id that is not one of
+the user's identities (see `Identity/get`) is rejected with an `invalidProperties` SetError on `identityId`, and the
+email is not sent. A creation without `identityId` is still accepted.
-JMAP clients that omit `identityId` must be updated to send it before upgrading.
+JMAP clients that send a stale or foreign `identityId` must be updated to send one listed by `Identity/get`.
### Migration of embedded mail queue broker from ActiveMQ Classic to ActiveMQ Artemis
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org