You are viewing a plain text version of this content. The canonical link for it is here.
Posted to server-dev@james.apache.org by ma...@apache.org on 2018/06/15 14:39:40 UTC

james-project git commit: JAMES-2321 WebAdmin should allow to see more details about Mails

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 <gd...@linagora.com>
Authored: Mon Jun 4 12:20:39 2018 +0200
Committer: Gautier DI FOLCO <gd...@linagora.com>
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=?= <fr...@linagora.com>")
+                .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 <fr...@linagora.com>")));
+    }
+
+    @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: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org