You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2023/03/03 02:06:07 UTC

[james-project] branch master updated: JAMES-3893 Add a WebAdmin API allowing listing user identity

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

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


The following commit(s) were added to refs/heads/master by this push:
     new 2a51f9cfc0 JAMES-3893 Add a WebAdmin API allowing listing user identity
2a51f9cfc0 is described below

commit 2a51f9cfc09968d134e71677330aa63545c36bd8
Author: Tung Van TRAN <vt...@linagora.com>
AuthorDate: Tue Feb 28 07:49:52 2023 +0700

    JAMES-3893 Add a WebAdmin API allowing listing user identity
---
 .../docs/modules/ROOT/pages/operate/webadmin.adoc  |  46 ++++
 .../jmap/api/identity/CustomIdentityDAO.scala      |  23 +-
 .../apache/james/jmap/api/model/EmailAddress.scala |   2 +
 .../james/webadmin/utils/ParametersExtractor.java  |  14 +
 server/protocols/webadmin/webadmin-jmap/pom.xml    |   8 +
 .../webadmin/data/jmap/UserIdentityRoutes.java     | 111 ++++++++
 .../james/webadmin/data/jmap/dto/UserIdentity.java | 151 +++++++++++
 .../data/jmap/UserIdentitiesRoutesTest.java        | 295 +++++++++++++++++++++
 .../webadmin/data/jmap/UserIdentitiesHelper.scala  |  38 +++
 src/site/markdown/server/manage-webadmin.md        |  47 ++++
 10 files changed, 733 insertions(+), 2 deletions(-)

diff --git a/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc b/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
index 9621808429..6b66d208f8 100644
--- a/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
+++ b/server/apps/distributed-app/docs/modules/ROOT/pages/operate/webadmin.adoc
@@ -671,6 +671,52 @@ Valid status includes:
  - `FAILED`: Error encountered while executing this step. Check the logs.
  - `ABORTED`: Won't be executed because of previous step failures.
 
+=== Retrieving the user identities
+
+....
+curl -XGET http://ip:port/users/{baseUser}/identities?default=true
+....
+
+API to get the list of identities of a user
+
+The response will look like:
+
+```
+[
+   {
+      "name":"identity name 1",
+      "email":"bob@domain.tld",
+      "id":"4c039533-75b9-45db-becc-01fb0e747aa8",
+      "mayDelete":true,
+      "textSignature":"textSignature 1",
+      "htmlSignature":"htmlSignature 1",
+      "sortOrder":1,
+      "bcc":[
+         {
+            "emailerName":"bcc name 1",
+            "mailAddress":"bcc1@domain.org"
+         }
+      ],
+      "replyTo":[
+         {
+            "emailerName":"reply name 1",
+            "mailAddress":"reply1@domain.org"
+         }
+      ]
+   }
+]
+```
+
+Query parameters:
+
+* default: (Optional) allows getting the default identity of a user. In order to do that: `default=true`
+
+Response codes:
+
+* 200: The list was successfully retrieved
+* 400: The user is invalid
+* 404: The user is unknown or the default identity can not be found.
+
 == Administrating vacation settings
 
 === Get vacation settings
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 679c05bdd8..0d67cb23c7 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
@@ -20,8 +20,7 @@
 package org.apache.james.jmap.api.identity
 
 import java.nio.charset.StandardCharsets
