You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/04/07 00:57:08 UTC

[james-project] branch master updated: JAMES-3434 Fix EmailSubmission/set onSuccessUpdateEmail property (#356)

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

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git


The following commit(s) were added to refs/heads/master by this push:
     new 486d03c  JAMES-3434 Fix EmailSubmission/set onSuccessUpdateEmail property (#356)
486d03c is described below

commit 486d03cd2f8cfb14d3344377a098c106d8e4a652
Author: Tellier Benoit <bt...@linagora.com>
AuthorDate: Wed Apr 7 07:57:03 2021 +0700

    JAMES-3434 Fix EmailSubmission/set onSuccessUpdateEmail property (#356)
    
    * JAMES-3434 Fix EmailSubmission/set onSuccessUpdateEmail property
    
    Re-reading https://jmap.io/spec-mail.html#emailsubmissionset
    
    ```
    onSuccessUpdateEmail: Id[PatchObject]|null A map of EmailSubmission id
    to an object containing properties to update on the Email object
    referenced by the EmailSubmission if the create/update/destroy
    succeeds. (For references to EmailSubmissions created in the same
    “/set” invocation, this is equivalent to a creation-reference, so the
    id will be the creation id prefixed with a #.)
    ```
    
    We are currently using a message id, not an EmailSubmission id...
    
    * JAMES-3434 Pre-validate EmailSubmissionSet
    
    We should not send the email if we are not able to resolve the
    onSuccessUpdateEmail afterwards.
    
    * JAMES-3434 Document EmailSubmission/set onSuccessUpdateEmail limitations
    
    * JAMES-3434 EmailSubmission/set onSuccessDestroy fix
    
    It also relies on an EmailSubmission creation id, that then
    needs to be resoled to MessageIds.
---
 .../EmailSubmissionSetMethodContract.scala         | 268 ++++++++++++++++++++-
 .../doc/specs/spec/mail/messagesubmission.mdown    |   8 +-
 .../james/jmap/mail/EmailSubmissionSet.scala       |  77 +++++-
 .../jmap/method/EmailSubmissionSetMethod.scala     | 101 +++++---
 4 files changed, 406 insertions(+), 48 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 79ca9d5..b526e76 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
@@ -857,7 +857,7 @@ trait EmailSubmissionSetMethodContract {
          |         }
          |       },
          |       "onSuccessUpdateEmail": {
-         |         "${messageId.serialize}": {
+         |         "#k1490": {
          |           "keywords": {"$$sent":true}
          |         }
          |       }
@@ -933,6 +933,142 @@ trait EmailSubmissionSetMethodContract {
   }
 
   @Test
+  def setShouldFailWhenOnSuccessUpdateEmailMissesTheCreationIdSharp(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |       },
+         |       "onSuccessUpdateEmail": {
+         |         "notStored": {
+         |           "keywords": {"$$sent":true}
+         |         }
+         |       }
+         |   }, "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)
+      .isEqualTo(s"""{
+                    |    "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+                    |    "methodResponses": [
+                    |        [
+                    |            "error",
+                    |            {
+                    |                "type": "invalidArguments",
+                    |                "description": "notStored cannot be retrieved as storage for EmailSubmission is not yet implemented"
+                    |            },
+                    |            "c1"
+                    |        ]
+                    |    ]
+                    |}""".stripMargin)
+  }
+
+  @Test
+  def setShouldFailWhenOnSuccessUpdateEmailDoesNotReferenceACreationWithinThisCall(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |       },
+         |       "onSuccessUpdateEmail": {
+         |         "#badReference": {
+         |           "keywords": {"$$sent":true}
+         |         }
+         |       }
+         |   }, "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)
+      .isEqualTo(s"""{
+                    |    "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+                    |    "methodResponses": [
+                    |        [
+                    |            "error",
+                    |            {
+                    |                "type": "invalidArguments",
+                    |                "description": "#badReference cannot be referenced in current method call"
+                    |            },
+                    |            "c1"
+                    |        ]
+                    |    ]
+                    |}""".stripMargin)
+  }
+
+  @Test
   def onSuccessDestroyEmailShouldTriggerAnImplicitEmailSetCall(server: GuiceJamesServer): Unit = {
     val message: Message = Message.Builder
       .of
@@ -964,7 +1100,7 @@ trait EmailSubmissionSetMethodContract {
          |           }
          |         }
          |       },
