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

[james-project] branch master updated: JAMES-3534 Identity/set update should work on existing server-set Identities (#757)

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

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


The following commit(s) were added to refs/heads/master by this push:
     new f59163f  JAMES-3534 Identity/set update should work on existing server-set Identities (#757)
f59163f is described below

commit f59163f5600bcd1210a0bb7c3ba3a01cbd790725
Author: vttran <vt...@linagora.com>
AuthorDate: Mon Nov 29 10:41:55 2021 +0700

    JAMES-3534 Identity/set update should work on existing server-set Identities (#757)
---
 .../identity/CassandraCustomIdentityDAO.scala      |  11 +-
 .../jmap/api/identity/CustomIdentityDAO.scala      |  93 ++++++++--
 .../memory/identity/MemoryCustomIdentityDAO.scala  |   5 +-
 .../jmap/api/identity/IdentityRepositoryTest.scala | 200 +++++++++++++++++++++
 .../rfc8621/contract/IdentityGetContract.scala     |   5 +
 .../rfc8621/contract/IdentitySetContract.scala     | 132 +++++++++++++-
 6 files changed, 420 insertions(+), 26 deletions(-)

diff --git a/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala
index 1e7f347..bd21f1e 100644
--- a/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala
+++ b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala
@@ -22,7 +22,6 @@ package org.apache.james.jmap.cassandra.identity
 import com.datastax.driver.core.querybuilder.QueryBuilder
 import com.datastax.driver.core.querybuilder.QueryBuilder.{bindMarker, insertInto, select}
 import com.datastax.driver.core.{BoundStatement, PreparedStatement, Row, Session, UDTValue}
-import javax.inject.Inject
 import org.apache.james.backends.cassandra.init.CassandraTypesProvider
 import org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor
 import org.apache.james.core.{MailAddress, Username}
@@ -34,6 +33,7 @@ import org.apache.james.jmap.cassandra.utils.EmailAddressTupleUtil
 import reactor.core.publisher.Mono
 import reactor.core.scala.publisher.{SFlux, SMono}
 
+import javax.inject.Inject
 import scala.jdk.javaapi.CollectionConverters
 
 case class CassandraCustomIdentityDAO @Inject()(session: Session,
@@ -66,12 +66,13 @@ case class CassandraCustomIdentityDAO @Inject()(session: Session,
     .where(QueryBuilder.eq(USER, bindMarker(USER)))
     .and(QueryBuilder.eq(ID, bindMarker(ID))))
 
-  override def save(user: Username, creationRequest: IdentityCreationRequest): SMono[Identity] = {
-    val id = IdentityId.generate
-    SMono.just(id)
+  override def save(user: Username, creationRequest: IdentityCreationRequest): SMono[Identity] =
+    save(user, IdentityId.generate, creationRequest)
+
+  override def save(user: Username, identityId: IdentityId, creationRequest: IdentityCreationRequest): SMono[Identity] =
+    SMono.just(identityId)
       .map(creationRequest.asIdentity)
       .flatMap(identity => insert(user, identity))
-  }
 
   override def list(user: Username): SFlux[Identity] =
     SFlux.fromPublisher(executor.executeRows(selectAllStatement.bind().setString(USER, user.asString()))
diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
index 55e88e9..9b1a2a2 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/identity/CustomIdentityDAO.scala
@@ -21,10 +21,11 @@ package org.apache.james.jmap.api.identity
 
 import java.nio.charset.StandardCharsets
 import java.util.UUID
+import javax.inject.Inject
 
 import com.google.common.collect.ImmutableList
-import javax.inject.Inject
 import org.apache.james.core.{MailAddress, Username}
+import org.apache.james.jmap.api.identity.IdentityWithOrigin.IdentityWithOrigin
 import org.apache.james.jmap.api.model.{EmailAddress, ForbiddenSendFromException, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
 import org.apache.james.rrt.api.CanSendFrom
 import org.apache.james.user.api.UsersRepository
@@ -36,11 +37,11 @@ import scala.jdk.CollectionConverters._
 import scala.util.Try
 
 case class IdentityCreationRequest(name: Option[IdentityName],
-                                    email: MailAddress,
-                                    replyTo: Option[List[EmailAddress]],
-                                    bcc: Option[List[EmailAddress]],
-                                    textSignature: Option[TextSignature],
-                                    htmlSignature: Option[HtmlSignature]) {
+                                   email: MailAddress,
+                                   replyTo: Option[List[EmailAddress]],
+                                   bcc: Option[List[EmailAddress]],
+                                   textSignature: Option[TextSignature],
+                                   htmlSignature: Option[HtmlSignature]) {
   def asIdentity(id: IdentityId): Identity = Identity(id, name.getOrElse(IdentityName.DEFAULT), email, replyTo, bcc, textSignature.getOrElse(TextSignature.DEFAULT), htmlSignature.getOrElse(HtmlSignature.DEFAULT), mayDelete = MayDeleteIdentity(true))
 }
 
@@ -63,20 +64,31 @@ case class IdentityHtmlSignatureUpdate(htmlSignature: HtmlSignature) extends Ide
   override def update(identity: Identity): Identity = identity.copy(htmlSignature = htmlSignature)
 }
 
-case class IdentityUpdateRequest(name: Option[IdentityNameUpdate],
-                                 replyTo: Option[IdentityReplyToUpdate],
-                                 bcc: Option[IdentityBccUpdate],
-                                 textSignature: Option[IdentityTextSignatureUpdate],
-                                 htmlSignature: Option[IdentityHtmlSignatureUpdate]) extends IdentityUpdate {
+case class IdentityUpdateRequest(name: Option[IdentityNameUpdate] = None,
+                                 replyTo: Option[IdentityReplyToUpdate] = None,
+                                 bcc: Option[IdentityBccUpdate] = None,
+                                 textSignature: Option[IdentityTextSignatureUpdate] = None,
+                                 htmlSignature: Option[IdentityHtmlSignatureUpdate] = None) extends IdentityUpdate {
   def update(identity: Identity): Identity =
     List(name, replyTo, bcc, textSignature, htmlSignature)
       .flatten
       .foldLeft(identity)((acc, update) => update.update(acc))
+
+  def asCreationRequest(email: MailAddress): IdentityCreationRequest =
+    IdentityCreationRequest(
+      name = name.map(_.name),
+      email = email,
+      replyTo = replyTo.flatMap(_.replyTo),
+      bcc = bcc.flatMap(_.bcc),
+      textSignature = textSignature.map(_.textSignature),
+      htmlSignature = htmlSignature.map(_.htmlSignature))
 }
 
 trait CustomIdentityDAO {
   def save(user: Username, creationRequest: IdentityCreationRequest): Publisher[Identity]
 
+  def save(user: Username, identityId: IdentityId, creationRequest: IdentityCreationRequest): Publisher[Identity]
+
   def list(user: Username): Publisher[Identity]
 
   def update(user: Username, identityId: IdentityId, identityUpdate: IdentityUpdate): Publisher[Unit]
@@ -123,13 +135,32 @@ class IdentityRepository @Inject()(customIdentityDao: CustomIdentityDAO, identit
       SMono.error(ForbiddenSendFromException(creationRequest.email))
     }
 
-  def list(user: Username): Publisher[Identity] = SFlux.merge(Seq(
-    customIdentityDao.list(user),
-    SMono.fromCallable(() => identityFactory.listIdentities(user))
-      .subscribeOn(Schedulers.elastic())
-      .flatMapMany(SFlux.fromIterable)))
+  def list(user: Username): Publisher[Identity] = {
+    val customIdentities: SFlux[IdentityWithOrigin] = SFlux.fromPublisher(customIdentityDao.list(user))
+      .map(IdentityWithOrigin.fromCustom)
 
-  def update(user: Username, identityId: IdentityId, identityUpdate: IdentityUpdate): Publisher[Unit] = customIdentityDao.update(user, identityId, identityUpdate)
+    val serverSetIdentities: SFlux[IdentityWithOrigin] = SMono.fromCallable(() => identityFactory.listIdentities(user))
+      .subscribeOn(Schedulers.elastic())
+      .flatMapMany(SFlux.fromIterable)
+      .map(IdentityWithOrigin.fromServerSet)
+
+    SFlux.merge(Seq(customIdentities, serverSetIdentities))
+      .groupBy(_.identity.id)
+      .flatMap(_.reduce(IdentityWithOrigin.merge))
+      .map(_.identity)
+  }
+
+  def update(user: Username, identityId: IdentityId, identityUpdateRequest: IdentityUpdateRequest): Publisher[Unit] =
+    SMono.fromPublisher(customIdentityDao.update(user, identityId, identityUpdateRequest))
+      .onErrorResume {
+        case error: IdentityNotFoundException =>
+          SFlux.fromIterable(identityFactory.listIdentities(user))
+            .filter(identity => identity.id.equals(identityId))
+            .next()
+            .flatMap(identity => SMono.fromPublisher(customIdentityDao.save(user, identityId, identityUpdateRequest.asCreationRequest(identity.email))))
+            .switchIfEmpty(SMono.error(error))
+            .`then`()
+      }
 
   def delete(username: Username, ids: Seq[IdentityId]): Publisher[Unit] =
     SMono.just(ids)
@@ -145,4 +176,30 @@ class IdentityRepository @Inject()(customIdentityDao: CustomIdentityDAO, identit
 }
 
 case class IdentityNotFoundException(id: IdentityId) extends RuntimeException(s"$id could not be found")
-case class IdentityForbiddenDeleteException(id: IdentityId) extends IllegalArgumentException(s"User do not have permission to delete $id")
\ No newline at end of file
+case class IdentityForbiddenDeleteException(id: IdentityId) extends IllegalArgumentException(s"User do not have permission to delete $id")
+
+object IdentityWithOrigin {
+  sealed trait IdentityWithOrigin {
+    def identity: Identity
+
+    def merge(other: IdentityWithOrigin): IdentityWithOrigin
+  }
+
+  case class CustomIdentityOrigin(inputIdentity: Identity) extends IdentityWithOrigin {
+    override def identity: Identity = inputIdentity
+
+    override def merge(other: IdentityWithOrigin): IdentityWithOrigin = this
+  }
+
+  case class ServerSetIdentityOrigin(inputIdentity: Identity) extends IdentityWithOrigin {
+    override def identity: Identity = inputIdentity
+
+    override def merge(other: IdentityWithOrigin): IdentityWithOrigin = other
+  }
+
+  def fromCustom(identity: Identity): IdentityWithOrigin = CustomIdentityOrigin(identity)
+
+  def fromServerSet(identity: Identity): IdentityWithOrigin = ServerSetIdentityOrigin(identity)
+
+  def merge(value1: IdentityWithOrigin, value2: IdentityWithOrigin): IdentityWithOrigin = value1.merge(value2)
+}
\ No newline at end of file
diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/memory/identity/MemoryCustomIdentityDAO.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/memory/identity/MemoryCustomIdentityDAO.scala
index 3b7b5db..c5edbe6 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/memory/identity/MemoryCustomIdentityDAO.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/memory/identity/MemoryCustomIdentityDAO.scala
@@ -32,7 +32,10 @@ class MemoryCustomIdentityDAO extends CustomIdentityDAO {
   private val table: Table[Username, IdentityId, Identity] = HashBasedTable.create
 
   override def save(user: Username, creationRequest: IdentityCreationRequest): Publisher[Identity] =
-    SMono.fromCallable(() => IdentityId.generate)
+    save(user, IdentityId.generate, creationRequest)
+
+  override def save(user: Username, identityId: IdentityId, creationRequest: IdentityCreationRequest): Publisher[Identity] =
+    SMono.just(identityId)
       .map(creationRequest.asIdentity)
       .doOnNext(identity => table.put(user, identity.id, identity))
 
diff --git a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/IdentityRepositoryTest.scala b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/IdentityRepositoryTest.scala
new file mode 100644
index 0000000..e476396
--- /dev/null
+++ b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/IdentityRepositoryTest.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.api.identity
+
+import org.apache.james.core.{MailAddress, Username}
+import org.apache.james.jmap.api.identity.IdentityRepositoryTest.{BOB, CREATION_REQUEST, IDENTITY1, UPDATE_REQUEST}
+import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, ForbiddenSendFromException, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
+import org.apache.james.jmap.memory.identity.MemoryCustomIdentityDAO
+import org.assertj.core.api.Assertions.{assertThat, assertThatCode, assertThatThrownBy}
+import org.junit.jupiter.api.{BeforeEach, Test}
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.{mock, when}
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+import scala.jdk.CollectionConverters._
+
+object IdentityRepositoryTest {
+  val BOB: Username = Username.of("bob@domain.tld")
+
+  val CREATION_REQUEST: IdentityCreationRequest = IdentityCreationRequest(name = Some(IdentityName("Bob (custom address)")),
+    email = BOB.asMailAddress(),
+    replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss")), new MailAddress("boss@domain.tld")))),
+    bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2")), new MailAddress("boss2@domain.tld")))),
+    textSignature = Some(TextSignature("text signature")),
+    htmlSignature = Some(HtmlSignature("html signature")))
+
+  val UPDATE_REQUEST: IdentityUpdateRequest = IdentityUpdateRequest(
+    name = Some(IdentityNameUpdate(IdentityName("Bob (new name)"))),
+    replyTo = Some(IdentityReplyToUpdate(Some(List(EmailAddress(Some(EmailerName("My Boss (updated)")), new MailAddress("boss-updated@domain.tld")))))),
+    bcc = Some(IdentityBccUpdate(Some(List(EmailAddress(Some(EmailerName("My Boss 2 (updated)")), new MailAddress("boss-updated-2@domain.tld")))))),
+    textSignature = Some(IdentityTextSignatureUpdate(TextSignature("text 2 signature"))),
+    htmlSignature = Some(IdentityHtmlSignatureUpdate(HtmlSignature("html 2 signature"))))
+
+  val IDENTITY1: Identity = Identity(id = IdentityId.generate,
+    name = IdentityName(""),
+    email = BOB.asMailAddress(),
+    replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss 1")), new MailAddress("boss1@domain.tld")))),
+    bcc = Some(List(EmailAddress(Some(EmailerName("My Boss bcc 1")), new MailAddress("boss_bcc_1@domain.tld")))),
+    textSignature = TextSignature(""),
+    htmlSignature = HtmlSignature(""),
+    mayDelete = MayDeleteIdentity(false))
+
+  val IDENTITY2: Identity = Identity(id = IdentityId.generate,
+    name = IdentityName(""),
+    email = BOB.asMailAddress(),
+    replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss")), new MailAddress("boss@domain.tld")))),
+    bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2")), new MailAddress("boss2@domain.tld")))),
+    textSignature = TextSignature("text signature"),
+    htmlSignature = HtmlSignature("html signature"),
+    mayDelete = MayDeleteIdentity(true))
+
+}
+
+class IdentityRepositoryTest {
+
+  var testee: IdentityRepository = _
+  var identityFactory: DefaultIdentitySupplier = _
+  var customIdentityDAO: CustomIdentityDAO = _
+
+
+  @BeforeEach
+  def setUp(): Unit = {
+    customIdentityDAO = new MemoryCustomIdentityDAO()
+    identityFactory = mock(classOf[DefaultIdentitySupplier])
+    testee = new IdentityRepository(customIdentityDAO, identityFactory)
+  }
+
+  @Test
+  def saveShouldSuccess(): Unit = {
+    when(identityFactory.userCanSendFrom(any(), any())).thenReturn(true)
+    assertThatCode(() => SMono.fromPublisher(testee.save(BOB, CREATION_REQUEST)).block())
+      .doesNotThrowAnyException()
+  }
+
+  @Test
+  def saveShouldFailWhenUserCanNotSendFrom(): Unit = {
+    when(identityFactory.userCanSendFrom(any(), any())).thenReturn(false)
+    assertThatThrownBy(() => SMono.fromPublisher(testee.save(BOB, CREATION_REQUEST)).block())
+      .isInstanceOf(classOf[ForbiddenSendFromException])
+  }
+
+  @Test
+  def listShouldReturnCustomAndServerSetEntries(): Unit = {
+    val customIdentity: Identity = SMono.fromPublisher(customIdentityDAO.save(BOB, CREATION_REQUEST)).block()
+    when(identityFactory.listIdentities(BOB)).thenReturn(List(IDENTITY1))
+
+    assertThat(SFlux.fromPublisher(testee.list(BOB)).collectSeq().block().asJava)
+      .containsExactlyInAnyOrder(IDENTITY1, customIdentity)
+  }
+
+  @Test
+  def listShouldReturnCustomEntryWhenIdExistsInBothCustomAndServerSet(): Unit = {
+    val customIdentity: Identity = SMono.fromPublisher(customIdentityDAO.save(BOB, CREATION_REQUEST)).block()
+    val differentIdentityWithSameId: Identity = customIdentity.copy(name = IdentityName("different name"))
+    when(identityFactory.listIdentities(BOB)).thenReturn(List(differentIdentityWithSameId))
+
+    assertThat(SFlux.fromPublisher(testee.list(BOB)).collectSeq().block().asJava)
+      .containsExactlyInAnyOrder(customIdentity)
+  }
+
+  @Test
+  def updateShouldFailWhenIdNotFoundInBothCustomAndServerSetDAO(): Unit = {
+    when(identityFactory.listIdentities(BOB)).thenReturn(List())
+
+    assertThatThrownBy(() => SMono.fromPublisher(testee.update(BOB, IdentityId.generate, UPDATE_REQUEST)).block())
+      .isInstanceOf(classOf[IdentityNotFoundException])
+  }
+
+  @Test
+  def updateShouldSuccessWhenCustomNotFoundAndServerSetExists(): Unit = {
+    when(identityFactory.listIdentities(BOB)).thenReturn(List(IDENTITY1))
+
+    assertThatCode(() => SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, UPDATE_REQUEST)).block())
+      .doesNotThrowAnyException()
+  }
+
+  @Test
+  def updateShouldSuccessWhenCustomExists(): Unit = {
+    when(identityFactory.listIdentities(BOB)).thenReturn(List())
+    val customIdentity: Identity = SMono.fromPublisher(customIdentityDAO.save(BOB, CREATION_REQUEST)).block()
+
+    assertThatCode(() => SMono.fromPublisher(testee.update(BOB, customIdentity.id, UPDATE_REQUEST)).block())
+      .doesNotThrowAnyException()
+  }
+
+  @Test
+  def updateShouldModifiedEntry(): Unit = {
+    when(identityFactory.listIdentities(BOB)).thenReturn(List())
+    val customIdentity: Identity = SMono.fromPublisher(customIdentityDAO.save(BOB, CREATION_REQUEST)).block()
+
+    assertThatCode(() => SMono.fromPublisher(testee.update(BOB, customIdentity.id, UPDATE_REQUEST)).block())
+      .doesNotThrowAnyException()
+
+    assertThat(SFlux(testee.list(BOB)).collectSeq().block().asJava)
+      .containsExactlyInAnyOrder(Identity(id = customIdentity.id,
+        name = IdentityName("Bob (new name)"),
+        email = BOB.asMailAddress(),
+        replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss (updated)")), new MailAddress("boss-updated@domain.tld")))),
+        bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2 (updated)")), new MailAddress("boss-updated-2@domain.tld")))),
+        textSignature = TextSignature("text 2 signature"),
+        htmlSignature = HtmlSignature("html 2 signature"),
+        mayDelete = MayDeleteIdentity(true)))
+  }
+
+  @Test
+  def updateShouldSuccessWhenMultiUpdateServerSetId(): Unit = {
+    when(identityFactory.listIdentities(BOB)).thenReturn(List(IDENTITY1))
+    SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, UPDATE_REQUEST)).block()
+
+    assertThatCode(() => SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, UPDATE_REQUEST.copy(name = Some(IdentityNameUpdate(IdentityName("Bob (3)")))))).block())
+      .doesNotThrowAnyException()
+
+    assertThat(SFlux(testee.list(BOB)).collectSeq().block().asJava)
+      .containsExactlyInAnyOrder(Identity(id = IDENTITY1.id,
+        name = IdentityName("Bob (3)"),
+        email = BOB.asMailAddress(),
+        replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss (updated)")), new MailAddress("boss-updated@domain.tld")))),
+        bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2 (updated)")), new MailAddress("boss-updated-2@domain.tld")))),
+        textSignature = TextSignature("text 2 signature"),
+        htmlSignature = HtmlSignature("html 2 signature"),
+        mayDelete = MayDeleteIdentity(true)))
+  }
+
+  @Test
+  def updateShouldSuccessWhenSecondPartialUpdateServerSetId(): Unit = {
+    when(identityFactory.listIdentities(BOB)).thenReturn(List(IDENTITY1))
+    SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, UPDATE_REQUEST)).block()
+    val secondUpdateWithName: IdentityUpdateRequest = IdentityUpdateRequest(name = Some(IdentityNameUpdate(name = IdentityName("Bob (3)"))))
+
+    assertThatCode(() => SMono.fromPublisher(testee.update(BOB, IDENTITY1.id, secondUpdateWithName)).block())
+      .doesNotThrowAnyException()
+
+    assertThat(SFlux(testee.list(BOB)).collectSeq().block().asJava)
+      .containsExactlyInAnyOrder(Identity(id = IDENTITY1.id,
+        name = IdentityName("Bob (3)"),
+        email = BOB.asMailAddress(),
+        replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss (updated)")), new MailAddress("boss-updated@domain.tld")))),
+        bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 2 (updated)")), new MailAddress("boss-updated-2@domain.tld")))),
+        textSignature = TextSignature("text 2 signature"),
+        htmlSignature = HtmlSignature("html 2 signature"),
+        mayDelete = MayDeleteIdentity(true)))
+  }
+}
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/IdentityGetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
index 69219e0..dadfbb6 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentityGetContract.scala
@@ -26,8 +26,11 @@ import com.google.inject.multibindings.Multibinder
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.http.ContentType.JSON
+
 import javax.inject.Inject
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
+import net.javacrumbs.jsonunit.core.internal.Options
 import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
 import org.apache.james.core.{MailAddress, Username}
