You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2020/10/20 02:54:12 UTC

[james-project] branch master updated (5725467 -> cfc6b0d)

This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a change to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git.


    from 5725467  JAMES-3414 MailboxIds partial updates patch validation
     new f7aa673  JAMES-3411 Email/set update keywords
     new b887525  JAMES-3148 Fix instability in CassandraMailboxManagerTest
     new 866cf73  JAMES-3407 Read repair: improve build stability
     new f70662c  JAMES-3407 Disable read repairs upon Ghost mailbox fixing
     new 3f9397b  JAMES-2891 CORS Options should be supported for JMAP session endpoint
     new cfc6b0d  JAMES-2891 redirect to JMAP session resource from /.well-known/jmap

The 6 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 .../mailbox/cassandra/DeleteMessageListener.java   |  29 +-
 .../cassandra/mail/CassandraMailboxMapper.java     |   2 +-
 .../rfc8621/contract/EmailSetMethodContract.scala  | 550 ++++++++++++++++++++-
 .../org/apache/james/jmap/http/SessionRoutes.scala |  15 +-
 .../james/jmap/json/EmailSetSerializer.scala       |  34 +-
 .../org/apache/james/jmap/mail/EmailSet.scala      |  27 +-
 .../apache/james/jmap/method/EmailSetMethod.scala  |  30 +-
 .../apache/james/jmap/http/SessionRoutesTest.scala |  16 +
 .../java/org/apache/james/jmap/JMAPRoutes.java     |   5 +
 .../rabbitmq/FixingGhostMailboxTest.java           |   9 +-
 10 files changed, 685 insertions(+), 32 deletions(-)


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


[james-project] 02/06: JAMES-3148 Fix instability in CassandraMailboxManagerTest

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit b887525f6b24815abaa4db850963d5becd0d3e98
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Oct 19 11:40:35 2020 +0700

    JAMES-3148 Fix instability in CassandraMailboxManagerTest
---
 .../mailbox/cassandra/DeleteMessageListener.java   | 29 ++++++++++++----------
 1 file changed, 16 insertions(+), 13 deletions(-)

diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java
index d4a3ea7..fa70727 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java
@@ -142,17 +142,20 @@ public class DeleteMessageListener implements MailboxListener.GroupMailboxListen
     }
 
     private Mono<Void> handleMailboxDeletion(CassandraId mailboxId) {
-        return messageIdDAO.retrieveMessages(mailboxId, MessageRange.all(), Limit.unlimited())
-            .map(ComposedMessageIdWithMetaData::getComposedMessageId)
-            .concatMap(metadata -> handleMessageDeletionAsPartOfMailboxDeletion((CassandraMessageId) metadata.getMessageId(), mailboxId)
-                .then(imapUidDAO.delete((CassandraMessageId) metadata.getMessageId(), mailboxId))
-                .then(messageIdDAO.delete(mailboxId, metadata.getUid())))
-            .then(deleteAcl(mailboxId))
-            .then(applicableFlagDAO.delete(mailboxId))
-            .then(firstUnseenDAO.removeAll(mailboxId))
-            .then(deletedMessageDAO.removeAll(mailboxId))
-            .then(counterDAO.delete(mailboxId))
-            .then(recentsDAO.delete(mailboxId));
+        int prefetch = 1;
+        return Flux.mergeDelayError(prefetch,
+                messageIdDAO.retrieveMessages(mailboxId, MessageRange.all(), Limit.unlimited())
+                    .map(ComposedMessageIdWithMetaData::getComposedMessageId)
+                    .concatMap(metadata -> handleMessageDeletionAsPartOfMailboxDeletion((CassandraMessageId) metadata.getMessageId(), mailboxId)
+                        .then(imapUidDAO.delete((CassandraMessageId) metadata.getMessageId(), mailboxId))
+                        .then(messageIdDAO.delete(mailboxId, metadata.getUid()))),
+                deleteAcl(mailboxId),
+                applicableFlagDAO.delete(mailboxId),
+                firstUnseenDAO.removeAll(mailboxId),
+                deletedMessageDAO.removeAll(mailboxId),
+                counterDAO.delete(mailboxId),
+                recentsDAO.delete(mailboxId))
+            .then();
     }
 
     private Mono<Void> handleMessageDeletion(Expunged expunged) {
@@ -166,8 +169,8 @@ public class DeleteMessageListener implements MailboxListener.GroupMailboxListen
 
     private Mono<Void> deleteAcl(CassandraId mailboxId) {
         return aclMapper.getACL(mailboxId)
-            .flatMap(acl -> rightsDAO.update(mailboxId, ACLDiff.computeDiff(acl, MailboxACL.EMPTY)))
-            .then(aclMapper.delete(mailboxId));
+            .flatMap(acl -> rightsDAO.update(mailboxId, ACLDiff.computeDiff(acl, MailboxACL.EMPTY))
+                .then(aclMapper.delete(mailboxId)));
     }
 
     private Mono<Void> handleMessageDeletion(CassandraMessageId messageId) {


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


[james-project] 05/06: JAMES-2891 CORS Options should be supported for JMAP session endpoint

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 3f9397b21844276eb19adeb16752841a4a2a23fb
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Oct 19 08:49:48 2020 +0700

    JAMES-2891 CORS Options should be supported for JMAP session endpoint
---
 .../src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala  | 2 +-
 .../test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala  | 7 +++++++
 2 files changed, 8 insertions(+), 1 deletion(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
index a15726d..c087ed9 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
@@ -65,7 +65,7 @@ class SessionRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator:
         .action(generateSession)
         .corsHeaders,
       JMAPRoute.builder()
-        .endpoint(new Endpoint(HttpMethod.OPTIONS, AUTHENTICATION))
+        .endpoint(new Endpoint(HttpMethod.OPTIONS, JMAP_SESSION))
         .action(CORS_CONTROL)
         .noCorsHeaders)
 
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
index 2db8d50..dc7895a 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
@@ -92,6 +92,13 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
       .contentType(ContentType.JSON)
   }
 
+  "options" should "return OK status" in {
+    RestAssured.when()
+      .options
+    .`then`
+      .statusCode(HttpStatus.SC_OK)
+  }
+
   "get" should "return correct session" in {
     val sessionJson = RestAssured.`with`()
         .get


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


[james-project] 04/06: JAMES-3407 Disable read repairs upon Ghost mailbox fixing

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit f70662c3fdc7a615f9358e93ee533c4f8731b4b0
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Oct 19 15:31:42 2020 +0700

    JAMES-3407 Disable read repairs upon Ghost mailbox fixing
    
    Read repairs caused already fixed inconsistencies to be fixed again, leading to unexpected results.
---
 .../webadmin/integration/rabbitmq/FixingGhostMailboxTest.java    | 9 ++++++++-
 1 file changed, 8 insertions(+), 1 deletion(-)

diff --git a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java
index be3e26b..0581f9b 100644
--- a/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java
+++ b/server/protocols/webadmin-integration-test/distributed-webadmin-integration-test/src/test/java/org/apache/james/webadmin/integration/rabbitmq/FixingGhostMailboxTest.java
@@ -51,6 +51,7 @@ import org.apache.james.JamesServerBuilder;
 import org.apache.james.JamesServerExtension;
 import org.apache.james.SearchConfiguration;
 import org.apache.james.backends.cassandra.init.ClusterFactory;
+import org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration;
 import org.apache.james.backends.cassandra.init.configuration.CassandraConsistenciesConfiguration;
 import org.apache.james.backends.cassandra.init.configuration.ClusterConfiguration;
 import org.apache.james.core.Username;
@@ -128,7 +129,13 @@ class FixingGhostMailboxTest {
         .extension(new RabbitMQExtension())
         .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration)
             .overrideWith(new TestJMAPServerModule())
-            .overrideWith(new WebadminIntegrationTestModule()))
+            .overrideWith(new WebadminIntegrationTestModule())
+            .overrideWith(binder -> binder.bind(CassandraConfiguration.class)
+                .toInstance(CassandraConfiguration.builder()
+                    .mailboxReadRepair(0)
+                    .mailboxCountersReadRepairMax(0)
+                    .mailboxCountersReadRepairChanceOneHundred(0)
+                    .build())))
         .build();
 
     private AccessToken accessToken;


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


[james-project] 03/06: JAMES-3407 Read repair: improve build stability

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 866cf73c139e917d75de136ca2fff6dd2510c1c7
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Mon Oct 19 11:59:00 2020 +0700

    JAMES-3407 Read repair: improve build stability
    
    Do not perform read repair upon read fallback to previous tables as it fails
---
 .../org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java
index e918b8e..953f1c1 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMailboxMapper.java
@@ -190,7 +190,7 @@ public class CassandraMailboxMapper implements MailboxMapper {
         return mailboxPathV2DAO.retrieveId(path)
             .switchIfEmpty(mailboxPathDAO.retrieveId(path))
             .map(CassandraIdAndPath::getCassandraId)
-            .flatMap(this::retrieveMailbox)
+            .flatMap(mailboxDAO::retrieveMailbox)
             .flatMap(this::migrate);
     }
 


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