-         |       "onSuccessDestroyEmail": ["${messageId.serialize}"]
+         |       "onSuccessDestroyEmail": ["#k1490"]
          |   }, "c1"],
          |   ["Email/get",
          |     {
@@ -1030,6 +1166,134 @@ trait EmailSubmissionSetMethodContract {
   }
 
   @Test
+  def setShouldFailWhenOnSuccessDestroyEmailMissesTheCreationIdSharp(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |       },
+         |       "onSuccessDestroyEmail": ["notFound"]
+         |   }, "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)
+      .isEqualTo(s"""{
+                    |    "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+                    |    "methodResponses": [
+                    |        [
+                    |            "error",
+                    |            {
+                    |                "type": "invalidArguments",
+                    |                "description": "notFound cannot be retrieved as storage for EmailSubmission is not yet implemented"
+                    |            },
+                    |            "c1"
+                    |        ]
+                    |    ]
+                    |}""".stripMargin)
+  }
+
+  @Test
+  def setShouldFailWhenOnSuccessDestroyEmailDoesNotReferenceACreationWithinThisCall(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |       },
+         |       "onSuccessDestroyEmail": ["#notFound"]
+         |   }, "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)
+      .isEqualTo(s"""{
+                    |    "sessionState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+                    |    "methodResponses": [
+                    |        [
+                    |            "error",
+                    |            {
+                    |                "type": "invalidArguments",
+                    |                "description": "#notFound cannot be referenced in current method call"
+                    |            },
+                    |            "c1"
+                    |        ]
+                    |    ]
+                    |}""".stripMargin)
+  }
+
+  @Test
   def setShouldRejectOtherAccountIds(server: GuiceJamesServer): Unit = {
     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 0777f32..0a45fd1 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
@@ -211,14 +211,14 @@ This is a standard "/set" method as described in [@!RFC8620], Section 5.3 with t
 - **onSuccessUpdateEmail**: `Id[PatchObject]|null`
   A map of *EmailSubmission id* to an object containing properties to update on the Email object referenced by the EmailSubmission if the create/update/destroy succeeds. (For references to EmailSubmissions created in the same "/set" invocation, this is equivalent to a creation-reference, so the id will be the creation id prefixed with a `#`.)
 
-> :information_source:
-> Implemented
+> :warning:
+> Partially implemented: Because we do not have storage for email submission, references across method calls, and API requests will not be supported.
 
 - **onSuccessDestroyEmail**: `Id[]|null`
   A list of *EmailSubmission ids* for which the Email with the corresponding emailId should be destroyed if the create/update/destroy succeeds. (For references to EmailSubmission creations, this is equivalent to a creation-reference so the id will be the creation id prefixed with a `#`.)
 
-> :information_source:
-> Implemented
+> :warning:
+> Partially implemented: Because we do not have storage for email submission, references across method calls, and API requests will not be supported.
 
 After all create/update/destroy items in the *EmailSubmission/set* invocation  have been processed, a single implicit *Email/set* call MUST be made to perform any changes requested in these two arguments. The response to this MUST be returned after the *EmailSubmission/set* response.
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
index e0944fc..0e1da02 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
@@ -21,11 +21,13 @@ package org.apache.james.jmap.mail
 
 import java.util.UUID
 
+import cats.implicits._
+import eu.timepit.refined.auto.autoUnwrap
 import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.refineV
 import eu.timepit.refined.types.string.NonEmptyString
 import org.apache.james.core.MailAddress
-import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.Id.{Id, IdConstraint}
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.core.{AccountId, Id, Properties, SetError, State}
 import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
@@ -44,13 +46,72 @@ object EmailSubmissionId {
 
 case class EmailSubmissionSetRequest(accountId: AccountId,
                                      create: Option[Map[EmailSubmissionCreationId, JsObject]],
-                                     onSuccessUpdateEmail: Option[Map[UnparsedMessageId, JsObject]],
-                                     onSuccessDestroyEmail: Option[DestroyIds]) extends WithAccountId {
-  def implicitEmailSetRequest: EmailSetRequest = EmailSetRequest(
-    accountId = accountId,
-    create = None,
-    update = onSuccessUpdateEmail,
-    destroy = onSuccessDestroyEmail)
+                                     onSuccessUpdateEmail: Option[Map[EmailSubmissionCreationId, JsObject]],
+                                     onSuccessDestroyEmail: Option[List[EmailSubmissionCreationId]]) extends WithAccountId {
+  def implicitEmailSetRequest(messageIdResolver: EmailSubmissionCreationId => Either[IllegalArgumentException, MessageId]): Either[IllegalArgumentException, EmailSetRequest] =
+    for {
+      update <- resolveOnSuccessUpdateEmail(messageIdResolver)
+      destroy <- resolveOnSuccessDestroyEmail(messageIdResolver)
+    } yield {
+      EmailSetRequest(
+        accountId = accountId,
+        create = None,
+        update = update,
+        destroy = destroy.map(DestroyIds(_)))
+    }
+
+  def resolveOnSuccessUpdateEmail(messageIdResolver: EmailSubmissionCreationId => Either[IllegalArgumentException, MessageId]): Either[IllegalArgumentException, Option[Map[UnparsedMessageId, JsObject]]]=
+    onSuccessUpdateEmail.map(map => map.toList
+      .map {
+        case (creationId, json) => messageIdResolver.apply(creationId).map(messageId => (EmailSet.asUnparsed(messageId), json))
+      }
+      .sequence
+      .map(list => list.toMap))
+      .sequence
+
+  def resolveOnSuccessDestroyEmail(messageIdResolver: EmailSubmissionCreationId => Either[IllegalArgumentException, MessageId]): Either[IllegalArgumentException, Option[List[UnparsedMessageId]]]=
+    onSuccessDestroyEmail.map(list => list
+      .map(creationId => messageIdResolver.apply(creationId).map(messageId => EmailSet.asUnparsed(messageId)))
+      .sequence)
+      .sequence
+
+  def validate: Either[IllegalArgumentException, EmailSubmissionSetRequest] = {
+    val supportedCreationIds: List[EmailSubmissionCreationId] = create.getOrElse(Map()).keys.toList
+
+    validateOnSuccessUpdateEmail(supportedCreationIds)
+      .flatMap(_ => validateOnSuccessDestroyEmail(supportedCreationIds))
+  }
+
+  private def validateOnSuccessDestroyEmail(supportedCreationIds: List[EmailSubmissionCreationId]) : Either[IllegalArgumentException, EmailSubmissionSetRequest] =
+    onSuccessDestroyEmail.getOrElse(List())
+      .map(id => validate(id, supportedCreationIds))
+      .sequence
+      .map(_ => this)
+
+
+  private def validateOnSuccessUpdateEmail(supportedCreationIds: List[EmailSubmissionCreationId]) : Either[IllegalArgumentException, EmailSubmissionSetRequest] =
+    onSuccessUpdateEmail.getOrElse(Map())
+      .keys
+      .toList
+      .map(id => validate(id, supportedCreationIds))
+      .sequence
+      .map(_ => this)
+
+  private def validate(creationId: EmailSubmissionCreationId, supportedCreationIds: List[EmailSubmissionCreationId]): Either[IllegalArgumentException, EmailSubmissionCreationId] = {
+    if (creationId.startsWith("#")) {
+      val realId = creationId.substring(1)
+      val validatedId: Either[String, EmailSubmissionCreationId] = refineV[IdConstraint](realId)
+      validatedId
+        .left.map(s => new IllegalArgumentException(s))
+        .flatMap(id => if (supportedCreationIds.contains(id)) {
+          scala.Right(id)
+        } else {
+          Left(new IllegalArgumentException(s"$creationId cannot be referenced in current method call"))
+        })
+    } else {
+      Left(new IllegalArgumentException(s"$creationId cannot be retrieved as storage for EmailSubmission is not yet implemented"))
+    }
+  }
 }
 
 case class EmailSubmissionSetResponse(accountId: AccountId,
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 544f13b..4c7f82e 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
@@ -23,6 +23,7 @@ import java.io.InputStream
 
 import cats.implicits._
 import eu.timepit.refined.auto._
+import eu.timepit.refined.refineV
 import javax.annotation.PreDestroy
 import javax.inject.Inject
 import javax.mail.Address
@@ -30,6 +31,7 @@ import javax.mail.Message.RecipientType
 import javax.mail.internet.{InternetAddress, MimeMessage}
 import org.apache.james.core.{MailAddress, Username}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, EMAIL_SUBMISSION, JMAP_CORE}
+import org.apache.james.jmap.core.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, Id, Invocation, Properties, ServerId, SetError, State}
@@ -39,7 +41,7 @@ import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreati
 import org.apache.james.jmap.method.EmailSubmissionSetMethod.{LOGGER, MAIL_METADATA_USERNAME_ATTRIBUTE}
 import org.apache.james.jmap.routes.{ProcessingContext, SessionSupplier}
 import org.apache.james.lifecycle.api.{LifecycleUtil, Startable}
-import org.apache.james.mailbox.model.{FetchGroup, MessageResult}
+import org.apache.james.mailbox.model.{FetchGroup, MessageId, MessageResult}
 import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
 import org.apache.james.metrics.api.MetricFactory
 import org.apache.james.queue.api.MailQueueFactory.SPOOL
@@ -47,7 +49,6 @@ import org.apache.james.queue.api.{MailQueue, MailQueueFactory}
 import org.apache.james.rrt.api.CanSendFrom
 import org.apache.james.server.core.{MailImpl, MimeMessageSource, MimeMessageWrapper}
 import org.apache.mailet.{Attribute, AttributeName, AttributeValue}
-import org.reactivestreams.Publisher
 import org.slf4j.{Logger, LoggerFactory}
 import play.api.libs.json._
 import reactor.core.scala.publisher.{SFlux, SMono}
@@ -91,7 +92,9 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
   sealed trait CreationResult {
     def emailSubmissionCreationId: EmailSubmissionCreationId
   }
-  case class CreationSuccess(emailSubmissionCreationId: EmailSubmissionCreationId, emailSubmissionCreationResponse: EmailSubmissionCreationResponse) extends CreationResult
+  case class CreationSuccess(emailSubmissionCreationId: EmailSubmissionCreationId,
+                             emailSubmissionCreationResponse: EmailSubmissionCreationResponse,
+                             messageId: MessageId) extends CreationResult
   case class CreationFailure(emailSubmissionCreationId: EmailSubmissionCreationId, exception: Throwable) extends CreationResult {
     def asSetError: SetError = exception match {
       case e: EmailSubmissionCreationParseException => e.setError
@@ -126,6 +129,29 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
         case _ => None
       }
       .toMap
+
+    def resolveMessageId(creationId: EmailSubmissionCreationId): Either[IllegalArgumentException, MessageId] = {
+      if (creationId.startsWith("#")) {
+        val realId = creationId.substring(1)
+        val validatedId: Either[String, EmailSubmissionCreationId] = refineV[IdConstraint](realId)
+        validatedId
+          .left.map(s => new IllegalArgumentException(s))
+          .flatMap(id => retrieveMessageId(id)
+            .map(scala.Right(_))
+            .getOrElse(Left(new IllegalArgumentException(s"$creationId cannot be referenced in current method call"))))
+      } else {
+        Left(new IllegalArgumentException(s"$creationId cannot be retrieved as storage for EmailSubmission is not yet implemented"))
+      }
+    }
+
+    private def retrieveMessageId(creationId: EmailSubmissionCreationId): Option[MessageId] =
+      created.flatMap {
+        case success: CreationSuccess => Some(success)
+        case _: CreationFailure => None
+      }.filter(_.emailSubmissionCreationId.equals(creationId))
+        .map(_.messageId)
+        .toList
+        .headOption
   }
 
   def init: Unit =
@@ -135,34 +161,40 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
     Try(queue.close())
       .recover(e => LOGGER.debug("error closing queue", e))
 
-  override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSubmissionSetRequest): Publisher[InvocationWithContext] = {
-    val result = for {
-      createdResults <- create(request, mailboxSession, invocation.processingContext)
-    } yield InvocationWithContext(
-      invocation = Invocation(
-        methodName = invocation.invocation.methodName,
-        arguments = Arguments(serializer.serializeEmailSubmissionSetResponse(EmailSubmissionSetResponse(
-            accountId = request.accountId,
-            newState = State.INSTANCE,
-            created = Some(createdResults._1.retrieveCreated).filter(_.nonEmpty),
-            notCreated = Some(createdResults._1.retrieveErrors).filter(_.nonEmpty)))
-          .as[JsObject]),
-        methodCallId = invocation.invocation.methodCallId),
-      processingContext = createdResults._2)
-
-    SFlux.concat(result,
-      emailSetMethod.doProcess(
-        capabilities = capabilities,
-        invocation = invocation,
-        mailboxSession = mailboxSession,
-        request = request.implicitEmailSetRequest))
-  }
+  override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSubmissionSetRequest): SFlux[InvocationWithContext] =
+    create(request, mailboxSession, invocation.processingContext)
+      .flatMapMany(createdResults => {
+        val explicitInvocation: InvocationWithContext = InvocationWithContext(
+          invocation = Invocation(
+            methodName = invocation.invocation.methodName,
+            arguments = Arguments(serializer.serializeEmailSubmissionSetResponse(EmailSubmissionSetResponse(
+              accountId = request.accountId,
+              newState = State.INSTANCE,
+              created = Some(createdResults._1.retrieveCreated).filter(_.nonEmpty),
+              notCreated = Some(createdResults._1.retrieveErrors).filter(_.nonEmpty)))
+              .as[JsObject]),
+            methodCallId = invocation.invocation.methodCallId),
+          processingContext = createdResults._2)
+
 
-  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, EmailSubmissionSetRequest] =
-    serializer.deserializeEmailSubmissionSetRequest(invocation.arguments.value) match {
+        val emailSetCall: SMono[InvocationWithContext] = request.implicitEmailSetRequest(createdResults._1.resolveMessageId)
+          .fold(e => SMono.error(e),
+            emailSetRequest => emailSetMethod.doProcess(
+              capabilities = capabilities,
+              invocation = invocation,
+              mailboxSession = mailboxSession,
+              request = emailSetRequest))
+
+        SFlux.concat(SMono.just(explicitInvocation), emailSetCall)
+      })
+
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, EmailSubmissionSetRequest] = {
+    val maybeRequestRequest = serializer.deserializeEmailSubmissionSetRequest(invocation.arguments.value) match {
       case JsSuccess(emailSubmissionSetRequest, _) => Right(emailSubmissionSetRequest)
       case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
     }
+    maybeRequestRequest.flatMap(_.validate)
+  }
 
   private def create(request: EmailSubmissionSetRequest,
                      session: MailboxSession,
@@ -185,12 +217,13 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
                             processingContext: ProcessingContext): (CreationResult, ProcessingContext) =
     parseCreate(jsObject)
       .flatMap(emailSubmissionCreationRequest => sendEmail(mailboxSession, emailSubmissionCreationRequest))
