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]