This is an automated email from the ASF dual-hosted git repository.

Arsnael pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 331116eb6148b1508a3d9c3e79ed5a1bdadd3e83
Author: Rene Cordier <[email protected]>
AuthorDate: Fri May 15 16:11:38 2026 +0700

    JAMES-4204 Webadmin endpoint for restore mailbox backup
---
 .../modules/servers/partials/operate/webadmin.adoc |  29 ++-
 .../james/mailbox/backup/DefaultMailboxBackup.java |   1 +
 .../james/CassandraRabbitMQJamesServerMain.java    |   4 +-
 .../james/DistributedPOP3JamesServerMain.java      |   4 +-
 .../org/apache/james/MemoryJamesServerMain.java    |   4 +-
 .../org/apache/james/PostgresJamesServerMain.java  |   4 +-
 ...odule.java => MailboxesBackupRoutesModule.java} |  13 +-
 ...adminMailboxBackupTaskSerializationModule.java} |  22 +-
 .../integration/WebAdminServerIntegrationTest.java |  28 +++
 .../service/MailboxesRestoreRequestToTask.java     |  74 +++++++
 .../webadmin/service/MailboxesRestoreTask.java     |  88 ++++++++
 ...ilboxesRestoreTaskAdditionalInformationDTO.java |  71 ++++++
 .../webadmin/service/MailboxesRestoreTaskDTO.java  |  75 +++++++
 .../james/webadmin/service/RestoreService.java     |  79 +++++++
 .../service/MailboxesRestoreRequestToTaskTest.java | 240 +++++++++++++++++++++
 ...esRestoreTaskAdditionalInformationDTOTest.java} |  34 ++-
 .../MailboxesRestoreTaskSerializationTest.java}    |  40 ++--
 .../james/webadmin/service/RestoreServiceTest.java | 195 +++++++++++++++++
 .../mailboxesRestore.additionalInformation.json    |   5 +
 .../test/resources/json/mailboxesRestore.task.json |   5 +
 src/site/markdown/server/manage-webadmin.md        |  28 ++-
 21 files changed, 993 insertions(+), 50 deletions(-)

diff --git a/docs/modules/servers/partials/operate/webadmin.adoc 
b/docs/modules/servers/partials/operate/webadmin.adoc
index 442861e3ae..0a3cc30f44 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -1737,7 +1737,7 @@ Response codes:
 === Exporting user mailboxes
 
 ....
-curl -XPOST http://ip:port/users/{usernameToBeUsed}/mailboxes?action=export
+curl -XPOST http://ip:port/users/{usernameToBeUsed}/mailboxes?task=export
 ....
 
 Resource name `usernameToBeUsed` should be an existing user
@@ -1759,6 +1759,33 @@ and the following `additionalInformation`:
 }
 ....
 
+=== Restoring user mailboxes
+
+....
+curl -XPOST http://ip:port/users/{usernameToBeUsed}/mailboxes?task=restore 
--data-binary @backup.zip
+....
+
+Resource name `usernameToBeUsed` should be an existing user. The request body 
must contain the ZIP backup data.
+
+Response codes:
+
+* 201: Success. Corresponding task id is returned
+* 400: The request body is empty
+* 404: The user name does not exist
+
+The scheduled task will have the following type `MailboxesRestoreTask`
+and the following `additionalInformation`:
+
+....
+{
+  "type":"MailboxesRestoreTask",
+  "timestamp":"2007-12-03T10:15:30Z",
+  "username": "user"
+}
+....
+
+Note: The account must be empty for the restore to succeed. If the user 
already has mailboxes, the task will fail.
+
 === ReIndexing a user mails
 
 ....
diff --git 
a/mailbox/backup/src/main/java/org/apache/james/mailbox/backup/DefaultMailboxBackup.java
 