@@ -198,6 +201,7 @@ trait IdentityGetContract {
       .asString
 
     assertThatJson(response)
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
       .inPath("methodResponses[0][1]")
       .isEqualTo(
       s"""{
@@ -300,6 +304,7 @@ trait IdentityGetContract {
       .asString
 
     assertThatJson(response)
+      .withOptions(new Options(IGNORING_ARRAY_ORDER))
       .inPath("methodResponses[0][1]")
       .isEqualTo(
       s"""{
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/IdentitySetContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
index f628c06..c2d0ec0 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/IdentitySetContract.scala
@@ -212,7 +212,6 @@ trait IdentitySetContract {
          |}""".stripMargin
 
     val response =  `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(request)
     .when
       .post
@@ -1164,7 +1163,6 @@ trait IdentitySetContract {
 
   private def createNewIdentity(clientId: String): String =
     `given`
-      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
       .body(
         s"""{
            |	"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
@@ -1539,4 +1537,134 @@ trait IdentitySetContract {
            |}""".stripMargin)
   }
 
+
+  @Test
+  def updateShouldAcceptServerSetId(): Unit = {
+    val identityId: String = `given`
+      .body(
+        s"""{
+           |	"using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:submission"],
+           |	"methodCalls": [
+           |		["Identity/get",
+           |			{
+           |				"accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |				"ids": null
+           |			}, "c2"
+           |		]
+           |
+           |	]
+           |}""".stripMargin)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .extract()
+      .jsonPath()
+      .get("methodResponses[0][1].list[0].id")
+
+    val response: String = `given`
+      .body(
+        s"""{
+           |    "using": [
+           |        "urn:ietf:params:jmap:core",
+           |        "urn:ietf:params:jmap:submission"
+           |    ],
+           |    "methodCalls": [
+           |        [
+           |            "Identity/set",
+           |            {
+           |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |                "update": {
+           |                    "$identityId": {
+           |                        "name": "NewName1",
+           |                        "replyTo": [
+           |                            {
+           |                                "name": "Difference Alice",
+           |                                "email": "alice2@domain.tld"
+           |                            }
+           |                        ],
+           |                        "bcc": [
+           |                            {
+           |                                "name": "Difference David",
+           |                                "email": "david2@domain.tld"
+           |                            }
+           |                        ],
+           |                        "textSignature": "Difference text signature",
+           |                        "htmlSignature": "<p>Difference html signature</p>"
+           |                    }
+           |                }
+           |            },
+           |            "c1"
+           |        ],
+           |        [
+           |            "Identity/get",
+           |            {
+           |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |                "ids": [
+           |                    "$identityId"
+           |                ]
+           |            },
+           |            "c2"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "Identity/set",
+           |            {
+           |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |                "newState": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |                "updated": {
+           |                    "$identityId": { }
+           |                }
+           |            },
+           |            "c1"
+           |        ],
+           |        [
+           |            "Identity/get",
+           |            {
+           |                "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+           |                "state": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+           |                "list": [
+           |                    {
+           |                        "id": "$identityId",
+           |                        "name": "NewName1",
+           |                        "email": "bob@domain.tld",
+           |                        "replyTo": [
+           |                            {
+           |                                "name": "Difference Alice",
+           |                                "email": "alice2@domain.tld"
+           |                            }
+           |                        ],
+           |                        "bcc": [
+           |                            {
+           |                                "name": "Difference David",
+           |                                "email": "david2@domain.tld"
+           |                            }
+           |                        ],
+           |                        "textSignature": "Difference text signature",
+           |                        "htmlSignature": "<p>Difference html signature</p>",
+           |                        "mayDelete": true
+           |                    }
+           |                ]
+           |            },
+           |            "c2"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+  
 }

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