james-server-dev mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From adup...@apache.org
Subject [5/5] james-project git commit: JAMES-1790 check that blobids exist when attaching them
Date Fri, 08 Jul 2016 14:46:40 GMT
JAMES-1790 check that blobids exist when attaching them


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

Branch: refs/heads/master
Commit: 662fa4a954571ef6a0d7044873359211eeaae181
Parents: 7be2166
Author: Matthieu Baechler <matthieu.baechler@linagora.com>
Authored: Tue Jul 5 17:09:13 2016 +0200
Committer: Antoine Duprat <aduprat@linagora.com>
Committed: Fri Jul 8 16:45:00 2016 +0200

----------------------------------------------------------------------
 .../integration/SetMessagesMethodTest.java      | 619 +++++++++++++------
 .../AttachmentsNotFoundException.java           |  40 ++
 .../jmap/methods/MIMEMessageConverter.java      | 127 +++-
 .../methods/SetMessagesCreationProcessor.java   |  90 ++-
 .../james/jmap/model/CreationMessage.java       |  32 +-
 .../org/apache/james/jmap/model/SetError.java   |   9 +-
 .../james/jmap/model/SetMessagesError.java      |  92 +++
 .../jmap/methods/MIMEMessageConverterTest.java  |  73 ++-
 .../SetMessagesCreationProcessorTest.java       |  84 ++-
 9 files changed, 887 insertions(+), 279 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/james-project/blob/662fa4a9/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
index af96309..be3297b 100644
--- a/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
+++ b/server/protocols/jmap-integration-testing/jmap-integration-testing-common/src/test/java/org/apache/james/jmap/methods/integration/SetMessagesMethodTest.java
@@ -33,10 +33,12 @@ import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.hasSize;
 import static org.hamcrest.Matchers.isEmptyOrNullString;
 import static org.hamcrest.Matchers.not;
+import static org.hamcrest.Matchers.nullValue;
 import static org.hamcrest.collection.IsMapWithSize.aMapWithSize;
 import static org.hamcrest.collection.IsMapWithSize.anEmptyMap;
 
 import java.io.ByteArrayInputStream;
+import java.io.IOException;
 import java.time.ZonedDateTime;
 import java.util.Date;
 import java.util.List;
@@ -52,6 +54,7 @@ import org.apache.james.jmap.model.mailbox.Role;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.model.MailboxConstants;
 import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.store.mail.model.Attachment;
 import org.apache.james.mailbox.store.mail.model.Mailbox;
 import org.hamcrest.Matchers;
 import org.junit.After;
@@ -93,12 +96,12 @@ public abstract class SetMessagesMethodTest {
         jmapServer = createJmapServer();
         jmapServer.start();
         RestAssured.requestSpecification = new RequestSpecBuilder()
-        		.setContentType(ContentType.JSON)
-        		.setAccept(ContentType.JSON)
-        		.setConfig(newConfig().encoderConfig(encoderConfig().defaultContentCharset(Charsets.UTF_8)))
-        		.setPort(jmapServer.getJmapPort())
-        		.build();
-        
+                .setContentType(ContentType.JSON)
+                .setAccept(ContentType.JSON)
+                .setConfig(newConfig().encoderConfig(encoderConfig().defaultContentCharset(Charsets.UTF_8)))
+                .setPort(jmapServer.getJmapPort())
+                .build();
+
         username = "username@" + USERS_DOMAIN;
         String password = "password";
         jmapServer.serverProbe().addDomain(USERS_DOMAIN);
@@ -148,6 +151,7 @@ public abstract class SetMessagesMethodTest {
         .when()
             .post("/jmap")
         .then()
+            .log().ifValidationFails()
             .statusCode(200)
             .body(NAME, equalTo("error"))
             .body(ARGUMENTS + ".type", equalTo("Not yet implemented"));
@@ -161,6 +165,7 @@ public abstract class SetMessagesMethodTest {
         .when()
             .post("/jmap")
         .then()
+            .log().ifValidationFails()
             .statusCode(200)
             .body(NAME, equalTo("error"))
             .body(ARGUMENTS + ".type", equalTo("Not yet implemented"));
@@ -215,7 +220,7 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         given()
@@ -224,6 +229,7 @@ public abstract class SetMessagesMethodTest {
         .when()
             .post("/jmap")
         .then()
+            .log().ifValidationFails()
             .statusCode(200)
             .body(NAME, equalTo("messagesSet"))
             .body(ARGUMENTS + ".notDestroyed", anEmptyMap())
@@ -237,7 +243,7 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         // When
@@ -247,6 +253,7 @@ public abstract class SetMessagesMethodTest {
         .when()
             .post("/jmap")
         .then()
+            .log().ifValidationFails()
             .statusCode(200);
 
         // Then
@@ -256,6 +263,7 @@ public abstract class SetMessagesMethodTest {
         .when()
             .post("/jmap")
         .then()
+            .log().ifValidationFails()
             .statusCode(200)
             .body(NAME, equalTo("messages"))
             .body(ARGUMENTS + ".list", empty());
@@ -266,13 +274,13 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test2\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test2\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test3\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test3\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         String missingMessageId = username + "|mailbox|4";
@@ -282,6 +290,7 @@ public abstract class SetMessagesMethodTest {
         .when()
             .post("/jmap")
         .then()
+            .log().ifValidationFails()
             .statusCode(200)
             .body(NAME, equalTo("messagesSet"))
             .body(ARGUMENTS + ".destroyed", hasSize(2))
@@ -299,13 +308,13 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test2\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test2\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test3\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test3\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         // When
@@ -315,6 +324,7 @@ public abstract class SetMessagesMethodTest {
         .when()
             .post("/jmap")
         .then()
+            .log().ifValidationFails()
             .statusCode(200);
 
         // Then
@@ -324,6 +334,7 @@ public abstract class SetMessagesMethodTest {
         .when()
             .post("/jmap")
         .then()
+            .log().ifValidationFails()
             .statusCode(200)
             .body(NAME, equalTo("messages"))
             .body(ARGUMENTS + ".list", hasSize(1));
@@ -335,7 +346,7 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         String presumedMessageId = username + "|mailbox|1";
@@ -348,8 +359,8 @@ public abstract class SetMessagesMethodTest {
             .post("/jmap")
         // Then
         .then()
-            .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId))
-            .log().ifValidationFails();
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId));
     }
 
     private ResponseSpecification getSetMessagesUpdateOKResponseAssertions(String messageId) {
@@ -369,27 +380,27 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         String presumedMessageId = username + "|mailbox|1";
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : false } } }, \"#0\"]]", presumedMessageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : false } } }, \"#0\"]]", presumedMessageId))
         // When
         .when()
-                .post("/jmap");
+            .post("/jmap");
         // Then
         with()
-                .header("Authorization", accessToken.serialize())
-                .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
-                .post("/jmap")
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
+            .post("/jmap")
         .then()
-                .statusCode(200)
-                .body(NAME, equalTo("messages"))
-                .body(ARGUMENTS + ".list", hasSize(1))
-                .body(ARGUMENTS + ".list[0].isUnread", equalTo(false))
-                .log().ifValidationFails();
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].isUnread", equalTo(false));
     }
 
     @Test
@@ -398,20 +409,20 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.SEEN));
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.SEEN));
         await();
 
         String presumedMessageId = username + "|mailbox|1";
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : true } } }, \"#0\"]]", presumedMessageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : true } } }, \"#0\"]]", presumedMessageId))
         // When
         .when()
-                .post("/jmap")
+            .post("/jmap")
         // Then
         .then()
-                .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId))
-                .log().ifValidationFails();
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId));
     }
 
     @Test