b/mailbox/backup/src/main/java/org/apache/james/mailbox/backup/DefaultMailboxBackup.java
index bd45101443..e8c964270b 100644
--- 
a/mailbox/backup/src/main/java/org/apache/james/mailbox/backup/DefaultMailboxBackup.java
+++ 
b/mailbox/backup/src/main/java/org/apache/james/mailbox/backup/DefaultMailboxBackup.java
@@ -116,6 +116,7 @@ public class DefaultMailboxBackup implements MailboxBackup {
     public Publisher<BackupStatus> restore(Username username, InputStream 
source) {
         try {
             if (isAccountNonEmpty(username)) {
+                LOGGER.warn("Warning, account should be empty before 
performing a restoration for user: {}", username);
                 return Mono.just(BackupStatus.NON_EMPTY_RECEIVER_ACCOUNT);
             }
         } catch (Exception e) {
diff --git 
a/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesServerMain.java
 
b/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesServerMain.java
index ce9af6b50f..335c00faf8 100644
--- 
a/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesServerMain.java
+++ 
b/server/apps/distributed-app/src/main/java/org/apache/james/CassandraRabbitMQJamesServerMain.java
@@ -86,7 +86,7 @@ import 
org.apache.james.modules.server.JmapUploadCleanupModule;
 import org.apache.james.modules.server.MailQueueRoutesModule;
 import org.apache.james.modules.server.MailRepositoriesRoutesModule;
 import org.apache.james.modules.server.MailboxRoutesModule;
-import org.apache.james.modules.server.MailboxesExportRoutesModule;
+import org.apache.james.modules.server.MailboxesBackupRoutesModule;
 import org.apache.james.modules.server.MessagesRoutesModule;
 import org.apache.james.modules.server.RabbitMailQueueRoutesModule;
 import org.apache.james.modules.server.SieveRoutesModule;
@@ -119,7 +119,7 @@ public class CassandraRabbitMQJamesServerMain implements 
JamesServerMain {
         new JmapUploadCleanupModule(),
         new UserIdentityModule(),
         new JmapTasksModule(),
-        new MailboxesExportRoutesModule(),
+        new MailboxesBackupRoutesModule(),
         new MailboxRoutesModule(),
         new MailQueueRoutesModule(),
         new MailRepositoriesRoutesModule(),
diff --git 
a/server/apps/distributed-pop3-app/src/main/java/org/apache/james/DistributedPOP3JamesServerMain.java
 
b/server/apps/distributed-pop3-app/src/main/java/org/apache/james/DistributedPOP3JamesServerMain.java
index 0b799ca7dd..1d3112d08c 100644
--- 
a/server/apps/distributed-pop3-app/src/main/java/org/apache/james/DistributedPOP3JamesServerMain.java
+++ 
b/server/apps/distributed-pop3-app/src/main/java/org/apache/james/DistributedPOP3JamesServerMain.java
@@ -86,7 +86,7 @@ import org.apache.james.modules.server.JMXServerModule;
 import org.apache.james.modules.server.MailQueueRoutesModule;
 import org.apache.james.modules.server.MailRepositoriesRoutesModule;
 import org.apache.james.modules.server.MailboxRoutesModule;
-import org.apache.james.modules.server.MailboxesExportRoutesModule;
+import org.apache.james.modules.server.MailboxesBackupRoutesModule;
 import org.apache.james.modules.server.MessagesRoutesModule;
 import org.apache.james.modules.server.RabbitMailQueueRoutesModule;
 import org.apache.james.modules.server.UserIdentityModule;
@@ -114,7 +114,7 @@ public class DistributedPOP3JamesServerMain implements 
JamesServerMain {
         new VacationRoutesModule(),
         new InconsistencyQuotasSolvingRoutesModule(),
         new InconsistencySolvingRoutesModule(),
-        new MailboxesExportRoutesModule(),
+        new MailboxesBackupRoutesModule(),
         new MailboxRoutesModule(),
         new MailQueueRoutesModule(),
         new MailRepositoriesRoutesModule(),
diff --git 
a/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java
 
b/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java
index 0ae1d22ad4..7c1b829ab4 100644
--- 
a/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java
+++ 
b/server/apps/memory-app/src/main/java/org/apache/james/MemoryJamesServerMain.java
@@ -57,7 +57,7 @@ import org.apache.james.modules.server.JmapTasksModule;
 import org.apache.james.modules.server.MailQueueRoutesModule;
 import org.apache.james.modules.server.MailRepositoriesRoutesModule;
 import org.apache.james.modules.server.MailboxRoutesModule;
-import org.apache.james.modules.server.MailboxesExportRoutesModule;
+import org.apache.james.modules.server.MailboxesBackupRoutesModule;
 import org.apache.james.modules.server.MailetContainerModule;
 import org.apache.james.modules.server.NoJwtModule;
 import org.apache.james.modules.server.RawPostDequeueDecoratorModule;
@@ -86,7 +86,7 @@ public class MemoryJamesServerMain implements JamesServerMain 
{
         new DeletedMessageVaultRoutesModule(),
         new DLPRoutesModule(),
         new InconsistencyQuotasSolvingRoutesModule(),
-        new MailboxesExportRoutesModule(),
+        new MailboxesBackupRoutesModule(),
         new MailboxRoutesModule(),
         new MailQueueRoutesModule(),
         new MailRepositoriesRoutesModule(),
diff --git 
a/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesServerMain.java
 
b/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesServerMain.java
index 55254401e0..3095262e37 100644
--- 
a/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesServerMain.java
+++ 
b/server/apps/postgres-app/src/main/java/org/apache/james/PostgresJamesServerMain.java
@@ -83,7 +83,7 @@ import 
org.apache.james.modules.server.JmapUploadCleanupModule;
 import org.apache.james.modules.server.MailQueueRoutesModule;
 import org.apache.james.modules.server.MailRepositoriesRoutesModule;
 import org.apache.james.modules.server.MailboxRoutesModule;
-import org.apache.james.modules.server.MailboxesExportRoutesModule;
+import org.apache.james.modules.server.MailboxesBackupRoutesModule;
 import org.apache.james.modules.server.RabbitMailQueueRoutesModule;
 import org.apache.james.modules.server.ReIndexingModule;
 import org.apache.james.modules.server.SieveRoutesModule;
@@ -121,7 +121,7 @@ public class PostgresJamesServerMain implements 
JamesServerMain {
         new ReIndexingModule(),
         new SieveRoutesModule(),
         new WebAdminReIndexingTaskSerializationModule(),
-        new MailboxesExportRoutesModule(),
+        new MailboxesBackupRoutesModule(),
         new UserIdentityModule(),
         new DLPRoutesModule(),
         new JmapUploadCleanupModule(),
diff --git 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
 
b/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesBackupRoutesModule.java
similarity index 71%
copy from 
server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
copy to 
server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesBackupRoutesModule.java
index 2a7021f842..43898538c7 100644
--- 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
+++ 
b/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesBackupRoutesModule.java
@@ -22,6 +22,8 @@ package org.apache.james.modules.server;
 import org.apache.james.webadmin.routes.UserMailboxesRoutes;
 import org.apache.james.webadmin.service.ExportService;
 import org.apache.james.webadmin.service.MailboxesExportRequestToTask;
+import org.apache.james.webadmin.service.MailboxesRestoreRequestToTask;
+import org.apache.james.webadmin.service.RestoreService;
 import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
 
 import com.google.inject.AbstractModule;
@@ -29,15 +31,18 @@ import com.google.inject.Scopes;
 import com.google.inject.multibindings.Multibinder;
 import com.google.inject.name.Names;
 
-public class MailboxesExportRoutesModule extends AbstractModule {
+public class MailboxesBackupRoutesModule extends AbstractModule {
 
     @Override
     protected void configure() {
         install(new MailboxesBackupModule());
-        install(new WebadminMailboxExportTaskSerializationModule());
+        install(new WebadminMailboxBackupTaskSerializationModule());
 
         bind(ExportService.class).in(Scopes.SINGLETON);
-        Multibinder.newSetBinder(binder(), 
TaskFromRequestRegistry.TaskRegistration.class, 
Names.named(UserMailboxesRoutes.USER_MAILBOXES_OPERATIONS_INJECTION_KEY))
-            .addBinding().to(MailboxesExportRequestToTask.class);
+        bind(RestoreService.class).in(Scopes.SINGLETON);
+
+        Multibinder<TaskFromRequestRegistry.TaskRegistration> 
taskRegistrations = Multibinder.newSetBinder(binder(), 
TaskFromRequestRegistry.TaskRegistration.class, 
Names.named(UserMailboxesRoutes.USER_MAILBOXES_OPERATIONS_INJECTION_KEY));
+        taskRegistrations.addBinding().to(MailboxesExportRequestToTask.class);
+        taskRegistrations.addBinding().to(MailboxesRestoreRequestToTask.class);
     }
 }
diff --git 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/WebadminMailboxExportTaskSerializationModule.java
 
b/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/WebadminMailboxBackupTaskSerializationModule.java
similarity index 70%
rename from 
server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/WebadminMailboxExportTaskSerializationModule.java
rename to 
server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/WebadminMailboxBackupTaskSerializationModule.java
index 7afff0bfab..8d580c115c 100644
--- 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/WebadminMailboxExportTaskSerializationModule.java
+++ 
b/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/WebadminMailboxBackupTaskSerializationModule.java
@@ -18,6 +18,7 @@
  ****************************************************************/
 package org.apache.james.modules.server;
 
+import org.apache.james.blob.api.BlobId;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTO;
 import org.apache.james.server.task.json.dto.AdditionalInformationDTOModule;
 import org.apache.james.server.task.json.dto.TaskDTO;
@@ -28,12 +29,15 @@ import org.apache.james.webadmin.dto.DTOModuleInjections;
 import org.apache.james.webadmin.service.ExportService;
 import org.apache.james.webadmin.service.MailboxesExportTask;
 import 
org.apache.james.webadmin.service.MailboxesExportTaskAdditionalInformationDTO;
+import 
org.apache.james.webadmin.service.MailboxesRestoreTaskAdditionalInformationDTO;
+import org.apache.james.webadmin.service.MailboxesRestoreTaskDTO;
+import org.apache.james.webadmin.service.RestoreService;
 
 import com.google.inject.AbstractModule;
 import com.google.inject.multibindings.ProvidesIntoSet;
 import com.google.inject.name.Named;
 
-public class WebadminMailboxExportTaskSerializationModule extends 
AbstractModule {
+public class WebadminMailboxBackupTaskSerializationModule extends 
AbstractModule {
     @ProvidesIntoSet
     public TaskDTOModule<? extends Task, ? extends TaskDTO> 
mailboxesExportTask(ExportService exportService) {
         return MailboxesExportTask.module(exportService);
@@ -49,4 +53,20 @@ public class WebadminMailboxExportTaskSerializationModule 
extends AbstractModule
     public AdditionalInformationDTOModule<? extends 
TaskExecutionDetails.AdditionalInformation, ? extends  
AdditionalInformationDTO> webAdminMailboxesExportAdditionalInformation() {
         return 
MailboxesExportTaskAdditionalInformationDTO.SERIALIZATION_MODULE;
     }
+
+    @ProvidesIntoSet
+    public TaskDTOModule<? extends Task, ? extends TaskDTO> 
mailboxesRestoreTask(RestoreService restoreService, BlobId.Factory 
blobIdFactory) {
+        return MailboxesRestoreTaskDTO.module(restoreService, blobIdFactory);
+    }
+
+    @ProvidesIntoSet
+    public AdditionalInformationDTOModule<? extends 
TaskExecutionDetails.AdditionalInformation, ? extends AdditionalInformationDTO> 
mailboxesRestoreAdditionalInformation() {
+        return 
MailboxesRestoreTaskAdditionalInformationDTO.SERIALIZATION_MODULE;
+    }
+
+    @Named(DTOModuleInjections.WEBADMIN_DTO)
+    @ProvidesIntoSet
+    public AdditionalInformationDTOModule<? extends 
TaskExecutionDetails.AdditionalInformation, ? extends AdditionalInformationDTO> 
webAdminMailboxesRestoreAdditionalInformation() {
+        return 
MailboxesRestoreTaskAdditionalInformationDTO.SERIALIZATION_MODULE;
+    }
 }
diff --git 
a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java
 
b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java
index 2e1531470b..144db2c2dd 100644
--- 
a/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java
+++ 
b/server/protocols/webadmin-integration-test/webadmin-integration-test-common/src/main/java/org/apache/james/webadmin/integration/WebAdminServerIntegrationTest.java
@@ -34,7 +34,9 @@ import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.is;
 import static org.hamcrest.Matchers.notNullValue;
 
+import java.io.ByteArrayOutputStream;
 import java.util.List;
+import java.util.zip.ZipOutputStream;
 
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.modules.MailboxProbeImpl;
@@ -456,6 +458,32 @@ public abstract class WebAdminServerIntegrationTest {
             .body("type", is("MailboxesExportTask"));
     }
 
+    @Test
+    void mailboxesRestoreTasksShouldBeExposed() throws Exception {
+        dataProbe.addUser(USERNAME, "anyPassword");
+
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+            // empty zip
+        }
+        byte[] emptyZip = baos.toByteArray();
+
+        String taskId = with()
+            .queryParam("task", "restore")
+            .body(emptyZip)
+            .post("/users/" + USERNAME + "/mailboxes")
+            .jsonPath()
+            .get("taskId");
+
+        given()
+            .basePath(TasksRoutes.BASE)
+        .when()
+            .get(taskId + "/await")
+        .then()
+            .body("status", is("completed"))
+            .body("type", is("MailboxesRestoreTask"));
+    }
+
     @Test
     void createMissParentsTasksShouldBeExposed() {
         String taskId = with()
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreRequestToTask.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreRequestToTask.java
new file mode 100644
index 0000000000..0b95d3efe2
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreRequestToTask.java
@@ -0,0 +1,74 @@
+/****************************************************************
+ * 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 jakarta.inject.Inject;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.core.Username;
+import org.apache.james.task.Task;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.user.api.UsersRepositoryException;
+import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+import org.apache.james.webadmin.tasks.TaskRegistrationKey;
+import org.apache.james.webadmin.utils.ErrorResponder;
+import org.eclipse.jetty.http.HttpStatus;
+
+import reactor.core.publisher.Mono;
+import spark.Request;
+
+public class MailboxesRestoreRequestToTask extends 
TaskFromRequestRegistry.TaskRegistration {
+
+    public static final TaskRegistrationKey TASK_REGISTRATION_KEY = 
TaskRegistrationKey.of("restore");
+
+    @Inject
+    MailboxesRestoreRequestToTask(RestoreService restoreService, 
UsersRepository usersRepository, BlobStore blobStore) {
+        super(TASK_REGISTRATION_KEY,
+            request -> toTask(restoreService, usersRepository, blobStore, 
request));
+    }
+
+    private static Task toTask(RestoreService restoreService,
+                               UsersRepository usersRepository,
+                               BlobStore blobStore,
+                               Request request) throws 
UsersRepositoryException {
+        Username username = Username.of(request.params("username"));
+        if (!usersRepository.contains(username)) {
+            throw ErrorResponder.builder()
+                .type(ErrorResponder.ErrorType.NOT_FOUND)
+                .statusCode(HttpStatus.NOT_FOUND_404)
+                .message(String.format("User '%s' does not exist", 
username.asString()))
+                .haltError();
+        }
+
+        byte[] data = request.bodyAsBytes();
+        if (data.length == 0) {
+            throw ErrorResponder.builder()
+                .type(ErrorResponder.ErrorType.INVALID_ARGUMENT)
+                .statusCode(HttpStatus.BAD_REQUEST_400)
+                .message("Request body must contain the ZIP backup data")
+                .haltError();
+        }
+
+        BlobId blobId = 
Mono.from(blobStore.save(blobStore.getDefaultBucketName(), data, 
BlobStore.StoragePolicy.LOW_COST)).block();
+
+        return new MailboxesRestoreTask(restoreService, username, blobId);
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTask.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTask.java
new file mode 100644
index 0000000000..e7365e0b8d
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTask.java
@@ -0,0 +1,88 @@
+/****************************************************************
+ * 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.Optional;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.core.Username;
+import org.apache.james.task.Task;
+import org.apache.james.task.TaskExecutionDetails;
+import org.apache.james.task.TaskType;
+
+public class MailboxesRestoreTask implements Task {
+    static final TaskType TASK_TYPE = TaskType.of("MailboxesRestoreTask");
+
+    public static class AdditionalInformation implements 
TaskExecutionDetails.AdditionalInformation {
+        private final Username username;
+        private final Instant timestamp;
+
+        public AdditionalInformation(Username username, Instant timestamp) {
+            this.username = username;
+            this.timestamp = timestamp;
+        }
+
+        public String getUsername() {
+            return username.asString();
+        }
+
+        @Override
+        public Instant timestamp() {
+            return timestamp;
+        }
+    }
+
+    private final Username username;
+    private final RestoreService service;
+    private final BlobId blobId;
+
+    MailboxesRestoreTask(RestoreService service, Username username, BlobId 
blobId) {
+        this.username = username;
+        this.service = service;
+        this.blobId = blobId;
+    }
+
+    @Override
+    public Result run() {
+        return service.restore(username, blobId)
+            .block();
+    }
+
+    @Override
+    public TaskType type() {
+        return TASK_TYPE;
+    }
+
+    public Username getUsername() {
+        return username;
+    }
+
+    public BlobId getBlobId() {
+        return blobId;
+    }
+
+    @Override
+    public Optional<TaskExecutionDetails.AdditionalInformation> details() {
+        return Optional.of(new AdditionalInformation(username,
+            Clock.systemUTC().instant()));
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTaskAdditionalInformationDTO.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTaskAdditionalInformationDTO.java
new file mode 100644
index 0000000000..0a0e8e6bf0
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTaskAdditionalInformationDTO.java
@@ -0,0 +1,71 @@
+/****************************************************************
+ * 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.Username;
+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 MailboxesRestoreTaskAdditionalInformationDTO implements 
AdditionalInformationDTO {
+
+    public static final 
AdditionalInformationDTOModule<MailboxesRestoreTask.AdditionalInformation, 
MailboxesRestoreTaskAdditionalInformationDTO> SERIALIZATION_MODULE =
+        
DTOModule.forDomainObject(MailboxesRestoreTask.AdditionalInformation.class)
+            .convertToDTO(MailboxesRestoreTaskAdditionalInformationDTO.class)
+            .toDomainObjectConverter(dto -> new 
MailboxesRestoreTask.AdditionalInformation(
+                Username.of(dto.username),
+                dto.timestamp))
+            .toDTOConverter((details, type) -> new 
MailboxesRestoreTaskAdditionalInformationDTO(
+                type,
+                details.timestamp(),
+                details.getUsername()))
+            .typeName(MailboxesRestoreTask.TASK_TYPE.asString())
+            .withFactory(AdditionalInformationDTOModule::new);
+
+    private final String username;
+    private final Instant timestamp;
+    private final String type;
+
+    private MailboxesRestoreTaskAdditionalInformationDTO(@JsonProperty("type") 
String type,
+                                                         
@JsonProperty("timestamp") Instant timestamp,
+                                                         
@JsonProperty("username") String username) {
+        this.type = type;
+        this.timestamp = timestamp;
+        this.username = username;
+    }
+
+    @Override
+    public String getType() {
+        return type;
+    }
+
+    @Override
+    public Instant getTimestamp() {
+        return timestamp;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTaskDTO.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTaskDTO.java
new file mode 100644
index 0000000000..1307e53250
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/MailboxesRestoreTaskDTO.java
@@ -0,0 +1,75 @@
+/****************************************************************
+ * 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.apache.james.webadmin.service.MailboxesRestoreTask.TASK_TYPE;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.core.Username;
+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 com.fasterxml.jackson.annotation.JsonProperty;
+
+public class MailboxesRestoreTaskDTO implements TaskDTO {
+    private final String type;
+    private final String username;
+    private final String blobId;
+
+    public MailboxesRestoreTaskDTO(@JsonProperty("type") String type,
+                                   @JsonProperty("username") String username,
+                                   @JsonProperty("blobId") String blobId) {
+        this.type = type;
+        this.username = username;
+        this.blobId = blobId;
+    }
+
+    @Override
+    public String getType() {
+        return type;
+    }
+
+    public String getUsername() {
+        return username;
+    }
+
+    public String getBlobId() {
+        return blobId;
+    }
+
+    public static TaskDTOModule<MailboxesRestoreTask, MailboxesRestoreTaskDTO> 
module(RestoreService service, BlobId.Factory blobIdFactory) {
+        return DTOModule
+            .forDomainObject(MailboxesRestoreTask.class)
+            .convertToDTO(MailboxesRestoreTaskDTO.class)
+            .toDomainObjectConverter(dto -> dto.fromDTO(service, 
blobIdFactory))
+            .toDTOConverter(MailboxesRestoreTaskDTO::toDTO)
+            .typeName(TASK_TYPE.asString())
+            .withFactory(TaskDTOModule::new);
+    }
+
+    public MailboxesRestoreTask fromDTO(RestoreService service, BlobId.Factory 
blobIdFactory) {
+        return new MailboxesRestoreTask(service, Username.of(username), 
blobIdFactory.of(blobId));
+    }
+
+    public static MailboxesRestoreTaskDTO toDTO(MailboxesRestoreTask 
domainObject, String typeName) {
+        return new MailboxesRestoreTaskDTO(typeName, 
domainObject.getUsername().asString(), domainObject.getBlobId().asString());
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/RestoreService.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/RestoreService.java
new file mode 100644
index 0000000000..54c47188bf
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/main/java/org/apache/james/webadmin/service/RestoreService.java
@@ -0,0 +1,79 @@
+/****************************************************************
+ * 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.io.InputStream;
+
+import jakarta.inject.Inject;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.core.Username;
+import org.apache.james.mailbox.backup.MailboxBackup;
+import org.apache.james.mailbox.backup.MailboxBackup.BackupStatus;
+import org.apache.james.task.Task;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import reactor.core.publisher.Mono;
+
+public class RestoreService {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(RestoreService.class);
+
+    private final MailboxBackup mailboxBackup;
+    private final BlobStore blobStore;
+
+    @Inject
+    public RestoreService(MailboxBackup mailboxBackup, BlobStore blobstore) {
+        this.mailboxBackup = mailboxBackup;
+        this.blobStore = blobstore;
+    }
+
+    public Mono<Task.Result> restore(Username username, BlobId blobId) {
+        try (InputStream inputStream = 
blobStore.read(blobStore.getDefaultBucketName(), blobId)) {
+            return restore(username, inputStream);
+        } catch (Exception e) {
+            LOGGER.error("Error restoring mailboxes for user {}", 
username.asString(), e);
+            return Mono.just(Task.Result.PARTIAL);
+        } finally {
+            try {
+                Mono.from(blobStore.delete(blobStore.getDefaultBucketName(), 
blobId)).block();
+            } catch (Exception e) {
+                LOGGER.error("Error deleting blob {} after restore", 
blobId.asString(), e);
+            }
+        }
+    }
+
+    private Mono<Task.Result> restore(Username username, InputStream source) {
+        try {
+            return Mono.from(mailboxBackup.restore(username, source))
+                .map(this::computeTaskResult);
+        } catch (Exception e) {
+            return Mono.error(e);
+        }
+    }
+
+    private Task.Result computeTaskResult(BackupStatus status) {
+        if (status == BackupStatus.DONE) {
+            return Task.Result.COMPLETED;
+        }
+        return Task.Result.PARTIAL;
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreRequestToTaskTest.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreRequestToTaskTest.java
new file mode 100644
index 0000000000..72ba4ef92f
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreRequestToTaskTest.java
@@ -0,0 +1,240 @@
+/****************************************************************
+ * 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 io.restassured.RestAssured.given;
+import static io.restassured.RestAssured.when;
+import static org.apache.james.webadmin.service.ExportServiceTestSystem.BOB;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+
+import java.io.ByteArrayOutputStream;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.PlainBlobId;
+import org.apache.james.blob.export.file.FileSystemExtension;
+import org.apache.james.filesystem.api.FileSystem;
+import org.apache.james.json.DTOConverter;
+import org.apache.james.task.Hostname;
+import org.apache.james.task.MemoryTaskManager;
+import org.apache.james.task.TaskManager;
+import org.apache.james.user.api.UsersRepository;
+import org.apache.james.webadmin.Routes;
+import org.apache.james.webadmin.WebAdminServer;
+import org.apache.james.webadmin.WebAdminUtils;
+import org.apache.james.webadmin.routes.TasksRoutes;
+import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+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.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import io.restassured.RestAssured;
+import spark.Service;
+
+@ExtendWith(FileSystemExtension.class)
+class MailboxesRestoreRequestToTaskTest {
+
+    private final class RestoreRoutes implements Routes {
+        private final RestoreService restoreService;
+        private final TaskManager taskManager;
+        private final UsersRepository usersRepository;
+        private final BlobStore blobStore;
+
+        private RestoreRoutes(RestoreService restoreService, TaskManager 
taskManager, UsersRepository usersRepository, BlobStore blobStore) {
+            this.restoreService = restoreService;
+            this.taskManager = taskManager;
+            this.usersRepository = usersRepository;
+            this.blobStore = blobStore;
+        }
+
+        @Override
+        public String getBasePath() {
+            return BASE_PATH;
+        }
+
+        @Override
+        public void define(Service service) {
+            service.post(BASE_PATH,
+                TaskFromRequestRegistry.builder()
+                    .parameterName("task")
+                    .registrations(new 
MailboxesRestoreRequestToTask(this.restoreService, usersRepository, blobStore))
+                    .buildAsRoute(taskManager),
+                new JsonTransformer());
+        }
+    }
+
+    private static final String BASE_PATH = "users/:username/mailboxes";
+    private static final BlobId.Factory BLOB_ID_FACTORY = new 
PlainBlobId.Factory();
+
+    private WebAdminServer webAdminServer;
+    private MemoryTaskManager taskManager;
+    private ExportServiceTestSystem testSystem;
+
+    @BeforeEach
+    void setUp(FileSystem fileSystem) throws Exception {
+        testSystem = new ExportServiceTestSystem(fileSystem);
+        taskManager = new MemoryTaskManager(new Hostname("foo"));
+
+        JsonTransformer jsonTransformer = new JsonTransformer();
+        webAdminServer = WebAdminUtils.createWebAdminServer(
+            new TasksRoutes(taskManager, jsonTransformer,
+                
DTOConverter.of(MailboxesRestoreTaskAdditionalInformationDTO.SERIALIZATION_MODULE)),
+            new RestoreRoutes(
+                new RestoreService(testSystem.backup, testSystem.blobStore), 
taskManager, testSystem.usersRepository, testSystem.blobStore))
+            .start();
+
+        RestAssured.requestSpecification = 
WebAdminUtils.buildRequestSpecification(webAdminServer)
+            .setBasePath("users/" + BOB.asString() + "/mailboxes")
+            .build();
+    }
+
+    @AfterEach
+    void afterEach() {
+        webAdminServer.destroy();
+        taskManager.stop();
+    }
+
+    private byte[] emptyZip() throws Exception {
+        ByteArrayOutputStream baos = new ByteArrayOutputStream();
+        try (ZipOutputStream zos = new ZipOutputStream(baos)) {
+            // empty zip
+        }
+        return baos.toByteArray();
+    }
+
+    @Test
+    void taskRequestParameterShouldBeCompulsory() {
+        when()
+            .post()
+        .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("'task' query parameter is compulsory. 
Supported values are [restore]"));
+    }
+
+    @Test
+    void restoreMailboxesShouldFailUponEmptyTask() {
+        given()
+            .queryParam("task", "")
+            .post()
+        .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("'task' query parameter cannot be empty or 
blank. Supported values are [restore]"));
+    }
+
+    @Test
+    void restoreMailboxesShouldFailUponInvalidTask() {
+        given()
+            .queryParam("task", "invalid")
+            .post()
+        .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 
'task': invalid. Supported values are [restore]"));
+    }
+
+    @Test
+    void restoreMailboxesShouldFailUponBadUsername() throws Exception {
+        given()
+            .basePath("users/bad@bad@bad/mailboxes")
+            .queryParam("task", "restore")
+            .body(emptyZip())
+            .post()
+        .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("Domain parts ASCII chars must be a-z A-Z 0-9 
- or _ in bad@bad"));
+    }
+
+    @Test
+    void restoreMailboxesShouldFailUponUnknownUser() throws Exception {
+        given()
+            .basePath("users/notFound/mailboxes")
+            .queryParam("task", "restore")
+            .body(emptyZip())
+            .post()
+        .then()
+            .statusCode(HttpStatus.NOT_FOUND_404)
+            .body("statusCode", is(404))
+            .body("type", is(ErrorResponder.ErrorType.NOT_FOUND.getType()))
+            .body("message", is("User 'notfound' does not exist"));
+    }
+
+    @Test
+    void restoreMailboxesShouldFailUponEmptyBody() {
+        given()
+            .queryParam("task", "restore")
+            .post()
+        .then()
+            .statusCode(HttpStatus.BAD_REQUEST_400)
+            .body("statusCode", is(400))
+            .body("type", 
is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType()))
+            .body("message", is("Request body must contain the ZIP backup 
data"));
+    }
+
+    @Test
+    void postShouldCreateANewTask() throws Exception {
+        given()
+            .queryParam("task", "restore")
+            .body(emptyZip())
+            .post()
+        .then()
+            .statusCode(HttpStatus.CREATED_201)
+            .body("taskId", is(notNullValue()));
+    }
+
+    @Test
+    void restoreMailboxesShouldCompleteWhenUserHasNoMailbox() throws Exception 
{
+        String taskId = given()
+            .queryParam("task", "restore")
+            .body(emptyZip())
+            .post()
+            .jsonPath()
+            .get("taskId");
+
+        given()
+            .basePath(TasksRoutes.BASE)
+        .when()
+            .get(taskId + "/await")
+        .then()
+            .body("status", is("completed"))
+            .body("taskId", is(taskId))
+            .body("type", is("MailboxesRestoreTask"))
+            .body("additionalInformation.username", is(BOB.asString()))
+            .body("startedDate", is(notNullValue()))
+            .body("submitDate", is(notNullValue()))
+            .body("completedDate", is(notNullValue()));
+    }
+}
diff --git 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreTaskAdditionalInformationDTOTest.java
similarity index 55%
copy from 
server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
copy to 
server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreTaskAdditionalInformationDTOTest.java
index 2a7021f842..633e6abc74 100644
--- 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreTaskAdditionalInformationDTOTest.java
@@ -17,27 +17,25 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.modules.server;
+package org.apache.james.webadmin.service;
 
-import org.apache.james.webadmin.routes.UserMailboxesRoutes;
-import org.apache.james.webadmin.service.ExportService;
-import org.apache.james.webadmin.service.MailboxesExportRequestToTask;
-import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+import java.time.Instant;
 
-import com.google.inject.AbstractModule;
-import com.google.inject.Scopes;
-import com.google.inject.multibindings.Multibinder;
-import com.google.inject.name.Names;
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.core.Username;
+import org.apache.james.util.ClassLoaderUtils;
+import org.junit.jupiter.api.Test;
 
-public class MailboxesExportRoutesModule extends AbstractModule {
+class MailboxesRestoreTaskAdditionalInformationDTOTest {
+    private static final Instant INSTANT = 
Instant.parse("2007-12-03T10:15:30.00Z");
+    private static final MailboxesRestoreTask.AdditionalInformation 
DOMAIN_OBJECT = new MailboxesRestoreTask.AdditionalInformation(
+        Username.of("bob"), INSTANT);
 
-    @Override
-    protected void configure() {
-        install(new MailboxesBackupModule());
-        install(new WebadminMailboxExportTaskSerializationModule());
-
-        bind(ExportService.class).in(Scopes.SINGLETON);
-        Multibinder.newSetBinder(binder(), 
TaskFromRequestRegistry.TaskRegistration.class, 
Names.named(UserMailboxesRoutes.USER_MAILBOXES_OPERATIONS_INJECTION_KEY))
-            .addBinding().to(MailboxesExportRequestToTask.class);
+    @Test
+    void shouldMatchJsonSerializationContract() throws Exception {
+        
JsonSerializationVerifier.dtoModule(MailboxesRestoreTaskAdditionalInformationDTO.SERIALIZATION_MODULE)
+            .bean(DOMAIN_OBJECT)
+            
.json(ClassLoaderUtils.getSystemResourceAsString("json/mailboxesRestore.additionalInformation.json"))
+            .verify();
     }
 }
diff --git 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreTaskSerializationTest.java
similarity index 53%
rename from 
server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
rename to 
server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreTaskSerializationTest.java
index 2a7021f842..1391ff0abf 100644
--- 
a/server/container/guice/protocols/webadmin-mailbox/src/main/java/org/apache/james/modules/server/MailboxesExportRoutesModule.java
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/MailboxesRestoreTaskSerializationTest.java
@@ -17,27 +17,33 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.modules.server;
+package org.apache.james.webadmin.service;
 
-import org.apache.james.webadmin.routes.UserMailboxesRoutes;
-import org.apache.james.webadmin.service.ExportService;
-import org.apache.james.webadmin.service.MailboxesExportRequestToTask;
-import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
+import static org.mockito.Mockito.mock;
 
-import com.google.inject.AbstractModule;
-import com.google.inject.Scopes;
-import com.google.inject.multibindings.Multibinder;
-import com.google.inject.name.Names;
+import org.apache.james.JsonSerializationVerifier;
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.PlainBlobId;
+import org.apache.james.core.Username;
+import org.apache.james.util.ClassLoaderUtils;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
 
-public class MailboxesExportRoutesModule extends AbstractModule {
+class MailboxesRestoreTaskSerializationTest {
+    RestoreService service;
+    BlobId.Factory blobIdFactory;
 
-    @Override
-    protected void configure() {
-        install(new MailboxesBackupModule());
-        install(new WebadminMailboxExportTaskSerializationModule());
+    @BeforeEach
+    void setUp() {
+        service = mock(RestoreService.class);
+        blobIdFactory = new PlainBlobId.Factory();
+    }
 
-        bind(ExportService.class).in(Scopes.SINGLETON);
-        Multibinder.newSetBinder(binder(), 
TaskFromRequestRegistry.TaskRegistration.class, 
Names.named(UserMailboxesRoutes.USER_MAILBOXES_OPERATIONS_INJECTION_KEY))
-            .addBinding().to(MailboxesExportRequestToTask.class);
+    @Test
+    void shouldMatchJsonSerializationContract() throws Exception {
+        
JsonSerializationVerifier.dtoModule(MailboxesRestoreTaskDTO.module(service, 
blobIdFactory))
+            .bean(new MailboxesRestoreTask(service, Username.of("bob"), 
blobIdFactory.of("abc123")))
+            
.json(ClassLoaderUtils.getSystemResourceAsString("json/mailboxesRestore.task.json"))
+            .verify();
     }
 }
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/RestoreServiceTest.java
 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/RestoreServiceTest.java
new file mode 100644
index 0000000000..0ddc394ccb
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/test/java/org/apache/james/webadmin/service/RestoreServiceTest.java
@@ -0,0 +1,195 @@
+/****************************************************************
+ * 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.apache.james.webadmin.service.ExportServiceTestSystem.BOB;
+import static org.apache.james.webadmin.service.ExportServiceTestSystem.CEDRIC;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.doThrow;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.ObjectNotFoundException;
+import org.apache.james.blob.export.file.FileSystemExtension;
+import org.apache.james.filesystem.api.FileSystem;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.ComposedMessageId;
+import org.apache.james.mailbox.model.FetchGroup;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.MessageRange;
+import org.apache.james.mailbox.model.MessageResultIterator;
+import org.apache.james.task.Task;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mockito;
+
+import com.github.fge.lambdas.Throwing;
+
+import reactor.core.publisher.Mono;
+
+@ExtendWith(FileSystemExtension.class)
+class RestoreServiceTest {
+    private static final int BUFFER_SIZE = 4096;
+    private static final String MESSAGE_CONTENT = "MIME-Version: 1.0\r\n" +
+        "Subject: test\r\n" +
+        "Content-Type: text/plain; charset=UTF-8\r\n" +
+        "\r\n" +
+        "testmail";
+
+    private RestoreService testee;
+    private ExportServiceTestSystem testSystem;
+
+    @BeforeEach
+    void setUp(FileSystem fileSystem) throws Exception {
+        testSystem = new ExportServiceTestSystem(fileSystem);
+        testee = Mockito.spy(new RestoreService(testSystem.backup, 
testSystem.blobStore));
+    }
+
+    @Test
+    void restoreShouldReturnCompleteWhenExistingUserWithoutDataAndEmptyZip() 
throws Exception {
+        ByteArrayOutputStream destination = new 
ByteArrayOutputStream(BUFFER_SIZE);
+        testSystem.backup.backupAccount(BOB, destination);
+
+        InputStream source = new 
ByteArrayInputStream(destination.toByteArray());
+        BlobId blobId = 
Mono.from(testSystem.blobStore.save(testSystem.blobStore.getDefaultBucketName(),
 source, BlobStore.StoragePolicy.LOW_COST)).block();
+
+        assertThat(testee.restore(CEDRIC, blobId).block())
+            .isEqualTo(Task.Result.COMPLETED);
+    }
+
+    @Test
+    void 
restoreShouldReturnCompleteWhenExistingUserWithoutDataAndNonEmptyZip() throws 
Exception {
+        createAMailboxWithAMail(MESSAGE_CONTENT);
+
+        ByteArrayOutputStream destination = new 
ByteArrayOutputStream(BUFFER_SIZE);
+        testSystem.backup.backupAccount(BOB, destination);
+
+        InputStream source = new 
ByteArrayInputStream(destination.toByteArray());
+        BlobId blobId = 
Mono.from(testSystem.blobStore.save(testSystem.blobStore.getDefaultBucketName(),
 source, BlobStore.StoragePolicy.LOW_COST)).block();
+
+        assertThat(testee.restore(CEDRIC, blobId).block())
+            .isEqualTo(Task.Result.COMPLETED);
+    }
+
+    @Test
+    void restoreShouldReturnPartialWhenNonEmptyAccount() throws Exception {
+        createAMailboxWithAMail(MESSAGE_CONTENT);
+
+        ByteArrayOutputStream destination = new 
ByteArrayOutputStream(BUFFER_SIZE);
+        testSystem.backup.backupAccount(BOB, destination);
+
+        InputStream source = new 
ByteArrayInputStream(destination.toByteArray());
+        BlobId blobId = 
Mono.from(testSystem.blobStore.save(testSystem.blobStore.getDefaultBucketName(),
 source, BlobStore.StoragePolicy.LOW_COST)).block();
+
+        assertThat(testee.restore(BOB, blobId).block())
+            .isEqualTo(Task.Result.PARTIAL);
+    }
+
+    @Test
+    void restoreShouldReturnPartialWhenFailed() throws Exception {
+        doThrow(new RuntimeException())
+            .when(testSystem.blobStore)
+            .read(any(), any());
+
+        createAMailboxWithAMail(MESSAGE_CONTENT);
+
+        ByteArrayOutputStream destination = new 
ByteArrayOutputStream(BUFFER_SIZE);
+        testSystem.backup.backupAccount(BOB, destination);
+
+        InputStream source = new 
ByteArrayInputStream(destination.toByteArray());
+        BlobId blobId = 
Mono.from(testSystem.blobStore.save(testSystem.blobStore.getDefaultBucketName(),
 source, BlobStore.StoragePolicy.LOW_COST)).block();
+
+        assertThat(testee.restore(CEDRIC, blobId).block())
+            .isEqualTo(Task.Result.PARTIAL);
+    }
+
+    @Test
+    void restoreShouldNoopWhenEmptyZip() throws Exception {
+        ByteArrayOutputStream destination = new 
ByteArrayOutputStream(BUFFER_SIZE);
+        testSystem.backup.backupAccount(BOB, destination);
+
+        InputStream source = new 
ByteArrayInputStream(destination.toByteArray());
+        BlobId blobId = 
Mono.from(testSystem.blobStore.save(testSystem.blobStore.getDefaultBucketName(),
 source, BlobStore.StoragePolicy.LOW_COST)).block();
+
+        testee.restore(CEDRIC, blobId).block();
+
+        MailboxSession cedricSession = 
testSystem.mailboxManager.createSystemSession(CEDRIC);
+        assertThat(testSystem.mailboxManager.list(cedricSession))
+            .isEmpty();
+    }
+
+    @Test
+    void restoreShouldRestoreContentFromNonEmptyZip() throws Exception {
+        createAMailboxWithAMail(MESSAGE_CONTENT);
+
+        ByteArrayOutputStream destination = new 
ByteArrayOutputStream(BUFFER_SIZE);
+        testSystem.backup.backupAccount(BOB, destination);
+
+        InputStream source = new 
ByteArrayInputStream(destination.toByteArray());
+        BlobId blobId = 
Mono.from(testSystem.blobStore.save(testSystem.blobStore.getDefaultBucketName(),
 source, BlobStore.StoragePolicy.LOW_COST)).block();
+
+        testee.restore(CEDRIC, blobId).block();
+
+        MailboxSession cedricSession = 
testSystem.mailboxManager.createSystemSession(CEDRIC);
+        MessageManager mailbox = 
testSystem.mailboxManager.getMailbox(MailboxPath.inbox(CEDRIC), cedricSession);
+        MessageResultIterator resultIterator = 
mailbox.getMessages(MessageRange.all(), FetchGroup.FULL_CONTENT, cedricSession);
+        SoftAssertions.assertSoftly(softly -> {
+            softly.assertThat(resultIterator).toIterable().hasSize(1);
+            softly.assertThat(Throwing.supplier(() -> 
resultIterator.next().getBody().asBytesSequence())).isEqualTo(MESSAGE_CONTENT.getBytes(StandardCharsets.UTF_8));
+        });
+    }
+
+    @Test
+    void restoreShouldDeleteBlobAfterCompletion() throws Exception {
+        createAMailboxWithAMail(MESSAGE_CONTENT);
+
+        ByteArrayOutputStream destination = new 
ByteArrayOutputStream(BUFFER_SIZE);
+        testSystem.backup.backupAccount(BOB, destination);
+
+        InputStream source = new 
ByteArrayInputStream(destination.toByteArray());
+        BlobId blobId = 
Mono.from(testSystem.blobStore.save(testSystem.blobStore.getDefaultBucketName(),
 source, BlobStore.StoragePolicy.LOW_COST)).block();
+
+        testee.restore(CEDRIC, blobId).block();
+
+        assertThatThrownBy(() -> 
testSystem.blobStore.read(testSystem.blobStore.getDefaultBucketName(), blobId))
+            .isInstanceOf(ObjectNotFoundException.class);
+    }
+
+    private ComposedMessageId createAMailboxWithAMail(String message) throws 
MailboxException {
+        MailboxPath bobInboxPath = MailboxPath.inbox(BOB);
+        testSystem.mailboxManager.createMailbox(bobInboxPath, 
testSystem.bobSession);
+        return testSystem.mailboxManager.getMailbox(bobInboxPath, 
testSystem.bobSession)
+            .appendMessage(MessageManager.AppendCommand.builder()
+                    .build(message),
+                testSystem.bobSession)
+            .getId();
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/test/resources/json/mailboxesRestore.additionalInformation.json
 
b/server/protocols/webadmin/webadmin-mailbox/src/test/resources/json/mailboxesRestore.additionalInformation.json
new file mode 100644
index 0000000000..498cf44057
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/test/resources/json/mailboxesRestore.additionalInformation.json
@@ -0,0 +1,5 @@
+{
+  "type":"MailboxesRestoreTask",
+  "timestamp":"2007-12-03T10:15:30Z",
+  "username": "bob"
+}
diff --git 
a/server/protocols/webadmin/webadmin-mailbox/src/test/resources/json/mailboxesRestore.task.json
 
b/server/protocols/webadmin/webadmin-mailbox/src/test/resources/json/mailboxesRestore.task.json
new file mode 100644
index 0000000000..c19fc26185
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox/src/test/resources/json/mailboxesRestore.task.json
@@ -0,0 +1,5 @@
+{
+  "type":"MailboxesRestoreTask",
+  "username": "bob",
+  "blobId": "abc123"
+}
diff --git a/src/site/markdown/server/manage-webadmin.md 
b/src/site/markdown/server/manage-webadmin.md
index 73a61639e7..3431b47717 100644
--- a/src/site/markdown/server/manage-webadmin.md
+++ b/src/site/markdown/server/manage-webadmin.md
@@ -1665,7 +1665,7 @@ Response codes:
 ### Exporting user mailboxes
 
 ```
-curl -XPOST http://ip:port/users/{usernameToBeUsed}/mailboxes?action=export
+curl -XPOST http://ip:port/users/{usernameToBeUsed}/mailboxes?task=export
 ```
 
 Resource name `usernameToBeUsed` should be an existing user
@@ -1686,6 +1686,32 @@ The scheduled task will have the following type 
`MailboxesExportTask` and the fo
 }
 ```
 
+### Restoring user mailboxes
+
+```
+curl -XPOST http://ip:port/users/{usernameToBeUsed}/mailboxes?task=restore 
--data-binary @backup.zip
+```
+
+Resource name `usernameToBeUsed` should be an existing user. The request body 
must contain the ZIP backup data.
+
+Response codes:
+
+ - 201: Success. Corresponding task id is returned
+ - 400: The request body is empty
+ - 404: The user name does not exist
+
+The scheduled task will have the following type `MailboxesRestoreTask` and the 
following `additionalInformation`:
+
+```
+{
+  "type":"MailboxesRestoreTask",
+  "timestamp":"2007-12-03T10:15:30Z",
+  "username": "user"
+}
+```
+
+Note: The account must be empty for the restore to succeed. If the user 
already has mailboxes, the task will fail.
+
 ### ReIndexing a user mails
  
 ```


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to