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

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

commit e7db516995a165a69890896967355954068561f7
Author: Benoit TELLIER <[email protected]>
AuthorDate: Thu Mar 5 20:52:41 2026 +0100

    JAMES-4185 Ability to browse deleted message vault
---
 .../modules/servers/partials/operate/webadmin.adoc |  62 +++++++++++
 .../webadmin/vault/routes/DeletedMessageDTO.java   | 116 +++++++++++++++++++++
 .../vault/routes/DeletedMessagesVaultRoutes.java   |  16 ++-
 .../routes/DeletedMessagesVaultRoutesTest.java     | 113 ++++++++++++++++++++
 4 files changed, 306 insertions(+), 1 deletion(-)

diff --git a/docs/modules/servers/partials/operate/webadmin.adoc 
b/docs/modules/servers/partials/operate/webadmin.adoc
index da74695d2f..c5dee36353 100644
--- a/docs/modules/servers/partials/operate/webadmin.adoc
+++ b/docs/modules/servers/partials/operate/webadmin.adoc
@@ -4576,6 +4576,68 @@ _restore_ users deleted messages or export them in an 
archive.
 To move deleted messages in the vault, you need to specifically
 configure the DeletedMessageVault PreDeletionHook.
 
+=== Browse Deleted Messages
+
+Deleted messages of a specific user can be listed by calling the following 
endpoint:
+
+....
+curl -XPOST http://ip:port/deletedMessages/users/[email protected]/messages
+
+{
+  "combinator": "and",
+  "criteria": [
+    {
+      "fieldName": "subject",
+      "operator": "containsIgnoreCase",
+      "value": "Apache James"
+    }
+  ]
+}
+....
+
+The query body follows the same structure as 
link:#_restore_deleted_messages[Restore Deleted Messages].
+Pass an empty criteria list to retrieve all deleted messages:
+
+....
+{
+  "combinator": "and",
+  "criteria": []
+}
+....
+
+Response code:
+
+* 200: List of deleted messages matching the query
+* 400: Bad request:
+** user parameter is invalid
+** can not parse the JSON body
+** Json query object contains unsupported operator, fieldName
+** Json query object values violate parsing rules
+* 404: User not found
+
+Response body is a JSON array:
+
+....
+[
+  {
+    "messageId": "3294a976-ce63-491e-bd52-1b6f465ed7a2",
+    "originMailboxes": [
+      "02874f7c-d10e-102f-acda-0015176f7922"
+    ],
+    "owner": "[email protected]",
+    "deliveryDate": "2014-10-30T14:12:00Z",
+    "deletionDate": "2015-10-20T09:08:00Z",
+    "sender": "[email protected]",
+    "recipients": [
+      "[email protected]"
+    ],
+    "subject": "Apache James",
+    "hasAttachment": false,
+    "size": 1234
+  }
+]
+....
+
 === Restore Deleted Messages
 
 Deleted messages of a specific user can be restored by calling the
diff --git 
a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessageDTO.java
 
