Repository: james-project Updated Branches: refs/heads/master dc5cefcd7 -> c94d35e22
JAMES-2321 WebAdmin should allow to see more details about Mails Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/c94d35e2 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/c94d35e2 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/c94d35e2 Branch: refs/heads/master Commit: c94d35e22ea841c67054ad2247fe76ff3d20a61a Parents: dc5cefc Author: Gautier DI FOLCO <[email protected]> Authored: Mon Jun 4 12:20:39 2018 +0200 Committer: Gautier DI FOLCO <[email protected]> Committed: Fri Jun 15 15:53:12 2018 +0200 ---------------------------------------------------------------------- .../james/core/builder/MimeMessageBuilder.java | 7 + .../core/builder/MimeMessageBuilderTest.java | 13 + server/protocols/webadmin/webadmin-core/pom.xml | 4 + .../james/webadmin/utils/JsonTransformer.java | 2 + .../webadmin/webadmin-mailrepository/pom.xml | 11 + .../apache/james/webadmin/dto/HeadersDto.java | 36 +++ .../dto/InaccessibleFieldException.java | 42 ++++ .../org/apache/james/webadmin/dto/MailDto.java | 238 +++++++++++++++++- .../webadmin/routes/MailRepositoriesRoutes.java | 49 +++- .../service/MailRepositoryStoreService.java | 7 +- .../routes/MailRepositoriesRoutesTest.java | 244 ++++++++++++++++++- 11 files changed, 639 insertions(+), 14 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/core/src/main/java/org/apache/james/core/builder/MimeMessageBuilder.java ---------------------------------------------------------------------- diff --git a/core/src/main/java/org/apache/james/core/builder/MimeMessageBuilder.java b/core/src/main/java/org/apache/james/core/builder/MimeMessageBuilder.java index a45746d..5a33892 100644 --- a/core/src/main/java/org/apache/james/core/builder/MimeMessageBuilder.java +++ b/core/src/main/java/org/apache/james/core/builder/MimeMessageBuilder.java @@ -81,6 +81,12 @@ public class MimeMessageBuilder { public static class MultipartBuilder { private ImmutableList.Builder<BodyPart> bodyParts = ImmutableList.builder(); + private Optional<String> subType = Optional.empty(); + + public MultipartBuilder subType(String subType) { + this.subType = Optional.of(subType); + return this; + } public MultipartBuilder addBody(BodyPart bodyPart) { this.bodyParts.add(bodyPart); @@ -106,6 +112,7 @@ public class MimeMessageBuilder { public MimeMultipart build() throws MessagingException { MimeMultipart multipart = new MimeMultipart(); + subType.ifPresent(Throwing.consumer(multipart::setSubType)); List<BodyPart> bodyParts = this.bodyParts.build(); for (BodyPart bodyPart : bodyParts) { multipart.addBodyPart(bodyPart); http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/core/src/test/java/org/apache/james/core/builder/MimeMessageBuilderTest.java ---------------------------------------------------------------------- diff --git a/core/src/test/java/org/apache/james/core/builder/MimeMessageBuilderTest.java b/core/src/test/java/org/apache/james/core/builder/MimeMessageBuilderTest.java index 1cfdba8..3b401a5 100644 --- a/core/src/test/java/org/apache/james/core/builder/MimeMessageBuilderTest.java +++ b/core/src/test/java/org/apache/james/core/builder/MimeMessageBuilderTest.java @@ -61,4 +61,17 @@ public class MimeMessageBuilderTest { .containsExactly(value); } + @Test + public void buildShouldAllowToSpecifyMultipartSubtype() throws Exception { + MimeMessage mimeMessage = MimeMessageBuilder.mimeMessageBuilder() + .setContent(MimeMessageBuilder.multipartBuilder() + .subType("alternative") + .addBody(MimeMessageBuilder.bodyPartBuilder().data("Body 1")) + .addBody(MimeMessageBuilder.bodyPartBuilder().data("Body 2"))) + .build(); + + assertThat(mimeMessage.getContentType()) + .startsWith("multipart/alternative"); + } + } \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-core/pom.xml ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-core/pom.xml b/server/protocols/webadmin/webadmin-core/pom.xml index e86562d..5318d79 100644 --- a/server/protocols/webadmin/webadmin-core/pom.xml +++ b/server/protocols/webadmin/webadmin-core/pom.xml @@ -71,6 +71,10 @@ <artifactId>jackson-datatype-jsr310</artifactId> </dependency> <dependency> + <groupId>com.fasterxml.jackson.datatype</groupId> + <artifactId>jackson-datatype-guava</artifactId> + </dependency> + <dependency> <groupId>com.github.fge</groupId> <artifactId>throwing-lambdas</artifactId> </dependency> http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/JsonTransformer.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/JsonTransformer.java b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/JsonTransformer.java index 907cc3f..e8a22e6 100644 --- a/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/JsonTransformer.java +++ b/server/protocols/webadmin/webadmin-core/src/main/java/org/apache/james/webadmin/utils/JsonTransformer.java @@ -29,6 +29,7 @@ import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.Module; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.guava.GuavaModule; import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.google.common.collect.ImmutableSet; @@ -56,6 +57,7 @@ public class JsonTransformer implements ResponseTransformer { .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS) .registerModule(new Jdk8Module()) .registerModule(new JavaTimeModule()) + .registerModule(new GuavaModule()) .registerModules(modules); } http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-mailrepository/pom.xml ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-mailrepository/pom.xml b/server/protocols/webadmin/webadmin-mailrepository/pom.xml index 20ffd89..25d373f 100644 --- a/server/protocols/webadmin/webadmin-mailrepository/pom.xml +++ b/server/protocols/webadmin/webadmin-mailrepository/pom.xml @@ -86,6 +86,12 @@ <artifactId>jackson-databind</artifactId> </dependency> <dependency> + <groupId>${project.groupId}</groupId> + <artifactId>javax-mail-extension</artifactId> + <type>test-jar</type> + <scope>test</scope> + </dependency> + <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> </dependency> @@ -104,6 +110,11 @@ <scope>test</scope> </dependency> <dependency> + <groupId>net.javacrumbs.json-unit</groupId> + <artifactId>json-unit-fluent</artifactId> + <scope>test</scope> + </dependency> + <dependency> <groupId>org.assertj</groupId> <artifactId>assertj-core</artifactId> <scope>test</scope> http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/HeadersDto.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/HeadersDto.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/HeadersDto.java new file mode 100644 index 0000000..1194ff9 --- /dev/null +++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/HeadersDto.java @@ -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.JsonValue; +import com.google.common.collect.ImmutableListMultimap; + +public class HeadersDto { + private ImmutableListMultimap<String, String> headers; + + public HeadersDto(ImmutableListMultimap<String, String> headers) { + this.headers = headers; + } + + @JsonValue + public ImmutableListMultimap<String, String> getHeaders() { + return headers; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/InaccessibleFieldException.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/InaccessibleFieldException.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/InaccessibleFieldException.java new file mode 100644 index 0000000..d57666b --- /dev/null +++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/InaccessibleFieldException.java @@ -0,0 +1,42 @@ +/**************************************************************** + * 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 org.apache.james.webadmin.dto.MailDto.AdditionalField; + +public class InaccessibleFieldException extends Exception { + + private final AdditionalField field; + private final Exception cause; + + public InaccessibleFieldException(AdditionalField field, Exception cause) { + super(cause); + this.field = field; + this.cause = cause; + } + + public AdditionalField getField() { + return field; + } + + public Exception getCause() { + return cause; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/MailDto.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/MailDto.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/MailDto.java index 4587412..defbcbc 100644 --- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/MailDto.java +++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/dto/MailDto.java @@ -19,24 +19,161 @@ package org.apache.james.webadmin.dto; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; import java.util.List; import java.util.Objects; import java.util.Optional; +import java.util.Set; +import java.util.function.Function; +import javax.mail.Header; import javax.mail.MessagingException; +import javax.mail.internet.MimeMessage; import org.apache.james.core.MailAddress; +import org.apache.james.mime4j.dom.Message; +import org.apache.james.mime4j.stream.MimeConfig; +import org.apache.james.mime4j.util.MimeUtil; +import org.apache.james.util.mime.MessageContentExtractor; +import org.apache.james.util.mime.MessageContentExtractor.MessageContent; +import org.apache.james.util.streams.Iterators; import org.apache.mailet.Mail; +import org.apache.mailet.PerRecipientHeaders; +import com.github.fge.lambdas.Throwing; import com.github.steveash.guavate.Guavate; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Multimap; public class MailDto { - public static MailDto fromMail(Mail mail) throws MessagingException { + public static MailDto fromMail(Mail mail, Set<AdditionalField> additionalFields) throws MessagingException, InaccessibleFieldException { + Optional<MessageContent> messageContent = fetchMessage(additionalFields, mail); return new MailDto(mail.getName(), Optional.ofNullable(mail.getSender()).map(MailAddress::asString), mail.getRecipients().stream().map(MailAddress::asString).collect(Guavate.toImmutableList()), Optional.ofNullable(mail.getErrorMessage()), - Optional.ofNullable(mail.getState())); + Optional.ofNullable(mail.getState()), + Optional.ofNullable(mail.getRemoteHost()), + Optional.ofNullable(mail.getRemoteAddr()), + Optional.ofNullable(mail.getLastUpdated()), + fetchAttributes(additionalFields, mail), + fetchPerRecipientsHeaders(additionalFields, mail), + fetchHeaders(additionalFields, mail), + fetchTextBody(additionalFields, messageContent), + fetchHtmlBody(additionalFields, messageContent), + fetchMessageSize(additionalFields, mail)); + } + + private static Optional<Long> fetchMessageSize(Set<AdditionalField> additionalFields, Mail mail) throws InaccessibleFieldException { + if (!additionalFields.contains(AdditionalField.MESSAGE_SIZE)) { + return Optional.empty(); + } + try { + return Optional.of(mail.getMessageSize()); + } catch (MessagingException e) { + throw new InaccessibleFieldException(AdditionalField.MESSAGE_SIZE, e); + } + } + + private static Optional<String> fetchTextBody(Set<AdditionalField> additionalFields, Optional<MessageContent> messageContent) throws InaccessibleFieldException { + if (!additionalFields.contains(AdditionalField.TEXT_BODY)) { + return Optional.empty(); + } + + return messageContent.flatMap(MessageContent::getTextBody); + } + + private static Optional<String> fetchHtmlBody(Set<AdditionalField> additionalFields, Optional<MessageContent> messageContent) throws InaccessibleFieldException { + if (!additionalFields.contains(AdditionalField.HTML_BODY)) { + return Optional.empty(); + } + + return messageContent.flatMap(MessageContent::getHtmlBody); + } + + private static Optional<MessageContent> fetchMessage(Set<AdditionalField> additionalFields, Mail mail) throws InaccessibleFieldException { + if (!additionalFields.contains(AdditionalField.TEXT_BODY) && !additionalFields.contains(AdditionalField.HTML_BODY)) { + return Optional.empty(); + } + + try { + MessageContentExtractor extractor = new MessageContentExtractor(); + return Optional.ofNullable(mail.getMessage()) + .map(Throwing.function(MailDto::convertMessage).sneakyThrow()) + .map(Throwing.function(extractor::extract).sneakyThrow()); + } catch (MessagingException e) { + if (additionalFields.contains(AdditionalField.TEXT_BODY)) { + throw new InaccessibleFieldException(AdditionalField.TEXT_BODY, e); + } else { + throw new InaccessibleFieldException(AdditionalField.HTML_BODY, e); + } + } + } + + private static Message convertMessage(MimeMessage message) throws IOException, MessagingException { + ByteArrayOutputStream rawMessage = new ByteArrayOutputStream(); + message.writeTo(rawMessage); + return Message.Builder + .of() + .use(MimeConfig.PERMISSIVE) + .parse(new ByteArrayInputStream(rawMessage.toByteArray())) + .build(); + } + + private static Optional<HeadersDto> fetchHeaders(Set<AdditionalField> additionalFields, Mail mail) throws InaccessibleFieldException { + if (!additionalFields.contains(AdditionalField.HEADERS)) { + return Optional.empty(); + } + + try { + return Optional.ofNullable(mail.getMessage()) + .map(Throwing.function(MailDto::extractHeaders).sneakyThrow()); + } catch (MessagingException e) { + throw new InaccessibleFieldException(AdditionalField.HEADERS, e); + } + } + + private static HeadersDto extractHeaders(MimeMessage message) throws MessagingException { + return new HeadersDto(Collections + .list(message.getAllHeaders()) + .stream() + .collect(Guavate.toImmutableListMultimap(Header::getName, (header) -> MimeUtil.unscrambleHeaderValue(header.getValue())))); + } + + private static Optional<ImmutableMap<String, HeadersDto>> fetchPerRecipientsHeaders(Set<AdditionalField> additionalFields, Mail mail) { + if (!additionalFields.contains(AdditionalField.PER_RECIPIENTS_HEADERS)) { + return Optional.empty(); + } + Multimap<MailAddress, PerRecipientHeaders.Header> headersByRecipient = mail + .getPerRecipientSpecificHeaders() + .getHeadersByRecipient(); + + return Optional.of(headersByRecipient + .keySet() + .stream() + .collect(Guavate.toImmutableMap(MailAddress::asString, (address) -> fetchPerRecipientHeader(headersByRecipient, address)))); + } + + private static HeadersDto fetchPerRecipientHeader( + Multimap<MailAddress, PerRecipientHeaders.Header> headersByRecipient, + MailAddress address) { + return new HeadersDto(headersByRecipient.get(address) + .stream() + .collect(Guavate.toImmutableListMultimap(PerRecipientHeaders.Header::getName, PerRecipientHeaders.Header::getValue))); + } + + private static Optional<ImmutableMap<String, String>> fetchAttributes(Set<AdditionalField> additionalFields, Mail mail) { + if (!additionalFields.contains(AdditionalField.ATTRIBUTES)) { + return Optional.empty(); + } + + return Optional.of(Iterators.toStream(mail.getAttributeNames()) + .collect(Guavate.toImmutableMap(Function.identity(), attributeName -> mail.getAttribute(attributeName).toString()))); } private final String name; @@ -44,14 +181,60 @@ public class MailDto { private final List<String> recipients; private final Optional<String> error; private final Optional<String> state; + private final Optional<String> remoteHost; + private final Optional<String> remoteAddr; + private final Optional<Date> lastUpdated; + private final Optional<ImmutableMap<String, String>> attributes; + private final Optional<ImmutableMap<String, HeadersDto>> perRecipientsHeaders; + private final Optional<HeadersDto> headers; + private final Optional<String> textBody; + private final Optional<String> htmlBody; + private final Optional<Long> messageSize; + + public enum AdditionalField { + ATTRIBUTES("attributes"), + PER_RECIPIENTS_HEADERS("perRecipientsHeaders"), + TEXT_BODY("textBody"), + HTML_BODY("htmlBody"), + HEADERS("headers"), + MESSAGE_SIZE("messageSize"); + + public static Optional<AdditionalField> find(String fieldName) { + return Arrays.stream(values()) + .filter(value -> value.fieldName.equalsIgnoreCase(fieldName)) + .findAny(); + } + + private final String fieldName; + + AdditionalField(String fieldName) { + this.fieldName = fieldName; + } + + public String getName() { + return fieldName; + } + } public MailDto(String name, Optional<String> sender, List<String> recipients, Optional<String> error, - Optional<String> state) { + Optional<String> state, Optional<String> remoteHost, Optional<String> remoteAddr, + Optional<Date> lastUpdated, Optional<ImmutableMap<String, String>> attributes, + Optional<ImmutableMap<String, HeadersDto>> perRecipientsHeaders, Optional<HeadersDto> headers, + Optional<String> textBody, Optional<String> htmlBody, Optional<Long> messageSize) { this.name = name; this.sender = sender; this.recipients = recipients; this.error = error; this.state = state; + this.remoteHost = remoteHost; + this.remoteAddr = remoteAddr; + this.lastUpdated = lastUpdated; + this.attributes = attributes; + this.perRecipientsHeaders = perRecipientsHeaders; + this.headers = headers; + this.textBody = textBody; + this.htmlBody = htmlBody; + this.messageSize = messageSize; } public String getName() { @@ -74,6 +257,42 @@ public class MailDto { return state; } + public Optional<String> getRemoteHost() { + return remoteHost; + } + + public Optional<String> getRemoteAddr() { + return remoteAddr; + } + + public Optional<Date> getLastUpdated() { + return lastUpdated; + } + + public Optional<ImmutableMap<String, String>> getAttributes() { + return attributes; + } + + public Optional<ImmutableMap<String, HeadersDto>> getPerRecipientsHeaders() { + return perRecipientsHeaders; + } + + public Optional<HeadersDto> getHeaders() { + return headers; + } + + public Optional<String> getTextBody() { + return textBody; + } + + public Optional<String> getHtmlBody() { + return htmlBody; + } + + public Optional<Long> getMessageSize() { + return messageSize; + } + @Override public final boolean equals(Object o) { if (o instanceof MailDto) { @@ -83,13 +302,22 @@ public class MailDto { && Objects.equals(this.sender, mailDto.sender) && Objects.equals(this.recipients, mailDto.recipients) && Objects.equals(this.error, mailDto.error) - && Objects.equals(this.state, mailDto.state); + && Objects.equals(this.state, mailDto.state) + && Objects.equals(this.remoteHost, mailDto.remoteHost) + && Objects.equals(this.remoteAddr, mailDto.remoteAddr) + && Objects.equals(this.lastUpdated, mailDto.lastUpdated) + && Objects.equals(this.attributes, mailDto.attributes) + && Objects.equals(this.perRecipientsHeaders, mailDto.perRecipientsHeaders) + && Objects.equals(this.headers, mailDto.headers) + && Objects.equals(this.textBody, mailDto.textBody) + && Objects.equals(this.htmlBody, mailDto.htmlBody) + && Objects.equals(this.messageSize, mailDto.messageSize); } return false; } @Override public final int hashCode() { - return Objects.hash(name, sender, recipients, error, state); + return Objects.hash(name, sender, recipients, error, state, remoteHost, remoteAddr, lastUpdated, attributes, perRecipientsHeaders, headers, textBody, htmlBody, messageSize); } } http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java index 29ea380..53d7a2a 100644 --- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java +++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/routes/MailRepositoriesRoutes.java @@ -24,6 +24,7 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.List; import java.util.Optional; +import java.util.Set; import java.util.function.Supplier; import javax.inject.Inject; @@ -48,7 +49,9 @@ 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; @@ -60,6 +63,9 @@ import org.apache.james.webadmin.utils.JsonTransformer; import org.apache.james.webadmin.utils.ParametersExtractor; 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; @@ -215,9 +221,8 @@ public class MailRepositoriesRoutes implements Routes { }) public void defineGetMail() { service.get(MAIL_REPOSITORIES + "/:encodedUrl/mails/:mailKey", Constants.JSON_CONTENT_TYPE, - (request, response) -> getMailAsJson( - decodedRepositoryUrl(request), - new MailKey(request.params("mailKey"))), + (request, response) -> + getMailAsJson(decodedRepositoryUrl(request), new MailKey(request.params("mailKey")), request), jsonTransformer); service.get(MAIL_REPOSITORIES + "/:encodedUrl/mails/:mailKey", Constants.RFC822_CONTENT_TYPE, @@ -250,15 +255,37 @@ public class MailRepositoriesRoutes implements Routes { } } - private MailDto getMailAsJson(MailRepositoryUrl url, MailKey mailKey) { + private MailDto getMailAsJson(MailRepositoryUrl url, MailKey mailKey, Request request) { try { - return repositoryStoreService.retrieveMail(url, mailKey) + return repositoryStoreService.retrieveMail(url, 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) @@ -477,4 +504,16 @@ public class MailRepositoriesRoutes implements Routes { private MailRepositoryUrl decodedRepositoryUrl(Request request) throws UnsupportedEncodingException { return MailRepositoryUrl.fromEncoded(request.params("encodedUrl")); } + + 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()); + } + } http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/MailRepositoryStoreService.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/MailRepositoryStoreService.java b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/MailRepositoryStoreService.java index d8e0e95..6c7ed64 100644 --- a/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/MailRepositoryStoreService.java +++ b/server/protocols/webadmin/webadmin-mailrepository/src/main/java/org/apache/james/webadmin/service/MailRepositoryStoreService.java @@ -21,6 +21,7 @@ package org.apache.james.webadmin.service; import java.util.List; import java.util.Optional; +import java.util.Set; import javax.inject.Inject; import javax.mail.MessagingException; @@ -34,7 +35,9 @@ import org.apache.james.task.Task; import org.apache.james.util.streams.Iterators; import org.apache.james.util.streams.Limit; import org.apache.james.util.streams.Offset; +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.MailKeyDTO; import org.apache.james.webadmin.dto.MailRepositoryResponse; import org.apache.james.webadmin.utils.ErrorResponder; @@ -82,11 +85,11 @@ public class MailRepositoryStoreService { return mailRepository.map(Throwing.function(MailRepository::size).sneakyThrow()); } - public Optional<MailDto> retrieveMail(MailRepositoryUrl url, MailKey mailKey) throws MailRepositoryStore.MailRepositoryStoreException, MessagingException { + public Optional<MailDto> retrieveMail(MailRepositoryUrl url, MailKey mailKey, Set<AdditionalField> additionalAttributes) throws MailRepositoryStore.MailRepositoryStoreException, MessagingException, InaccessibleFieldException { MailRepository mailRepository = getRepository(url); return Optional.ofNullable(mailRepository.retrieve(mailKey)) - .map(Throwing.function(MailDto::fromMail).sneakyThrow()); + .map(Throwing.function((Mail mail) -> MailDto.fromMail(mail, additionalAttributes)).sneakyThrow()); } public Optional<MimeMessage> retrieveMessage(MailRepositoryUrl url, MailKey mailKey) throws MailRepositoryStore.MailRepositoryStoreException, MessagingException { http://git-wip-us.apache.org/repos/asf/james-project/blob/c94d35e2/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/routes/MailRepositoriesRoutesTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/routes/MailRepositoriesRoutesTest.java b/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/routes/MailRepositoriesRoutesTest.java index 2fdcea3..6ea3072 100644 --- a/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/routes/MailRepositoriesRoutesTest.java +++ b/server/protocols/webadmin/webadmin-mailrepository/src/test/java/org/apache/james/webadmin/routes/MailRepositoriesRoutesTest.java @@ -22,6 +22,9 @@ package org.apache.james.webadmin.routes; import static com.jayway.restassured.RestAssured.given; import static com.jayway.restassured.RestAssured.when; import static com.jayway.restassured.RestAssured.with; +import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER; +import static net.javacrumbs.jsonunit.core.Option.IGNORING_EXTRA_FIELDS; +import static net.javacrumbs.jsonunit.fluent.JsonFluentAssert.assertThatJson; import static org.apache.james.webadmin.WebAdminServer.NO_CONFIGURATION; import static org.assertj.core.api.Assertions.assertThat; import static org.hamcrest.Matchers.contains; @@ -32,6 +35,7 @@ import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isEmptyOrNullString; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.Matchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -40,10 +44,20 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import java.nio.charset.StandardCharsets; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Arrays; +import java.util.Date; import java.util.List; import java.util.Optional; import java.util.stream.Stream; +import javax.mail.internet.MimeMessage; + +import org.apache.james.core.MailAddress; +import org.apache.james.core.builder.MimeMessageBuilder; +import org.apache.james.core.builder.MimeMessageBuilder.BodyPartBuilder; import org.apache.james.mailrepository.api.MailKey; import org.apache.james.mailrepository.api.MailRepositoryStore; import org.apache.james.mailrepository.api.MailRepositoryUrl; @@ -66,6 +80,7 @@ import org.apache.james.webadmin.service.ReprocessingService; import org.apache.james.webadmin.utils.ErrorResponder; import org.apache.james.webadmin.utils.JsonTransformer; import org.apache.mailet.Mail; +import org.apache.mailet.PerRecipientHeaders.Header; import org.apache.mailet.base.test.FakeMail; import org.eclipse.jetty.http.HttpStatus; import org.junit.After; @@ -467,19 +482,24 @@ public class MailRepositoriesRoutesTest { @Test public void retrievingAMailShouldDisplayItsInformation() throws Exception { when(mailRepositoryStore.get(URL_MY_REPO)).thenReturn(Optional.of(mailRepository)); - String name = NAME_1; String sender = "sender@domain"; String recipient1 = "recipient1@domain"; String recipient2 = "recipient2@domain"; String state = "state"; String errorMessage = "Error: why this mail is stored"; + String remoteHost = "smtp.domain"; + String remoteAddr = "66.66.66.66"; + Date lastUpdated = new Date(07060504030201L); mailRepository.store(FakeMail.builder() .name(name) .sender(sender) .recipients(recipient1, recipient2) .state(state) .errorMessage(errorMessage) + .remoteHost(remoteHost) + .remoteAddr(remoteAddr) + .lastUpdated(lastUpdated) .build()); when() @@ -488,9 +508,229 @@ public class MailRepositoriesRoutesTest { .statusCode(HttpStatus.OK_200) .body("name", is(name)) .body("sender", is(sender)) + .body("recipients", containsInAnyOrder(recipient1, recipient2)) .body("state", is(state)) .body("error", is(errorMessage)) - .body("recipients", containsInAnyOrder(recipient1, recipient2)); + .body("remoteHost", is(remoteHost)) + .body("remoteAddr", is(remoteAddr)) + .body("lastUpdated", is(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ") + .format(ZonedDateTime.ofInstant(lastUpdated.toInstant(), ZoneId.of("UTC"))))); + } + + @Test + public void retrievingAMailShouldDisplayAllAdditionalFieldsWhenRequested() throws Exception { + when(mailRepositoryStore.get(URL_MY_REPO)).thenReturn(Optional.of(mailRepository)); + String name = NAME_1; + + BodyPartBuilder textMessage = MimeMessageBuilder.bodyPartBuilder() + .addHeader("Content-type", "text/plain") + .data("My awesome body!!"); + BodyPartBuilder htmlMessage = MimeMessageBuilder.bodyPartBuilder() + .addHeader("Content-type", "text/html") + .data("My awesome <em>body</em>!!"); + MimeMessage mimeMessage = MimeMessageBuilder.mimeMessageBuilder() + .addHeader("headerName3", "value5") + .addHeader("headerName3", "value8") + .addHeader("headerName4", "value6") + .addHeader("headerName4", "value7") + .setContent(MimeMessageBuilder.multipartBuilder() + .subType("alternative") + .addBody(textMessage) + .addBody(htmlMessage)) + .build(); + + MailAddress recipientHeaderAddress = new MailAddress("third@party"); + FakeMail mail = FakeMail.builder() + .name(name) + .attribute("name1", "value1") + .attribute("name2", "value2") + .mimeMessage(mimeMessage) + .size(42424242) + .addHeaderForRecipient(Header.builder() + .name("headerName1") + .value("value1") + .build(), recipientHeaderAddress) + .addHeaderForRecipient(Header.builder() + .name("headerName1") + .value("value2") + .build(), recipientHeaderAddress) + .addHeaderForRecipient(Header.builder() + .name("headerName2") + .value("value3") + .build(), recipientHeaderAddress) + .addHeaderForRecipient(Header.builder() + .name("headerName2") + .value("value4") + .build(), recipientHeaderAddress) + .build(); + + mailRepository.store(mail); + + String jsonAsString = + given() + .parameters("additionalFields", "attributes,headers,textBody,htmlBody,messageSize,perRecipientsHeaders") + .when() + .get(URL_ESCAPED_MY_REPO + "/mails/" + name) + .then() + .extract() + .body() + .asString(); + + assertThatJson(jsonAsString) + .when(IGNORING_ARRAY_ORDER) + .when(IGNORING_EXTRA_FIELDS) + .isEqualTo("{" + + " \"name\": \"name1\"," + + " \"sender\": null," + + " \"recipients\": []," + + " \"error\": null," + + " \"state\": null," + + " \"remoteHost\": \"111.222.333.444\"," + + " \"remoteAddr\": \"127.0.0.1\"," + + " \"lastUpdated\": null," + + " \"attributes\": {" + + " \"name2\": \"value2\"," + + " \"name1\": \"value1\"" + + " }," + + " \"perRecipientsHeaders\": {" + + " \"third@party\": {" + + " \"headerName1\": [" + + " \"value1\"," + + " \"value2\"" + + " ]," + + " \"headerName2\": [" + + " \"value3\"," + + " \"value4\"" + + " ]" + + " }" + + " }," + + " \"headers\": {" + + " \"headerName4\": [" + + " \"value6\"," + + " \"value7\"" + + " ]," + + " \"headerName3\": [" + + " \"value5\"," + + " \"value8\"" + + " ]" + + " }," + + " \"textBody\": \"My awesome body!!\"," + + " \"htmlBody\": \"My awesome <em>body</em>!!\"," + + " \"messageSize\": 42424242" + + "}"); + } + + @Test + public void retrievingAMailShouldDisplayAllValidAdditionalFieldsWhenRequested() throws Exception { + when(mailRepositoryStore.get(URL_MY_REPO)).thenReturn(Optional.of(mailRepository)); + String name = NAME_1; + String sender = "sender@domain"; + String recipient1 = "recipient1@domain"; + int messageSize = 42424242; + mailRepository.store(FakeMail.builder() + .name(name) + .sender(sender) + .recipients(recipient1) + .size(messageSize) + .build()); + + given() + .parameters("additionalFields", ",,,messageSize") + .when() + .get(URL_ESCAPED_MY_REPO + "/mails/" + name) + .then() + .statusCode(HttpStatus.OK_200) + .body("name", is(name)) + .body("sender", is(sender)) + .body("headers", nullValue()) + .body("textBody", nullValue()) + .body("htmlBody", nullValue()) + .body("messageSize", is(messageSize)) + .body("attributes", nullValue()) + .body("perRecipientsHeaders", nullValue()); + } + + @Test + public void retrievingAMailShouldDisplayCorrectlyEncodedHeadersInValidAdditionalFieldsWhenRequested() throws Exception { + when(mailRepositoryStore.get(URL_MY_REPO)).thenReturn(Optional.of(mailRepository)); + String name = NAME_1; + String sender = "sender@domain"; + String recipient1 = "recipient1@domain"; + MimeMessage mimeMessage = MimeMessageBuilder.mimeMessageBuilder() + .addHeader("friend", "=?UTF-8?B?RnLDqWTDqXJpYyBNQVJUSU4=?= <[email protected]>") + .build(); + + mailRepository.store(FakeMail.builder() + .name(name) + .sender(sender) + .recipients(recipient1) + .mimeMessage(mimeMessage) + .build()); + + given() + .parameters("additionalFields", "headers") + .when() + .get(URL_ESCAPED_MY_REPO + "/mails/" + name) + .then() + .statusCode(HttpStatus.OK_200) + .body("name", is(name)) + .body("sender", is(sender)) + .body("headers.friend", is(Arrays.asList("Frédéric MARTIN <[email protected]>"))); + } + + @Test + public void retrievingAMailShouldDisplayAllValidAdditionalFieldsEvenTheDuplicatedOnesWhenRequested() throws Exception { + when(mailRepositoryStore.get(URL_MY_REPO)).thenReturn(Optional.of(mailRepository)); + String name = NAME_1; + String sender = "sender@domain"; + String recipient1 = "recipient1@domain"; + int messageSize = 42424242; + mailRepository.store(FakeMail.builder() + .name(name) + .sender(sender) + .recipients(recipient1) + .size(messageSize) + .build()); + + given() + .parameters("additionalFields", "messageSize,messageSize") + .when() + .get(URL_ESCAPED_MY_REPO + "/mails/" + name) + .then() + .statusCode(HttpStatus.OK_200) + .body("name", is(name)) + .body("sender", is(sender)) + .body("headers", nullValue()) + .body("textBody", nullValue()) + .body("htmlBody", nullValue()) + .body("messageSize", is(messageSize)) + .body("attributes", nullValue()) + .body("perRecipientsHeaders", nullValue()); + } + + @Test + public void retrievingAMailShouldFailWhenAnUnknownFieldIsRequested() throws Exception { + when(mailRepositoryStore.get(URL_MY_REPO)).thenReturn(Optional.of(mailRepository)); + String name = NAME_1; + String sender = "sender@domain"; + String recipient1 = "recipient1@domain"; + int messageSize = 42424242; + mailRepository.store(FakeMail.builder() + .name(name) + .sender(sender) + .recipients(recipient1) + .size(messageSize) + .build()); + + given() + .parameters("additionalFields", "nonExistingField") + .when() + .get(URL_ESCAPED_MY_REPO + "/mails/" + name) + .then() + .statusCode(HttpStatus.BAD_REQUEST_400) + .body("statusCode", is(400)) + .body("type", is(ErrorResponder.ErrorType.INVALID_ARGUMENT.getType())) + .body("message", is("The field 'nonExistingField' can't be requested in additionalFields parameter")); } @Test --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
