You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/11/09 11:45:28 UTC

[james-project] branch master updated (8103df2 -> 21d1a90)

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

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


    from 8103df2  JAMES-3400 Add domain exist command
     new 452b44f  JAMES-3434 EmailSubmission/set create
     new ef35a0d  JAMES-3434 Specify tests to be written for EmailSubmission/set create
     new a937abe  JAMES-3434 Validation testing for EmailSubmission/set create
     new 9cdfea9  JAMES-3434 Various EmailSubmission refactorings
     new 06f9d30  JAMES-3434 Allow the use of aliases for EmailSubmission/set create
     new 18462af  JAMES-3434 Allow Methods to produce several responses
     new ec2e021  JAMES-3434 EmailSubmissionCreationResponse should rely on valueWrites
     new 6e725e8  JAMES-3434 EmailSubmission/set implicit onSuccessUpdateEmail call
     new 21d1a90  JAMES-3434 EmailSubmission/set implicit onSuccessDestroyEmail call

The 9 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../modules/activemq/ActiveMQQueueModule.java      |    8 +
 .../modules/server/MemoryMailQueueModule.java      |    7 +
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |   15 +-
 .../james/modules/rabbitmq/RabbitMQModule.java     |    7 +
 ...> DistributedEmailSubmissionSetMethodtest.java} |    4 +-
 .../EmailSubmissionSetMethodContract.scala         | 1402 ++++++++++++++++++++
 .../james/jmap/rfc8621/contract/Fixture.scala      |    1 +
 ...ava => MemoryEmailSubmissionSetMethodTest.java} |    4 +-
 server/protocols/jmap-rfc-8621/pom.xml             |    8 +
 .../james/jmap/json/EmailSetSerializer.scala       |    4 +-
 .../jmap/json/EmailSubmissionSetSerializer.scala   |   83 ++
 .../james/jmap/mail/EmailSubmissionSet.scala       |   90 ++
 .../jmap/method/EmailSetDeletePerformer.scala      |    4 +-
 .../apache/james/jmap/method/EmailSetMethod.scala  |    4 +-
 .../jmap/method/EmailSetUpdatePerformer.scala      |    4 +-
 .../jmap/method/EmailSubmissionSetMethod.scala     |  260 ++++
 .../org/apache/james/jmap/method/Method.scala      |   22 +-
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   |   28 +-
 18 files changed, 1917 insertions(+), 38 deletions(-)
 copy server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/{DistributedEmailSetMethodTest.java => DistributedEmailSubmissionSetMethodtest.java} (94%)
 create mode 100644 server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
 copy server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/{MemoryEmailGetMethodTest.java => MemoryEmailSubmissionSetMethodTest.java} (93%)
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala


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


[james-project] 09/09: JAMES-3434 EmailSubmission/set implicit onSuccessDestroyEmail call

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 21d1a90111188f13713137a384b2d5c673d1e453
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 6 12:03:15 2020 +0700

    JAMES-3434 EmailSubmission/set implicit onSuccessDestroyEmail call
---
 .../EmailSubmissionSetMethodContract.scala         | 95 ++++++++++++++++++++++
 .../james/jmap/json/EmailSetSerializer.scala       |  4 +-
 .../jmap/json/EmailSubmissionSetSerializer.scala   |  3 +-
 .../james/jmap/mail/EmailSubmissionSet.scala       |  5 +-
 4 files changed, 101 insertions(+), 6 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index 247d04f..e0e74bc 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -798,6 +798,101 @@ trait EmailSubmissionSetMethodContract {
   }
 
   @Test
+  def onSuccessDestroyEmailShouldTriggerAnImplicitEmailSetCall(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |       },
+         |       "onSuccessDestroyEmail": ["${messageId.serialize}"]
+         |   }, "c1"],
+         |   ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["${messageId.serialize}"],
+         |       "properties": ["keywords"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      // Ids are randomly generated, and not stored, let's ignore it
+      .whenIgnoringPaths("methodResponses[0][1].created.k1490")
+      .isEqualTo(s"""{
+                   |    "sessionState": "75128aab4b1b",
+                   |    "methodResponses": [
+                   |        [
+                   |            "EmailSubmission/set",
+                   |            {
+                   |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                   |                "newState": "000001",
+                   |                "created": {
+                   |                    "k1490": "f0850507-bb63-4675-b14f-d560f8dca21f"
+                   |                }
+                   |            },
+                   |            "c1"
+                   |        ],
+                   |        [
+                   |            "Email/set",
+                   |            {
+                   |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                   |                "newState": "000001",
+                   |                "destroyed": ["${messageId.serialize}"]
+                   |            },
+                   |            "c1"
+                   |        ],
+                   |        [
+                   |            "Email/get",
+                   |            {
+                   |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                   |                "state": "000001",
+                   |                "list":[],
+                   |                "notFound": ["${messageId.serialize}"]
+                   |            },
+                   |            "c2"
+                   |        ]
+                   |    ]
+                   |}""".stripMargin)
+  }
+
+  @Test
   def setShouldRejectOtherAccountIds(server: GuiceJamesServer): Unit = {
     val message: Message = Message.Builder
       .of
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index 3d455b5..074517c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -223,9 +223,7 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
   private implicit val unitWrites: Writes[Unit] = _ => JsNull
   private implicit val updatedWrites: Writes[Map[MessageId, Unit]] = mapWrites[MessageId, Unit](_.serialize, unitWrites)
   private implicit val notDestroyedWrites: Writes[Map[UnparsedMessageId, SetError]] = mapWrites[UnparsedMessageId, SetError](_.value, setErrorWrites)
-  private implicit val destroyIdsReads: Reads[DestroyIds] = {
-    Json.valueFormat[DestroyIds]
-  }
+  private implicit val destroyIdsReads: Reads[DestroyIds] = Json.valueFormat[DestroyIds]
   private implicit val destroyIdsWrites: Writes[DestroyIds] = Json.valueWrites[DestroyIds]
   private implicit val emailRequestSetReads: Reads[EmailSetRequest] = Json.reads[EmailSetRequest]
   private implicit val emailCreationResponseWrites: Writes[EmailCreationResponse] = Json.writes[EmailCreationResponse]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
index a8e9dbb..645e90e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
@@ -26,7 +26,7 @@ import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.SetError
 import org.apache.james.jmap.mail.EmailSet.{UnparsedMessageId, UnparsedMessageIdConstraint}
 import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
-import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope}
+import org.apache.james.jmap.mail.{DestroyIds, EmailSubmissionAddress, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope}
 import org.apache.james.mailbox.model.MessageId
 import play.api.libs.json.{JsError, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, Reads, Writes}
 
@@ -59,6 +59,7 @@ class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory
         case o: JsObject => JsSuccess(o)
         case _ => JsError("Expecting a JsObject as an update entry")
       })
+  private implicit val destroyIdsReads: Reads[DestroyIds] = Json.valueFormat[DestroyIds]
 
   private implicit val emailSubmissionSetRequestReads: Reads[EmailSubmissionSetRequest] = Json.reads[EmailSubmissionSetRequest]
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
index 416c4f8..9f226e5 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
@@ -45,12 +45,13 @@ object EmailSubmissionId {
 
 case class EmailSubmissionSetRequest(accountId: AccountId,
                                      create: Option[Map[EmailSubmissionCreationId, JsObject]],
-                                     onSuccessUpdateEmail: Option[Map[UnparsedMessageId, JsObject]]) extends WithAccountId {
+                                     onSuccessUpdateEmail: Option[Map[UnparsedMessageId, JsObject]],
+                                     onSuccessDestroyEmail: Option[DestroyIds]) extends WithAccountId {
   def implicitEmailSetRequest: EmailSetRequest = EmailSetRequest(
     accountId = accountId,
     create = None,
     update = onSuccessUpdateEmail,
-    destroy = None)
+    destroy = onSuccessDestroyEmail)
 }
 
 case class EmailSubmissionSetResponse(accountId: AccountId,


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


[james-project] 08/09: JAMES-3434 EmailSubmission/set implicit onSuccessUpdateEmail call

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 6e725e8d85a66e996047514b8dcd358ef14f38eb
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 6 11:56:21 2020 +0700

    JAMES-3434 EmailSubmission/set implicit onSuccessUpdateEmail call
---
 .../EmailSubmissionSetMethodContract.scala         | 106 +++++++++++++++++++++
 .../jmap/json/EmailSubmissionSetSerializer.scala   |   8 ++
 .../james/jmap/mail/EmailSubmissionSet.scala       |  12 ++-
 .../apache/james/jmap/method/EmailSetMethod.scala  |   2 +-
 .../jmap/method/EmailSubmissionSetMethod.scala     |  13 ++-
 5 files changed, 136 insertions(+), 5 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index df9ff97..247d04f 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -692,6 +692,112 @@ trait EmailSubmissionSetMethodContract {
   }
 
   @Test
+  def onSuccessUpdateEmailShouldTriggerAnImplicitEmailSetCall(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |       },
+         |       "onSuccessUpdateEmail": {
+         |         "${messageId.serialize}": {
+         |           "keywords": {"$$sent":true}
+         |         }
+         |       }
+         |   }, "c1"],
+         |   ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["${messageId.serialize}"],
+         |       "properties": ["keywords"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      // Ids are randomly generated, and not stored, let's ignore it
+      .whenIgnoringPaths("methodResponses[0][1].created.k1490")
+      .isEqualTo(s"""{
+                   |    "sessionState": "75128aab4b1b",
+                   |    "methodResponses": [
+                   |        [
+                   |            "EmailSubmission/set",
+                   |            {
+                   |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                   |                "newState": "000001",
+                   |                "created": {
+                   |                    "k1490": "f0850507-bb63-4675-b14f-d560f8dca21f"
+                   |                }
+                   |            },
+                   |            "c1"
+                   |        ],
+                   |        [
+                   |            "Email/set",
+                   |            {
+                   |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                   |                "newState": "000001",
+                   |                "updated": {
+                   |                    "${messageId.serialize}": null
+                   |                }
+                   |            },
+                   |            "c1"
+                   |        ],
+                   |        [
+                   |            "Email/get",
+                   |            {
+                   |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+                   |                "state": "000001",
+                   |                "list": [
+                   |                    {
+                   |                        "keywords": {"$$sent": true},
+                   |                        "id": "${messageId.serialize}"
+                   |                    }
+                   |                ],
+                   |                "notFound": []
+                   |            },
+                   |            "c2"
+                   |        ]
+                   |    ]
+                   |}""".stripMargin)
+  }
+
+  @Test
   def setShouldRejectOtherAccountIds(server: GuiceJamesServer): Unit = {
     val message: Message = Message.Builder
       .of
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
index 66e49b7..a8e9dbb 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
@@ -24,6 +24,7 @@ import javax.inject.Inject
 import org.apache.james.core.MailAddress
 import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.SetError
+import org.apache.james.jmap.mail.EmailSet.{UnparsedMessageId, UnparsedMessageIdConstraint}
 import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
 import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope}
 import org.apache.james.mailbox.model.MessageId
