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 bt...@apache.org on 2018/03/13 08:12:54 UTC

[13/13] james-project git commit: JAMES-2346 sendMDN should send a notification back to the original sender

JAMES-2346 sendMDN should send a notification back to the original sender


Project: http://git-wip-us.apache.org/repos/asf/james-project/repo
Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/9481435b
Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/9481435b
Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/9481435b

Branch: refs/heads/master
Commit: 9481435b89ec32e3220202e4e2198e1bbb8b3b73
Parents: 1be875b
Author: benwa <bt...@linagora.com>
Authored: Thu Mar 8 16:20:15 2018 +0700
Committer: benwa <bt...@linagora.com>
Committed: Tue Mar 13 15:11:54 2018 +0700

----------------------------------------------------------------------
 .../java/org/apache/james/mdn/MDNReport.java    |   5 +
 .../methods/integration/SendMDNMethodTest.java  | 259 +++++++++++++++++--
 .../jmap/methods/MIMEMessageConverter.java      |   5 +-
 .../james/jmap/methods/MessageAppender.java     |  27 ++
 .../james/jmap/methods/SendMDNProcessor.java    | 179 ++++++++++++-
 .../apache/james/jmap/methods/ValueWithId.java  |   6 +-
 .../org/apache/james/jmap/model/Envelope.java   |  31 +++
 .../org/apache/james/jmap/model/JmapMDN.java    | 148 +++++++++++
 .../java/org/apache/james/jmap/model/MDN.java   | 148 -----------
 .../james/jmap/model/SetMessagesRequest.java    |  21 +-
 .../james/jmap/model/SetMessagesResponse.java   |  46 +++-
 .../apache/james/jmap/model/JmapMDNTest.java    | 125 +++++++++
 .../org/apache/james/jmap/model/MDNTest.java    | 125 ---------
 .../jmap/model/SetMessagesResponseTest.java     |   6 +-
 14 files changed, 794 insertions(+), 337 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/mdn/src/main/java/org/apache/james/mdn/MDNReport.java