@@ -420,26 +431,26 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags(Flags.Flag.SEEN));
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags(Flags.Flag.SEEN));
         await();
 
         String presumedMessageId = username + "|mailbox|1";
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : true } } }, \"#0\"]]", presumedMessageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : true } } }, \"#0\"]]", presumedMessageId))
         // When
         .when()
-                .post("/jmap");
+            .post("/jmap");
         // Then
         with()
-                .header("Authorization", accessToken.serialize())
-                .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
-                .post("/jmap")
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
+            .post("/jmap")
         .then()
-                .body(NAME, equalTo("messages"))
-                .body(ARGUMENTS + ".list", hasSize(1))
-                .body(ARGUMENTS + ".list[0].isUnread", equalTo(true))
-                .log().ifValidationFails();
+            .log().ifValidationFails()
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].isUnread", equalTo(true));
     }
 
 
@@ -449,20 +460,20 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         String presumedMessageId = username + "|mailbox|1";
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isFlagged\" : true } } }, \"#0\"]]", presumedMessageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isFlagged\" : true } } }, \"#0\"]]", presumedMessageId))
         // When
         .when()
-                .post("/jmap")
+            .post("/jmap")
         // Then
         .then()
-                .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId))
-                .log().ifValidationFails();
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId));
     }
 
     @Test
@@ -471,53 +482,53 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         String presumedMessageId = username + "|mailbox|1";
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isFlagged\" : true } } }, \"#0\"]]", presumedMessageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isFlagged\" : true } } }, \"#0\"]]", presumedMessageId))
         // When
         .when()
-                .post("/jmap");
+            .post("/jmap");
         // Then
         with()
-                .header("Authorization", accessToken.serialize())
-                .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
-                .post("/jmap")
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
+            .post("/jmap")
         .then()
-                .body(NAME, equalTo("messages"))
-                .body(ARGUMENTS + ".list", hasSize(1))
-                .body(ARGUMENTS + ".list[0].isFlagged", equalTo(true))
-                .log().ifValidationFails();
+            .log().ifValidationFails()
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].isFlagged", equalTo(true));
     }
 
     @Test
     public void setMessagesShouldRejectUpdateWhenPropertyHasWrongType() throws MailboxException {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
 
         await();
 
         String messageId = username + "|mailbox|1";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : \"123\" } } }, \"#0\"]]", messageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : \"123\" } } }, \"#0\"]]", messageId))
         .when()
-                .post("/jmap")
+            .post("/jmap")
         .then()
-                .log().ifValidationFails()
-                .statusCode(200)
-                .body(NAME, equalTo("messagesSet"))
-                .body(NOT_UPDATED, hasKey(messageId))
-                .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
-                .body(NOT_UPDATED + "[\""+messageId+"\"].properties[0]", equalTo("isUnread"))
-                .body(NOT_UPDATED + "[\""+messageId+"\"].description", equalTo("isUnread: Can not construct instance of java.lang.Boolean from String value '123': only \"true\" or \"false\" recognized\n" +
-                        " at [Source: {\"isUnread\":\"123\"}; line: 1, column: 2] (through reference chain: org.apache.james.jmap.model.Builder[\"isUnread\"])"))
-                .body(ARGUMENTS + ".updated", hasSize(0));
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(NOT_UPDATED, hasKey(messageId))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].properties[0]", equalTo("isUnread"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].description", equalTo("isUnread: Can not construct instance of java.lang.Boolean from String value '123': only \"true\" or \"false\" recognized\n" +
+                    " at [Source: {\"isUnread\":\"123\"}; line: 1, column: 2] (through reference chain: org.apache.james.jmap.model.Builder[\"isUnread\"])"))
+            .body(ARGUMENTS + ".updated", hasSize(0));
     }
 
     @Test
@@ -525,27 +536,27 @@ public abstract class SetMessagesMethodTest {
     public void setMessagesShouldRejectUpdateWhenPropertiesHaveWrongTypes() throws MailboxException {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
 
         await();
 
         String messageId = username + "|mailbox|1";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : \"123\", \"isFlagged\" : 456 } } }, \"#0\"]]", messageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : \"123\", \"isFlagged\" : 456 } } }, \"#0\"]]", messageId))
         .when()
-                .post("/jmap")
+            .post("/jmap")
         .then()
-                .log().ifValidationFails()
-                .statusCode(200)
-                .body(NAME, equalTo("messagesSet"))
-                .body(NOT_UPDATED, hasKey(messageId))
-                .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
-                .body(NOT_UPDATED + "[\""+messageId+"\"].properties", hasSize(2))
-                .body(NOT_UPDATED + "[\""+messageId+"\"].properties[0]", equalTo("isUnread"))
-                .body(NOT_UPDATED + "[\""+messageId+"\"].properties[1]", equalTo("isFlagged"))
-                .body(ARGUMENTS + ".updated", hasSize(0));
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(NOT_UPDATED, hasKey(messageId))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].type", equalTo("invalidProperties"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].properties", hasSize(2))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].properties[0]", equalTo("isUnread"))
+            .body(NOT_UPDATED + "[\""+messageId+"\"].properties[1]", equalTo("isFlagged"))
+            .body(ARGUMENTS + ".updated", hasSize(0));
     }
 
     @Test
@@ -554,20 +565,20 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         String presumedMessageId = username + "|mailbox|1";
         // When
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isAnswered\" : true } } }, \"#0\"]]", presumedMessageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isAnswered\" : true } } }, \"#0\"]]", presumedMessageId))
         .when()
-                .post("/jmap")
+            .post("/jmap")
         // Then
         .then()
-                .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId))
-                .log().ifValidationFails();
+            .log().ifValidationFails()
+            .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId));
     }
 
     @Test
@@ -576,26 +587,26 @@ public abstract class SetMessagesMethodTest {
         jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox");
 
         jmapServer.serverProbe().appendMessage(username, new MailboxPath(MailboxConstants.USER_NAMESPACE, username, "mailbox"),
-                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes()), new Date(), false, new Flags());
+                new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), new Date(), false, new Flags());
         await();
 
         String presumedMessageId = username + "|mailbox|1";
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isAnswered\" : true } } }, \"#0\"]]", presumedMessageId))
+            .header("Authorization", accessToken.serialize())
+            .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isAnswered\" : true } } }, \"#0\"]]", presumedMessageId))
         // When
         .when()
-                .post("/jmap");
+            .post("/jmap");
         // Then
         with()
-                .header("Authorization", accessToken.serialize())
-                .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
-                .post("/jmap")
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
+            .post("/jmap")
         .then()
-                .body(NAME, equalTo("messages"))
-                .body(ARGUMENTS + ".list", hasSize(1))
-                .body(ARGUMENTS + ".list[0].isAnswered", equalTo(true))
-                .log().ifValidationFails();
+            .log().ifValidationFails()
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].isAnswered", equalTo(true));
     }
 
     @Test