@@ -52,6 +53,13 @@ class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory
     case _ => JsError("Expecting mailAddress to be represented by a JsString")
   }
 
+  private implicit val emailUpdatesMapReads: Reads[Map[UnparsedMessageId, JsObject]] =
+    readMapEntry[UnparsedMessageId, JsObject](s => refineV[UnparsedMessageIdConstraint](s),
+      {
+        case o: JsObject => JsSuccess(o)
+        case _ => JsError("Expecting a JsObject as an update entry")
+      })
+
   private implicit val emailSubmissionSetRequestReads: Reads[EmailSubmissionSetRequest] = Json.reads[EmailSubmissionSetRequest]
 
   private implicit val emailSubmissionIdWrites: Writes[EmailSubmissionId] = Json.valueWrites[EmailSubmissionId]
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
index d9a0ae2..416c4f8 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
@@ -29,6 +29,7 @@ import org.apache.james.jmap.core.Id.Id
 import org.apache.james.jmap.core.SetError.SetErrorDescription
 import org.apache.james.jmap.core.State.State
 import org.apache.james.jmap.core.{AccountId, Id, Properties, SetError}
+import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
 import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
 import org.apache.james.jmap.method.{EmailSubmissionCreationParseException, WithAccountId}
 import org.apache.james.mailbox.model.MessageId