----------------------------------------------------------------------
diff --git a/mdn/src/main/java/org/apache/james/mdn/MDNReport.java b/mdn/src/main/java/org/apache/james/mdn/MDNReport.java
index 2878bfa..8785737 100644
--- a/mdn/src/main/java/org/apache/james/mdn/MDNReport.java
+++ b/mdn/src/main/java/org/apache/james/mdn/MDNReport.java
@@ -57,6 +57,11 @@ public class MDNReport {
         private ImmutableList.Builder<Error> errorField = ImmutableList.builder();
         private ImmutableList.Builder<ExtensionField> extensionFields = ImmutableList.builder();
 
+        public Builder reportingUserAgentField(String userAgentName) {
+            this.reportingUserAgentField = Optional.of(new ReportingUserAgent(userAgentName, Optional.empty()));
+            return this;
+        }
+
         public Builder reportingUserAgentField(String userAgentName, String userAgentProduct) {
             this.reportingUserAgentField = Optional.of(new ReportingUserAgent(userAgentName, Optional.ofNullable(userAgentProduct)));
             return this;

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java
index 059a7bb..d7690c7 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SendMDNMethodTest.java
@@ -20,26 +20,29 @@
 package org.apache.james.jmap.methods.integration;
 
 import static com.jayway.restassured.RestAssured.given;
+import static com.jayway.restassured.RestAssured.with;
 import static com.jayway.restassured.config.EncoderConfig.encoderConfig;
 import static com.jayway.restassured.config.RestAssuredConfig.newConfig;
+import static org.hamcrest.Matchers.contains;
 import static org.hamcrest.Matchers.containsString;
 import static org.hamcrest.Matchers.equalTo;
 import static org.hamcrest.Matchers.hasEntry;
 import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.notNullValue;
+import static org.hamcrest.Matchers.startsWith;
 
 import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
 
 import org.apache.http.client.utils.URIBuilder;
 import org.apache.james.GuiceJamesServer;
 import org.apache.james.jmap.HttpJmapAuthentication;
 import org.apache.james.jmap.api.access.AccessToken;
 import org.apache.james.mailbox.DefaultMailboxes;
-import org.apache.james.mailbox.model.MailboxConstants;
+import org.apache.james.mailbox.Role;
 import org.apache.james.mailbox.model.MessageId;
 import org.apache.james.mailbox.store.probe.MailboxProbe;
-import org.apache.james.mdn.action.mode.DispositionActionMode;
-import org.apache.james.mdn.sending.mode.DispositionSendingMode;
-import org.apache.james.mdn.type.DispositionType;
 import org.apache.james.modules.MailboxProbeImpl;
 import org.apache.james.probe.DataProbe;
 import org.apache.james.utils.DataProbeImpl;
@@ -62,7 +65,6 @@ public abstract class SendMDNMethodTest {
     private static final String PASSWORD = "password";
     private static final String BOB_PASSWORD = "bobPassword";
 
-
     protected abstract GuiceJamesServer createJmapServer();
 
     protected abstract MessageId randomMessageId();
@@ -70,6 +72,7 @@ public abstract class SendMDNMethodTest {
     protected abstract void await();
 
     private AccessToken accessToken;
+    private AccessToken bobAccessToken;
     private GuiceJamesServer jmapServer;
 
     @Before
@@ -85,7 +88,6 @@ public abstract class SendMDNMethodTest {
                 .setConfig(newConfig().encoderConfig(encoderConfig().defaultContentCharset(StandardCharsets.UTF_8)))
                 .setPort(jmapServer.getProbe(JmapGuiceProbe.class).getJmapPort())
                 .build();
-        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
         RestAssured.defaultParser = Parser.JSON;
 
         dataProbe.addDomain(USERS_DOMAIN);
@@ -93,11 +95,39 @@ public abstract class SendMDNMethodTest {
         dataProbe.addUser(BOB, BOB_PASSWORD);
         mailboxProbe.createMailbox("#private", USERNAME, DefaultMailboxes.INBOX);
         accessToken = HttpJmapAuthentication.authenticateJamesUser(baseUri(), USERNAME, PASSWORD);
+        bobAccessToken = HttpJmapAuthentication.authenticateJamesUser(baseUri(), BOB, BOB_PASSWORD);
+        await();
+    }
+
+    public void sendAnInitialMessage() {
+        String messageCreationId = "creationId";
+        String outboxId = getOutboxId(bobAccessToken);
+        String requestBody = "[" +
+            "  [" +
+            "    \"setMessages\"," +
+            "    {" +
+            "      \"create\": { \"" + messageCreationId  + "\" : {" +
+            "        \"from\": { \"name\": \"Bob\", \"email\": \"" + BOB + "\"}," +
+            "        \"to\": [{ \"name\": \"User\", \"email\": \"" + USERNAME + "\"}]," +
+            "        \"subject\": \"Message with an attachment\"," +
+            "        \"textBody\": \"Test body, plain text version\"," +
+            "        \"htmlBody\": \"Test <b>body</b>, HTML version\"," +
+            "        \"mailboxIds\": [\"" + outboxId + "\"] " +
+            "      }}" +
+            "    }," +
+            "    \"#0\"" +
+            "  ]" +
+            "]";
+
+        with()
+            .header("Authorization", bobAccessToken.serialize())
+            .body(requestBody)
+            .post("/jmap")
+        .then()
+            .extract()
+            .body()
+            .path(ARGUMENTS + ".created." + messageCreationId + ".id");
 
-        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, DefaultMailboxes.OUTBOX);
-        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, DefaultMailboxes.TRASH);
-        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, DefaultMailboxes.DRAFTS);
-        mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, DefaultMailboxes.SENT);
         await();
     }
 
@@ -116,20 +146,54 @@ public abstract class SendMDNMethodTest {
     }
 
     @Test
-    public void sendMDNIsNotSupportedYet() {
+    public void sendMDNShouldReturnCreatedMessageId() {
+        sendAnInitialMessage();
+
+        List<String> messageIds = getMessageIdListForAccount(accessToken.serialize());
+
         String creationId = "creation-1";
         given()
             .header("Authorization", accessToken.serialize())
             .body("[[\"setMessages\", {\"sendMDN\": {" +
                 "\"" + creationId + "\":{" +
-                    "    \"messageId\":\"" + randomMessageId().serialize() + "\"," +
+                    "    \"messageId\":\"" + messageIds.get(0) + "\"," +
+                    "    \"subject\":\"subject\"," +
+                    "    \"textBody\":\"textBody\"," +
+                    "    \"reportingUA\":\"reportingUA\"," +
+                    "    \"disposition\":{" +
+                    "        \"actionMode\":\"automatic-action\","+
+                    "        \"sendingMode\":\"MDN-sent-automatically\","+
+                    "        \"type\":\"processed\""+
+                    "    }" +
+                    "}" +
+                "}}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".MDNSent." + creationId, notNullValue());
+    }
+
+    @Test
+    public void sendMDNShouldFailOnUnknownMessageId() {
+        sendAnInitialMessage();
+
+        String creationId = "creation-1";
+        String randomMessageId = randomMessageId().serialize();
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"setMessages\", {\"sendMDN\": {" +
+                "\"" + creationId + "\":{" +
+                    "    \"messageId\":\"" + randomMessageId + "\"," +
                     "    \"subject\":\"subject\"," +
                     "    \"textBody\":\"textBody\"," +
                     "    \"reportingUA\":\"reportingUA\"," +
                     "    \"disposition\":{" +
-                    "        \"actionMode\":\"" + DispositionActionMode.Automatic.getValue() + "\","+
-                    "        \"sendingMode\":\"" + DispositionSendingMode.Automatic.getValue() + "\","+
-                    "        \"type\":\"" + DispositionType.Processed.getValue() + "\""+
+                    "        \"actionMode\":\"automatic-action\","+
+                    "        \"sendingMode\":\"MDN-sent-automatically\","+
+                    "        \"type\":\"processed\""+
                     "    }" +
                     "}" +
                 "}}, \"#0\"]]")
@@ -141,7 +205,107 @@ public abstract class SendMDNMethodTest {
             .body(NAME, equalTo("messagesSet"))
             .body(ARGUMENTS + ".MDNNotSent", hasEntry(
                 equalTo(creationId),
-                hasEntry("type", "Not implemented yet")));
+                hasEntry("type", "invalidArgument")))
+            .body(ARGUMENTS + ".MDNNotSent", hasEntry(
+                equalTo(creationId),
+                hasEntry("description", "Message with id " + randomMessageId + " not found. Thus could not send MDN.")));
+    }
+
+    @Test
+    public void sendMDNShouldSendAMDNBackToTheOriginalMessageAuthor() {
+        sendAnInitialMessage();
+
+        List<String> messageIds = getMessageIdListForAccount(accessToken.serialize());
+
+        // USER sends a MDN back to BOB
+        String creationId = "creation-1";
+        with()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"setMessages\", {\"sendMDN\": {" +
+                "\"" + creationId + "\":{" +
+                "    \"messageId\":\"" + messageIds.get(0) + "\"," +
+                "    \"subject\":\"subject\"," +
+                "    \"textBody\":\"Read confirmation\"," +
+                "    \"reportingUA\":\"reportingUA\"," +
+                "    \"disposition\":{" +
+                "        \"actionMode\":\"automatic-action\","+
+                "        \"sendingMode\":\"MDN-sent-automatically\","+
+                "        \"type\":\"processed\""+
+                "    }" +
+                "}" +
+                "}}, \"#0\"]]")
+            .post("/jmap");
+
+        await();
+
+        // BOB should have received it
+        List<String> bobInboxMessageIds = listMessagesInMailbox(bobAccessToken, getInboxId(bobAccessToken));
+
+        given()
+            .header("Authorization", bobAccessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + bobInboxMessageIds.get(0) + "\"]}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(ARGUMENTS + ".list[0].from.email", is(USERNAME))
+            .body(ARGUMENTS + ".list[0].to.email", contains(BOB))
+            .body(ARGUMENTS + ".list[0].hasAttachment", is(true))
+            .body(ARGUMENTS + ".list[0].textBody", is("Read confirmation"))
+            .body(ARGUMENTS + ".list[0].subject", is("subject"))
+            .body(ARGUMENTS + ".list[0].headers.Content-Type", startsWith("multipart/report;"))
+            .body(ARGUMENTS + ".list[0].attachments[0].type", startsWith("message/disposition-notification"));
+    }
+
+    @Test
+    public void sendMDNShouldPositionTheReportAsAnAttachment() {
+        sendAnInitialMessage();
+
+        List<String> messageIds = getMessageIdListForAccount(accessToken.serialize());
+
+        // USER sends a MDN back to BOB
+        String creationId = "creation-1";
+        with()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"setMessages\", {\"sendMDN\": {" +
+                "\"" + creationId + "\":{" +
+                "    \"messageId\":\"" + messageIds.get(0) + "\"," +
+                "    \"subject\":\"subject\"," +
+                "    \"textBody\":\"Read confirmation\"," +
+                "    \"reportingUA\":\"reportingUA\"," +
+                "    \"disposition\":{" +
+                "        \"actionMode\":\"automatic-action\","+
+                "        \"sendingMode\":\"MDN-sent-automatically\","+
+                "        \"type\":\"processed\""+
+                "    }" +
+                "}" +
+                "}}, \"#0\"]]")
+            .post("/jmap");
+
+        await();
+
+        // BOB should have received it
+        List<String> bobInboxMessageIds = listMessagesInMailbox(bobAccessToken, getInboxId(bobAccessToken));
+
+        String blobId = with()
+            .header("Authorization", bobAccessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + bobInboxMessageIds.get(0) + "\"]}, \"#0\"]]")
+            .post("/jmap")
+        .then()
+            .extract()
+            .body()
+            .path(ARGUMENTS + ".list[0].attachments[0].blobId");
+
+        given()
+            .header("Authorization", bobAccessToken.serialize())
+        .when()
+            .get("/download/" + blobId)
+        .then()
+            .statusCode(200)
+            .body(containsString("Reporting-UA: reportingUA;"))
+            .body(containsString("Final-Recipient: rfc822; username@domain.tld"))
+            .body(containsString("Original-Message-ID: "))
+            .body(containsString("Disposition: automatic-action/MDN-sent-automatically;processed"));
     }
 
     @Test
@@ -156,9 +320,9 @@ public abstract class SendMDNMethodTest {
                     "    \"textBody\":\"textBody\"," +
                     "    \"reportingUA\":\"reportingUA\"," +
                     "    \"disposition\":{" +
-                    "        \"actionMode\":\"" + DispositionActionMode.Automatic.getValue() + "\","+
-                    "        \"sendingMode\":\"" + DispositionSendingMode.Automatic.getValue() + "\","+
-                    "        \"type\":\"" + DispositionType.Processed.getValue() + "\""+
+                    "        \"actionMode\":\"automatic-action\","+
+                    "        \"sendingMode\":\"MDN-sent-automatically\","+
+                    "        \"type\":\"processed\""+
                     "    }" +
                     "}" +
                 "}}, \"#0\"]]")
