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/05/25 05:45:21 UTC

[james-project] 01/03: JAMES-3909 Task + Webadmin route for delete all users data of a domain

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

commit 79092f21427011722b656fca5c58b5044235424f
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Fri May 19 17:18:51 2023 +0700

    JAMES-3909 Task + Webadmin route for delete all users data of a domain
---
 .../docs/modules/ROOT/pages/operate/webadmin.adoc  |  27 +++
 .../james/modules/server/DataRoutesModules.java    |  20 +++
 .../james/webadmin/routes/DomainsRoutes.java       |  33 +++-
 .../webadmin/service/DeleteUserDataService.java    |   4 +
 .../service/DeleteUsersDataOfDomainTask.java       | 173 ++++++++++++++++++
 ...rsDataOfDomainTaskAdditionalInformationDTO.java |  81 +++++++++
 .../service/DeleteUsersDataOfDomainTaskDTO.java    |  68 ++++++++
 .../james/webadmin/routes/DomainsRoutesTest.java   | 174 +++++++++++++++++-
 ...leteUsersDataOfDomainTaskSerializationTest.java |  93 ++++++++++
 .../service/DeleteUsersDataOfDomainTaskTest.java   | 194 +++++++++++++++++++++
 src/site/markdown/server/manage-webadmin.md        |  28 +++
 11 files changed, 892 insertions(+), 3 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 1a343c7d30..0a69f5618f 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
@@ -455,6 +455,33 @@ syntax
 * 400: source, domain and destination domain are the same
 * 404: `source.domain.tld` are not part of handled domains.
 
