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

[james-project] 16/17: JAMES-3460 MailboxChangeListener implementation

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

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

commit 50561bd91a3438779f16fc5cca046a8a37a73cb6
Author: LanKhuat <dl...@linagora.com>
AuthorDate: Wed Dec 2 11:39:35 2020 +0700

    JAMES-3460 MailboxChangeListener implementation
---
 .../james/jmap/api/change/MailboxChange.java       |  53 +++++
 .../james/jmap/change/MailboxChangeListener.scala  |  41 ++++
 .../jmap/change/MailboxChangeListenerTest.scala    | 232 +++++++++++++++++++++
 3 files changed, 326 insertions(+)

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 3bb849e..2ed3c64 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
@@ -19,14 +19,28 @@
 
 package org.apache.james.jmap.api.change;
 
+import java.time.Clock;
 import java.time.ZonedDateTime;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.UUID;
 
+import javax.mail.Flags;
+
 import org.apache.james.jmap.api.model.AccountId;
+import org.apache.james.mailbox.events.Event;
+import org.apache.james.mailbox.events.MailboxListener.Added;
+import org.apache.james.mailbox.events.MailboxListener.Expunged;
+import org.apache.james.mailbox.events.MailboxListener.FlagsUpdated;
+import org.apache.james.mailbox.events.MailboxListener.MailboxACLUpdated;
+import org.apache.james.mailbox.events.MailboxListener.MailboxAdded;
+import org.apache.james.mailbox.events.MailboxListener.MailboxDeletion;
+import org.apache.james.mailbox.events.MailboxListener.MailboxRenamed;
 import org.apache.james.mailbox.model.MailboxId;
 