@@ -185,8 +349,8 @@ public abstract class SendMDNMethodTest {
                 "    \"textBody\":\"textBody\"," +
                 "    \"reportingUA\":\"reportingUA\"," +
                 "    \"disposition\":{" +
-                "        \"sendingMode\":\"" + DispositionSendingMode.Automatic.getValue() + "\","+
-                "        \"type\":\"" + DispositionType.Processed.getValue() + "\""+
+                "        \"sendingMode\":\"MDN-sent-automatically\","+
+                "        \"type\":\"processed\""+
                 "    }" +
                 "}" +
                 "}}, \"#0\"]]")
@@ -203,7 +367,6 @@ public abstract class SendMDNMethodTest {
     @Test
     public void invalidEnumValuesInMDNShouldBeReported() {
         String creationId = "creation-1";
-        // Missing actionMode
         given()
             .header("Authorization", accessToken.serialize())
             .body("[[\"setMessages\", {\"sendMDN\": {" +
@@ -214,8 +377,8 @@ public abstract class SendMDNMethodTest {
                 "    \"reportingUA\":\"reportingUA\"," +
                 "    \"disposition\":{" +
                 "        \"actionMode\":\"invalid\","+
-                "        \"sendingMode\":\"" + DispositionSendingMode.Automatic.getValue() + "\","+
-                "        \"type\":\"" + DispositionType.Processed.getValue() + "\""+
+                "        \"sendingMode\":\"MDN-sent-automatically\","+
+                "        \"type\":\"processed\""+
                 "    }" +
                 "}" +
                 "}}, \"#0\"]]")
@@ -229,4 +392,52 @@ public abstract class SendMDNMethodTest {
             .body(ARGUMENTS + ".description", containsString("Unrecognized MDN Disposition action mode invalid. Should be one of [manual-action, automatic-action]"));
     }
 
+    private String getInboxId(AccessToken accessToken) {
+        return getMailboxId(accessToken, Role.INBOX);
+    }
+
+    private String getOutboxId(AccessToken accessToken) {
+        return getMailboxId(accessToken, Role.OUTBOX);
+    }
+
+    private String getMailboxId(AccessToken accessToken, Role role) {
+        return getAllMailboxesIds(accessToken).stream()
+            .filter(x -> x.get("role").equalsIgnoreCase(role.serialize()))
+            .map(x -> x.get("id"))
+            .findFirst().get();
+    }
+
+    private List<Map<String, String>> getAllMailboxesIds(AccessToken accessToken) {
+        return with()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMailboxes\", {\"properties\": [\"role\", \"id\"]}, \"#0\"]]")
+            .post("/jmap")
+        .andReturn()
+            .body()
+            .jsonPath()
+            .getList(ARGUMENTS + ".list");
+    }
+
+    public List<String> getMessageIdListForAccount(String accessToken) {
+        return with()
+            .header("Authorization", accessToken)
+            .body("[[\"getMessageList\", {}, \"#0\"]]")
+            .post("/jmap")
+        .then()
+            .extract()
+            .body()
+            .path(ARGUMENTS + ".messageIds");
+    }
+
+    public List<String> listMessagesInMailbox(AccessToken accessToken, String mailboxId) {
+        return with()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + mailboxId + "\"]}}, \"#0\"]]")
+            .post("/jmap")
+        .then()
+            .extract()
+            .body()
+            .path(ARGUMENTS + ".messageIds");
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java
index 49688b1..a8b1cbf 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MIMEMessageConverter.java
@@ -107,11 +107,14 @@ public class MIMEMessageConverter {
     }
 
     public byte[] convert(ValueWithId.CreationMessageEntry creationMessageEntry, ImmutableList<MessageAttachment> messageAttachments) {
+        return asBytes(convertToMime(creationMessageEntry, messageAttachments));
+    }
 
+    public byte[] asBytes(Message message) {
         ByteArrayOutputStream buffer = new ByteArrayOutputStream();
         DefaultMessageWriter writer = new DefaultMessageWriter();
         try {
-            writer.writeMessage(convertToMime(creationMessageEntry, messageAttachments), buffer);
+            writer.writeMessage(message, buffer);
         } catch (IOException e) {
             throw Throwables.propagate(e);
         }

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java
index c74f59d..409bd91 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java
@@ -30,6 +30,7 @@ import javax.mail.util.SharedByteArrayInputStream;
 import org.apache.james.jmap.methods.ValueWithId.CreationMessageEntry;
 import org.apache.james.jmap.model.Attachment;
 import org.apache.james.jmap.model.CreationMessage;
+import org.apache.james.jmap.model.Keywords;
 import org.apache.james.jmap.model.MessageFactory;
 import org.apache.james.mailbox.AttachmentManager;
 import org.apache.james.mailbox.MailboxManager;
@@ -97,6 +98,32 @@ public class MessageAppender {
             .build();
     }
 
+    public MessageFactory.MetaDataWithContent appendMessageInMailbox(org.apache.james.mime4j.dom.Message message,
+                                                                     MessageManager messageManager,
+                                                                     List<MessageAttachment> attachments,
+                                                                     Flags flags,
+                                                                     MailboxSession session) throws MailboxException {
+
+
+        byte[] messageContent = mimeMessageConverter.asBytes(message);
+        SharedByteArrayInputStream content = new SharedByteArrayInputStream(messageContent);
+        Date internalDate = new Date();
+        boolean notRecent = false;
+
+        ComposedMessageId appendedMessage = messageManager.appendMessage(content, internalDate, session, notRecent, flags);
+
+        return MessageFactory.MetaDataWithContent.builder()
+            .uid(appendedMessage.getUid())
+            .keywords(Keywords.factory().fromFlags(flags))
+            .internalDate(internalDate.toInstant())
+            .sharedContent(content)
+            .size(messageContent.length)
+            .attachments(attachments)
+            .mailboxId(messageManager.getId())
+            .messageId(appendedMessage.getMessageId())
+            .build();
+    }
+
     public MessageFactory.MetaDataWithContent appendMessageInMailbox(CreationMessageEntry createdEntry,
                                                                        MailboxId targetMailbox,
                                                                        MailboxSession session) throws MailboxException {

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java
index c0dfa27..5f13d44 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SendMDNProcessor.java
@@ -21,38 +21,195 @@ package org.apache.james.jmap.methods;
 
 import static org.apache.james.jmap.methods.Method.JMAP_PREFIX;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
 import javax.inject.Inject;
+import javax.mail.Flags;
+import javax.mail.MessagingException;
 
+import org.apache.james.core.User;
+import org.apache.james.jmap.exceptions.MessageNotFoundException;
+import org.apache.james.jmap.model.Envelope;
+import org.apache.james.jmap.model.JmapMDN;
+import org.apache.james.jmap.model.MDNDisposition;
+import org.apache.james.jmap.model.MessageFactory;
 import org.apache.james.jmap.model.SetError;
 import org.apache.james.jmap.model.SetMessagesRequest;
 import org.apache.james.jmap.model.SetMessagesResponse;
+import org.apache.james.jmap.utils.SystemMailboxesProvider;
 import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageIdManager;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.Role;
+import org.apache.james.mailbox.exception.MailboxException;
+import org.apache.james.mailbox.model.Attachment;
+import org.apache.james.mailbox.model.FetchGroupImpl;
+import org.apache.james.mailbox.model.MessageAttachment;
+import org.apache.james.mailbox.model.MessageId;
+import org.apache.james.mailbox.model.MessageResult;
+import org.apache.james.mdn.MDN;
+import org.apache.james.mdn.MDNReport;
+import org.apache.james.mdn.fields.Disposition;
 import org.apache.james.metrics.api.MetricFactory;
+import org.apache.james.mime4j.codec.DecodeMonitor;
+import org.apache.james.mime4j.dom.Message;
+import org.apache.james.mime4j.dom.field.ParseException;
+import org.apache.james.mime4j.message.DefaultMessageBuilder;
+import org.apache.james.mime4j.stream.MimeConfig;
+import org.apache.james.mime4j.util.MimeUtil;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.google.common.collect.ImmutableList;
 
 public class SendMDNProcessor implements SetMessagesProcessor {
+
+    private static final Logger LOGGER = LoggerFactory.getLogger(SendMDNProcessor.class);
+    private static final MimeConfig MIME_ENTITY_CONFIG = MimeConfig.custom()
+        .setMaxContentLen(-1)
+        .setMaxHeaderCount(-1)
+        .setMaxHeaderLen(-1)
+        .setMaxLineLen(-1)
+        .build();
+
     private final MetricFactory metricFactory;
+    private final SystemMailboxesProvider systemMailboxesProvider;
+    private final MessageIdManager messageIdManager;
+    private final MessageAppender messageAppender;
+    private final MessageSender messageSender;
 
     @Inject
-    public SendMDNProcessor(MetricFactory metricFactory) {
+    public SendMDNProcessor(MetricFactory metricFactory, SystemMailboxesProvider systemMailboxesProvider,
+                            MessageIdManager messageIdManager, MessageAppender messageAppender, MessageSender messageSender) {
         this.metricFactory = metricFactory;
+        this.systemMailboxesProvider = systemMailboxesProvider;
+        this.messageIdManager = messageIdManager;
+        this.messageAppender = messageAppender;
+        this.messageSender = messageSender;
     }
 
     @Override
     public SetMessagesResponse process(SetMessagesRequest request, MailboxSession mailboxSession) {
         return metricFactory.withMetric(JMAP_PREFIX + "SendMDN",
-            () -> handleMDNCreation(request));
+            () -> handleMDNCreation(request, mailboxSession));
+    }
+
+    private SetMessagesResponse handleMDNCreation(SetMessagesRequest request, MailboxSession mailboxSession) {
+        return request.getSendMDN()
+            .stream()
+            .map(MDNCreationEntry -> handleMDNCreation(MDNCreationEntry, mailboxSession))
+            .reduce(SetMessagesResponse.builder(), SetMessagesResponse.Builder::mergeWith)
+            .build();
+    }
+
+    private SetMessagesResponse.Builder handleMDNCreation(ValueWithId.MDNCreationEntry MDNCreationEntry, MailboxSession mailboxSession) {
+        try {
+            MessageId messageId = sendMdn(MDNCreationEntry, mailboxSession);
+            return SetMessagesResponse.builder()
+                .mdnSent(MDNCreationEntry.getCreationId(), messageId);
+        } catch (MessageNotFoundException e) {
+            return SetMessagesResponse.builder()
+                .mdnNotSent(MDNCreationEntry.getCreationId(),
+                    SetError.builder()
+                        .description(String.format("Message with id %s not found. Thus could not send MDN.",
+                            MDNCreationEntry.getValue().getMessageId().serialize()))
+                        .type("invalidArgument")
+                        .build());
+
+        } catch (Exception e) {
+            LOGGER.error("Error while sending MDN", e);
+            return SetMessagesResponse.builder()
+                .mdnNotSent(MDNCreationEntry.getCreationId(),
+                    SetError.builder()
+                        .description(String.format("Could not send MDN %s", MDNCreationEntry.getCreationId().getId()))
+                        .type("error")
+                        .build());
+        }
     }
 
-    public SetMessagesResponse handleMDNCreation(SetMessagesRequest request) {
-        SetMessagesResponse.Builder builder = SetMessagesResponse.builder();
+    private MessageId sendMdn(ValueWithId.MDNCreationEntry MDNCreationEntry, MailboxSession mailboxSession) throws MailboxException, IOException, MessagingException, ParseException, MessageNotFoundException {
+        JmapMDN mdn = MDNCreationEntry.getValue();
+        Message originalMessage = retrieveOriginalMessage(mdn, mailboxSession);
+        MDNReport mdnReport = generateReport(mdn, originalMessage, mailboxSession);
+        List<MessageAttachment> reportAsAttachment = ImmutableList.of(convertReportToAttachment(mdnReport));
+        User user = User.fromUsername(mailboxSession.getUser().getUserName());
+
+        Message mdnAnswer = generateMDNMessage(originalMessage, mdn, mdnReport, user);
+
+        Flags seen = new Flags(Flags.Flag.SEEN);
+        MessageFactory.MetaDataWithContent metaDataWithContent = messageAppender.appendMessageInMailbox(mdnAnswer,
+            getOutbox(mailboxSession), reportAsAttachment, seen, mailboxSession);
 
-        request.getSendMDN()
-            .forEach(creationMDNEntry -> builder.MDNNotSent(creationMDNEntry.getCreationId(),
-                SetError.builder()
-                    .description(String.format("Could not send MDN %s", creationMDNEntry.getCreationId().getId()))
-                    .type("Not implemented yet")
-                    .build()));
+        messageSender.sendMessage(metaDataWithContent,
+            Envelope.fromMime4JMessage(mdnAnswer), mailboxSession);
 
-        return builder.build();
+        return metaDataWithContent.getMessageId();
     }
+
+    private Message generateMDNMessage(Message originalMessage, JmapMDN mdn, MDNReport mdnReport, User user) throws ParseException, IOException {
+        return MDN.builder()
+            .report(mdnReport)
+            .humanReadableText(mdn.getTextBody())
+            .build()
+        .asMime4JMessageBuilder()
+            .setTo(originalMessage.getSender().getAddress())
+            .setFrom(user.asString())
+            .setSubject(mdn.getSubject())
+            .setMessageId(MimeUtil.createUniqueMessageId(user.getDomainPart().orElse(null)))
+            .build();
+    }
+
+    private Message retrieveOriginalMessage(JmapMDN mdn, MailboxSession mailboxSession) throws MailboxException, IOException, MessageNotFoundException {
+        List<MessageResult> messages = messageIdManager.getMessages(ImmutableList.of(mdn.getMessageId()),
+            FetchGroupImpl.HEADERS,
+            mailboxSession);
+
+        if (messages.size() == 0) {
+            throw new MessageNotFoundException();
+        }
+
+        DefaultMessageBuilder messageBuilder = new DefaultMessageBuilder();
+        messageBuilder.setMimeEntityConfig(MIME_ENTITY_CONFIG);
+        messageBuilder.setDecodeMonitor(DecodeMonitor.SILENT);
+        return messageBuilder.parseMessage(messages.get(0).getHeaders().getInputStream());
+    }
+
+    private MessageAttachment convertReportToAttachment(MDNReport mdnReport) {
+        Attachment attachment = Attachment.builder()
+            .bytes(mdnReport.formattedValue().getBytes(StandardCharsets.UTF_8))
+            .type(MDN.DISPOSITION_CONTENT_TYPE)
+            .build();
+
+        return MessageAttachment.builder()
+            .attachment(attachment)
+            .isInline(true)
+            .build();
+    }
+
+    private MDNReport generateReport(JmapMDN mdn, Message originalMessage, MailboxSession mailboxSession) {
+        return MDNReport.builder()
+                .dispositionField(generateDisposition(mdn.getDisposition()))
+                .originalRecipientField(mailboxSession.getUser().getUserName())
+                .originalMessageIdField(originalMessage.getMessageId())
+                .finalRecipientField(mailboxSession.getUser().getUserName())
+                .reportingUserAgentField(mdn.getReportingUA())
+                .build();
+    }
+
+    private Disposition generateDisposition(MDNDisposition disposition) {
+        return Disposition.builder()
+            .actionMode(disposition.getActionMode())
+            .sendingMode(disposition.getSendingMode())
+            .type(disposition.getType())
+            .build();
+    }
+
+    private MessageManager getOutbox(MailboxSession mailboxSession) throws MailboxException {
+        return systemMailboxesProvider.getMailboxByRole(Role.OUTBOX, mailboxSession)
+            .findAny()
+            .orElseThrow(() -> new IllegalStateException("User don't have an Outbox"));
+    }
+
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java
index e8ce60c..6304951 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/ValueWithId.java
@@ -21,7 +21,7 @@ package org.apache.james.jmap.methods;
 
 import org.apache.james.jmap.model.CreationMessage;
 import org.apache.james.jmap.model.CreationMessageId;
-import org.apache.james.jmap.model.MDN;
+import org.apache.james.jmap.model.JmapMDN;
 import org.apache.james.jmap.model.Message;
 import org.apache.james.jmap.model.SetError;
 
@@ -51,8 +51,8 @@ public class ValueWithId<T> {
         }
     }
 
-    public static class CreationMDNEntry extends ValueWithId<MDN> {
-        public CreationMDNEntry(CreationMessageId creationId, MDN mdn) {
+    public static class MDNCreationEntry extends ValueWithId<JmapMDN> {
+        public MDNCreationEntry(CreationMessageId creationId, JmapMDN mdn) {
             super(creationId, mdn);
         }
     }

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java
index c4d80f9..28f61c1 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/Envelope.java
@@ -21,12 +21,17 @@ package org.apache.james.jmap.model;
 
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Stream;
 
 import org.apache.james.core.MailAddress;
+import org.apache.james.mime4j.dom.address.AddressList;
+import org.apache.james.mime4j.dom.address.Mailbox;
+import org.apache.james.mime4j.dom.address.MailboxList;
 import org.apache.james.util.StreamUtils;
 
+import com.github.fge.lambdas.Throwing;
 import com.github.steveash.guavate.Guavate;
 import com.google.common.base.Preconditions;
 
@@ -46,11 +51,37 @@ public class Envelope {
                 .collect(Guavate.toImmutableSet()));
     }
 
+    public static Envelope fromMime4JMessage(org.apache.james.mime4j.dom.Message mime4JMessage) {
+        MailAddress sender = mime4JMessage.getFrom()
+            .stream()
+            .findAny()
+            .map(Mailbox::getAddress)
+            .map(Throwing.function(MailAddress::new))
+            .orElseThrow(() -> new RuntimeException("Sender is mandatory"));
+
+        Stream<MailAddress> to = emailersToMailAddresses(mime4JMessage.getTo());
+        Stream<MailAddress> cc = emailersToMailAddresses(mime4JMessage.getCc());
+        Stream<MailAddress> bcc = emailersToMailAddresses(mime4JMessage.getBcc());
+
+        return new Envelope(sender,
+            StreamUtils.flatten(Stream.of(to, cc, bcc))
+                .collect(Guavate.toImmutableSet()));
+    }
+
     private static Stream<MailAddress> emailersToMailAddresses(List<Emailer> emailers) {
         return emailers.stream()
             .map(Emailer::toMailAddress);
     }
 
+    private static Stream<MailAddress> emailersToMailAddresses(AddressList addresses) {
+        return Optional.ofNullable(addresses)
+            .map(AddressList::flatten)
+            .map(MailboxList::stream)
+            .orElse(Stream.of())
+            .map(Mailbox::getAddress)
+            .map(Throwing.function(MailAddress::new));
+    }
+
 
     private final MailAddress from;
     private final Set<MailAddress> recipients;

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/JmapMDN.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/JmapMDN.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/JmapMDN.java
new file mode 100644
index 0000000..0e1df56
--- /dev/null
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/JmapMDN.java
@@ -0,0 +1,148 @@
+/****************************************************************
+ * 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.jmap.model;
+
+import java.util.Objects;
+
+import org.apache.james.mailbox.model.MessageId;
+
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.MoreObjects;
+import com.google.common.base.Preconditions;
+
+@JsonDeserialize(builder = JmapMDN.Builder.class)
+public class JmapMDN {
+
+    public static Builder builder() {
+        return new Builder();
+    }
+
+    @JsonPOJOBuilder(withPrefix = "")
+    public static class Builder {
+        private MessageId messageId;
+        private String subject;
+        private String textBody;
+        private String reportingUA;
+        private MDNDisposition disposition;
+
+        public Builder messageId(MessageId messageId) {
+            this.messageId = messageId;
+            return this;
+        }
+
+        public Builder subject(String subject) {
+            this.subject = subject;
+            return this;
+        }
+
+        public Builder textBody(String textBody) {
+            this.textBody = textBody;
+            return this;
+        }
+
+        public Builder reportingUA(String reportingUA) {
+            this.reportingUA = reportingUA;
+            return this;
+        }
+
+        public Builder disposition(MDNDisposition disposition) {
+            this.disposition = disposition;
+            return this;
+        }
+
+        public JmapMDN build() {
+            Preconditions.checkState(messageId != null, "'messageId' is mandatory");
+            Preconditions.checkState(subject != null, "'subject' is mandatory");
+            Preconditions.checkState(textBody != null, "'textBody' is mandatory");
+            Preconditions.checkState(reportingUA != null, "'reportingUA' is mandatory");
+            Preconditions.checkState(disposition != null, "'disposition' is mandatory");
+
+            return new JmapMDN(messageId, subject, textBody, reportingUA, disposition);
+        }
+
+    }
+
+    private final MessageId messageId;
+    private final String subject;
+    private final String textBody;
+    private final String reportingUA;
+    private final MDNDisposition disposition;
+
+    @VisibleForTesting
+    JmapMDN(MessageId messageId, String subject, String textBody, String reportingUA, MDNDisposition disposition) {
+        this.messageId = messageId;
+        this.subject = subject;
+        this.textBody = textBody;
+        this.reportingUA = reportingUA;
+        this.disposition = disposition;
+    }
+
+    public MessageId getMessageId() {
+        return messageId;
+    }
+
+    public String getSubject() {
+        return subject;
+    }
+
+    public String getTextBody() {
+        return textBody;
+    }
+
+    public String getReportingUA() {
+        return reportingUA;
+    }
+
+    public MDNDisposition getDisposition() {
+        return disposition;
+    }
+
+    @Override
+    public final boolean equals(Object o) {
+        if (o instanceof JmapMDN) {
+            JmapMDN that = (JmapMDN) o;
+
+            return Objects.equals(this.messageId, that.messageId)
+                && Objects.equals(this.subject, that.subject)
+                && Objects.equals(this.textBody, that.textBody)
+                && Objects.equals(this.reportingUA, that.reportingUA)
+                && Objects.equals(this.disposition, that.disposition);
+        }
+        return false;
+    }
+
+    @Override
+    public final int hashCode() {
+        return Objects.hash(messageId, subject, textBody, reportingUA, disposition);
+    }
+
+    @Override
+    public String toString() {
+        return MoreObjects.toStringHelper(this)
+            .add("messageId", messageId)
+            .add("subject", subject)
+            .add("textBody", textBody)
+            .add("reportingUA", reportingUA)
+            .add("mdnDisposition", disposition)
+            .toString();
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MDN.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MDN.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MDN.java
deleted file mode 100644
index cf54031..0000000
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/MDN.java
+++ /dev/null
@@ -1,148 +0,0 @@
-/****************************************************************
- * 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.jmap.model;
-
-import java.util.Objects;
-
-import org.apache.james.mailbox.model.MessageId;
-
-import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
-import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder;
-import com.google.common.annotations.VisibleForTesting;
-import com.google.common.base.MoreObjects;
-import com.google.common.base.Preconditions;
-
-@JsonDeserialize(builder = MDN.Builder.class)
-public class MDN {
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    @JsonPOJOBuilder(withPrefix = "")
-    public static class Builder {
-        private MessageId messageId;
-        private String subject;
-        private String textBody;
-        private String reportingUA;
-        private MDNDisposition disposition;
-
-        public Builder messageId(MessageId messageId) {
-            this.messageId = messageId;
-            return this;
-        }
-
-        public Builder subject(String subject) {
-            this.subject = subject;
-            return this;
-        }
-
-        public Builder textBody(String textBody) {
-            this.textBody = textBody;
-            return this;
-        }
-
-        public Builder reportingUA(String reportingUA) {
-            this.reportingUA = reportingUA;
-            return this;
-        }
-
-        public Builder disposition(MDNDisposition disposition) {
-            this.disposition = disposition;
-            return this;
-        }
-
-        public MDN build() {
-            Preconditions.checkState(messageId != null, "'messageId' is mandatory");
-            Preconditions.checkState(subject != null, "'subject' is mandatory");
-            Preconditions.checkState(textBody != null, "'textBody' is mandatory");
-            Preconditions.checkState(reportingUA != null, "'reportingUA' is mandatory");
-            Preconditions.checkState(disposition != null, "'disposition' is mandatory");
-
-            return new MDN(messageId, subject, textBody, reportingUA, disposition);
-        }
-
-    }
-
-    private final MessageId messageId;
-    private final String subject;
-    private final String textBody;
-    private final String reportingUA;
-    private final MDNDisposition disposition;
-
-    @VisibleForTesting
-    MDN(MessageId messageId, String subject, String textBody, String reportingUA, MDNDisposition disposition) {
-        this.messageId = messageId;
-        this.subject = subject;
-        this.textBody = textBody;
-        this.reportingUA = reportingUA;
-        this.disposition = disposition;
-    }
-
-    public MessageId getMessageId() {
-        return messageId;
-    }
-
-    public String getSubject() {
-        return subject;
-    }
-
-    public String getTextBody() {
-        return textBody;
-    }
-
-    public String getReportingUA() {
-        return reportingUA;
-    }
-
-    public MDNDisposition getDisposition() {
-        return disposition;
-    }
-
-    @Override
-    public final boolean equals(Object o) {
-        if (o instanceof MDN) {
-            MDN that = (MDN) o;
-
-            return Objects.equals(this.messageId, that.messageId)
-                && Objects.equals(this.subject, that.subject)
-                && Objects.equals(this.textBody, that.textBody)
-                && Objects.equals(this.reportingUA, that.reportingUA)
-                && Objects.equals(this.disposition, that.disposition);
-        }
-        return false;
-    }
-
-    @Override
-    public final int hashCode() {
-        return Objects.hash(messageId, subject, textBody, reportingUA, disposition);
-    }
-
-    @Override
-    public String toString() {
-        return MoreObjects.toStringHelper(this)
-            .add("messageId", messageId)
-            .add("subject", subject)
-            .add("textBody", textBody)
-            .add("reportingUA", reportingUA)
-            .add("mdnDisposition", disposition)
-            .toString();
-    }
-}

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
index 2f3cd68..ec6b1a8 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java
@@ -28,8 +28,8 @@ import java.util.function.Function;
 import org.apache.commons.lang.NotImplementedException;
 import org.apache.james.jmap.methods.JmapRequest;
 import org.apache.james.jmap.methods.UpdateMessagePatchConverter;
-import org.apache.james.jmap.methods.ValueWithId.CreationMDNEntry;
 import org.apache.james.jmap.methods.ValueWithId.CreationMessageEntry;
+import org.apache.james.jmap.methods.ValueWithId.MDNCreationEntry;
 import org.apache.james.mailbox.model.MessageId;
 
 import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
@@ -54,7 +54,7 @@ public class SetMessagesRequest implements JmapRequest {
         private String accountId;
         private String ifInState;
         private HashMap<CreationMessageId, CreationMessage> create;
-        private HashMap<CreationMessageId, MDN> sendMDN;
+        private HashMap<CreationMessageId, JmapMDN> sendMDN;
         private ImmutableMap.Builder<MessageId, Function<UpdateMessagePatchConverter, UpdateMessagePatch>> updatesProvider;
 
         private ImmutableList.Builder<MessageId> destroy;
@@ -90,12 +90,12 @@ public class SetMessagesRequest implements JmapRequest {
             return this;
         }
 
-        public Builder sendMDN(CreationMessageId creationMessageId, MDN mdn) {
+        public Builder sendMDN(CreationMessageId creationMessageId, JmapMDN mdn) {
             this.sendMDN.put(creationMessageId, mdn);
             return this;
         }
 
-        public Builder sendMDN(Map<CreationMessageId, MDN> mdns) {
+        public Builder sendMDN(Map<CreationMessageId, JmapMDN> mdns) {
             this.sendMDN.putAll(mdns);
             return this;
         }
@@ -121,9 +121,9 @@ public class SetMessagesRequest implements JmapRequest {
                     .collect(Guavate.toImmutableList());
         }
 
-        private ImmutableList<CreationMDNEntry> mdnSendings() {
+        private ImmutableList<MDNCreationEntry> mdnSendings() {
             return sendMDN.entrySet().stream()
-                    .map(entry -> new CreationMDNEntry(entry.getKey(), entry.getValue()))
+                    .map(entry -> new MDNCreationEntry(entry.getKey(), entry.getValue()))
                     .collect(Guavate.toImmutableList());
         }
     }
@@ -131,13 +131,14 @@ public class SetMessagesRequest implements JmapRequest {
     private final Optional<String> accountId;
     private final Optional<String> ifInState;
     private final List<CreationMessageEntry> create;
-    private final List<CreationMDNEntry> sendMDN;
+    private final List<MDNCreationEntry> sendMDN;
     private final Map<MessageId, Function<UpdateMessagePatchConverter, UpdateMessagePatch>> update;
     private final List<MessageId> destroy;
 
     @VisibleForTesting SetMessagesRequest(Optional<String> accountId, Optional<String> ifInState,
-                    List<CreationMessageEntry> create, List<CreationMDNEntry> sendMDN, Map<MessageId,
-                    Function<UpdateMessagePatchConverter, UpdateMessagePatch>> update, List<MessageId> destroy) {
+                                          List<CreationMessageEntry> create, List<MDNCreationEntry> sendMDN,
+                                          Map<MessageId, Function<UpdateMessagePatchConverter, UpdateMessagePatch>> update,
+                                          List<MessageId> destroy) {
         this.accountId = accountId;
         this.ifInState = ifInState;
         this.create = create;
@@ -158,7 +159,7 @@ public class SetMessagesRequest implements JmapRequest {
         return create;
     }
 
-    public List<CreationMDNEntry> getSendMDN() {
+    public List<MDNCreationEntry> getSendMDN() {
         return sendMDN;
     }
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
index 5700df0..c8e20e6 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesResponse.java
@@ -51,19 +51,21 @@ public class SetMessagesResponse implements Method.Response {
         private String oldState;
         private String newState;
         private final ImmutableMap.Builder<CreationMessageId, Message> created;
+        private final ImmutableMap.Builder<CreationMessageId, MessageId> mdnSent;
         private final ImmutableList.Builder<MessageId> updated;
         private final ImmutableList.Builder<MessageId> destroyed;
         private final ImmutableMap.Builder<CreationMessageId, SetError> notCreated;
-        private final ImmutableMap.Builder<CreationMessageId, SetError> MDNNotSent;
+        private final ImmutableMap.Builder<CreationMessageId, SetError> mdnNotSent;
         private final ImmutableMap.Builder<MessageId, SetError> notUpdated;
         private final ImmutableMap.Builder<MessageId, SetError> notDestroyed;
 
         private Builder() {
             created = ImmutableMap.builder();
+            mdnSent = ImmutableMap.builder();
             updated = ImmutableList.builder();
             destroyed = ImmutableList.builder();
             notCreated = ImmutableMap.builder();
-            MDNNotSent = ImmutableMap.builder();
+            mdnNotSent = ImmutableMap.builder();
             notUpdated = ImmutableMap.builder();
             notDestroyed = ImmutableMap.builder();
         }
@@ -90,6 +92,16 @@ public class SetMessagesResponse implements Method.Response {
             return this;
         }
 
+        public Builder mdnSent(CreationMessageId creationMessageId, MessageId messageId) {
+            this.mdnSent.put(creationMessageId, messageId);
+            return this;
+        }
+
+        public Builder mdnSent(ImmutableMap<CreationMessageId, MessageId> sent) {
+            this.mdnSent.putAll(sent);
+            return this;
+        }
+
         public Builder updated(List<MessageId> updated) {
             this.updated.addAll(updated);
             return this;
@@ -110,13 +122,13 @@ public class SetMessagesResponse implements Method.Response {
             return this;
         }
 
-        public Builder MDNNotSent(Map<CreationMessageId, SetError> notCreated) {
-            this.MDNNotSent.putAll(notCreated);
+        public Builder mdnNotSent(Map<CreationMessageId, SetError> notCreated) {
+            this.mdnNotSent.putAll(notCreated);
             return this;
         }
 
-        public Builder MDNNotSent(CreationMessageId creationMessageId, SetError error) {
-            this.MDNNotSent.put(creationMessageId, error);
+        public Builder mdnNotSent(CreationMessageId creationMessageId, SetError error) {
+            this.mdnNotSent.put(creationMessageId, error);
             return this;
         }
         
@@ -146,8 +158,8 @@ public class SetMessagesResponse implements Method.Response {
 
         public SetMessagesResponse build() {
             return new SetMessagesResponse(accountId, oldState, newState, 
-                created.build(), updated.build(), destroyed.build(),
-                notCreated.build(), MDNNotSent.build(), notUpdated.build(), notDestroyed.build());
+                created.build(), mdnSent.build(), updated.build(), destroyed.build(),
+                notCreated.build(), mdnNotSent.build(), notUpdated.build(), notDestroyed.build());
         }
     }
 
@@ -155,23 +167,25 @@ public class SetMessagesResponse implements Method.Response {
     private final String oldState;
     private final String newState;
     private final ImmutableMap<CreationMessageId, Message> created;
+    private final ImmutableMap<CreationMessageId, MessageId> mdnSent;
     private final ImmutableList<MessageId> updated;
     private final ImmutableList<MessageId> destroyed;
     private final ImmutableMap<CreationMessageId, SetError> notCreated;
-    private final ImmutableMap<CreationMessageId, SetError> MDNNotSent;
+    private final ImmutableMap<CreationMessageId, SetError> mdnNotSent;
     private final ImmutableMap<MessageId, SetError> notUpdated;
     private final ImmutableMap<MessageId, SetError> notDestroyed;
 
-    @VisibleForTesting SetMessagesResponse(String accountId, String oldState, String newState, ImmutableMap<CreationMessageId, Message> created, ImmutableList<MessageId> updated, ImmutableList<MessageId> destroyed,
+    @VisibleForTesting SetMessagesResponse(String accountId, String oldState, String newState, ImmutableMap<CreationMessageId, Message> created, ImmutableMap<CreationMessageId, MessageId> mdnSent, ImmutableList<MessageId> updated, ImmutableList<MessageId> destroyed,
                                            ImmutableMap<CreationMessageId, SetError> notCreated, ImmutableMap<CreationMessageId, SetError> mdnNotSent, ImmutableMap<MessageId, SetError> notUpdated, ImmutableMap<MessageId, SetError> notDestroyed) {
         this.accountId = accountId;
         this.oldState = oldState;
         this.newState = newState;
         this.created = created;
+        this.mdnSent = mdnSent;
         this.updated = updated;
         this.destroyed = destroyed;
         this.notCreated = notCreated;
-        this.MDNNotSent = mdnNotSent;
+        this.mdnNotSent = mdnNotSent;
         this.notUpdated = notUpdated;
         this.notDestroyed = notDestroyed;
     }
@@ -212,9 +226,14 @@ public class SetMessagesResponse implements Method.Response {
         return notDestroyed;
     }
 
+    @JsonProperty("MDNSent")
+    public ImmutableMap<CreationMessageId, MessageId> getMDNSent() {
+        return mdnSent;
+    }
+
     @JsonProperty("MDNNotSent")
     public ImmutableMap<CreationMessageId, SetError> getMDNNotSent() {
-        return MDNNotSent;
+        return mdnNotSent;
     }
 
     public SetMessagesResponse.Builder mergeInto(SetMessagesResponse.Builder responseBuilder) {
@@ -224,7 +243,8 @@ public class SetMessagesResponse implements Method.Response {
         responseBuilder.notCreated(getNotCreated());
         responseBuilder.notUpdated(getNotUpdated());
         responseBuilder.notDestroyed(getNotDestroyed());
-        responseBuilder.MDNNotSent(getMDNNotSent());
+        responseBuilder.mdnNotSent(getMDNNotSent());
+        responseBuilder.mdnSent(getMDNSent());
         if (! Strings.isNullOrEmpty(getAccountId())) {
             responseBuilder.accountId(getAccountId());
         }

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/JmapMDNTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/JmapMDNTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/JmapMDNTest.java
new file mode 100644
index 0000000..5cb045d
--- /dev/null
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/JmapMDNTest.java
@@ -0,0 +1,125 @@
+/****************************************************************
+ * 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.jmap.model;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+import org.apache.james.mailbox.model.TestMessageId;
+import org.apache.james.mdn.action.mode.DispositionActionMode;
+import org.apache.james.mdn.sending.mode.DispositionSendingMode;
+import org.apache.james.mdn.type.DispositionType;
+import org.junit.Test;
+
+import nl.jqno.equalsverifier.EqualsVerifier;
+
+public class JmapMDNTest {
+
+    public static final String TEXT_BODY = "text body";
+    public static final String SUBJECT = "subject";
+    public static final String REPORTING_UA = "reportingUA";
+    public static final MDNDisposition DISPOSITION = MDNDisposition.builder()
+        .actionMode(DispositionActionMode.Automatic)
+        .sendingMode(DispositionSendingMode.Automatic)
+        .type(DispositionType.Processed)
+        .build();
+    public static final TestMessageId MESSAGE_ID = TestMessageId.of(45);
+
+    @Test
+    public void shouldMatchBeanContract() {
+        EqualsVerifier.forClass(JmapMDN.class)
+            .allFieldsShouldBeUsed()
+            .verify();
+    }
+
+    @Test
+    public void builderShouldReturnObjectWhenAllFieldsAreValid() {
+        assertThat(
+            JmapMDN.builder()
+                .disposition(DISPOSITION)
+                .messageId(MESSAGE_ID)
+                .reportingUA(REPORTING_UA)
+                .subject(SUBJECT)
+                .textBody(TEXT_BODY)
+                .build())
+            .isEqualTo(new JmapMDN(MESSAGE_ID, SUBJECT, TEXT_BODY, REPORTING_UA, DISPOSITION));
+    }
+
+    @Test
+    public void dispositionIsCompulsory() {
+        assertThatThrownBy(() ->
+            JmapMDN.builder()
+                .messageId(MESSAGE_ID)
+                .reportingUA(REPORTING_UA)
+                .subject(SUBJECT)
+                .textBody(TEXT_BODY)
+                .build())
+            .isInstanceOf(IllegalStateException.class);
+    }
+
+    @Test
+    public void messageIdIsCompulsory() {
+        assertThatThrownBy(() ->
+            JmapMDN.builder()
+                .disposition(DISPOSITION)
+                .reportingUA(REPORTING_UA)
+                .subject(SUBJECT)
+                .textBody(TEXT_BODY)
+                .build())
+            .isInstanceOf(IllegalStateException.class);
+    }
+
+    @Test
+    public void reportingUAIsCompulsory() {
+        assertThatThrownBy(() ->
+            JmapMDN.builder()
+                .disposition(DISPOSITION)
+                .messageId(MESSAGE_ID)
+                .subject(SUBJECT)
+                .textBody(TEXT_BODY)
+                .build())
+            .isInstanceOf(IllegalStateException.class);
+    }
+
+    @Test
+    public void subjectIsCompulsory() {
+        assertThatThrownBy(() ->
+            JmapMDN.builder()
+                .disposition(DISPOSITION)
+                .messageId(MESSAGE_ID)
+                .reportingUA(REPORTING_UA)
+                .textBody(TEXT_BODY)
+                .build())
+            .isInstanceOf(IllegalStateException.class);
+    }
+
+    @Test
+    public void textBodyIsCompulsory() {
+        assertThatThrownBy(() ->
+            JmapMDN.builder()
+                .disposition(DISPOSITION)
+                .messageId(MESSAGE_ID)
+                .reportingUA(REPORTING_UA)
+                .subject(SUBJECT)
+                .build())
+            .isInstanceOf(IllegalStateException.class);
+    }
+
+}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MDNTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MDNTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MDNTest.java
deleted file mode 100644
index 1527719..0000000
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/MDNTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-/****************************************************************
- * 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.jmap.model;
-
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
-
-import org.apache.james.mailbox.model.TestMessageId;
-import org.apache.james.mdn.action.mode.DispositionActionMode;
-import org.apache.james.mdn.sending.mode.DispositionSendingMode;
-import org.apache.james.mdn.type.DispositionType;
-import org.junit.Test;
-
-import nl.jqno.equalsverifier.EqualsVerifier;
-
-public class MDNTest {
-
-    public static final String TEXT_BODY = "text body";
-    public static final String SUBJECT = "subject";
-    public static final String REPORTING_UA = "reportingUA";
-    public static final MDNDisposition DISPOSITION = MDNDisposition.builder()
-        .actionMode(DispositionActionMode.Automatic)
-        .sendingMode(DispositionSendingMode.Automatic)
-        .type(DispositionType.Processed)
-        .build();
-    public static final TestMessageId MESSAGE_ID = TestMessageId.of(45);
-
-    @Test
-    public void shouldMatchBeanContract() {
-        EqualsVerifier.forClass(MDN.class)
-            .allFieldsShouldBeUsed()
-            .verify();
-    }
-
-    @Test
-    public void builderShouldReturnObjectWhenAllFieldsAreValid() {
-        assertThat(
-            MDN.builder()
-                .disposition(DISPOSITION)
-                .messageId(MESSAGE_ID)
-                .reportingUA(REPORTING_UA)
-                .subject(SUBJECT)
-                .textBody(TEXT_BODY)
-                .build())
-            .isEqualTo(new MDN(MESSAGE_ID, SUBJECT, TEXT_BODY, REPORTING_UA, DISPOSITION));
-    }
-
-    @Test
-    public void dispositionIsCompulsory() {
-        assertThatThrownBy(() ->
-            MDN.builder()
-                .messageId(MESSAGE_ID)
-                .reportingUA(REPORTING_UA)
-                .subject(SUBJECT)
-                .textBody(TEXT_BODY)
-                .build())
-            .isInstanceOf(IllegalStateException.class);
-    }
-
-    @Test
-    public void messageIdIsCompulsory() {
-        assertThatThrownBy(() ->
-            MDN.builder()
-                .disposition(DISPOSITION)
-                .reportingUA(REPORTING_UA)
-                .subject(SUBJECT)
-                .textBody(TEXT_BODY)
-                .build())
-            .isInstanceOf(IllegalStateException.class);
-    }
-
-    @Test
-    public void reportingUAIsCompulsory() {
-        assertThatThrownBy(() ->
-            MDN.builder()
-                .disposition(DISPOSITION)
-                .messageId(MESSAGE_ID)
-                .subject(SUBJECT)
-                .textBody(TEXT_BODY)
-                .build())
-            .isInstanceOf(IllegalStateException.class);
-    }
-
-    @Test
-    public void subjectIsCompulsory() {
-        assertThatThrownBy(() ->
-            MDN.builder()
-                .disposition(DISPOSITION)
-                .messageId(MESSAGE_ID)
-                .reportingUA(REPORTING_UA)
-                .textBody(TEXT_BODY)
-                .build())
-            .isInstanceOf(IllegalStateException.class);
-    }
-
-    @Test
-    public void textBodyIsCompulsory() {
-        assertThatThrownBy(() ->
-            MDN.builder()
-                .disposition(DISPOSITION)
-                .messageId(MESSAGE_ID)
-                .reportingUA(REPORTING_UA)
-                .subject(SUBJECT)
-                .build())
-            .isInstanceOf(IllegalStateException.class);
-    }
-
-}
\ No newline at end of file

http://git-wip-us.apache.org/repos/asf/james-project/blob/9481435b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
index 61cf8fb..4410adc 100644
--- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
+++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesResponseTest.java
@@ -74,7 +74,8 @@ public class SetMessagesResponseTest {
         ImmutableMap<MessageId, SetError> notUpdated = ImmutableMap.of(TestMessageId.of(4), SetError.builder().type("updated").build());
         ImmutableMap<MessageId, SetError> notDestroyed  = ImmutableMap.of(TestMessageId.of(5), SetError.builder().type("destroyed").build());
         ImmutableMap<CreationMessageId, SetError> mdnNotSent = ImmutableMap.of(CreationMessageId.of("dead-beef-defec9"), SetError.builder().type("MDNNotSent").build());
-        SetMessagesResponse expected = new SetMessagesResponse(null, null, null, created, updated, destroyed, notCreated, mdnNotSent, notUpdated, notDestroyed);
+        ImmutableMap<CreationMessageId, MessageId> mdnSent = ImmutableMap.of(CreationMessageId.of("dead-beef-defed0"), TestMessageId.of(12));
+        SetMessagesResponse expected = new SetMessagesResponse(null, null, null, created, mdnSent, updated, destroyed, notCreated, mdnNotSent, notUpdated, notDestroyed);
 
         SetMessagesResponse setMessagesResponse = SetMessagesResponse.builder()
             .created(created)
@@ -83,7 +84,8 @@ public class SetMessagesResponseTest {
             .notCreated(notCreated)
             .notUpdated(notUpdated)
             .notDestroyed(notDestroyed)
-            .MDNNotSent(mdnNotSent)
+            .mdnNotSent(mdnNotSent)
+            .mdnSent(mdnSent)
             .build();
 
         assertThat(setMessagesResponse).isEqualToComparingFieldByField(expected);


---------------------------------------------------------------------
To unsubscribe, e-mail: server-dev-unsubscribe@james.apache.org
For additional commands, e-mail: server-dev-help@james.apache.org