[james-project] 01/06: JAMES-3411 Email/set update keywords

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit f7aa673640acdee20d3f538fc7a379735cd1b241
Author: duc91 <vd...@linagora.com>
AuthorDate: Tue Oct 13 17:26:03 2020 +0700

    JAMES-3411 Email/set update keywords
---
 .../rfc8621/contract/EmailSetMethodContract.scala  | 550 ++++++++++++++++++++-
 .../james/jmap/json/EmailSetSerializer.scala       |  34 +-
 .../org/apache/james/jmap/mail/EmailSet.scala      |  27 +-
 .../apache/james/jmap/method/EmailSetMethod.scala  |  30 +-
 4 files changed, 626 insertions(+), 15 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
index 758d6ac..3267358 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/EmailSetMethodContract.scala
@@ -18,25 +18,34 @@
  ****************************************************************/
 package org.apache.james.jmap.rfc8621.contract
 
+import java.io.ByteArrayInputStream
 import java.nio.charset.StandardCharsets
+import java.util.Date
 
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.http.ContentType.JSON
+import javax.mail.Flags
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
 import org.apache.james.jmap.draft.{JmapGuiceProbe, MessageIdProbe}
 import org.apache.james.jmap.http.UserCredential
-import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.jmap.rfc8621.contract.Fixture.{ACCEPT_RFC8621_VERSION_HEADER, ACCOUNT_ID, ANDRE, ANDRE_PASSWORD, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder}
+import org.apache.james.mailbox.FlagsBuilder
 import org.apache.james.mailbox.MessageManager.AppendCommand
 import org.apache.james.mailbox.model.MailboxACL.Right
