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]

Reply via email to