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]