+=== Delete all users data of a domain
+
+....
+curl -XPOST http://ip:port/domains/{domainToBeUsed}?action=deleteData
+....
+
+Would create a task that deletes data of all users of the domain.
+
+[More details about endpoints returning a task](#_endpoints_returning_a_task).
+
+Response codes:
+
+* 201: Success. Corresponding task id is returned.
+* 400: Error in the request. Details can be found in the reported error.
+
+The scheduled task will have the following type `DeleteUsersDataOfDomainTask` and the following `additionalInformation`:
+
+....
+{
+        "type": "DeleteUsersDataOfDomainTask",
+        "domain": "domain.tld",
+        "successfulUsersCount": 2,
+        "failedUsersCount": 1,
+        "timestamp": "2023-05-22T08:52:47.076261Z"
+}
+....
+
 == Administrating users
 
 === Create a user
diff --git a/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java b/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
index 5b3addf079..48fcf5efe2 100644
--- a/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
+++ b/server/container/guice/protocols/webadmin-data/src/main/java/org/apache/james/modules/server/DataRoutesModules.java
@@ -25,6 +25,7 @@ import org.apache.james.server.task.json.dto.TaskDTO;
 import org.apache.james.server.task.json.dto.TaskDTOModule;
 import org.apache.james.task.Task;
 import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.user.api.UsersRepository;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.dto.DTOModuleInjections;
 import org.apache.james.webadmin.dto.MappingSourceModule;
@@ -44,6 +45,8 @@ import org.apache.james.webadmin.routes.UsernameChangeRoutes;
 import org.apache.james.webadmin.service.DeleteUserDataService;
 import org.apache.james.webadmin.service.DeleteUserDataTaskAdditionalInformationDTO;
 import org.apache.james.webadmin.service.DeleteUserDataTaskDTO;
+import org.apache.james.webadmin.service.DeleteUsersDataOfDomainTaskAdditionalInformationDTO;
+import org.apache.james.webadmin.service.DeleteUsersDataOfDomainTaskDTO;
 import org.apache.james.webadmin.service.UsernameChangeService;
 import org.apache.james.webadmin.service.UsernameChangeTaskAdditionalInformationDTO;
 import org.apache.james.webadmin.service.UsernameChangeTaskDTO;
@@ -108,4 +111,21 @@ public class DataRoutesModules extends AbstractModule {
     public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends AdditionalInformationDTO> webAdminDeleteUserDataTaskAdditionalInformationDTO() {
         return DeleteUserDataTaskAdditionalInformationDTO.module();
     }
+
+    // delete all users data of a domain DTO modules
+    @ProvidesIntoSet
+    public TaskDTOModule<? extends Task, ? extends TaskDTO> deleteUsersDataOfDomainTaskDTO(DeleteUserDataService service, UsersRepository usersRepository) {
+        return DeleteUsersDataOfDomainTaskDTO.module(service, usersRepository);
+    }
+
+    @ProvidesIntoSet
+    public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends AdditionalInformationDTO> deleteUsersDataOfDomainTaskAdditionalInformationDTO() {
+        return DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module();
+    }
+
+    @Named(DTOModuleInjections.WEBADMIN_DTO)
+    @ProvidesIntoSet
+    public AdditionalInformationDTOModule<? extends TaskExecutionDetails.AdditionalInformation, ? extends AdditionalInformationDTO> webAdminDeleteUsersDataOfDomainTaskAdditionalInformationDTO() {
+        return DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module();
+    }
 }
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
index 59ae633c61..34ef2f9ab9 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/routes/DomainsRoutes.java
@@ -34,9 +34,15 @@ import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.DomainListException;
 import org.apache.james.rrt.api.RecipientRewriteTableException;
 import org.apache.james.rrt.api.SameSourceAndDestinationException;
+import org.apache.james.task.TaskManager;
+import org.apache.james.user.api.UsersRepository;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.dto.DomainAliasResponse;
+import org.apache.james.webadmin.service.DeleteUserDataService;
+import org.apache.james.webadmin.service.DeleteUsersDataOfDomainTask;
 import org.apache.james.webadmin.service.DomainAliasService;
+import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+import org.apache.james.webadmin.tasks.TaskRegistrationKey;
 import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
 import org.apache.james.webadmin.utils.JsonTransformer;
@@ -45,11 +51,13 @@ import org.eclipse.jetty.http.HttpStatus;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.base.Preconditions;
 import com.google.common.collect.ImmutableSet;
 
 import spark.HaltException;
 import spark.Request;
 import spark.Response;
+import spark.Route;
 import spark.Service;
 
 public class DomainsRoutes implements Routes {
@@ -62,18 +70,26 @@ public class DomainsRoutes implements Routes {
     private static final String SPECIFIC_DOMAIN = DOMAINS + SEPARATOR + DOMAIN_NAME;
     private static final String ALIASES = "aliases";
     private static final String DOMAIN_ALIASES = SPECIFIC_DOMAIN + SEPARATOR + ALIASES;
+    private static final String DELETE_ALL_USERS_DATA_OF_A_DOMAIN_PATH = "/domains/:domainName";
     private static final String SPECIFIC_ALIAS = DOMAINS + SEPARATOR + DESTINATION_DOMAIN + SEPARATOR + ALIASES + SEPARATOR + SOURCE_DOMAIN;
+    private static final TaskRegistrationKey DELETE_USERS_DATA = TaskRegistrationKey.of("deleteData");
 
     private final DomainList domainList;
     private final DomainAliasService domainAliasService;
     private final JsonTransformer jsonTransformer;
+    private final DeleteUserDataService deleteUserDataService;
+    private final UsersRepository usersRepository;
+    private final TaskManager taskManager;
     private Service service;
 
     @Inject
-    DomainsRoutes(DomainList domainList, DomainAliasService domainAliasService, JsonTransformer jsonTransformer) {
+    DomainsRoutes(DomainList domainList, DomainAliasService domainAliasService, JsonTransformer jsonTransformer, DeleteUserDataService deleteUserDataService, UsersRepository usersRepository, TaskManager taskManager) {
         this.domainList = domainList;
         this.domainAliasService = domainAliasService;
         this.jsonTransformer = jsonTransformer;
+        this.deleteUserDataService = deleteUserDataService;
+        this.usersRepository = usersRepository;
+        this.taskManager = taskManager;
     }
 
     @Override
@@ -95,6 +111,21 @@ public class DomainsRoutes implements Routes {
         defineListAliases(service);
         defineAddAlias(service);
         defineRemoveAlias(service);
+
+        // delete data of all users of a domain
+        service.post(DELETE_ALL_USERS_DATA_OF_A_DOMAIN_PATH, deleteAllUsersData(), jsonTransformer);
+    }
+
+    public Route deleteAllUsersData() {
+        return TaskFromRequestRegistry.builder()
+            .parameterName("action")
+            .register(DELETE_USERS_DATA, request -> {
+                Domain domain = checkValidDomain(request.params(DOMAIN_NAME));
+                Preconditions.checkArgument(domainList.containsDomain(domain), "'domainName' parameter should be an existing domain");
+
+                return new DeleteUsersDataOfDomainTask(deleteUserDataService, domain, usersRepository);
+            })
+            .buildAsRoute(taskManager);
     }
 
     public void defineDeleteDomain() {
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUserDataService.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUserDataService.java
index 2bd572b68c..e24987358d 100644
--- a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUserDataService.java
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUserDataService.java
@@ -143,4 +143,8 @@ public class DeleteUserDataService {
     public Performer performer(Optional<StepName> fromStep) {
         return new Performer(steps, new DeleteUserDataStatus(steps), fromStep);
     }
+
+    public Performer performer() {
+        return new Performer(steps, new DeleteUserDataStatus(steps), Optional.empty());
+    }
 }
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTask.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTask.java
new file mode 100644
index 0000000000..84487aade6
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTask.java
@@ -0,0 +1,173 @@
+/****************************************************************
+ * 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.service;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.function.Function;
+
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.task.Task;
+import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.task.TaskType;
+import org.apache.james.user.api.UsersRepository;
+import org.reactivestreams.Publisher;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class DeleteUsersDataOfDomainTask implements Task {
+    static final TaskType TYPE = TaskType.of("DeleteUsersDataOfDomainTask");
+    private static final int LOW_CONCURRENCY = 2;
+
+    public static class AdditionalInformation implements TaskExecutionDetails.AdditionalInformation {
+        private final Instant timestamp;
+        private final Domain domain;
+        private final long successfulUsersCount;
+        private final long failedUsersCount;
+
+        public AdditionalInformation(Instant timestamp, Domain domain, long successfulUsersCount, long failedUsersCount) {
+            this.timestamp = timestamp;
+            this.domain = domain;
+            this.successfulUsersCount = successfulUsersCount;
+            this.failedUsersCount = failedUsersCount;
+        }
+
+        public Domain getDomain() {
+            return domain;
+        }
+
+        public long getSuccessfulUsersCount() {
+            return successfulUsersCount;
+        }
+
+        public long getFailedUsersCount() {
+            return failedUsersCount;
+        }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
+
+        @Override
+        public final boolean equals(Object o) {
+            if (o instanceof AdditionalInformation) {
+                AdditionalInformation that = (AdditionalInformation) o;
+
+                return Objects.equals(this.successfulUsersCount, that.successfulUsersCount)
+                    && Objects.equals(this.failedUsersCount, that.failedUsersCount)
+                    && Objects.equals(this.timestamp, that.timestamp)
+                    && Objects.equals(this.domain, that.domain);
+            }
+            return false;
+        }
+
+        @Override
+        public final int hashCode() {
+            return Objects.hash(timestamp, domain, successfulUsersCount, failedUsersCount);
+        }
+    }
+
+    static class Context {
+        private final AtomicLong successfulUsersCount;
+        private final AtomicLong failedUsersCount;
+
+        public Context() {
+            this.successfulUsersCount = new AtomicLong();
+            this.failedUsersCount = new AtomicLong();
+        }
+
+        private void increaseSuccessfulUsers() {
+            successfulUsersCount.incrementAndGet();
+        }
+
+        private void increaseFailedUsers() {
+            failedUsersCount.incrementAndGet();
+        }
+
+        public long getSuccessfulUsersCount() {
+            return successfulUsersCount.get();
+        }
+
+        public long getFailedUsersCount() {
+            return failedUsersCount.get();
+        }
+    }
+
+    private final Domain domain;
+    private final DeleteUserDataService deleteUserDataService;
+    private final UsersRepository usersRepository;
+    private final Context context;
+
+    public DeleteUsersDataOfDomainTask(DeleteUserDataService deleteUserDataService, Domain domain, UsersRepository usersRepository) {
+        this.deleteUserDataService = deleteUserDataService;
+        this.domain = domain;
+        this.usersRepository = usersRepository;
+        this.context = new Context();
+    }
+
+    @Override
+    public Result run() {
+        return Flux.from(usersRepository.listUsersOfADomainReactive(domain))
+            .flatMap(deleteUserData(), LOW_CONCURRENCY)
+            .reduce(Task::combine)
+            .switchIfEmpty(Mono.just(Result.COMPLETED))
+            .block();
+    }
+
+    private Function<Username, Publisher<Result>> deleteUserData() {
+        return username -> deleteUserDataService.performer().deleteUserData(username)
+            .then(Mono.fromCallable(() -> {
+                context.increaseSuccessfulUsers();
+                return Result.COMPLETED;
+            }))
+            .onErrorResume(error -> {
+                LOGGER.error("Error when deleting data of user {}", username.asString(), error);
+                context.increaseFailedUsers();
+                return Mono.just(Result.PARTIAL);
+            });
+    }
+
+    @Override
+    public TaskType type() {
+        return TYPE;
+    }
+
+    @Override
+    public Optional<TaskExecutionDetails.AdditionalInformation> details() {
+        return Optional.of(new AdditionalInformation(Clock.systemUTC().instant(), domain, context.getSuccessfulUsersCount(), context.getFailedUsersCount()));
+    }
+
+    public Domain getDomain() {
+        return domain;
+    }
+
+    @VisibleForTesting
+    Context getContext() {
+        return context;
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskAdditionalInformationDTO.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskAdditionalInformationDTO.java
new file mode 100644
index 0000000000..7bcd3c033d
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskAdditionalInformationDTO.java
@@ -0,0 +1,81 @@
+/****************************************************************
+ * 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.service;
+
+import java.time.Instant;
+
+import org.apache.james.core.Domain;
+import org.apache.james.json.DTOModule;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
+import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DeleteUsersDataOfDomainTaskAdditionalInformationDTO implements AdditionalInformationDTO {
+    public static AdditionalInformationDTOModule<DeleteUsersDataOfDomainTask.AdditionalInformation, DeleteUsersDataOfDomainTaskAdditionalInformationDTO> module() {
+        return DTOModule.forDomainObject(DeleteUsersDataOfDomainTask.AdditionalInformation.class)
+            .convertToDTO(DeleteUsersDataOfDomainTaskAdditionalInformationDTO.class)
+            .toDomainObjectConverter(dto -> new DeleteUsersDataOfDomainTask.AdditionalInformation(
+                dto.timestamp, Domain.of(dto.domain), dto.successfulUsersCount, dto.failedUsersCount))
+            .toDTOConverter((details, type) -> new DeleteUsersDataOfDomainTaskAdditionalInformationDTO(
+                type, details.getDomain().asString(), details.getSuccessfulUsersCount(), details.getFailedUsersCount(), details.timestamp()))
+            .typeName(DeleteUsersDataOfDomainTask.TYPE.asString())
+            .withFactory(AdditionalInformationDTOModule::new);
+    }
+
+    private final String type;
+    private final String domain;
+    private final long successfulUsersCount;
+    private final long failedUsersCount;
+    private final Instant timestamp;
+
+    public DeleteUsersDataOfDomainTaskAdditionalInformationDTO(@JsonProperty("type") String type,
+                                                               @JsonProperty("domain") String domain,
+                                                               @JsonProperty("successfulUsersCount") long successfulUsersCount,
+                                                               @JsonProperty("failedUsersCount") long failedUsersCount,
+                                                               @JsonProperty("timestamp") Instant timestamp) {
+        this.type = type;
+        this.domain = domain;
+        this.successfulUsersCount = successfulUsersCount;
+        this.failedUsersCount = failedUsersCount;
+        this.timestamp = timestamp;
+    }
+
+    public String getDomain() {
+        return domain;
+    }
+
+    public long getSuccessfulUsersCount() {
+        return successfulUsersCount;
+    }
+
+    public long getFailedUsersCount() {
+        return failedUsersCount;
+    }
+
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    @Override
+    public String getType() {
+        return type;
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskDTO.java b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskDTO.java
new file mode 100644
index 0000000000..df9f11bd6d
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/main/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskDTO.java
@@ -0,0 +1,68 @@
+/****************************************************************
+ * 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.service;
+
+import org.apache.james.core.Domain;
+import org.apache.james.json.DTOModule;
+import org.apache.james.server.task.json.dto.TaskDTO;
+import org.apache.james.server.task.json.dto.TaskDTOModule;
+import org.apache.james.user.api.UsersRepository;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class DeleteUsersDataOfDomainTaskDTO implements TaskDTO {
+
+    public static TaskDTOModule<DeleteUsersDataOfDomainTask, DeleteUsersDataOfDomainTaskDTO> module(DeleteUserDataService service, UsersRepository usersRepository) {
+        return DTOModule
+            .forDomainObject(DeleteUsersDataOfDomainTask.class)
+            .convertToDTO(DeleteUsersDataOfDomainTaskDTO.class)
+            .toDomainObjectConverter(dto -> dto.fromDTO(service, usersRepository))
+            .toDTOConverter(DeleteUsersDataOfDomainTaskDTO::toDTO)
+            .typeName(DeleteUsersDataOfDomainTask.TYPE.asString())
+            .withFactory(TaskDTOModule::new);
+    }
+
+    public static DeleteUsersDataOfDomainTaskDTO toDTO(DeleteUsersDataOfDomainTask domainObject, String typeName) {
+        return new DeleteUsersDataOfDomainTaskDTO(typeName,
+            domainObject.getDomain().asString());
+    }
+
+    private final String type;
+    private final String domain;
+
+    public DeleteUsersDataOfDomainTaskDTO(@JsonProperty("type") String type,
+                                          @JsonProperty("domain") String domain) {
+        this.type = type;
+        this.domain = domain;
+    }
+
+    public DeleteUsersDataOfDomainTask fromDTO(DeleteUserDataService service, UsersRepository usersRepository) {
+        return new DeleteUsersDataOfDomainTask(service, Domain.of(domain), usersRepository);
+    }
+
+    @Override
+    public String getType() {
+        return type;
+    }
+
+    public String getDomain() {
+        return domain;
+    }
+}
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
index e4f0b9fe7b..baefeaf289 100644
--- a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/routes/DomainsRoutesTest.java
@@ -35,41 +35,97 @@ import static org.mockito.Mockito.when;
 
 import java.net.InetAddress;
 import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
 
 import org.apache.commons.lang3.StringUtils;
 import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
 import org.apache.james.dnsservice.api.DNSService;
 import org.apache.james.domainlist.api.DomainList;
 import org.apache.james.domainlist.api.DomainListException;
 import org.apache.james.domainlist.lib.DomainListConfiguration;
 import org.apache.james.domainlist.memory.MemoryDomainList;
+import org.apache.james.json.DTOConverter;
 import org.apache.james.rrt.memory.MemoryRecipientRewriteTable;
+import org.apache.james.task.Hostname;
+import org.apache.james.task.MemoryTaskManager;
+import org.apache.james.user.api.DeleteUserDataTaskStep;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.user.memory.MemoryUsersRepository;
 import org.apache.james.webadmin.WebAdminServer;
 import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.service.DeleteUserDataService;
+import org.apache.james.webadmin.service.DeleteUsersDataOfDomainTaskAdditionalInformationDTO;
 import org.apache.james.webadmin.service.DomainAliasService;
+import org.apache.james.webadmin.utils.ErrorResponder;
 import org.apache.james.webadmin.utils.JsonTransformer;
 import org.eclipse.jetty.http.HttpStatus;
 import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Nested;
 import org.junit.jupiter.api.Test;
+import org.reactivestreams.Publisher;
 
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 
 import io.restassured.RestAssured;
 import io.restassured.http.ContentType;
+import reactor.core.publisher.Mono;
 
 class DomainsRoutesTest {
+    private static class RecordProcessedUsersStep implements DeleteUserDataTaskStep {
+        private final Set<Username> processedUsers = ConcurrentHashMap.newKeySet();
+
+        public RecordProcessedUsersStep() {
+        }
+
+        @Override
+        public StepName name() {
+            return new StepName("RecordProcessedUsersStep");
+        }
+
+        @Override
+        public int priority() {
+            return 0;
+        }
+
+        @Override
+        public Publisher<Void> deleteUserData(Username username) {
+            processedUsers.add(username);
+            return Mono.empty();
+        }
+    }
+
     private static final String DOMAIN = "domain";
     private static final String ALIAS_DOMAIN = "alias.domain";
     private static final String ALIAS_DOMAIN_2 = "alias.domain.bis";
     private static final String EXTERNAL_DOMAIN = "external.domain.tld";
 
     private WebAdminServer webAdminServer;
+    private MemoryUsersRepository usersRepository;
 
     private void createServer(DomainList domainList) {
+        MemoryTaskManager taskManager = new MemoryTaskManager(new Hostname("foo"));
+        DomainAliasService domainAliasService = new DomainAliasService(new MemoryRecipientRewriteTable(), domainList);
+        usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+        webAdminServer = WebAdminUtils.createWebAdminServer(new DomainsRoutes(domainList, domainAliasService, new JsonTransformer(),
+                    new DeleteUserDataService(Set.of()), usersRepository, taskManager), new TasksRoutes(taskManager, new JsonTransformer(), DTOConverter.of(DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module())))
+            .start();
+
+        RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .setBasePath(DomainsRoutes.DOMAINS)
+            .build();
+    }
+
+    private void createServer(DomainList domainList, Set<DeleteUserDataTaskStep> steps) {
+        MemoryTaskManager taskManager = new MemoryTaskManager(new Hostname("foo"));
         DomainAliasService domainAliasService = new DomainAliasService(new MemoryRecipientRewriteTable(), domainList);
-        webAdminServer = WebAdminUtils.createWebAdminServer(new DomainsRoutes(domainList, domainAliasService, new JsonTransformer()))
+        DeleteUserDataService deleteUserDataService = new DeleteUserDataService(steps);
+        usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+        webAdminServer = WebAdminUtils.createWebAdminServer(new DomainsRoutes(domainList, domainAliasService, new JsonTransformer(), deleteUserDataService, usersRepository, taskManager),
+                new TasksRoutes(taskManager, new JsonTransformer(), DTOConverter.of(DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module())))
             .start();
 
         RestAssured.requestSpecification = WebAdminUtils.buildRequestSpecification(webAdminServer)
@@ -82,6 +138,120 @@ class DomainsRoutesTest {
         webAdminServer.destroy();
     }
 
+    @Nested
+    class DeleteAllUsersDataTests {
+        private RecordProcessedUsersStep recordProcessedUsersStep;
+
+        @BeforeEach
+        void setUp() throws Exception {
+            DNSService dnsService = mock(DNSService.class);
+            when(dnsService.getHostName(any())).thenReturn("localhost");
+            when(dnsService.getLocalHost()).thenReturn(InetAddress.getByName("localhost"));
+            MemoryDomainList domainList = new MemoryDomainList(dnsService);
+            domainList.configure(DomainListConfiguration.builder()
+                .autoDetect(false)
+                .autoDetectIp(false)
+                .build());
+            domainList.addDomain(Domain.of("domain.tld"));
+
+            recordProcessedUsersStep = new RecordProcessedUsersStep();
+            Set<DeleteUserDataTaskStep> steps = ImmutableSet.of(new DeleteUserDataRoutesTest.StepImpl(new DeleteUserDataTaskStep.StepName("A"), 35, Mono.empty()),
+                recordProcessedUsersStep);
+            createServer(domainList, steps);
+        }
+
+        @Test
+        void shouldDeleteAllUsersDataOfTheDomain() throws UsersRepositoryException {
+            // GIVEN localhost domain has 2 users
+            usersRepository.addUser(Username.of("user1@localhost"), "secret");
+            usersRepository.addUser(Username.of("user2@localhost"), "secret");
+
+            // THEN delete all users data of localhost domain
+            String taskId = with()
+                .basePath("/domains")
+                .queryParam("action", "deleteData")
+                .post("/localhost")
+                .jsonPath()
+                .get("taskId");
+
+            given()
+                .basePath(TasksRoutes.BASE)
+            .when()
+                .get(taskId + "/await")
+            .then()
+                .body("type", is("DeleteUsersDataOfDomainTask"))
+                .body("status", is("completed"))
+                .body("additionalInformation.type", is("DeleteUsersDataOfDomainTask"))
+                .body("additionalInformation.domain", is("localhost"))
+                .body("additionalInformation.successfulUsersCount", is(2))
+                .body("additionalInformation.failedUsersCount", is(0));
+
+            // then should delete data of the 2 users
+            assertThat(recordProcessedUsersStep.processedUsers)
+                .containsExactlyInAnyOrder(Username.of("user1@localhost"), Username.of("user2@localhost"));
+        }
+
+        @Test
+        void shouldFailWhenInvalidAction() {
+            given()
+                .basePath("/domains")
+                .queryParam("action", "invalid")
+            .post("/localhost")
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", is(400))
+                .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", is("Invalid arguments supplied in the user request"))
+                .body("details", is("Invalid value supplied for query parameter 'action': invalid. Supported values are [deleteData]"));
+        }
+
+        @Test
+        void shouldFailWhenNonExistingDomain() {
+            given()
+                .basePath("/domains")
+                .queryParam("action", "deleteData")
+            .post("/nonExistingDomain")
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .body("statusCode", is(400))
+                .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+                .body("message", is("Invalid arguments supplied in the user request"))
+                .body("details", is("'domainName' parameter should be an existing domain"));
+        }
+
+        @Test
+        void shouldNotDeleteUsersDataOfOtherDomains() throws UsersRepositoryException {
+            // GIVEN localhost domain has 2 users and domain.tld domain has 1 user
+            usersRepository.addUser(Username.of("user1@localhost"), "secret");
+            usersRepository.addUser(Username.of("user2@localhost"), "secret");
+            usersRepository.addUser(Username.of("user3@domain.tld"), "secret");
+
+            // WHEN delete users data of localhost domain
+            String taskId = with()
+                .basePath("/domains")
+                .queryParam("action", "deleteData")
+                .post("/localhost")
+                .jsonPath()
+                .get("taskId");
+
+            given()
+                .basePath(TasksRoutes.BASE)
+            .when()
+                .get(taskId + "/await")
+            .then()
+                .body("type", is("DeleteUsersDataOfDomainTask"))
+                .body("status", is("completed"))
+                .body("additionalInformation.type", is("DeleteUsersDataOfDomainTask"))
+                .body("additionalInformation.domain", is("localhost"))
+                .body("additionalInformation.successfulUsersCount", is(2))
+                .body("additionalInformation.failedUsersCount", is(0));
+
+            // THEN users data of domain.tld should not be clear
+            assertThat(recordProcessedUsersStep.processedUsers)
+                .doesNotContain(Username.of("user3@domain.tld"));
+        }
+    }
+
     @Nested
     class NormalBehaviour {
 
@@ -433,7 +603,7 @@ class DomainsRoutesTest {
                 with().put(DOMAIN);
 
                 when()
-                    .put(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN).prettyPeek()
+                    .put(EXTERNAL_DOMAIN + "/aliases/" + DOMAIN)
                 .then()
                     .contentType(ContentType.JSON)
                     .statusCode(HttpStatus.NO_CONTENT_204);
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskSerializationTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskSerializationTest.java
new file mode 100644
index 0000000000..8de0149656
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskSerializationTest.java
@@ -0,0 +1,93 @@
+/****************************************************************
+ * 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.service;
+
+import static org.mockito.Mockito.mock;
+
+import java.time.Instant;
+
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.user.api.DeleteUserDataTaskStep;
+import org.apache.james.user.api.UsersRepository;
+import org.junit.jupiter.api.Test;
+import org.reactivestreams.Publisher;
+
+import com.google.common.collect.ImmutableSet;
+
+import reactor.core.publisher.Mono;
+
+class DeleteUsersDataOfDomainTaskSerializationTest {
+    private static final Instant TIMESTAMP = Instant.parse("2018-11-13T12:00:55Z");
+    private static final Domain DOMAIN = Domain.of("domain");
+    private static final long SUCCESSFUL_USERS_COUNT = 99L;
+    private static final long FAILED_USERS_COUNT = 1L;
+    private static final DeleteUserDataTaskStep.StepName STEP_A = new DeleteUserDataTaskStep.StepName("A");
+    private static final DeleteUserDataTaskStep.StepName STEP_B = new DeleteUserDataTaskStep.StepName("B");
+    private static final DeleteUserDataTaskStep.StepName STEP_C = new DeleteUserDataTaskStep.StepName("C");
+    private static final DeleteUserDataTaskStep.StepName STEP_D = new DeleteUserDataTaskStep.StepName("D");
+    private static final DeleteUserDataTaskStep A = asStep(STEP_A);
+    private static final DeleteUserDataTaskStep B = asStep(STEP_B);
+    private static final DeleteUserDataTaskStep C = asStep(STEP_C);
+    private static final DeleteUserDataTaskStep D = asStep(STEP_D);
+
+    private static DeleteUserDataTaskStep asStep(DeleteUserDataTaskStep.StepName name) {
+        return new DeleteUserDataTaskStep() {
+            @Override
+            public StepName name() {
+                return name;
+            }
+
+            @Override
+            public int priority() {
+                return 0;
+            }
+
+            @Override
+            public Publisher<Void> deleteUserData(Username username) {
+                return Mono.empty();
+            }
+        };
+    }
+
+    private static final String SERIALIZED_TASK = "{\"type\":\"DeleteUsersDataOfDomainTask\",\"domain\":\"domain\"}";
+    private static final String SERIALIZED_ADDITIONAL_INFORMATION = "{\"type\":\"DeleteUsersDataOfDomainTask\",\"domain\":\"domain\",\"successfulUsersCount\":99,\"failedUsersCount\":1,\"timestamp\":\"2018-11-13T12:00:55Z\"}";
+
+    private static final DeleteUserDataService SERVICE = new DeleteUserDataService(ImmutableSet.of(A, B, C, D));
+
+    @Test
+    void taskShouldBeSerializable() throws Exception {
+        UsersRepository usersRepository = mock(UsersRepository.class);
+        JsonSerializationVerifier.dtoModule(DeleteUsersDataOfDomainTaskDTO.module(SERVICE, usersRepository))
+            .bean(new DeleteUsersDataOfDomainTask(SERVICE, DOMAIN, usersRepository))
+            .json(SERIALIZED_TASK)
+            .verify();
+    }
+
+    @Test
+    void additionalInformationShouldBeSerializable() throws Exception {
+        JsonSerializationVerifier.dtoModule(DeleteUsersDataOfDomainTaskAdditionalInformationDTO.module())
+            .bean(new DeleteUsersDataOfDomainTask.AdditionalInformation(
+                TIMESTAMP, DOMAIN, SUCCESSFUL_USERS_COUNT, FAILED_USERS_COUNT))
+            .json(SERIALIZED_ADDITIONAL_INFORMATION)
+            .verify();
+    }
+}
\ No newline at end of file
diff --git a/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskTest.java b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskTest.java
new file mode 100644
index 0000000000..f9bc9cecea
--- /dev/null
+++ b/server/protocols/webadmin/webadmin-data/src/test/java/org/apache/james/webadmin/service/DeleteUsersDataOfDomainTaskTest.java
@@ -0,0 +1,194 @@
+/****************************************************************
+ * 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.service;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.net.InetAddress;
+import java.util.Set;
+
+import org.apache.james.core.Domain;
+import org.apache.james.core.Username;
+import org.apache.james.dnsservice.api.DNSService;
+import org.apache.james.domainlist.lib.DomainListConfiguration;
+import org.apache.james.domainlist.memory.MemoryDomainList;
+import org.apache.james.task.Task;
+import org.apache.james.user.api.DeleteUserDataTaskStep;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.user.memory.MemoryUsersRepository;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.reactivestreams.Publisher;
+
+import reactor.core.publisher.Mono;
+
+class DeleteUsersDataOfDomainTaskTest {
+    public static class FailureStepUponUser implements DeleteUserDataTaskStep {
+        private final Set<Username> usersToBeFailed;
+
+        public FailureStepUponUser(Set<Username> usersToBeFailed) {
+            this.usersToBeFailed = usersToBeFailed;
+        }
+
+        @Override
+        public StepName name() {
+            return new StepName("FailureStepUponUser");
+        }
+
+        @Override
+        public int priority() {
+            return 0;
+        }
+
+        @Override
+        public Publisher<Void> deleteUserData(Username username) {
+            if (usersToBeFailed.contains(username)) {
+                return Mono.error(new RuntimeException());
+            }
+            return Mono.empty();
+        }
+    }
+
+    private static final Domain DOMAIN_1 = Domain.of("domain1.tld");
+    private static final Domain DOMAIN_2 = Domain.of("domain2.tld");
+
+    private DeleteUserDataService service;
+    private MemoryUsersRepository usersRepository;
+
+    @BeforeEach
+    void setup() throws Exception {
+        DNSService dnsService = mock(DNSService.class);
+        when(dnsService.getHostName(any())).thenReturn("localhost");
+        when(dnsService.getLocalHost()).thenReturn(InetAddress.getByName("localhost"));
+        MemoryDomainList domainList = new MemoryDomainList(dnsService);
+        domainList.configure(DomainListConfiguration.builder()
+            .autoDetect(false)
+            .autoDetectIp(false)
+            .build());
+        domainList.addDomain(DOMAIN_1);
+        domainList.addDomain(DOMAIN_2);
+
+        usersRepository = MemoryUsersRepository.withVirtualHosting(domainList);
+    }
+
+    @Test
+    void shouldCountSuccessfulUsers() throws UsersRepositoryException {
+        // GIVEN DOMAIN1 has 2 users
+        usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+        usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+
+        // WHEN run task for DOMAIN1
+        service = new DeleteUserDataService(Set.of());
+        DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+        Task.Result result = task.run();
+
+        // THEN should count successful DOMAIN1 users
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(result).isEqualTo(Task.Result.COMPLETED);
+            softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(2L);
+            softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(0L);
+        });
+    }
+
+    @Test
+    void shouldCountOnlySuccessfulUsersOfRequestedDomain() throws UsersRepositoryException {
+        // GIVEN DOMAIN1 has 2 users and DOMAIN2 has 1 user
+        usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+        usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+        usersRepository.addUser(Username.of("user3@domain2.tld"), "password");
+
+        // WHEN run task for DOMAIN1
+        service = new DeleteUserDataService(Set.of());
+        DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+        Task.Result result = task.run();
+
+        // THEN should count only successful DOMAIN1 users
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(result).isEqualTo(Task.Result.COMPLETED);
+            softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(2L);
+            softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(0L);
+        });
+    }
+
+    @Test
+    void shouldCountFailedUsers() throws UsersRepositoryException {
+        // GIVEN DOMAIN1 has 2 users
+        usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+        usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+
+        // WHEN run task for DOMAIN1
+        Set<Username> usersTobeFailed = Set.of(Username.of("user1@domain1.tld"), Username.of("user2@domain1.tld"));
+        service = new DeleteUserDataService(Set.of(new FailureStepUponUser(usersTobeFailed)));
+        DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+        Task.Result result = task.run();
+
+        // THEN should count failed DOMAIN1 users
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(result).isEqualTo(Task.Result.PARTIAL);
+            softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(0L);
+            softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(2L);
+        });
+    }
+
+    @Test
+    void shouldCountOnlyFailedUsersOfRequestedDomain() throws UsersRepositoryException {
+        // GIVEN DOMAIN1 has 2 users and DOMAIN2 has 1 user
+        usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+        usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+        usersRepository.addUser(Username.of("user3@domain2.tld"), "password");
+
+        // WHEN run task for DOMAIN1
+        Set<Username> usersTobeFailed = Set.of(Username.of("user1@domain1.tld"), Username.of("user2@domain1.tld"), Username.of("user3@domain2.tld"));
+        service = new DeleteUserDataService(Set.of(new FailureStepUponUser(usersTobeFailed)));
+        DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+        Task.Result result = task.run();
+
+        // THEN should count only failed DOMAIN1 users
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(result).isEqualTo(Task.Result.PARTIAL);
+            softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(0L);
+            softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(2L);
+        });
+    }
+
+    @Test
+    void mixedSuccessfulAndFailedUsersCase() throws UsersRepositoryException {
+        // GIVEN DOMAIN1 has 3 users
+        usersRepository.addUser(Username.of("user1@domain1.tld"), "password");
+        usersRepository.addUser(Username.of("user2@domain1.tld"), "password");
+        usersRepository.addUser(Username.of("user3@domain1.tld"), "password");
+
+        // WHEN run task for DOMAIN1
+        Set<Username> usersTobeFailed = Set.of(Username.of("user1@domain1.tld"));
+        service = new DeleteUserDataService(Set.of(new FailureStepUponUser(usersTobeFailed)));
+        DeleteUsersDataOfDomainTask task = new DeleteUsersDataOfDomainTask(service, DOMAIN_1, usersRepository);
+        Task.Result result = task.run();
+
+        // THEN should count both successful and failed users
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(result).isEqualTo(Task.Result.PARTIAL);
+            softly.assertThat(task.getContext().getSuccessfulUsersCount()).isEqualTo(2L);
+            softly.assertThat(task.getContext().getFailedUsersCount()).isEqualTo(1L);
+        });
+    }
+}
diff --git a/src/site/markdown/server/manage-webadmin.md b/src/site/markdown/server/manage-webadmin.md
index 329cd488ad..5864d0a644 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -178,6 +178,7 @@ Response codes:
    - [Get the list of aliases for a domain](#Get_the_list_of_aliases_for_a_domain)
    - [Create an alias for a domain](#Create_an_alias_for_a_domain)
    - [Delete an alias for a domain](#Delete_an_alias_for_a_domain)
+   - [Delete all users data of a domain](#delete-all-users-data-of-a-domain)
 
 ### Create a domain
 
@@ -302,6 +303,33 @@ Response codes:
  - 400: source, domain and destination domain are the same
  - 404: `source.domain.tld` are not part of handled domains.
 
+### Delete all users data of a domain
+
+```
+curl -XPOST http://ip:port/domains/{domainToBeUsed}?action=deleteData
+```
+
+Would create a task that deletes data of all users of the domain.
+
+[More details about endpoints returning a task](#_endpoints_returning_a_task).
+
+Response codes:
+
+* 201: Success. Corresponding task id is returned.
+* 400: Error in the request. Details can be found in the reported error.
+
+The scheduled task will have the following type `DeleteUsersDataOfDomainTask` and the following `additionalInformation`:
+
+```
+{
+        "type": "DeleteUsersDataOfDomainTask",
+        "domain": "domain.tld",
+        "successfulUsersCount": 2,
+        "failedUsersCount": 1,
+        "timestamp": "2023-05-22T08:52:47.076261Z"
+}
+```
+
 ## Administrating users
 
    - [Create a user](#Create_a_user)


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