Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions docs/modules/servers/partials/operate/webadmin.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -4136,6 +4136,61 @@ the following `additionalInformation`:
}
....

=== Moving mails from a mail repository to another

To move all mails from one repository to another:

....
curl -XPATCH http://ip:port/mailRepositories/{encodedPathOfTheRepository}/mails \
-d '{"mailRepository": "/var/mail/error-saved"}' \
-H "Content-Type: application/json"
....

Resource name `encodedPathOfTheRepository` should be the URL-encoded path of an existing mail
repository. The request body must contain the path of an existing target mail repository.

For instance:

....
curl -XPATCH http://ip:port/mailRepositories/var%2Fmail%2Ferror%2F/mails \
-d '{"mailRepository": "var/mail/error-saved"}' \
-H "Content-Type: application/json"
....

Response codes:

* 204: Mails were successfully moved.
* 400: The target repository does not exist, or the request body is invalid.
* 404: The source repository does not exist.

=== Moving a specific mail to another repository

To move a specific mail from one repository to another:

....
curl -XPATCH http://ip:port/mailRepositories/{encodedPathOfTheRepository}/mails/{mailKey} \
-d '{"mailRepository": "/var/mail/error-saved"}' \
-H "Content-Type: application/json"
....

Resource name `encodedPathOfTheRepository` should be the URL-encoded path of an existing mail
repository. Resource name `mailKey` should be the key of a mail stored in that repository.
The request body must contain the path of an existing target mail repository.

For instance:

....
curl -XPATCH http://ip:port/mailRepositories/var%2Fmail%2Ferror%2F/mails/name1 \
-d '{"mailRepository": "var/mail/error-saved"}' \
-H "Content-Type: application/json"
....

Response codes:

* 204: The mail was successfully moved (or the mail key was not found, in which case nothing is done).
* 400: The target repository does not exist, or the request body is invalid.
* 404: The source repository does not exist.

== Administrating mail queues

