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.baech...@linagora.com> Authored: Tue Jul 5 17:09:13 2016 +0200 Committer: Antoine Duprat <adup...@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\": \"some...@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\": \"some...@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 = "usern...@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-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org