JAMES-1676 Implement message updating feature in setMessages
Project: http://git-wip-us.apache.org/repos/asf/james-project/repo Commit: http://git-wip-us.apache.org/repos/asf/james-project/commit/958a96e7 Tree: http://git-wip-us.apache.org/repos/asf/james-project/tree/958a96e7 Diff: http://git-wip-us.apache.org/repos/asf/james-project/diff/958a96e7 Branch: refs/heads/master Commit: 958a96e71632c74670d1a0b74affd135192214d5 Parents: c4ee557 Author: Fabien Vignon <fvig...@linagora.com> Authored: Wed Feb 3 14:52:27 2016 +0100 Committer: Fabien Vignon <fvig...@linagora.com> Committed: Wed Feb 10 17:45:29 2016 +0100 ---------------------------------------------------------------------- .../jmap/methods/SetMessagesMethodTest.java | 311 ++++++++++++++++++- .../james/jmap/methods/SetMessagesMethod.java | 68 +++- .../james/jmap/model/SetMessagesRequest.java | 18 +- .../james/jmap/model/UpdateMessagePatch.java | 125 ++++++++ .../jmap/model/SetMessagesRequestTest.java | 20 +- 5 files changed, 507 insertions(+), 35 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/james-project/blob/958a96e7/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java b/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java index 8941367..04921b3 100644 --- a/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java +++ b/server/protocols/jmap-integration-testing/src/test/java/org/apache/james/jmap/methods/SetMessagesMethodTest.java @@ -20,20 +20,22 @@ package org.apache.james.jmap.methods; import static com.jayway.restassured.RestAssured.given; +import static com.jayway.restassured.RestAssured.with; import static com.jayway.restassured.config.EncoderConfig.encoderConfig; import static com.jayway.restassured.config.RestAssuredConfig.newConfig; import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.empty; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.hasEntry; +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.collection.IsMapWithSize.aMapWithSize; import static org.hamcrest.collection.IsMapWithSize.anEmptyMap; import java.io.ByteArrayInputStream; import java.util.Date; - import javax.mail.Flags; import org.apache.james.backends.cassandra.EmbeddedCassandra; @@ -41,19 +43,23 @@ import org.apache.james.jmap.JmapAuthentication; import org.apache.james.jmap.JmapServer; import org.apache.james.jmap.api.access.AccessToken; import org.apache.james.mailbox.elasticsearch.EmbeddedElasticSearch; +import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.model.MailboxConstants; import org.apache.james.mailbox.model.MailboxPath; + +import com.google.common.base.Charsets; +import com.jayway.restassured.RestAssured; +import com.jayway.restassured.builder.ResponseSpecBuilder; +import com.jayway.restassured.http.ContentType; +import com.jayway.restassured.specification.ResponseSpecification; import org.hamcrest.Matchers; import org.junit.Before; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.RuleChain; import org.junit.rules.TemporaryFolder; -import com.google.common.base.Charsets; -import com.jayway.restassured.RestAssured; -import com.jayway.restassured.http.ContentType; - public abstract class SetMessagesMethodTest { private static final String NAME = "[0][0]"; @@ -297,4 +303,297 @@ public abstract class SetMessagesMethodTest { .body(NAME, equalTo("messages")) .body(ARGUMENTS + ".list", hasSize(1)); } -} + + @Test + public void setMessagesShouldReturnUpdatedIdAndNoErrorWhenIsUnreadPassedToFalse() throws MailboxException { + // Given + 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()); + embeddedElasticSearch.awaitForElasticSearch(); + + String presumedMessageId = username + "|mailbox|1"; + + // When + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : false } } }, \"#0\"]]", presumedMessageId)) + .when() + .post("/jmap") + // Then + .then() + .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId)) + .log().ifValidationFails(); + } + + private ResponseSpecification getSetMessagesUpdateOKResponseAssertions(String messageId) { + String responseHeaderPath = "[0][0]"; + String responseBodyBasePath = "[0][1]"; + ResponseSpecBuilder builder = new ResponseSpecBuilder() + .expectStatusCode(200) + .expectBody(responseHeaderPath, equalTo("messagesSet")) + .expectBody(responseBodyBasePath +".updated", hasSize(1)) + .expectBody(responseBodyBasePath +".updated", contains(messageId)) + .expectBody(responseBodyBasePath +".error", isEmptyOrNullString()) + .expectBody(responseBodyBasePath +".notUpdated", not(hasKey(messageId))); + return builder.build(); + } + + @Test + public void setMessagesShouldMarkAsReadWhenIsUnreadPassedToFalse() throws MailboxException { + // Given + 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()); + embeddedElasticSearch.awaitForElasticSearch(); + + String presumedMessageId = username + "|mailbox|1"; + with() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : false } } }, \"#0\"]]", presumedMessageId)) + // When + .post("/jmap"); + // Then + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .statusCode(200) + .body("[0][0]", equalTo("messages")) + .body("[0][1].list", hasSize(1)) + .body("[0][1].list[0].isUnread", equalTo(false)) + .log().ifValidationFails(); + } + + @Test + public void setMessagesShouldReturnUpdatedIdAndNoErrorWhenIsUnreadPassed() throws MailboxException { + // Given + 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)); + embeddedElasticSearch.awaitForElasticSearch(); + + String presumedMessageId = username + "|mailbox|1"; + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : true } } }, \"#0\"]]", presumedMessageId)) + // When + .when() + .post("/jmap") + // Then + .then() + .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId)) + .log().ifValidationFails(); + } + + @Test + public void setMessagesShouldMarkAsUnreadWhenIsUnreadPassed() throws MailboxException { + // Given + 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)); + embeddedElasticSearch.awaitForElasticSearch(); + + String presumedMessageId = username + "|mailbox|1"; + with() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : true } } }, \"#0\"]]", presumedMessageId)) + // When + .post("/jmap"); + // Then + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .body("[0][0]", equalTo("messages")) + .body("[0][1].list", hasSize(1)) + .body("[0][1].list[0].isUnread", equalTo(true)) + .log().ifValidationFails(); + } + + + @Test + public void setMessagesShouldReturnUpdatedIdAndNoErrorWhenIsFlaggedPassed() throws MailboxException { + // Given + 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()); + embeddedElasticSearch.awaitForElasticSearch(); + + String presumedMessageId = username + "|mailbox|1"; + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isFlagged\" : true } } }, \"#0\"]]", presumedMessageId)) + // When + .when() + .post("/jmap") + // Then + .then() + .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId)) + .log().ifValidationFails(); + } + + @Test + public void setMessagesShouldMarkAsFlaggedWhenIsFlaggedPassed() throws MailboxException { + // Given + 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()); + embeddedElasticSearch.awaitForElasticSearch(); + + String presumedMessageId = username + "|mailbox|1"; + with() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isFlagged\" : true } } }, \"#0\"]]", presumedMessageId)) + // When + .post("/jmap"); + // Then + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .body("[0][0]", equalTo("messages")) + .body("[0][1].list", hasSize(1)) + .body("[0][1].list[0].isFlagged", equalTo(true)) + .log().ifValidationFails(); + } + + @Test + @Ignore("Unable to deal with invalid types from SetMessages requests handler") + 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()); + + embeddedElasticSearch.awaitForElasticSearch(); + + String messageId = username + "|mailbox|1"; + + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : \"123\" } } }, \"#0\"]]", messageId)) + // Does not work, jackson throws InvalidFormatException way before SetMessageMethod can deal with ! + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body("[0][0]", equalTo("messagesSet")) + .body("[0][1].notUpdated", hasKey(messageId)) + .body("[0][1].notUpdated[\""+messageId+"\"].type", equalTo("invalidProperties")) + .body("[0][1].notUpdated[\""+messageId+"\"].properties", equalTo("isUnread")) + .body("[0][1].notUpdated[\""+messageId+"\"].description", equalTo("invalid properties")) + .body("[0][1].updated", hasSize(0)); + } + + @Test + public void setMessagesShouldMarkMessageAsAnsweredWhenIsAnsweredPassed() throws MailboxException { + // Given + 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()); + embeddedElasticSearch.awaitForElasticSearch(); + + String presumedMessageId = username + "|mailbox|1"; + // When + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isAnswered\" : true } } }, \"#0\"]]", presumedMessageId)) + .when() + .post("/jmap") + // Then + .then() + .spec(getSetMessagesUpdateOKResponseAssertions(presumedMessageId)) + .log().ifValidationFails(); + } + + @Test + public void setMessagesShouldMarkAsAnsweredWhenIsAnsweredPassed() throws MailboxException { + // Given + 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()); + embeddedElasticSearch.awaitForElasticSearch(); + + String presumedMessageId = username + "|mailbox|1"; + with() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isAnswered\" : true } } }, \"#0\"]]", presumedMessageId)) + // When + .post("/jmap"); + // Then + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body("[[\"getMessages\", {\"ids\": [\"" + presumedMessageId + "\"]}, \"#0\"]]") + .when() + .post("/jmap") + .then() + .body("[0][0]", equalTo("messages")) + .body("[0][1].list", hasSize(1)) + .body("[0][1].list[0].isAnswered", equalTo(true)) + .log().ifValidationFails(); + } + + @Test + public void setMessageShouldReturnNotFoundWhenUpdateUnknownMessage() { + jmapServer.serverProbe().createMailbox(MailboxConstants.USER_NAMESPACE, username, "mailbox"); + + String nonExistingMessageId = username + "|mailbox|12345"; + + given() + .accept(ContentType.JSON) + .contentType(ContentType.JSON) + .header("Authorization", accessToken.serialize()) + .body(String.format("[[\"setMessages\", {\"update\": {\"%s\" : { \"isUnread\" : true } } }, \"#0\"]]", nonExistingMessageId)) + .when() + .post("/jmap") + .then() + .log().ifValidationFails() + .statusCode(200) + .body("[0][0]", equalTo("messagesSet")) + .body("[0][1].notUpdated", hasKey(nonExistingMessageId)) + .body("[0][1].notUpdated[\""+nonExistingMessageId+"\"].type", equalTo("notFound")) + .body("[0][1].notUpdated[\""+nonExistingMessageId+"\"].description", equalTo("message not found")) + .body("[0][1].updated", hasSize(0)); + } +} \ No newline at end of file http://git-wip-us.apache.org/repos/asf/james-project/blob/958a96e7/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java index ebdaf77..c849718 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/methods/SetMessagesMethod.java @@ -21,20 +21,26 @@ package org.apache.james.jmap.methods; import java.util.Iterator; import java.util.List; +import java.util.Map; +import java.util.NoSuchElementException; import java.util.function.Consumer; import java.util.stream.Stream; - import javax.inject.Inject; +import javax.mail.Flags; import org.apache.james.jmap.exceptions.MessageNotFoundException; import org.apache.james.jmap.model.ClientId; import org.apache.james.jmap.model.MessageId; +import org.apache.james.jmap.model.MessageProperties; 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.UpdateMessagePatch; import org.apache.james.mailbox.MailboxSession; +import org.apache.james.mailbox.MessageManager; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.model.MessageRange; +import org.apache.james.mailbox.store.FlagsUpdateCalculator; import org.apache.james.mailbox.store.MailboxSessionMapperFactory; import org.apache.james.mailbox.store.mail.MailboxMapperFactory; import org.apache.james.mailbox.store.mail.MessageMapper; @@ -44,9 +50,11 @@ import org.apache.james.mailbox.store.mail.model.MailboxId; import org.apache.james.mailbox.store.mail.model.MailboxMessage; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Preconditions; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +import com.google.common.collect.ImmutableSet; public class SetMessagesMethod<Id extends MailboxId> implements Method { @@ -94,9 +102,65 @@ public class SetMessagesMethod<Id extends MailboxId> implements Method { private SetMessagesResponse setMessagesResponse(SetMessagesRequest request, MailboxSession mailboxSession) throws MailboxException { SetMessagesResponse.Builder responseBuilder = SetMessagesResponse.builder(); processDestroy(request.getDestroy(), mailboxSession, responseBuilder); + processUpdates(request.getUpdate(), mailboxSession, responseBuilder); return responseBuilder.build(); } + private void processUpdates(Map<MessageId, UpdateMessagePatch> mapOfMessagePatchesById, MailboxSession mailboxSession, + SetMessagesResponse.Builder responseBuilder) { + mapOfMessagePatchesById.entrySet().stream() + .filter(kv -> kv.getValue().isValid()) + .forEach(kv -> update(kv.getKey(), kv.getValue(), mailboxSession, responseBuilder)); + } + + private void update(MessageId messageId, UpdateMessagePatch updateMessagePatch, MailboxSession mailboxSession, SetMessagesResponse.Builder builder){ + try { + MessageMapper<Id> messageMapper = mailboxSessionMapperFactory.createMessageMapper(mailboxSession); + Mailbox<Id> mailbox = mailboxMapperFactory.getMailboxMapper(mailboxSession).findMailboxByPath(messageId.getMailboxPath(mailboxSession)); + Iterator<MailboxMessage<Id>> mailboxMessage = messageMapper.findInMailbox(mailbox, MessageRange.one(messageId.getUid()), FetchType.Metadata, LIMIT_BY_ONE); + MailboxMessage<Id> messageWithUpdatedFlags = applyMessagePatch(messageId, mailboxMessage.next(), updateMessagePatch, builder); + savePatchedMessage(mailbox, messageId, messageWithUpdatedFlags, messageMapper); + } catch(NoSuchElementException e) { + addMessageIdNotFoundToResponse(messageId, builder); + } catch(MailboxException e) { + handleMessageUpdateException(messageId, builder, e); + } + } + + private void handleMessageUpdateException(MessageId messageId, SetMessagesResponse.Builder builder, MailboxException e) { + LOGGER.error("An error occurred when updating a message", e); + builder.notUpdated(ImmutableMap.of(messageId, SetError.builder() + .type("anErrorOccurred") + .description("An error occurred when updating a message") + .build())); + } + + private boolean savePatchedMessage(Mailbox<Id> mailbox, MessageId messageId, + MailboxMessage<Id> message, + MessageMapper<Id> messageMapper) throws MailboxException { + return messageMapper.updateFlags(mailbox, new FlagsUpdateCalculator(message.createFlags(), + MessageManager.FlagsUpdateMode.REPLACE), + MessageRange.one(messageId.getUid())) + .hasNext() + ; + } + + private void addMessageIdNotFoundToResponse(MessageId messageId, SetMessagesResponse.Builder builder) { + builder.notUpdated(ImmutableMap.of( messageId, + SetError.builder() + .type("notFound") + .properties(ImmutableSet.of(MessageProperties.MessageProperty.id)) + .description("message not found") + .build())); + } + + private MailboxMessage<Id> applyMessagePatch(MessageId messageId, MailboxMessage<Id> message, UpdateMessagePatch updatePatch, SetMessagesResponse.Builder builder) { + Flags newStateFlags = updatePatch.applyToState(message.isSeen(), message.isAnswered(), message.isFlagged()); + message.setFlags(newStateFlags); + builder.updated(ImmutableList.of(messageId)); + return message; + } + private void processDestroy(List<MessageId> messageIds, MailboxSession mailboxSession, SetMessagesResponse.Builder responseBuilder) throws MailboxException { MessageMapper<Id> messageMapper = mailboxSessionMapperFactory.createMessageMapper(mailboxSession); Consumer<? super MessageId> delete = delete(messageMapper, mailboxSession, responseBuilder); http://git-wip-us.apache.org/repos/asf/james-project/blob/958a96e7/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java index 9d6d158..1986df0 100644 --- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/SetMessagesRequest.java @@ -20,6 +20,7 @@ package org.apache.james.jmap.model; import java.util.List; +import java.util.Map; import java.util.Optional; import org.apache.commons.lang.NotImplementedException; @@ -29,6 +30,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; @JsonDeserialize(builder = SetMessagesRequest.Builder.class) public class SetMessagesRequest implements JmapRequest { @@ -43,12 +45,12 @@ public class SetMessagesRequest implements JmapRequest { private String accountId; private String ifInState; private ImmutableList.Builder<Message> create; - private ImmutableList.Builder<Message> update; + private ImmutableMap.Builder<MessageId, UpdateMessagePatch> update; private ImmutableList.Builder<MessageId> destroy; private Builder() { create = ImmutableList.builder(); - update = ImmutableList.builder(); + update = ImmutableMap.builder(); destroy = ImmutableList.builder(); } @@ -73,10 +75,8 @@ public class SetMessagesRequest implements JmapRequest { return this; } - public Builder update(List<Message> update) { - if (update != null && !update.isEmpty()) { - throw new NotImplementedException(); - } + public Builder update(Map<MessageId, UpdateMessagePatch> updates) { + this.update.putAll(updates); return this; } @@ -93,10 +93,10 @@ public class SetMessagesRequest implements JmapRequest { private final Optional<String> accountId; private final Optional<String> ifInState; private final List<Message> create; - private final List<Message> update; + private final Map<MessageId, UpdateMessagePatch> update; private final List<MessageId> destroy; - @VisibleForTesting SetMessagesRequest(Optional<String> accountId, Optional<String> ifInState, List<Message> create, List<Message> update, List<MessageId> destroy) { + @VisibleForTesting SetMessagesRequest(Optional<String> accountId, Optional<String> ifInState, List<Message> create, Map<MessageId, UpdateMessagePatch> update, List<MessageId> destroy) { this.accountId = accountId; this.ifInState = ifInState; this.create = create; @@ -116,7 +116,7 @@ public class SetMessagesRequest implements JmapRequest { return create; } - public List<Message> getUpdate() { + public Map<MessageId, UpdateMessagePatch> getUpdate() { return update; } http://git-wip-us.apache.org/repos/asf/james-project/blob/958a96e7/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java new file mode 100644 index 0000000..3751368 --- /dev/null +++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/model/UpdateMessagePatch.java @@ -0,0 +1,125 @@ +/**************************************************************** + * Licensed to the Apache Software Foundation (ASF) under one * + * or more contributor license agreements. See the NOTICE file * + * distributed with this work for additional information * + * regarding copyright ownership. The ASF licenses this file * + * to you under the Apache License, Version 2.0 (the * + * "License"); you may not use this file except in compliance * + * with the License. You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, * + * software distributed under the License is distributed on an * + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * + * KIND, either express or implied. See the License for the * + * specific language governing permissions and limitations * + * under the License. * + ****************************************************************/ + +package org.apache.james.jmap.model; + +import java.util.List; +import java.util.Optional; + +import javax.mail.Flags; + +import org.apache.commons.lang.NotImplementedException; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import com.google.common.annotations.VisibleForTesting; +import com.google.common.collect.ImmutableList; + +@JsonDeserialize(builder = UpdateMessagePatch.Builder.class) +public class UpdateMessagePatch { + + public static Builder builder() { + return new Builder(); + } + + @JsonPOJOBuilder(withPrefix = "") + public static class Builder { + private ImmutableList.Builder<String> mailboxIds = ImmutableList.builder(); + private Optional<Boolean> isFlagged = Optional.empty(); + private Optional<Boolean> isUnread = Optional.empty(); + private Optional<Boolean> isAnswered = Optional.empty(); + + public Builder mailboxIds(Optional<List<String>> mailboxIds) { + if (mailboxIds.isPresent()) { + throw new NotImplementedException(); + } + return this; + } + + public Builder isFlagged(Optional<Boolean> isFlagged) { + this.isFlagged = isFlagged; + return this; + } + + public Builder isUnread(Optional<Boolean> isUnread) { + this.isUnread = isUnread; + return this; + } + + public Builder isAnswered(Optional<Boolean> isAnswered) { + this.isAnswered = isAnswered; + return this; + } + + public UpdateMessagePatch build() { + + return new UpdateMessagePatch(mailboxIds.build(), isUnread, isFlagged, isAnswered); + } + } + + private final List<String> mailboxIds; + private final Optional<Boolean> isUnread; + private final Optional<Boolean> isFlagged; + private final Optional<Boolean> isAnswered; + + @VisibleForTesting + UpdateMessagePatch(List<String> mailboxIds, + Optional<Boolean> isUnread, + Optional<Boolean> isFlagged, + Optional<Boolean> isAnswered) { + + this.mailboxIds = mailboxIds; + this.isUnread = isUnread; + this.isFlagged = isFlagged; + this.isAnswered = isAnswered; + } + + public List<String> getMailboxIds() { + return mailboxIds; + } + + public Optional<Boolean> isUnread() { + return isUnread; + } + + public Optional<Boolean> isFlagged() { + return isFlagged; + } + + public Optional<Boolean> isAnswered() { + return isAnswered; + } + + public boolean isValid() { + return true; // to be implemented when UpdateMessagePatch would allow any message property to be set + } + + public Flags applyToState(boolean isSeen, boolean isAnswered, boolean isFlagged) { + Flags newStateFlags = new Flags(); + if (!isSeen && isUnread().isPresent() && !isUnread().get()) { + newStateFlags.add(Flags.Flag.SEEN); + } + if (!isAnswered && isAnswered().isPresent() && isAnswered().get()) { + newStateFlags.add(Flags.Flag.ANSWERED); + } + if (!isFlagged && isFlagged().isPresent() && isFlagged().get()) { + newStateFlags.add(Flags.Flag.FLAGGED); + } + return newStateFlags; + } +} http://git-wip-us.apache.org/repos/asf/james-project/blob/958a96e7/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesRequestTest.java ---------------------------------------------------------------------- diff --git a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesRequestTest.java b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesRequestTest.java index b600172..0a079b6 100644 --- a/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesRequestTest.java +++ b/server/protocols/jmap/src/test/java/org/apache/james/jmap/model/SetMessagesRequestTest.java @@ -62,32 +62,16 @@ public class SetMessagesRequestTest { } @Test - public void builderShouldThrowWhenUpdateIsNotEmpty() { - assertThatThrownBy(() -> SetMessagesRequest.builder().update(ImmutableList.of(Message.builder() - .id(MessageId.of("user|create|1")) - .blobId("blobId") - .threadId("threadId") - .mailboxIds(ImmutableList.of("mailboxId")) - .headers(ImmutableMap.of("key", "value")) - .subject("subject") - .size(123) - .date(ZonedDateTime.now()) - .preview("preview") - .build()))) - .isInstanceOf(NotImplementedException.class); - } - - @Test public void builderShouldWork() { ImmutableList<MessageId> destroy = ImmutableList.of(MessageId.of("user|destroy|1")); - SetMessagesRequest expected = new SetMessagesRequest(Optional.empty(), Optional.empty(), ImmutableList.of(), ImmutableList.of(), destroy); + SetMessagesRequest expected = new SetMessagesRequest(Optional.empty(), Optional.empty(), ImmutableList.of(), ImmutableMap.of(), destroy); SetMessagesRequest setMessagesRequest = SetMessagesRequest.builder() .accountId(null) .ifInState(null) .create(ImmutableList.of()) - .update(ImmutableList.of()) + .update(ImmutableMap.of()) .destroy(destroy) .build(); --------------------------------------------------------------------- To unsubscribe, e-mail: server-dev-unsubscr...@james.apache.org For additional commands, e-mail: server-dev-h...@james.apache.org