=== Listing mail queues
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/****************************************************************
* 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.dto;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

public class MoveMailRepositoryRequest {
private final String mailRepository;

@JsonCreator
public MoveMailRepositoryRequest(@JsonProperty("mailRepository") String mailRepository) {
this.mailRepository = mailRepository;
}

public String getMailRepository() {
return mailRepository;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
import org.apache.james.webadmin.dto.InaccessibleFieldException;
import org.apache.james.webadmin.dto.MailDto;
import org.apache.james.webadmin.dto.MailDto.AdditionalField;
import org.apache.james.webadmin.dto.MoveMailRepositoryRequest;
import org.apache.james.webadmin.service.MailRepositoryStoreService;
import org.apache.james.webadmin.service.ReprocessingAllMailsTask;
import org.apache.james.webadmin.service.ReprocessingOneMailTask;
Expand All @@ -61,6 +62,8 @@
import org.apache.james.webadmin.tasks.TaskRegistrationKey;
import org.apache.james.webadmin.utils.ErrorResponder;
import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
import org.apache.james.webadmin.utils.JsonExtractException;
import org.apache.james.webadmin.utils.JsonExtractor;
import org.apache.james.webadmin.utils.JsonTransformer;
import org.apache.james.webadmin.utils.ParametersExtractor;
import org.apache.james.webadmin.utils.Responses;
Expand All @@ -73,12 +76,16 @@

import spark.HaltException;
import spark.Request;
import spark.Response;
import spark.Route;
import spark.Service;

public class MailRepositoriesRoutes implements Routes {

public static final String MAIL_REPOSITORIES = "mailRepositories";
private static final TaskRegistrationKey REPROCESS_ACTION = TaskRegistrationKey.of("reprocess");
private static final JsonExtractor<MoveMailRepositoryRequest> MOVE_REQUEST_EXTRACTOR =
new JsonExtractor<>(MoveMailRepositoryRequest.class);

private final JsonTransformer jsonTransformer;
private final MailRepositoryStoreService repositoryStoreService;
Expand Down Expand Up @@ -117,9 +124,9 @@ public void define(Service service) {

defineDeleteAll();

defineReprocessAll();
definePatchAll();

defineReprocessOne();
definePatchOne();
}

public void definePutMailRepository() {
Expand Down Expand Up @@ -328,11 +335,14 @@ public void defineDeleteAll() {
service.delete(MAIL_REPOSITORIES + "/:encodedPath/mails", taskFromRequest.asRoute(taskManager), jsonTransformer);
}

public void defineReprocessAll() {
service.patch(MAIL_REPOSITORIES + "/:encodedPath/mails",
TaskFromRequestRegistry.of(REPROCESS_ACTION, this::reprocessAll)
.asRoute(taskManager),
jsonTransformer);
public void definePatchAll() {
Route reprocessRoute = TaskFromRequestRegistry.of(REPROCESS_ACTION, this::reprocessAll).asRoute(taskManager);
service.patch(MAIL_REPOSITORIES + "/:encodedPath/mails", (request, response) -> {
if (hasMoveRequestBody(request)) {
return moveAllMails(request, response);
}
return reprocessRoute.handle(request, response);
}, jsonTransformer);
}

private Task reprocessAll(Request request) throws MailRepositoryStore.MailRepositoryStoreException {
Expand All @@ -351,11 +361,14 @@ private ReprocessingService.Configuration extractConfiguration(Request request)
parseLimit(request));
}

public void defineReprocessOne() {
service.patch(MAIL_REPOSITORIES + "/:encodedPath/mails/:key",
TaskFromRequestRegistry.of(REPROCESS_ACTION, this::reprocessOne)
.asRoute(taskManager),
jsonTransformer);
public void definePatchOne() {
Route reprocessOneRoute = TaskFromRequestRegistry.of(REPROCESS_ACTION, this::reprocessOne).asRoute(taskManager);
service.patch(MAIL_REPOSITORIES + "/:encodedPath/mails/:key", (request, response) -> {
if (hasMoveRequestBody(request)) {
return moveOneMail(request, response);
}
return reprocessOneRoute.handle(request, response);
}, jsonTransformer);
}

private Task reprocessOne(Request request) {
Expand All @@ -365,6 +378,87 @@ private Task reprocessOne(Request request) {
return new ReprocessingOneMailTask(reprocessingService, path, extractConfiguration(request), key, Clock.systemUTC());
}

private boolean hasMoveRequestBody(Request request) {
String body = request.body();
return body != null && !body.isBlank();
}

private Object moveAllMails(Request request, Response response) {
MailRepositoryPath sourcePath = getRepositoryPath(request);
MoveMailRepositoryRequest moveRequest = parseMoveRequest(request);
MailRepositoryPath targetPath = MailRepositoryPath.from(moveRequest.getMailRepository());
try {
if (!repositoryStoreService.repositoryExists(sourcePath)) {
throw repositoryNotFound(request.params("encodedPath"), sourcePath);
}
if (!repositoryStoreService.repositoryExists(targetPath)) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.BAD_REQUEST_400)
.type(ErrorType.INVALID_ARGUMENT)
.message("The target repository '%s' does not exist", moveRequest.getMailRepository())
.haltError();
}
repositoryStoreService.moveAllMails(sourcePath, targetPath);
return Responses.returnNoContent(response);
} catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorType.SERVER_ERROR)
.cause(e)
.message("Error while moving mails")
.haltError();
}
}

private Object moveOneMail(Request request, Response response) {
MailRepositoryPath sourcePath = getRepositoryPath(request);
MailKey mailKey = new MailKey(request.params("key"));
MoveMailRepositoryRequest moveRequest = parseMoveRequest(request);
MailRepositoryPath targetPath = MailRepositoryPath.from(moveRequest.getMailRepository());
try {
if (!repositoryStoreService.repositoryExists(sourcePath)) {
throw repositoryNotFound(request.params("encodedPath"), sourcePath);
}
if (!repositoryStoreService.repositoryExists(targetPath)) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.BAD_REQUEST_400)
.type(ErrorType.INVALID_ARGUMENT)
.message("The target repository '%s' does not exist", moveRequest.getMailRepository())
.haltError();
}
repositoryStoreService.moveMail(sourcePath, targetPath, mailKey);
return Responses.returnNoContent(response);
} catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorType.SERVER_ERROR)
.cause(e)
.message("Error while moving mail")
.haltError();
}
}

private MoveMailRepositoryRequest parseMoveRequest(Request request) {
try {
MoveMailRepositoryRequest moveRequest = MOVE_REQUEST_EXTRACTOR.parse(request.body());
if (moveRequest.getMailRepository() == null || moveRequest.getMailRepository().isBlank()) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.BAD_REQUEST_400)
.type(ErrorType.INVALID_ARGUMENT)
.message("'mailRepository' field is mandatory in request body")
.haltError();
}
return moveRequest;
} catch (JsonExtractException e) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.BAD_REQUEST_400)
.type(ErrorType.INVALID_ARGUMENT)
.cause(e)
.message("Invalid JSON body")
.haltError();
}
}

private Set<AdditionalField> extractAdditionalFields(String additionalFieldsParam) throws IllegalArgumentException {
return Splitter
.on(',')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
import jakarta.mail.MessagingException;
import jakarta.mail.internet.MimeMessage;

import org.apache.commons.lang3.tuple.Pair;
import org.apache.james.lifecycle.api.LifecycleUtil;
import org.apache.james.mailrepository.api.MailKey;
import org.apache.james.mailrepository.api.MailRepository;
Expand Down Expand Up @@ -116,6 +117,40 @@ public void deleteMail(MailRepositoryPath path, MailKey mailKey) throws MailRepo
.forEach(Throwing.consumer((MailRepository repository) -> repository.remove(mailKey)).sneakyThrow());
}

public boolean repositoryExists(MailRepositoryPath path) throws MailRepositoryStore.MailRepositoryStoreException {
return mailRepositoryStore.getByPath(path).findAny().isPresent();
}

public void moveMail(MailRepositoryPath sourcePath, MailRepositoryPath targetPath, MailKey mailKey) throws MailRepositoryStore.MailRepositoryStoreException, MessagingException {
MailRepository source = getRepositories(sourcePath).findFirst()
.orElseThrow(() -> new MailRepositoryStore.MailRepositoryStoreException("No repository found for path: " + sourcePath.asString()));
MailRepository target = getRepositories(targetPath).findFirst()
.orElseThrow(() -> new MailRepositoryStore.MailRepositoryStoreException("No repository found for path: " + targetPath.asString()));

moveMail(source, target, mailKey);
}

public void moveAllMails(MailRepositoryPath sourcePath, MailRepositoryPath targetPath) throws MailRepositoryStore.MailRepositoryStoreException, MessagingException {
MailRepository target = getRepositories(targetPath).findFirst()
.orElseThrow(() -> new MailRepositoryStore.MailRepositoryStoreException("No repository found for path: " + targetPath.asString()));

getRepositories(sourcePath)
.flatMap(Throwing.function(repo -> Iterators.toStream(repo.list()).map(key -> Pair.of(repo, key))))
.forEach(Throwing.<Pair<MailRepository, MailKey>>consumer(pair -> moveMail(pair.getKey(), target, pair.getValue())).sneakyThrow());
}

private void moveMail(MailRepository source, MailRepository target, MailKey key) throws MessagingException {
Optional.ofNullable(source.retrieve(key))
.ifPresent(Throwing.consumer(mail -> {
try {
target.store(mail);
source.remove(key);
} finally {
LifecycleUtil.dispose(mail);
}
}));
}

public Task createClearMailRepositoryTask(MailRepositoryPath path) throws MailRepositoryStore.MailRepositoryStoreException, MessagingException {
getRepositories(path);
return new ClearMailRepositoryTask(mailRepositoryStore, path);
Expand Down
Loading