You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by GitBox <gi...@apache.org> on 2021/11/17 09:12:54 UTC

[GitHub] [james-project] chibenwa commented on a change in pull request #752: JAMES-3534 Implement Identity/set create

chibenwa commented on a change in pull request #752:
URL: https://github.com/apache/james-project/pull/752#discussion_r751028057



##########
File path: server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
##########
@@ -0,0 +1,618 @@
+/****************************************************************
+ * 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 io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+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.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.core.UuidState.INSTANCE
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait IdentitySetContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addDomain("domain-alias.tld")
+      .addUser(BOB.asString, BOB_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def setIdentityShouldSucceed(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"email": "bob@domain.tld",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |
+           |		}

Review comment:
       `mayDelete` is a server set property and should be returned.
   
   `id` is a server set property and should thus be also returned.

##########
File path: server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
##########
@@ -0,0 +1,618 @@
+/****************************************************************
+ * 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 io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+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.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.core.UuidState.INSTANCE
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait IdentitySetContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addDomain("domain-alias.tld")
+      .addUser(BOB.asString, BOB_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def setIdentityShouldSucceed(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"email": "bob@domain.tld",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityWithSomePropertiesOmittedShouldSucceedAndReturnDefaultValues(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"email": "bob@domain.tld"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |			"name": "",
+           |			"textSignature": "",
+           |			"htmlSignature": ""

Review comment:
       `mayDelete` is a server set property and should be returned.
   
   `id` is a server set property and should thus be also returned.

##########
File path: server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
##########
@@ -0,0 +1,618 @@
+/****************************************************************
+ * 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 io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+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.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.core.UuidState.INSTANCE
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait IdentitySetContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addDomain("domain-alias.tld")
+      .addUser(BOB.asString, BOB_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def setIdentityShouldSucceed(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"email": "bob@domain.tld",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityWithSomePropertiesOmittedShouldSucceedAndReturnDefaultValues(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"email": "bob@domain.tld"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |			"name": "",
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldCreatedSeveralValidCreationRequest(): Unit = {
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "Identity1",
+        |						"email": "bob@domain.tld",
+        |						"replyTo": [{
+        |							"name": "Alice",
+        |							"email": "alice@domain.tld"
+        |						}],
+        |						"bcc": [{
+        |							"name": "David",
+        |							"email": "david@domain.tld"
+        |						}],
+        |						"textSignature": "Some text signature",
+        |						"htmlSignature": "<p>Some html signature</p>"
+        |					},
+        |					"4f29": {
+        |						"name": "Identity2",
+        |						"email": "bob@domain.tld",
+        |						"replyTo": null,
+        |						"bcc": null,
+        |						"textSignature": "Some text signature",
+        |						"htmlSignature": "<p>Some html signature</p>"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           | 	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           | 	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           | 	"created": {
+           | 		"4f28": {
+           |
+           | 		},
+           | 		"4f29": {
+           |
+           | 		}
+           | 	}
+           | }""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldReturnForbiddenFromErrorWhenForbiddenEmailProperty(): Unit = {
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "valid send from identity",
+        |						"email": "bob@domain.tld"
+        |					},
+        |					"4f29": {
+        |						"name": "forbidden send from identity",
+        |						"email": "bob-alias@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"created": {
+           |		"4f28": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	},
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "forbiddenFrom",
+           |				"description": "Can not send from bob-alias@domain.tld"
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldSucceedWhenValidEmailProperty(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob-alias", "domain.tld", "bob@domain.tld")
+
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "valid send from identity",
+        |						"email": "bob@domain.tld"
+        |					},
+        |					"4f29": {
+        |						"name": "valid send from identity",
+        |						"email": "bob-alias@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"created": {
+           |		"4f28": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		},
+           |		"4f29": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenEmailPropertyIsMissing(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "invalidArguments",
+           |				"description": "Missing '/email' property in Identity object"
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenEmailPropertyIsNull(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"email": null,
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "invalidArguments",
+           |				"description": "'/email' property in Identity object is not valid: mail address needs to be represented with a JsString"
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldNotCreatedWhenCreationRequestHasServerSetProperty(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"id": "someId",
+         |            "mayDelete": false,
+         |						"email": "bob@domain.tld"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "invalidArguments",
+           |				"description": "Some server-set properties were specified",
+           |				"properties": [
+           |					"id",
+           |					"mayDelete"
+           |				]
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenWrongAccountId(): Unit = {
+    val request =
+      s"""
+         |{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "unknownAccountId",
+         |				"create": {}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |  "sessionState": "${SESSION_STATE.value}",
+         |  "methodResponses": [
+         |    ["error", {
+         |      "type": "accountNotFound"
+         |    }, "c1"]
+         |  ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenMissingCapability(): Unit = {
+    val request: String =
+      """{
+        |	"using": [],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f29": {
+        |						"email": "bob@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "error",
+           |            {
+           |                "type": "unknownMethod",
+           |                "description": "Missing capability(ies): urn:ietf:params:jmap:core"
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+}

Review comment:
       I would like to have a test ensuring that client creation ids for identities are well added to the processing context.
   
   Something like one API call with `Identity/set create af29 + Identity/get #af29` 
   
   CF https://jmap.io/spec-core.html#set
   
   ```
   Some records may hold references to other records (foreign keys). That reference may be set (via create or update) in the same request as the referenced record is created. To do this, the client refers to the new record using its creation id prefixed with a #. The order of the method calls in the request by the client MUST be such that the record being referenced is created in the same or an earlier call. Thus, the server never has to look ahead. Instead, while processing a request, the server MUST keep a simple map for the duration of the request of creation id to record id for each newly created record, so it can substitute in the correct value if necessary in later method calls. In the case of records with references to the same type, the server MUST order the creates and updates within a single method call so that creates happen before their creation ids are referenced by another create/update/destroy in the same call.
   ```
   
   We have a generic mechanism in place managing this but we need to ensure the actual creation is registered...

##########
File path: server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
##########
@@ -0,0 +1,618 @@
+/****************************************************************
+ * 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 io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+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.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.core.UuidState.INSTANCE
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait IdentitySetContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addDomain("domain-alias.tld")
+      .addUser(BOB.asString, BOB_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def setIdentityShouldSucceed(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"email": "bob@domain.tld",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityWithSomePropertiesOmittedShouldSucceedAndReturnDefaultValues(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"email": "bob@domain.tld"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |			"name": "",
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldCreatedSeveralValidCreationRequest(): Unit = {
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "Identity1",
+        |						"email": "bob@domain.tld",
+        |						"replyTo": [{
+        |							"name": "Alice",
+        |							"email": "alice@domain.tld"
+        |						}],
+        |						"bcc": [{
+        |							"name": "David",
+        |							"email": "david@domain.tld"
+        |						}],
+        |						"textSignature": "Some text signature",
+        |						"htmlSignature": "<p>Some html signature</p>"
+        |					},
+        |					"4f29": {
+        |						"name": "Identity2",
+        |						"email": "bob@domain.tld",
+        |						"replyTo": null,
+        |						"bcc": null,
+        |						"textSignature": "Some text signature",
+        |						"htmlSignature": "<p>Some html signature</p>"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           | 	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           | 	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           | 	"created": {
+           | 		"4f28": {
+           |
+           | 		},
+           | 		"4f29": {
+           |
+           | 		}
+           | 	}
+           | }""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldReturnForbiddenFromErrorWhenForbiddenEmailProperty(): Unit = {
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "valid send from identity",
+        |						"email": "bob@domain.tld"
+        |					},
+        |					"4f29": {
+        |						"name": "forbidden send from identity",
+        |						"email": "bob-alias@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"created": {
+           |		"4f28": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	},
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "forbiddenFrom",
+           |				"description": "Can not send from bob-alias@domain.tld"
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldSucceedWhenValidEmailProperty(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob-alias", "domain.tld", "bob@domain.tld")
+
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "valid send from identity",
+        |						"email": "bob@domain.tld"
+        |					},
+        |					"4f29": {
+        |						"name": "valid send from identity",
+        |						"email": "bob-alias@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"created": {
+           |		"4f28": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		},
+           |		"4f29": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenEmailPropertyIsMissing(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "invalidArguments",
+           |				"description": "Missing '/email' property in Identity object"
+           |			}

Review comment:
       ```suggestion
              |			{
              |				"type": "invalidArguments",
              |				"description": "Missing '/email' property in Identity object"
              |                            "properties": "email"
              |			}
   ```
   
   I bet we could easily have the impacted property specified explicitly with the invalidArgument format....

##########
File path: server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
##########
@@ -99,6 +99,9 @@ class DefaultIdentitySupplier @Inject()(canSendFrom: CanSendFrom) {
             htmlSignature = HtmlSignature.DEFAULT,
             mayDelete = MayDeleteIdentity(false))))
 
+  def userCanSendFrom(username: Username, mailAddress: MailAddress): Boolean =
+    canSendFrom.userCanSendFrom(username, Username.of(mailAddress.asString()))

Review comment:
       `Username.of(mailAddress.asString())` should be replaced by `usersRepository.getUsername(mailAddress)`

##########
File path: server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
##########
@@ -0,0 +1,618 @@
+/****************************************************************
+ * 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 io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+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.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.core.UuidState.INSTANCE
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait IdentitySetContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addDomain("domain-alias.tld")
+      .addUser(BOB.asString, BOB_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def setIdentityShouldSucceed(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"email": "bob@domain.tld",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityWithSomePropertiesOmittedShouldSucceedAndReturnDefaultValues(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"email": "bob@domain.tld"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |			"name": "",
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldCreatedSeveralValidCreationRequest(): Unit = {
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "Identity1",
+        |						"email": "bob@domain.tld",
+        |						"replyTo": [{
+        |							"name": "Alice",
+        |							"email": "alice@domain.tld"
+        |						}],
+        |						"bcc": [{
+        |							"name": "David",
+        |							"email": "david@domain.tld"
+        |						}],
+        |						"textSignature": "Some text signature",
+        |						"htmlSignature": "<p>Some html signature</p>"
+        |					},
+        |					"4f29": {
+        |						"name": "Identity2",
+        |						"email": "bob@domain.tld",
+        |						"replyTo": null,
+        |						"bcc": null,
+        |						"textSignature": "Some text signature",
+        |						"htmlSignature": "<p>Some html signature</p>"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           | 	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           | 	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           | 	"created": {
+           | 		"4f28": {
+           |
+           | 		},
+           | 		"4f29": {
+           |
+           | 		}
+           | 	}
+           | }""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldReturnForbiddenFromErrorWhenForbiddenEmailProperty(): Unit = {
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "valid send from identity",
+        |						"email": "bob@domain.tld"
+        |					},
+        |					"4f29": {
+        |						"name": "forbidden send from identity",
+        |						"email": "bob-alias@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"created": {
+           |		"4f28": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	},
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "forbiddenFrom",
+           |				"description": "Can not send from bob-alias@domain.tld"
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldSucceedWhenValidEmailProperty(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob-alias", "domain.tld", "bob@domain.tld")
+
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "valid send from identity",
+        |						"email": "bob@domain.tld"
+        |					},
+        |					"4f29": {
+        |						"name": "valid send from identity",
+        |						"email": "bob-alias@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"created": {
+           |		"4f28": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		},
+           |		"4f29": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenEmailPropertyIsMissing(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "invalidArguments",
+           |				"description": "Missing '/email' property in Identity object"
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenEmailPropertyIsNull(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"email": null,
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "invalidArguments",
+           |				"description": "'/email' property in Identity object is not valid: mail address needs to be represented with a JsString"
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldNotCreatedWhenCreationRequestHasServerSetProperty(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"id": "someId",
+         |            "mayDelete": false,
+         |						"email": "bob@domain.tld"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "invalidArguments",
+           |				"description": "Some server-set properties were specified",
+           |				"properties": [
+           |					"id",
+           |					"mayDelete"
+           |				]
+           |			}
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenWrongAccountId(): Unit = {
+    val request =
+      s"""
+         |{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "unknownAccountId",
+         |				"create": {}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .log().ifValidationFails()
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response).isEqualTo(
+      s"""{
+         |  "sessionState": "${SESSION_STATE.value}",
+         |  "methodResponses": [
+         |    ["error", {
+         |      "type": "accountNotFound"
+         |    }, "c1"]
+         |  ]
+         |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldFailWhenMissingCapability(): Unit = {
+    val request: String =
+      """{
+        |	"using": [],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f29": {
+        |						"email": "bob@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "error",
+           |            {
+           |                "type": "unknownMethod",
+           |                "description": "Missing capability(ies): urn:ietf:params:jmap:core"
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+

Review comment:
       Do we also fail when missing `urn:ietf:params:jmap:submission` ?
   
   ```
   This represents support for the Identity and EmailSubmission data types and associated API methods. 
   ```

##########
File path: server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentitySetCreatePerformer.scala
##########
@@ -0,0 +1,105 @@
+/****************************************************************
+ * 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 javax.inject.Inject
+import org.apache.james.jmap.api.identity.{IdentityCreationRequest, IdentityRepository}
+import org.apache.james.jmap.api.model.{ForbiddenSendFromException, HtmlSignature, IdentityName, TextSignature}
+import org.apache.james.jmap.core.SetError
+import org.apache.james.jmap.core.SetError.SetErrorDescription
+import org.apache.james.jmap.json.IdentitySerializer
+import org.apache.james.jmap.mail.{IdentityCreation, IdentityCreationId, IdentityCreationParseException, IdentityCreationResponse, IdentitySetRequest}
+import org.apache.james.jmap.method.IdentitySetCreatePerformer.{CreationFailure, CreationResult, CreationResults, CreationSuccess}
+import org.apache.james.mailbox.MailboxSession
+import play.api.libs.json.{JsObject, JsPath, JsonValidationError}
+import reactor.core.scala.publisher.{SFlux, SMono}
+import reactor.core.scheduler.Schedulers
+
+object IdentitySetCreatePerformer {
+  case class CreationResults(results: Seq[CreationResult]) {
+    def created: Option[Map[IdentityCreationId, IdentityCreationResponse]] =
+      Option(results.flatMap {
+        case result: CreationSuccess => Some((result.clientId, result.response))
+        case _ => None
+      }.toMap)
+        .filter(_.nonEmpty)
+
+    def notCreated: Option[Map[IdentityCreationId, SetError]] = {

Review comment:
       Useless `{}` spotted...

##########
File path: server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
##########
@@ -0,0 +1,618 @@
+/****************************************************************
+ * 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 io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+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.core.ResponseObject.SESSION_STATE
+import org.apache.james.jmap.core.UuidState.INSTANCE
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait IdentitySetContract {
+  @BeforeEach
+  def setUp(server: GuiceJamesServer): Unit = {
+    server.getProbe(classOf[DataProbeImpl])
+      .fluent
+      .addDomain(DOMAIN.asString)
+      .addDomain("domain-alias.tld")
+      .addUser(BOB.asString, BOB_PASSWORD)
+
+    requestSpecification = baseRequestSpecBuilder(server)
+      .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+      .build
+  }
+
+  @Test
+  def setIdentityShouldSucceed(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"name": "Bob",
+         |						"email": "bob@domain.tld",
+         |						"replyTo": [{
+         |							"name": "Alice",
+         |							"email": "alice@domain.tld"
+         |						}],
+         |						"bcc": [{
+         |							"name": "David",
+         |							"email": "david@domain.tld"
+         |						}],
+         |						"textSignature": "Some text signature",
+         |						"htmlSignature": "<p>Some html signature</p>"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityWithSomePropertiesOmittedShouldSucceedAndReturnDefaultValues(): Unit = {
+    val request =
+      s"""{
+         |	"using": ["urn:ietf:params:jmap:core"],
+         |	"methodCalls": [
+         |		[
+         |			"Identity/set",
+         |			{
+         |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+         |				"create": {
+         |					"4f29": {
+         |						"email": "bob@domain.tld"
+         |					}
+         |				}
+         |			},
+         |			"c1"
+         |		]
+         |	]
+         |}""".stripMargin
+
+    val response =  `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s"""{
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "${INSTANCE.serialize}",
+           |	"created": {
+           |		"4f29": {
+           |			"name": "",
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	}
+           |}""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldCreatedSeveralValidCreationRequest(): Unit = {
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "Identity1",
+        |						"email": "bob@domain.tld",
+        |						"replyTo": [{
+        |							"name": "Alice",
+        |							"email": "alice@domain.tld"
+        |						}],
+        |						"bcc": [{
+        |							"name": "David",
+        |							"email": "david@domain.tld"
+        |						}],
+        |						"textSignature": "Some text signature",
+        |						"htmlSignature": "<p>Some html signature</p>"
+        |					},
+        |					"4f29": {
+        |						"name": "Identity2",
+        |						"email": "bob@domain.tld",
+        |						"replyTo": null,
+        |						"bcc": null,
+        |						"textSignature": "Some text signature",
+        |						"htmlSignature": "<p>Some html signature</p>"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           | 	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           | 	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           | 	"created": {
+           | 		"4f28": {
+           |
+           | 		},
+           | 		"4f29": {
+           |
+           | 		}
+           | 	}
+           | }""".stripMargin)
+  }
+
+  @Test
+  def setIdentityShouldReturnForbiddenFromErrorWhenForbiddenEmailProperty(): Unit = {
+    val request: String =
+      """{
+        |	"using": ["urn:ietf:params:jmap:core"],
+        |	"methodCalls": [
+        |		[
+        |			"Identity/set",
+        |			{
+        |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+        |				"create": {
+        |					"4f28": {
+        |						"name": "valid send from identity",
+        |						"email": "bob@domain.tld"
+        |					},
+        |					"4f29": {
+        |						"name": "forbidden send from identity",
+        |						"email": "bob-alias@domain.tld"
+        |					}
+        |				}
+        |			},
+        |			"c1"
+        |		]
+        |	]
+        |}""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1]")
+      .isEqualTo(
+        s""" {
+           |	"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |	"newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |	"created": {
+           |		"4f28": {
+           |			"textSignature": "",
+           |			"htmlSignature": ""
+           |		}
+           |	},
+           |	"notCreated": [
+           |		[
+           |			"4f29",
+           |			{
+           |				"type": "forbiddenFrom",
+           |				"description": "Can not send from bob-alias@domain.tld"
+           |			}
+           |		]
+           |	]

Review comment:
       This is not a correct format for a map!
   
   ```suggestion
              |	"notCreated": {
              |         "4f29" : {
              |		"type": "forbiddenFrom",
              |		"description": "Can not send from bob-alias@domain.tld"
              |	     }
              |    }
   ```




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

To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org

For queries about this service, please contact Infrastructure at:
users@infra.apache.org



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