@@ -640,34 +651,34 @@ public abstract class SetMessagesMethodTest {
                 "]";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(requestBody)
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
         .when()
-                .post("/jmap")
+            .post("/jmap")
         .then()
-                .log().ifValidationFails()
-                .statusCode(200)
-                .body(NAME, equalTo("messagesSet"))
-                .body(ARGUMENTS + ".notCreated", aMapWithSize(0))
-                // note that assertions on result message had to be split between
-                // string-typed values and boolean-typed value assertions on the same .created entry
-                // make sure only one creation has been processed
-                .body(ARGUMENTS + ".created", aMapWithSize(1))
-                // assert server-set attributes are returned
-                .body(ARGUMENTS + ".created", hasEntry(equalTo(messageCreationId), Matchers.allOf(
-                        hasEntry(equalTo("id"), not(isEmptyOrNullString())),
-                        hasEntry(equalTo("blobId"), not(isEmptyOrNullString())),
-                        hasEntry(equalTo("threadId"), not(isEmptyOrNullString())),
-                        hasEntry(equalTo("size"), not(isEmptyOrNullString()))
-                )))
-                // assert that message flags are all unset
-                .body(ARGUMENTS + ".created", hasEntry(equalTo(messageCreationId), Matchers.allOf(
-                        hasEntry(equalTo("isDraft"), equalTo(false)),
-                        hasEntry(equalTo("isUnread"), equalTo(false)),
-                        hasEntry(equalTo("isFlagged"), equalTo(false)),
-                        hasEntry(equalTo("isAnswered"), equalTo(false))
-                )))
-                ;
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notCreated", aMapWithSize(0))
+            // note that assertions on result message had to be split between
+            // string-typed values and boolean-typed value assertions on the same .created entry
+            // make sure only one creation has been processed
+            .body(ARGUMENTS + ".created", aMapWithSize(1))
+            // assert server-set attributes are returned
+            .body(ARGUMENTS + ".created", hasEntry(equalTo(messageCreationId), Matchers.allOf(
+                    hasEntry(equalTo("id"), not(isEmptyOrNullString())),
+                    hasEntry(equalTo("blobId"), not(isEmptyOrNullString())),
+                    hasEntry(equalTo("threadId"), not(isEmptyOrNullString())),
+                    hasEntry(equalTo("size"), not(isEmptyOrNullString()))
+            )))
+            // assert that message flags are all unset
+            .body(ARGUMENTS + ".created", hasEntry(equalTo(messageCreationId), Matchers.allOf(
+                    hasEntry(equalTo("isDraft"), equalTo(false)),
+                    hasEntry(equalTo("isUnread"), equalTo(false)),
+                    hasEntry(equalTo("isFlagged"), equalTo(false)),
+                    hasEntry(equalTo("isAnswered"), equalTo(false))
+            )))
+            ;
     }
 
     @Test
@@ -728,23 +739,24 @@ public abstract class SetMessagesMethodTest {
                 "]";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(requestBody)
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
         // When
         .when()
-                .post("/jmap");
+            .post("/jmap");
 
         // Then
         with()
-                .header("Authorization", accessToken.serialize())
-                .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
         .post("/jmap")
         .then()
-                .body(NAME, equalTo("messages"))
-                .body(ARGUMENTS + ".list", hasSize(1))
-                .body(ARGUMENTS + ".list[0].subject", equalTo(messageSubject))
-                .body(ARGUMENTS + ".list[0].mailboxIds", contains(outboxId))
-                ;
+            .log().ifValidationFails()
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(ARGUMENTS + ".list[0].subject", equalTo(messageSubject))
+            .body(ARGUMENTS + ".list[0].mailboxIds", contains(outboxId))
+            ;
     }
 
     @Test
@@ -822,19 +834,19 @@ public abstract class SetMessagesMethodTest {
                 "]";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(requestBody)
-                .when()
-                .post("/jmap")
-                .then()
-                .log().ifValidationFails()
-                .statusCode(200)
-                .body(NAME, equalTo("messagesSet"))
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
+        .when()
+            .post("/jmap")
+        .then()
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
 
-                .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].type", equalTo("invalidProperties"))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].description", endsWith("no recipient address set"))
-                .body(ARGUMENTS + ".created", aMapWithSize(0));
+            .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].type", equalTo("invalidProperties"))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].description", endsWith("no recipient address set"))
+            .body(ARGUMENTS + ".created", aMapWithSize(0));
     }
 
     @Test
@@ -857,20 +869,20 @@ public abstract class SetMessagesMethodTest {
                 "]";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(requestBody)
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
         .when()
-                .post("/jmap")
+            .post("/jmap")
         .then()
-                .log().ifValidationFails()
-                .statusCode(200)
-                .body(NAME, equalTo("messagesSet"))
-                .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].type", equalTo("invalidProperties"))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].description", endsWith("'from' address is mandatory"))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", hasSize(1))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", contains("from"))
-                .body(ARGUMENTS + ".created", aMapWithSize(0));
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].type", equalTo("invalidProperties"))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].description", endsWith("'from' address is mandatory"))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", hasSize(1))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", contains("from"))
+            .body(ARGUMENTS + ".created", aMapWithSize(0));
     }
 
     @Test
@@ -895,19 +907,19 @@ public abstract class SetMessagesMethodTest {
                 "]";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(requestBody)
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
         .when()
-                .post("/jmap")
+            .post("/jmap")
         .then()
-                .log().ifValidationFails()
-                .statusCode(200)
-                .body(NAME, equalTo("messagesSet"))
-                .body(ARGUMENTS + ".created", aMapWithSize(1))
-                .body(ARGUMENTS + ".created", hasKey(messageCreationId))
-                .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].headers.from", equalTo(fromAddress))
-                .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].from.name", equalTo(fromAddress))
-                .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].from.email", equalTo(fromAddress));
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".created", aMapWithSize(1))
+            .body(ARGUMENTS + ".created", hasKey(messageCreationId))
+            .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].headers.from", equalTo(fromAddress))
+            .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].from.name", equalTo(fromAddress))
+            .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].from.email", equalTo(fromAddress));
     }
 
     @Test
@@ -932,19 +944,19 @@ public abstract class SetMessagesMethodTest {
                 "]";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(requestBody)
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
         .when()
-                .post("/jmap")
+            .post("/jmap")
         .then()
-                .log().ifValidationFails()
-                .statusCode(200)
-                .body(NAME, equalTo("messagesSet"))
-                .body(ARGUMENTS + ".created", aMapWithSize(1))
-                .body(ARGUMENTS + ".created", hasKey(messageCreationId))
-                .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].headers.from", equalTo(fromAddress))
-                .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].from.name", equalTo(fromAddress))
-                .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].from.email", equalTo(fromAddress));
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".created", aMapWithSize(1))
+            .body(ARGUMENTS + ".created", hasKey(messageCreationId))
+            .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].headers.from", equalTo(fromAddress))
+            .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].from.name", equalTo(fromAddress))
+            .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].from.email", equalTo(fromAddress));
     }
 
     @Test
@@ -1004,20 +1016,20 @@ public abstract class SetMessagesMethodTest {
                 "]";
 
         given()
-                .header("Authorization", accessToken.serialize())
-                .body(requestBody)
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
         .when()
-                .post("/jmap")
+            .post("/jmap")
         .then()
-                .log().ifValidationFails()
-                .statusCode(200)
-                .body(NAME, equalTo("messagesSet"))
-                .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].type", equalTo("invalidProperties"))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", hasSize(1))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", contains("subject"))
-                .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].description", endsWith("'subject' is missing"))
-                .body(ARGUMENTS + ".created", aMapWithSize(0));
+            .log().ifValidationFails()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].type", equalTo("invalidProperties"))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", hasSize(1))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", contains("subject"))
+            .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].description", endsWith("'subject' is missing"))
+            .body(ARGUMENTS + ".created", aMapWithSize(0));
     }
 
 
@@ -1516,7 +1528,7 @@ public abstract class SetMessagesMethodTest {
 
         ZonedDateTime dateTime = ZonedDateTime.parse("2014-10-30T14:12:00Z");
         jmapServer.serverProbe().appendMessage(username, new MailboxPath("#private", username, "inbox"),
-                new ByteArrayInputStream("Subject: my test subject\r\n\r\ntestmail".getBytes()), Date.from(dateTime.toInstant()), false, new Flags());
+                new ByteArrayInputStream("Subject: my test subject\r\n\r\ntestmail".getBytes(Charsets.UTF_8)), Date.from(dateTime.toInstant()), false, new Flags());
 
         String messageToMoveId = "user|inbox|1";
 