@@ -43,7 +44,14 @@ object EmailSubmissionId {
 }
 
 case class EmailSubmissionSetRequest(accountId: AccountId,
-                                     create: Option[Map[EmailSubmissionCreationId, JsObject]]) extends WithAccountId
+                                     create: Option[Map[EmailSubmissionCreationId, JsObject]],
+                                     onSuccessUpdateEmail: Option[Map[UnparsedMessageId, JsObject]]) extends WithAccountId {
+  def implicitEmailSetRequest: EmailSetRequest = EmailSetRequest(
+    accountId = accountId,
+    create = None,
+    update = onSuccessUpdateEmail,
+    destroy = None)
+}
 
 case class EmailSubmissionSetResponse(accountId: AccountId,
                                       newState: State,
@@ -59,7 +67,7 @@ case class EmailSubmissionAddress(email: MailAddress)
 case class Envelope(mailFrom: EmailSubmissionAddress, rcptTo: List[EmailSubmissionAddress])
 
 object EmailSubmissionCreationRequest {
-  private val assignableProperties = Set("emailId", "envelope")
+  private val assignableProperties = Set("emailId", "envelope", "onSuccessUpdateEmail")
 
   def validateProperties(jsObject: JsObject): Either[EmailSubmissionCreationParseException, JsObject] =
     jsObject.keys.diff(assignableProperties) match {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index 495c5f5..ffe0679 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -50,7 +50,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
       created <- createPerformer.create(request, mailboxSession)
     } yield InvocationWithContext(
       invocation = Invocation(
-        methodName = invocation.invocation.methodName,
+        methodName = methodName,
         arguments = Arguments(serializer.serialize(EmailSetResponse(
           accountId = request.accountId,
           newState = State.INSTANCE,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index 3a01e1b..34ce804 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -44,6 +44,7 @@ import org.apache.james.queue.api.{MailQueue, MailQueueFactory}
 import org.apache.james.rrt.api.CanSendFrom
 import org.apache.james.server.core.{MailImpl, MimeMessageCopyOnWriteProxy, MimeMessageInputStreamSource}
 import org.apache.mailet.{Attribute, AttributeName, AttributeValue}
+import org.reactivestreams.Publisher
 import org.slf4j.{Logger, LoggerFactory}
 import play.api.libs.json._
 import reactor.core.scala.publisher.{SFlux, SMono}
@@ -69,6 +70,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
                                          messageIdManager: MessageIdManager,
                                          mailQueueFactory: MailQueueFactory[_ <: MailQueue],
                                          canSendFrom: CanSendFrom,
+                                         emailSetMethod: EmailSetMethod,
                                          val metricFactory: MetricFactory,
                                          val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[EmailSubmissionSetRequest] with Startable {
   override val methodName: MethodName = MethodName("EmailSubmission/set")
@@ -122,8 +124,8 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
     Try(queue.close())
       .recover(e => LOGGER.debug("error closing queue", e))
 
-  override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSubmissionSetRequest): SMono[InvocationWithContext] = {
-    for {
+  override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSubmissionSetRequest): Publisher[InvocationWithContext] = {
+    val result = for {
       createdResults <- create(request, mailboxSession, invocation.processingContext)
     } yield InvocationWithContext(
       invocation = Invocation(
@@ -136,6 +138,13 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
           .as[JsObject]),
         methodCallId = invocation.invocation.methodCallId),
       processingContext = createdResults._2)
+
+    SFlux.concat(result,
+      emailSetMethod.doProcess(
+        capabilities = capabilities,
+        invocation = invocation,
+        mailboxSession = mailboxSession,
+        request = request.implicitEmailSetRequest))
   }
 
   override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailSubmissionSetRequest] =


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


[james-project] 05/09: JAMES-3434 Allow the use of aliases for EmailSubmission/set create

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 06f9d30d38e5458b422f7d3ddd0fe77fbbc426d5
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Nov 5 11:33:52 2020 +0700

    JAMES-3434 Allow the use of aliases for EmailSubmission/set create
---
 .../EmailSubmissionSetMethodContract.scala         | 234 +++++++++++++++++++++
 .../jmap/method/EmailSubmissionSetMethod.scala     |   7 +-
 2 files changed, 239 insertions(+), 2 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index f27345d..e277d82 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -222,6 +222,240 @@ trait EmailSubmissionSetMethodContract {
   }
 
   @Test
+  def mimeSenderShouldAcceptAliases(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender("bob.alias@domain.tld")
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob.alias", DOMAIN.asString(), "bob@domain.tld")
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${BOB.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val requestReadMail =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${bobInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(requestReadMail)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def mimeFromShouldAcceptAliases(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom("bob.alias@domain.tld")
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob.alias", DOMAIN.asString(), "bob@domain.tld")
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${BOB.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val requestReadMail =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${bobInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(requestReadMail)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def envelopeFromShouldAcceptAliases(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
+
+    server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob.alias", DOMAIN.asString(), "bob@domain.tld")
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "bob.alias@domain.tld"},
+         |             "rcptTo": [{"email": "${BOB.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val requestReadMail =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${bobInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(requestReadMail)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
   def emailSubmissionSetCreateShouldSendMailSuccessfullyToBothRecipients(server: GuiceJamesServer): Unit = {
     val message: Message = Message.Builder
       .of
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index d3d6e25..3a01e1b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -25,6 +25,7 @@ import eu.timepit.refined.auto._
 import javax.annotation.PreDestroy
 import javax.inject.Inject
 import javax.mail.internet.{InternetAddress, MimeMessage}
+import org.apache.james.core.{MailAddress, Username}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.core.SetError.{SetErrorDescription, SetErrorType}
@@ -40,6 +41,7 @@ import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
 import org.apache.james.metrics.api.MetricFactory
 import org.apache.james.queue.api.MailQueueFactory.SPOOL
 import org.apache.james.queue.api.{MailQueue, MailQueueFactory}
+import org.apache.james.rrt.api.CanSendFrom
 import org.apache.james.server.core.{MailImpl, MimeMessageCopyOnWriteProxy, MimeMessageInputStreamSource}
 import org.apache.mailet.{Attribute, AttributeName, AttributeValue}
 import org.slf4j.{Logger, LoggerFactory}
@@ -66,6 +68,7 @@ case class ForbiddenMailFromException(from: List[String]) extends Exception
 class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerializer,
                                          messageIdManager: MessageIdManager,
                                          mailQueueFactory: MailQueueFactory[_ <: MailQueue],
+                                         canSendFrom: CanSendFrom,
                                          val metricFactory: MetricFactory,
                                          val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[EmailSubmissionSetRequest] with Startable {
   override val methodName: MethodName = MethodName("EmailSubmission/set")
@@ -223,13 +226,13 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
   private def validate(session: MailboxSession)(mimeMessage: MimeMessage, envelope: Envelope): Try[MimeMessage] = {
     val forbiddenMailFrom: List[String] = (Option(mimeMessage.getSender).toList ++ Option(mimeMessage.getFrom).toList.flatten)
       .map(_.asInstanceOf[InternetAddress].getAddress)
-      .filter(from => !from.equals(session.getUser.asString()))
+      .filter(addressAsString => !canSendFrom.userCanSendFrom(session.getUser, Username.fromMailAddress(new MailAddress(addressAsString))))
 
     if (forbiddenMailFrom.nonEmpty) {
       Failure(ForbiddenMailFromException(forbiddenMailFrom))
     } else if (envelope.rcptTo.isEmpty) {
       Failure(NoRecipientException())
-    } else if (!envelope.mailFrom.email.asString.equals(session.getUser.asString())) {
+    } else if (!canSendFrom.userCanSendFrom(session.getUser, Username.fromMailAddress(envelope.mailFrom.email))) {
       Failure(ForbiddenFromException(envelope.mailFrom.email.asString))
     } else {
       Success(mimeMessage)


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


[james-project] 06/09: JAMES-3434 Allow Methods to produce several responses

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 18462afb1684000037872bad9d6edfb760639d01
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 6 10:52:28 2020 +0700

    JAMES-3434 Allow Methods to produce several responses
---
 .../org/apache/james/jmap/method/Method.scala      | 22 +++++++++--------
 .../apache/james/jmap/routes/JMAPApiRoutes.scala   | 28 +++++++++++-----------
 2 files changed, 26 insertions(+), 24 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
index bcb7edb..7a4cdff 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/Method.scala
@@ -29,7 +29,9 @@ import org.apache.james.metrics.api.MetricFactory
 import org.reactivestreams.Publisher
 import reactor.core.scala.publisher.SMono
 
-case class InvocationWithContext(invocation: Invocation, processingContext: ProcessingContext)
+case class InvocationWithContext(invocation: Invocation, processingContext: ProcessingContext) {
+  def recordInvocation: InvocationWithContext = InvocationWithContext(invocation, processingContext.recordInvocation(invocation))
+}
 
 trait Method {
   val JMAP_RFC8621_PREFIX: String = "JMAP-RFC8621-"
@@ -49,13 +51,14 @@ trait MethodRequiringAccountId[REQUEST <: WithAccountId] extends Method {
   def sessionSupplier: SessionSupplier
 
   override def process(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): Publisher[InvocationWithContext] = {
-    val result = (for {
-      request <- getRequest(mailboxSession, invocation.invocation)
-      response <- validateAccountId(request.accountId, mailboxSession, sessionSupplier, invocation.invocation).flatMap{
-        case Right(_) => doProcess(capabilities, invocation, mailboxSession, request)
-        case Left(errorInvocation) => SMono.just(InvocationWithContext(errorInvocation, invocation.processingContext))
-      }
-    } yield response)
+    val result = getRequest(mailboxSession, invocation.invocation)
+      .flatMapMany(request => {
+        validateAccountId(request.accountId, mailboxSession, sessionSupplier, invocation.invocation)
+          .flatMapMany {
+            case Right(_) => doProcess(capabilities, invocation, mailboxSession, request)
+            case Left(errorInvocation) => SMono.just(InvocationWithContext(errorInvocation, invocation.processingContext))
+          }
+      })
       .onErrorResume {
         case e: UnsupportedRequestParameterException => SMono.just(InvocationWithContext(Invocation.error(
           ErrorCode.InvalidArguments,
@@ -78,7 +81,6 @@ trait MethodRequiringAccountId[REQUEST <: WithAccountId] extends Method {
         case e: Throwable => SMono.raiseError(e)
       }
 
-
     metricFactory.decoratePublisherWithTimerMetricLogP99(JMAP_RFC8621_PREFIX + methodName.value, result)
   }
 
@@ -89,7 +91,7 @@ trait MethodRequiringAccountId[REQUEST <: WithAccountId] extends Method {
       .switchIfEmpty(SMono.just(Left[Invocation, Session](Invocation.error(ErrorCode.AccountNotFound, invocation.methodCallId))))
   }
 
-  def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: REQUEST): SMono[InvocationWithContext]
+  def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: REQUEST): Publisher[InvocationWithContext]
 
   def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[REQUEST]
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
index 42efd8f..e7d2c8f 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/JMAPApiRoutes.scala
@@ -44,7 +44,7 @@ import org.apache.james.jmap.{Endpoint, JMAPRoute, JMAPRoutes}
 import org.apache.james.mailbox.MailboxSession
 import org.slf4j.{Logger, LoggerFactory}
 import play.api.libs.json.{JsError, JsSuccess}
-import reactor.core.publisher.Mono
+import reactor.core.publisher.{Flux, Mono}
 import reactor.core.scala.publisher.{SFlux, SMono}
 import reactor.core.scheduler.Schedulers
 import reactor.netty.http.server.{HttpServerRequest, HttpServerResponse}
@@ -134,11 +134,11 @@ class JMAPApiRoutes (val authenticator: Authenticator,
 
   private def processSequentiallyAndUpdateContext(requestObject: RequestObject, mailboxSession: MailboxSession, processingContext: ProcessingContext, capabilities: Set[CapabilityIdentifier]): SMono[Seq[(InvocationWithContext)]] = {
     SFlux.fromIterable(requestObject.methodCalls)
-      .foldLeft(List[SMono[InvocationWithContext]]())((acc, elem) => {
+      .foldLeft(List[SFlux[InvocationWithContext]]())((acc, elem) => {
         val lastProcessingContext: SMono[ProcessingContext] = acc.headOption
-          .map(last => last.map(_.processingContext))
+          .map(last => SMono.fromPublisher(Flux.from(last.map(_.processingContext)).last()))
           .getOrElse(SMono.just(processingContext))
-        val invocation: SMono[InvocationWithContext] = lastProcessingContext.flatMap(context => process(capabilities, mailboxSession, InvocationWithContext(elem, context)))
+        val invocation: SFlux[InvocationWithContext] = lastProcessingContext.flatMapMany(context => process(capabilities, mailboxSession, InvocationWithContext(elem, context)))
         invocation.cache() :: acc
       })
       .map(_.reverse)
@@ -147,26 +147,26 @@ class JMAPApiRoutes (val authenticator: Authenticator,
         .collectSeq())
   }
 
-  private def process(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession, invocation: InvocationWithContext) : SMono[InvocationWithContext] = {
-    SMono.defer(() => {
+  private def process(capabilities: Set[CapabilityIdentifier], mailboxSession: MailboxSession, invocation: InvocationWithContext) : SFlux[InvocationWithContext] = {
+    SFlux.fromPublisher(Flux.defer(() => {
       invocation.processingContext.resolveBackReferences(invocation.invocation) match {
-        case Left(e) => SMono.just(InvocationWithContext(Invocation.error(
+        case Left(e) => SFlux.just[InvocationWithContext](InvocationWithContext(Invocation.error(
           errorCode = ErrorCode.InvalidResultReference,
           description = s"Failed resolving back-reference: ${e.message}",
           methodCallId = invocation.invocation.methodCallId), invocation.processingContext))
         case Right(resolvedInvocation) => processMethodWithMatchName(capabilities, InvocationWithContext(resolvedInvocation, invocation.processingContext), mailboxSession)
-          .map((invocationWithContext: InvocationWithContext) => InvocationWithContext(invocationWithContext.invocation, invocationWithContext.processingContext.recordInvocation(invocationWithContext.invocation)))
+          .map(_.recordInvocation)
       }
-    })
+    }))
   }
 
-  private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): SMono[InvocationWithContext] =
+  private def processMethodWithMatchName(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession): SFlux[InvocationWithContext] =
     SMono.justOrEmpty(methodsByName.get(invocation.invocation.methodName))
-      .flatMap(method => validateCapabilities(capabilities, method.requiredCapabilities)
-      .fold(e => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, e.description, invocation.invocation.methodCallId), invocation.processingContext)),
-        _ => SMono.fromPublisher(method.process(capabilities, invocation, mailboxSession))))
+      .flatMapMany(method => validateCapabilities(capabilities, method.requiredCapabilities)
+        .fold(e => SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, e.description, invocation.invocation.methodCallId), invocation.processingContext)),
+          _ => SFlux.fromPublisher(method.process(capabilities, invocation, mailboxSession))))
       .onErrorResume(throwable => SMono.just(InvocationWithContext(Invocation.error(ErrorCode.ServerFail, throwable.getMessage, invocation.invocation.methodCallId), invocation.processingContext)))
-      .switchIfEmpty(SMono.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, invocation.invocation.methodCallId), invocation.processingContext)))
+      .switchIfEmpty(SFlux.just(InvocationWithContext(Invocation.error(ErrorCode.UnknownMethod, invocation.invocation.methodCallId), invocation.processingContext)))
 
   private def validateCapabilities(capabilities: Set[CapabilityIdentifier], requiredCapabilities: Set[CapabilityIdentifier]): Either[MissingCapabilityException, Unit] = {
     val missingCapabilities = requiredCapabilities -- capabilities


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


[james-project] 04/09: JAMES-3434 Various EmailSubmission refactorings

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 9cdfea9327a2941f9ccb5f81d6a6772b0b266643
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Nov 5 10:47:25 2020 +0700

    JAMES-3434 Various EmailSubmission refactorings
---
 .../org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala | 7 +++----
 .../scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala     | 4 +---
 .../org/apache/james/jmap/method/EmailSubmissionSetMethod.scala   | 8 ++++----
 3 files changed, 8 insertions(+), 11 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
index f927639..6dd492e 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
@@ -19,13 +19,13 @@
 
 package org.apache.james.jmap.json
 
-import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.refineV
 import javax.inject.Inject
 import org.apache.james.core.MailAddress
+import org.apache.james.jmap.core.Id.IdConstraint
 import org.apache.james.jmap.core.SetError
 import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
-import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope, Parameters}
+import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope}
 import org.apache.james.mailbox.model.MessageId
 import play.api.libs.json.{JsError, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, Reads, Writes}
 
@@ -33,7 +33,7 @@ import scala.util.Try
 
 class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory) {
   private implicit val mapCreationRequestByEmailSubmissionCreationId: Reads[Map[EmailSubmissionCreationId, JsObject]] =
-    readMapEntry[EmailSubmissionCreationId, JsObject](s => refineV[NonEmpty](s),
+    readMapEntry[EmailSubmissionCreationId, JsObject](s => refineV[IdConstraint](s),
       {
         case o: JsObject => JsSuccess(o)
         case _ => JsError("Expecting a JsObject as a creation entry")
@@ -56,7 +56,6 @@ class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory
 
   private implicit val emailSubmissionIdWrites: Writes[EmailSubmissionId] = Json.valueWrites[EmailSubmissionId]
 
-  private implicit val parametersReads: Reads[Parameters] = Json.valueReads[Parameters]
   private implicit val emailSubmissionAddresReads: Reads[EmailSubmissionAddress] = Json.reads[EmailSubmissionAddress]
   private implicit val envelopeReads: Reads[Envelope] = Json.reads[Envelope]
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
index ea9b51c..d9a0ae2 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
@@ -21,7 +21,6 @@ package org.apache.james.jmap.mail
 
 import java.util.UUID
 
-import eu.timepit.refined.api.Refined
 import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.refineV
 import eu.timepit.refined.types.string.NonEmptyString
@@ -36,7 +35,7 @@ import org.apache.james.mailbox.model.MessageId
 import play.api.libs.json.JsObject
 
 object EmailSubmissionSet {
-  type EmailSubmissionCreationId = String Refined NonEmpty
+  type EmailSubmissionCreationId = Id
 }
 
 object EmailSubmissionId {
@@ -55,7 +54,6 @@ case class EmailSubmissionId(value: Id)
 
 case class EmailSubmissionCreationResponse(id: EmailSubmissionId)
 
-case class Parameters(value: String)
 case class EmailSubmissionAddress(email: MailAddress)
 
 case class Envelope(mailFrom: EmailSubmissionAddress, rcptTo: List[EmailSubmissionAddress])
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index 315c330..d3d6e25 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -96,19 +96,19 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
   }
   case class CreationResults(created: Seq[CreationResult]) {
     def retrieveCreated: Map[EmailSubmissionCreationId, EmailSubmissionCreationResponse] = created
-      .flatMap(result => result match {
+      .flatMap {
         case success: CreationSuccess => Some(success.emailSubmissionCreationId, success.emailSubmissionCreationResponse)
         case _ => None
-      })
+      }
       .toMap
       .map(creation => (creation._1, creation._2))
 
 
     def retrieveErrors: Map[EmailSubmissionCreationId, SetError] = created
-      .flatMap(result => result match {
+      .flatMap {
         case failure: CreationFailure => Some(failure.emailSubmissionCreationId, failure.asSetError)
         case _ => None
-      })
+      }
       .toMap
   }
 


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


[james-project] 01/09: JAMES-3434 EmailSubmission/set create

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit 452b44ffe87d5a2506ea17e68ab40cd4bca693d7
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Fri Oct 23 16:58:38 2020 +0700

    JAMES-3434 EmailSubmission/set create
---
 .../modules/activemq/ActiveMQQueueModule.java      |   8 +
 .../modules/server/MemoryMailQueueModule.java      |   7 +
 .../james/jmap/rfc8621/RFC8621MethodsModule.java   |  15 +-
 .../james/modules/rabbitmq/RabbitMQModule.java     |   7 +
 .../DistributedEmailSubmissionSetMethodtest.java   |  65 +++++++
 .../EmailSubmissionSetMethodContract.scala         | 200 +++++++++++++++++++
 .../MemoryEmailSubmissionSetMethodTest.java}       |  44 ++---
 server/protocols/jmap-rfc-8621/pom.xml             |   8 +
 .../jmap/json/EmailSubmissionSetSerializer.scala   |  73 +++++++
 .../james/jmap/mail/EmailSubmissionSet.scala       |  83 ++++++++
 .../jmap/method/EmailSetDeletePerformer.scala      |   4 +-
 .../apache/james/jmap/method/EmailSetMethod.scala  |   2 +-
 .../jmap/method/EmailSetUpdatePerformer.scala      |   4 +-
 .../jmap/method/EmailSubmissionSetMethod.scala     | 215 +++++++++++++++++++++
 14 files changed, 706 insertions(+), 29 deletions(-)

diff --git a/server/container/guice/activemq/src/main/java/org/apache/james/modules/activemq/ActiveMQQueueModule.java b/server/container/guice/activemq/src/main/java/org/apache/james/modules/activemq/ActiveMQQueueModule.java
index 03dcc70..f6af3e0 100644
--- a/server/container/guice/activemq/src/main/java/org/apache/james/modules/activemq/ActiveMQQueueModule.java
+++ b/server/container/guice/activemq/src/main/java/org/apache/james/modules/activemq/ActiveMQQueueModule.java
@@ -25,6 +25,7 @@ import org.apache.activemq.store.PersistenceAdapter;
 import org.apache.activemq.store.kahadb.KahaDBPersistenceAdapter;
 import org.apache.james.queue.activemq.ActiveMQMailQueueFactory;
 import org.apache.james.queue.activemq.EmbeddedActiveMQ;
+import org.apache.james.queue.api.MailQueue;
 import org.apache.james.queue.api.MailQueueFactory;
 import org.apache.james.queue.api.ManageableMailQueue;
 
@@ -40,6 +41,7 @@ public class ActiveMQQueueModule extends AbstractModule {
         bind(PersistenceAdapter.class).to(KahaDBPersistenceAdapter.class);
         bind(KahaDBPersistenceAdapter.class).in(Scopes.SINGLETON);
         bind(EmbeddedActiveMQ.class).in(Scopes.SINGLETON);
+        bind(ActiveMQMailQueueFactory.class).in(Scopes.SINGLETON);
     }
     
     @Provides
@@ -61,4 +63,10 @@ public class ActiveMQQueueModule extends AbstractModule {
     public MailQueueFactory<?> provideActiveMQMailQueueFactory(MailQueueFactory<? extends ManageableMailQueue> mailQueueFactory) {
         return mailQueueFactory;
     }
+
+    @Provides
+    @Singleton
+    public MailQueueFactory<? extends MailQueue> provideMailQueueFactoryGenerics(ActiveMQMailQueueFactory queueFactory) {
+        return queueFactory;
+    }
 }
diff --git a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/server/MemoryMailQueueModule.java b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/server/MemoryMailQueueModule.java
index 8a05148..2421318 100644
--- a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/server/MemoryMailQueueModule.java
+++ b/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/server/MemoryMailQueueModule.java
@@ -19,6 +19,7 @@
 
 package org.apache.james.modules.server;
 
+import org.apache.james.queue.api.MailQueue;
 import org.apache.james.queue.api.MailQueueFactory;
 import org.apache.james.queue.api.ManageableMailQueue;
 import org.apache.james.queue.memory.MemoryMailQueueFactory;
@@ -46,4 +47,10 @@ public class MemoryMailQueueModule extends AbstractModule {
     public MailQueueFactory<?> provideMailQueueFactory(MemoryMailQueueFactory memoryMailQueueFactory) {
         return memoryMailQueueFactory;
     }
+
+    @Provides
+    @Singleton
+    public MailQueueFactory<? extends MailQueue> provideMailQueueFactoryGenerics(MemoryMailQueueFactory memoryMailQueueFactory) {
+        return memoryMailQueueFactory;
+    }
 }
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index 08b9328..253ff36 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -19,7 +19,6 @@
 
 package org.apache.james.jmap.rfc8621;
 
-
 import static org.apache.james.jmap.core.JmapRfc8621Configuration.LOCALHOST_CONFIGURATION;
 
 import java.io.FileNotFoundException;
@@ -37,6 +36,7 @@ import org.apache.james.jmap.method.CoreEchoMethod;
 import org.apache.james.jmap.method.EmailGetMethod;
 import org.apache.james.jmap.method.EmailQueryMethod;
 import org.apache.james.jmap.method.EmailSetMethod;
+import org.apache.james.jmap.method.EmailSubmissionSetMethod;
 import org.apache.james.jmap.method.MailboxGetMethod;
 import org.apache.james.jmap.method.MailboxQueryMethod;
 import org.apache.james.jmap.method.MailboxSetMethod;
@@ -50,12 +50,15 @@ import org.apache.james.jmap.routes.JMAPApiRoutes;
 import org.apache.james.jmap.routes.SessionRoutes;
 import org.apache.james.jmap.routes.UploadRoutes;
 import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.utils.InitializationOperation;
+import org.apache.james.utils.InitilizationOperationBuilder;
 import org.apache.james.utils.PropertiesProvider;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.inject.AbstractModule;
 import com.google.inject.Provides;
+import com.google.inject.Scopes;
 import com.google.inject.Singleton;
 import com.google.inject.multibindings.Multibinder;
 import com.google.inject.multibindings.ProvidesIntoSet;
@@ -68,6 +71,8 @@ public class RFC8621MethodsModule extends AbstractModule {
     protected void configure() {
         bind(ZoneIdProvider.class).to(SystemZoneIdProvider.class);
 
+        bind(EmailSubmissionSetMethod.class).in(Scopes.SINGLETON);
+
         Multibinder<Method> methods = Multibinder.newSetBinder(binder(), Method.class);
         methods.addBinding().to(CoreEchoMethod.class);
         methods.addBinding().to(MailboxGetMethod.class);
@@ -75,6 +80,7 @@ public class RFC8621MethodsModule extends AbstractModule {
         methods.addBinding().to(MailboxSetMethod.class);
         methods.addBinding().to(EmailGetMethod.class);
         methods.addBinding().to(EmailSetMethod.class);
+        methods.addBinding().to(EmailSubmissionSetMethod.class);
         methods.addBinding().to(EmailQueryMethod.class);
         methods.addBinding().to(VacationResponseGetMethod.class);
         methods.addBinding().to(VacationResponseSetMethod.class);
@@ -108,4 +114,11 @@ public class RFC8621MethodsModule extends AbstractModule {
             return LOCALHOST_CONFIGURATION();
         }
     }
+
+    @ProvidesIntoSet
+    InitializationOperation initSubmissions(EmailSubmissionSetMethod instance) {
+        return InitilizationOperationBuilder
+                .forClass(EmailSubmissionSetMethod.class)
+                .init(instance::init);
+    }
 }
diff --git a/server/container/guice/rabbitmq/src/main/java/org/apache/james/modules/rabbitmq/RabbitMQModule.java b/server/container/guice/rabbitmq/src/main/java/org/apache/james/modules/rabbitmq/RabbitMQModule.java
index b21be1d..36cfa20 100644
--- a/server/container/guice/rabbitmq/src/main/java/org/apache/james/modules/rabbitmq/RabbitMQModule.java
+++ b/server/container/guice/rabbitmq/src/main/java/org/apache/james/modules/rabbitmq/RabbitMQModule.java
@@ -35,6 +35,7 @@ import org.apache.james.eventsourcing.Event;
 import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTO;
 import org.apache.james.eventsourcing.eventstore.cassandra.dto.EventDTOModule;
 import org.apache.james.lifecycle.api.StartUpCheck;
+import org.apache.james.queue.api.MailQueue;
 import org.apache.james.queue.api.MailQueueFactory;
 import org.apache.james.queue.api.ManageableMailQueue;
 import org.apache.james.queue.rabbitmq.RabbitMQMailQueue;
@@ -120,6 +121,12 @@ public class RabbitMQModule extends AbstractModule {
     }
 
     @Provides
+    @Singleton
+    public MailQueueFactory<? extends MailQueue> provideMailQueueFactoryGenerics(MailQueueFactory<RabbitMQMailQueue> queueFactory) {
+        return queueFactory;
+    }
+
+    @Provides
     @Named(RABBITMQ_CONFIGURATION_NAME)
     @Singleton
     private org.apache.commons.configuration2.Configuration getConfiguration(PropertiesProvider propertiesProvider) throws ConfigurationException {
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSubmissionSetMethodtest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSubmissionSetMethodtest.java
new file mode 100644
index 0000000..c7164f9
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedEmailSubmissionSetMethodtest.java
@@ -0,0 +1,65 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.distributed;
+
+import org.apache.james.CassandraExtension;
+import org.apache.james.CassandraRabbitMQJamesConfiguration;
+import org.apache.james.CassandraRabbitMQJamesServerMain;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.EmailSubmissionSetMethodContract;
+import org.apache.james.mailbox.cassandra.ids.CassandraMessageId;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.modules.AwsS3BlobStoreExtension;
+import org.apache.james.modules.RabbitMQExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import com.datastax.driver.core.utils.UUIDs;
+
+class DistributedEmailSubmissionSetMethodtest implements EmailSubmissionSetMethodContract {
+    public static final DockerElasticSearchExtension ELASTIC_SEARCH_EXTENSION = new DockerElasticSearchExtension();
+    public static final CassandraMessageId.Factory MESSAGE_ID_FACTORY = new CassandraMessageId.Factory();
+
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+        CassandraRabbitMQJamesConfiguration.builder()
+            .workingDirectory(tmpDir)
+            .configurationFromClasspath()
+            .blobStore(BlobStoreConfiguration.builder()
+                .s3()
+                .disableCache()
+                .deduplication())
+            .build())
+        .extension(ELASTIC_SEARCH_EXTENSION)
+        .extension(new CassandraExtension())
+        .extension(new RabbitMQExtension())
+        .extension(new AwsS3BlobStoreExtension())
+        .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
+
+    @Override
+    public MessageId randomMessageId() {
+        return MESSAGE_ID_FACTORY.of(UUIDs.timeBased());
+    }
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
new file mode 100644
index 0000000..6974db6
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -0,0 +1,200 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.contract
+
+import java.nio.charset.StandardCharsets
+import java.util.concurrent.TimeUnit
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, `with`, requestSpecification}
+import io.restassured.builder.ResponseSpecBuilder
+import io.restassured.http.ContentType.JSON
+import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.mailbox.DefaultMailboxes
+import org.apache.james.mailbox.MessageManager.AppendCommand
+import org.apache.james.mailbox.model.{MailboxId, MailboxPath, MessageId}
+import org.apache.james.mime4j.dom.Message
+import org.apache.james.modules.MailboxProbeImpl
+import org.apache.james.utils.DataProbeImpl
+import org.awaitility.Awaitility
+import org.awaitility.Duration.ONE_HUNDRED_MILLISECONDS
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait EmailSubmissionSetMethodContract {
+  private lazy val slowPacedPollInterval = ONE_HUNDRED_MILLISECONDS
+  private lazy val calmlyAwait = Awaitility.`with`
+    .pollInterval(slowPacedPollInterval)
+    .and.`with`.pollDelay(slowPacedPollInterval)
+    .await
+  private lazy val awaitAtMostTenSeconds = calmlyAwait.atMost(10, TimeUnit.SECONDS)
+
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addUser(BOB.asString, BOB_PASSWORD)
+      .addUser(ANDRE.asString, ANDRE_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  def randomMessageId: MessageId
+
+  @Test
+  def emailSubmissionSetCreateShouldSendMailSuccessfully(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 requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val requestAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "1e8584548eca20f26faf6becc1704a0f352839f12c208a47fbd486d60f491f7c",
+         |      "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 emailSubmissionSetCreateShouldReturnSuccess(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      // Ids are randomly generated, and not stored, let's ignore it
+      .whenIgnoringPaths("methodResponses[0][1].created.k1490.id")
+      .inPath("methodResponses[0][1].created")
+      .isEqualTo("""{"k1490": {}}""")
+  }
+}
diff --git a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/server/MemoryMailQueueModule.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSubmissionSetMethodTest.java
similarity index 50%
copy from server/container/guice/memory-guice/src/main/java/org/apache/james/modules/server/MemoryMailQueueModule.java
copy to server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSubmissionSetMethodTest.java
index 8a05148..127c691 100644
--- a/server/container/guice/memory-guice/src/main/java/org/apache/james/modules/server/MemoryMailQueueModule.java
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryEmailSubmissionSetMethodTest.java
@@ -17,33 +17,31 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.modules.server;
+package org.apache.james.jmap.rfc8621.memory;
 
-import org.apache.james.queue.api.MailQueueFactory;
-import org.apache.james.queue.api.ManageableMailQueue;
-import org.apache.james.queue.memory.MemoryMailQueueFactory;
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
 
-import com.google.inject.AbstractModule;
-import com.google.inject.Provides;
-import com.google.inject.Scopes;
-import com.google.inject.Singleton;
+import java.util.concurrent.ThreadLocalRandom;
 
-public class MemoryMailQueueModule extends AbstractModule {
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.EmailSubmissionSetMethodContract;
+import org.apache.james.mailbox.inmemory.InMemoryMessageId;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
 
-    @Override
-    protected void configure() {
-        bind(MemoryMailQueueFactory.class).in(Scopes.SINGLETON);
-    }
+class MemoryEmailSubmissionSetMethodTest implements EmailSubmissionSetMethodContract {
+    @RegisterExtension
+    static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+        .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+            .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+            .overrideWith(new TestJMAPServerModule()))
+        .build();
 
-    @Provides
-    @Singleton
-    public MailQueueFactory<? extends ManageableMailQueue> provideManageableMailQueueFactory(MemoryMailQueueFactory memoryMailQueueFactory) {
-        return memoryMailQueueFactory;
-    }
-
-    @Provides
-    @Singleton
-    public MailQueueFactory<?> provideMailQueueFactory(MemoryMailQueueFactory memoryMailQueueFactory) {
-        return memoryMailQueueFactory;
+    @Override
+    public MessageId randomMessageId() {
+        return InMemoryMessageId.of(ThreadLocalRandom.current().nextInt(100000) + 100);
     }
 }
diff --git a/server/protocols/jmap-rfc-8621/pom.xml b/server/protocols/jmap-rfc-8621/pom.xml
index 048b51e..c7b3a4e 100644
--- a/server/protocols/jmap-rfc-8621/pom.xml
+++ b/server/protocols/jmap-rfc-8621/pom.xml
@@ -60,6 +60,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-data-api</artifactId>
         </dependency>
         <dependency>
@@ -81,6 +85,10 @@
         </dependency>
         <dependency>
             <groupId>${james.groupId}</groupId>
+            <artifactId>james-server-queue-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>${james.groupId}</groupId>
             <artifactId>james-server-util</artifactId>
             <scope>test</scope>
             <type>test-jar</type>
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
new file mode 100644
index 0000000..6bdb66e
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
@@ -0,0 +1,73 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.json
+
+import eu.timepit.refined.collection.NonEmpty
+import eu.timepit.refined.refineV
+import javax.inject.Inject
+import org.apache.james.core.MailAddress
+import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
+import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope, Parameters}
+import org.apache.james.mailbox.model.MessageId
+import play.api.libs.json.{JsError, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, Reads, Writes}
+
+import scala.util.Try
+
+class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory) {
+  private implicit val mapCreationRequestByEmailSubmissionCreationId: Reads[Map[EmailSubmissionCreationId, JsObject]] =
+    readMapEntry[EmailSubmissionCreationId, JsObject](s => refineV[NonEmpty](s),
+      {
+        case o: JsObject => JsSuccess(o)
+        case _ => JsError("Expecting a JsObject as a creation entry")
+      })
+
+  private implicit val messageIdReads: Reads[MessageId] = {
+    case JsString(serializedMessageId) => Try(JsSuccess(messageIdFactory.fromString(serializedMessageId)))
+      .fold(_ => JsError("Invalid messageId"), messageId => messageId)
+    case _ => JsError("Expecting messageId to be represented by a JsString")
+  }
+
+  private implicit val mailAddressReads: Reads[MailAddress] = {
+    case JsString(value) => Try(JsSuccess(new MailAddress(value)))
+      .fold(e => JsError(s"Invalid mailAddress: ${e.getMessage}"), mailAddress => mailAddress)
+    case _ => JsError("Expecting mailAddress to be represented by a JsString")
+  }
+
+  private implicit val emailSubmissionSetRequestReads: Reads[EmailSubmissionSetRequest] = Json.reads[EmailSubmissionSetRequest]
+
+  private implicit val emailSubmissionIdWrites: Writes[EmailSubmissionId] = Json.valueWrites[EmailSubmissionId]
+
+  private implicit val parametersReads: Reads[Parameters] = Json.valueReads[Parameters]
+  private implicit val emailSubmissionAddresReads: Reads[EmailSubmissionAddress] = Json.reads[EmailSubmissionAddress]
+  private implicit val envelopeReads: Reads[Envelope] = Json.reads[Envelope]
+
+  implicit val emailSubmissionCreationRequestReads: Reads[EmailSubmissionCreationRequest] = Json.reads[EmailSubmissionCreationRequest]
+
+  private implicit val emailSubmissionCreationResponseWrites: Writes[EmailSubmissionCreationResponse] = Json.writes[EmailSubmissionCreationResponse]
+
+  private implicit def emailSubmissionSetResponseWrites(implicit emailSubmissionCreationResponseWrites: Writes[EmailSubmissionCreationResponse]): Writes[EmailSubmissionSetResponse] = Json.writes[EmailSubmissionSetResponse]
+
+  private implicit def emailSubmissionMapCreationResponseWrites(implicit emailSubmissionSetCreationResponseWrites: Writes[EmailSubmissionCreationResponse]): Writes[Map[EmailSubmissionCreationId, EmailSubmissionCreationResponse]] =
+    mapWrites[EmailSubmissionCreationId, EmailSubmissionCreationResponse](_.value, emailSubmissionSetCreationResponseWrites)
+
+  def deserializeEmailSubmissionSetRequest(input: JsValue): JsResult[EmailSubmissionSetRequest] = Json.fromJson[EmailSubmissionSetRequest](input)
+
+  def serializeEmailSubmissionSetResponse(response: EmailSubmissionSetResponse): JsValue = Json.toJson(response)
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
new file mode 100644
index 0000000..ea9b51c
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSubmissionSet.scala
@@ -0,0 +1,83 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.mail
+
+import java.util.UUID
+
+import eu.timepit.refined.api.Refined
+import eu.timepit.refined.collection.NonEmpty
+import eu.timepit.refined.refineV
+import eu.timepit.refined.types.string.NonEmptyString
+import org.apache.james.core.MailAddress
+import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.State.State
+import org.apache.james.jmap.core.{AccountId, Id, Properties, SetError}
+import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
+import org.apache.james.jmap.method.{EmailSubmissionCreationParseException, WithAccountId}
+import org.apache.james.mailbox.model.MessageId
+import play.api.libs.json.JsObject
+
+object EmailSubmissionSet {
+  type EmailSubmissionCreationId = String Refined NonEmpty
+}
+
+object EmailSubmissionId {
+  def generate: EmailSubmissionId = EmailSubmissionId(Id.validate(UUID.randomUUID().toString).toOption.get)
+}
+
+case class EmailSubmissionSetRequest(accountId: AccountId,
+                                     create: Option[Map[EmailSubmissionCreationId, JsObject]]) extends WithAccountId
+
+case class EmailSubmissionSetResponse(accountId: AccountId,
+                                      newState: State,
+                                      created: Option[Map[EmailSubmissionCreationId, EmailSubmissionCreationResponse]],
+                                      notCreated: Option[Map[EmailSubmissionCreationId, SetError]])
+
+case class EmailSubmissionId(value: Id)
+
+case class EmailSubmissionCreationResponse(id: EmailSubmissionId)
+
+case class Parameters(value: String)
+case class EmailSubmissionAddress(email: MailAddress)
+
+case class Envelope(mailFrom: EmailSubmissionAddress, rcptTo: List[EmailSubmissionAddress])
+
+object EmailSubmissionCreationRequest {
+  private val assignableProperties = Set("emailId", "envelope")
+
+  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)
+    }
+
+  private def toProperties(strings: Set[String]): Properties = Properties(strings
+    .flatMap(string => {
+      val refinedValue: Either[String, NonEmptyString] = refineV[NonEmpty](string)
+      refinedValue.fold(_ => None,  Some(_))
+    }))
+}
+
+case class EmailSubmissionCreationRequest(emailId: MessageId,
+                                          envelope: Envelope)
\ No newline at end of file
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetDeletePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetDeletePerformer.scala
index da56ab5..037e7dc 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetDeletePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetDeletePerformer.scala
@@ -54,7 +54,7 @@ object EmailSetDeletePerformer {
       val success: Seq[DestroySuccess] = deleteResult.getDestroyed.asScala.toSeq
         .map(DestroySuccess)
       val notFound: Seq[DestroyResult] = deleteResult.getNotFound.asScala.toSeq
-        .map(id => DestroyFailure(EmailSet.asUnparsed(id), MessageNotFoundExeception(id)))
+        .map(id => DestroyFailure(EmailSet.asUnparsed(id), MessageNotFoundException(id)))
 
       success ++ notFound
     }
@@ -64,7 +64,7 @@ object EmailSetDeletePerformer {
   case class DestroyFailure(unparsedMessageId: UnparsedMessageId, e: Throwable) extends DestroyResult {
     def asMessageSetError: SetError = e match {
       case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(s"$unparsedMessageId is not a messageId: ${e.getMessage}"))
-      case e: MessageNotFoundExeception => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
+      case e: MessageNotFoundException => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
       case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
     }
   }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index 1bcc308..495c5f5 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -32,7 +32,7 @@ import org.apache.james.metrics.api.MetricFactory
 import play.api.libs.json.{JsError, JsSuccess}
 import reactor.core.scala.publisher.SMono
 
-case class MessageNotFoundExeception(messageId: MessageId) extends Exception
+case class MessageNotFoundException(messageId: MessageId) extends Exception
 
 class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
                                val metricFactory: MetricFactory,
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
index 662435d..094f823 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetUpdatePerformer.scala
@@ -48,7 +48,7 @@ object EmailSetUpdatePerformer {
     def asMessageSetError: SetError = e match {
       case e: IllegalArgumentException => SetError.invalidPatch(SetErrorDescription(s"Message $unparsedMessageId update is invalid: ${e.getMessage}"))
       case _: MailboxNotFoundException => SetError.notFound(SetErrorDescription(s"Mailbox not found"))
-      case e: MessageNotFoundExeception => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
+      case e: MessageNotFoundException => SetError.notFound(SetErrorDescription(s"Cannot find message with messageId: ${e.messageId.serialize()}"))
       case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
     }
   }
@@ -207,7 +207,7 @@ class EmailSetUpdatePerformer @Inject() (serializer: EmailSetSerializer,
       })
 
     if (mailboxIds.value.isEmpty) {
-      SMono.just[EmailUpdateResult](EmailUpdateFailure(EmailSet.asUnparsed(messageId), MessageNotFoundExeception(messageId)))
+      SMono.just[EmailUpdateResult](EmailUpdateFailure(EmailSet.asUnparsed(messageId), MessageNotFoundException(messageId)))
     } else {
       updateFlags(messageId, update, mailboxIds, originFlags, session)
         .flatMap {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
new file mode 100644
index 0000000..cfc96b5
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -0,0 +1,215 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one   *
+ * or more contributor license agreements.  See the NOTICE file *
+ * distributed with this work for additional information        *
+ * regarding copyright ownership.  The ASF licenses this file   *
+ * to you under the Apache License, Version 2.0 (the            *
+ * "License"); you may not use this file except in compliance   *
+ * with the License.  You may obtain a copy of the License at   *
+ *                                                              *
+ *   http://www.apache.org/licenses/LICENSE-2.0                 *
+ *                                                              *
+ * Unless required by applicable law or agreed to in writing,   *
+ * software distributed under the License is distributed on an  *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY       *
+ * KIND, either express or implied.  See the License for the    *
+ * specific language governing permissions and limitations      *
+ * under the License.                                           *
+ ****************************************************************/
+
+package org.apache.james.jmap.method
+
+import java.io.InputStream
+
+import eu.timepit.refined.auto._
+import javax.annotation.PreDestroy
+import javax.inject.Inject
+import javax.mail.internet.MimeMessage
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
+import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
+import org.apache.james.jmap.core.{ClientId, Id, Invocation, ServerId, SetError, State}
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.json.{EmailSubmissionSetSerializer, ResponseSerializer}
+import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
+import org.apache.james.jmap.mail.{EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse}
+import org.apache.james.jmap.method.EmailSubmissionSetMethod.{LOGGER, MAIL_METADATA_USERNAME_ATTRIBUTE}
+import org.apache.james.jmap.routes.{ProcessingContext, SessionSupplier}
+import org.apache.james.lifecycle.api.{LifecycleUtil, Startable}
+import org.apache.james.mailbox.model.{FetchGroup, MessageResult}
+import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
+import org.apache.james.metrics.api.MetricFactory
+import org.apache.james.queue.api.MailQueueFactory.SPOOL
+import org.apache.james.queue.api.{MailQueue, MailQueueFactory}
+import org.apache.james.server.core.{MailImpl, MimeMessageCopyOnWriteProxy, MimeMessageInputStreamSource}
+import org.apache.mailet.{Attribute, AttributeName, AttributeValue}
+import org.slf4j.{Logger, LoggerFactory}
+import play.api.libs.json._
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+import scala.jdk.CollectionConverters._
+import scala.util.Try
+
+object EmailSubmissionSetMethod {
+  val MAIL_METADATA_USERNAME_ATTRIBUTE: AttributeName = AttributeName.of("org.apache.james.jmap.send.MailMetaData.username")
+  val LOGGER: Logger = LoggerFactory.getLogger(classOf[EmailSubmissionSetMethod])
+}
+
+case class EmailSubmissionCreationParseException(setError: SetError) extends Exception
+
+class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerializer,
+                                         messageIdManager: MessageIdManager,
+                                         mailQueueFactory: MailQueueFactory[_ <: MailQueue],
+                                         val metricFactory: MetricFactory,
+                                         val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[EmailSubmissionSetRequest] with Startable {
+  override val methodName: MethodName = MethodName("EmailSubmission/set")
+  override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
+  var queue: MailQueue = null
+
+  sealed trait CreationResult {
+    def emailSubmissionCreationId: EmailSubmissionCreationId
+  }
+  case class CreationSuccess(emailSubmissionCreationId: EmailSubmissionCreationId, emailSubmissionCreationResponse: EmailSubmissionCreationResponse) extends CreationResult
+  case class CreationFailure(emailSubmissionCreationId: EmailSubmissionCreationId, exception: Throwable) extends CreationResult {
+    def asSetError: SetError = exception match {
+      case e: EmailSubmissionCreationParseException => e.setError
+      case e: Exception =>
+        e.printStackTrace()
+        SetError.serverFail(SetErrorDescription(exception.getMessage))
+    }
+  }
+  case class CreationResults(created: Seq[CreationResult]) {
+    def retrieveCreated: Map[EmailSubmissionCreationId, EmailSubmissionCreationResponse] = created
+      .flatMap(result => result match {
+        case success: CreationSuccess => Some(success.emailSubmissionCreationId, success.emailSubmissionCreationResponse)
+        case _ => None
+      })
+      .toMap
+      .map(creation => (creation._1, creation._2))
+
+
+    def retrieveErrors: Map[EmailSubmissionCreationId, SetError] = created
+      .flatMap(result => result match {
+        case failure: CreationFailure => Some(failure.emailSubmissionCreationId, failure.asSetError)
+        case _ => None
+      })
+      .toMap
+  }
+
+  def init: Unit =
+    queue = mailQueueFactory.createQueue(SPOOL)
+
+  @PreDestroy def dispose: Unit =
+    Try(queue.close())
+      .recover(e => LOGGER.debug("error closing queue", e))
+
+  override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: EmailSubmissionSetRequest): SMono[InvocationWithContext] = {
+    for {
+      createdResults <- create(request, mailboxSession, invocation.processingContext)
+    } yield InvocationWithContext(
+      invocation = Invocation(
+        methodName = invocation.invocation.methodName,
+        arguments = Arguments(serializer.serializeEmailSubmissionSetResponse(EmailSubmissionSetResponse(
+            accountId = request.accountId,
+            newState = State.INSTANCE,
+            created = Some(createdResults._1.retrieveCreated).filter(_.nonEmpty),
+            notCreated = Some(createdResults._1.retrieveErrors).filter(_.nonEmpty)))
+          .as[JsObject]),
+        methodCallId = invocation.invocation.methodCallId),
+      processingContext = createdResults._2)
+  }
+
+  override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): SMono[EmailSubmissionSetRequest] =
+    serializer.deserializeEmailSubmissionSetRequest(invocation.arguments.value) match {
+      case JsSuccess(emailSubmissionSetRequest, _) => SMono.just(emailSubmissionSetRequest)
+      case errors: JsError => SMono.raiseError(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+    }
+
+  private def create(request: EmailSubmissionSetRequest,
+                     session: MailboxSession,
+                     processingContext: ProcessingContext): SMono[(CreationResults, ProcessingContext)] =
+    SFlux.fromIterable(request.create
+      .getOrElse(Map.empty)
+      .view)
+      .foldLeft((CreationResults(Nil), processingContext)) {
+        (acc : (CreationResults, ProcessingContext), elem: (EmailSubmissionCreationId, JsObject)) => {
+          val (emailSubmissionCreationId, jsObject) = elem
+          val (creationResult, updatedProcessingContext) = createSubmission(session, emailSubmissionCreationId, jsObject, acc._2)
+          (CreationResults(acc._1.created :+ creationResult), updatedProcessingContext)
+        }
+      }
+      .subscribeOn(Schedulers.elastic())
+
+  private def createSubmission(mailboxSession: MailboxSession,
+                            emailSubmissionCreationId: EmailSubmissionCreationId,
+                            jsObject: JsObject,
+                            processingContext: ProcessingContext): (CreationResult, ProcessingContext) =
+    parseCreate(jsObject)
+      .flatMap(emailSubmissionCreationRequest => sendEmail(mailboxSession, emailSubmissionCreationRequest))
+      .flatMap(creationResponse => recordCreationIdInProcessingContext(emailSubmissionCreationId, processingContext, creationResponse.id)
+        .map(context => (creationResponse, context)))
+      .fold(e => (CreationFailure(emailSubmissionCreationId, e), processingContext),
+        creationResponseWithUpdatedContext => {
+          (CreationSuccess(emailSubmissionCreationId, creationResponseWithUpdatedContext._1), creationResponseWithUpdatedContext._2)
+        })
+
+  private def parseCreate(jsObject: JsObject): Either[EmailSubmissionCreationParseException, EmailSubmissionCreationRequest] =
+    EmailSubmissionCreationRequest.validateProperties(jsObject)
+      .flatMap(validJsObject => Json.fromJson(validJsObject)(serializer.emailSubmissionCreationRequestReads) match {
+        case JsSuccess(creationRequest, _) => Right(creationRequest)
+        case JsError(errors) => Left(EmailSubmissionCreationParseException(emailSubmissionSetError(errors)))
+      })
+
+  private def emailSubmissionSetError(errors: collection.Seq[(JsPath, collection.Seq[JsonValidationError])]): SetError =
+    errors.head match {
+      case (path, Seq()) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in EmailSubmission object is not valid"))
+      case (path, Seq(JsonValidationError(Seq("error.path.missing")))) => SetError.invalidArguments(SetErrorDescription(s"Missing '$path' property in EmailSubmission object"))
+      case (path, Seq(JsonValidationError(Seq(message)))) => SetError.invalidArguments(SetErrorDescription(s"'$path' property in EmailSubmission object is not valid: $message"))
+      case (path, _) => SetError.invalidArguments(SetErrorDescription(s"Unknown error on property '$path'"))
+    }
+
+  private def sendEmail(mailboxSession: MailboxSession,
+                        request: EmailSubmissionCreationRequest): Either[Throwable, EmailSubmissionCreationResponse] = {
+    val message: Either[Exception, MessageResult] = messageIdManager.getMessage(request.emailId, FetchGroup.FULL_CONTENT, mailboxSession)
+      .asScala
+      .toList
+      .headOption
+      .toRight(MessageNotFoundException(request.emailId))
+
+    message.flatMap(m => {
+      val submissionId = EmailSubmissionId.generate
+      toMimeMessage(submissionId.value, m.getFullContent.getInputStream)
+        .flatMap(mimeMessage => {
+          Try(queue.enQueue(MailImpl.builder()
+              .name(submissionId.value)
+              .addRecipients(request.envelope.rcptTo.map(_.email).asJava)
+              .sender(request.envelope.mailFrom.email)
+              .mimeMessage(mimeMessage)
+              .addAttribute(new Attribute(MAIL_METADATA_USERNAME_ATTRIBUTE, AttributeValue.of(mailboxSession.getUser.asString())))
+              .build()))
+            .map(_ => EmailSubmissionCreationResponse(submissionId))
+        }).toEither
+    })
+  }
+
+  private def toMimeMessage(name: String, inputStream: InputStream): Try[MimeMessage] = {
+    val source = new MimeMessageInputStreamSource(name, inputStream)
+    // if MimeMessageCopyOnWriteProxy throws an error in the constructor we
+    // have to manually care disposing our source.
+    Try(new MimeMessageCopyOnWriteProxy(source))
+      .recover(e => {
+        LifecycleUtil.dispose(source)
+        throw e
+      })
+  }
+
+  private def recordCreationIdInProcessingContext(emailSubmissionCreationId: EmailSubmissionCreationId,
+                                                  processingContext: ProcessingContext,
+                                                  emailSubmissionId: EmailSubmissionId): Either[IllegalArgumentException, ProcessingContext] =
+    for {
+      creationId <- Id.validate(emailSubmissionCreationId)
+      serverAssignedId <- Id.validate(emailSubmissionId.value)
+    } yield {
+      processingContext.recordCreatedId(ClientId(creationId), ServerId(serverAssignedId))
+    }
+}


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


[james-project] 02/09: JAMES-3434 Specify tests to be written for EmailSubmission/set create

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit ef35a0d521970aeb44a7c73b1d7053163fc5b9d3
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Tue Nov 3 16:28:38 2020 +0700

    JAMES-3434 Specify tests to be written for EmailSubmission/set create
---
 .../contract/EmailSubmissionSetMethodContract.scala      | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index 6974db6..0009d81 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -41,6 +41,22 @@ import org.awaitility.Awaitility
 import org.awaitility.Duration.ONE_HUNDRED_MILLISECONDS
 import org.junit.jupiter.api.{BeforeEach, Test}
 
+/*
+  TODO
+   - Bob can not send from Andre Account ID
+   - Bob can not use Andre in mailFrom
+   - Bob cannot use Andra in sender & from Mime fields
+   - Emails with empty recipients are rejected
+   - Can send an email to himself
+   - message not found are handled
+   - extra properties are rejected
+   - message not founds are handled
+   - I can chain Email/set create & EmailSubmission/create
+   - multiple recipients
+   - cannot send other people mail
+   - can send delegated emails (read permission)
+ */
+
 trait EmailSubmissionSetMethodContract {
   private lazy val slowPacedPollInterval = ONE_HUNDRED_MILLISECONDS
   private lazy val calmlyAwait = Awaitility.`with`


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


[james-project] 03/09: JAMES-3434 Validation testing for EmailSubmission/set create

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit a937abe79d936c3aec0a446ab71f5d762495222a
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Nov 5 10:41:10 2020 +0700

    JAMES-3434 Validation testing for EmailSubmission/set create
---
 .../EmailSubmissionSetMethodContract.scala         | 793 ++++++++++++++++++++-
 .../james/jmap/rfc8621/contract/Fixture.scala      |   1 +
 .../jmap/json/EmailSubmissionSetSerializer.scala   |   2 +
 .../jmap/method/EmailSubmissionSetMethod.scala     |  45 +-
 4 files changed, 814 insertions(+), 27 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index 0009d81..f27345d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -23,40 +23,25 @@ import java.nio.charset.StandardCharsets
 import java.util.concurrent.TimeUnit
 
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
-import io.restassured.RestAssured.{`given`, `with`, requestSpecification}
+import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.builder.ResponseSpecBuilder
 import io.restassured.http.ContentType.JSON
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
 import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, andreAccountId, authScheme, baseRequestSpecBuilder}
 import org.apache.james.mailbox.DefaultMailboxes
 import org.apache.james.mailbox.MessageManager.AppendCommand
-import org.apache.james.mailbox.model.{MailboxId, MailboxPath, MessageId}
+import org.apache.james.mailbox.model.MailboxACL.Right
+import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, MessageId}
 import org.apache.james.mime4j.dom.Message
-import org.apache.james.modules.MailboxProbeImpl
+import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
 import org.apache.james.utils.DataProbeImpl
 import org.awaitility.Awaitility
 import org.awaitility.Duration.ONE_HUNDRED_MILLISECONDS
 import org.junit.jupiter.api.{BeforeEach, Test}
 
-/*
-  TODO
-   - Bob can not send from Andre Account ID
-   - Bob can not use Andre in mailFrom
-   - Bob cannot use Andra in sender & from Mime fields
-   - Emails with empty recipients are rejected
-   - Can send an email to himself
-   - message not found are handled
-   - extra properties are rejected
-   - message not founds are handled
-   - I can chain Email/set create & EmailSubmission/create
-   - multiple recipients
-   - cannot send other people mail
-   - can send delegated emails (read permission)
- */
-
 trait EmailSubmissionSetMethodContract {
   private lazy val slowPacedPollInterval = ONE_HUNDRED_MILLISECONDS
   private lazy val calmlyAwait = Awaitility.`with`
@@ -132,7 +117,7 @@ trait EmailSubmissionSetMethodContract {
          |  "methodCalls": [[
          |    "Email/query",
          |    {
-         |      "accountId": "1e8584548eca20f26faf6becc1704a0f352839f12c208a47fbd486d60f491f7c",
+         |      "accountId": "$andreAccountId",
          |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
          |    },
          |    "c1"]]
@@ -161,6 +146,264 @@ trait EmailSubmissionSetMethodContract {
   }
 
   @Test
+  def emailSubmissionSetCreateShouldSendMailSuccessfullyToSelf(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 bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${BOB.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val requestReadMail =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${bobInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(requestReadMail)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCreateShouldSendMailSuccessfullyToBothRecipients(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 bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    val andreInboxPath = MailboxPath.inbox(ANDRE)
+    val andreInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreInboxPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${BOB.asString}"}, {"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val requestReadMailBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${bobInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    val requestReadMailAndre =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$andreAccountId",
+         |      "filter": {"inMailbox": "${andreInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val responseBob = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(requestReadMailBob)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+      val responseAndre = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(requestReadMailAndre)
+          .build, new ResponseSpecBuilder().build)
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(responseBob)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+      assertThatJson(responseAndre)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def emailSubmissionSetCanBeChainedAfterEmailSet(server: GuiceJamesServer): Unit = {
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    val draftId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val bobInboxId: MailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "create": {
+         |        "e1526":{
+         |          "mailboxIds": {"${draftId.serialize}": true},
+         |          "to": [{"email": "${BOB.asString}"}],
+         |          "from": [{"email": "${BOB.asString}"}]
+         |        }
+         |      }
+         |    }, "c1"],
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "#e1526",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${BOB.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val requestReadMailBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${bobInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val responseBob = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(requestReadMailBob)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(responseBob)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
   def emailSubmissionSetCreateShouldReturnSuccess(server: GuiceJamesServer): Unit = {
     val message: Message = Message.Builder
       .of
@@ -213,4 +456,512 @@ trait EmailSubmissionSetMethodContract {
       .inPath("methodResponses[0][1].created")
       .isEqualTo("""{"k1490": {}}""")
   }
+
+  @Test
+  def setShouldRejectOtherAccountIds(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$andreAccountId",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0]")
+      .isEqualTo("""[
+                   |  "error",
+                   |  {"type": "accountNotFound"},
+                   |  "c1"
+                   |]""".stripMargin)
+  }
+
+  @Test
+  def setShouldRejectMessageNotFound(): Unit = {
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${randomMessageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated")
+      .isEqualTo("""{
+                   |  "k1490": {
+                   |    "type": "invalidArguments",
+                   |    "description": "The email to be sent cannot be found",
+                   |    "properties": ["emailId"]
+                   |  }
+                   |}""".stripMargin)
+  }
+
+  @Test
+  def setShouldRejectExtraProperties(): Unit = {
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${randomMessageId.serialize}",
+         |           "extra": true,
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated")
+      .isEqualTo("""{
+                   |  "k1490": {
+                   |    "type": "invalidArguments",
+                   |    "description": "Some unknown properties were specified",
+                   |    "properties": ["extra"]
+                   |  }
+                   |}""".stripMargin)
+  }
+
+  @Test
+  def setShouldRejectMessageOfOtherUsers(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 andreDraftsPath = MailboxPath.forUser(ANDRE, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(ANDRE.asString(), andreDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated")
+      .isEqualTo("""{
+                   |  "k1490": {
+                   |    "type": "invalidArguments",
+                   |    "description": "The email to be sent cannot be found",
+                   |    "properties": ["emailId"]
+                   |  }
+                   |}""".stripMargin)
+  }
+
+  @Test
+  def setShouldAcceptDelegatedMessages(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(BOB.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val andreDraftsPath = MailboxPath.forUser(ANDRE, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andreDraftsPath)
+    val bobInboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(MailboxPath.inbox(BOB))
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(andreDraftsPath, BOB.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(ANDRE.asString(), andreDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${BOB.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+
+    val requestReadMail =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core","urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [[
+         |    "Email/query",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "filter": {"inMailbox": "${bobInboxId.serialize}"}
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    awaitAtMostTenSeconds.untilAsserted { () =>
+      val response = `given`
+        .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+        .body(requestReadMail)
+      .when
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(response)
+        .inPath("methodResponses[0][1].ids")
+        .isArray
+        .hasSize(1)
+    }
+  }
+
+  @Test
+  def setShouldRejectOtherUserUsageInSenderMimeField(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(ANDRE.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated")
+      .isEqualTo("""{
+                   |  "k1490": {
+                   |    "type": "forbiddenMailFrom",
+                   |    "description": "Attempt to send a mail whose MimeMessage From and Sender fields not allowed for connected user: List(andre@domain.tld)"
+                   |  }
+                   |}""".stripMargin)
+  }
+
+  @Test
+  def setShouldRejectOtherUserUsageInFromMimeField(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString, ANDRE.asString())
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated")
+      .isEqualTo("""{
+                   |  "k1490": {
+                   |    "type": "forbiddenMailFrom",
+                   |    "description": "Attempt to send a mail whose MimeMessage From and Sender fields not allowed for connected user: List(andre@domain.tld)"
+                   |  }
+                   |}""".stripMargin)
+  }
+
+  @Test
+  def setShouldRejectOtherUserUsageInFromEnvelopeField(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${ANDRE.asString}"},
+         |             "rcptTo": [{"email": "${ANDRE.asString}"}]
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated")
+      .isEqualTo("""{
+                   |  "k1490": {
+                   |    "type": "forbiddenFrom",
+                   |    "description": "Attempt to send a mail whose envelope From not allowed for connected user: andre@domain.tld",
+                   |    "properties":["envelope.mailFrom"]
+                   |  }
+                   |}""".stripMargin)
+  }
+
+  @Test
+  def setShouldRejectNoRecipients(server: GuiceJamesServer): Unit = {
+    val message: Message = Message.Builder
+      .of
+      .setSubject("test")
+      .setSender(BOB.asString)
+      .setFrom(BOB.asString)
+      .setTo(ANDRE.asString)
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+
+    val bobDraftsPath = MailboxPath.forUser(BOB, DefaultMailboxes.DRAFTS)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobDraftsPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobDraftsPath, AppendCommand.builder()
+      .build(message))
+      .getMessageId
+
+    val requestBob =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |     ["EmailSubmission/set", {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "create": {
+         |         "k1490": {
+         |           "emailId": "${messageId.serialize}",
+         |           "envelope": {
+         |             "mailFrom": {"email": "${BOB.asString}"},
+         |             "rcptTo": []
+         |           }
+         |         }
+         |    }
+         |  }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(requestBob)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notCreated")
+      .isEqualTo("""{
+                   |  "k1490": {
+                   |    "type": "noRecipients",
+                   |    "description": "Attempt to send a mail with no recipients"
+                   |  }
+                   |}""".stripMargin)
+  }
 }
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
index 6b2a9fb..c9844b3 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/Fixture.scala
@@ -39,6 +39,7 @@ import org.apache.james.mime4j.dom.Message
 object Fixture {
   val ACCOUNT_ID: String = "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6"
   val ALICE_ACCOUNT_ID: String = "2bd806c97f0e00af1a1fc3328fa763a9269723c8db8fac4f93af71db186d6e90"
+  def andreAccountId: String = "1e8584548eca20f26faf6becc1704a0f352839f12c208a47fbd486d60f491f7c"
 
   def createTestMessage: Message = Message.Builder
       .of
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
index 6bdb66e..f927639 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
@@ -23,6 +23,7 @@ import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.refineV
 import javax.inject.Inject
 import org.apache.james.core.MailAddress
+import org.apache.james.jmap.core.SetError
 import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
 import org.apache.james.jmap.mail.{EmailSubmissionAddress, EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope, Parameters}
 import org.apache.james.mailbox.model.MessageId
@@ -43,6 +44,7 @@ class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory
       .fold(_ => JsError("Invalid messageId"), messageId => messageId)
     case _ => JsError("Expecting messageId to be represented by a JsString")
   }
+  private implicit val notCreatedWrites: Writes[Map[EmailSubmissionCreationId, SetError]] = mapWrites[EmailSubmissionCreationId, SetError](_.value, setErrorWrites)
 
   private implicit val mailAddressReads: Reads[MailAddress] = {
     case JsString(value) => Try(JsSuccess(new MailAddress(value)))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
index cfc96b5..315c330 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSubmissionSetMethod.scala
@@ -24,14 +24,14 @@ import java.io.InputStream
 import eu.timepit.refined.auto._
 import javax.annotation.PreDestroy
 import javax.inject.Inject
-import javax.mail.internet.MimeMessage
+import javax.mail.internet.{InternetAddress, MimeMessage}
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
 import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
-import org.apache.james.jmap.core.{ClientId, Id, Invocation, ServerId, SetError, State}
-import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.core.SetError.{SetErrorDescription, SetErrorType}
+import org.apache.james.jmap.core.{ClientId, Id, Invocation, Properties, ServerId, SetError, State}
 import org.apache.james.jmap.json.{EmailSubmissionSetSerializer, ResponseSerializer}
 import org.apache.james.jmap.mail.EmailSubmissionSet.EmailSubmissionCreationId
-import org.apache.james.jmap.mail.{EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse}
+import org.apache.james.jmap.mail.{EmailSubmissionCreationRequest, EmailSubmissionCreationResponse, EmailSubmissionId, EmailSubmissionSetRequest, EmailSubmissionSetResponse, Envelope}
 import org.apache.james.jmap.method.EmailSubmissionSetMethod.{LOGGER, MAIL_METADATA_USERNAME_ATTRIBUTE}
 import org.apache.james.jmap.routes.{ProcessingContext, SessionSupplier}
 import org.apache.james.lifecycle.api.{LifecycleUtil, Startable}
@@ -48,14 +48,20 @@ import reactor.core.scala.publisher.{SFlux, SMono}
 import reactor.core.scheduler.Schedulers
 
 import scala.jdk.CollectionConverters._
-import scala.util.Try
+import scala.util.{Failure, Success, Try}
 
 object EmailSubmissionSetMethod {
   val MAIL_METADATA_USERNAME_ATTRIBUTE: AttributeName = AttributeName.of("org.apache.james.jmap.send.MailMetaData.username")
   val LOGGER: Logger = LoggerFactory.getLogger(classOf[EmailSubmissionSetMethod])
+  val noRecipients: SetErrorType = "noRecipients"
+  val forbiddenFrom: SetErrorType = "forbiddenFrom"
+  val forbiddenMailFrom: SetErrorType = "forbiddenMailFrom"
 }
 
 case class EmailSubmissionCreationParseException(setError: SetError) extends Exception
+case class NoRecipientException() extends Exception
+case class ForbiddenFromException(from: String) extends Exception
+case class ForbiddenMailFromException(from: List[String]) extends Exception
 
 class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerializer,
                                          messageIdManager: MessageIdManager,
@@ -64,7 +70,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
                                          val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[EmailSubmissionSetRequest] with Startable {
   override val methodName: MethodName = MethodName("EmailSubmission/set")
   override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
-  var queue: MailQueue = null
+  var queue: MailQueue = _
 
   sealed trait CreationResult {
     def emailSubmissionCreationId: EmailSubmissionCreationId
@@ -73,6 +79,16 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
   case class CreationFailure(emailSubmissionCreationId: EmailSubmissionCreationId, exception: Throwable) extends CreationResult {
     def asSetError: SetError = exception match {
       case e: EmailSubmissionCreationParseException => e.setError
+      case _: NoRecipientException => SetError(EmailSubmissionSetMethod.noRecipients,
+        SetErrorDescription("Attempt to send a mail with no recipients"), None)
+      case e: ForbiddenMailFromException => SetError(EmailSubmissionSetMethod.forbiddenMailFrom,
+        SetErrorDescription(s"Attempt to send a mail whose MimeMessage From and Sender fields not allowed for connected user: ${e.from}"), None)
+      case e: ForbiddenFromException => SetError(EmailSubmissionSetMethod.forbiddenFrom,
+        SetErrorDescription(s"Attempt to send a mail whose envelope From not allowed for connected user: ${e.from}"),
+        Some(Properties("envelope.mailFrom")))
+      case _: MessageNotFoundException => SetError(SetError.invalidArgumentValue,
+        SetErrorDescription("The email to be sent cannot be found"),
+        Some(Properties("emailId")))
       case e: Exception =>
         e.printStackTrace()
         SetError.serverFail(SetErrorDescription(exception.getMessage))
@@ -179,6 +195,7 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
     message.flatMap(m => {
       val submissionId = EmailSubmissionId.generate
       toMimeMessage(submissionId.value, m.getFullContent.getInputStream)
+        .flatMap(message => validate(mailboxSession)(message, request.envelope))
         .flatMap(mimeMessage => {
           Try(queue.enQueue(MailImpl.builder()
               .name(submissionId.value)
@@ -203,6 +220,22 @@ class EmailSubmissionSetMethod @Inject()(serializer: EmailSubmissionSetSerialize
       })
   }
 
+  private def validate(session: MailboxSession)(mimeMessage: MimeMessage, envelope: Envelope): Try[MimeMessage] = {
+    val forbiddenMailFrom: List[String] = (Option(mimeMessage.getSender).toList ++ Option(mimeMessage.getFrom).toList.flatten)
+      .map(_.asInstanceOf[InternetAddress].getAddress)
+      .filter(from => !from.equals(session.getUser.asString()))
+
+    if (forbiddenMailFrom.nonEmpty) {
+      Failure(ForbiddenMailFromException(forbiddenMailFrom))
+    } else if (envelope.rcptTo.isEmpty) {
+      Failure(NoRecipientException())
+    } else if (!envelope.mailFrom.email.asString.equals(session.getUser.asString())) {
+      Failure(ForbiddenFromException(envelope.mailFrom.email.asString))
+    } else {
+      Success(mimeMessage)
+    }
+  }
+
   private def recordCreationIdInProcessingContext(emailSubmissionCreationId: EmailSubmissionCreationId,
                                                   processingContext: ProcessingContext,
                                                   emailSubmissionId: EmailSubmissionId): Either[IllegalArgumentException, ProcessingContext] =


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


[james-project] 07/09: JAMES-3434 EmailSubmissionCreationResponse should rely on valueWrites

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

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

commit ec2e021402fc371eaed087cc43011a2d35f71dab
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Nov 6 11:54:33 2020 +0700

    JAMES-3434 EmailSubmissionCreationResponse should rely on valueWrites
---
 .../james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala  | 2 +-
 .../scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
index e277d82..df9ff97 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSubmissionSetMethodContract.scala
@@ -686,7 +686,7 @@ trait EmailSubmissionSetMethodContract {
 
     assertThatJson(response)
       // Ids are randomly generated, and not stored, let's ignore it
-      .whenIgnoringPaths("methodResponses[0][1].created.k1490.id")
+      .whenIgnoringPaths("methodResponses[0][1].created.k1490")
       .inPath("methodResponses[0][1].created")
       .isEqualTo("""{"k1490": {}}""")
   }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
index 6dd492e..66e49b7 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSubmissionSetSerializer.scala
@@ -61,7 +61,7 @@ class EmailSubmissionSetSerializer @Inject()(messageIdFactory: MessageId.Factory
 
   implicit val emailSubmissionCreationRequestReads: Reads[EmailSubmissionCreationRequest] = Json.reads[EmailSubmissionCreationRequest]
 
-  private implicit val emailSubmissionCreationResponseWrites: Writes[EmailSubmissionCreationResponse] = Json.writes[EmailSubmissionCreationResponse]
+  private implicit val emailSubmissionCreationResponseWrites: Writes[EmailSubmissionCreationResponse] = Json.valueWrites[EmailSubmissionCreationResponse]
 
   private implicit def emailSubmissionSetResponseWrites(implicit emailSubmissionCreationResponseWrites: Writes[EmailSubmissionCreationResponse]): Writes[EmailSubmissionSetResponse] = Json.writes[EmailSubmissionSetResponse]
 


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