-import java.util.UUID
-
+import java.util.{Optional, UUID}
 import javax.inject.Inject
 import org.apache.james.core.{MailAddress, Username}
 import org.apache.james.jmap.api.model.{EmailAddress, ForbiddenSendFromException, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
@@ -33,6 +32,26 @@ import reactor.core.scala.publisher.{SFlux, SMono}
 
 import scala.jdk.StreamConverters._
 import scala.util.Try
+import scala.jdk.OptionConverters._
+
+object IdentityCreationRequest {
+  def fromJava(mailAddress: MailAddress,
+               identityName: Optional[String],
+               replyTo: Optional[List[EmailAddress]],
+               bcc: Optional[List[EmailAddress]],
+               sortOrder: Optional[Integer],
+               textSignature: Optional[String],
+               htmlSignature: Optional[String]): IdentityCreationRequest = {
+    IdentityCreationRequest(
+      name = identityName.toScala.map(IdentityName(_)),
+      email = mailAddress,
+      replyTo = replyTo.toScala,
+      bcc = bcc.toScala,
+      sortOrder = sortOrder.toScala.map(_.toInt),
+      textSignature = textSignature.toScala.map(TextSignature(_)),
+      htmlSignature = htmlSignature.toScala.map(HtmlSignature(_)))
+  }
+}
 
 case class IdentityCreationRequest(name: Option[IdentityName],
                                    email: MailAddress,
diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/EmailAddress.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/EmailAddress.scala
index 68fdebe401..8fca1136f6 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/EmailAddress.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/EmailAddress.scala
@@ -53,4 +53,6 @@ case class EmailAddress(name: Option[EmailerName], email: MailAddress) {
     name.map(_.value).orNull,
     email.getLocalPart,
     email.getDomain.asString)
+
+  val nameAsString: String = name.map(_.value).orNull
 }
diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/ParametersExtractor.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/ParametersExtractor.java
index 7d16a1b691..61a3e09ccc 100644
--- a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/ParametersExtractor.java
+++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/ParametersExtractor.java
@@ -28,6 +28,7 @@ import org.apache.james.util.streams.Limit;
 import org.apache.james.util.streams.Offset;
 import org.eclipse.jetty.http.HttpStatus;
 
+import com.google.common.base.Preconditions;
 import com.google.common.base.Strings;
 
 import spark.Request;
@@ -60,6 +61,19 @@ public class ParametersExtractor {
             .map(raw -> DurationParser.parse(raw, ChronoUnit.SECONDS));
     }
 
+    public static Optional<Boolean> extractBoolean(Request request, String parameterName) {
+        return Optional.ofNullable(request.queryParams(parameterName))
+            .filter(s -> !s.isEmpty())
+            .map(String::trim)
+            .map(s -> {
+                Preconditions.checkArgument(s.equalsIgnoreCase(Boolean.TRUE.toString())
+                        || s.equalsIgnoreCase(Boolean.FALSE.toString()),
+                    "Invalid '" + parameterName + "' query parameter");
+                return Boolean.parseBoolean(s);
+            });
+    }
+
+
     private static <T extends Number> Optional<T> extractPositiveNumber(Request request, String parameterName, Function<String, T> toNumber) {
         try {
             return Optional.ofNullable(request.queryParams(parameterName))
diff --git a/server/protocols/webadmin/webadmin-jmap/pom.xml b/server/protocols/webadmin/webadmin-jmap/pom.xml
index 58cf06ead9..164bcfe4cf 100644
--- a/server/protocols/webadmin/webadmin-jmap/pom.xml
+++ b/server/protocols/webadmin/webadmin-jmap/pom.xml
@@ -154,4 +154,12 @@
             <scope>test</scope>
         </dependency>
     </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>net.alchim31.maven</groupId>
+                <artifactId>scala-maven-plugin</artifactId>
+            </plugin>
+        </plugins>
+    </build>
 </project>
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/UserIdentityRoutes.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/UserIdentityRoutes.java
new file mode 100644
index 0000000000..8a60771704
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/UserIdentityRoutes.java
@@ -0,0 +1,111 @@
+/****************************************************************
+ * 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.webadmin.data.jmap;
+
+import static org.apache.james.webadmin.Constants.SEPARATOR;
+
+import java.util.Comparator;
+import java.util.List;
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.identity.IdentityRepository;
+import org.apache.james.util.FunctionalUtils;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.data.jmap.dto.UserIdentity;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.apache.james.webadmin.utils.ParametersExtractor;
+import org.eclipse.jetty.http.HttpStatus;
+
+import reactor.core.publisher.Flux;
+import spark.HaltException;
+import spark.Request;
+import spark.Response;
+import spark.Service;
+
+public class UserIdentityRoutes implements Routes {
+    public static final String USERS = "/users";
+    public static final String IDENTITIES = "identities";
+    private static final String USER_NAME = ":userName";
+    private Service service;
+    private final IdentityRepository identityRepository;
+    private final JsonTransformer jsonTransformer;
+
+    @Inject
+    public UserIdentityRoutes(IdentityRepository identityRepository,
+                              JsonTransformer jsonTransformer) {
+        this.identityRepository = identityRepository;
+        this.jsonTransformer = jsonTransformer;
+    }
+
+    @Override
+    public String getBasePath() {
+        return USERS;
+    }
+
+    @Override
+    public void define(Service service) {
+        this.service = service;
+        getUserIdentities();
+    }
+
+    public void getUserIdentities() {
+        service.get(USERS + SEPARATOR + USER_NAME + SEPARATOR + IDENTITIES, this::listIdentities, jsonTransformer);
+    }
+
+    private List<UserIdentity> listIdentities(Request request, Response response) {
+        Username username = extractUsername(request);
+        Optional<Boolean> defaultFilter = ParametersExtractor.extractBoolean(request, "default");
+
+        List<UserIdentity> identities = Flux.from(identityRepository.list(username))
+            .map(UserIdentity::from)
+            .collectList()
+            .block();
+
+        return defaultFilter
+            .filter(FunctionalUtils.identityPredicate())
+            .map(queryDefault -> getDefaultIdentity(identities)
+                .map(List::of)
+                .orElseThrow(() -> throw404("Default identity can not be found")))
+            .orElse(identities);
+    }
+
+    private Optional<UserIdentity> getDefaultIdentity(List<UserIdentity> identities) {
+        return identities.stream()
+            .filter(UserIdentity::getMayDelete)
+            .min(Comparator.comparing(UserIdentity::getSortOrder));
+    }
+
+    private HaltException throw404(String message) {
+        throw ErrorResponder.builder()
+            .statusCode(HttpStatus.NOT_FOUND_404)
+            .type(ErrorResponder.ErrorType.NOT_FOUND)
+            .message(message)
+            .haltError();
+    }
+
+    private Username extractUsername(Request request) {
+        return Username.of(request.params(USER_NAME));
+    }
+
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/dto/UserIdentity.java b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/dto/UserIdentity.java
new file mode 100644
index 0000000000..3e58290732
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/main/java/org/apache/james/webadmin/data/jmap/dto/UserIdentity.java
@@ -0,0 +1,151 @@
+/****************************************************************
+ * 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.webadmin.data.jmap.dto;
+
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.jmap.api.model.Identity;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import scala.jdk.javaapi.CollectionConverters;
+import scala.jdk.javaapi.OptionConverters;
+
+public class UserIdentity {
+    public static UserIdentity from(Identity identity) {
+        return new UserIdentity(
+            identity.name(),
+            identity.email().asString(),
+            identity.id().id().toString(),
+            identity.mayDelete(),
+            identity.textSignature(),
+            identity.htmlSignature(),
+            identity.sortOrder(),
+            getBccFromIdentity(identity),
+            getReplyFromIdentity(identity));
+    }
+
+    public static class EmailAddress {
+        public static EmailAddress from(org.apache.james.jmap.api.model.EmailAddress scala) {
+            return new EmailAddress(scala.nameAsString(), scala.email());
+        }
+
+        @JsonProperty("name")
+
+        private String emailerName;
+
+        @JsonProperty("email")
+        private String mailAddress;
+
+        public EmailAddress(String emailerName, MailAddress mailAddress) {
+            this.emailerName = emailerName;
+            this.mailAddress = mailAddress.asString();
+        }
+
+        public String getEmailerName() {
+            return emailerName;
+        }
+
+        public String getMailAddress() {
+            return mailAddress;
+        }
+    }
+
+    private static List<EmailAddress> getBccFromIdentity(Identity identity) {
+        return OptionConverters.toJava(identity.bcc())
+            .map(CollectionConverters::asJava)
+            .orElseGet(List::of)
+            .stream()
+            .map(EmailAddress::from)
+            .collect(Collectors.toList());
+    }
+
+    private static List<EmailAddress> getReplyFromIdentity(Identity identity) {
+        return OptionConverters.toJava(identity.replyTo())
+            .map(CollectionConverters::asJava)
+            .orElseGet(List::of)
+            .stream()
+            .map(EmailAddress::from)
+            .collect(Collectors.toList());
+    }
+
+    private String name;
+    private String email;
+    private String id;
+    private Boolean mayDelete;
+    private String textSignature;
+    private String htmlSignature;
+    private Integer sortOrder;
+    private List<EmailAddress> bcc;
+    private List<EmailAddress> replyTo;
+
+    public UserIdentity(String name, String email, String id,
+                        Boolean mayDelete, String textSignature, String htmlSignature,
+                        Integer sortOrder, List<EmailAddress> bcc, List<EmailAddress> replyTo) {
+        this.name = name;
+        this.email = email;
+        this.id = id;
+        this.mayDelete = mayDelete;
+        this.textSignature = textSignature;
+        this.htmlSignature = htmlSignature;
+        this.sortOrder = sortOrder;
+        this.bcc = bcc;
+        this.replyTo = replyTo;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public String getId() {
+        return id;
+    }
+
+    public Boolean getMayDelete() {
+        return mayDelete;
+    }
+
+    public String getTextSignature() {
+        return textSignature;
+    }
+
+    public String getHtmlSignature() {
+        return htmlSignature;
+    }
+
+    public Integer getSortOrder() {
+        return sortOrder;
+    }
+
+    public List<EmailAddress> getBcc() {
+        return bcc;
+    }
+
+    public List<EmailAddress> getReplyTo() {
+        return replyTo;
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/UserIdentitiesRoutesTest.java b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/UserIdentitiesRoutesTest.java
new file mode 100644
index 0000000000..51038427a1
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/java/org/apache/james/webadmin/data/jmap/UserIdentitiesRoutesTest.java
@@ -0,0 +1,295 @@
+/****************************************************************
+ * 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.webadmin.data.jmap;
+
+import static io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.james.core.MailAddress;
+import org.apache.james.core.Username;
+import org.apache.james.jmap.api.identity.DefaultIdentitySupplier;
+import org.apache.james.jmap.api.identity.IdentityCreationRequest;
+import org.apache.james.jmap.api.identity.IdentityRepository;
+import org.apache.james.jmap.api.model.EmailAddress;
+import org.apache.james.jmap.api.model.Identity;
+import org.apache.james.jmap.memory.identity.MemoryCustomIdentityDAO;
+import org.apache.james.json.DTOConverter;
+import org.apache.james.mime4j.dom.address.Mailbox;
+import org.apache.james.mime4j.dom.address.MailboxList;
+import org.apache.james.task.Hostname;
+import org.apache.james.task.MemoryTaskManager;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.routes.TasksRoutes;
+import org.apache.james.webadmin.utils.JsonTransformer;
+import org.eclipse.jetty.http.HttpStatus;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+import io.restassured.RestAssured;
+import net.javacrumbs.jsonunit.core.Option;
+import reactor.core.publisher.Mono;
+import reactor.core.scala.publisher.SMono;
+import scala.jdk.javaapi.CollectionConverters;
+
+class UserIdentitiesRoutesTest {
+
+    private static final Username BOB = Username.of("bob@domain.tld");
+    private static final String BASE_PATH = "/users";
+    private static final String GET_IDENTITIES_USERS_PATH = "/%s/identities";
+    private WebAdminServer webAdminServer;
+    private IdentityRepository identityRepository;
+    private DefaultIdentitySupplier identityFactory;
+
+    @BeforeEach
+    void setUp() {
+        MemoryTaskManager taskManager = new MemoryTaskManager(new Hostname("foo"));
+        identityFactory = mock(DefaultIdentitySupplier.class);
+        Mockito.when(identityFactory.userCanSendFrom(any(), any())).thenReturn(SMono.just(true).hasElement());
+
+        identityRepository = new IdentityRepository(new MemoryCustomIdentityDAO(), identityFactory);
+
+        JsonTransformer jsonTransformer = new JsonTransformer();
+        TasksRoutes tasksRoutes = new TasksRoutes(taskManager, jsonTransformer, DTOConverter.of(UploadCleanupTaskAdditionalInformationDTO.SERIALIZATION_MODULE));
+        UserIdentityRoutes userIdentityRoutes = new UserIdentityRoutes(identityRepository, new JsonTransformer());
+
+        webAdminServer = WebAdminUtils.createWebAdminServer(userIdentityRoutes, tasksRoutes).start();
+        RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .setBasePath(BASE_PATH)
+            .build();
+    }
+
+    @AfterEach
+    void stop() {
+        webAdminServer.destroy();
+    }
+
+    @Test
+    void listIdentitiesShouldReturnBothCustomAndServerSetIdentities() throws Exception {
+        // identity: server set
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(UserIdentitiesHelper.IDENTITY1())).toList());
+
+        IdentityCreationRequest creationRequest = IdentityCreationRequest.fromJava(
+            BOB.asMailAddress(),
+            Optional.of("identity name 1"),
+            Optional.of(EmailAddress.from(new MailboxList(
+                new Mailbox("replyTo1", "james.org"),
+                new Mailbox("replyTo2", "james.org")))),
+            Optional.of(EmailAddress.from(new MailboxList(
+                new Mailbox("bcc1", "james.org"),
+                new Mailbox("bcc2", "james.org")))),
+            Optional.of(1),
+            Optional.of("textSignature 1"),
+            Optional.of("htmlSignature 1"));
+
+        // identity: custom
+        Mono.from(identityRepository.save(BOB, creationRequest)).block();
+
+        String response = when()
+            .get(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.OK_200)
+            .contentType(io.restassured.http.ContentType.JSON)
+            .extract()
+            .body()
+            .asString();
+
+        String expectedResponse = "[" +
+            "    {" +
+            "        \"name\": \"identity name 1\"," +
+            "        \"email\": \"bob@domain.tld\"," +
+            "        \"id\": \"${json-unit.ignore}\"," +
+            "        \"mayDelete\": true," +
+            "        \"textSignature\": \"textSignature 1\"," +
+            "        \"htmlSignature\": \"htmlSignature 1\"," +
+            "        \"sortOrder\": 1," +
+            "        \"bcc\": [" +
+            "            {" +
+            "                \"name\": null," +
+            "                \"email\": \"bcc1@james.org\"" +
+            "            }," +
+            "            {" +
+            "                \"name\": null," +
+            "                \"email\": \"bcc2@james.org\"" +
+            "            }" +
+            "        ]," +
+            "        \"replyTo\": [" +
+            "            {" +
+            "                \"name\": null," +
+            "                \"email\": \"replyTo1@james.org\"" +
+            "            }," +
+            "            {" +
+            "                \"name\": null," +
+            "                \"email\": \"replyTo2@james.org\"" +
+            "            }" +
+            "        ]" +
+            "    }," +
+            "    {" +
+            "        \"name\": \"base name\"," +
+            "        \"email\": \"bob@domain.tld\"," +
+            "        \"id\": \"${json-unit.ignore}\"," +
+            "        \"mayDelete\": false," +
+            "        \"textSignature\": \"text signature base\"," +
+            "        \"htmlSignature\": \"html signature base\"," +
+            "        \"sortOrder\": 100," +
+            "        \"bcc\": [" +
+            "            {" +
+            "                \"name\": \"My Boss bcc 1\"," +
+            "                \"email\": \"boss_bcc_1@domain.tld\"" +
+            "            }" +
+            "        ]," +
+            "        \"replyTo\": [" +
+            "            {" +
+            "                \"name\": \"My Boss 1\"," +
+            "                \"email\": \"boss1@domain.tld\"" +
+            "            }" +
+            "        ]" +
+            "    }" +
+            "]";
+        assertThatJson(response)
+            .when(Option.IGNORING_ARRAY_ORDER)
+            .isEqualTo(expectedResponse);
+    }
+
+    @Test
+    void listIdentitiesShouldSupportDefaultParam() throws Exception {
+        // identity: server set
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(UserIdentitiesHelper.IDENTITY1())).toList());
+
+        Integer highPriorityOrder = 1;
+        Integer lowPriorityOrder = 2;
+        IdentityCreationRequest creationRequest1 = IdentityCreationRequest.fromJava(
+            BOB.asMailAddress(),
+            Optional.of("identity name 1"),
+            Optional.of(UserIdentitiesHelper.emailAddressFromJava("reply name 1", new MailAddress("reply1@domain.org"))),
+            Optional.of(UserIdentitiesHelper.emailAddressFromJava("bcc name 1", new MailAddress("bcc1@domain.org"))),
+            Optional.of(highPriorityOrder),
+            Optional.of("textSignature 1"),
+            Optional.of("htmlSignature 1"));
+
+        IdentityCreationRequest creationRequest2 = IdentityCreationRequest.fromJava(
+            BOB.asMailAddress(),
+            Optional.of("identity name 2"),
+            Optional.of(UserIdentitiesHelper.emailAddressFromJava("reply name 2", new MailAddress("reply2@domain.org"))),
+            Optional.of(UserIdentitiesHelper.emailAddressFromJava("bcc name 2", new MailAddress("bcc2@domain.org"))),
+            Optional.of(lowPriorityOrder),
+            Optional.of("textSignature 2"),
+            Optional.of("htmlSignature 2"));
+
+        // identity: custom
+        Mono.from(identityRepository.save(BOB, creationRequest1)).block();
+        Mono.from(identityRepository.save(BOB, creationRequest2)).block();
+
+        String response = given()
+            .queryParam("default", "true")
+            .get(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.OK_200)
+            .contentType(io.restassured.http.ContentType.JSON)
+            .extract()
+            .body()
+            .asString();
+
+        String expectedResponse = "[" +
+            "    {" +
+            "        \"name\": \"identity name 1\"," +
+            "        \"email\": \"bob@domain.tld\"," +
+            "        \"id\": \"${json-unit.ignore}\"," +
+            "        \"mayDelete\": true," +
+            "        \"textSignature\": \"textSignature 1\"," +
+            "        \"htmlSignature\": \"htmlSignature 1\"," +
+            "        \"sortOrder\": 1," +
+            "        \"bcc\": [" +
+            "            {" +
+            "                \"name\": \"bcc name 1\"," +
+            "                \"email\": \"bcc1@domain.org\"" +
+            "            }" +
+            "        ]," +
+            "        \"replyTo\": [" +
+            "            {" +
+            "                \"name\": \"reply name 1\"," +
+            "                \"email\": \"reply1@domain.org\"" +
+            "            }" +
+            "        ]" +
+            "    }" +
+            "]";
+        assertThatJson(response)
+            .when(Option.IGNORING_ARRAY_ORDER)
+            .isEqualTo(expectedResponse);
+    }
+
+    @Test
+    void listIdentitiesShouldReturnBadRequestWhenInvalidDefaultParam() {
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(UserIdentitiesHelper.IDENTITY1())).toList());
+
+        String response = given()
+            .queryParam("default", "invalid")
+            .get(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .contentType(io.restassured.http.ContentType.JSON)
+            .extract()
+            .body()
+            .asString();
+
+        assertThatJson(response)
+            .isEqualTo("{" +
+                "    \"statusCode\": 400," +
+                "    \"type\": \"InvalidArgument\"," +
+                "    \"message\": \"Invalid arguments supplied in the user request\"," +
+                "    \"details\": \"Invalid 'default' query parameter\"" +
+                "}");
+    }
+
+    @Test
+    void listIdentitiesShouldReturnNotFoundWhenCanNotQueryDefaultIdentity() {
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.<Identity>of()).toList());
+
+        String response = given()
+            .queryParam("default", "true")
+            .get(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404)
+            .contentType(io.restassured.http.ContentType.JSON)
+            .extract()
+            .body()
+            .asString();
+
+        assertThatJson(response)
+            .isEqualTo("{" +
+                "    \"statusCode\": 404," +
+                "    \"type\": \"notFound\"," +
+                "    \"message\": \"Default identity can not be found\"," +
+                "    \"details\": null" +
+                "}");
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-jmap/src/test/scala/org/apache/james/webadmin/data/jmap/UserIdentitiesHelper.scala b/server/protocols/webadmin/webadmin-jmap/src/test/scala/org/apache/james/webadmin/data/jmap/UserIdentitiesHelper.scala
new file mode 100644
index 0000000000..4cbaf60b3f
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-jmap/src/test/scala/org/apache/james/webadmin/data/jmap/UserIdentitiesHelper.scala
@@ -0,0 +1,38 @@
+/****************************************************************
+ * 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.webadmin.data.jmap
+
+import org.apache.james.core.{MailAddress, Username}
+import org.apache.james.jmap.api.model.{EmailAddress, EmailerName, HtmlSignature, Identity, IdentityId, IdentityName, MayDeleteIdentity, TextSignature}
+
+object UserIdentitiesHelper {
+
+  val IDENTITY1: Identity = Identity(id = IdentityId.generate,
+    name = IdentityName("base name"),
+    email = Username.of("bob@domain.tld").asMailAddress(),
+    replyTo = Some(List(EmailAddress(Some(EmailerName("My Boss 1")), new MailAddress("boss1@domain.tld")))),
+    bcc = Some(List(EmailAddress(Some(EmailerName("My Boss bcc 1")), new MailAddress("boss_bcc_1@domain.tld")))),
+    textSignature = TextSignature("text signature base"),
+    htmlSignature = HtmlSignature("html signature base"),
+    mayDelete = MayDeleteIdentity(false))
+
+  def emailAddressFromJava(name: String, email: MailAddress) : List[EmailAddress] =
+    List(EmailAddress(Some(EmailerName(name)), email))
+}
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index 9212a810e9..f4e5577ab7 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -523,6 +523,53 @@ Valid status includes:
  - `FAILED`: Error encountered while executing this step. Check the logs.
  - `ABORTED`: Won't be executed because of previous step failures.
 
+
+### Retrieving the user identities
+
+```
+curl -XGET http://ip:port/users/{baseUser}/identities?default=true
+```
+
+API to get the list of identities of a user
+
+The response will look like:
+
+```
+[
+   {
+      "name":"identity name 1",
+      "email":"bob@domain.tld",
+      "id":"4c039533-75b9-45db-becc-01fb0e747aa8",
+      "mayDelete":true,
+      "textSignature":"textSignature 1",
+      "htmlSignature":"htmlSignature 1",
+      "sortOrder":1,
+      "bcc":[
+         {
+            "emailerName":"bcc name 1",
+            "mailAddress":"bcc1@domain.org"
+         }
+      ],
+      "replyTo":[
+         {
+            "emailerName":"reply name 1",
+            "mailAddress":"reply1@domain.org"
+         }
+      ]
+   }
+]
+```
+
+Query parameters:
+
+ - default: (Optional) allows getting the default identity of a user. In order to do that: `default=true`
+
+Response codes:
+
+ - 200: The list was successfully retrieved
+ - 400: The user is invalid
+ - 404: The user is unknown or the default identity can not be found.
+
 ## Administrating mailboxes
 
 ### All mailboxes


---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@james.apache.org
For additional commands, e-mail: notifications-help@james.apache.org