@@ -1547,4 +1559,209 @@ public abstract class SetMessagesMethodTest {
                         + "(through reference chain: org.apache.james.jmap.model.Builder[\"mailboxIds\"])"))
                .body(ARGUMENTS + ".updated", hasSize(0));
     }
+    
+    @Test
+    public void setMessagesShouldReturnAttachmentsNotFoundWhenBlobIdDoesntExist() throws Exception {
+        jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "sent");
+        await();
+        String messageCreationId = "creationId";
+        String fromAddress = username;
+        String outboxId = getOutboxId(accessToken);
+        String requestBody = "[" +
+                "  [" +
+                "    \"setMessages\","+
+                "    {" +
+                "      \"create\": { \"" + messageCreationId  + "\" : {" +
+                "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+                "        \"to\": [{ \"name\": \"BOB\", \"email\": \"someone@example.com\"}]," +
+                "        \"subject\": \"Message with a broken blobId\"," +
+                "        \"textBody\": \"Test body\"," +
+                "        \"mailboxIds\": [\"" + outboxId + "\"], " +
+                "        \"attachments\": [" +
+                "                {\"blobId\" : \"brokenId1\", \"type\" : \"image/gif\", \"size\" : 1337}," +
+                "                {\"blobId\" : \"brokenId2\", \"type\" : \"image/jpeg\", \"size\" : 1337}" +
+                "             ]" +
+                "      }}" +
+                "    }," +
+                "    \"#0\"" +
+                "  ]" +
+                "]";
+
+        String notCreatedPath = ARGUMENTS + ".notCreated[\""+messageCreationId+"\"]";
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId))
+            .body(notCreatedPath + ".type", equalTo("invalidProperties"))
+            .body(notCreatedPath + ".attachmentsNotFound", contains("brokenId1", "brokenId2"))
+            .body(ARGUMENTS + ".created", aMapWithSize(0));
+    }
+
+    @Test
+    public void setMessagesShouldReturnAttachmentsWhenMessageHasAttachment() throws Exception {
+        jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "sent");
+
+        Attachment attachment = Attachment.builder()
+                .bytes("attachment".getBytes(Charsets.UTF_8))
+                .type("application/octet-stream")
+                .build();
+        uploadAttachment(attachment);
+        Attachment attachment2 = Attachment.builder()
+                .bytes("attachment2".getBytes(Charsets.UTF_8))
+                .type("application/octet-stream")
+                .build();
+        uploadAttachment(attachment2);
+
+        String messageCreationId = "creationId";
+        String fromAddress = username;
+        String outboxId = getOutboxId(accessToken);
+        String requestBody = "[" +
+                "  [" +
+                "    \"setMessages\","+
+                "    {" +
+                "      \"create\": { \"" + messageCreationId  + "\" : {" +
+                "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+                "        \"to\": [{ \"name\": \"BOB\", \"email\": \"someone@example.com\"}]," +
+                "        \"subject\": \"Message with two attachments\"," +
+                "        \"textBody\": \"Test body\"," +
+                "        \"mailboxIds\": [\"" + outboxId + "\"], " +
+                "        \"attachments\": [" +
+                "               {\"blobId\" : \"" + attachment.getAttachmentId().getId() + "\", " +
+                "               \"type\" : \"" + attachment.getType() + "\", " +
+                "               \"size\" : " + attachment.getSize() + "}," +
+                "               {\"blobId\" : \"" + attachment2.getAttachmentId().getId() + "\", " +
+                "               \"type\" : \"" + attachment2.getType() + "\", " +
+                "               \"size\" : " + attachment2.getSize() + ", " +
+                "               \"cid\" : \"123456789\", " +
+                "               \"isInline\" : true }" +
+                "           ]" +
+                "      }}" +
+                "    }," +
+                "    \"#0\"" +
+                "  ]" +
+                "]";
+
+        String createdPath = ARGUMENTS + ".created[\""+messageCreationId+"\"]";
+        String firstAttachment = createdPath + ".attachments[0]";
+        String secondAttachment = createdPath + ".attachments[1]";
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messagesSet"))
+            .body(ARGUMENTS + ".notCreated", aMapWithSize(0))
+            .body(ARGUMENTS + ".created", aMapWithSize(1))
+            .body(createdPath + ".attachments", hasSize(2))
+            .body(firstAttachment + ".blobId", equalTo(attachment.getAttachmentId().getId()))
+            .body(firstAttachment + ".type", equalTo("application/octet-stream; charset=UTF-8"))
+            .body(firstAttachment + ".size", equalTo((int) attachment.getSize()))
+            .body(firstAttachment + ".cid", nullValue())
+            .body(firstAttachment + ".isInline", equalTo(false))
+            .body(secondAttachment + ".blobId", equalTo(attachment2.getAttachmentId().getId()))
+            .body(secondAttachment + ".type", equalTo("application/octet-stream; charset=UTF-8"))
+            .body(secondAttachment + ".size", equalTo((int) attachment2.getSize()))
+            .body(secondAttachment + ".cid", equalTo("123456789"))
+            .body(secondAttachment + ".isInline", equalTo(true));
+    }
+
+    private void uploadAttachment(Attachment attachment) throws IOException {
+        with()
+            .header("Authorization", accessToken.serialize())
+            .contentType(attachment.getType())
+            .content(attachment.getStream())
+        .post("/upload");
+    }
+
+    @Test
+    public void attachmentsShouldBeRetrievedWhenChainingSetMessagesAndGetMessages() throws Exception {
+        jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "sent");
+
+        Attachment attachment = Attachment.builder()
+                .bytes("attachment".getBytes(Charsets.UTF_8))
+                .type("application/octet-stream")
+                .build();
+        uploadAttachment(attachment);
+
+        String messageCreationId = "creationId";
+        String fromAddress = username;
+        String outboxId = getOutboxId(accessToken);
+        String requestBody = "[" +
+                "  [" +
+                "    \"setMessages\","+
+                "    {" +
+                "      \"create\": { \"" + messageCreationId  + "\" : {" +
+                "        \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," +
+                "        \"to\": [{ \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}]," +
+                "        \"subject\": \"Message with an attachment\"," +
+                "        \"textBody\": \"Test body\"," +
+                "        \"mailboxIds\": [\"" + outboxId + "\"], " +
+                "        \"attachments\": [" +
+                "               {\"blobId\" : \"" + attachment.getAttachmentId().getId() + "\", " +
+                "               \"type\" : \"" + attachment.getType() + "\", " +
+                "               \"size\" : " + attachment.getSize() + ", " +
+                "               \"cid\" : \"123456789\", " +
+                "               \"isInline\" : true }" +
+                "           ]" +
+                "      }}" +
+                "    }," +
+                "    \"#0\"" +
+                "  ]" +
+                "]";
+
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body(requestBody)
+        .when()
+            .post("/jmap");
+
+        calmlyAwait.atMost(30, TimeUnit.SECONDS).until( () -> isAnyMessageFoundInInbox(accessToken));
+
+        String firstMessage = ARGUMENTS + ".list[0]";
+        String firstAttachment = firstMessage + ".attachments[0]";
+        String presumedMessageId = "username@domain.tld|INBOX|1";
+        given()
+            .header("Authorization", accessToken.serialize())
+            .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]")
+        .when()
+            .post("/jmap")
+        .then()
+            .statusCode(200)
+            .body(NAME, equalTo("messages"))
+            .body(ARGUMENTS + ".list", hasSize(1))
+            .body(firstMessage + ".attachments", hasSize(1))
+            .body(firstAttachment + ".blobId", equalTo(attachment.getAttachmentId().getId()))
+            .body(firstAttachment + ".type", equalTo("application/octet-stream"))
+            .body(firstAttachment + ".size", equalTo((int) attachment.getSize()))
+            .body(firstAttachment + ".cid", equalTo("123456789"))
+            .body(firstAttachment + ".isInline", equalTo(true));
+    }
+
+    private boolean isAnyMessageFoundInInbox(AccessToken recipientToken) {
+        try {
+            String inboxId = getMailboxId(accessToken, Role.INBOX);
+            with()
+                    .header("Authorization", recipientToken.serialize())
+                    .body("[[\"getMessageList\", {\"filter\":{\"inMailboxes\":[\"" + inboxId + "\"]}}, \"#0\"]]")
+            .when()
+                    .post("/jmap")
+            .then()
+                    .statusCode(200)
+                    .body(NAME, equalTo("messageList"))
+                    .body(ARGUMENTS + ".messageIds", hasSize(1));
+            return true;
+            
+        } catch (AssertionError e) {
+            return false;
+        }
+    }
 }

