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