chibenwa commented on code in PR #1561:
URL: https://github.com/apache/james-project/pull/1561#discussion_r1204950343


##########
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/SessionRoutesContract.scala:
##########
@@ -18,6 +18,8 @@
  * ***************************************************************/
 package org.apache.james.jmap.rfc8621.contract
 
+import java.nio.charset.StandardCharsets

Review Comment:
   Noise in the import changes in this file



##########
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala:
##########
@@ -248,30 +252,54 @@ class EmailSubmissionSetMethod @Inject()(serializer: 
EmailSubmissionSetSerialize
   private def sendEmail(mailboxSession: MailboxSession,
                         request: EmailSubmissionCreationRequest): 
SMono[(EmailSubmissionCreationResponse, MessageId)] =
    for {
-      message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
+     message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
         .next
         .switchIfEmpty(SMono.error(MessageNotFoundException(request.emailId)))
-      submissionId = EmailSubmissionId.generate
-      message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
-      envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
-      _ <- validate(mailboxSession)(message, envelope)
-      mail = {
-        val mailImpl = MailImpl.builder()
-          .name(submissionId.value)
-          .addRecipients(envelope.rcptTo.map(_.email).asJava)
-          .sender(envelope.mailFrom.email)
-          .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
-          .build()
-        mailImpl.setMessageNoCopy(message)
-        mailImpl
-      }
-      _ <- SMono(queue.enqueueReactive(mail))
-        .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
-        .`then`(SMono.just(submissionId))
+     submissionId = EmailSubmissionId.generate
+     message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
+     envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
+     _ <- validate(mailboxSession)(message, envelope)
+
+     parameters = request.envelope.get.mailFrom.parameters

Review Comment:
   parameters -> fromParameters



##########
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala:
##########
@@ -248,30 +252,54 @@ class EmailSubmissionSetMethod @Inject()(serializer: 
EmailSubmissionSetSerialize
   private def sendEmail(mailboxSession: MailboxSession,
                         request: EmailSubmissionCreationRequest): 
SMono[(EmailSubmissionCreationResponse, MessageId)] =
    for {
-      message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
+     message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
         .next
         .switchIfEmpty(SMono.error(MessageNotFoundException(request.emailId)))
-      submissionId = EmailSubmissionId.generate
-      message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
-      envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
-      _ <- validate(mailboxSession)(message, envelope)
-      mail = {
-        val mailImpl = MailImpl.builder()
-          .name(submissionId.value)
-          .addRecipients(envelope.rcptTo.map(_.email).asJava)
-          .sender(envelope.mailFrom.email)
-          .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
-          .build()
-        mailImpl.setMessageNoCopy(message)
-        mailImpl
-      }
-      _ <- SMono(queue.enqueueReactive(mail))
-        .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
-        .`then`(SMono.just(submissionId))
+     submissionId = EmailSubmissionId.generate
+     message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
+     envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
+     _ <- validate(mailboxSession)(message, envelope)
+
+     parameters = request.envelope.get.mailFrom.parameters
+     delay = getDuration(parameters)
+     mail = {
+       val mailImpl = MailImpl.builder()
+         .name(submissionId.value)
+         .addRecipients(envelope.rcptTo.map(_.email).asJava)
+         .sender(envelope.mailFrom.email)
+         .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
+         .build()
+       mailImpl.setMessageNoCopy(message)
+       mailImpl
+     }
+     _ <- SMono (queue.enqueueReactive(mail, delay))
+     .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
+     .`then`(SMono.just(submissionId))
+
     } yield {
-      EmailSubmissionCreationResponse(submissionId) -> request.emailId
+//     if (validateDuration(delay)) {
+//
+//     }
+     EmailSubmissionCreationResponse(submissionId) -> request.emailId
+    }
+  private def getDuration(mailParameters: Option[Map[ParameterName, 
Option[ParameterValue]]]): Duration = {
+    if (mailParameters.isEmpty) {
+      Duration.ofSeconds(0)
     }
+    if (mailParameters.get.size == 1)
+    {
+      val parameterName = mailParameters.get.head._1.value
+      val parameterValue = mailParameters.get.head._2.get.value
+      if (parameterName.eq("holdFor")) {
+        Duration.ofSeconds(parameterValue.toLong)
+      } else if (parameterName.eq("holdUntil")) {
+        val formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("Z"))
+        Duration.between(LocalDateTime.now(CLOCK), 
LocalDateTime.parse(parameterValue, formatter))
+      } else Duration.ofSeconds(-1);
+    } else null

Review Comment:
   What is -1?
   What is null?
   
   Manage errors properly with Either monad please.



##########
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala:
##########
@@ -248,30 +252,54 @@ class EmailSubmissionSetMethod @Inject()(serializer: 
EmailSubmissionSetSerialize
   private def sendEmail(mailboxSession: MailboxSession,
                         request: EmailSubmissionCreationRequest): 
SMono[(EmailSubmissionCreationResponse, MessageId)] =
    for {
-      message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
+     message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
         .next
         .switchIfEmpty(SMono.error(MessageNotFoundException(request.emailId)))
-      submissionId = EmailSubmissionId.generate
-      message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
-      envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
-      _ <- validate(mailboxSession)(message, envelope)
-      mail = {
-        val mailImpl = MailImpl.builder()
-          .name(submissionId.value)
-          .addRecipients(envelope.rcptTo.map(_.email).asJava)
-          .sender(envelope.mailFrom.email)
-          .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
-          .build()
-        mailImpl.setMessageNoCopy(message)
-        mailImpl
-      }
-      _ <- SMono(queue.enqueueReactive(mail))
-        .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
-        .`then`(SMono.just(submissionId))
+     submissionId = EmailSubmissionId.generate
+     message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
+     envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
+     _ <- validate(mailboxSession)(message, envelope)
+
+     parameters = request.envelope.get.mailFrom.parameters
+     delay = getDuration(parameters)
+     mail = {
+       val mailImpl = MailImpl.builder()
+         .name(submissionId.value)
+         .addRecipients(envelope.rcptTo.map(_.email).asJava)
+         .sender(envelope.mailFrom.email)
+         .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
+         .build()
+       mailImpl.setMessageNoCopy(message)
+       mailImpl
+     }
+     _ <- SMono (queue.enqueueReactive(mail, delay))
+     .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
+     .`then`(SMono.just(submissionId))
+
     } yield {
-      EmailSubmissionCreationResponse(submissionId) -> request.emailId
+//     if (validateDuration(delay)) {
+//
+//     }
+     EmailSubmissionCreationResponse(submissionId) -> request.emailId
+    }
+  private def getDuration(mailParameters: Option[Map[ParameterName, 
Option[ParameterValue]]]): Duration = {
+    if (mailParameters.isEmpty) {
+      Duration.ofSeconds(0)
     }
+    if (mailParameters.get.size == 1)
+    {
+      val parameterName = mailParameters.get.head._1.value
+      val parameterValue = mailParameters.get.head._2.get.value
+      if (parameterName.eq("holdFor")) {
+        Duration.ofSeconds(parameterValue.toLong)
+      } else if (parameterName.eq("holdUntil")) {
+        val formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("Z"))
+        Duration.between(LocalDateTime.now(CLOCK), 
LocalDateTime.parse(parameterValue, formatter))

Review Comment:
   Inject a clock for this please



##########
server/queue/queue-jms/src/main/java/org/apache/james/queue/jms/JMSCacheableMailQueue.java:
##########
@@ -312,6 +312,11 @@ public Publisher<Void> enqueueReactive(Mail mail) {
         return Mono.fromRunnable(Throwing.runnable(() -> 
enQueue(mail)).sneakyThrow());
     }
 
+    @Override
+    public Publisher<Void> enqueueReactive(Mail mail, Duration delay) {
+        return null;
+    }

Review Comment:
   Not acceptable



##########
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala:
##########
@@ -248,30 +252,54 @@ class EmailSubmissionSetMethod @Inject()(serializer: 
EmailSubmissionSetSerialize
   private def sendEmail(mailboxSession: MailboxSession,
                         request: EmailSubmissionCreationRequest): 
SMono[(EmailSubmissionCreationResponse, MessageId)] =
    for {
-      message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
+     message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
         .next
         .switchIfEmpty(SMono.error(MessageNotFoundException(request.emailId)))
-      submissionId = EmailSubmissionId.generate
-      message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
-      envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
-      _ <- validate(mailboxSession)(message, envelope)
-      mail = {
-        val mailImpl = MailImpl.builder()
-          .name(submissionId.value)
-          .addRecipients(envelope.rcptTo.map(_.email).asJava)
-          .sender(envelope.mailFrom.email)
-          .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
-          .build()
-        mailImpl.setMessageNoCopy(message)
-        mailImpl
-      }
-      _ <- SMono(queue.enqueueReactive(mail))
-        .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
-        .`then`(SMono.just(submissionId))
+     submissionId = EmailSubmissionId.generate
+     message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
+     envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
+     _ <- validate(mailboxSession)(message, envelope)
+
+     parameters = request.envelope.get.mailFrom.parameters
+     delay = getDuration(parameters)
+     mail = {
+       val mailImpl = MailImpl.builder()
+         .name(submissionId.value)
+         .addRecipients(envelope.rcptTo.map(_.email).asJava)
+         .sender(envelope.mailFrom.email)
+         .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
+         .build()
+       mailImpl.setMessageNoCopy(message)
+       mailImpl
+     }
+     _ <- SMono (queue.enqueueReactive(mail, delay))
+     .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
+     .`then`(SMono.just(submissionId))
+
     } yield {
-      EmailSubmissionCreationResponse(submissionId) -> request.emailId
+//     if (validateDuration(delay)) {
+//
+//     }
+     EmailSubmissionCreationResponse(submissionId) -> request.emailId
+    }
+  private def getDuration(mailParameters: Option[Map[ParameterName, 
Option[ParameterValue]]]): Duration = {
+    if (mailParameters.isEmpty) {
+      Duration.ofSeconds(0)
     }
+    if (mailParameters.get.size == 1)
+    {
+      val parameterName = mailParameters.get.head._1.value
+      val parameterValue = mailParameters.get.head._2.get.value
+      if (parameterName.eq("holdFor")) {

Review Comment:
   ignore case?



##########
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala:
##########
@@ -128,18 +128,25 @@ case class EmailSubmissionAddress(email: MailAddress, 
parameters: Option[Map[Par
 case class Envelope(mailFrom: EmailSubmissionAddress, rcptTo: 
List[EmailSubmissionAddress])
 
 object EmailSubmissionCreationRequest {
-  private val assignableProperties = Set("emailId", "envelope", "identityId", 
"onSuccessUpdateEmail")
+  private val assignableProperties = Set("emailId", "envelope", "identityId", 
"onSuccessUpdateEmail", "sendAt")
 
-  def validateProperties(jsObject: JsObject): 
Either[EmailSubmissionCreationParseException, JsObject] =
+  def validateProperties(jsObject: JsObject): 
Either[EmailSubmissionCreationParseException, JsObject] = {
     jsObject.keys.diff(assignableProperties) match {
       case unknownProperties if unknownProperties.nonEmpty =>
         Left(EmailSubmissionCreationParseException(SetError.invalidArguments(
           SetErrorDescription("Some unknown properties were specified"),
           Some(toProperties(unknownProperties.toSet)))))
-      case _ => scala.Right(jsObject)
+      case _ => {
+        println("jsO" + jsObject)
+        scala.Right(jsObject)
+      }
     }
+
+  }
 }
 
 case class EmailSubmissionCreationRequest(emailId: MessageId,
                                           identityId: Option[Id],
-                                          envelope: Option[Envelope])
\ No newline at end of file
+                                          envelope: Option[Envelope],
+                                          sendAt: UTCDate)

Review Comment:
   In EmailSubmissionCreationResponse actually



##########
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala:
##########
@@ -417,4 +672,560 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
         .hasSize(1)
     }
   }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldForIsNotANumber(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdFor": 
"not a number",
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(requestAndre)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+        .statusCode(SC_OK)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldDelayEmailWithHoldUntil(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-14T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(1))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldDeliverEmailWhenHoldUntilExpired(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-14T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldUntilIsInThePast(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-13T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()

Review Comment:
   Assert here!



##########
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala:
##########
@@ -417,4 +672,560 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
         .hasSize(1)
     }
   }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldForIsNotANumber(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdFor": 
"not a number",
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(requestAndre)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+        .statusCode(SC_OK)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldDelayEmailWithHoldUntil(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-14T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(1))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldDeliverEmailWhenHoldUntilExpired(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-14T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldUntilIsInThePast(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-13T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldUntilTooFar(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-16T10:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()

Review Comment:
   Assert here



##########
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala:
##########
@@ -295,10 +294,10 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
     `with`()
       .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
-      .post.prettyPeek()
+      .post
 
     // Wait one second
-    Thread.sleep(1000)
+    CLOCK.setInstant(DATE.plusSeconds(1))

Review Comment:
   Keep the sleep: Processing likely still takes time (for real)



##########
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala:
##########
@@ -248,30 +252,54 @@ class EmailSubmissionSetMethod @Inject()(serializer: 
EmailSubmissionSetSerialize
   private def sendEmail(mailboxSession: MailboxSession,
                         request: EmailSubmissionCreationRequest): 
SMono[(EmailSubmissionCreationResponse, MessageId)] =
    for {
-      message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
+     message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
         .next
         .switchIfEmpty(SMono.error(MessageNotFoundException(request.emailId)))
-      submissionId = EmailSubmissionId.generate
-      message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
-      envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
-      _ <- validate(mailboxSession)(message, envelope)
-      mail = {
-        val mailImpl = MailImpl.builder()
-          .name(submissionId.value)
-          .addRecipients(envelope.rcptTo.map(_.email).asJava)
-          .sender(envelope.mailFrom.email)
-          .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
-          .build()
-        mailImpl.setMessageNoCopy(message)
-        mailImpl
-      }
-      _ <- SMono(queue.enqueueReactive(mail))
-        .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
-        .`then`(SMono.just(submissionId))
+     submissionId = EmailSubmissionId.generate
+     message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
+     envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
+     _ <- validate(mailboxSession)(message, envelope)
+
+     parameters = request.envelope.get.mailFrom.parameters
+     delay = getDuration(parameters)
+     mail = {
+       val mailImpl = MailImpl.builder()
+         .name(submissionId.value)
+         .addRecipients(envelope.rcptTo.map(_.email).asJava)
+         .sender(envelope.mailFrom.email)
+         .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
+         .build()
+       mailImpl.setMessageNoCopy(message)
+       mailImpl
+     }
+     _ <- SMono (queue.enqueueReactive(mail, delay))
+     .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
+     .`then`(SMono.just(submissionId))
+
     } yield {
-      EmailSubmissionCreationResponse(submissionId) -> request.emailId
+//     if (validateDuration(delay)) {
+//
+//     }
+     EmailSubmissionCreationResponse(submissionId) -> request.emailId
+    }
+  private def getDuration(mailParameters: Option[Map[ParameterName, 
Option[ParameterValue]]]): Duration = {
+    if (mailParameters.isEmpty) {
+      Duration.ofSeconds(0)
     }
+    if (mailParameters.get.size == 1)
+    {
+      val parameterName = mailParameters.get.head._1.value
+      val parameterValue = mailParameters.get.head._2.get.value
+      if (parameterName.eq("holdFor")) {
+        Duration.ofSeconds(parameterValue.toLong)
+      } else if (parameterName.eq("holdUntil")) {
+        val formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("Z"))
+        Duration.between(LocalDateTime.now(CLOCK), 
LocalDateTime.parse(parameterValue, formatter))
+      } else Duration.ofSeconds(-1);
+    } else null
+  }
 
+  private def validateDuration(delay: Duration): Boolean = 
delay.getSeconds.>=(0).&&(delay.getSeconds.<=(SubmissionCapabilityFactory.maximumDelays.getSeconds))

Review Comment:
   Line break and code style



##########
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala:
##########
@@ -121,24 +121,32 @@ case class EmailSubmissionSetResponse(accountId: 
AccountId,
 case class EmailSubmissionId(value: Id)
 
 case class EmailSubmissionCreationResponse(id: EmailSubmissionId)
-
-case class EmailSubmissionAddress(email: MailAddress)
+case class ParameterName(value: String) extends AnyVal
+case class ParameterValue(value: String) extends AnyVal
+case class EmailSubmissionAddress(email: MailAddress, parameters: 
Option[Map[ParameterName, Option[ParameterValue]]] = Option.empty)
 
 case class Envelope(mailFrom: EmailSubmissionAddress, rcptTo: 
List[EmailSubmissionAddress])
 
 object EmailSubmissionCreationRequest {
-  private val assignableProperties = Set("emailId", "envelope", "identityId", 
"onSuccessUpdateEmail")
+  private val assignableProperties = Set("emailId", "envelope", "identityId", 
"onSuccessUpdateEmail", "sendAt")

Review Comment:
   sendAt is server set so NOT part of the creation request



##########
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala:
##########
@@ -417,4 +672,560 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
         .hasSize(1)
     }
   }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldForIsNotANumber(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdFor": 
"not a number",
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post

Review Comment:
   Idem asset this



##########
server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala:
##########
@@ -248,30 +252,54 @@ class EmailSubmissionSetMethod @Inject()(serializer: 
EmailSubmissionSetSerialize
   private def sendEmail(mailboxSession: MailboxSession,
                         request: EmailSubmissionCreationRequest): 
SMono[(EmailSubmissionCreationResponse, MessageId)] =
    for {
-      message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
+     message <- 
SFlux(messageIdManager.getMessagesReactive(List(request.emailId).asJava, 
FetchGroup.FULL_CONTENT, mailboxSession))
         .next
         .switchIfEmpty(SMono.error(MessageNotFoundException(request.emailId)))
-      submissionId = EmailSubmissionId.generate
-      message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
-      envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
-      _ <- validate(mailboxSession)(message, envelope)
-      mail = {
-        val mailImpl = MailImpl.builder()
-          .name(submissionId.value)
-          .addRecipients(envelope.rcptTo.map(_.email).asJava)
-          .sender(envelope.mailFrom.email)
-          .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
-          .build()
-        mailImpl.setMessageNoCopy(message)
-        mailImpl
-      }
-      _ <- SMono(queue.enqueueReactive(mail))
-        .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
-        .`then`(SMono.just(submissionId))
+     submissionId = EmailSubmissionId.generate
+     message <- SMono.fromTry(toMimeMessage(submissionId.value, message))
+     envelope <- SMono.fromTry(resolveEnvelope(message, request.envelope))
+     _ <- validate(mailboxSession)(message, envelope)
+
+     parameters = request.envelope.get.mailFrom.parameters
+     delay = getDuration(parameters)
+     mail = {
+       val mailImpl = MailImpl.builder()
+         .name(submissionId.value)
+         .addRecipients(envelope.rcptTo.map(_.email).asJava)
+         .sender(envelope.mailFrom.email)
+         .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, 
AttributeValue.of(mailboxSession.getUser.asString())))
+         .build()
+       mailImpl.setMessageNoCopy(message)
+       mailImpl
+     }
+     _ <- SMono (queue.enqueueReactive(mail, delay))
+     .`then`(SMono.fromCallable(() => 
LifecycleUtil.dispose(mail)).subscribeOn(Schedulers.boundedElastic()))
+     .`then`(SMono.just(submissionId))
+
     } yield {
-      EmailSubmissionCreationResponse(submissionId) -> request.emailId
+//     if (validateDuration(delay)) {
+//
+//     }
+     EmailSubmissionCreationResponse(submissionId) -> request.emailId
+    }
+  private def getDuration(mailParameters: Option[Map[ParameterName, 
Option[ParameterValue]]]): Duration = {
+    if (mailParameters.isEmpty) {
+      Duration.ofSeconds(0)
     }
+    if (mailParameters.get.size == 1)
+    {
+      val parameterName = mailParameters.get.head._1.value
+      val parameterValue = mailParameters.get.head._2.get.value
+      if (parameterName.eq("holdFor")) {
+        Duration.ofSeconds(parameterValue.toLong)
+      } else if (parameterName.eq("holdUntil")) {
+        val formatter = DateTimeFormatter.ISO_INSTANT.withZone(ZoneId.of("Z"))

Review Comment:
   Formatter should be static (in the `object` section)



##########
server/queue/queue-api/src/main/java/org/apache/james/queue/api/MailQueue.java:
##########
@@ -107,6 +107,9 @@ default void enQueue(Mail mail, long delay, TimeUnit unit) 
throws MailQueueExcep
 
     Publisher<Void> enqueueReactive(Mail mail);
 
+
+    Publisher<Void> enqueueReactive(Mail mail, Duration delay);

Review Comment:
   Provide a default implementation?



##########
server/queue/queue-rabbitmq/src/main/java/org/apache/james/queue/rabbitmq/RabbitMQMailQueue.java:
##########
@@ -97,6 +97,11 @@ public Publisher<Void> enqueueReactive(Mail mail) {
         }
     }
 
+    @Override
+    public Publisher<Void> enqueueReactive(Mail mail, Duration delay) {
+        return null;
+    }
+

Review Comment:
   Not accptable



##########
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala:
##########
@@ -384,7 +383,263 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
 
     CLOCK.setInstant(DATE.plusSeconds(77000))
 
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(requestAndre)
+          .build, new ResponseSpecBuilder().build)
+        .post.prettyPeek()
+        .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldForGreaterThanSupportedValue(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdFor": 
"7776000"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldForIsNegative 
(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdFor": 
"-1000"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()

Review Comment:
   
   
   Make an assertion on this response and discard the rest of the test
   
   IE enforce that the method returns an error here



##########
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala:
##########
@@ -417,4 +672,560 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
         .hasSize(1)
     }
   }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldForIsNotANumber(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdFor": 
"not a number",
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(requestAndre)
+          .build, new ResponseSpecBuilder().build)
+        .post
+        .`then`
+        .statusCode(SC_OK)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldDelayEmailWithHoldUntil(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-14T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(1))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldDeliverEmailWhenHoldUntilExpired(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-14T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldUntilIsInThePast(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-13T15:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldUntilTooFar(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-16T10:00:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()
+
+    CLOCK.setInstant(DATE.plusSeconds(77000))
+
+    // Ensure Andre did not receive the email
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, 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)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(0)
+    }
+  }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldSubmitedMailSuccessfullyWithHoldUntil(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 request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-14T10:30:00Z"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    val greaterThanZero: Matcher[Integer] = Matchers.greaterThan(0)
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .when
+      .post
+      .`then`
+      .statusCode(SC_OK)
+      .body("methodResponses[0][1].created.k1490.id", 
CharSequenceLength.hasLength(greaterThanZero))
+  }
+
+  @Disabled("Not yet implemented")
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedWhenMailContainsBothHoldForAndHoldUntil(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 request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdUntil": 
"2023-04-14T10:30:00Z",
+         |                "holdFor:": "76000"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    val isZero: Matcher[Integer] = Matchers.is(0)
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .when
+      .post
+      .`then`
+      .statusCode(SC_OK)
+      .body("methodResponses[0][1].created.k1490.id", 
CharSequenceLength.hasLength(isZero))
+  }
+  // TODO testA: holdFor with a biiiiiig number => EmailSubmission/set-create 
should be rejected with an error describing the problem
+  // TODO testB: holdFor with a negative number => EmailSubmission/set-create 
should be rejected with an error describing the problem
+  // TODO testC: holdFor with a zero number (delivered immediately) => 
EmailSubmission/set-create should be rejected with an error describing the 
problem
+  // TODO testD: holdFor with a non numeric value => 
EmailSubmission/set-create should be rejected with an error describing the 
problem
+  // TODO testE: holdUntil ensure that the email is delayed => Same 
emailSubmissionSetCreateShouldDelayEmailWithHoldFor
+  // TODO testF: holdUntil ensure that the email is eventually delivered => 
Same emailSubmissionSetCreateShouldDeliverEmailWhenHoldForExpired
+  // TODO testE: holdUntil date in the past => EmailSubmission/set-create 
should be rejected with an error describing the problem
+  // TODO testG: holdUntil date in the too far future => 
EmailSubmission/set-create should be rejected with an error describing the 
problem
+  // TODO testH: holdUntil with a string that is not a valid date => 
EmailSubmission/set-create should be rejected with an error describing the 
problem
+  // TODO testI: unknown mailParameters should be rejected with an error 
describing the problem
+  // TODO testJ: holdFor/holdUntil should be rejected in rcpt mailAddresses
+  // TODO testK: specifying holdFor AND holdUntil should be rejected with an 
error describing the problem
+
+  // AFTER...
+  // TODO testL: when I use holdUntil then EmailSubmission/set create response 
contains sendAt property matching holdUntil value
+  // TODO testM: when I use holdFor then EmailSubmission/set create response 
contains sendAt property matching holdUntil value

Review Comment:
   Can you remove comments for tests you did implement?



##########
server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodFutureReleaseContract.scala:
##########
@@ -384,7 +383,263 @@ trait EmailSubmissionSetMethodFutureReleaseContract {
 
     CLOCK.setInstant(DATE.plusSeconds(77000))
 
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ANDRE_ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(requestAndre)
+          .build, new ResponseSpecBuilder().build)
+        .post.prettyPeek()
+        .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def 
emailSubmissionSetCreateShouldBeRejectedEmailWhenHoldForGreaterThanSupportedValue(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 andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = 
server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+
+    val request =
+      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}",
+         |                                                     "parameters": {
+         |                                                       "holdFor": 
"7776000"
+         |                                                     }
+         |                                             },
+         |                                             "rcptTo": [{
+         |                                                     "email": 
"${ANDRE.asString}"
+         |                                             }]
+         |                                     }
+         |                             }
+         |                     }
+         |             }, "c1"]
+         |     ]
+         |}""".stripMargin
+
+    `with`()
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+      .post.prettyPeek()

Review Comment:
   Make an assertion on this response and discard the rest of the test



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


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

Reply via email to