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 2023/03/21 02:17:46 UTC

[james-project] branch master updated: [FIX] Subscribe/unsubscribe mailbox should be per user based (#1491)

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


The following commit(s) were added to refs/heads/master by this push:
     new a8c97061d5 [FIX] Subscribe/unsubscribe mailbox should be per user based (#1491)
a8c97061d5 is described below

commit a8c97061d5f4d6cf40da607eee3bb3a3c15d0c4a
Author: Rene Cordier <rc...@linagora.com>
AuthorDate: Tue Mar 21 09:17:40 2023 +0700

    [FIX] Subscribe/unsubscribe mailbox should be per user based (#1491)
---
 .../james/jmap/api/change/MailboxChange.java       |  36 +---
 .../contract/MailboxChangesMethodContract.scala    | 230 ++++++++++++++++++++-
 .../james/jmap/change/MailboxChangeListener.scala  |   6 +-
 3 files changed, 232 insertions(+), 40 deletions(-)

diff --git a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
index f8cc6b02c3..231dfd98e7 100644
--- a/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
+++ b/server/data/data-jmap/src/main/java/org/apache/james/jmap/api/change/MailboxChange.java
@@ -159,52 +159,24 @@ public class MailboxChange implements JmapChange {
                 .collect(ImmutableList.toImmutableList());
         }
 
-        public List<JmapChange> fromMailboxSubscribed(List<AccountId> sharees, MailboxEvents.MailboxSubscribedEvent mailboxSubscribedEvent,
-                                                      ZonedDateTime now) {
-            MailboxChange ownerChange = MailboxChange.builder()
+        public JmapChange fromMailboxSubscribed(MailboxEvents.MailboxSubscribedEvent mailboxSubscribedEvent, ZonedDateTime now) {
+            return MailboxChange.builder()
                 .accountId(AccountId.fromUsername(mailboxSubscribedEvent.getUsername()))
                 .state(stateFactory.generate())
                 .date(now)
                 .isCountChange(false)
                 .updated(ImmutableList.of(mailboxSubscribedEvent.getMailboxId()))
                 .build();
-
-            Stream<MailboxChange> shareeChanges = sharees.stream()
-                .map(shareeId -> MailboxChange.builder()
-                    .accountId(shareeId)
-                    .state(stateFactory.generate())
-                    .date(now)
-                    .isCountChange(false)
-                    .updated(ImmutableList.of(mailboxSubscribedEvent.getMailboxId()))
-                    .delegated()
-                    .build());
-
-            return Stream.concat(Stream.of(ownerChange), shareeChanges)
-                .collect(ImmutableList.toImmutableList());
         }
 
-        public List<JmapChange> fromMailboxUnSubscribed(List<AccountId> sharees, MailboxEvents.MailboxUnsubscribedEvent mailboxUnsubscribedEvent,
-                                                      ZonedDateTime now) {
-            MailboxChange ownerChange = MailboxChange.builder()
+        public JmapChange fromMailboxUnSubscribed(MailboxEvents.MailboxUnsubscribedEvent mailboxUnsubscribedEvent, ZonedDateTime now) {
+            return MailboxChange.builder()
                 .accountId(AccountId.fromUsername(mailboxUnsubscribedEvent.getUsername()))
                 .state(stateFactory.generate())
                 .date(now)
                 .isCountChange(false)
                 .updated(ImmutableList.of(mailboxUnsubscribedEvent.getMailboxId()))
                 .build();
-
-            Stream<MailboxChange> shareeChanges = sharees.stream()
-                .map(shareeId -> MailboxChange.builder()
-                    .accountId(shareeId)
-                    .state(stateFactory.generate())
-                    .date(now)
-                    .isCountChange(false)
-                    .updated(ImmutableList.of(mailboxUnsubscribedEvent.getMailboxId()))
-                    .delegated()
-                    .build());
-
-            return Stream.concat(Stream.of(ownerChange), shareeChanges)
-                .collect(ImmutableList.toImmutableList());
         }
 
         public List<JmapChange> fromMailboxACLUpdated(MailboxACLUpdated mailboxACLUpdated, ZonedDateTime now, List<AccountId> sharees) {
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/MailboxChangesMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
index 24442ab51c..bec088808e 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/MailboxChangesMethodContract.scala
@@ -19,14 +19,10 @@
 
 package org.apache.james.jmap.rfc8621.contract
 
-import java.nio.charset.StandardCharsets
-import java.util.concurrent.TimeUnit
-
 import io.netty.handler.codec.http.HttpHeaderNames.ACCEPT
 import io.restassured.RestAssured.{`given`, requestSpecification}
 import io.restassured.builder.ResponseSpecBuilder
 import io.restassured.http.ContentType.JSON
-import javax.mail.Flags
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER
 import net.javacrumbs.jsonunit.core.internal.Options
@@ -50,6 +46,10 @@ import org.awaitility.Durations.ONE_HUNDRED_MILLISECONDS
 import org.junit.jupiter.api.{BeforeEach, Nested, Test}
 import play.api.libs.json.{JsArray, JsString, Json}
 
+import java.nio.charset.StandardCharsets
+import java.util.concurrent.TimeUnit
+import javax.mail.Flags
+
 trait MailboxChangesMethodContract {
 
   private lazy val slowPacedPollInterval = ONE_HUNDRED_MILLISECONDS
@@ -1023,6 +1023,228 @@ trait MailboxChangesMethodContract {
       }
     }
 
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenSubscribedOnlyPerUser(server: GuiceJamesServer): Unit = {
+      val accountId: AccountId = AccountId.fromUsername(BOB)
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+      val provisioningState: State = provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val oldStateBob: State = waitForNextState(server, accountId, provisioningState)
+      val oldStateAndre: State = waitForNextStateWithDelegation(server, AccountId.fromUsername(ANDRE), State.INITIAL)
+
+      // change subscription
+      JmapRequests.subscribe(mailboxId)
+
+      awaitAtMostTenSeconds.untilAsserted { () =>
+        val response = `given`
+          .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .body(
+            s"""{
+               |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
+               |  "methodCalls": [[
+               |    "Mailbox/changes",
+               |    {
+               |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+               |      "sinceState": "${oldStateBob.getValue}"
+               |    },
+               |    "c1"]]
+               |}""".stripMargin)
+        .when
+          .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+        assertThatJson(response)
+          .whenIgnoringPaths("methodResponses[0][1].newState")
+          .withOptions(new Options(IGNORING_ARRAY_ORDER))
+          .isEqualTo(
+            s"""{
+               |    "sessionState": "${SESSION_STATE.value}",
+               |    "methodResponses": [
+               |      [ "Mailbox/changes", {
+               |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+               |        "oldState": "${oldStateBob.getValue}",
+               |        "hasMoreChanges": false,
+               |        "updatedProperties": null,
+               |        "created": [],
+               |        "updated": ["$mailboxId"],
+               |        "destroyed": []
+               |      }, "c1"]
+               |    ]
+               |}""".stripMargin)
+      }
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldStateAndre.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      Thread.sleep(1000)
+
+      val responseAndre = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(responseAndre)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldStateAndre.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": null,
+             |        "created": [],
+             |        "updated": [],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
+    @Test
+    def mailboxChangesShouldReturnUpdatedChangesWhenUnsubscribedOnlyPerUser(server: GuiceJamesServer): Unit = {
+      val accountId: AccountId = AccountId.fromUsername(BOB)
+      val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
+      val provisioningState: State = provisionSystemMailboxes(server)
+
+      val path = MailboxPath.forUser(BOB, "mailbox1")
+      val mailboxId: String = mailboxProbe
+        .createMailbox(path)
+        .serialize
+
+      server.getProbe(classOf[ACLProbeImpl])
+        .replaceRights(path, ANDRE.asString, new MailboxACL.Rfc4314Rights(Right.Lookup, Right.Read))
+
+      val oldStateBob: State = waitForNextState(server, accountId, provisioningState)
+      val oldStateAndre: State = waitForNextStateWithDelegation(server, AccountId.fromUsername(ANDRE), State.INITIAL)
+
+      // change subscription
+      JmapRequests.unSubscribe(mailboxId)
+
+      awaitAtMostTenSeconds.untilAsserted { () =>
+        val response = `given`
+          .header(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .body(
+            s"""{
+               |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
+               |  "methodCalls": [[
+               |    "Mailbox/changes",
+               |    {
+               |      "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+               |      "sinceState": "${oldStateBob.getValue}"
+               |    },
+               |    "c1"]]
+               |}""".stripMargin)
+        .when
+          .post
+        .`then`
+          .statusCode(SC_OK)
+          .contentType(JSON)
+          .extract
+          .body
+          .asString
+
+        assertThatJson(response)
+          .whenIgnoringPaths("methodResponses[0][1].newState")
+          .withOptions(new Options(IGNORING_ARRAY_ORDER))
+          .isEqualTo(
+            s"""{
+               |    "sessionState": "${SESSION_STATE.value}",
+               |    "methodResponses": [
+               |      [ "Mailbox/changes", {
+               |        "accountId": "29883977c13473ae7cb7678ef767cbfbaffc8a44a6e463d971d23a65c1dc4af6",
+               |        "oldState": "${oldStateBob.getValue}",
+               |        "hasMoreChanges": false,
+               |        "updatedProperties": null,
+               |        "created": [],
+               |        "updated": ["$mailboxId"],
+               |        "destroyed": []
+               |      }, "c1"]
+               |    ]
+               |}""".stripMargin)
+      }
+
+      val request =
+        s"""{
+           |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail", "urn:apache:james:params:jmap:mail:shares"],
+           |  "methodCalls": [[
+           |    "Mailbox/changes",
+           |    {
+           |      "accountId": "$ANDRE_ACCOUNT_ID",
+           |      "sinceState": "${oldStateAndre.getValue}"
+           |    },
+           |    "c1"]]
+           |}""".stripMargin
+
+      Thread.sleep(1000)
+
+      val responseAndre = `given`(
+        baseRequestSpecBuilder(server)
+          .setAuth(authScheme(UserCredential(ANDRE, ANDRE_PASSWORD)))
+          .addHeader(ACCEPT.toString, ACCEPT_RFC8621_VERSION_HEADER)
+          .setBody(request)
+          .build, new ResponseSpecBuilder().build)
+        .post
+      .`then`
+        .statusCode(SC_OK)
+        .contentType(JSON)
+        .extract
+        .body
+        .asString
+
+      assertThatJson(responseAndre)
+        .whenIgnoringPaths("methodResponses[0][1].newState")
+        .withOptions(new Options(IGNORING_ARRAY_ORDER))
+        .isEqualTo(
+          s"""{
+             |    "sessionState": "${SESSION_STATE.value}",
+             |    "methodResponses": [
+             |      [ "Mailbox/changes", {
+             |        "accountId": "$ANDRE_ACCOUNT_ID",
+             |        "oldState": "${oldStateAndre.getValue}",
+             |        "hasMoreChanges": false,
+             |        "updatedProperties": null,
+             |        "created": [],
+             |        "updated": [],
+             |        "destroyed": []
+             |      }, "c1"]
+             |    ]
+             |}""".stripMargin)
+    }
+
     @Test
     def mailboxChangesShouldNotReturnUpdatedChangesWhenMissingSharesCapability(server: GuiceJamesServer): Unit = {
       val mailboxProbe: MailboxProbeImpl = server.getProbe(classOf[MailboxProbeImpl])
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
index 9165342cf7..04ff153801 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
@@ -91,11 +91,9 @@ case class MailboxChangeListener @Inject() (@Named(InjectionKeys.JMAP) eventBus:
           .flatMapMany(sharees =>
             SFlux(emailChangeFactory.fromExpunged(expunged, now, sharees.map(_.getIdentifier).map(Username.of).asJava)))
       case subscribed: MailboxEvents.MailboxSubscribedEvent =>
-        getSharees(mailboxId, username)
-          .flatMapIterable(sharees => mailboxChangeFactory.fromMailboxSubscribed(sharees.asJava, subscribed, now).asScala)
+        SFlux.just(mailboxChangeFactory.fromMailboxSubscribed(subscribed, now))
       case unSubscribed: MailboxEvents.MailboxUnsubscribedEvent =>
-        getSharees(mailboxId, username)
-          .flatMapIterable(sharees => mailboxChangeFactory.fromMailboxUnSubscribed(sharees.asJava, unSubscribed, now).asScala)
+        SFlux.just(mailboxChangeFactory.fromMailboxUnSubscribed(unSubscribed, now))
     }
   }
 


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