blob: c9fc45ee28f2c47a89d2d889efa636abae5cbbbb [file] [log] [blame]
/****************************************************************
* 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.routes;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.time.Clock;
import java.util.List;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
import javax.inject.Inject;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import org.apache.james.mailrepository.api.MailKey;
import org.apache.james.mailrepository.api.MailRepositoryPath;
import org.apache.james.mailrepository.api.MailRepositoryStore;
import org.apache.james.queue.api.MailQueueFactory;
import org.apache.james.task.Task;
import org.apache.james.task.TaskId;
import org.apache.james.task.TaskManager;
import org.apache.james.util.streams.Limit;
import org.apache.james.util.streams.Offset;
import org.apache.james.webadmin.Constants;
import org.apache.james.webadmin.Routes;
import org.apache.james.webadmin.dto.ExtendedMailRepositoryResponse;
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.TaskIdDto;
import org.apache.james.webadmin.service.MailRepositoryStoreService;
import org.apache.james.webadmin.service.ReprocessingAllMailsTask;
import org.apache.james.webadmin.service.ReprocessingOneMailTask;
import org.apache.james.webadmin.service.ReprocessingService;
import org.apache.james.webadmin.utils.ErrorResponder;
import org.apache.james.webadmin.utils.ErrorResponder.ErrorType;
import org.apache.james.webadmin.utils.JsonTransformer;
import org.apache.james.webadmin.utils.ParametersExtractor;
import org.apache.james.webadmin.utils.Responses;
import org.eclipse.jetty.http.HttpStatus;
import com.github.steveash.guavate.Guavate;
import com.google.common.base.Splitter;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import io.swagger.annotations.ApiResponses;
import io.swagger.jaxrs.PATCH;
import spark.HaltException;
import spark.Request;
import spark.Service;
@Api(tags = "MailRepositories", consumes = "application/json")
@Path("/mailRepositories")
@Produces("application/json")
public class MailRepositoriesRoutes implements Routes {
public static final String MAIL_REPOSITORIES = "mailRepositories";
private final JsonTransformer jsonTransformer;
private final MailRepositoryStoreService repositoryStoreService;
private final ReprocessingService reprocessingService;
private final TaskManager taskManager;
private Service service;
@Inject
public MailRepositoriesRoutes(MailRepositoryStoreService repositoryStoreService, JsonTransformer jsonTransformer, ReprocessingService reprocessingService, TaskManager taskManager) {
this.repositoryStoreService = repositoryStoreService;
this.jsonTransformer = jsonTransformer;
this.reprocessingService = reprocessingService;
this.taskManager = taskManager;
}
@Override
public String getBasePath() {
return MAIL_REPOSITORIES;
}
@Override
public void define(Service service) {
this.service = service;
definePutMailRepository();
defineGetMailRepositories();
defineListMails();
defineGetMailRepository();
defineGetMail();
defineDeleteMail();
defineDeleteAll();
defineReprocessAll();
defineReprocessOne();
}
@PUT
@Path("/{encodedPath}")
@ApiOperation(value = "Create a repository")
@ApiImplicitParams({
@ApiImplicitParam(
required = true,
dataType = "String",
name = "protocol",
paramType = "query",
example = "?protocol=file",
value = "Specify the storage protocol to use"),
})
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.NO_CONTENT_204, message = "The repository is created"),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
})
public void definePutMailRepository() {
service.put(MAIL_REPOSITORIES + "/:encodedPath", (request, response) -> {
MailRepositoryPath path = decodedRepositoryPath(request);
String protocol = request.queryParams("protocol");
try {
repositoryStoreService.createMailRepository(path, protocol);
return Responses.returnNoContent(response);
} catch (MailRepositoryStore.MailRepositoryStoreException e) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorResponder.ErrorType.SERVER_ERROR)
.cause(e)
.message(String.format("Error while creating a mail repository with path '%s' and protocol '%s'", path.asString(), protocol))
.haltError();
}
}, jsonTransformer);
}
@GET
@Path("/{encodedPath}/mails")
@ApiOperation(value = "Listing all mails in a repository")
@ApiImplicitParams({
@ApiImplicitParam(
required = false,
name = "offset",
paramType = "query parameter",
dataType = "Integer",
defaultValue = "0",
example = "?offset=100",
value = "If present, skips the given number of key in the output."),
@ApiImplicitParam(
required = false,
paramType = "query parameter",
name = "limit",
dataType = "Integer",
defaultValue = "absent",
example = "?limit=100",
value = "If present, fixes the maximal number of key returned in that call. Must be more than zero if specified.")
})
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.OK_200, message = "The list of all mails in a repository", response = List.class),
@ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - invalid parameter"),
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The repository does not exist", response = ErrorResponder.class),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
})
public void defineListMails() {
service.get(MAIL_REPOSITORIES + "/:encodedPath/mails", (request, response) -> {
Offset offset = ParametersExtractor.extractOffset(request);
Limit limit = ParametersExtractor.extractLimit(request);
MailRepositoryPath path = decodedRepositoryPath(request);
try {
return repositoryStoreService.listMails(path, offset, limit)
.orElseThrow(() -> repositoryNotFound(request.params("encodedPath"), path));
} catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorResponder.ErrorType.SERVER_ERROR)
.cause(e)
.message("Error while listing keys")
.haltError();
}
}, jsonTransformer);
}
@GET
@ApiOperation(value = "Listing all mail repositories URLs")
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.OK_200, message = "Listing all mail repositories URLs", response = List.class),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side.")
})
public void defineGetMailRepositories() {
service.get(MAIL_REPOSITORIES,
(request, response) -> repositoryStoreService.listMailRepositories().collect(Guavate.toImmutableList()),
jsonTransformer);
}
@GET
@Produces("application/json, message/rfc822")
@Path("/{encodedPath}/mails/{mailKey}")
@ApiOperation(value = "Retrieving a specific mail details (this endpoint can accept both \"application/json\" or \"message/rfc822\")")
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.OK_200, message = "The list of all mails in a repository", response = List.class),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "Not found - Could not retrieve the given mail.")
})
public void defineGetMail() {
service.get(MAIL_REPOSITORIES + "/:encodedPath/mails/:mailKey", Constants.JSON_CONTENT_TYPE,
(request, response) ->
getMailAsJson(decodedRepositoryPath(request), new MailKey(request.params("mailKey")), request),
jsonTransformer);
service.get(MAIL_REPOSITORIES + "/:encodedPath/mails/:mailKey", Constants.RFC822_CONTENT_TYPE,
(request, response) -> writeMimeMessage(
getMailAsMimeMessage(
decodedRepositoryPath(request),
new MailKey(request.params("mailKey"))),
response.raw()));
}
private Object writeMimeMessage(MimeMessage mimeMessage, HttpServletResponse rawResponse) throws MessagingException, IOException {
rawResponse.setContentType(Constants.RFC822_CONTENT_TYPE);
rawResponse.setHeader("Content-Length", String.valueOf(computeExactSize(mimeMessage)));
mimeMessage.writeTo(rawResponse.getOutputStream());
return rawResponse;
}
private long computeExactSize(MimeMessage mimeMessage) throws IOException, MessagingException {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
mimeMessage.writeTo(byteArrayOutputStream);
return byteArrayOutputStream.size();
}
private MimeMessage getMailAsMimeMessage(MailRepositoryPath path, MailKey mailKey) {
try {
return repositoryStoreService.retrieveMessage(path, mailKey)
.orElseThrow(mailNotFoundError(mailKey));
} catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
throw internalServerError(e);
}
}
private MailDto getMailAsJson(MailRepositoryPath path, MailKey mailKey, Request request) {
try {
return repositoryStoreService.retrieveMail(path, mailKey, extractAdditionalFields(request.queryParamOrDefault("additionalFields", "")))
.orElseThrow(mailNotFoundError(mailKey));
} catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
throw internalServerError(e);
} catch (IllegalArgumentException e) {
throw invalidField(e);
} catch (InaccessibleFieldException e) {
throw inaccessibleField(e);
}
}
private HaltException inaccessibleField(InaccessibleFieldException e) {
return ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorType.SERVER_ERROR)
.cause(e)
.message("The field '" + e.getField().getName() + "' requested in additionalFields parameter can't be accessed")
.haltError();
}
private HaltException invalidField(IllegalArgumentException e) {
return ErrorResponder.builder()
.statusCode(HttpStatus.BAD_REQUEST_400)
.type(ErrorType.INVALID_ARGUMENT)
.cause(e)
.message("The field '" + e.getMessage() + "' can't be requested in additionalFields parameter")
.haltError();
}
private Supplier<HaltException> mailNotFoundError(MailKey mailKey) {
return () -> ErrorResponder.builder()
.statusCode(HttpStatus.NOT_FOUND_404)
.type(ErrorResponder.ErrorType.NOT_FOUND)
.message("Could not retrieve " + mailKey.asString())
.haltError();
}
private HaltException repositoryNotFound(String encodedPath, MailRepositoryPath path) {
return ErrorResponder.builder()
.statusCode(HttpStatus.NOT_FOUND_404)
.type(ErrorType.NOT_FOUND)
.message("The repository '" + encodedPath + "' (decoded value: '" + path.asString() + "') does not exist")
.haltError();
}
private HaltException internalServerError(Exception e) {
return ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorResponder.ErrorType.SERVER_ERROR)
.cause(e)
.message("Error while retrieving mail")
.haltError();
}
@GET
@Path("/{encodedPath}")
@ApiOperation(value = "Reading the information of a repository, such as size (can take some time to compute)")
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.OK_200, message = "The repository information", response = List.class),
@ApiResponse(code = HttpStatus.NOT_FOUND_404, message = "The repository does not exist", response = ErrorResponder.class),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
})
public void defineGetMailRepository() {
service.get(MAIL_REPOSITORIES + "/:encodedPath", (request, response) -> {
MailRepositoryPath path = decodedRepositoryPath(request);
try {
long size = repositoryStoreService.size(path)
.orElseThrow(() -> repositoryNotFound(request.params("encodedPath"), path));
return new ExtendedMailRepositoryResponse(path, size);
} catch (MailRepositoryStore.MailRepositoryStoreException e) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorResponder.ErrorType.SERVER_ERROR)
.cause(e)
.message("Error while retrieving mail repository information")
.haltError();
}
}, jsonTransformer);
}
@DELETE
@Path("/{encodedPath}/mails/{mailKey}")
@ApiOperation(value = "Deleting a specific mail from that mailRepository")
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.OK_200, message = "Mail is no more stored in the repository", response = List.class),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
})
public void defineDeleteMail() {
service.delete(MAIL_REPOSITORIES + "/:encodedPath/mails/:mailKey", (request, response) -> {
MailRepositoryPath path = decodedRepositoryPath(request);
MailKey mailKey = new MailKey(request.params("mailKey"));
try {
repositoryStoreService.deleteMail(path, mailKey);
return Responses.returnNoContent(response);
} catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorResponder.ErrorType.SERVER_ERROR)
.cause(e)
.message("Error while deleting mail")
.haltError();
}
});
}
@DELETE
@Path("/{encodedPath}/mails")
@ApiOperation(value = "Deleting all mails in that mailRepository")
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.CREATED_201, message = "All mails are deleted", response = TaskIdDto.class),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
@ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - unknown action")
})
public void defineDeleteAll() {
service.delete(MAIL_REPOSITORIES + "/:encodedPath/mails", (request, response) -> {
MailRepositoryPath path = decodedRepositoryPath(request);
try {
Task task = repositoryStoreService.createClearMailRepositoryTask(path);
TaskId taskId = taskManager.submit(task);
return TaskIdDto.respond(response, taskId);
} catch (MailRepositoryStore.MailRepositoryStoreException | MessagingException e) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.INTERNAL_SERVER_ERROR_500)
.type(ErrorResponder.ErrorType.SERVER_ERROR)
.cause(e)
.message("Error while deleting all mails")
.haltError();
}
}, jsonTransformer);
}
@PATCH
@Path("/{encodedPath}/mails")
@ApiOperation(value = "Reprocessing all mails in that mailRepository")
@ApiImplicitParams({
@ApiImplicitParam(
required = true,
name = "action",
paramType = "query parameter",
dataType = "String",
defaultValue = "none",
example = "?action=reprocess",
value = "Compulsory. Only supported value is `reprocess`"),
@ApiImplicitParam(
required = false,
name = "queue",
paramType = "query parameter",
dataType = "String",
defaultValue = "spool",
example = "?queue=outgoing",
value = "Indicates in which queue the mails stored in the repository should be re-enqueued"),
@ApiImplicitParam(
required = false,
paramType = "query parameter",
name = "processor",
dataType = "String",
defaultValue = "absent",
example = "?processor=transport",
value = "If present, modifies the state property of the mail to allow their processing by a specific mail container processor.")
})
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.CREATED_201, message = "Task is created", response = TaskIdDto.class),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
@ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - unknown action")
})
public void defineReprocessAll() {
service.patch(MAIL_REPOSITORIES + "/:encodedPath/mails", (request, response) -> {
Task task = toAllMailReprocessingTask(request);
TaskId taskId = taskManager.submit(task);
return TaskIdDto.respond(response, taskId);
}, jsonTransformer);
}
private Task toAllMailReprocessingTask(Request request) throws UnsupportedEncodingException, MailRepositoryStore.MailRepositoryStoreException, MessagingException {
MailRepositoryPath path = decodedRepositoryPath(request);
enforceActionParameter(request);
Optional<String> targetProcessor = Optional.ofNullable(request.queryParams("processor"));
String targetQueue = Optional.ofNullable(request.queryParams("queue")).orElse(MailQueueFactory.SPOOL);
Long repositorySize = repositoryStoreService.size(path).orElse(0L);
return new ReprocessingAllMailsTask(reprocessingService, repositorySize, path, targetQueue, targetProcessor);
}
@PATCH
@Path("/{encodedPath}/mails/{key}")
@ApiOperation(value = "Reprocessing a single mail in that mailRepository")
@ApiImplicitParams({
@ApiImplicitParam(
required = true,
name = "action",
paramType = "query parameter",
dataType = "String",
defaultValue = "none",
example = "?action=reprocess",
value = "Compulsory. Only supported value is `reprocess`"),
@ApiImplicitParam(
required = false,
name = "queue",
paramType = "query parameter",
dataType = "String",
defaultValue = "spool",
example = "?queue=outgoing",
value = "Indicates in which queue the mails stored in the repository should be re-enqueued"),
@ApiImplicitParam(
required = false,
paramType = "query parameter",
name = "processor",
dataType = "String",
defaultValue = "absent",
example = "?processor=transport",
value = "If present, modifies the state property of the mail to allow their processing by a specific mail container processor.")
})
@ApiResponses(value = {
@ApiResponse(code = HttpStatus.CREATED_201, message = "Task is created", response = TaskIdDto.class),
@ApiResponse(code = HttpStatus.INTERNAL_SERVER_ERROR_500, message = "Internal server error - Something went bad on the server side."),
@ApiResponse(code = HttpStatus.BAD_REQUEST_400, message = "Bad request - unknown action")
})
public void defineReprocessOne() {
service.patch(MAIL_REPOSITORIES + "/:encodedPath/mails/:key", (request, response) -> {
Task task = toOneMailReprocessingTask(request);
TaskId taskId = taskManager.submit(task);
return TaskIdDto.respond(response, taskId);
}, jsonTransformer);
}
private Task toOneMailReprocessingTask(Request request) throws UnsupportedEncodingException {
MailRepositoryPath path = decodedRepositoryPath(request);
MailKey key = new MailKey(request.params("key"));
enforceActionParameter(request);
Optional<String> targetProcessor = Optional.ofNullable(request.queryParams("processor"));
String targetQueue = Optional.ofNullable(request.queryParams("queue")).orElse(MailQueueFactory.SPOOL);
return new ReprocessingOneMailTask(reprocessingService, path, targetQueue, key, targetProcessor, Clock.systemUTC());
}
private void enforceActionParameter(Request request) {
String action = request.queryParams("action");
if (!"reprocess".equals(action)) {
throw ErrorResponder.builder()
.statusCode(HttpStatus.BAD_REQUEST_400)
.type(ErrorType.INVALID_ARGUMENT)
.message("action query parameter is mandatory. The only supported value is `reprocess`")
.haltError();
}
}
private Set<AdditionalField> extractAdditionalFields(String additionalFieldsParam) throws IllegalArgumentException {
return Splitter
.on(',')
.trimResults()
.omitEmptyStrings()
.splitToList(additionalFieldsParam)
.stream()
.map((field) -> AdditionalField.find(field).orElseThrow(() -> new IllegalArgumentException(field)))
.collect(Guavate.toImmutableSet());
}
private MailRepositoryPath decodedRepositoryPath(Request request) throws UnsupportedEncodingException {
return MailRepositoryPath.fromEncoded(request.params("encodedPath"));
}
}