From e0189d180d214e25f60caf0d54bb292d03e64abe Mon Sep 17 00:00:00 2001 From: Quan Tran Date: Mon, 15 Nov 2021 09:41:53 +0700 Subject: [PATCH 1/3] JAMES-3534 Cassandra implement for CustomIdentityDAO --- server/data/data-jmap-cassandra/pom.xml | 4 + .../identity/CassandraCustomIdentityDAO.scala | 142 ++++++++++++++++++ .../CassandraCustomIdentityModule.scala | 48 ++++++ .../tables/CassandraCustomIdentityTable.scala | 39 +++++ .../utils/EmailAddressTupleUtil.scala | 36 +++++ .../identity/CassandraCustomIdentityTest.java | 49 ++++++ .../identity/CustomIdentityDAOContract.scala | 5 +- 7 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala create mode 100644 server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityModule.scala create mode 100644 server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/tables/CassandraCustomIdentityTable.scala create mode 100644 server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/utils/EmailAddressTupleUtil.scala create mode 100644 server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityTest.java diff --git a/server/data/data-jmap-cassandra/pom.xml b/server/data/data-jmap-cassandra/pom.xml index e5b0db70477..b21712a5370 100644 --- a/server/data/data-jmap-cassandra/pom.xml +++ b/server/data/data-jmap-cassandra/pom.xml @@ -141,6 +141,10 @@ + + net.alchim31.maven + scala-maven-plugin + org.apache.maven.plugins maven-surefire-plugin 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 new file mode 100644 index 00000000000..1e7f347a412 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityDAO.scala @@ -0,0 +1,142 @@ +/** ************************************************************** + * 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.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} +import org.apache.james.jmap.api.identity.{CustomIdentityDAO, IdentityCreationRequest, IdentityNotFoundException, IdentityUpdate} +import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature} +import org.apache.james.jmap.cassandra.identity.tables.CassandraCustomIdentityTable +import org.apache.james.jmap.cassandra.identity.tables.CassandraCustomIdentityTable.{BCC, EMAIL, HTML_SIGNATURE, ID, MAY_DELETE, NAME, REPLY_TO, TABLE_NAME, TEXT_SIGNATURE, USER} +import org.apache.james.jmap.cassandra.utils.EmailAddressTupleUtil +import reactor.core.publisher.Mono +import reactor.core.scala.publisher.{SFlux, SMono} + +import scala.jdk.javaapi.CollectionConverters + +case class CassandraCustomIdentityDAO @Inject()(session: Session, + typesProvider: CassandraTypesProvider) extends CustomIdentityDAO { + val executor: CassandraAsyncExecutor = new CassandraAsyncExecutor(session) + val emailAddressTupleUtil: EmailAddressTupleUtil = EmailAddressTupleUtil(typesProvider) + + val insertStatement: PreparedStatement = session.prepare(insertInto(TABLE_NAME) + .value(USER, bindMarker(USER)) + .value(ID, bindMarker(ID)) + .value(NAME, bindMarker(NAME)) + .value(EMAIL, bindMarker(EMAIL)) + .value(REPLY_TO, bindMarker(REPLY_TO)) + .value(BCC, bindMarker(BCC)) + .value(TEXT_SIGNATURE, bindMarker(TEXT_SIGNATURE)) + .value(HTML_SIGNATURE, bindMarker(HTML_SIGNATURE)) + .value(MAY_DELETE, bindMarker(MAY_DELETE))) + + val selectAllStatement: PreparedStatement = session.prepare(select() + .from(TABLE_NAME) + .where(QueryBuilder.eq(USER, bindMarker(USER)))) + + val selectOneStatement: PreparedStatement = session.prepare(select() + .from(TABLE_NAME) + .where(QueryBuilder.eq(USER, bindMarker(USER))) + .and(QueryBuilder.eq(ID, bindMarker(ID)))) + + val deleteOneStatement: PreparedStatement = session.prepare(QueryBuilder.delete() + .from(TABLE_NAME) + .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) + .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())) + .map(toIdentity(_))) + + override def update(user: Username, identityId: IdentityId, identityUpdate: IdentityUpdate): SMono[Unit] = + SMono.fromPublisher(executor.executeSingleRow(selectOneStatement.bind().setString(USER, user.asString()) + .setUUID(ID, identityId.id)) + .switchIfEmpty(Mono.error(() => IdentityNotFoundException(identityId))) + .map(toIdentity) + .map(identityUpdate.update) + .flatMap(patch => insert(user, patch).`then`().asJava())) + + override def delete(username: Username, ids: Seq[IdentityId]): SMono[Unit] = + SFlux.fromIterable(ids) + .flatMap(id => executor.executeVoid(deleteOneStatement.bind() + .setString(USER, username.asString()) + .setUUID(ID, id.id))) + .`then`() + + private def insert(username: Username, identity: Identity): SMono[Identity] = { + val insertIdentity: BoundStatement = insertStatement.bind() + .setString(USER, username.asString()) + .setUUID(ID, identity.id.id) + .setString(NAME, identity.name.name) + .setString(EMAIL, identity.email.asString()) + .setString(TEXT_SIGNATURE, identity.textSignature.name) + .setString(HTML_SIGNATURE, identity.htmlSignature.name) + .setBool(MAY_DELETE, identity.mayDelete.value) + + identity.replyTo + .map(listEmailAddress => insertIdentity.setSet(REPLY_TO, toJavaSet(listEmailAddress))) + identity.bcc + .map(listEmailAddress => insertIdentity.setSet(BCC, toJavaSet(listEmailAddress))) + + SMono.fromPublisher(executor.executeVoid(insertIdentity) + .thenReturn(identity)) + } + + private def toIdentity(row: Row): Identity = + Identity(IdentityId(row.getUUID(ID)), + IdentityName(row.getString(NAME)), + new MailAddress(row.getString(EMAIL)), + toReplyTo(row), + toBcc(row), + TextSignature(row.getString(TEXT_SIGNATURE)), + HtmlSignature(row.getString(HTML_SIGNATURE)), + MayDeleteIdentity(row.getBool(MAY_DELETE))) + + private def toReplyTo(row: Row): Option[List[EmailAddress]] = + Option(CollectionConverters.asScala(row.getSet(REPLY_TO, classOf[UDTValue])) + .toList + .map(toEmailAddress)) + + private def toBcc(row: Row): Option[List[EmailAddress]] = + Option(CollectionConverters.asScala(row.getSet(BCC, classOf[UDTValue])) + .toList + .map(toEmailAddress)) + + private def toJavaSet(listEmailAddress: List[EmailAddress]): java.util.Set[UDTValue] = + CollectionConverters.asJava(listEmailAddress.map(emailAddress => + emailAddressTupleUtil.createEmailAddressUDT(emailAddress.name.map(name => name.value), emailAddress.email.asString())) + .toSet) + + private def toEmailAddress(udtValue: UDTValue): EmailAddress = + EmailAddress(name = Option(udtValue.getString(CassandraCustomIdentityTable.EmailAddress.NAME)).map(string => EmailerName(string)), + email = new MailAddress(udtValue.getString(CassandraCustomIdentityTable.EmailAddress.EMAIL))) +} \ No newline at end of file diff --git a/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityModule.scala b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityModule.scala new file mode 100644 index 00000000000..d5e06d834d2 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityModule.scala @@ -0,0 +1,48 @@ +/** ************************************************************** + * 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.cassandra.identity + +import com.datastax.driver.core.DataType.{cboolean, text, uuid} +import com.datastax.driver.core.schemabuilder.SchemaBuilder +import org.apache.james.backends.cassandra.components.CassandraModule +import org.apache.james.jmap.cassandra.identity.tables.CassandraCustomIdentityTable +import org.apache.james.jmap.cassandra.identity.tables.CassandraCustomIdentityTable.{BCC, EMAIL, EMAIL_ADDRESS, EmailAddress, HTML_SIGNATURE, ID, MAY_DELETE, NAME, REPLY_TO, TEXT_SIGNATURE, USER} + +object CassandraCustomIdentityModule { + val MODULE: CassandraModule = CassandraModule.builder() + .`type`(EMAIL_ADDRESS) + .statement(statement => statement + .addColumn(EmailAddress.NAME, text()) + .addColumn(EmailAddress.EMAIL, text())) + + .table(CassandraCustomIdentityTable.TABLE_NAME) + .comment("Hold user custom identities data following JMAP RFC-8621 Identity concept") + .statement(statement => statement + .addPartitionKey(USER, text()) + .addClusteringColumn(ID, uuid()) + .addColumn(NAME, text()) + .addColumn(EMAIL, text()) + .addUDTSetColumn(REPLY_TO, SchemaBuilder.frozen(EMAIL_ADDRESS)) + .addUDTSetColumn(BCC, SchemaBuilder.frozen(EMAIL_ADDRESS)) + .addColumn(TEXT_SIGNATURE, text()) + .addColumn(HTML_SIGNATURE, text()) + .addColumn(MAY_DELETE, cboolean())) + .build() +} \ No newline at end of file diff --git a/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/tables/CassandraCustomIdentityTable.scala b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/tables/CassandraCustomIdentityTable.scala new file mode 100644 index 00000000000..98e5f4a88b3 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/identity/tables/CassandraCustomIdentityTable.scala @@ -0,0 +1,39 @@ +/**************************************************************** + * 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.cassandra.identity.tables + +object CassandraCustomIdentityTable { + val TABLE_NAME: String = "custom_identity" + val USER: String = "user" + val ID: String = "id" + val NAME: String = "name" + val EMAIL: String = "email" + val REPLY_TO: String = "reply_to" + val BCC: String = "bcc" + val TEXT_SIGNATURE: String = "text_signature" + val HTML_SIGNATURE: String = "html_signature" + val MAY_DELETE: String = "may_delete" + val EMAIL_ADDRESS: String = "email_address" + + object EmailAddress { + val NAME: String = "name" + val EMAIL: String = "email" + } +} \ No newline at end of file diff --git a/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/utils/EmailAddressTupleUtil.scala b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/utils/EmailAddressTupleUtil.scala new file mode 100644 index 00000000000..f8f7c420715 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/scala/org/apache/james/jmap/cassandra/utils/EmailAddressTupleUtil.scala @@ -0,0 +1,36 @@ +/**************************************************************** + * 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.cassandra.utils + +import com.datastax.driver.core.UDTValue +import org.apache.james.backends.cassandra.init.CassandraTypesProvider +import org.apache.james.jmap.cassandra.identity.tables.CassandraCustomIdentityTable + +case class EmailAddressTupleUtil(typesProvider: CassandraTypesProvider) { + def createEmailAddressUDT(name: Option[String], email: String): UDTValue = { + val value = typesProvider.getDefinedUserType(CassandraCustomIdentityTable.EMAIL_ADDRESS) + .newValue() + .setString(CassandraCustomIdentityTable.EmailAddress.EMAIL, email) + + name.map(name => value.setString(CassandraCustomIdentityTable.EmailAddress.NAME, name)) + + value + } +} \ No newline at end of file diff --git a/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityTest.java b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityTest.java new file mode 100644 index 00000000000..4949a934b35 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/test/java/org/apache/james/jmap/cassandra/identity/CassandraCustomIdentityTest.java @@ -0,0 +1,49 @@ +/**************************************************************** + * 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.cassandra.identity; + +import org.apache.james.backends.cassandra.CassandraCluster; +import org.apache.james.backends.cassandra.CassandraClusterExtension; +import org.apache.james.backends.cassandra.components.CassandraModule; +import org.apache.james.backends.cassandra.versions.CassandraSchemaVersionModule; +import org.apache.james.jmap.api.identity.CustomIdentityDAO; +import org.apache.james.jmap.api.identity.CustomIdentityDAOContract; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.RegisterExtension; + +public class CassandraCustomIdentityTest implements CustomIdentityDAOContract { + private CassandraCustomIdentityDAO testee; + + @RegisterExtension + static CassandraClusterExtension cassandraCluster = new CassandraClusterExtension(CassandraModule.aggregateModules( + CassandraSchemaVersionModule.MODULE, + CassandraCustomIdentityModule.MODULE())); + + @BeforeEach + void setup(CassandraCluster cassandra) { + testee = new CassandraCustomIdentityDAO(cassandra.getConf(), + cassandra.getTypesProvider()); + } + + @Override + public CustomIdentityDAO testee() { + return testee; + } +} \ No newline at end of file diff --git a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/CustomIdentityDAOContract.scala b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/CustomIdentityDAOContract.scala index 2de96f18e7b..97c9443fe35 100644 --- a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/CustomIdentityDAOContract.scala +++ b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/CustomIdentityDAOContract.scala @@ -20,13 +20,16 @@ package org.apache.james.jmap.api.identity import org.apache.james.core.{MailAddress, Username} +import org.apache.james.jmap.api.identity.CustomIdentityDAOContract.bob import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature} import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy} import org.junit.jupiter.api.Test import reactor.core.scala.publisher.{SFlux, SMono} -trait CustomIdentityDAOContract { +object CustomIdentityDAOContract { private val bob = Username.of("bob@localhost") +} +trait CustomIdentityDAOContract { def testee(): CustomIdentityDAO From d87d923b1d495d9da8833cad20ee997ed422e17b Mon Sep 17 00:00:00 2001 From: Quan Tran Date: Mon, 15 Nov 2021 15:40:44 +0700 Subject: [PATCH 2/3] JAMES-3534 Identity name properties is always present --- .../jmap/api/identity/CustomIdentityDAO.scala | 4 +- .../james/jmap/api/model/Identity.scala | 3 ++ .../identity/CustomIdentityDAOContract.scala | 38 +++++++++++++++---- .../contract/IdentityGetContract.scala | 2 +- 4 files changed, 36 insertions(+), 11 deletions(-) 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 42ef1f265e2..027aea75cf5 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 @@ -34,13 +34,13 @@ import reactor.core.scheduler.Schedulers import scala.jdk.CollectionConverters._ import scala.util.Try -case class IdentityCreationRequest(name: IdentityName, +case class IdentityCreationRequest(name: Option[IdentityName], email: MailAddress, replyTo: Option[List[EmailAddress]], bcc: Option[List[EmailAddress]], textSignature: Option[TextSignature], htmlSignature: Option[HtmlSignature]) { - def asIdentity(id: IdentityId): Identity = Identity(id, name, email, replyTo, bcc, textSignature.getOrElse(TextSignature.DEFAULT), htmlSignature.getOrElse(HtmlSignature.DEFAULT), mayDelete = MayDeleteIdentity(true)) + 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)) } trait IdentityUpdate { diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/Identity.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/Identity.scala index db376471302..acf65b430e3 100644 --- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/Identity.scala +++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/Identity.scala @@ -23,6 +23,9 @@ import java.util.UUID import org.apache.james.core.MailAddress +object IdentityName { + val DEFAULT: IdentityName = IdentityName("") +} case class IdentityName(name: String) extends AnyVal object TextSignature { val DEFAULT: TextSignature = TextSignature("") diff --git a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/CustomIdentityDAOContract.scala b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/CustomIdentityDAOContract.scala index 97c9443fe35..531d483d196 100644 --- a/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/CustomIdentityDAOContract.scala +++ b/server/data/data-jmap/src/test/scala/org/apache/james/jmap/api/identity/CustomIdentityDAOContract.scala @@ -42,7 +42,7 @@ trait CustomIdentityDAOContract { @Test def listShouldReturnSavedIdentity(): Unit = { val identity = SMono(testee() - .save(bob, IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + .save(bob, 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")))), @@ -57,7 +57,7 @@ trait CustomIdentityDAOContract { @Test def listShouldReturnSavedIdentities(): Unit = { val identity1 = SMono(testee() - .save(bob, IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + .save(bob, 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")))), @@ -65,7 +65,7 @@ trait CustomIdentityDAOContract { htmlSignature = Some(HtmlSignature("html signature"))))) .block() val identity2 = SMono(testee() - .save(bob, IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + .save(bob, IdentityCreationRequest(name = Some(IdentityName("Bob (custom address)")), email = bob.asMailAddress(), replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss2")), new MailAddress("boss@domain.tld")))), bcc = Some(List(EmailAddress(Some(EmailerName("My Boss 3")), new MailAddress("boss2@domain.tld")))), @@ -80,7 +80,7 @@ trait CustomIdentityDAOContract { @Test def saveShouldReturnPersistedValues(): Unit = { val identity: Identity = SMono(testee().save(bob, - IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + 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")))), @@ -102,7 +102,7 @@ trait CustomIdentityDAOContract { @Test def saveShouldNotReturnDeletedValues(): Unit = { val identity: Identity = SMono(testee().save(bob, - IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + 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")))), @@ -116,10 +116,32 @@ trait CustomIdentityDAOContract { .isEmpty() } + @Test + def saveShouldDefineDefaultValuesInCaseSomePropertiesEmpty(): Unit = { + val identity: Identity = SMono(testee().save(bob, + IdentityCreationRequest(name = None, + 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 = None, + htmlSignature = None))) + .block() + + assertThat(identity) + .isEqualTo(Identity(id = identity.id, + 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(""), + htmlSignature = HtmlSignature(""), + mayDelete = MayDeleteIdentity(true))) + } + @Test def deleteShouldBeIdempotent(): Unit = { val identity: Identity = SMono(testee().save(bob, - IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + 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")))), @@ -137,7 +159,7 @@ trait CustomIdentityDAOContract { @Test def updateShouldModifyUnderlyingRecord(): Unit = { val identity: Identity = SMono(testee().save(bob, - IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + 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")))), @@ -167,7 +189,7 @@ trait CustomIdentityDAOContract { @Test def partialUpdatesShouldBePossible(): Unit = { val identity: Identity = SMono(testee().save(bob, - IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + 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")))), 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 3aaf7eeaa1a..ee6e70da33c 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 @@ -114,7 +114,7 @@ trait IdentityGetContract { @Test def getIdentityShouldReturnCustomIdentity(server: GuiceJamesServer): Unit = { val id = SMono(server.getProbe(classOf[IdentityProbe]) - .save(BOB, IdentityCreationRequest(name = IdentityName("Bob (custom address)"), + .save(BOB, 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")))), From 8e818682d847fe32171bc490bcf1e4e4b1589da2 Mon Sep 17 00:00:00 2001 From: Quan Tran Date: Mon, 15 Nov 2021 15:51:02 +0700 Subject: [PATCH 3/3] JAMES-3534 Plug CassandraCustomIdentityDAO to distributed server --- .../apache/james/modules/data/CassandraJmapModule.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java index 01a3f9e4303..0b95ff94f20 100644 --- a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java +++ b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java @@ -40,6 +40,8 @@ import org.apache.james.jmap.cassandra.change.CassandraEmailChangeModule; import org.apache.james.jmap.cassandra.change.CassandraMailboxChangeModule; import org.apache.james.jmap.cassandra.filtering.FilteringRuleSetDefineDTOModules; +import org.apache.james.jmap.cassandra.identity.CassandraCustomIdentityDAO; +import org.apache.james.jmap.cassandra.identity.CassandraCustomIdentityModule; import org.apache.james.jmap.cassandra.projections.CassandraEmailQueryView; import org.apache.james.jmap.cassandra.projections.CassandraEmailQueryViewModule; import org.apache.james.jmap.cassandra.projections.CassandraMessageFastViewProjection; @@ -54,7 +56,6 @@ import org.apache.james.jmap.cassandra.vacation.CassandraNotificationRegistryModule; import org.apache.james.jmap.cassandra.vacation.CassandraVacationModule; import org.apache.james.jmap.cassandra.vacation.CassandraVacationRepository; -import org.apache.james.jmap.memory.identity.MemoryCustomIdentityDAO; import com.google.inject.AbstractModule; import com.google.inject.Scopes; @@ -76,8 +77,8 @@ protected void configure() { bind(CassandraVacationRepository.class).in(Scopes.SINGLETON); bind(VacationRepository.class).to(CassandraVacationRepository.class); - bind(MemoryCustomIdentityDAO.class).in(Scopes.SINGLETON); - bind(CustomIdentityDAO.class).to(MemoryCustomIdentityDAO.class); + bind(CassandraCustomIdentityDAO.class).in(Scopes.SINGLETON); + bind(CustomIdentityDAO.class).to(CassandraCustomIdentityDAO.class); bind(CassandraNotificationRegistry.class).in(Scopes.SINGLETON); bind(NotificationRegistry.class).to(CassandraNotificationRegistry.class); @@ -108,6 +109,7 @@ protected void configure() { cassandraDataDefinitions.addBinding().toInstance(CassandraEmailChangeModule.MODULE); cassandraDataDefinitions.addBinding().toInstance(UploadModule.MODULE); cassandraDataDefinitions.addBinding().toInstance(CassandraPushSubscriptionModule.MODULE); + cassandraDataDefinitions.addBinding().toInstance(CassandraCustomIdentityModule.MODULE()); Multibinder> eventDTOModuleBinder = Multibinder.newSetBinder(binder(), new TypeLiteral>() {}); eventDTOModuleBinder.addBinding().toInstance(FilteringRuleSetDefineDTOModules.FILTERING_RULE_SET_DEFINED);