-      .flatMap(creationResponse => recordCreationIdInProcessingContext(emailSubmissionCreationId, processingContext, creationResponse.id)
-        .map(context => (creationResponse, context)))
+      .flatMap {
+        case (creationResponse, messageId) =>
+          recordCreationIdInProcessingContext(emailSubmissionCreationId, processingContext, creationResponse.id)
+            .map(context => (creationResponse, messageId, context))
+      }
       .fold(e => (CreationFailure(emailSubmissionCreationId, e), processingContext),
-        creationResponseWithUpdatedContext => {
-          (CreationSuccess(emailSubmissionCreationId, creationResponseWithUpdatedContext._1), creationResponseWithUpdatedContext._2)
-        })
+        creation => CreationSuccess(emailSubmissionCreationId, creation._1, creation._2) -> creation._3)
 
   private def parseCreate(jsObject: JsObject): Either[EmailSubmissionCreationParseException, EmailSubmissionCreationRequest] =
     EmailSubmissionCreationRequest.validateProperties(jsObject)
@@ -208,7 +241,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
     }
 
   private def sendEmail(mailboxSession: MailboxSession,
-                        request: EmailSubmissionCreationRequest): Either[Throwable, EmailSubmissionCreationResponse] = {
+                        request: EmailSubmissionCreationRequest): Either[Throwable, (EmailSubmissionCreationResponse, MessageId)] = {
     val message: Either[Exception, MessageResult] = messageIdManager.getMessage(request.emailId, FetchGroup.FULL_CONTENT, mailboxSession)
       .asScala
       .toList
@@ -236,7 +269,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
       } yield {
         EmailSubmissionCreationResponse(submissionId)
       }
-      result.toEither
+      result.toEither.map(response => (response, request.emailId))
     })
   }
 

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org