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

Reply via email to