b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessageDTO.java
new file mode 100644
index 0000000000..d19f0002d6
--- /dev/null
+++ 
b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessageDTO.java
@@ -0,0 +1,116 @@
+/****************************************************************
+ * 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.vault.routes;
+
+import java.time.ZonedDateTime;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.james.vault.DeletedMessage;
+
+import com.google.common.collect.ImmutableList;
+
+public class DeletedMessageDTO {
+
+    public static DeletedMessageDTO from(DeletedMessage message) {
+        return new DeletedMessageDTO(
+            message.getMessageId().serialize(),
+            message.getOriginMailboxes().stream()
+                .map(id -> id.serialize())
+                .collect(ImmutableList.toImmutableList()),
+            message.getOwner().asString(),
+            message.getDeliveryDate(),
+            message.getDeletionDate(),
+            message.getSender().asOptional().map(Object::toString),
+            message.getRecipients().stream()
+                .map(Object::toString)
+                .collect(ImmutableList.toImmutableList()),
+            message.getSubject(),
+            message.hasAttachment(),
+            message.getSize());
+    }
+
+    private final String messageId;
+    private final List<String> originMailboxes;
+    private final String owner;
+    private final ZonedDateTime deliveryDate;
+    private final ZonedDateTime deletionDate;
+    private final Optional<String> sender;
+    private final List<String> recipients;
+    private final Optional<String> subject;
+    private final boolean hasAttachment;
+    private final long size;
+
+    private DeletedMessageDTO(String messageId, List<String> originMailboxes, 
String owner,
+                               ZonedDateTime deliveryDate, ZonedDateTime 
deletionDate,
+                               Optional<String> sender, List<String> 
recipients,
+                               Optional<String> subject, boolean 
hasAttachment, long size) {
+        this.messageId = messageId;
+        this.originMailboxes = originMailboxes;
+        this.owner = owner;
+        this.deliveryDate = deliveryDate;
+        this.deletionDate = deletionDate;
+        this.sender = sender;
+        this.recipients = recipients;
+        this.subject = subject;
+        this.hasAttachment = hasAttachment;
+        this.size = size;
+    }
+
+    public String getMessageId() {
+        return messageId;
+    }
+
+    public List<String> getOriginMailboxes() {
+        return originMailboxes;
+    }
+
+    public String getOwner() {
+        return owner;
+    }
+
+    public ZonedDateTime getDeliveryDate() {
+        return deliveryDate;
+    }
+
+    public ZonedDateTime getDeletionDate() {
+        return deletionDate;
+    }
+
+    public Optional<String> getSender() {
+        return sender;
+    }
+
+    public List<String> getRecipients() {
+        return recipients;
+    }
+
+    public Optional<String> getSubject() {
+        return subject;
+    }
+
+    public boolean isHasAttachment() {
+        return hasAttachment;
+    }
+
+    public long getSize() {
+        return size;
+    }
+}
diff --git 
a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java
 
b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java
index a3efb0c239..dcf3bd997d 100644
--- 
a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java
+++ 
b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/main/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutes.java
@@ -40,6 +40,8 @@ import org.apache.james.vault.dto.query.QueryTranslator;
 import org.apache.james.vault.search.Query;
 import org.apache.james.webadmin.Routes;
 import org.apache.james.webadmin.tasks.TaskFromRequest;
+
+import reactor.core.publisher.Flux;
 import org.apache.james.webadmin.tasks.TaskFromRequestRegistry;
 import org.apache.james.webadmin.tasks.TaskRegistrationKey;
 import org.apache.james.webadmin.utils.ErrorResponder;
@@ -65,7 +67,8 @@ public class DeletedMessagesVaultRoutes implements Routes {
     private static final String USER_PATH_PARAM = ":user";
     private static final String MESSAGE_ID_PARAM = ":messageId";
     static final String USER_PATH = ROOT_PATH + SEPARATOR + USERS + SEPARATOR 
+ USER_PATH_PARAM;
-    private static final String DELETE_PATH = ROOT_PATH + SEPARATOR + USERS + 
SEPARATOR + USER_PATH_PARAM + SEPARATOR + MESSAGE_PATH_PARAM + SEPARATOR + 
MESSAGE_ID_PARAM;
+    static final String MESSAGES_PATH = ROOT_PATH + SEPARATOR + USERS + 
SEPARATOR + USER_PATH_PARAM + SEPARATOR + MESSAGE_PATH_PARAM;
+    private static final String DELETE_PATH = MESSAGES_PATH + SEPARATOR + 
MESSAGE_ID_PARAM;
     private static final String SCOPE_QUERY_PARAM = "scope";
     private static final String EXPORT_TO_QUERY_PARAM = "exportTo";
 
@@ -103,6 +106,7 @@ public class DeletedMessagesVaultRoutes implements Routes {
     @Override
     public void define(Service service) {
         service.post(USER_PATH, userActions(), jsonTransformer);
+        service.post(MESSAGES_PATH, this::browseMessages, jsonTransformer);
         service.delete(ROOT_PATH, deleteWithScope(), jsonTransformer);
 
         TaskFromRequest deleteTaskFromRequest = this::deleteMessage;
@@ -122,6 +126,16 @@ public class DeletedMessagesVaultRoutes implements Routes {
         return new DeletedMessagesVaultExportTask(vaultExport, username, 
extractQuery(request), extractMailAddress(request));
     }
 
+    private Object browseMessages(Request request, spark.Response response) 
throws JsonExtractException {
+        Username username = extractUser(request);
+        validateUserExist(username);
+        Query query = extractQuery(request);
+        return Flux.from(deletedMessageVault.search(username, query))
+            .map(DeletedMessageDTO::from)
+            .collectList()
+            .block();
+    }
+
     private Task restore(Request request) throws JsonExtractException {
         Username username = extractUser(request);
         validateUserExist(username);
diff --git 
a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java
 
b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java
index b1d8e0f26e..80ba05d218 100644
--- 
a/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java
+++ 
b/server/protocols/webadmin/webadmin-mailbox-deleted-message-vault/src/test/java/org/apache/james/webadmin/vault/routes/DeletedMessagesVaultRoutesTest.java
@@ -25,6 +25,7 @@ import static io.restassured.RestAssured.with;
 import static org.apache.james.vault.DeletedMessageFixture.CONTENT;
 import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE;
 import static org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE_2;
+import static 
org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE_OTHER_USER;
 import static 
org.apache.james.vault.DeletedMessageFixture.DELETED_MESSAGE_GENERATOR;
 import static org.apache.james.vault.DeletedMessageFixture.DELETION_DATE;
 import static org.apache.james.vault.DeletedMessageFixture.DELIVERY_DATE;
@@ -2362,4 +2363,116 @@ class DeletedMessagesVaultRoutesTest {
         Mono.from(Mono.from(vault.append(deletedMessage, new 
ByteArrayInputStream(CONTENT))))
             .block();
     }
+
+    @Nested
+    class BrowseMessagesTest {
+
+        private static final String BOB_MESSAGES_PATH = BOB_PATH + SEPARATOR + 
MESSAGE_PATH_PARAM;
+
+        @Test
+        void browseMessagesShouldReturn404WhenUserDoesNotExist() {
+            given()
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(USERS + SEPARATOR + "[email protected]" + SEPARATOR + 
MESSAGE_PATH_PARAM)
+            .then()
+                .statusCode(HttpStatus.NOT_FOUND_404);
+        }
+
+        @Test
+        void browseMessagesShouldReturn400WhenQueryBodyIsInvalid() {
+            given()
+                .body("{\"invalid\": \"json query\"}")
+            .when()
+                .post(BOB_MESSAGES_PATH)
+            .then()
+                .statusCode(HttpStatus.BAD_REQUEST_400);
+        }
+
+        @Test
+        void browseMessagesShouldReturnEmptyListWhenVaultIsEmpty() {
+            given()
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(BOB_MESSAGES_PATH)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .body("", hasSize(0));
+        }
+
+        @Test
+        void browseMessagesShouldReturnStoredMessages() {
+            storeDeletedMessage(DELETED_MESSAGE);
+            storeDeletedMessage(DELETED_MESSAGE_2);
+
+            given()
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(BOB_MESSAGES_PATH)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .body("", hasSize(2));
+        }
+
+        @Test
+        void browseMessagesShouldReturnMessageFields() {
+            storeDeletedMessage(DELETED_MESSAGE);
+
+            given()
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(BOB_MESSAGES_PATH)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .body("[0].messageId", is(MESSAGE_ID.serialize()))
+                .body("[0].owner", is(USERNAME.asString()))
+                .body("[0].hasAttachment", is(false))
+                .body("[0].size", is((int) CONTENT.length))
+                .body("[0].deliveryDate", is(notNullValue()))
+                .body("[0].deletionDate", is(notNullValue()))
+                .body("[0].originMailboxes", hasSize(2))
+                .body("[0].recipients", hasSize(2));
+        }
+
+        @Test
+        void browseMessagesShouldNotReturnMessagesFromOtherUsers() {
+            storeDeletedMessage(DELETED_MESSAGE);
+            Mono.from(vault.append(DELETED_MESSAGE_OTHER_USER, new 
ByteArrayInputStream(CONTENT))).block();
+
+            given()
+                .body(MATCH_ALL_QUERY)
+            .when()
+                .post(BOB_MESSAGES_PATH)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .body("", hasSize(1))
+                .body("[0].messageId", is(MESSAGE_ID.serialize()));
+        }
+
+        @Test
+        void browseMessagesShouldFilterByCriteria() {
+            storeDeletedMessage(DELETED_MESSAGE);
+            storeDeletedMessage(DELETED_MESSAGE_2);
+
+            String subjectQuery = "{" +
+                "\"combinator\": \"and\"," +
+                "\"criteria\": [{" +
+                "  \"fieldName\": \"subject\"," +
+                "  \"operator\": \"equals\"," +
+                "  \"value\": \"" + SUBJECT + "\"" +
+                "}]}";
+
+            DeletedMessage messageWithSubject = 
FINAL_STAGE.get().subject(SUBJECT).build();
+            storeDeletedMessage(messageWithSubject);
+
+            given()
+                .body(subjectQuery)
+            .when()
+                .post(BOB_MESSAGES_PATH)
+            .then()
+                .statusCode(HttpStatus.OK_200)
+                .body("", hasSize(1))
+                .body("[0].messageId", is(MESSAGE_ID.serialize()));
+        }
+    }
 }
\ No newline at end of file


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

Reply via email to