+import com.google.common.collect.ImmutableList;
+
 public class MailboxChange {
 
     public static class State {
@@ -82,6 +96,45 @@ public class MailboxChange {
         return new MailboxChange(accountId, state, date, created, updated, destroyed);
     }
 
+    public static Optional<MailboxChange> fromEvent(Event event) {
+        ZonedDateTime now = ZonedDateTime.now(Clock.systemUTC());
+        if (event instanceof MailboxAdded) {
+            MailboxAdded mailboxAdded = (MailboxAdded) event;
+            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(mailboxAdded.getMailboxId()), ImmutableList.of(), ImmutableList.of()));
+        }
+        if (event instanceof MailboxRenamed) {
+            MailboxRenamed mailboxRenamed = (MailboxRenamed) event;
+            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxRenamed.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(mailboxRenamed.getMailboxId()), ImmutableList.of()));
+        }
+        if (event instanceof MailboxACLUpdated) {
+            MailboxACLUpdated mailboxACLUpdated = (MailboxACLUpdated) event;
+            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxACLUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(mailboxACLUpdated.getMailboxId()), ImmutableList.of()));
+        }
+        if (event instanceof MailboxDeletion) {
+            MailboxDeletion mailboxDeletion = (MailboxDeletion) event;
+            return Optional.of(MailboxChange.of(AccountId.fromUsername(mailboxDeletion.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(), ImmutableList.of(mailboxDeletion.getMailboxId())));
+        }
+        if (event instanceof Added) {
+            Added messageAdded = (Added) event;
+            return Optional.of(MailboxChange.of(AccountId.fromUsername(messageAdded.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(messageAdded.getMailboxId()), ImmutableList.of()));
+        }
+        if (event instanceof FlagsUpdated) {
+            FlagsUpdated messageFlagUpdated = (FlagsUpdated) event;
+            boolean isSeenChanged = messageFlagUpdated.getUpdatedFlags()
+                .stream()
+                .anyMatch(flags -> flags.isChanged(Flags.Flag.SEEN));
+            if (isSeenChanged) {
+                return Optional.of(MailboxChange.of(AccountId.fromUsername(messageFlagUpdated.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(messageFlagUpdated.getMailboxId()), ImmutableList.of()));
+            }
+        }
+        if (event instanceof Expunged) {
+            Expunged expunged = (Expunged) event;
+            return Optional.of(MailboxChange.of(AccountId.fromUsername(expunged.getUsername()), State.of(UUID.randomUUID()), now, ImmutableList.of(), ImmutableList.of(expunged.getMailboxId()), ImmutableList.of()));
+        }
+
+        return Optional.empty();
+    }
+
     private final AccountId accountId;
     private final State state;
     private final ZonedDateTime date;
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
new file mode 100644
index 0000000..c251fb5
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
@@ -0,0 +1,41 @@
+/****************************************************************
+ * 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.change
+
+import javax.inject.Inject
+import org.apache.james.jmap.api.change.{MailboxChange, MailboxChangeRepository}
+import org.apache.james.mailbox.events.MailboxListener.ReactiveGroupMailboxListener
+import org.apache.james.mailbox.events.{Event, Group}
+import org.reactivestreams.Publisher
+import reactor.core.scala.publisher.SMono
+
+case class MailboxChangeListenerGroup() extends Group {}
+
+case class MailboxChangeListener @Inject() (mailboxChangeRepository: MailboxChangeRepository) extends ReactiveGroupMailboxListener {
+
+  override def reactiveEvent(event: Event): Publisher[Void] =
+    MailboxChange.fromEvent(event)
+      .map(mailboxChangeRepository.save(_))
+      .orElse(SMono.empty[Void].asJava)
+
+  override def getDefaultGroup: Group = MailboxChangeListenerGroup()
+
+  override def isHandling(event: Event): Boolean = MailboxChange.fromEvent(event).isPresent
+}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
new file mode 100644
index 0000000..4c7acb8
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/MailboxChangeListenerTest.scala
@@ -0,0 +1,232 @@
+/****************************************************************
+ * 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.change
+
+import java.time.ZonedDateTime
+import java.util.UUID
+
+import javax.mail.Flags
+import org.apache.james.jmap.api.change.MailboxChange.State
+import org.apache.james.jmap.api.change.{MailboxChange, MailboxChangeRepository}
+import org.apache.james.jmap.api.model.AccountId
+import org.apache.james.jmap.change.MailboxChangeListenerTest.ACCOUNT_ID
+import org.apache.james.jmap.memory.change.MemoryMailboxChangeRepository
+import org.apache.james.mailbox.MessageManager.{AppendCommand, AppendResult, FlagsUpdateMode}
+import org.apache.james.mailbox.events.delivery.InVmEventDelivery
+import org.apache.james.mailbox.events.{InVMEventBus, MemoryEventDeadLetters, RetryBackoffConfiguration}
+import org.apache.james.mailbox.fixture.MailboxFixture.{ALICE, BOB}
+import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources
+import org.apache.james.mailbox.model.{MailboxACL, MailboxId, MailboxPath, MessageRange, TestId}
+import org.apache.james.mailbox.{MailboxManager, MailboxSessionUtil, MessageManager}
+import org.apache.james.metrics.tests.RecordingMetricFactory
+import org.assertj.core.api.Assertions.assertThat
+import org.junit.jupiter.api.{BeforeEach, Test}
+
+import scala.jdk.CollectionConverters._
+import scala.jdk.OptionConverters._
+
+object MailboxChangeListenerTest {
+  val ACCOUNT_ID = AccountId.fromUsername(BOB)
+}
+
+class MailboxChangeListenerTest {
+
+  var repository: MailboxChangeRepository = _
+  var mailboxManager: MailboxManager = _
+  var listener: MailboxChangeListener = _
+
+  @BeforeEach
+  def setUp: Unit = {
+    val resources = InMemoryIntegrationResources.builder
+      .preProvisionnedFakeAuthenticator
+      .fakeAuthorizator
+      .eventBus(new InVMEventBus(new InVmEventDelivery(new RecordingMetricFactory), RetryBackoffConfiguration.DEFAULT, new MemoryEventDeadLetters))
+      .defaultAnnotationLimits.defaultMessageParser.scanningSearchIndex.noPreDeletionHooks.storeQuotaManager
+      .build
+
+    mailboxManager = resources.getMailboxManager
+    repository = new MemoryMailboxChangeRepository()
+    listener = MailboxChangeListener(repository)
+    resources.getEventBus.register(listener)
+  }
+
+  @Test
+  def createMailboxShouldStoreCreatedEvent(): Unit = {
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val inboxId: MailboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), mailboxSession).get
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getCreated)
+      .containsExactly(inboxId)
+  }
+
+  @Test
+  def updateMailboxNameShouldStoreUpdatedEvent(): Unit = {
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.forUser(BOB, "test")
+    val newPath = MailboxPath.forUser(BOB, "another")
+    val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
+
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    mailboxManager.renameMailbox(path, newPath, mailboxSession)
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
+      .containsExactly(inboxId)
+  }
+
+  @Test
+  def updateMailboxACLShouldStoreUpdatedEvent(): Unit = {
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.inbox(BOB)
+    val inboxId: MailboxId = mailboxManager.createMailbox(MailboxPath.inbox(BOB), mailboxSession).get
+
+    mailboxManager.applyRightsCommand(path, MailboxACL.command().forUser(ALICE).rights(MailboxACL.Right.Read).asAddition(), mailboxSession);
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
+      .containsExactly(inboxId)
+  }
+
+  @Test
+  def appendMessageToMailboxShouldStoreUpdateEvent(): Unit = {
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.forUser(BOB, "test")
+    val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
+
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    mailboxManager
+      .getMailbox(inboxId, mailboxSession)
+      .appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
+      .containsExactly(inboxId)
+  }
+
+  @Test
+  def addSeenFlagsShouldStoreUpdateEvent(): Unit = {
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.forUser(BOB, "test")
+    val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
+    val messageManager: MessageManager = mailboxManager.getMailbox(inboxId, mailboxSession)
+    messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
+
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
+      .containsExactly(inboxId)
+  }
+
+  @Test
+  def removeSeenFlagsShouldStoreUpdateEvent(): Unit = {
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.forUser(BOB, "test")
+    val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
+    val messageManager: MessageManager = mailboxManager.getMailbox(inboxId, mailboxSession)
+    messageManager.appendMessage(AppendCommand.builder()
+      .withFlags(new Flags(Flags.Flag.SEEN))
+      .build("header: value\r\n\r\nbody"), mailboxSession)
+
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    messageManager.setFlags(new Flags(Flags.Flag.SEEN), FlagsUpdateMode.REMOVE, MessageRange.all(), mailboxSession)
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
+      .containsExactly(inboxId)
+  }
+
+  @Test
+  def addOtherThanSeenFlagsShouldNotStoreUpdateEvent(): Unit = {
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.forUser(BOB, "test")
+    val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
+    val messageManager: MessageManager = mailboxManager.getMailbox(inboxId, mailboxSession)
+    messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
+
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    messageManager.setFlags(new Flags(Flags.Flag.ANSWERED), FlagsUpdateMode.ADD, MessageRange.all(), mailboxSession)
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
+      .isEmpty()
+  }
+
+  @Test
+  def updateOtherThanSeenFlagsShouldNotStoreUpdateEvent(): Unit = {
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.forUser(BOB, "test")
+    val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
+    val messageManager: MessageManager = mailboxManager.getMailbox(inboxId, mailboxSession)
+    messageManager.appendMessage(AppendCommand.builder()
+      .withFlags(new Flags(Flags.Flag.ANSWERED))
+      .build("header: value\r\n\r\nbody"), mailboxSession)
+
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    messageManager.setFlags(new Flags(Flags.Flag.DELETED), FlagsUpdateMode.REPLACE, MessageRange.all(), mailboxSession)
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
+      .isEmpty()
+  }
+
+  @Test
+  def deleteMessageFromMailboxShouldStoreUpdateEvent(): Unit = {
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.forUser(BOB, "test")
+    val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
+    val messageManager: MessageManager = mailboxManager.getMailbox(inboxId, mailboxSession)
+    val appendResult: AppendResult = messageManager.appendMessage(AppendCommand.builder().build("header: value\r\n\r\nbody"), mailboxSession)
+
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+    messageManager.delete(List(appendResult.getId.getUid).asJava, mailboxSession)
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getUpdated)
+      .containsExactly(inboxId)
+  }
+
+  @Test
+  def deleteMailboxNameShouldStoreDestroyedEvent(): Unit = {
+    val mailboxSession = MailboxSessionUtil.create(BOB)
+    val path = MailboxPath.forUser(BOB, "test")
+    val inboxId: MailboxId = mailboxManager.createMailbox(path, mailboxSession).get
+
+    val state = State.of(UUID.randomUUID)
+    repository.save(MailboxChange.of(ACCOUNT_ID, state, ZonedDateTime.now, List[MailboxId](TestId.of(0)).asJava, List().asJava, List().asJava)).block()
+
+    mailboxManager.deleteMailbox(inboxId, mailboxSession)
+
+    assertThat(repository.getSinceState(ACCOUNT_ID, state, None.toJava).block().getDestroyed)
+      .containsExactly(inboxId)
+  }
+}


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