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

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

commit 85cd12424ca3c6b39a580cffacdc227cb328c8bb
Author: Benoit TELLIER <[email protected]>
AuthorDate: Thu Apr 30 09:59:16 2026 +0200

    JAMES-4203 Identity events serialization
---
 .../jmap/change/IdentityEventsSerializer.scala     | 156 +++++++++++++++++
 .../james/jmap/change/JmapEventSerializer.scala    |   7 +-
 .../jmap/change/IdentityEventsSerializerTest.scala | 187 +++++++++++++++++++++
 3 files changed, 349 insertions(+), 1 deletion(-)

diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/IdentityEventsSerializer.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/IdentityEventsSerializer.scala
new file mode 100644
index 0000000000..0615e41033
--- /dev/null
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/IdentityEventsSerializer.scala
@@ -0,0 +1,156 @@
+/****************************************************************
+ * 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.change
+
+import java.util.{Optional, UUID}
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import org.apache.james.core.{MailAddress, Username}
+import org.apache.james.events.Event.EventId
+import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, 
HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, 
TextSignature}
+import org.apache.james.jmap.mail.{AllCustomIdentitiesDeleted, 
CustomIdentityCreated, CustomIdentityDeleted, CustomIdentityUpdated}
+
+import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
+
+case class EmailAddressDTO(@JsonProperty("name") getName: Optional[String],
+                           @JsonProperty("email") getEmail: String)
+
+case class IdentityDTO(@JsonProperty("id") getId: String,
+                       @JsonProperty("sortOrder") getSortOrder: Int,
+                       @JsonProperty("name") getName: String,
+                       @JsonProperty("email") getEmail: String,
+                       @JsonProperty("replyTo") getReplyTo: 
Optional[java.util.List[EmailAddressDTO]],
+                       @JsonProperty("bcc") getBcc: 
Optional[java.util.List[EmailAddressDTO]],
+                       @JsonProperty("textSignature") getTextSignature: String,
+                       @JsonProperty("htmlSignature") getHtmlSignature: String,
+                       @JsonProperty("mayDelete") getMayDelete: Boolean)
+
+case class CustomIdentityCreatedDTO(@JsonProperty("type") getType: String,
+                                    @JsonProperty("eventId") getEventId: 
String,
+                                    @JsonProperty("username") getUsername: 
String,
+                                    @JsonProperty("identity") getIdentity: 
IdentityDTO) extends EventDTO
+
+case class CustomIdentityUpdatedDTO(@JsonProperty("type") getType: String,
+                                    @JsonProperty("eventId") getEventId: 
String,
+                                    @JsonProperty("username") getUsername: 
String,
+                                    @JsonProperty("identity") getIdentity: 
IdentityDTO) extends EventDTO
+
+case class CustomIdentityDeletedDTO(@JsonProperty("type") getType: String,
+                                    @JsonProperty("eventId") getEventId: 
String,
+                                    @JsonProperty("username") getUsername: 
String,
+                                    @JsonProperty("identityIds") 
getIdentityIds: java.util.List[String]) extends EventDTO
+
+case class AllCustomIdentitiesDeletedDTO(@JsonProperty("type") getType: String,
+                                         @JsonProperty("eventId") getEventId: 
String,
+                                         @JsonProperty("username") 
getUsername: String,
+                                         @JsonProperty("identityIds") 
getIdentityIds: java.util.List[String]) extends EventDTO
+
+object IdentityEventsSerializer {
+  private def toEmailAddressDTO(ea: EmailAddress): EmailAddressDTO =
+    EmailAddressDTO(ea.name.map(_.value).toJava, ea.email.asString)
+
+  private def fromEmailAddressDTO(dto: EmailAddressDTO): EmailAddress =
+    EmailAddress(dto.getName.toScala.map(EmailerName(_)), new 
MailAddress(dto.getEmail))
+
+  def toIdentityDTO(identity: Identity): IdentityDTO =
+    IdentityDTO(
+      getId = identity.id.id.toString,
+      getSortOrder = identity.sortOrder,
+      getName = identity.name.name,
+      getEmail = identity.email.asString,
+      getReplyTo = 
identity.replyTo.map(_.map(toEmailAddressDTO).asJava).toJava,
+      getBcc = identity.bcc.map(_.map(toEmailAddressDTO).asJava).toJava,
+      getTextSignature = identity.textSignature.name,
+      getHtmlSignature = identity.htmlSignature.name,
+      getMayDelete = identity.mayDelete.value)
+
+  def fromIdentityDTO(dto: IdentityDTO): Identity =
+    Identity(
+      id = IdentityId(UUID.fromString(dto.getId)),
+      sortOrder = dto.getSortOrder,
+      name = IdentityName(dto.getName),
+      email = new MailAddress(dto.getEmail),
+      replyTo = 
dto.getReplyTo.toScala.map(_.asScala.toList.map(fromEmailAddressDTO)),
+      bcc = dto.getBcc.toScala.map(_.asScala.toList.map(fromEmailAddressDTO)),
+      textSignature = TextSignature(dto.getTextSignature),
+      htmlSignature = HtmlSignature(dto.getHtmlSignature),
+      mayDelete = MayDeleteIdentity(dto.getMayDelete))
+
+  val customIdentityCreatedModule: EventDTOModule[CustomIdentityCreated, 
CustomIdentityCreatedDTO] =
+    EventDTOModule.forEvent(classOf[CustomIdentityCreated])
+      .convertToDTO(classOf[CustomIdentityCreatedDTO])
+      .toDomainObjectConverter(dto => CustomIdentityCreated(
+        eventId = EventId.of(dto.getEventId),
+        username = Username.of(dto.getUsername),
+        identity = fromIdentityDTO(dto.getIdentity)))
+      .toDTOConverter((event, _) => CustomIdentityCreatedDTO(
+        getType = classOf[CustomIdentityCreated].getCanonicalName,
+        getEventId = event.eventId.getId.toString,
+        getUsername = event.username.asString,
+        getIdentity = toIdentityDTO(event.identity)))
+      .typeName(classOf[CustomIdentityCreated].getCanonicalName)
+      .withFactory(EventDTOModule.apply)
+
+  val customIdentityUpdatedModule: EventDTOModule[CustomIdentityUpdated, 
CustomIdentityUpdatedDTO] =
+    EventDTOModule.forEvent(classOf[CustomIdentityUpdated])
+      .convertToDTO(classOf[CustomIdentityUpdatedDTO])
+      .toDomainObjectConverter(dto => CustomIdentityUpdated(
+        eventId = EventId.of(dto.getEventId),
+        username = Username.of(dto.getUsername),
+        identity = fromIdentityDTO(dto.getIdentity)))
+      .toDTOConverter((event, _) => CustomIdentityUpdatedDTO(
+        getType = classOf[CustomIdentityUpdated].getCanonicalName,
+        getEventId = event.eventId.getId.toString,
+        getUsername = event.username.asString,
+        getIdentity = toIdentityDTO(event.identity)))
+      .typeName(classOf[CustomIdentityUpdated].getCanonicalName)
+      .withFactory(EventDTOModule.apply)
+
+  val customIdentityDeletedModule: EventDTOModule[CustomIdentityDeleted, 
CustomIdentityDeletedDTO] =
+    EventDTOModule.forEvent(classOf[CustomIdentityDeleted])
+      .convertToDTO(classOf[CustomIdentityDeletedDTO])
+      .toDomainObjectConverter(dto => CustomIdentityDeleted(
+        eventId = EventId.of(dto.getEventId),
+        username = Username.of(dto.getUsername),
+        identityIds = dto.getIdentityIds.asScala.map(id => 
IdentityId(UUID.fromString(id))).toSet))
+      .toDTOConverter((event, _) => CustomIdentityDeletedDTO(
+        getType = classOf[CustomIdentityDeleted].getCanonicalName,
+        getEventId = event.eventId.getId.toString,
+        getUsername = event.username.asString,
+        getIdentityIds = event.identityIds.map(_.id.toString).toList.asJava))
+      .typeName(classOf[CustomIdentityDeleted].getCanonicalName)
+      .withFactory(EventDTOModule.apply)
+
+  val allCustomIdentitiesDeletedModule: 
EventDTOModule[AllCustomIdentitiesDeleted, AllCustomIdentitiesDeletedDTO] =
+    EventDTOModule.forEvent(classOf[AllCustomIdentitiesDeleted])
+      .convertToDTO(classOf[AllCustomIdentitiesDeletedDTO])
+      .toDomainObjectConverter(dto => AllCustomIdentitiesDeleted(
+        eventId = EventId.of(dto.getEventId),
+        username = Username.of(dto.getUsername),
+        identityIds = dto.getIdentityIds.asScala.map(id => 
IdentityId(UUID.fromString(id))).toSet))
+      .toDTOConverter((event, _) => AllCustomIdentitiesDeletedDTO(
+        getType = classOf[AllCustomIdentitiesDeleted].getCanonicalName,
+        getEventId = event.eventId.getId.toString,
+        getUsername = event.username.asString,
+        getIdentityIds = event.identityIds.map(_.id.toString).toList.asJava))
+      .typeName(classOf[AllCustomIdentitiesDeleted].getCanonicalName)
+      .withFactory(EventDTOModule.apply)
+}
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
index 85217a574c..e615fe2761 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
@@ -81,7 +81,12 @@ case class StateChangeEventDTO(@JsonProperty("type") 
getType: String,
 
 case class JmapEventSerializer @Inject()(stateChangeEventDTOFactory: 
StateChangeEventDTOFactory) extends EventSerializer {
   private val genericSerializer: JsonGenericSerializer[Event, EventDTO] = 
JsonGenericSerializer
-    
.forModules(stateChangeEventDTOFactory.dtoModule.asInstanceOf[EventDTOModule[Event,
 EventDTO]])
+    .forModules(
+      stateChangeEventDTOFactory.dtoModule.asInstanceOf[EventDTOModule[Event, 
EventDTO]],
+      
IdentityEventsSerializer.customIdentityCreatedModule.asInstanceOf[EventDTOModule[Event,
 EventDTO]],
+      
IdentityEventsSerializer.customIdentityUpdatedModule.asInstanceOf[EventDTOModule[Event,
 EventDTO]],
+      
IdentityEventsSerializer.customIdentityDeletedModule.asInstanceOf[EventDTOModule[Event,
 EventDTO]],
+      
IdentityEventsSerializer.allCustomIdentitiesDeletedModule.asInstanceOf[EventDTOModule[Event,
 EventDTO]])
     .withoutNestedType()
 
   override def toJson(event: Event): SerializationResult = 
SerializationResult.of(
diff --git 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/IdentityEventsSerializerTest.scala
 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/IdentityEventsSerializerTest.scala
new file mode 100644
index 0000000000..9f9a53fb1c
--- /dev/null
+++ 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/IdentityEventsSerializerTest.scala
@@ -0,0 +1,187 @@
+/****************************************************************
+ * 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.change
+
+import java.util.UUID
+
+import org.apache.james.JsonSerializationVerifier
+import org.apache.james.core.{MailAddress, Username}
+import org.apache.james.events.Event.EventId
+import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, 
HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, 
TextSignature}
+import org.apache.james.jmap.change.IdentityEventsSerializerTest._
+import org.apache.james.jmap.mail.{AllCustomIdentitiesDeleted, 
CustomIdentityCreated, CustomIdentityDeleted, CustomIdentityUpdated}
+import org.apache.james.json.JsonGenericSerializer
+import org.apache.james.json.JsonGenericSerializer.UnknownTypeException
+import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.junit.jupiter.api.Test
+
+object IdentityEventsSerializerTest {
+  val EVENT_ID: EventId = EventId.of("6e0dd59d-660e-4d9b-b22f-0354479f47b4")
+  val USERNAME: Username = Username.of("bob")
+  val IDENTITY_ID: IdentityId = 
IdentityId(UUID.fromString("2c9f1b12-b35a-43e6-9af2-0106fb53a943"))
+  val IDENTITY_ID_2: IdentityId = 
IdentityId(UUID.fromString("3d9f1b12-b35a-43e6-9af2-0106fb53a943"))
+
+  val IDENTITY: Identity = Identity(
+    id = IDENTITY_ID,
+    sortOrder = 100,
+    name = IdentityName("Bob"),
+    email = new MailAddress("[email protected]"),
+    replyTo = Some(List(EmailAddress(None, new 
MailAddress("[email protected]")))),
+    bcc = Some(List(EmailAddress(Some(EmailerName("Admin")), new 
MailAddress("[email protected]")))),
+    textSignature = TextSignature("Best regards"),
+    htmlSignature = HtmlSignature("<b>Best regards</b>"),
+    mayDelete = MayDeleteIdentity(true))
+
+  val IDENTITY_NO_OPTIONAL: Identity = Identity(
+    id = IDENTITY_ID,
+    sortOrder = 100,
+    name = IdentityName("Bob"),
+    email = new MailAddress("[email protected]"),
+    replyTo = None,
+    bcc = None,
+    textSignature = TextSignature(""),
+    htmlSignature = HtmlSignature(""),
+    mayDelete = MayDeleteIdentity(false))
+
+  val CREATED_EVENT: CustomIdentityCreated = CustomIdentityCreated(EVENT_ID, 
USERNAME, IDENTITY)
+  val CREATED_EVENT_JSON: String =
+    """{
+      |  "type": "org.apache.james.jmap.mail.CustomIdentityCreated",
+      |  "eventId": "6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+      |  "username": "bob",
+      |  "identity": {
+      |    "id": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+      |    "sortOrder": 100,
+      |    "name": "Bob",
+      |    "email": "[email protected]",
+      |    "replyTo": [{"email": "[email protected]"}],
+      |    "bcc": [{"name": "Admin", "email": "[email protected]"}],
+      |    "textSignature": "Best regards",
+      |    "htmlSignature": "<b>Best regards</b>",
+      |    "mayDelete": true
+      |  }
+      |}""".stripMargin
+
+  val CREATED_EVENT_NO_OPTIONAL: CustomIdentityCreated = 
CustomIdentityCreated(EVENT_ID, USERNAME, IDENTITY_NO_OPTIONAL)
+  val CREATED_EVENT_NO_OPTIONAL_JSON: String =
+    """{
+      |  "type": "org.apache.james.jmap.mail.CustomIdentityCreated",
+      |  "eventId": "6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+      |  "username": "bob",
+      |  "identity": {
+      |    "id": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+      |    "sortOrder": 100,
+      |    "name": "Bob",
+      |    "email": "[email protected]",
+      |    "textSignature": "",
+      |    "htmlSignature": "",
+      |    "mayDelete": false
+      |  }
+      |}""".stripMargin
+
+  val UPDATED_EVENT: CustomIdentityUpdated = CustomIdentityUpdated(EVENT_ID, 
USERNAME, IDENTITY)
+  val UPDATED_EVENT_JSON: String =
+    """{
+      |  "type": "org.apache.james.jmap.mail.CustomIdentityUpdated",
+      |  "eventId": "6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+      |  "username": "bob",
+      |  "identity": {
+      |    "id": "2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+      |    "sortOrder": 100,
+      |    "name": "Bob",
+      |    "email": "[email protected]",
+      |    "replyTo": [{"email": "[email protected]"}],
+      |    "bcc": [{"name": "Admin", "email": "[email protected]"}],
+      |    "textSignature": "Best regards",
+      |    "htmlSignature": "<b>Best regards</b>",
+      |    "mayDelete": true
+      |  }
+      |}""".stripMargin
+
+  val DELETED_EVENT: CustomIdentityDeleted = CustomIdentityDeleted(EVENT_ID, 
USERNAME, Set(IDENTITY_ID))
+  val DELETED_EVENT_JSON: String =
+    """{
+      |  "type": "org.apache.james.jmap.mail.CustomIdentityDeleted",
+      |  "eventId": "6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+      |  "username": "bob",
+      |  "identityIds": ["2c9f1b12-b35a-43e6-9af2-0106fb53a943"]
+      |}""".stripMargin
+
+  val ALL_DELETED_EVENT: AllCustomIdentitiesDeleted = 
AllCustomIdentitiesDeleted(EVENT_ID, USERNAME, Set(IDENTITY_ID))
+  val ALL_DELETED_EVENT_JSON: String =
+    """{
+      |  "type": "org.apache.james.jmap.mail.AllCustomIdentitiesDeleted",
+      |  "eventId": "6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+      |  "username": "bob",
+      |  "identityIds": ["2c9f1b12-b35a-43e6-9af2-0106fb53a943"]
+      |}""".stripMargin
+}
+
+class IdentityEventsSerializerTest {
+  @Test
+  def shouldSerializeCustomIdentityCreated(): Unit =
+    
JsonSerializationVerifier.dtoModule(IdentityEventsSerializer.customIdentityCreatedModule)
+      .bean(CREATED_EVENT)
+      .json(CREATED_EVENT_JSON)
+      .verify()
+
+  @Test
+  def shouldSerializeCustomIdentityCreatedWithoutOptionalFields(): Unit =
+    
JsonSerializationVerifier.dtoModule(IdentityEventsSerializer.customIdentityCreatedModule)
+      .bean(CREATED_EVENT_NO_OPTIONAL)
+      .json(CREATED_EVENT_NO_OPTIONAL_JSON)
+      .verify()
+
+  @Test
+  def shouldSerializeCustomIdentityUpdated(): Unit =
+    
JsonSerializationVerifier.dtoModule(IdentityEventsSerializer.customIdentityUpdatedModule)
+      .bean(UPDATED_EVENT)
+      .json(UPDATED_EVENT_JSON)
+      .verify()
+
+  @Test
+  def shouldSerializeCustomIdentityDeleted(): Unit =
+    
JsonSerializationVerifier.dtoModule(IdentityEventsSerializer.customIdentityDeletedModule)
+      .bean(DELETED_EVENT)
+      .json(DELETED_EVENT_JSON)
+      .verify()
+
+  @Test
+  def shouldSerializeAllCustomIdentitiesDeleted(): Unit =
+    
JsonSerializationVerifier.dtoModule(IdentityEventsSerializer.allCustomIdentitiesDeletedModule)
+      .bean(ALL_DELETED_EVENT)
+      .json(ALL_DELETED_EVENT_JSON)
+      .verify()
+
+  @Test
+  def shouldThrowWhenDeserializeUnknownEventType(): Unit =
+    assertThatThrownBy(() =>
+      JsonGenericSerializer
+        .forModules(IdentityEventsSerializer.customIdentityCreatedModule)
+        .withoutNestedType()
+        .deserialize(
+          """{
+            |  "type": "org.apache.james.jmap.mail.UnknownEvent",
+            |  "eventId": "6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+            |  "username": "bob",
+            |  "identityIds": []
+            |}""".stripMargin))
+      .isInstanceOf(classOf[UnknownTypeException])
+}


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

Reply via email to