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]
