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