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 2020/11/23 08:20:08 UTC
[james-project] 09/19: JAMES-3452 Implement Identity/get
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
commit cba8856088c9f52ce3a7ab546b80a8c1f57d69ad
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Sat Nov 21 17:08:58 2020 +0700
JAMES-3452 Implement Identity/get
Based on top of CanSendFrom API it allows a JMAP client
to discover which addresses it can use and would need to
position on EmailSubmissions...
---
.../james/jmap/rfc8621/RFC8621MethodsModule.java | 15 +-
.../distributed/DistributedIdentityGetTest.java | 55 +++
.../rfc8621/contract/IdentityGetContract.scala | 377 +++++++++++++++++++++
.../memory/MemoryIdentityGetMethodTest.java | 43 +++
.../james/jmap/json/IdentitySerializer.scala | 47 +++
.../org/apache/james/jmap/mail/Identity.scala | 89 +++++
.../james/jmap/method/IdentityGetMethod.scala | 73 ++++
7 files changed, 686 insertions(+), 13 deletions(-)
diff --git a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index 253ff36..c11528c 100644
--- a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++ b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -32,19 +32,7 @@ import org.apache.james.jmap.http.Authenticator;
import org.apache.james.jmap.http.BasicAuthenticationStrategy;
import org.apache.james.jmap.http.rfc8621.InjectionKeys;
import org.apache.james.jmap.jwt.JWTAuthenticationStrategy;
-import org.apache.james.jmap.method.CoreEchoMethod;
-import org.apache.james.jmap.method.EmailGetMethod;
-import org.apache.james.jmap.method.EmailQueryMethod;
-import org.apache.james.jmap.method.EmailSetMethod;
-import org.apache.james.jmap.method.EmailSubmissionSetMethod;
-import org.apache.james.jmap.method.MailboxGetMethod;
-import org.apache.james.jmap.method.MailboxQueryMethod;
-import org.apache.james.jmap.method.MailboxSetMethod;
-import org.apache.james.jmap.method.Method;
-import org.apache.james.jmap.method.SystemZoneIdProvider;
-import org.apache.james.jmap.method.VacationResponseGetMethod;
-import org.apache.james.jmap.method.VacationResponseSetMethod;
-import org.apache.james.jmap.method.ZoneIdProvider;
+import org.apache.james.jmap.method.*;
import org.apache.james.jmap.routes.DownloadRoutes;
import org.apache.james.jmap.routes.JMAPApiRoutes;
import org.apache.james.jmap.routes.SessionRoutes;
@@ -84,6 +72,7 @@ public class RFC8621MethodsModule extends AbstractModule {
methods.addBinding().to(EmailQueryMethod.class);
methods.addBinding().to(VacationResponseGetMethod.class);
methods.addBinding().to(VacationResponseSetMethod.class);
+ methods.addBinding().to(IdentityGetMethod.class);
}
@ProvidesIntoSet
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedIdentityGetTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedIdentityGetTest.java
new file mode 100644
index 0000000..0475b48
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedIdentityGetTest.java
@@ -0,0 +1,55 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.distributed;
+
+import org.apache.james.CassandraExtension;
+import org.apache.james.CassandraRabbitMQJamesConfiguration;
+import org.apache.james.CassandraRabbitMQJamesServerMain;
+import org.apache.james.DockerElasticSearchExtension;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.SearchConfiguration;
+import org.apache.james.jmap.rfc8621.contract.IdentityGetContract;
+import org.apache.james.modules.AwsS3BlobStoreExtension;
+import org.apache.james.modules.RabbitMQExtension;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.apache.james.modules.blobstore.BlobStoreConfiguration;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+class DistributedIdentityGetTest implements IdentityGetContract {
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<CassandraRabbitMQJamesConfiguration>(tmpDir ->
+ CassandraRabbitMQJamesConfiguration.builder()
+ .workingDirectory(tmpDir)
+ .configurationFromClasspath()
+ .blobStore(BlobStoreConfiguration.builder()
+ .s3()
+ .disableCache()
+ .deduplication())
+ .searchConfiguration(SearchConfiguration.elasticSearch())
+ .build())
+ .extension(new DockerElasticSearchExtension())
+ .extension(new CassandraExtension())
+ .extension(new RabbitMQExtension())
+ .extension(new AwsS3BlobStoreExtension())
+ .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
+ .overrideWith(new TestJMAPServerModule()))
+ .build();
+}
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
new file mode 100644
index 0000000..a2c91cf
--- /dev/null
+++ 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
@@ -0,0 +1,377 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.contract
+
+import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
+import io.restassured.RestAssured.{`given`, requestSpecification}
+import io.restassured.http.ContentType.JSON
+import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
+import org.apache.http.HttpStatus.SC_OK
+import org.apache.james.GuiceJamesServer
+import org.apache.james.jmap.http.UserCredential
+import org.apache.james.jmap.rfc8621.contract.Fixture._
+import org.apache.james.utils.DataProbeImpl
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+trait IdentityGetContract {
+ @BeforeEach
+ def setUp(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl])
+ .fluent
+ .addDomain(DOMAIN.asString)
+ .addDomain("domain-alias.tld")
+ .addUser(BOB.asString, BOB_PASSWORD)
+
+ requestSpecification = baseRequestSpecBuilder(server)
+ .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD)))
+ .build
+ }
+
+ @Test
+ def getIdentityShouldReturnDefaultIdentity(): Unit = {
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": null
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1]")
+ .isEqualTo(
+ """{
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "name": "bob@domain.tld",
+ | "email": "bob@domain.tld",
+ | "mayDelete": false
+ | }
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def getIdentityShouldReturnAliases(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob-alias", "domain.tld", "bob@domain.tld")
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": null
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1]")
+ .isEqualTo(
+ """{
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "name": "bob@domain.tld",
+ | "email": "bob@domain.tld",
+ | "mayDelete": false
+ | },
+ | {
+ | "id": "6310e0a86aedaad878f634a5ff5c2cb8bb3c2401319305ef3272591ebcdc6cb4",
+ | "name": "bob-alias@domain.tld",
+ | "email": "bob-alias@domain.tld",
+ | "mayDelete": false
+ | }
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def getIdentityShouldReturnDomainAliases(server: GuiceJamesServer): Unit = {
+ server.getProbe(classOf[DataProbeImpl]).addUserAliasMapping("bob-alias", "domain.tld", "bob@domain.tld")
+ server.getProbe(classOf[DataProbeImpl]).addDomainAliasMapping("domain-alias.tld", "domain.tld")
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": null
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1]")
+ .isEqualTo(
+ """{
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "name": "bob@domain.tld",
+ | "email": "bob@domain.tld",
+ | "mayDelete": false
+ | },
+ | {
+ | "id": "725cfddc2c1905fefa6b8c3a6ab5dd9f8ba611c4d7772cf066f69cfd2ec23832",
+ | "name": "bob@domain-alias.tld",
+ | "email": "bob@domain-alias.tld",
+ | "mayDelete": false
+ | },
+ | {
+ | "id": "6310e0a86aedaad878f634a5ff5c2cb8bb3c2401319305ef3272591ebcdc6cb4",
+ | "name": "bob-alias@domain.tld",
+ | "email": "bob-alias@domain.tld",
+ | "mayDelete": false
+ | },
+ | {
+ | "id": "62844b5cd203bcb86cb590355fc509773ef1972ce8457b13a7d55d99a308c8f6",
+ | "name": "bob-alias@domain-alias.tld",
+ | "email": "bob-alias@domain-alias.tld",
+ | "mayDelete": false
+ | }
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def propertiesShouldBeSupported(): Unit = {
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": null,
+ | "properties": ["id", "name", "email", "replyTo", "bcc", "textSignature", "htmlSignature", "mayDelete"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1]")
+ .isEqualTo(
+ """{
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "name": "bob@domain.tld",
+ | "email": "bob@domain.tld",
+ | "mayDelete": false
+ | }
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def propertiesShouldBeFiltered(): Unit = {
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": null,
+ | "properties": ["id", "email"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+
+ assertThatJson(response)
+ .inPath("methodResponses[0][1]")
+ .isEqualTo(
+ """{
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "state": "000001",
+ | "list": [
+ | {
+ | "id": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "email": "bob@domain.tld"
+ | }
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def badPropertiesShouldBeRejected(): Unit = {
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Identity/get",
+ | {
+ | "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+ | "ids": null,
+ | "properties": ["id", "bad"]
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+
+ assertThatJson(response)
+ .isEqualTo(
+ """{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "error",
+ | {
+ | "type": "invalidArguments",
+ | "description": "The following properties [bad] do not exist."
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+
+ @Test
+ def badAccountIdShouldBeRejected(): Unit = {
+ val request =
+ s"""{
+ | "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+ | "methodCalls": [[
+ | "Identity/get",
+ | {
+ | "accountId": "bad",
+ | "ids": null
+ | },
+ | "c1"]]
+ |}""".stripMargin
+
+ val response = `given`
+ .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+ .body(request)
+ .when
+ .post
+ .`then`
+ .statusCode(SC_OK)
+ .contentType(JSON)
+ .extract
+ .body
+ .asString
+
+ assertThatJson(response)
+ .isEqualTo(
+ """{
+ | "sessionState": "75128aab4b1b",
+ | "methodResponses": [
+ | [
+ | "error",
+ | {
+ | "type": "accountNotFound"
+ | },
+ | "c1"
+ | ]
+ | ]
+ |}""".stripMargin)
+ }
+}
diff --git a/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryIdentityGetMethodTest.java b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryIdentityGetMethodTest.java
new file mode 100644
index 0000000..7542c18
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621-integration-tests/memory-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/memory/MemoryIdentityGetMethodTest.java
@@ -0,0 +1,43 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.rfc8621.memory;
+
+import org.apache.james.GuiceJamesServer;
+import org.apache.james.JamesServerBuilder;
+import org.apache.james.JamesServerExtension;
+import org.apache.james.jmap.rfc8621.contract.IdentityGetContract;
+import org.apache.james.jmap.rfc8621.contract.MailboxSetMethodContract;
+import org.apache.james.mailbox.inmemory.InMemoryId;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.modules.TestJMAPServerModule;
+import org.junit.jupiter.api.extension.RegisterExtension;
+
+import java.util.concurrent.ThreadLocalRandom;
+
+import static org.apache.james.MemoryJamesServerMain.IN_MEMORY_SERVER_AGGREGATE_MODULE;
+
+public class MemoryIdentityGetMethodTest implements IdentityGetContract {
+ @RegisterExtension
+ static JamesServerExtension testExtension = new JamesServerBuilder<>(JamesServerBuilder.defaultConfigurationProvider())
+ .server(configuration -> GuiceJamesServer.forConfiguration(configuration)
+ .combineWith(IN_MEMORY_SERVER_AGGREGATE_MODULE)
+ .overrideWith(new TestJMAPServerModule()))
+ .build();
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala
new file mode 100644
index 0000000..bca92b2
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/IdentitySerializer.scala
@@ -0,0 +1,47 @@
+/****************************************************************
+ * 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.json
+
+import org.apache.james.jmap.core.Properties
+import org.apache.james.jmap.mail._
+import play.api.libs.json.{JsArray, JsObject, JsResult, JsSuccess, JsValue, Json, OWrites, Reads, Writes, __}
+
+object IdentitySerializer {
+ private implicit val emailerNameReads: Writes[EmailerName] = Json.valueWrites[EmailerName]
+ private implicit val emailAddressReads: Writes[EmailAddress] = Json.writes[EmailAddress]
+ private implicit val nameWrites: Writes[IdentityName] = Json.valueWrites[IdentityName]
+ private implicit val textSignatureWrites: Writes[TextSignature] = Json.valueWrites[TextSignature]
+ private implicit val htmlSignatureWrites: Writes[HtmlSignature] = Json.valueWrites[HtmlSignature]
+ private implicit val mayDeleteWrites: Writes[MayDeleteIdentity] = Json.valueWrites[MayDeleteIdentity]
+ private implicit val identityWrites: Writes[Identity] = Json.writes[Identity]
+ private implicit val identityGetRequestReads: Reads[IdentityGetRequest] = Json.reads[IdentityGetRequest]
+ private implicit val identityGetResponseWrites: OWrites[IdentityGetResponse] = Json.writes[IdentityGetResponse]
+
+ def serialize(response: IdentityGetResponse, properties: Properties): JsObject = Json.toJsObject(response)
+ .transform((__ \ "list").json.update {
+ case JsArray(underlying) => JsSuccess(JsArray(underlying.map {
+ case jsonObject: JsObject => properties.filter(jsonObject)
+ case jsValue => jsValue
+ }))
+ }).get
+
+ def deserialize(input: JsValue): JsResult[IdentityGetRequest] = Json.fromJson[IdentityGetRequest](input)
+
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Identity.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Identity.scala
new file mode 100644
index 0000000..9702f98
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/Identity.scala
@@ -0,0 +1,89 @@
+/****************************************************************
+ * 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.mail
+
+import java.nio.charset.StandardCharsets
+
+import com.github.steveash.guavate.Guavate
+import com.google.common.hash.Hashing
+import eu.timepit.refined.auto._
+import eu.timepit.refined.refineV
+import javax.inject.Inject
+import org.apache.james.core.MailAddress
+import org.apache.james.jmap.core.Id.Id
+import org.apache.james.jmap.core.State.State
+import org.apache.james.jmap.core.{AccountId, Properties}
+import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.rrt.api.CanSendFrom
+
+import scala.jdk.CollectionConverters._
+
+object Identity {
+ val allProperties: Properties = Properties("id", "name", "email", "replyTo", "bcc", "textSignature", "htmlSignature",
+ "mayDelete")
+ val idProperty: Properties = Properties("id")
+}
+
+case class IdentityName(name: String) extends AnyVal
+case class TextSignature(name: String) extends AnyVal
+case class HtmlSignature(name: String) extends AnyVal
+case class MayDeleteIdentity(value: Boolean) extends AnyVal
+
+case class Identity(id: Id,
+ name: IdentityName,
+ email: MailAddress,
+ replyTo: Option[List[EmailAddress]],
+ bcc: Option[List[EmailAddress]],
+ textSignature: Option[TextSignature],
+ htmlSignature: Option[HtmlSignature],
+ mayDelete: MayDeleteIdentity)
+
+case class IdentityGetRequest(accountId: AccountId,
+ properties: Option[Properties]) extends WithAccountId
+
+case class IdentityGetResponse(accountId: AccountId,
+ state: State,
+ list: List[Identity])
+
+class IdentityFactory @Inject()(canSendFrom: CanSendFrom) {
+ def listIdentities(session: MailboxSession): List[Identity] =
+ canSendFrom.allValidFromAddressesForUser(session.getUser)
+ .collect(Guavate.toImmutableList()).asScala.toList
+ .flatMap(address =>
+ from(address).map(id =>
+ Identity(
+ id = id,
+ name = IdentityName(address.asString()),
+ email = address,
+ replyTo = None,
+ bcc = None,
+ textSignature = None,
+ htmlSignature = None,
+ mayDelete = MayDeleteIdentity(false))))
+
+ private def from(address: MailAddress): Option[Id] = {
+ val sha256String = Hashing.sha256()
+ .hashString(address.asString(), StandardCharsets.UTF_8)
+ .toString
+ val refinedId: Either[String, Id] = refineV(sha256String)
+ refinedId.toOption
+ }
+}
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentityGetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentityGetMethod.scala
new file mode 100644
index 0000000..3e8a3f5
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/IdentityGetMethod.scala
@@ -0,0 +1,73 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.method
+
+import eu.timepit.refined.auto._
+import javax.inject.Inject
+import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, JMAP_CORE, JMAP_MAIL}
+import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
+import org.apache.james.jmap.core.State.INSTANCE
+import org.apache.james.jmap.core._
+import org.apache.james.jmap.json.{IdentitySerializer, ResponseSerializer}
+import org.apache.james.jmap.mail.{Identity, IdentityFactory, IdentityGetRequest, IdentityGetResponse}
+import org.apache.james.jmap.routes.SessionSupplier
+import org.apache.james.mailbox.MailboxSession
+import org.apache.james.metrics.api.MetricFactory
+import play.api.libs.json.{JsError, JsObject, JsSuccess}
+import reactor.core.scala.publisher.SMono
+import reactor.core.scheduler.Schedulers
+
+class IdentityGetMethod @Inject() (identityFactory: IdentityFactory,
+ val metricFactory: MetricFactory,
+ val sessionSupplier: SessionSupplier) extends MethodRequiringAccountId[IdentityGetRequest] {
+ override val methodName: MethodName = MethodName("Identity/get")
+ override val requiredCapabilities: Set[CapabilityIdentifier] = Set(JMAP_CORE, JMAP_MAIL)
+
+ override def doProcess(capabilities: Set[CapabilityIdentifier], invocation: InvocationWithContext, mailboxSession: MailboxSession, request: IdentityGetRequest): SMono[InvocationWithContext] = {
+ val requestedProperties: Properties = request.properties.getOrElse(Identity.allProperties)
+ (requestedProperties -- Identity.allProperties match {
+ case invalidProperties if invalidProperties.isEmpty() => getIdentities(request, mailboxSession)
+ .subscribeOn(Schedulers.elastic())
+ .map(identityGetResponse => Invocation(
+ methodName = methodName,
+ arguments = Arguments(IdentitySerializer.serialize(identityGetResponse, requestedProperties ++ Identity.idProperty).as[JsObject]),
+ methodCallId = invocation.invocation.methodCallId))
+ case invalidProperties: Properties =>
+ SMono.just(Invocation.error(errorCode = ErrorCode.InvalidArguments,
+ description = s"The following properties [${invalidProperties.format()}] do not exist.",
+ methodCallId = invocation.invocation.methodCallId))
+ }).map(InvocationWithContext(_, invocation.processingContext))
+ }
+
+ override def getRequest(mailboxSession: MailboxSession, invocation: Invocation): Either[IllegalArgumentException, IdentityGetRequest] =
+ IdentitySerializer.deserialize(invocation.arguments.value) match {
+ case JsSuccess(request, _) => Right(request)
+ case errors: JsError => Left(new IllegalArgumentException(ResponseSerializer.serialize(errors).toString))
+ }
+
+ private def getIdentities(request: IdentityGetRequest,
+ mailboxSession: MailboxSession): SMono[IdentityGetResponse] =
+ SMono.fromCallable(() => identityFactory.listIdentities(mailboxSession))
+ .map(identities => IdentityGetResponse(
+ accountId = request.accountId,
+ list = identities,
+ state = INSTANCE))
+
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org