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 2023/03/06 01:43:10 UTC

[james-project] branch master updated: JAMES-3893 Add a WebAdmin API allowing creation/updation user identity (#1471)

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


The following commit(s) were added to refs/heads/master by this push:
     new 24c49d5979 JAMES-3893 Add a WebAdmin API allowing creation/updation user identity (#1471)
24c49d5979 is described below

commit 24c49d5979447bfce1ef5f055f7b0a9449466a83
Author: vttran <vt...@linagora.com>
AuthorDate: Mon Mar 6 08:43:04 2023 +0700

    JAMES-3893 Add a WebAdmin API allowing creation/updation user identity (#1471)
---
 .../docs/modules/ROOT/pages/operate/webadmin.adoc  |  68 +++
 .../jmap/api/identity/CustomIdentityDAO.scala      |  26 +-
 .../apache/james/jmap/api/model/EmailAddress.scala |   5 +
 server/protocols/webadmin/webadmin-jmap/pom.xml    |  14 +-
 .../webadmin/data/jmap/UserIdentityRoutes.java     |  71 ++-
 .../james/webadmin/data/jmap/dto/UserIdentity.java |  80 ++++
 .../data/jmap/UserIdentitiesRoutesTest.java        | 489 ++++++++++++++++++++-
 .../webadmin/data/jmap/UserIdentitiesHelper.scala  |  38 --
 src/site/markdown/server/manage-webadmin.md        |  65 +++
 9 files changed, 787 insertions(+), 69 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 6b66d208f8..c6c0ae4cc4 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
@@ -717,6 +717,74 @@ Response codes:
 * 400: The user is invalid
 * 404: The user is unknown or the default identity can not be found.
 
+The optional `default` query parameter allows getting the default identity of a user.
+In order to do that: `default=true`
+
+The web-admin server will return `404` response code when the default identity can not be found.
+
+=== Creating a JMAP user identity
+
+API to create a new JMAP user identity
+....
+curl -XPOST http://ip:port/users/{username}/identities \
+-d '{
+	"name": "Bob",
+	"email": "bob@domain.tld",
+	"mayDelete": true,
+	"htmlSignature": "a html signature",
+	"textSignature": "a text signature",
+	"bcc": [{
+		"email": "boss2@domain.tld",
+		"name": "My Boss 2"
+	}],
+	"replyTo": [{
+		"email": "boss@domain.tld",
+		"name": "My Boss"
+	}],
+	"sortOrder": 0
+ }' \
+-H "Content-Type: application/json"
+....
+
+Response codes:
+
+* 201: The new identity was successfully created
+* 404: The username is unknown
+* 400: The payload is invalid
+
+Resource name ``username'' represents a valid user
+
+=== Updating a JMAP user identity
+
+API to update an exist JMAP user identity
+....
+curl -XPUT http://ip:port/users/{username}/identities/{identityId} \
+-d '{
+	"name": "Bob",
+	"htmlSignature": "a html signature",
+	"textSignature": "a text signature",
+	"bcc": [{
+		"email": "boss2@domain.tld",
+		"name": "My Boss 2"
+	}],
+	"replyTo": [{
+		"email": "boss@domain.tld",
+		"name": "My Boss"
+	}],
+	"sortOrder": 1
+ }' \
+-H "Content-Type: application/json"
+....
+
+Response codes:
+
+* 204: The identity were successfully updated
+* 404: The username is unknown
+* 400: The payload is invalid
+
+Resource name ``username'' represents a valid user
+Resource name ``identityId'' represents a exist user identity
+
 == 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 0d67cb23c7..c3f80b8836 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
@@ -33,20 +33,21 @@ import reactor.core.scala.publisher.{SFlux, SMono}
 import scala.jdk.StreamConverters._
 import scala.util.Try
 import scala.jdk.OptionConverters._
+import scala.jdk.CollectionConverters._
 
 object IdentityCreationRequest {
   def fromJava(mailAddress: MailAddress,
                identityName: Optional[String],
-               replyTo: Optional[List[EmailAddress]],
-               bcc: Optional[List[EmailAddress]],
+               replyTo: Optional[java.util.List[EmailAddress]],
+               bcc: Optional[java.util.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,
+      replyTo = replyTo.toScala.map(_.asScala.toList),
+      bcc = bcc.toScala.map(_.asScala.toList),
       sortOrder = sortOrder.toScala.map(_.toInt),
       textSignature = textSignature.toScala.map(TextSignature(_)),
       htmlSignature = htmlSignature.toScala.map(HtmlSignature(_)))
@@ -94,6 +95,23 @@ case class IdentityHtmlSignatureUpdate(htmlSignature: HtmlSignature) extends Ide
   override def update(identity: Identity): Identity = identity.copy(htmlSignature = htmlSignature)
 }
 
+object IdentityUpdateRequest {
+  def fromJava(name: Optional[String],
+               replyTo: Optional[java.util.List[EmailAddress]],
+               bcc: Optional[java.util.List[EmailAddress]],
+               sortOrder: Optional[Integer],
+               textSignature: Optional[String],
+               htmlSignature: Optional[String]): IdentityUpdateRequest = {
+    IdentityUpdateRequest(
+      name = name.toScala.map(IdentityName(_)).map(IdentityNameUpdate),
+      sortOrder = sortOrder.toScala.map(IdentitySortOrderUpdate(_)),
+      replyTo = Option(IdentityReplyToUpdate(replyTo.toScala.map(_.asScala.toList))),
+      bcc = Option(IdentityBccUpdate(bcc.toScala.map(_.asScala.toList))),
+      textSignature = textSignature.toScala.map(TextSignature(_)).map(IdentityTextSignatureUpdate),
+      htmlSignature = htmlSignature.toScala.map(HtmlSignature(_)).map(IdentityHtmlSignatureUpdate))
+  }
+}
+
 case class IdentityUpdateRequest(name: Option[IdentityNameUpdate] = None,
                                  replyTo: Option[IdentityReplyToUpdate] = None,
                                  sortOrder: Option[IdentitySortOrderUpdate] = None,
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 8fca1136f6..38d9989e38 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
@@ -22,7 +22,9 @@ package org.apache.james.jmap.api.model
 import org.apache.james.core.MailAddress
 import org.apache.james.mime4j.dom.address.{AddressList, MailboxList, Mailbox => Mime4jMailbox}
 
+import java.util.Optional
 import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
 import scala.util.Try
 
 object EmailerName {
@@ -46,6 +48,9 @@ object EmailAddress {
         .map(email => EmailAddress(
           name = Option(mailbox.getName).map(EmailerName.from),
           email = email))
+
+  def from(name: Optional[String], email: MailAddress): EmailAddress =
+    EmailAddress(name.toScala.map(EmailerName(_)), email)
 }
 
 case class EmailAddress(name: Option[EmailerName], email: MailAddress) {
diff --git a/server/protocols/webadmin/webadmin-jmap/pom.xml b/server/protocols/webadmin/webadmin-jmap/pom.xml
index 164bcfe4cf..b55c91b3ef 100644
--- a/server/protocols/webadmin/webadmin-jmap/pom.xml
+++ b/server/protocols/webadmin/webadmin-jmap/pom.xml
@@ -125,6 +125,12 @@
             <groupId>${project.groupId}</groupId>
             <artifactId>james-server-data-jmap</artifactId>
         </dependency>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>james-server-data-jmap</artifactId>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>${project.groupId}</groupId>
             <artifactId>james-server-jmap-draft</artifactId>
@@ -154,12 +160,4 @@
             <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
index 8a60771704..41b66029aa 100644
--- 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
@@ -20,15 +20,21 @@
 package org.apache.james.webadmin.data.jmap;
 
 import static org.apache.james.webadmin.Constants.SEPARATOR;
+import static org.apache.james.webadmin.data.jmap.dto.UserIdentity.UserIdentityUpsert;
+import static spark.Spark.halt;
 
 import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
 
 import javax.inject.Inject;
+import javax.mail.internet.AddressException;
 
 import org.apache.james.core.Username;
+import org.apache.james.jmap.api.identity.IdentityNotFoundException;
 import org.apache.james.jmap.api.identity.IdentityRepository;
+import org.apache.james.jmap.api.model.IdentityId;
 import org.apache.james.util.FunctionalUtils;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.data.jmap.dto.UserIdentity;
@@ -37,7 +43,14 @@ import org.apache.james.webadmin.utils.JsonTransformer;
 import org.apache.james.webadmin.utils.ParametersExtractor;
 import org.eclipse.jetty.http.HttpStatus;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.datatype.guava.GuavaModule;
+import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
+
 import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
 import spark.HaltException;
 import spark.Request;
 import spark.Response;
@@ -47,15 +60,23 @@ 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 static final String IDENTITY_ID = ":identityId";
+    public static final String USERS_IDENTITY_BASE_PATH = USERS + SEPARATOR + USER_NAME + SEPARATOR + "/identities";
+
     private Service service;
     private final IdentityRepository identityRepository;
     private final JsonTransformer jsonTransformer;
+    private final ObjectMapper jsonDeserialize;
 
     @Inject
     public UserIdentityRoutes(IdentityRepository identityRepository,
                               JsonTransformer jsonTransformer) {
         this.identityRepository = identityRepository;
         this.jsonTransformer = jsonTransformer;
+        this.jsonDeserialize =  new ObjectMapper()
+            .registerModule(new Jdk8Module())
+            .registerModule(new GuavaModule());
+        this.jsonDeserialize.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
     }
 
     @Override
@@ -67,10 +88,20 @@ public class UserIdentityRoutes implements Routes {
     public void define(Service service) {
         this.service = service;
         getUserIdentities();
+        createUserIdentity();
+        updateUserIdentity();
     }
 
     public void getUserIdentities() {
-        service.get(USERS + SEPARATOR + USER_NAME + SEPARATOR + IDENTITIES, this::listIdentities, jsonTransformer);
+        service.get(USERS_IDENTITY_BASE_PATH, this::listIdentities, jsonTransformer);
+    }
+
+    public void createUserIdentity() {
+        service.post(USERS_IDENTITY_BASE_PATH, this::createIdentity);
+    }
+
+    public void updateUserIdentity() {
+        service.put(USERS_IDENTITY_BASE_PATH + SEPARATOR + IDENTITY_ID, this::updateIdentity);
     }
 
     private List<UserIdentity> listIdentities(Request request, Response response) {
@@ -90,6 +121,44 @@ public class UserIdentityRoutes implements Routes {
             .orElse(identities);
     }
 
+    private HaltException createIdentity(Request request, Response response) {
+        Username username = extractUsername(request);
+        try {
+            UserIdentityUpsert creationRequest = jsonDeserialize.readValue(request.body(), UserIdentityUpsert.class);
+            Mono.from(identityRepository.save(username, creationRequest.asCreationRequest())).block();
+            return halt(HttpStatus.CREATED_201);
+        } catch (AddressException | JsonProcessingException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("JSON payload of the request is not valid")
+                .cause(e)
+                .haltError();
+        }
+    }
+
+    private HaltException updateIdentity(Request request, Response response) {
+        Username username = extractUsername(request);
+        IdentityId identityId = Optional.ofNullable(request.params(IDENTITY_ID))
+            .map(UUID::fromString)
+            .map(IdentityId::new)
+            .orElseThrow(() -> new IllegalArgumentException("Can not parse identityId"));
+        try {
+            UserIdentityUpsert updateRequest = jsonDeserialize.readValue(request.body(), UserIdentityUpsert.class);
+            Mono.from(identityRepository.update(username, identityId, updateRequest.asUpdateRequest())).block();
+            return halt(HttpStatus.NO_CONTENT_204);
+        } catch (JsonProcessingException e) {
+            throw ErrorResponder.builder()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .message("JSON payload of the request is not valid")
+                .cause(e)
+                .haltError();
+        } catch (IdentityNotFoundException notFoundException) {
+            throw throw404(String.format("IdentityId '%s' can not be found", identityId.id().toString()));
+        }
+    }
+
     private Optional<UserIdentity> getDefaultIdentity(List<UserIdentity> identities) {
         return identities.stream()
             .filter(UserIdentity::getMayDelete)
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
index 3e58290732..8c53ac6876 100644
--- 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
@@ -21,12 +21,20 @@ package org.apache.james.webadmin.data.jmap.dto;
 
 
 import java.util.List;
+import java.util.Optional;
 import java.util.stream.Collectors;
 
+import javax.mail.internet.AddressException;
+
 import org.apache.james.core.MailAddress;
+import org.apache.james.jmap.api.identity.IdentityCreationRequest;
+import org.apache.james.jmap.api.identity.IdentityUpdateRequest;
 import org.apache.james.jmap.api.model.Identity;
 
+import com.fasterxml.jackson.annotation.JsonCreator;
 import com.fasterxml.jackson.annotation.JsonProperty;
+import com.github.fge.lambdas.Throwing;
+import com.google.common.base.Preconditions;
 
 import scala.jdk.javaapi.CollectionConverters;
 import scala.jdk.javaapi.OptionConverters;
@@ -62,6 +70,13 @@ public class UserIdentity {
             this.mailAddress = mailAddress.asString();
         }
 
+        @JsonCreator
+        public EmailAddress(@JsonProperty("name") String emailerName,
+                            @JsonProperty("email") String mailAddress) {
+            this.emailerName = emailerName;
+            this.mailAddress = mailAddress;
+        }
+
         public String getEmailerName() {
             return emailerName;
         }
@@ -69,6 +84,71 @@ public class UserIdentity {
         public String getMailAddress() {
             return mailAddress;
         }
+
+        public org.apache.james.jmap.api.model.EmailAddress asScalaEmailAddress() throws AddressException {
+            return org.apache.james.jmap.api.model.EmailAddress.from(Optional.ofNullable(getEmailerName()), new MailAddress(getMailAddress()));
+        }
+    }
+
+    public static class UserIdentityUpsert {
+        private final String name;
+        private final String email;
+        private final String textSignature;
+        private final String htmlSignature;
+        private final Integer sortOrder;
+        private final List<EmailAddress> bcc;
+        private final List<EmailAddress> replyTo;
+
+        @JsonCreator
+        public UserIdentityUpsert(@JsonProperty("name") String name,
+                                  @JsonProperty("email") String email,
+                                  @JsonProperty("textSignature") String textSignature,
+                                  @JsonProperty("htmlSignature") String htmlSignature,
+                                  @JsonProperty("sortOrder") Integer sortOrder,
+                                  @JsonProperty("bcc") List<EmailAddress> bcc,
+                                  @JsonProperty("replyTo") List<EmailAddress> replyTo) {
+            this.name = name;
+            this.email = email;
+            this.textSignature = textSignature;
+            this.htmlSignature = htmlSignature;
+            this.sortOrder = sortOrder;
+            this.bcc = bcc;
+            this.replyTo = replyTo;
+        }
+
+        public IdentityCreationRequest asCreationRequest() throws AddressException {
+            Preconditions.checkArgument(email != null, "email must be not null");
+            return IdentityCreationRequest.fromJava(
+                new MailAddress(email),
+                Optional.ofNullable(name),
+                Optional.ofNullable(replyTo)
+                    .map(rt -> rt.stream()
+                        .map(Throwing.function(EmailAddress::asScalaEmailAddress))
+                        .collect(Collectors.toList())),
+                Optional.ofNullable(bcc)
+                    .map(rt -> rt.stream()
+                        .map(Throwing.function(EmailAddress::asScalaEmailAddress))
+                        .collect(Collectors.toList())),
+                Optional.ofNullable(sortOrder),
+                Optional.ofNullable(textSignature),
+                Optional.ofNullable(htmlSignature));
+        }
+
+        public IdentityUpdateRequest asUpdateRequest() {
+            return IdentityUpdateRequest.fromJava(
+                Optional.ofNullable(name),
+                Optional.ofNullable(replyTo)
+                    .map(rt -> rt.stream()
+                        .map(Throwing.function(EmailAddress::asScalaEmailAddress))
+                        .collect(Collectors.toList())),
+                Optional.ofNullable(bcc)
+                    .map(rt -> rt.stream()
+                        .map(Throwing.function(EmailAddress::asScalaEmailAddress))
+                        .collect(Collectors.toList())),
+                Optional.ofNullable(sortOrder),
+                Optional.ofNullable(textSignature),
+                Optional.ofNullable(htmlSignature));
+        }
     }
 
     private static List<EmailAddress> getBccFromIdentity(Identity identity) {
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
index 51038427a1..e9a7a043a4 100644
--- 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
@@ -22,23 +22,24 @@ 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.assertj.core.api.Assertions.assertThat;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 
 import java.util.List;
 import java.util.Optional;
+import java.util.UUID;
 
 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.identity.IdentityRepositoryTest;
 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;
@@ -53,6 +54,7 @@ import org.mockito.Mockito;
 
 import io.restassured.RestAssured;
 import net.javacrumbs.jsonunit.core.Option;
+import reactor.core.publisher.Flux;
 import reactor.core.publisher.Mono;
 import reactor.core.scala.publisher.SMono;
 import scala.jdk.javaapi.CollectionConverters;
@@ -93,17 +95,15 @@ class UserIdentitiesRoutesTest {
     void listIdentitiesShouldReturnBothCustomAndServerSetIdentities() throws Exception {
         // identity: server set
         Mockito.when(identityFactory.listIdentities(BOB))
-            .thenReturn(CollectionConverters.asScala(List.of(UserIdentitiesHelper.IDENTITY1())).toList());
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.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(List.of(EmailAddress.from(Optional.empty(), new MailAddress("replyTo1@james.org")),
+                EmailAddress.from(Optional.empty(), new MailAddress("replyTo2@james.org")))),
+            Optional.of(List.of(EmailAddress.from(Optional.empty(), new MailAddress("bcc1@james.org")),
+                EmailAddress.from(Optional.empty(), new MailAddress("bcc2@james.org")))),
             Optional.of(1),
             Optional.of("textSignature 1"),
             Optional.of("htmlSignature 1"));
@@ -151,12 +151,12 @@ class UserIdentitiesRoutesTest {
             "        ]" +
             "    }," +
             "    {" +
-            "        \"name\": \"base name\"," +
+            "        \"name\": \"\"," +
             "        \"email\": \"bob@domain.tld\"," +
             "        \"id\": \"${json-unit.ignore}\"," +
             "        \"mayDelete\": false," +
-            "        \"textSignature\": \"text signature base\"," +
-            "        \"htmlSignature\": \"html signature base\"," +
+            "        \"textSignature\": \"\"," +
+            "        \"htmlSignature\": \"\"," +
             "        \"sortOrder\": 100," +
             "        \"bcc\": [" +
             "            {" +
@@ -181,15 +181,15 @@ class UserIdentitiesRoutesTest {
     void listIdentitiesShouldSupportDefaultParam() throws Exception {
         // identity: server set
         Mockito.when(identityFactory.listIdentities(BOB))
-            .thenReturn(CollectionConverters.asScala(List.of(UserIdentitiesHelper.IDENTITY1())).toList());
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.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(List.of(EmailAddress.from(Optional.of("reply name 1"), new MailAddress("reply1@domain.org")))),
+            Optional.of(List.of(EmailAddress.from(Optional.of("bcc name 1"), new MailAddress("bcc1@domain.org")))),
             Optional.of(highPriorityOrder),
             Optional.of("textSignature 1"),
             Optional.of("htmlSignature 1"));
@@ -197,8 +197,8 @@ class UserIdentitiesRoutesTest {
         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(List.of(EmailAddress.from(Optional.of("reply name 2"), new MailAddress("reply2@domain.org")))),
+            Optional.of(List.of(EmailAddress.from(Optional.of("bcc name 2"), new MailAddress("bcc2@domain.org")))),
             Optional.of(lowPriorityOrder),
             Optional.of("textSignature 2"),
             Optional.of("htmlSignature 2"));
@@ -248,7 +248,7 @@ class UserIdentitiesRoutesTest {
     @Test
     void listIdentitiesShouldReturnBadRequestWhenInvalidDefaultParam() {
         Mockito.when(identityFactory.listIdentities(BOB))
-            .thenReturn(CollectionConverters.asScala(List.of(UserIdentitiesHelper.IDENTITY1())).toList());
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
 
         String response = given()
             .queryParam("default", "invalid")
@@ -292,4 +292,457 @@ class UserIdentitiesRoutesTest {
                 "    \"details\": null" +
                 "}");
     }
+
+    @Test
+    void createIdentityShouldWork() {
+        String creationRequest = "" +
+            "    {" +
+            "        \"name\": \"create name 1\"," +
+            "        \"email\": \"bob@domain.tld\"," +
+            "        \"textSignature\": \"create textSignature1\"," +
+            "        \"htmlSignature\": \"create htmlSignature1\"," +
+            "        \"sortOrder\": 99," +
+            "        \"bcc\": [" +
+            "            {" +
+            "                \"name\": \"create bcc 1\"," +
+            "                \"email\": \"create_boss_bcc_1@domain.tld\"" +
+            "            }" +
+            "        ]," +
+            "        \"replyTo\": [" +
+            "            {" +
+            "                \"name\": \"create replyTo 1\"," +
+            "                \"email\": \"create_boss1@domain.tld\"" +
+            "            }" +
+            "        ]" +
+            "    }" +
+            "";
+
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        given()
+            .body(creationRequest)
+            .post(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.CREATED_201);
+
+        assertThat(Flux.from(identityRepository.list(BOB)).collectList().block())
+            .hasSize(2);
+
+        // verify by api get user identities
+        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();
+
+        assertThatJson(response)
+            .isArray()
+            .contains("{" +
+                "        \"name\": \"create name 1\"," +
+                "        \"email\": \"bob@domain.tld\"," +
+                "        \"mayDelete\": true," +
+                "        \"textSignature\": \"create textSignature1\"," +
+                "        \"htmlSignature\": \"create htmlSignature1\"," +
+                "        \"sortOrder\": 99," +
+                "        \"bcc\": [" +
+                "            {" +
+                "                \"name\": \"create bcc 1\"," +
+                "                \"email\": \"create_boss_bcc_1@domain.tld\"" +
+                "            }" +
+                "        ]," +
+                "        \"replyTo\": [" +
+                "            {" +
+                "                \"name\": \"create replyTo 1\"," +
+                "                \"email\": \"create_boss1@domain.tld\"" +
+                "            }" +
+                "        ]," +
+                "        \"id\": \"${json-unit.ignore}\"" +
+                "    }");
+    }
+
+    @Test
+    void createIdentityShouldWorkWhenMissingOptionalPropertiesInRequest() {
+        String creationRequest = "" +
+            "    {" +
+            "        \"email\": \"bob@domain.tld\"" +
+            "    }" +
+            "";
+
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        given()
+            .body(creationRequest)
+            .post(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.CREATED_201);
+
+        assertThat(Flux.from(identityRepository.list(BOB)).collectList().block())
+            .hasSize(2);
+
+        // verify by api get user identities
+        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();
+
+        assertThatJson(response)
+            .isArray()
+            .contains("{" +
+                "        \"name\": \"\"," +
+                "        \"email\": \"bob@domain.tld\"," +
+                "        \"mayDelete\": true," +
+                "        \"textSignature\": \"\"," +
+                "        \"htmlSignature\": \"\"," +
+                "        \"sortOrder\": 100," +
+                "        \"bcc\": []," +
+                "        \"replyTo\": []," +
+                "        \"id\": \"${json-unit.ignore}\"" +
+                "    }");
+    }
+
+    @Test
+    void createIdentityShouldFailWhenMissingRequirePropertyInRequest() {
+        String creationRequest = "{" +
+            "        \"sortOrder\": 99" +
+            "    }";
+
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        String response = given()
+            .body(creationRequest)
+            .post(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .extract()
+            .body().asString();
+
+        assertThatJson(response)
+            .isEqualTo("{" +
+                "    \"statusCode\": 400," +
+                "    \"type\": \"InvalidArgument\"," +
+                "    \"message\": \"Invalid arguments supplied in the user request\"," +
+                "    \"details\": \"email must be not null\"" +
+                "}");
+    }
+
+    @Test
+    void createIdentityShouldFailWhenInvalidRequest() {
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        String response = given()
+            .body("invalid")
+            .post(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .extract()
+            .body().asString();
+
+        assertThatJson(response)
+            .isEqualTo("{" +
+                "    \"statusCode\": 400," +
+                "    \"type\": \"InvalidArgument\"," +
+                "    \"message\": \"JSON payload of the request is not valid\"," +
+                "    \"details\": \"${json-unit.ignore}\"" +
+                "}");
+    }
+
+    @Test
+    void createIdentityShouldNotSupportMayDeleteProperty() {
+        String creationRequest = "" +
+            "    {" +
+            "        \"mayDelete\": \"false\"," +
+            "        \"name\": \"create11\"," +
+            "        \"email\": \"bob@domain.tld\"" +
+            "    }" +
+            "";
+
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        given()
+            .body(creationRequest)
+            .post(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()))
+        .then()
+            .statusCode(HttpStatus.CREATED_201);
+
+        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();
+
+        assertThatJson(response)
+            .isArray()
+            .contains("{" +
+                "        \"name\": \"create11\"," +
+                "        \"email\": \"bob@domain.tld\"," +
+                "        \"textSignature\": \"\"," +
+                "        \"htmlSignature\": \"\"," +
+                "        \"sortOrder\": 100," +
+                "        \"bcc\": []," +
+                "        \"replyTo\": []," +
+                "        \"id\": \"${json-unit.ignore}\"," +
+                "        \"mayDelete\": true" +
+                "    }");
+    }
+
+    @Test
+    void updateIdentityShouldWork() throws Exception {
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        IdentityCreationRequest creationRequest = IdentityCreationRequest.fromJava(
+            BOB.asMailAddress(),
+            Optional.of("identity name 1"),
+            Optional.empty(),
+            Optional.empty(),
+            Optional.empty(),
+            Optional.empty(),
+            Optional.empty());
+
+        Identity customIdentity = Mono.from(identityRepository.save(BOB, creationRequest)).block();
+        String updateRequest = "" +
+            "    {" +
+            "        \"name\": \"identity name 1 changed\"," +
+            "        \"textSignature\": \"create textSignature1\"," +
+            "        \"htmlSignature\": \"create htmlSignature1\"," +
+            "        \"sortOrder\": 99," +
+            "        \"bcc\": [" +
+            "            {" +
+            "                \"name\": \"create bcc 1\"," +
+            "                \"email\": \"create_boss_bcc_1@domain.tld\"" +
+            "            }" +
+            "        ]," +
+            "        \"replyTo\": [" +
+            "            {" +
+            "                \"name\": \"create replyTo 1\"," +
+            "                \"email\": \"create_boss1@domain.tld\"" +
+            "            }" +
+            "        ]" +
+            "    }" +
+            "";
+
+        String customIdentityId = customIdentity.id().id().toString();
+
+        given()
+            .body(updateRequest)
+            .put(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()) + "/" + customIdentityId)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        // verify by api get user identities
+        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();
+
+        assertThatJson(response)
+            .isArray()
+            .contains(String.format("{" +
+                "        \"name\": \"identity name 1 changed\"," +
+                "        \"email\": \"bob@domain.tld\"," +
+                "        \"textSignature\": \"create textSignature1\"," +
+                "        \"htmlSignature\": \"create htmlSignature1\"," +
+                "        \"sortOrder\": 99," +
+                "        \"bcc\": [" +
+                "            {" +
+                "                \"name\": \"create bcc 1\"," +
+                "                \"email\": \"create_boss_bcc_1@domain.tld\"" +
+                "            }" +
+                "        ]," +
+                "        \"replyTo\": [" +
+                "            {" +
+                "                \"name\": \"create replyTo 1\"," +
+                "                \"email\": \"create_boss1@domain.tld\"" +
+                "            }" +
+                "        ]," +
+                "        \"id\": \"%s\"," +
+                "        \"mayDelete\": true" +
+                "    }", customIdentityId));
+    }
+
+    @Test
+    void updateIdentityShouldFailWhenIdNotFound() {
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        String updateRequest = "" +
+            "    {" +
+            "        \"name\": \"identity name 1 changed\"," +
+            "        \"textSignature\": \"create textSignature1\"," +
+            "        \"htmlSignature\": \"create htmlSignature1\"," +
+            "        \"sortOrder\": 99," +
+            "        \"bcc\": [" +
+            "            {" +
+            "                \"name\": \"create bcc 1\"," +
+            "                \"email\": \"create_boss_bcc_1@domain.tld\"" +
+            "            }" +
+            "        ]," +
+            "        \"replyTo\": [" +
+            "            {" +
+            "                \"name\": \"create replyTo 1\"," +
+            "                \"email\": \"create_boss1@domain.tld\"" +
+            "            }" +
+            "        ]" +
+            "    }" +
+            "";
+
+        String notFoundIdentityId = UUID.randomUUID().toString();
+
+        String response = given()
+            .body(updateRequest)
+            .put(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()) + "/" + notFoundIdentityId)
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404)
+            .extract()
+            .body()
+            .asString();
+
+        assertThatJson(response)
+            .isEqualTo(String.format("{" +
+                "    \"statusCode\": 404," +
+                "    \"type\": \"notFound\"," +
+                "    \"message\": \"IdentityId '%s' can not be found\"," +
+                "    \"details\": null" +
+                "}", notFoundIdentityId));
+    }
+
+    @Test
+    void updateIdentityShouldNotModifyAbsentPropertyInRequest() throws Exception {
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        IdentityCreationRequest creationRequest = IdentityCreationRequest.fromJava(
+            BOB.asMailAddress(),
+            Optional.of("identity name 1"),
+            Optional.of(List.of(EmailAddress.from(Optional.of("reply name 1"), new MailAddress("reply1@domain.org")))),
+            Optional.empty(),
+            Optional.empty(),
+            Optional.of("textSignature 1"),
+            Optional.empty());
+
+        Identity customIdentity = Mono.from(identityRepository.save(BOB, creationRequest)).block();
+        String updateRequest = "" +
+            "    {" +
+            "        \"name\": null," +
+            "        \"textSignature\": \"\"," +
+            "        \"htmlSignature\": \"htmlSignature 1\"," +
+            "        \"bcc\": [" +
+            "            {" +
+            "                \"name\": \"create bcc 1\"," +
+            "                \"email\": \"create_boss_bcc_1@domain.tld\"" +
+            "            }" +
+            "        ]," +
+            "        \"replyTo\": []" +
+            "    }" +
+            "";
+
+        String customIdentityId = customIdentity.id().id().toString();
+
+        given()
+            .body(updateRequest)
+            .put(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()) + "/" + customIdentityId)
+        .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        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();
+
+        assertThatJson(response)
+            .isArray()
+            .contains(String.format("{" +
+                "    \"name\": \"identity name 1\"," +
+                "    \"email\": \"bob@domain.tld\"," +
+                "    \"textSignature\": \"\"," +
+                "    \"htmlSignature\": \"htmlSignature 1\"," +
+                "    \"sortOrder\": 100," +
+                "    \"bcc\": [" +
+                "        {" +
+                "            \"name\": \"create bcc 1\"," +
+                "            \"email\": \"create_boss_bcc_1@domain.tld\"" +
+                "        }" +
+                "    ]," +
+                "    \"replyTo\": []," +
+                "    \"id\": \"%s\"," +
+                "    \"mayDelete\": true" +
+                "}", customIdentityId));
+    }
+
+    @Test
+    void updateIdentityShouldNotAcceptChangeMayDeleteProperty() throws Exception {
+        Mockito.when(identityFactory.listIdentities(BOB))
+            .thenReturn(CollectionConverters.asScala(List.of(IdentityRepositoryTest.IDENTITY1())).toList());
+
+        IdentityCreationRequest creationRequest = IdentityCreationRequest.fromJava(
+            BOB.asMailAddress(),
+            Optional.of("identity name 1"),
+            Optional.empty(),
+            Optional.empty(),
+            Optional.empty(),
+            Optional.empty(),
+            Optional.empty());
+
+        Identity customIdentity = Mono.from(identityRepository.save(BOB, creationRequest)).block();
+        String updateRequest = "" +
+            "    {" +
+            "        \"mayDelete\": \"false\"" +
+            "    }" +
+            "";
+
+        String customIdentityId = customIdentity.id().id().toString();
+
+        given()
+            .body(updateRequest)
+            .put(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()) + "/" + customIdentityId)
+            .then()
+            .statusCode(HttpStatus.NO_CONTENT_204);
+
+        // verify by api get user identities
+        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();
+
+        assertThatJson(response)
+            .isArray()
+            .contains(String.format("{" +
+                "    \"name\": \"identity name 1\"," +
+                "    \"email\": \"bob@domain.tld\"," +
+                "    \"textSignature\": \"\"," +
+                "    \"htmlSignature\": \"\"," +
+                "    \"sortOrder\": 100," +
+                "    \"bcc\": []," +
+                "    \"replyTo\": []," +
+                "    \"id\": \"%s\"," +
+                "    \"mayDelete\": true" +
+                "}", customIdentityId));
+    }
 }
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
deleted file mode 100644
index 4cbaf60b3f..0000000000
--- a/server/protocols/webadmin/webadmin-jmap/src/test/scala/org/apache/james/webadmin/data/jmap/UserIdentitiesHelper.scala
+++ /dev/null
@@ -1,38 +0,0 @@
-/****************************************************************
- * 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 f4e5577ab7..a3edcf7f75 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -570,6 +570,71 @@ Response codes:
  - 400: The user is invalid
  - 404: The user is unknown or the default identity can not be found.
 
+### Creating a JMAP user identity
+
+API to create a new JMAP user identity
+
+```
+curl -XPOST http://ip:port/users/{username}/identities \
+-d '{
+	"name": "Bob",
+	"email": "bob@domain.tld",
+	"mayDelete": true,
+	"htmlSignature": "a html signature",
+	"textSignature": "a text signature",
+	"bcc": [{
+		"email": "boss2@domain.tld",
+		"name": "My Boss 2"
+	}],
+	"replyTo": [{
+		"email": "boss@domain.tld",
+		"name": "My Boss"
+	}],
+	"sortOrder": 0
+    }' \
+-H "Content-Type: application/json"
+```
+
+Response codes:
+
+* 201: The new identity was successfully created
+* 404: The username is unknown
+* 400: The payload is invalid
+
+Resource name `username` represents a valid user
+
+### Updating a JMAP user identity
+
+API to update an exist JMAP user identity
+
+```
+curl -XPUT http://ip:port/users/{username}/identities/{identityId} \
+-d '{
+	"name": "Bob",
+	"htmlSignature": "a html signature",
+	"textSignature": "a text signature",
+	"bcc": [{
+		"email": "boss2@domain.tld",
+		"name": "My Boss 2"
+	}],
+	"replyTo": [{
+		"email": "boss@domain.tld",
+		"name": "My Boss"
+	}],
+	"sortOrder": 1
+    }' \
+-H "Content-Type: application/json"
+```
+
+Response codes:
+
+ - 204: The identity were successfully updated
+ - 404: The username is unknown
+ - 400: The payload is invalid
+
+Resource name `username` represents a valid user.
+Resource name `identityId` represents a exist user identity
+
 ## Administrating mailboxes
 
 ### All mailboxes


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