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