JAMES-2214 Implement Draft saving via JMAP
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/fe0522ac Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/fe0522ac Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/fe0522ac Branch: refs/heads/master Commit: fe0522ac5cc2eaa20619dcc8cef40075f9d39046 Parents: f85b5c6 Author: benwa <[email protected]> Authored: Mon Nov 13 11:46:22 2017 +0700 Committer: benwa <[email protected]> Committed: Wed Nov 15 18:04:09 2017 +0700 ---------------------------------------------------------------------- .../integration/SetMessagesMethodTest.java | 540 ++++++++++++++++++- .../InvalidDraftKeywordsException.java | 24 + .../james/jmap/methods/MessageAppender.java | 18 +- .../methods/SetMessagesCreationProcessor.java | 60 ++- .../SetMessagesCreationProcessorTest.java | 27 +- 5 files changed, 624 insertions(+), 45 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/fe0522ac/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 3508e65..13e1da4 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 @@ -119,8 +119,8 @@ public abstract class SetMessagesMethodTest { private static final String SECOND_ARGUMENTS = "[1][1]"; private static final String USERS_DOMAIN = "domain.tld"; private static final String USERNAME = "username@" + USERS_DOMAIN; - private static final String PASSWORD = "password"; private static final String BOB = "bob@" + USERS_DOMAIN; + private static final String PASSWORD = "password"; private static final String BOB_PASSWORD = "bobPassword"; private static final MailboxPath USER_MAILBOX = MailboxPath.forUser(USERNAME, "mailbox"); private static final String NOT_UPDATED = ARGUMENTS + ".notUpdated"; @@ -194,6 +194,10 @@ public abstract class SetMessagesMethodTest { return getMailboxId(accessToken, Role.OUTBOX); } + private String getDraftId(AccessToken accessToken) { + return getMailboxId(accessToken, Role.DRAFTS); + } + private String getMailboxId(AccessToken accessToken, Role role) { return getAllMailboxesIds(accessToken).stream() .filter(x -> x.get("role").equalsIgnoreCase(role.serialize())) @@ -1291,6 +1295,7 @@ public abstract class SetMessagesMethodTest { .body(NAME, equalTo("messagesSet")) .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId)) .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].type", equalTo("error")) + .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].properties", contains("mailboxIds")) .body(ARGUMENTS + ".notCreated[\""+messageCreationId+"\"].description", endsWith("MailboxId invalid")); } @@ -1361,8 +1366,529 @@ public abstract class SetMessagesMethodTest { .body(ARGUMENTS + ".created", aMapWithSize(1)) .body(ARGUMENTS + ".created", hasKey(messageCreationId)) .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].keywords.$Draft", equalTo(true)) - .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].keywords.$Flagged", equalTo(true)) - ; + .body(ARGUMENTS + ".created[\""+messageCreationId+"\"].keywords.$Flagged", equalTo(true)); + } + + @Test + public void setMessagesShouldAllowDraftCreation() { + String messageCreationId = "creationId1337"; + String fromAddress = USERNAME; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".notCreated", aMapWithSize(0)) + .body(ARGUMENTS + ".created", aMapWithSize(1)) + .body(ARGUMENTS + ".created", hasKey(messageCreationId)); + } + + @Test + public void setMessagesShouldFailWhenSavingADraftInSeveralMailboxes() { + String messageCreationId = "creationId1337"; + String fromAddress = USERNAME; + MailboxId mailboxId = mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, USERNAME, "mailbox"); + + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\", \"" + mailboxId.serialize() + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".created", aMapWithSize(0)) + .body(ARGUMENTS + ".notCreated", aMapWithSize(1)) + .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId)) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].type", equalTo("invalidProperties")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].properties", contains("mailboxIds")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].description", equalTo("Not yet implemented")); + } + + @Test + public void setMessagesShouldBeCompatibleWithIsDraftProperty() { + String messageCreationId = "creationId1337"; + String fromAddress = USERNAME; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"isDraft\": true," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".notCreated", aMapWithSize(0)) + .body(ARGUMENTS + ".created", aMapWithSize(1)) + .body(ARGUMENTS + ".created", hasKey(messageCreationId)); + } + + @Test + public void setMessagesShouldRejectCreateInDraftAndOutboxForASingleMessage() { + String messageCreationId = "creationId1337"; + String fromAddress = USERNAME; + + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\", \"" +getOutboxId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".created", aMapWithSize(0)) + .body(ARGUMENTS + ".notCreated", aMapWithSize(1)) + .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId)) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].type", equalTo("invalidProperties")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].properties", contains("mailboxIds")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].description", equalTo("Not yet implemented")); + } + + @Test + public void setMessagesShouldStoreDraft() { + String messageCreationId = "creationId1337"; + String fromAddress = USERNAME; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + String receivedMessageId = + with() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .post("/jmap") + .then() + .extract() + .path(ARGUMENTS + ".created[\""+messageCreationId+"\"].id"); + + String firstMessage = ARGUMENTS + ".list[0]"; + given() + .header("Authorization", accessToken.serialize()) + .body("[[\"getMessages\", {\"ids\": [\"" + receivedMessageId + "\"]}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .statusCode(200) + .body(NAME, equalTo("messages")) + .body(ARGUMENTS + ".list", hasSize(1)) + .body(firstMessage + ".id", equalTo(receivedMessageId)); + } + + @Test + public void setMessagesShouldNotCheckFromWhenDraft() { + String messageCreationId = "creationId1337"; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"[email protected]\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".notCreated", aMapWithSize(0)) + .body(ARGUMENTS + ".created", aMapWithSize(1)) + .body(ARGUMENTS + ".created", hasKey(messageCreationId)); + } + + @Test + public void setMessagesShouldNotCheckFromWhenInvalidEmailWhenDraft() { + String messageCreationId = "creationId1337"; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"invalid\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".notCreated", aMapWithSize(0)) + .body(ARGUMENTS + ".created", aMapWithSize(1)) + .body(ARGUMENTS + ".created", hasKey(messageCreationId)); + } + + @Test + public void setMessagesShouldAllowDraftCreationWithoutFrom() { + String messageCreationId = "creationId1337"; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".notCreated", aMapWithSize(0)) + .body(ARGUMENTS + ".created", aMapWithSize(1)) + .body(ARGUMENTS + ".created", hasKey(messageCreationId)); + } + + @Test + public void setMessagesShouldAllowDraftCreationWithoutRecipients() { + String messageCreationId = "creationId1337"; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"[email protected]\"}," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".notCreated", aMapWithSize(0)) + .body(ARGUMENTS + ".created", aMapWithSize(1)) + .body(ARGUMENTS + ".created", hasKey(messageCreationId)); + } + + @Test + public void setMessagesShouldRequireDraftFlagWhenSavingDraft() { + String messageCreationId = "creationId1337"; + String fromAddress = USERNAME; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Flagged\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".created", aMapWithSize(0)) + .body(ARGUMENTS + ".notCreated", aMapWithSize(1)) + .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId)) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].type", equalTo("invalidProperties")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].properties", contains("keywords")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].description", equalTo("A draft message should be flagged as Draft")); + } + + @Test + public void setMessagesShouldCheckAttachmentsWhenDraft() { + String messageCreationId = "creationId1337"; + String fromAddress = USERNAME; + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"attachments\": [" + + " {\"blobId\" : \"wrong\", \"type\" : \"image/jpeg\", \"size\" : 1337}" + + " ]," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".created", aMapWithSize(0)) + .body(ARGUMENTS + ".notCreated", aMapWithSize(1)) + .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId)) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].type", equalTo("invalidProperties")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].properties", contains("attachments")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].description", equalTo("Attachment not found")); + } + + @Test + public void setMessagesShouldAcceptAttachmentsWhenDraft() throws Exception { + String messageCreationId = "creationId1337"; + String fromAddress = USERNAME; + Attachment attachment = Attachment.builder() + .bytes("attachment".getBytes(Charsets.UTF_8)) + .type("application/octet-stream") + .build(); + uploadAttachment(attachment); + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"from\": { \"name\": \"Me\", \"email\": \"" + fromAddress + "\"}," + + " \"to\": [{ \"name\": \"BOB\", \"email\": \"[email protected]\"}]," + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"attachments\": [" + + " {\"blobId\" : \"" + attachment.getAttachmentId().getId() + "\", " + + " \"type\" : \"" + attachment.getType() + "\"," + + " \"size\" : " + attachment.getSize() + "}" + + " ]," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", accessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".notCreated", aMapWithSize(0)) + .body(ARGUMENTS + ".created", aMapWithSize(1)) + .body(ARGUMENTS + ".created", hasKey(messageCreationId)); + } + + @Test + public void setMessagesShouldNotAllowDraftCreationInSomeoneElseMailbox() throws Exception { + String messageCreationId = "creationId1337"; + Attachment attachment = Attachment.builder() + .bytes("attachment".getBytes(Charsets.UTF_8)) + .type("application/octet-stream") + .build(); + uploadAttachment(attachment); + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", bobAccessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".created", aMapWithSize(0)) + .body(ARGUMENTS + ".notCreated", aMapWithSize(1)) + .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId)) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].type", equalTo("error")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].description", endsWith("can not be found")); + } + + @Test + public void setMessagesShouldNotAllowDraftCreationInADelegatedMailbox() throws Exception { + String messageCreationId = "creationId1337"; + Attachment attachment = Attachment.builder() + .bytes("attachment".getBytes(Charsets.UTF_8)) + .type("application/octet-stream") + .build(); + uploadAttachment(attachment); + + jmapServer.getProbe(ACLProbeImpl.class) + .addRights( + MailboxPath.forUser(USERNAME, DefaultMailboxes.DRAFTS), + BOB, + MailboxACL.FULL_RIGHTS); + + String requestBody = "[" + + " [" + + " \"setMessages\","+ + " {" + + " \"create\": { \"" + messageCreationId + "\" : {" + + " \"subject\": \"subject\"," + + " \"keywords\": {\"$Draft\": true}," + + " \"mailboxIds\": [\"" + getDraftId(accessToken) + "\"]" + + " }}" + + " }," + + " \"#0\"" + + " ]" + + "]"; + + given() + .header("Authorization", bobAccessToken.serialize()) + .body(requestBody) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body(NAME, equalTo("messagesSet")) + .body(ARGUMENTS + ".created", aMapWithSize(0)) + .body(ARGUMENTS + ".notCreated", aMapWithSize(1)) + .body(ARGUMENTS + ".notCreated", hasKey(messageCreationId)) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].type", equalTo("error")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].properties", contains("mailboxIds")) + .body(ARGUMENTS + ".notCreated[\"" + messageCreationId + "\"].description", endsWith("MailboxId invalid")); } @Test @@ -1814,10 +2340,9 @@ public abstract class SetMessagesMethodTest { public void setMessagesShouldStripBccFromDeliveredEmail() throws Exception { // Recipient String recipientAddress = "recipient" + "@" + USERS_DOMAIN; - String bccRecipient = "bob@" + USERS_DOMAIN; + String bccRecipient = BOB; String password = "password"; dataProbe.addUser(recipientAddress, password); - dataProbe.addUser(bccRecipient, password); mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, recipientAddress, DefaultMailboxes.INBOX); await(); AccessToken recipientToken = HttpJmapAuthentication.authenticateJamesUser(baseUri(), recipientAddress, password); @@ -1933,7 +2458,6 @@ public abstract class SetMessagesMethodTest { String bccAddress = BOB; mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, bccAddress, DefaultMailboxes.INBOX); await(); - AccessToken bccToken = HttpJmapAuthentication.authenticateJamesUser(baseUri(), BOB, BOB_PASSWORD); String messageCreationId = "creationId1337"; String fromAddress = USERNAME; @@ -1964,9 +2488,9 @@ public abstract class SetMessagesMethodTest { .post("/jmap"); // Then - calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> isAnyMessageFoundInRecipientsMailboxes(bccToken)); + calmlyAwait.atMost(30, TimeUnit.SECONDS).until(() -> isAnyMessageFoundInRecipientsMailboxes(bobAccessToken)); with() - .header("Authorization", bccToken.serialize()) + .header("Authorization", bobAccessToken.serialize()) .body("[[\"getMessageList\", {\"fetchMessages\": true, \"fetchMessageProperties\": [\"bcc\"] }, \"#0\"]]") .when() .post("/jmap") http://git-wip-us.apache.org/repos/asf/james-project/blob/fe0522ac/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InvalidDraftKeywordsException.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InvalidDraftKeywordsException.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InvalidDraftKeywordsException.java new file mode 100644 index 0000000..08dd9b6 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/exceptions/InvalidDraftKeywordsException.java @@ -0,0 +1,24 @@ +/**************************************************************** + * 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; + +public class InvalidDraftKeywordsException extends IllegalArgumentException { + +} http://git-wip-us.apache.org/repos/asf/james-project/blob/fe0522ac/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java index 4b1fad6..4d09810 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/MessageAppender.java @@ -25,10 +25,12 @@ import java.util.Optional; import javax.inject.Inject; import javax.mail.util.SharedByteArrayInputStream; +import org.apache.james.jmap.methods.ValueWithId.CreationMessageEntry; import org.apache.james.jmap.model.Attachment; import org.apache.james.jmap.model.Keywords; import org.apache.james.jmap.model.MessageFactory; import org.apache.james.mailbox.AttachmentManager; +import org.apache.james.mailbox.MailboxManager; import org.apache.james.mailbox.MailboxSession; import org.apache.james.mailbox.MessageManager; import org.apache.james.mailbox.exception.AttachmentNotFoundException; @@ -36,6 +38,7 @@ import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.model.AttachmentId; import org.apache.james.mailbox.model.Cid; import org.apache.james.mailbox.model.ComposedMessageId; +import org.apache.james.mailbox.model.MailboxId; import org.apache.james.mailbox.model.MessageAttachment; import org.apache.james.util.OptionalUtils; import org.slf4j.Logger; @@ -49,16 +52,18 @@ import com.google.common.collect.ImmutableList; public class MessageAppender { private static final Logger LOGGER = LoggerFactory.getLogger(MessageAppender.class); + private final MailboxManager mailboxManager; private final AttachmentManager attachmentManager; private final MIMEMessageConverter mimeMessageConverter; @Inject - public MessageAppender(AttachmentManager attachmentManager, MIMEMessageConverter mimeMessageConverter) { + public MessageAppender(MailboxManager mailboxManager, AttachmentManager attachmentManager, MIMEMessageConverter mimeMessageConverter) { + this.mailboxManager = mailboxManager; this.attachmentManager = attachmentManager; this.mimeMessageConverter = mimeMessageConverter; } - public MessageFactory.MetaDataWithContent createMessageInMailbox(ValueWithId.MessageWithId.CreationMessageEntry createdEntry, + public MessageFactory.MetaDataWithContent appendMessageInMailbox(CreationMessageEntry createdEntry, MessageManager mailbox, MailboxSession session) throws MailboxException { ImmutableList<MessageAttachment> messageAttachments = getMessageAttachments(session, createdEntry.getValue().getAttachments()); @@ -85,6 +90,14 @@ public class MessageAppender { .build(); } + public MessageFactory.MetaDataWithContent appendMessageInMailbox(CreationMessageEntry createdEntry, + MailboxId mailboxId, + MailboxSession session) throws MailboxException { + return appendMessageInMailbox(createdEntry, + mailboxManager.getMailbox(mailboxId, session), + session); + } + private ImmutableList<MessageAttachment> getMessageAttachments(MailboxSession session, ImmutableList<Attachment> attachments) throws MailboxException { ThrowingFunction<Attachment, Optional<MessageAttachment>> toMessageAttachment = att -> messageAttachment(session, att); return attachments.stream() @@ -102,7 +115,6 @@ public class MessageAppender { .isInline(attachment.isIsInline()) .build()); } catch (AttachmentNotFoundException e) { - // should not happen (checked before) LOGGER.error(String.format("Attachment %s not found", attachment.getBlobId()), e); return Optional.empty(); } catch (IllegalStateException e) { http://git-wip-us.apache.org/repos/asf/james-project/blob/fe0522ac/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 870235c..a91a65c 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 @@ -30,9 +30,11 @@ import javax.inject.Inject; import javax.mail.MessagingException; import org.apache.james.jmap.exceptions.AttachmentsNotFoundException; +import org.apache.james.jmap.exceptions.InvalidDraftKeywordsException; import org.apache.james.jmap.methods.ValueWithId.CreationMessageEntry; import org.apache.james.jmap.methods.ValueWithId.MessageWithId; import org.apache.james.jmap.model.CreationMessage; +import org.apache.james.jmap.model.Keyword; import org.apache.james.jmap.model.Message; import org.apache.james.jmap.model.MessageFactory; import org.apache.james.jmap.model.MessageFactory.MetaDataWithContent; @@ -107,11 +109,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor { private void handleCreate(CreationMessageEntry create, Builder responseBuilder, MailboxSession mailboxSession) { try { validateIsUserOwnerOfMailboxes(create, mailboxSession); - validateImplementedFeature(create, mailboxSession); - validateArguments(create, mailboxSession); - MessageWithId created = handleOutboxMessages(create, mailboxSession); - responseBuilder.created(created.getCreationId(), created.getValue()); - + performCreate(create, responseBuilder, mailboxSession); } catch (MailboxSendingNotAllowedException e) { responseBuilder.notCreated(create.getCreationId(), SetError.builder() @@ -121,6 +119,14 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor { Joiner.on(", ").join(e.getAllowedFroms())) .build()); + } catch (InvalidDraftKeywordsException e) { + responseBuilder.notCreated(create.getCreationId(), + SetError.builder() + .type("invalidProperties") + .properties(MessageProperty.keywords) + .description("A draft message should be flagged as Draft") + .build()); + } catch (AttachmentsNotFoundException e) { responseBuilder.notCreated(create.getCreationId(), SetMessagesError.builder() @@ -154,6 +160,7 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor { responseBuilder.notCreated(create.getCreationId(), SetError.builder() .type("error") + .properties(MessageProperty.mailboxIds) .description("MailboxId invalid") .build()); @@ -167,15 +174,39 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor { } } - private void validateImplementedFeature(CreationMessageEntry entry, MailboxSession session) throws MailboxException, MailboxNotImplementedException { + private void performCreate(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session) throws MailboxException, MailboxNotImplementedException, MessagingException, AttachmentsNotFoundException { if (isAppendToMailboxWithRole(Role.DRAFTS, entry.getValue(), session)) { - throw new MailboxNotImplementedException("Drafts saving is not implemented"); + saveDraft(entry, responseBuilder, session); + } else if (isAppendToMailboxWithRole(Role.OUTBOX, entry.getValue(), session)) { + sendMailViaOutbox(entry, responseBuilder, session); + } else { + throw new MailboxNotImplementedException("The only implemented feature is sending via outbox and draft saving"); } - if (!isAppendToMailboxWithRole(Role.OUTBOX, entry.getValue(), session)) { - throw new MailboxNotImplementedException("The only implemented feature is sending via outbox"); + } + + private void sendMailViaOutbox(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session) throws AttachmentsNotFoundException, MailboxException, MessagingException { + validateArguments(entry, session); + MessageWithId created = handleOutboxMessages(entry, session); + responseBuilder.created(created.getCreationId(), created.getValue()); + } + + private void saveDraft(CreationMessageEntry entry, Builder responseBuilder, MailboxSession session) throws AttachmentsNotFoundException, MailboxException, MessagingException { + attachmentChecker.assertAttachmentsExist(entry, session); + assertDraftKeywords(entry); + MessageWithId created = handleDraftMessages(entry, session); + responseBuilder.created(created.getCreationId(), created.getValue()); + } + + private void assertDraftKeywords(CreationMessageEntry entry) { + Boolean isDraft = entry.getValue() + .getKeywords() + .map(keywords -> keywords.contains(Keyword.DRAFT)) + .orElse(false); + if (!isDraft) { + throw new InvalidDraftKeywordsException(); } } - + private void validateArguments(CreationMessageEntry entry, MailboxSession session) throws MailboxInvalidMessageCreationException, AttachmentsNotFoundException, MailboxException { CreationMessage message = entry.getValue(); if (!message.isValid()) { @@ -201,11 +232,18 @@ public class SetMessagesCreationProcessor implements SetMessagesProcessor { private MessageWithId handleOutboxMessages(CreationMessageEntry entry, MailboxSession session) throws MailboxException, MessagingException { MessageManager outbox = getMailboxWithRole(session, Role.OUTBOX).orElseThrow(() -> new MailboxNotFoundException(Role.OUTBOX.serialize())); - MetaDataWithContent newMessage = messageAppender.createMessageInMailbox(entry, outbox, session); + MetaDataWithContent newMessage = messageAppender.appendMessageInMailbox(entry, outbox, session); Message jmapMessage = messageFactory.fromMetaDataWithContent(newMessage); messageSender.sendMessage(jmapMessage, newMessage, session); return new ValueWithId.MessageWithId(entry.getCreationId(), jmapMessage); } + + private MessageWithId handleDraftMessages(CreationMessageEntry entry, MailboxSession session) throws MailboxException, MessagingException { + MessageManager draftMailbox = getMailboxWithRole(session, Role.DRAFTS).orElseThrow(() -> new MailboxNotFoundException(Role.DRAFTS.serialize())); + MetaDataWithContent newMessage = messageAppender.appendMessageInMailbox(entry, draftMailbox, session); + Message jmapMessage = messageFactory.fromMetaDataWithContent(newMessage); + return new ValueWithId.MessageWithId(entry.getCreationId(), jmapMessage); + } private boolean isAppendToMailboxWithRole(Role role, CreationMessage entry, MailboxSession mailboxSession) throws MailboxException { return getMailboxWithRole(mailboxSession, role) http://git-wip-us.apache.org/repos/asf/james-project/blob/fe0522ac/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java index 1ef68ea..a0df1b4 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/methods/SetMessagesCreationProcessorTest.java @@ -41,8 +41,6 @@ import org.apache.james.jmap.model.CreationMessage.DraftEmailer; import org.apache.james.jmap.model.CreationMessageId; import org.apache.james.jmap.model.MessageFactory; import org.apache.james.jmap.model.MessagePreviewGenerator; -import org.apache.james.jmap.model.MessageProperties.MessageProperty; -import org.apache.james.jmap.model.SetError; import org.apache.james.jmap.model.SetMessagesRequest; import org.apache.james.jmap.model.SetMessagesResponse; import org.apache.james.jmap.model.mailbox.Role; @@ -140,7 +138,7 @@ public class SetMessagesCreationProcessorTest { fakeSystemMailboxesProvider = new TestSystemMailboxesProvider(() -> optionalOutbox, () -> optionalDrafts); session = new MockMailboxSession(USER); MIMEMessageConverter mimeMessageConverter = new MIMEMessageConverter(); - messageAppender = new MessageAppender(mockedAttachmentManager, mimeMessageConverter); + messageAppender = new MessageAppender(mockedMailboxManager, mockedAttachmentManager, mimeMessageConverter); messageSender = new MessageSender(mockedMailSpool, mockedMailFactory); sut = new SetMessagesCreationProcessor(messageFactory, fakeSystemMailboxesProvider, @@ -297,25 +295,6 @@ public class SetMessagesCreationProcessorTest { } @Test - public void processShouldReturnNotImplementedErrorWhenSavingToDrafts() { - CreationMessageId creationMessageId = CreationMessageId.of("anything-really"); - SetMessagesRequest createMessageInDrafts = SetMessagesRequest.builder() - .create( - creationMessageId, creationMessageBuilder.mailboxId(DRAFTS_ID.serialize()).build()) - .build(); - - // When - SetMessagesResponse actual = sut.process(createMessageInDrafts, session); - - // Then - assertThat(actual.getNotCreated()).hasSize(1).containsEntry(creationMessageId, SetError.builder() - .type("invalidProperties") - .properties(MessageProperty.mailboxIds) - .description("Not yet implemented") - .build()); - } - - @Test public void processShouldNotSendWhenSavingToDrafts() throws Exception { // When CreationMessageId creationMessageId = CreationMessageId.of("anything-really"); @@ -323,7 +302,9 @@ public class SetMessagesCreationProcessorTest { .create( creationMessageId, creationMessageBuilder.mailboxId(DRAFTS_ID.serialize()).build()) .build(); - when(mockedMailboxManager.getMailbox(any(MailboxId.class), any())).thenReturn(drafts); + when(mockedMailboxManager.getMailbox(any(MailboxId.class), any())) + .thenReturn(drafts); + sut.process(createMessageInDrafts, session); // Then --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