http://git-wip-us.apache.org/repos/asf/james-project/blob/662fa4a9/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/AttachmentsNotFoundException.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/AttachmentsNotFoundException.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/AttachmentsNotFoundException.java
new file mode 100644
index 0000000..1c06cb4
--- /dev/null
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/AttachmentsNotFoundException.java
@@ -0,0 +1,40 @@
+/****************************************************************
+ * 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.exceptions;
+
+import java.util.List;
+
+import org.apache.james.jmap.model.BlobId;
+
+import com.google.common.collect.ImmutableList;
+
+public class AttachmentsNotFoundException extends Exception {
+    
+    private List<BlobId> attachmentIds;
+
+
+    public AttachmentsNotFoundException(List<BlobId> attachmentIds) {
+        this.attachmentIds = ImmutableList.copyOf(attachmentIds);
+    }
+    
+    public List<BlobId> getAttachmentIds() {
+        return attachmentIds;
+    }
+}

http://git-wip-us.apache.org/repos/asf/james-project/blob/662fa4a9/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 25ba906..2eb52e7 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
@@ -27,9 +27,11 @@ import java.util.TimeZone;
 import java.util.function.Consumer;
 import java.util.stream.Collectors;
 
+import org.apache.commons.io.IOUtils;
 import org.apache.james.jmap.model.CreationMessage;
 import org.apache.james.jmap.model.CreationMessage.DraftEmailer;
 import org.apache.james.jmap.model.CreationMessageId;
+import org.apache.james.mailbox.store.mail.model.MessageAttachment;
 import org.apache.james.mime4j.Charsets;
 import org.apache.james.mime4j.codec.DecodeMonitor;
 import org.apache.james.mime4j.dom.FieldParser;
@@ -37,9 +39,13 @@ import org.apache.james.mime4j.dom.Message;
 import org.apache.james.mime4j.dom.Multipart;
 import org.apache.james.mime4j.dom.TextBody;
 import org.apache.james.mime4j.dom.address.Mailbox;
+import org.apache.james.mime4j.dom.field.ContentDispositionField;
+import org.apache.james.mime4j.dom.field.ContentTypeField;
 import org.apache.james.mime4j.dom.field.UnstructuredField;
+import org.apache.james.mime4j.field.Fields;
 import org.apache.james.mime4j.field.UnstructuredFieldImpl;
 import org.apache.james.mime4j.message.BasicBodyFactory;
+import org.apache.james.mime4j.message.BodyPart;
 import org.apache.james.mime4j.message.BodyPartBuilder;
 import org.apache.james.mime4j.message.DefaultMessageWriter;
 import org.apache.james.mime4j.message.MessageBuilder;
@@ -51,16 +57,23 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
 import com.google.common.base.Throwables;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableMap.Builder;
 import com.google.common.net.MediaType;
 
 public class MIMEMessageConverter {
+
     private static final Logger LOGGER = LoggerFactory.getLogger(MIMEMessageConverter.class);
 
     private static final String PLAIN_TEXT_MEDIA_TYPE = MediaType.PLAIN_TEXT_UTF_8.withoutParameters().toString();
     private static final String HTML_MEDIA_TYPE = MediaType.HTML_UTF_8.withoutParameters().toString();
     private static final NameValuePair UTF_8_CHARSET = new NameValuePair("charset", Charsets.UTF_8.name());
     private static final String MIXED_SUB_TYPE = "mixed";
+    private static final String FIELD_PARAMETERS_SEPARATOR = ";";
 
     private final BasicBodyFactory bodyFactory;
 
@@ -68,34 +81,34 @@ public class MIMEMessageConverter {
         this.bodyFactory = new BasicBodyFactory();
     }
 
-    public byte[] convert(ValueWithId.CreationMessageEntry creationMessageEntry) {
+    public byte[] convert(ValueWithId.CreationMessageEntry creationMessageEntry, ImmutableList<MessageAttachment> messageAttachments) {
 
         ByteArrayOutputStream buffer = new ByteArrayOutputStream();
         DefaultMessageWriter writer = new DefaultMessageWriter();
         try {
-            writer.writeMessage(convertToMime(creationMessageEntry), buffer);
+            writer.writeMessage(convertToMime(creationMessageEntry, messageAttachments), buffer);
         } catch (IOException e) {
             throw Throwables.propagate(e);
         }
         return buffer.toByteArray();
     }
 
-    @VisibleForTesting Message convertToMime(ValueWithId.CreationMessageEntry creationMessageEntry) {
+    @VisibleForTesting Message convertToMime(ValueWithId.CreationMessageEntry creationMessageEntry, ImmutableList<MessageAttachment> messageAttachments) {
         if (creationMessageEntry == null || creationMessageEntry.getValue() == null) {
             throw new IllegalArgumentException("creationMessageEntry is either null or has null message");
         }
 
         MessageBuilder messageBuilder = MessageBuilder.create();
-        if (mixedTextAndHtml(creationMessageEntry.getValue())) {
-            messageBuilder.setBody(createMultipartBody(creationMessageEntry.getValue()));
+        if (isMultipart(creationMessageEntry.getValue(), messageAttachments)) {
+            messageBuilder.setBody(createMultipartBody(creationMessageEntry.getValue(), messageAttachments));
         } else {
             messageBuilder.setBody(createTextBody(creationMessageEntry.getValue()));
         }
-        buildMimeHeaders(messageBuilder, creationMessageEntry.getCreationId(), creationMessageEntry.getValue());
+        buildMimeHeaders(messageBuilder, creationMessageEntry.getCreationId(), creationMessageEntry.getValue(), messageAttachments);
         return messageBuilder.build();
     }
 
-    private void buildMimeHeaders(MessageBuilder messageBuilder, CreationMessageId creationId, CreationMessage newMessage) {
+    private void buildMimeHeaders(MessageBuilder messageBuilder, CreationMessageId creationId, CreationMessage newMessage, ImmutableList<MessageAttachment> messageAttachments) {
         Optional<Mailbox> fromAddress = newMessage.getFrom().filter(DraftEmailer::hasValidEmail).map(this::convertEmailToMimeHeader);
         fromAddress.ifPresent(messageBuilder::setFrom);
         fromAddress.ifPresent(messageBuilder::setSender);
@@ -121,7 +134,7 @@ public class MIMEMessageConverter {
         // note that date conversion probably lose milliseconds!
         messageBuilder.setDate(Date.from(newMessage.getDate().toInstant()), TimeZone.getTimeZone(newMessage.getDate().getZone()));
         newMessage.getInReplyToMessageId().ifPresent(addInReplyToHeader(messageBuilder::addField));
-        if (!mixedTextAndHtml(newMessage)) {
+        if (!isMultipart(newMessage, messageAttachments)) {
             newMessage.getHtmlBody().ifPresent(x -> messageBuilder.setContentType(HTML_MEDIA_TYPE, UTF_8_CHARSET));
         }
     }
@@ -134,8 +147,9 @@ public class MIMEMessageConverter {
         };
     }
 
-    private boolean mixedTextAndHtml(CreationMessage newMessage) {
-        return newMessage.getTextBody().isPresent() && newMessage.getHtmlBody().isPresent();
+    private boolean isMultipart(CreationMessage newMessage, ImmutableList<MessageAttachment> messageAttachments) {
+        return (newMessage.getTextBody().isPresent() && newMessage.getHtmlBody().isPresent())
+                || !messageAttachments.isEmpty();
     }
 
     private TextBody createTextBody(CreationMessage newMessage) {
@@ -145,26 +159,93 @@ public class MIMEMessageConverter {
         return bodyFactory.textBody(body, Charsets.UTF_8);
     }
 
-    private Multipart createMultipartBody(CreationMessage newMessage) {
+    private Multipart createMultipartBody(CreationMessage newMessage, ImmutableList<MessageAttachment> messageAttachments) {
         try {
-            return MultipartBuilder.create(MIXED_SUB_TYPE)
-                    .addBodyPart(BodyPartBuilder.create()
-                            .use(bodyFactory)
-                            .setBody(newMessage.getTextBody().get(), Charsets.UTF_8)
-                            .setContentType(PLAIN_TEXT_MEDIA_TYPE, UTF_8_CHARSET)
-                            .build())
-                    .addBodyPart(BodyPartBuilder.create()
-                            .use(bodyFactory)
-                            .setBody(newMessage.getHtmlBody().get(), Charsets.UTF_8)
-                            .setContentType(HTML_MEDIA_TYPE, UTF_8_CHARSET)
-                            .build())
-                    .build();
+            MultipartBuilder builder = MultipartBuilder.create(MIXED_SUB_TYPE);
+            addText(builder, newMessage.getTextBody());
+            addHtml(builder, newMessage.getHtmlBody());
+
+            Consumer<MessageAttachment> addAttachment = addAttachment(builder);
+            messageAttachments.stream()
+                .forEach(addAttachment);
+
+            return builder.build();
         } catch (IOException e) {
             LOGGER.error("Error while creating textBody \n"+ newMessage.getTextBody().get() +"\n or htmlBody \n" + newMessage.getHtmlBody().get(), e);
             throw Throwables.propagate(e);
         }
     }
 
+    private void addText(MultipartBuilder builder, Optional<String> textBody) throws IOException {
+        if (textBody.isPresent()) {
+            builder.addBodyPart(BodyPartBuilder.create()
+                .use(bodyFactory)
+                .setBody(textBody.get(), Charsets.UTF_8)
+                .setContentType(PLAIN_TEXT_MEDIA_TYPE, UTF_8_CHARSET)
+                .build());
+        }
+    }
+
+    private void addHtml(MultipartBuilder builder, Optional<String> htmlBody) throws IOException {
+        if (htmlBody.isPresent()) {
+            builder.addBodyPart(BodyPartBuilder.create()
+                .use(bodyFactory)
+                .setBody(htmlBody.get(), Charsets.UTF_8)
+                .setContentType(HTML_MEDIA_TYPE, UTF_8_CHARSET)
+                .build());
+        }
+    }
+
+    private Consumer<MessageAttachment> addAttachment(MultipartBuilder builder) {
+        return att -> { 
+            try {
+                builder.addBodyPart(attachmentBodyPart(att));
+            } catch (IOException e) {
+                LOGGER.error("Error while creating attachment", e);
+                throw Throwables.propagate(e);
+            }
+        };
+    }
+
+    private BodyPart attachmentBodyPart(MessageAttachment att) throws IOException {
+        BodyPartBuilder builder = BodyPartBuilder.create()
+            .use(bodyFactory)
+            .setBody(IOUtils.toString(att.getAttachment().getStream()), Charsets.UTF_8)
+            .setField(contentTypeField(att))
+            .setField(contentDispositionField(att.isInline()));
+        contentId(builder, att);
+        return builder.build();
+    }
+
+    private void contentId(BodyPartBuilder builder, MessageAttachment att) {
+        if (att.getCid().isPresent()) {
+            builder.setField(new RawField("Content-ID", att.getCid().get()));
+        }
+    }
+
+    private ContentTypeField contentTypeField(MessageAttachment att) {
+        Builder<String, String> parameters = ImmutableMap.<String, String> builder();
+        if (att.getName().isPresent()) {
+            parameters.put("name", att.getName().get());
+        }
+        String type = att.getAttachment().getType();
+        if (type.contains(FIELD_PARAMETERS_SEPARATOR)) {
+            return Fields.contentType(contentTypeWithoutParameters(type), parameters.build());
+        }
+        return Fields.contentType(type, parameters.build());
+    }
+
+    private String contentTypeWithoutParameters(String type) {
+        return FluentIterable.from(Splitter.on(FIELD_PARAMETERS_SEPARATOR).split(type)).get(0);
+    }
+
+    private ContentDispositionField contentDispositionField(boolean isInline) {
+        if (isInline) {
+            return Fields.contentDisposition("inline");
+        }
+        return Fields.contentDisposition("attachment");
+    }
+
     private Mailbox convertEmailToMimeHeader(DraftEmailer address) {
         if (!address.hasValidEmail()) {
             throw new IllegalArgumentException("address");

http://git-wip-us.apache.org/repos/asf/james-project/blob/662fa4a9/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
index 3da428b..9d6210e 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesCreationProcessor.java
@@ -26,6 +26,7 @@ import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 import javax.inject.Inject;
 import javax.mail.Flags;
@@ -33,8 +34,11 @@ import javax.mail.MessagingException;
 import javax.mail.internet.SharedInputStream;
 import javax.mail.util.SharedByteArrayInputStream;
 
+import org.apache.james.jmap.exceptions.AttachmentsNotFoundException;
 import org.apache.james.jmap.methods.ValueWithId.CreationMessageEntry;
 import org.apache.james.jmap.methods.ValueWithId.MessageWithId;
+import org.apache.james.jmap.model.Attachment;
+import org.apache.james.jmap.model.BlobId;
 import org.apache.james.jmap.model.CreationMessage;
 import org.apache.james.jmap.model.CreationMessageId;
 import org.apache.james.jmap.model.Message;
@@ -43,6 +47,7 @@ import org.apache.james.jmap.model.MessageId;
 import org.apache.james.jmap.model.MessageProperties;
 import org.apache.james.jmap.model.MessageProperties.MessageProperty;
 import org.apache.james.jmap.model.SetError;
+import org.apache.james.jmap.model.SetMessagesError;
 import org.apache.james.jmap.model.SetMessagesRequest;
 import org.apache.james.jmap.model.SetMessagesResponse;
 import org.apache.james.jmap.model.SetMessagesResponse.Builder;
@@ -52,16 +57,22 @@ import org.apache.james.jmap.send.MailMetadata;
 import org.apache.james.jmap.send.MailSpool;
 import org.apache.james.jmap.utils.SystemMailboxesProvider;
 import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.exception.AttachmentNotFoundException;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.MailboxNotFoundException;
 import org.apache.james.mailbox.model.MailboxPath;
 import org.apache.james.mailbox.store.MailboxSessionMapperFactory;
+import org.apache.james.mailbox.store.mail.AttachmentMapper;
+import org.apache.james.mailbox.store.mail.AttachmentMapperFactory;
 import org.apache.james.mailbox.store.mail.MessageMapper;
+import org.apache.james.mailbox.store.mail.model.AttachmentId;
 import org.apache.james.mailbox.store.mail.model.Mailbox;
 import org.apache.james.mailbox.store.mail.model.MailboxId;
 import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.apache.james.mailbox.store.mail.model.MessageAttachment;
 import org.apache.james.mailbox.store.mail.model.impl.PropertyBuilder;
 import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage;
+import org.apache.james.util.streams.ImmutableCollectors;
 import org.apache.mailet.Mail;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -80,6 +91,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
     private final MailFactory mailFactory;
     private final MessageFactory messageFactory;
     private final SystemMailboxesProvider systemMailboxesProvider;
+    private AttachmentMapperFactory attachmentMapperFactory;
 
     
     @VisibleForTesting @Inject
@@ -88,13 +100,15 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
                                  MailSpool mailSpool,
                                  MailFactory mailFactory,
                                  MessageFactory messageFactory,
-                                 SystemMailboxesProvider systemMailboxesProvider) {
+                                 SystemMailboxesProvider systemMailboxesProvider,
+                                 AttachmentMapperFactory attachmentMapperFactory) {
         this.mailboxSessionMapperFactory = mailboxSessionMapperFactory;
         this.mimeMessageConverter = mimeMessageConverter;
         this.mailSpool = mailSpool;
         this.mailFactory = mailFactory;
         this.messageFactory = messageFactory;
         this.systemMailboxesProvider = systemMailboxesProvider;
+        this.attachmentMapperFactory = attachmentMapperFactory;
     }
 
     @Override
@@ -109,7 +123,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
     private void handleCreate(CreationMessageEntry create, Builder responseBuilder, MailboxSession mailboxSession) {
         try {
             validateImplementedFeature(create, mailboxSession);
-            validateArguments(create);
+            validateArguments(create, mailboxSession);
             validateRights(create, mailboxSession);
             MessageWithId created = handleOutboxMessages(create, mailboxSession);
             responseBuilder.created(created.getCreationId(), created.getValue());
@@ -123,6 +137,15 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
                                 Joiner.on(", ").join(e.getAllowedFroms()))
                         .build());
 
+        } catch (AttachmentsNotFoundException e) {
+            responseBuilder.notCreated(create.getCreationId(), 
+                    SetMessagesError.builder()
+                        .type("invalidProperties")
+                        .properties(MessageProperty.mailboxIds)
+                        .attachmentsNotFound(e.getAttachmentIds())
+                        .description("Attachment not found")
+                        .build());
+            
         } catch (MailboxNotImplementedException e) {
             responseBuilder.notCreated(create.getCreationId(), 
                     SetError.builder()
@@ -161,13 +184,41 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
         }
     }
     
-    private void validateArguments(CreationMessageEntry entry) throws MailboxInvalidMessageCreationException {
+    private void validateArguments(CreationMessageEntry entry, MailboxSession session) throws MailboxInvalidMessageCreationException, AttachmentsNotFoundException, MailboxException {
         CreationMessage message = entry.getValue();
         if (!message.isValid()) {
             throw new MailboxInvalidMessageCreationException();
         }
+        assertAttachmentsExist(entry, session);
     }
     
+    @VisibleForTesting void assertAttachmentsExist(CreationMessageEntry entry, MailboxSession session) throws AttachmentsNotFoundException, MailboxException {
+        List<Attachment> attachments = entry.getValue().getAttachments();
+        if (!attachments.isEmpty()) {
+            AttachmentMapper attachmentMapper = attachmentMapperFactory.getAttachmentMapper(session);
+            List<BlobId> notFounds = listAttachmentsNotFound(attachments, attachmentMapper);
+            if (!notFounds.isEmpty()) {
+                throw new AttachmentsNotFoundException(notFounds);
+            }
+        }
+    }
+
+    private List<BlobId> listAttachmentsNotFound(List<Attachment> attachments, AttachmentMapper attachmentMapper) {
+        return attachments.stream()
+            .flatMap(attachment -> {
+                try {
+                    attachmentMapper.getAttachment(getAttachmentId(attachment));
+                    return Stream.of();
+                } catch (AttachmentNotFoundException e) {
+                    return Stream.of(attachment.getBlobId());
+                }
+            }).collect(ImmutableCollectors.toImmutableList());
+    }
+
+    private AttachmentId getAttachmentId(Attachment attachment) {
+        return AttachmentId.from(attachment.getBlobId().getRawValue());
+    }
+
     private void validateRights(CreationMessageEntry entry, MailboxSession session) throws MailboxSendingNotAllowedException {
         List<String> allowedSenders = ImmutableList.of(session.getUser().getUserName());
         if (!isAllowedFromAddress(entry.getValue(), allowedSenders)) {
@@ -200,9 +251,9 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
         
         CreationMessageId creationId = createdEntry.getCreationId();
         MessageMapper messageMapper = mailboxSessionMapperFactory.createMessageMapper(session);
-        MailboxMessage newMailboxMessage = buildMailboxMessage(createdEntry, outbox);
+        MailboxMessage newMailboxMessage = buildMailboxMessage(session, createdEntry, outbox);
         messageMapper.add(outbox, newMailboxMessage);
-        Message jmapMessage = messageFactory.fromMailboxMessage(newMailboxMessage, ImmutableList.of(), buildMessageIdFromUid);
+        Message jmapMessage = messageFactory.fromMailboxMessage(newMailboxMessage, newMailboxMessage.getAttachments(), buildMessageIdFromUid);
         sendMessage(newMailboxMessage, jmapMessage, session);
         return new MessageWithId(creationId, jmapMessage);
     }
@@ -248,8 +299,9 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
         return new MessageId(session.getUser(), outboxPath, uid);
     }
 
-    private MailboxMessage buildMailboxMessage(MessageWithId.CreationMessageEntry createdEntry, Mailbox outbox) {
-        byte[] messageContent = mimeMessageConverter.convert(createdEntry);
+    private MailboxMessage buildMailboxMessage(MailboxSession session, MessageWithId.CreationMessageEntry createdEntry, Mailbox outbox) throws MailboxException {
+        ImmutableList<MessageAttachment> messageAttachments = getMessageAttachments(session, createdEntry.getValue().getAttachments());
+        byte[] messageContent = mimeMessageConverter.convert(createdEntry, messageAttachments);
         SharedInputStream content = new SharedByteArrayInputStream(messageContent);
         long size = messageContent.length;
         int bodyStartOctet = 0;
@@ -260,7 +312,29 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor {
         Date internalDate = Date.from(createdEntry.getValue().getDate().toInstant());
 
         return new SimpleMailboxMessage(internalDate, size,
-                bodyStartOctet, content, flags, propertyBuilder, mailboxId);
+                bodyStartOctet, content, flags, propertyBuilder, mailboxId, messageAttachments);
+    }
+
+    private ImmutableList<MessageAttachment> getMessageAttachments(MailboxSession session, ImmutableList<Attachment> attachments) throws MailboxException {
+        AttachmentMapper attachmentMapper = attachmentMapperFactory.getAttachmentMapper(session);
+        return attachments.stream()
+            .map(att -> messageAttachment(attachmentMapper, att))
+            .collect(ImmutableCollectors.toImmutableList());
+    }
+
+    private MessageAttachment messageAttachment(AttachmentMapper attachmentMapper, Attachment attachment) {
+        try {
+            return MessageAttachment.builder()
+                    .attachment(attachmentMapper.getAttachment(AttachmentId.from(attachment.getBlobId().getRawValue())))
+                    .name(attachment.getName().orElse(null))
+                    .cid(attachment.getCid().orElse(null))
+                    .isInline(attachment.isIsInline())
+                    .build();
+        } catch (AttachmentNotFoundException e) {
+            // should not happen (checked before)
+            LOG.error(String.format("Attachment %s not found", attachment.getBlobId()), e);
+            return null;
+        }
     }
 
     private PropertyBuilder buildPropertyBuilder() {

http://git-wip-us.apache.org/repos/asf/james-project/blob/662fa4a9/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
index 60f7b91..5202a15 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/CreationMessage.java
@@ -25,6 +25,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 import java.util.Optional;
+import java.util.function.Predicate;
 import java.util.stream.Collectors;
 
 import javax.mail.internet.AddressException;
@@ -73,7 +74,7 @@ public class CreationMessage {
         private String textBody;
         private String htmlBody;
         private final ImmutableList.Builder<Attachment> attachments;
-        private final ImmutableMap.Builder<String, SubMessage> attachedMessages;
+        private final ImmutableMap.Builder<BlobId, SubMessage> attachedMessages;
 
         private Builder() {
             to = ImmutableList.builder();
@@ -89,6 +90,7 @@ public class CreationMessage {
             return mailboxIds(Arrays.asList(mailboxIds));
         }
 
+        @JsonDeserialize
         public Builder mailboxIds(List<String> mailboxIds) {
             this.mailboxIds = ImmutableList.copyOf(mailboxIds);
             return this;
@@ -169,27 +171,39 @@ public class CreationMessage {
             return this;
         }
 
+        public Builder attachments(Attachment... attachments) {
+            return attachments(Arrays.asList(attachments));
+        }
+        
+        @JsonDeserialize
         public Builder attachments(List<Attachment> attachments) {
             this.attachments.addAll(attachments);
             return this;
         }
 
-        public Builder attachedMessages(Map<String, SubMessage> attachedMessages) {
+        public Builder attachedMessages(Map<BlobId, SubMessage> attachedMessages) {
             this.attachedMessages.putAll(attachedMessages);
             return this;
         }
 
-        private static boolean areAttachedMessagesKeysInAttachments(ImmutableList<Attachment> attachments, ImmutableMap<String, SubMessage> attachedMessages) {
-            return attachments.stream()
+        private static boolean areAttachedMessagesKeysInAttachments(ImmutableList<Attachment> attachments, ImmutableMap<BlobId, SubMessage> attachedMessages) {
+            return attachedMessages.isEmpty() || attachedMessages.keySet().stream()
+                    .anyMatch(inAttachments(attachments));
+        }
+
+        private static Predicate<BlobId> inAttachments(ImmutableList<Attachment> attachments) {
+            return (key) -> {
+                return attachments.stream()
                     .map(Attachment::getBlobId)
-                    .allMatch(attachedMessages::containsKey);
+                    .anyMatch(blobId -> blobId.equals(key));
+            };
         }
 
         public CreationMessage build() {
             Preconditions.checkState(mailboxIds != null, "'mailboxIds' is mandatory");
             Preconditions.checkState(headers != null, "'headers' is mandatory");
             ImmutableList<Attachment> attachments = this.attachments.build();
-            ImmutableMap<String, SubMessage> attachedMessages = this.attachedMessages.build();
+            ImmutableMap<BlobId, SubMessage> attachedMessages = this.attachedMessages.build();
             Preconditions.checkState(areAttachedMessagesKeysInAttachments(attachments, attachedMessages), "'attachedMessages' keys must be in 'attachments'");
 
             if (date == null) {
@@ -218,12 +232,12 @@ public class CreationMessage {
     private final Optional<String> textBody;
     private final Optional<String> htmlBody;
     private final ImmutableList<Attachment> attachments;
-    private final ImmutableMap<String, SubMessage> attachedMessages;
+    private final ImmutableMap<BlobId, SubMessage> attachedMessages;
 
     @VisibleForTesting
     CreationMessage(ImmutableList<String> mailboxIds, Optional<String> inReplyToMessageId, boolean isUnread, boolean isFlagged, boolean isAnswered, boolean isDraft, ImmutableMap<String, String> headers, Optional<DraftEmailer> from,
                     ImmutableList<DraftEmailer> to, ImmutableList<DraftEmailer> cc, ImmutableList<DraftEmailer> bcc, ImmutableList<DraftEmailer> replyTo, String subject, ZonedDateTime date, Optional<String> textBody, Optional<String> htmlBody, ImmutableList<Attachment> attachments,
-                    ImmutableMap<String, SubMessage> attachedMessages) {
+                    ImmutableMap<BlobId, SubMessage> attachedMessages) {
         this.mailboxIds = mailboxIds;
         this.inReplyToMessageId = inReplyToMessageId;
         this.isUnread = isUnread;
@@ -312,7 +326,7 @@ public class CreationMessage {
         return attachments;
     }
 
-    public ImmutableMap<String, SubMessage> getAttachedMessages() {
+    public ImmutableMap<BlobId, SubMessage> getAttachedMessages() {
         return attachedMessages;
     }
 

http://git-wip-us.apache.org/repos/asf/james-project/blob/662fa4a9/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetError.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetError.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetError.java
index 658491c..cda1aef 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetError.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetError.java
@@ -49,7 +49,7 @@ public class SetError {
         private String description;
         private Optional<ImmutableSet<MessageProperty>> properties = Optional.empty();
 
-        private Builder() {
+        protected Builder() {
         }
 
         public Builder type(String type) {
@@ -91,6 +91,13 @@ public class SetError {
         this.properties = properties;
     }
 
+    protected SetError(SetError setError) {
+        this.type = setError.type;
+        this.description = setError.description;
+        this.properties = setError.properties;
+    }
+
+    
     @JsonSerialize
     public String getType() {
         return type;

http://git-wip-us.apache.org/repos/asf/james-project/blob/662fa4a9/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesError.java
----------------------------------------------------------------------
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesError.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesError.java
new file mode 100644
index 0000000..2cf19e0
--- /dev/null
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesError.java
@@ -0,0 +1,92 @@
+/****************************************************************
+ * 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.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.james.jmap.model.MessageProperties.MessageProperty;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.google.common.collect.ImmutableList;
+
+public class SetMessagesError extends SetError {
+
+    public static SetMessagesError.Builder builder() {
+        return new Builder();
+    }
+    
+    public static class Builder extends SetError.Builder {
+        
+        private List<BlobId> attachmentsNotFound;
+
+        private Builder() {
+            super();
+            attachmentsNotFound = new ArrayList<>();
+        }
+
+        @Override
+        public Builder description(String description) {
+            return (Builder) super.description(description);
+        }
+        
+        @Override
+        public Builder properties(MessageProperty... properties) {
+            return (Builder) super.properties(properties);
+        }
+        
+        @Override
+        public Builder properties(Set<MessageProperty> properties) {
+            return (Builder) super.properties(properties);
+        }
+        
+        @Override
+        public Builder type(String type) {
+            return (Builder) super.type(type);
+        }
+        
+        public Builder attachmentsNotFound(BlobId... attachmentIds) {
+            return attachmentsNotFound(Arrays.asList(attachmentIds));
+        }
+        
+        public Builder attachmentsNotFound(List<BlobId> attachmentIds) {
+            this.attachmentsNotFound.addAll(attachmentIds);
+            return this;
+        }
+        
+        @Override
+        public SetError build() {
+            return new SetMessagesError(super.build(), ImmutableList.copyOf(attachmentsNotFound));
+        }
+    }
+
+    private ImmutableList<BlobId> attachmentsNotFound;
+    
+    public SetMessagesError(SetError setError, ImmutableList<BlobId> attachmentsNotFound) {
+        super(setError);
+        this.attachmentsNotFound = attachmentsNotFound;
+    }
+    
+    @JsonSerialize
+    public List<BlobId> getAttachmentsNotFound() {
+        return attachmentsNotFound;
+    }
+}


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


Mime
View raw message