-import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, MessageId}
+import org.apache.james.mailbox.model.{ComposedMessageId, MailboxACL, MailboxConstants, MailboxId, MailboxPath, MessageId}
+import org.apache.james.mailbox.probe.MailboxProbe
 import org.apache.james.mime4j.dom.Message
 import org.apache.james.modules.{ACLProbeImpl, MailboxProbeImpl}
 import org.apache.james.utils.DataProbeImpl
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.{BeforeEach, Test}
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.ValueSource
+
+import scala.jdk.CollectionConverters._
 
 trait EmailSetMethodContract {
   @BeforeEach
@@ -55,6 +64,543 @@ trait EmailSetMethodContract {
   def randomMessageId: MessageId
 
   @Test
+  def shouldResetKeywords(server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords": {
+         |             "music": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["${messageId.serialize}"],
+         |       "properties": ["keywords"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list[0]")
+      .isEqualTo(String.format(
+        """{
+          |   "id":"%s",
+          |   "keywords": {
+          |     "music": true
+          |   }
+          |}
+      """.stripMargin, messageId.serialize))
+  }
+
+  @Test
+  def shouldNotResetKeywordWhenFalseValue(server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords": {
+         |             "music": true,
+         |             "movie": false
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
+      .isEqualTo(
+        """|{
+          |   "type":"invalidPatch",
+          |   "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(keyword value can only be true),ArraySeq()))))),ArraySeq()))))"
+          |}""".stripMargin)
+  }
+
+  @Test
+  def shouldNotResetKeywordWhenInvalidKeyword(server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords": {
+         |             "mus*c": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath(s"methodResponses[0][1].notUpdated.${messageId.serialize}")
+      .isEqualTo(
+        """|{
+           |   "type":"invalidPatch",
+           |   "description": "Message 1 update is invalid: List((,List(JsonValidationError(List(Value associated with keywords is invalid: List((,List(JsonValidationError(List(FlagName must not be null or empty, must have length form 1-255,must not contain characters with hex from '\\u0000' to '\\u00019' or {'(' ')' '{' ']' '%' '*' '\"' '\\'} ),ArraySeq()))))),ArraySeq()))))"
+           |}""".stripMargin)
+  }
+
+  @ParameterizedTest
+  @ValueSource(strings = Array(
+    "$Recent",
+    "$Deleted"
+  ))
+  def shouldNotResetNonExposedKeyword(unexposedKeyword: String, server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request = String.format(
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords": {
+         |             "music": true,
+         |             "$unexposedKeyword": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notUpdated")
+      .isEqualTo(
+        s"""{
+           |  "${messageId.serialize}":{
+           |      "type":"invalidPatch",
+           |      "description":"Message 1 update is invalid: Does not allow to update 'Deleted' or 'Recent' flag"}
+           |  }
+           |}"""
+          .stripMargin)
+  }
+
+  @Test
+  def shouldKeepUnexposedKeywordWhenResetKeywords(server: GuiceJamesServer): Unit = {
+    val mailboxProbe: MailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
+    mailboxProbe.createMailbox(MailboxConstants.USER_NAMESPACE, BOB.asString(), "mailbox");
+
+    val bobPath = MailboxPath.forUser(BOB, "mailbox")
+    val message: ComposedMessageId = mailboxProbe.appendMessage(BOB.asString, bobPath,
+      new ByteArrayInputStream("Subject: test\r\n\r\ntestmail".getBytes(StandardCharsets.UTF_8)),
+      new Date, false, new Flags(Flags.Flag.DELETED))
+
+    val messageId: String = message.getMessageId.serialize
+
+    val request = String.format(s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "$messageId":{
+         |          "keywords": {
+         |             "music": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin)
+
+    `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+
+    val flags: List[Flags] = server.getProbe(classOf[MessageIdProbe]).getMessages(message.getMessageId, BOB).asScala.map(m => m.getFlags).toList
+    val expectedFlags: Flags  = FlagsBuilder.builder.add("music").add(Flags.Flag.DELETED).build
+
+    assertThat(flags.asJava)
+      .containsExactly(expectedFlags)
+  }
+
+  @Test
+  def shouldResetKeywordsWhenNotDefault(server: GuiceJamesServer): Unit = {
+    val message: Message = Fixture.createTestMessage
+
+    val flags: Flags = new Flags(Flags.Flag.ANSWERED)
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl]).appendMessage(BOB.asString(), bobPath, AppendCommand.builder()
+      .withFlags(flags)
+      .build(message))
+      .getMessageId
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords": {
+         |             "music": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["${messageId.serialize}"],
+         |       "properties": ["keywords"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list[0]")
+      .isEqualTo(String.format(
+        """{
+          |   "id":"%s",
+          |   "keywords": {
+          |             "music": true
+          |    }
+          |}
+      """.stripMargin, messageId.serialize))
+  }
+
+  @Test
+  def shouldNotResetKeywordWhenInvalidMessageId(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    val request =
+      s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "invalid":{
+         |          "keywords": {
+         |             "music": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+     .inPath("methodResponses[0][1].notUpdated")
+     .isEqualTo("""{
+        | "invalid": {
+        |     "type":"invalidPatch",
+        |     "description":"Message invalid update is invalid: For input string: \"invalid\""
+        | }
+        |}""".stripMargin)
+  }
+
+  @Test
+  def shouldNotResetKeywordWhenMessageIdNonExisted(server: GuiceJamesServer): Unit = {
+    val invalidMessageId: MessageId = randomMessageId
+
+    val bobPath = MailboxPath.inbox(BOB)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    val request = s"""{
+         |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |    ["Email/set", {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${invalidMessageId.serialize}":{
+         |          "keywords": {
+         |             "music": true
+         |          }
+         |        }
+         |      }
+         |    }, "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notUpdated")
+      .isEqualTo(s"""{
+        | "${invalidMessageId.serialize}": {
+        |     "type":"notFound",
+        |     "description":"Cannot find message with messageId: ${invalidMessageId.serialize}"
+        | }
+        |}""".stripMargin)
+  }
+
+  @Test
+  def shouldNotUpdateInDelegatedMailboxesWhenReadOnly(server: GuiceJamesServer): Unit = {
+    val andreMailbox: String = "andrecustom"
+    val andrePath = MailboxPath.forUser(ANDRE, andreMailbox)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+    val message: Message = Message.Builder
+      .of
+      .setSender(BOB.asString())
+      .setFrom(ANDRE.asString())
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(ANDRE.asString, andrePath, AppendCommand.from(message))
+      .getMessageId
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(andrePath, BOB.asString, MailboxACL.Rfc4314Rights.of(Set(Right.Read, Right.Lookup).asJava))
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |  ["Email/set",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords": {
+         |             "music": true
+         |          }
+         |        }
+         |      }
+         |    },
+         |    "c1"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[0][1].notUpdated")
+      .isEqualTo(
+        s"""{
+           |  "${messageId.serialize}":{
+           |     "type": "notFound",
+           |     "description": "Mailbox not found"
+           |  }
+           |}""".stripMargin)
+  }
+
+  @Test
+  def shouldResetFlagsInDelegatedMailboxesWhenHadAtLeastWriteRight(server: GuiceJamesServer): Unit = {
+    val andreMailbox: String = "andrecustom"
+    val andrePath = MailboxPath.forUser(ANDRE, andreMailbox)
+    server.getProbe(classOf[MailboxProbeImpl]).createMailbox(andrePath)
+    val message: Message = Message.Builder
+      .of
+      .setSender(BOB.asString())
+      .setFrom(ANDRE.asString())
+      .setSubject("test")
+      .setBody("testmail", StandardCharsets.UTF_8)
+      .build
+    val messageId: MessageId = server.getProbe(classOf[MailboxProbeImpl])
+      .appendMessage(ANDRE.asString, andrePath, AppendCommand.from(message))
+      .getMessageId
+    server.getProbe(classOf[ACLProbeImpl])
+      .replaceRights(andrePath, BOB.asString, MailboxACL.Rfc4314Rights.of(Set(Right.Write, Right.Read).asJava))
+
+    val request =
+      s"""{
+         |  "using": [
+         |    "urn:ietf:params:jmap:core",
+         |    "urn:ietf:params:jmap:mail"],
+         |  "methodCalls": [
+         |  ["Email/set",
+         |    {
+         |      "accountId": "$ACCOUNT_ID",
+         |      "ids": ["${messageId.serialize}"],
+         |      "update": {
+         |        "${messageId.serialize}":{
+         |          "keywords": {
+         |             "music": true
+         |          }
+         |        }
+         |      }
+         |    },
+         |    "c1"],
+         |    ["Email/get",
+         |     {
+         |       "accountId": "$ACCOUNT_ID",
+         |       "ids": ["${messageId.serialize}"],
+         |       "properties": ["keywords"]
+         |     },
+         |     "c2"]]
+         |}""".stripMargin
+
+    val response = `given`
+      .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .inPath("methodResponses[1][1].list[0]")
+      .isEqualTo(String.format(
+        """{
+          |   "id":"%s",
+          |   "keywords": {
+          |     "music":true
+          |   }
+          |}
+      """.stripMargin, messageId.serialize))
+  }
+
+  @Test
   def emailSetShouldDestroyEmail(server: GuiceJamesServer): Unit = {
     val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl])
     mailboxProbe.createMailbox(MailboxPath.inbox(BOB))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
index 556f194..28e79c2 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/EmailSetSerializer.scala
@@ -23,7 +23,7 @@ import eu.timepit.refined.refineV
 import javax.inject.Inject
 import org.apache.james.jmap.mail.EmailSet.{UnparsedMessageId, UnparsedMessageIdConstraint}
 import org.apache.james.jmap.mail.{DestroyIds, EmailSetRequest, EmailSetResponse, EmailSetUpdate, MailboxIds}
-import org.apache.james.jmap.model.SetError
+import org.apache.james.jmap.model.{Keyword, Keywords, SetError}
 import org.apache.james.mailbox.model.{MailboxId, MessageId}
 import play.api.libs.json.{JsBoolean, JsError, JsNull, JsObject, JsResult, JsString, JsSuccess, JsValue, Json, OWrites, Reads, Writes}
 
@@ -46,6 +46,12 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
           }.headOption
             .map(_.ids)
 
+          val keywordsReset: Option[Keywords] = entries.flatMap {
+            case update: KeywordsReset => Some(update)
+            case _ => None
+          }.headOption
+            .map(_.keywords)
+
           val mailboxesToAdd: Option[MailboxIds] = Some(entries
             .flatMap {
               case update: MailboxAddition => Some(update)
@@ -62,7 +68,8 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
             .filter(_.nonEmpty)
             .map(MailboxIds)
 
-          JsSuccess(EmailSetUpdate(mailboxIds = mailboxReset,
+          JsSuccess(EmailSetUpdate(keywords = keywordsReset,
+            mailboxIds = mailboxReset,
             mailboxIdsToAdd = mailboxesToAdd,
             mailboxIdsToRemove = mailboxesToRemove))
         })
@@ -75,6 +82,10 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
           .fold(
             e => InvalidPatchEntryValue(property, e.toString()),
             MailboxReset)
+        case "keywords" => keywordsReads.reads(value)
+          .fold(
+            e => InvalidPatchEntryValue(property, e.toString()),
+            KeywordsReset)
         case name if name.startsWith(mailboxIdPrefix) => Try(mailboxIdFactory.fromString(name.substring(mailboxIdPrefix.length)))
           .fold(e => InvalidPatchEntryNameWithDetails(property, e.getMessage),
             id => value match {
@@ -108,6 +119,8 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
 
     private case class MailboxReset(ids: MailboxIds) extends EntryValidation
 
+    private case class KeywordsReset(keywords: Keywords) extends EntryValidation
+
   }
 
   private implicit val messageIdWrites: Writes[MessageId] = messageId => JsString(messageId.serialize)
@@ -140,6 +153,23 @@ class EmailSetSerializer @Inject()(messageIdFactory: MessageId.Factory, mailboxI
         case _ => JsError("Expecting a JsObject as an update entry")
       })
 
+  private implicit val keywordReads: Reads[Keyword] = {
+    case jsString: JsString => Keyword.parse(jsString.value)
+      .fold(JsError(_),
+        JsSuccess(_))
+    case _ => JsError("Expecting a string as a keyword")
+  }
+
+  private implicit val keywordsMapReads: Reads[Map[Keyword, Boolean]] =
+    readMapEntry[Keyword, Boolean](s => Keyword.parse(s),
+      {
+        case JsBoolean(true) => JsSuccess(true)
+        case JsBoolean(false) => JsError("keyword value can only be true")
+        case _ => JsError("Expecting keyword value to be a boolean")
+      })
+  private implicit val keywordsReads: Reads[Keywords] = jsValue => keywordsMapReads.reads(jsValue).map(
+    keywordsMap => Keywords(keywordsMap.keys.toSet))
+
   private implicit val unitWrites: Writes[Unit] = _ => JsNull
   private implicit val updatedWrites: Writes[Map[MessageId, Unit]] = mapWrites[MessageId, Unit](_.serialize, unitWrites)
   private implicit val notDestroyedWrites: Writes[Map[UnparsedMessageId, SetError]] = mapWrites[UnparsedMessageId, SetError](_.value, setErrorWrites)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
index ab692e5..c2ea230 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/mail/EmailSet.scala
@@ -23,12 +23,13 @@ import eu.timepit.refined.api.Refined
 import eu.timepit.refined.collection.NonEmpty
 import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
 import org.apache.james.jmap.method.WithAccountId
+import org.apache.james.jmap.model.KeywordsFactory.STRICT_KEYWORDS_FACTORY
 import org.apache.james.jmap.model.State.State
-import org.apache.james.jmap.model.{AccountId, SetError}
+import org.apache.james.jmap.model.{AccountId, Keywords, SetError}
 import org.apache.james.mailbox.model.MessageId
 import play.api.libs.json.JsObject
 
-import scala.util.Try
+import scala.util.{Failure, Right, Success, Try}
 
 object EmailSet {
   type UnparsedMessageIdConstraint = NonEmpty
@@ -56,7 +57,8 @@ case class EmailSetResponse(accountId: AccountId,
                             destroyed: Option[DestroyIds],
                             notDestroyed: Option[Map[UnparsedMessageId, SetError]])
 
-case class EmailSetUpdate(mailboxIds: Option[MailboxIds],
+case class EmailSetUpdate(keywords: Option[Keywords],
+                          mailboxIds: Option[MailboxIds],
                           mailboxIdsToAdd: Option[MailboxIds],
                           mailboxIdsToRemove: Option[MailboxIds]) {
   def validate: Either[IllegalArgumentException, ValidatedEmailSetUpdate] = if (mailboxIds.isDefined && (mailboxIdsToAdd.isDefined || mailboxIdsToRemove.isDefined)) {
@@ -75,15 +77,26 @@ case class EmailSetUpdate(mailboxIds: Option[MailboxIds],
     val mailboxIdsTransformation: Function[MailboxIds, MailboxIds] = mailboxIdsAddition
       .compose(mailboxIdsRemoval)
       .compose(mailboxIdsReset)
-    scala.Right(ValidatedEmailSetUpdate(mailboxIdsTransformation))
+    Right(mailboxIdsTransformation)
+      .flatMap(mailboxIdsTransformation => validateKeywords
+        .map(validatedKeywords => ValidatedEmailSetUpdate(validatedKeywords, mailboxIdsTransformation)))
+  }
+
+  private def validateKeywords: Either[IllegalArgumentException, Option[Keywords]] = {
+    keywords.map(_.getKeywords)
+      .map(STRICT_KEYWORDS_FACTORY.fromSet)
+      .map {
+        case Success(validatedKeywords: Keywords) => Right(Some(validatedKeywords))
+        case Failure(throwable: IllegalArgumentException) => Left(throwable)
+      }
+      .getOrElse(Right(None))
   }
 }
 
-case class ValidatedEmailSetUpdate private (mailboxIdsTransformation: Function[MailboxIds, MailboxIds])
+case class ValidatedEmailSetUpdate private (keywords: Option[Keywords],
+                                            mailboxIdsTransformation: Function[MailboxIds, MailboxIds])
 
 class EmailUpdateValidationException() extends IllegalArgumentException
 case class InvalidEmailPropertyException(property: String, cause: String) extends EmailUpdateValidationException
 case class InvalidEmailUpdateException(property: String, cause: String) extends EmailUpdateValidationException
 
-
-
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
index c7022f8..74d72b3 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailSetMethod.scala
@@ -18,8 +18,10 @@
  ****************************************************************/
 package org.apache.james.jmap.method
 
+import com.google.common.collect.ImmutableList
 import eu.timepit.refined.auto._
 import javax.inject.Inject
+import javax.mail.Flags
 import org.apache.james.jmap.http.SessionSupplier
 import org.apache.james.jmap.json.{EmailSetSerializer, ResponseSerializer}
 import org.apache.james.jmap.mail.EmailSet.UnparsedMessageId
@@ -29,6 +31,7 @@ import org.apache.james.jmap.model.DefaultCapabilities.{CORE_CAPABILITY, MAIL_CA
 import org.apache.james.jmap.model.Invocation.{Arguments, MethodName}
 import org.apache.james.jmap.model.SetError.SetErrorDescription
 import org.apache.james.jmap.model.{Capabilities, Invocation, SetError, State}
+import org.apache.james.mailbox.MessageManager.FlagsUpdateMode
 import org.apache.james.mailbox.exception.MailboxNotFoundException
 import org.apache.james.mailbox.model.{ComposedMessageIdWithMetaData, DeleteResult, MessageId}
 import org.apache.james.mailbox.{MailboxSession, MessageIdManager}
@@ -195,7 +198,7 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
         .collectMultimap(metaData => metaData.getComposedMessageId.getMessageId)
         .flatMap(metaData => {
           SFlux.fromIterable(validUpdates)
-            .flatMap[UpdateResult]({
+            .concatMap[UpdateResult]({
               case (messageId, updatePatch) =>
                 doUpdate(messageId, updatePatch, metaData.get(messageId).toList.flatten, session)
             })
@@ -208,6 +211,11 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
 
   private def doUpdate(messageId: MessageId, update: EmailSetUpdate, storedMetaData: List[ComposedMessageIdWithMetaData], session: MailboxSession): SMono[UpdateResult] = {
     val mailboxIds: MailboxIds = MailboxIds(storedMetaData.map(metaData => metaData.getComposedMessageId.getMailboxId))
+    val originFlags: Flags = storedMetaData
+      .foldLeft[Flags](new Flags())((flags: Flags, m: ComposedMessageIdWithMetaData) => {
+        flags.add(m.getFlags)
+        flags
+      })
 
     if (mailboxIds.value.isEmpty) {
       SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), MessageNotFoundExeception(messageId)))
@@ -215,9 +223,14 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
       update.validate
         .fold(
           e => SMono.just(UpdateFailure(EmailSet.asUnparsed(messageId), e)),
-          validatedUpdate => updateMailboxIds(messageId, validatedUpdate, mailboxIds, session)
-            .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
-            .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId))))
+          validatedUpdate =>
+            resetFlags(messageId, validatedUpdate, mailboxIds, originFlags, session)
+              .flatMap {
+                case failure: UpdateFailure => SMono.just[UpdateResult](failure)
+                case _: UpdateSuccess => updateMailboxIds(messageId, validatedUpdate, mailboxIds, session)
+              }
+              .onErrorResume(e => SMono.just[UpdateResult](UpdateFailure(EmailSet.asUnparsed(messageId), e)))
+              .switchIfEmpty(SMono.just[UpdateResult](UpdateSuccess(messageId))))
     }
   }
 
@@ -234,6 +247,15 @@ class EmailSetMethod @Inject()(serializer: EmailSetSerializer,
     }
   }
 
+  private def resetFlags(messageId: MessageId, update: ValidatedEmailSetUpdate, mailboxIds: MailboxIds, originalFlags: Flags, session: MailboxSession): SMono[UpdateResult] =
+    update.keywords
+      .map(keywords => keywords.asFlagsWithRecentAndDeletedFrom(originalFlags))
+      .map(flags => SMono.fromCallable(() =>
+        messageIdManager.setFlags(flags, FlagsUpdateMode.REPLACE, messageId, ImmutableList.copyOf(mailboxIds.value.asJavaCollection), session))
+        .subscribeOn(Schedulers.elastic())
+        .`then`(SMono.just[UpdateResult](UpdateSuccess(messageId))))
+      .getOrElse(SMono.just[UpdateResult](UpdateSuccess(messageId)))
+
   private def deleteMessage(destroyId: UnparsedMessageId, mailboxSession: MailboxSession): SMono[DestroyResult] =
     EmailSet.parse(messageIdFactory)(destroyId)
       .fold(e => SMono.just(DestroyFailure(destroyId, e)),


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


[james-project] 06/06: JAMES-2891 redirect to JMAP session resource from /.well-known/jmap

Posted by rc...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rcordier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit cfc6b0dbd3da81b06e767c04c498c0d71e0e4176
Author: Anton Lazarev <an...@gmail.com>
AuthorDate: Fri Oct 16 18:42:20 2020 -0400

    JAMES-2891 redirect to JMAP session resource from /.well-known/jmap
---
 .../scala/org/apache/james/jmap/http/SessionRoutes.scala    | 13 ++++++++++++-
 .../org/apache/james/jmap/http/SessionRoutesTest.scala      |  9 +++++++++
 .../src/main/java/org/apache/james/jmap/JMAPRoutes.java     |  5 +++++
 3 files changed, 26 insertions(+), 1 deletion(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
index c087ed9..10f1565 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/http/SessionRoutes.scala
@@ -29,7 +29,7 @@ import org.apache.james.jmap.HttpConstants.JSON_CONTENT_TYPE_UTF8
 import org.apache.james.jmap.JMAPRoutes.CORS_CONTROL
 import org.apache.james.jmap.JMAPUrls.AUTHENTICATION
 import org.apache.james.jmap.exceptions.UnauthorizedException
-import org.apache.james.jmap.http.SessionRoutes.{JMAP_SESSION, LOGGER}
+import org.apache.james.jmap.http.SessionRoutes.{JMAP_SESSION, WELL_KNOWN_JMAP, LOGGER}
 import org.apache.james.jmap.http.rfc8621.InjectionKeys
 import org.apache.james.jmap.json.ResponseSerializer
 import org.apache.james.jmap.model.Session
@@ -43,6 +43,7 @@ import reactor.netty.http.server.HttpServerResponse
 
 object SessionRoutes {
   private val JMAP_SESSION: String = "/jmap/session"
+  private val WELL_KNOWN_JMAP: String = "/.well-known/jmap"
   private val LOGGER = LoggerFactory.getLogger(classOf[SessionRoutes])
 }
 
@@ -58,6 +59,8 @@ class SessionRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator:
       .subscribeOn(Schedulers.elastic())
       .asJava()
 
+  private val redirectToSession: JMAPRoute.Action = JMAPRoutes.redirectTo(JMAP_SESSION)
+
   override def routes: Stream[JMAPRoute] =
     Stream.of(
       JMAPRoute.builder()
@@ -67,6 +70,14 @@ class SessionRoutes @Inject() (@Named(InjectionKeys.RFC_8621) val authenticator:
       JMAPRoute.builder()
         .endpoint(new Endpoint(HttpMethod.OPTIONS, JMAP_SESSION))
         .action(CORS_CONTROL)
+        .noCorsHeaders,
+      JMAPRoute.builder()
+        .endpoint(new Endpoint(HttpMethod.GET, WELL_KNOWN_JMAP))
+        .action(redirectToSession)
+        .corsHeaders,
+      JMAPRoute.builder()
+        .endpoint(new Endpoint(HttpMethod.OPTIONS, WELL_KNOWN_JMAP))
+        .action(CORS_CONTROL)
         .noCorsHeaders)
 
   private def sendRespond(session: Session, resp: HttpServerResponse) =
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
index dc7895a..a1a9e15 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/http/SessionRoutesTest.scala
@@ -99,6 +99,15 @@ class SessionRoutesTest extends AnyFlatSpec with BeforeAndAfter with Matchers {
       .statusCode(HttpStatus.SC_OK)
   }
 
+  "get .well-known/jmap" should "redirect" in {
+    RestAssured.`given`()
+      .basePath(".well-known/jmap")
+      .get
+    .`then`
+      .statusCode(308)
+      .header("Location", "/jmap/session")
+  }
+
   "get" should "return correct session" in {
     val sessionJson = RestAssured.`with`()
         .get
diff --git a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java
index 2908216..8c91027 100644
--- a/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java
+++ b/server/protocols/jmap/src/main/java/org/apache/james/jmap/JMAPRoutes.java
@@ -21,6 +21,7 @@ package org.apache.james.jmap;
 
 import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
 import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR;
+import static io.netty.handler.codec.http.HttpResponseStatus.PERMANENT_REDIRECT;
 import static io.netty.handler.codec.http.HttpResponseStatus.UNAUTHORIZED;
 
 import java.util.stream.Stream;
@@ -42,6 +43,10 @@ public interface JMAPRoutes {
             .header("Access-Control-Allow-Headers", "Content-Type, Authorization, Accept"));
     }
 
+    static JMAPRoute.Action redirectTo(String location) {
+        return (req, res) -> res.status(PERMANENT_REDIRECT).header("Location", location).send();
+    }
+
     default Mono<Void> handleInternalError(HttpServerResponse response, Logger logger, Throwable e) {
         logger.error("Internal server error", e);
         return response.status(INTERNAL_SERVER_ERROR).send();


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