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 dd3b6cb1a0 JAMES-3893 Allow deleting identities via WebAdmin
dd3b6cb1a0 is described below
commit dd3b6cb1a077a2fc16d7b8884f3853748f2fabf2
Author: Benoit TELLIER <[email protected]>
AuthorDate: Thu Apr 16 21:33:09 2026 +0200
JAMES-3893 Allow deleting identities via WebAdmin
---
.../modules/servers/partials/operate/webadmin.adoc | 17 ++++++
.../webadmin/data/jmap/UserIdentityRoutes.java | 25 +++++++++
.../data/jmap/UserIdentitiesRoutesTest.java | 63 ++++++++++++++++++++++
src/site/markdown/server/manage-webadmin.md | 17 ++++++
4 files changed, 122 insertions(+)
diff --git a/docs/modules/servers/partials/operate/webadmin.adoc
b/docs/modules/servers/partials/operate/webadmin.adoc
index 7fcbcd3edb..442861e3ae 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -987,6 +987,23 @@ Response codes:
Resource name ``username'' represents a valid user
Resource name ``identityId'' represents a exist user identity
+=== Deleting a JMAP user identity
+
+API to delete a JMAP user identity
+
+....
+curl -XDELETE http://ip:port/users/{username}/identities/{identityId}
+....
+
+Response codes:
+
+* 204: The identity was successfully deleted
+* 403: The identity is a server-set identity and cannot be deleted
+* 400: The identityId is invalid
+
+Resource name ``username'' represents a valid user
+Resource name ``identityId'' represents an existing user identity
+
== Administrating vacation settings
=== Get vacation settings
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 473318385a..da9381e9f4 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
@@ -32,6 +32,7 @@ import jakarta.inject.Inject;
import jakarta.mail.internet.AddressException;
import org.apache.james.core.Username;
+import org.apache.james.jmap.api.identity.IdentityForbiddenDeleteException;
import org.apache.james.jmap.api.identity.IdentityNotFoundException;
import org.apache.james.jmap.api.identity.IdentityRepository;
import org.apache.james.jmap.api.model.IdentityId;
@@ -53,6 +54,7 @@ import com.fasterxml.jackson.datatype.jdk8.Jdk8Module;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
+import scala.jdk.javaapi.CollectionConverters;
import spark.HaltException;
import spark.Request;
import spark.Response;
@@ -93,6 +95,7 @@ public class UserIdentityRoutes implements Routes {
getUserIdentities();
createUserIdentity();
updateUserIdentity();
+ deleteUserIdentity();
}
public void getUserIdentities() {
@@ -107,6 +110,10 @@ public class UserIdentityRoutes implements Routes {
service.put(USERS_IDENTITY_BASE_PATH + SEPARATOR + IDENTITY_ID,
this::updateIdentity);
}
+ public void deleteUserIdentity() {
+ service.delete(USERS_IDENTITY_BASE_PATH + SEPARATOR + IDENTITY_ID,
this::deleteIdentity);
+ }
+
private List<UserIdentity> listIdentities(Request request, Response
response) {
try {
Username username = extractUsername(request);
@@ -189,6 +196,24 @@ public class UserIdentityRoutes implements Routes {
}
}
+ private HaltException deleteIdentity(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 {
+ Mono.from(identityRepository.delete(username,
CollectionConverters.asScala(java.util.Set.of(identityId)).toSet())).block();
+ return halt(HttpStatus.NO_CONTENT_204);
+ } catch (IdentityForbiddenDeleteException e) {
+ throw ErrorResponder.builder()
+ .statusCode(HttpStatus.FORBIDDEN_403)
+ .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+ .message(String.format("IdentityId '%s' can not be deleted",
identityId.id().toString()))
+ .haltError();
+ }
+ }
+
private Optional<UserIdentity> getDefaultIdentity(List<UserIdentity>
identities) {
return identities.stream()
.filter(UserIdentity::getMayDelete)
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 d07729dca6..628c131eb7 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
@@ -691,6 +691,69 @@ class UserIdentitiesRoutesTest {
"}", customIdentityId));
}
+ @Test
+ void deleteIdentityShouldWork() throws Exception {
+ Mockito.when(identityFactory.listIdentities(BOB))
+ .thenReturn(Flux.empty());
+
+ 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 customIdentityId = customIdentity.id().id().toString();
+
+ given()
+ .delete(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()) +
"/" + customIdentityId)
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT_204);
+
+
assertThat(Flux.from(identityRepository.list(BOB)).collectList().block())
+ .isEmpty();
+ }
+
+ @Test
+ void deleteIdentityShouldReturnNoContentWhenIdentityNotFound() {
+ Mockito.when(identityFactory.listIdentities(BOB))
+ .thenReturn(Flux.empty());
+
+ String notFoundIdentityId = UUID.randomUUID().toString();
+
+ given()
+ .delete(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()) +
"/" + notFoundIdentityId)
+ .then()
+ .statusCode(HttpStatus.NO_CONTENT_204);
+ }
+
+ @Test
+ void deleteIdentityShouldReturnForbiddenWhenServerSetIdentity() throws
Exception {
+ Mockito.when(identityFactory.listIdentities(BOB))
+ .thenReturn(Flux.just(IdentityRepositoryTest.IDENTITY1()));
+
+ String serverSetIdentityId =
IdentityRepositoryTest.IDENTITY1().id().id().toString();
+
+ String response = given()
+ .delete(String.format(GET_IDENTITIES_USERS_PATH, BOB.asString()) +
"/" + serverSetIdentityId)
+ .then()
+ .statusCode(HttpStatus.FORBIDDEN_403)
+ .extract()
+ .body()
+ .asString();
+
+ assertThatJson(response)
+ .isEqualTo(String.format("{" +
+ " \"statusCode\": 403," +
+ " \"type\": \"InvalidArgument\"," +
+ " \"message\": \"IdentityId '%s' can not be deleted\"," +
+ " \"details\": null" +
+ "}", serverSetIdentityId));
+ }
+
@Test
void updateIdentityShouldNotAcceptChangeMayDeleteProperty() throws
Exception {
Mockito.when(identityFactory.listIdentities(BOB))
diff --git a/src/site/markdown/server/manage-webadmin.md
b/src/site/markdown/server/manage-webadmin.md
index ed32669692..73a61639e7 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -797,6 +797,23 @@ Response codes:
Resource name `username` represents a valid user.
Resource name `identityId` represents a exist user identity
+### Deleting a JMAP user identity
+
+API to delete a JMAP user identity
+
+```
+curl -XDELETE http://ip:port/users/{username}/identities/{identityId}
+```
+
+Response codes:
+
+ - 204: The identity was successfully deleted
+ - 403: The identity is a server-set identity and cannot be deleted
+ - 400: The identityId is invalid
+
+Resource name `username` represents a valid user.
+Resource name `identityId` represents an existing user identity
+
## Administrating mailboxes
### All mailboxes
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]