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
The following commit(s) were added to refs/heads/master by this push: new e3f027101a JAMES-2992 MessageFastViewProjection should remove entries when delet… (#2635) e3f027101a is described below commit e3f027101a57f7728ef431dab4c744670619ada4 Author: Rene Cordier <rcord...@linagora.com> AuthorDate: Tue Feb 18 22:23:29 2025 +0700 JAMES-2992 MessageFastViewProjection should remove entries when delet… (#2635) --- .../james/modules/data/CassandraJmapModule.java | 5 + .../org/apache/james/modules/MailboxProbeImpl.java | 12 ++ ...aMessageFastViewProjectionDeletionCallback.java | 41 ++++++ .../jmap/rfc8621/distributed/DistributedBase.java | 3 +- .../distributed/DistributedJmapPreviewTest.java | 26 ++++ .../rfc8621/contract/JmapPreviewContract.scala | 139 +++++++++++++++++++++ .../ComputeMessageFastViewProjectionListener.java | 15 --- ...mputeMessageFastViewProjectionListenerTest.java | 29 ----- 8 files changed, 225 insertions(+), 45 deletions(-) diff --git a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java index fce2ca29ea..232eb69415 100644 --- a/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java +++ b/server/container/guice/cassandra/src/main/java/org/apache/james/modules/data/CassandraJmapModule.java @@ -53,6 +53,7 @@ import org.apache.james.jmap.cassandra.identity.CassandraCustomIdentityModule; import org.apache.james.jmap.cassandra.projections.CassandraEmailQueryView; import org.apache.james.jmap.cassandra.projections.CassandraEmailQueryViewModule; import org.apache.james.jmap.cassandra.projections.CassandraMessageFastViewProjection; +import org.apache.james.jmap.cassandra.projections.CassandraMessageFastViewProjectionDeletionCallback; import org.apache.james.jmap.cassandra.projections.CassandraMessageFastViewProjectionModule; import org.apache.james.jmap.cassandra.pushsubscription.CassandraPushSubscriptionModule; import org.apache.james.jmap.cassandra.pushsubscription.CassandraPushSubscriptionRepository; @@ -60,6 +61,7 @@ import org.apache.james.jmap.cassandra.upload.CassandraUploadRepository; import org.apache.james.jmap.cassandra.upload.CassandraUploadUsageRepository; import org.apache.james.jmap.cassandra.upload.UploadDAO; import org.apache.james.jmap.cassandra.upload.UploadModule; +import org.apache.james.mailbox.cassandra.DeleteMessageListener; import org.apache.james.user.api.DeleteUserDataTaskStep; import org.apache.james.user.api.UsernameChangeTaskStep; import org.apache.james.utils.PropertiesProvider; @@ -114,6 +116,9 @@ public class CassandraJmapModule extends AbstractModule { eventDTOModuleBinder.addBinding().toInstance(FilteringRuleSetDefineDTOModules.FILTERING_RULE_SET_DEFINED); eventDTOModuleBinder.addBinding().toInstance(FilteringRuleSetDefineDTOModules.FILTERING_INCREMENT); + Multibinder.newSetBinder(binder(), DeleteMessageListener.DeletionCallback.class) + .addBinding() + .to(CassandraMessageFastViewProjectionDeletionCallback.class); Multibinder.newSetBinder(binder(), UsernameChangeTaskStep.class) .addBinding() diff --git a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java index 7829a4c87e..27c3cb4d61 100644 --- a/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java +++ b/server/container/guice/mailbox/src/main/java/org/apache/james/modules/MailboxProbeImpl.java @@ -26,6 +26,7 @@ import java.io.IOException; import java.io.InputStream; import java.util.Collection; import java.util.Date; +import java.util.List; import java.util.stream.Collectors; import jakarta.inject.Inject; @@ -222,4 +223,15 @@ public class MailboxProbeImpl implements GuiceProbe, MailboxProbe { closeSession(mailboxSession); } } + + public void deleteMessage(List<MessageUid> messageUids, MailboxPath mailboxPath, Username user) throws MailboxException { + MailboxSession mailboxSession = mailboxManager.createSystemSession(user); + MessageManager messageManager = mailboxManager.getMailbox(mailboxPath, mailboxSession); + messageManager.delete(messageUids, mailboxSession); + } + + public void moveMessages(MessageRange set, MailboxPath from, MailboxPath to, Username user) throws MailboxException { + MailboxSession mailboxSession = mailboxManager.createSystemSession(user); + mailboxManager.moveMessages(set, from, to, mailboxSession); + } } \ No newline at end of file diff --git a/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraMessageFastViewProjectionDeletionCallback.java b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraMessageFastViewProjectionDeletionCallback.java new file mode 100644 index 0000000000..c2e4d25c05 --- /dev/null +++ b/server/data/data-jmap-cassandra/src/main/java/org/apache/james/jmap/cassandra/projections/CassandraMessageFastViewProjectionDeletionCallback.java @@ -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.cassandra.projections; + +import jakarta.inject.Inject; + +import org.apache.james.jmap.api.projections.MessageFastViewProjection; +import org.apache.james.mailbox.cassandra.DeleteMessageListener; + +import reactor.core.publisher.Mono; + +public class CassandraMessageFastViewProjectionDeletionCallback implements DeleteMessageListener.DeletionCallback { + private final MessageFastViewProjection messageFastViewProjection; + + @Inject + public CassandraMessageFastViewProjectionDeletionCallback(MessageFastViewProjection messageFastViewProjection) { + this.messageFastViewProjection = messageFastViewProjection; + } + + @Override + public Mono<Void> forMessage(DeleteMessageListener.DeletedMessageCopyCommand copyCommand) { + return Mono.from(messageFastViewProjection.delete(copyCommand.getMessageId())); + } +} diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBase.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBase.java index dfb4902d1d..728acf9641 100644 --- a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBase.java +++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedBase.java @@ -27,6 +27,7 @@ import org.apache.james.JamesServerBuilder; import org.apache.james.JamesServerExtension; import org.apache.james.SearchConfiguration; import org.apache.james.jmap.rfc8621.contract.IdentityProbeModule; +import org.apache.james.jmap.rfc8621.contract.JmapPreviewProbeModule; import org.apache.james.jmap.rfc8621.contract.probe.DelegationProbeModule; import org.apache.james.modules.AwsS3BlobStoreExtension; import org.apache.james.modules.RabbitMQExtension; @@ -53,6 +54,6 @@ public class DistributedBase { .extension(new RabbitMQExtension()) .extension(new AwsS3BlobStoreExtension()) .server(configuration -> CassandraRabbitMQJamesServerMain.createServer(configuration) - .overrideWith(new TestJMAPServerModule(), new DelegationProbeModule(), new IdentityProbeModule())) + .overrideWith(new TestJMAPServerModule(), new DelegationProbeModule(), new IdentityProbeModule(), new JmapPreviewProbeModule())) .build(); } diff --git a/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedJmapPreviewTest.java b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedJmapPreviewTest.java new file mode 100644 index 0000000000..343a755662 --- /dev/null +++ b/server/protocols/jmap-rfc-8621-integration-tests/distributed-jmap-rfc-8621-integration-tests/src/test/java/org/apache/james/jmap/rfc8621/distributed/DistributedJmapPreviewTest.java @@ -0,0 +1,26 @@ +/**************************************************************** + * 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.rfc8621.distributed; + +import org.apache.james.jmap.rfc8621.contract.JmapPreviewContract; + +public class DistributedJmapPreviewTest extends DistributedBase implements JmapPreviewContract { + +} 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/JmapPreviewContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/JmapPreviewContract.scala new file mode 100644 index 0000000000..1a56137bb9 --- /dev/null +++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/JmapPreviewContract.scala @@ -0,0 +1,139 @@ +/**************************************************************** + * 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.rfc8621.contract + +import java.nio.charset.StandardCharsets +import java.time.Duration +import java.util.Optional +import java.util.concurrent.TimeUnit + +import com.google.common.collect.ImmutableList +import com.google.inject.AbstractModule +import com.google.inject.multibindings.Multibinder +import io.restassured.RestAssured.requestSpecification +import jakarta.inject.Inject +import org.apache.james.GuiceJamesServer +import org.apache.james.jmap.api.projections.{MessageFastViewPrecomputedProperties, MessageFastViewProjection} +import org.apache.james.jmap.http.UserCredential +import org.apache.james.jmap.rfc8621.contract.Fixture.{ANDRE, BOB, BOB_PASSWORD, DOMAIN, authScheme, baseRequestSpecBuilder} +import org.apache.james.jmap.rfc8621.contract.JmapPreviewContract.createTestMessage +import org.apache.james.mailbox.DefaultMailboxes +import org.apache.james.mailbox.MessageManager.AppendCommand +import org.apache.james.mailbox.model.{MailboxPath, MessageId, MessageRange} +import org.apache.james.mime4j.dom.Message +import org.apache.james.modules.MailboxProbeImpl +import org.apache.james.utils.{DataProbeImpl, GuiceProbe} +import org.awaitility.Awaitility +import org.junit.jupiter.api.{BeforeEach, Test} +import reactor.core.scala.publisher.SMono + +import scala.jdk.OptionConverters.RichOption + +class MessageFastViewProjectionProbe @Inject() (messageFastViewProjection: MessageFastViewProjection) extends GuiceProbe { + def retrieve(messageId: MessageId): Optional[MessageFastViewPrecomputedProperties] = + SMono.fromPublisher(messageFastViewProjection.retrieve(messageId)).blockOption().toJava +} + +class JmapPreviewProbeModule extends AbstractModule { + override protected def configure(): Unit = { + Multibinder.newSetBinder(binder(), classOf[GuiceProbe]) + .addBinding() + .to(classOf[MessageFastViewProjectionProbe]) + } +} + +object JmapPreviewContract { + private def createTestMessage: Message = Message.Builder + .of + .setSubject("test") + .setSender(ANDRE.asString()) + .setFrom(ANDRE.asString()) + .setSubject("World domination \r\n" + + " and this is also part of the header") + .setBody("testmail", StandardCharsets.UTF_8) + .build +} + +trait JmapPreviewContract { + private lazy val slowPacedPollInterval = Duration.ofMillis(100) + private lazy val calmlyAwait = Awaitility.`with` + .pollInterval(slowPacedPollInterval) + .and.`with`.pollDelay(slowPacedPollInterval) + .await + private lazy val awaitAtMostTenSeconds = calmlyAwait.atMost(10, TimeUnit.SECONDS) + + @BeforeEach + def setUp(server: GuiceJamesServer): Unit = { + server.getProbe(classOf[DataProbeImpl]) + .fluent + .addDomain(DOMAIN.asString) + .addUser(BOB.asString, BOB_PASSWORD) + + requestSpecification = baseRequestSpecBuilder(server) + .setAuth(authScheme(UserCredential(BOB, BOB_PASSWORD))) + .build + } + + @Test + def jmapPreviewShouldBeWellRemovedWhenDeleteMailbox(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + mailboxProbe.createMailbox("#private", BOB.asString, DefaultMailboxes.INBOX) + + val messageId = mailboxProbe.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder() + .build(createTestMessage)) + .getMessageId + + val messageFastViewProjectionProbe: MessageFastViewProjectionProbe = server.getProbe(classOf[MessageFastViewProjectionProbe]) + awaitAtMostTenSeconds.until(() => messageFastViewProjectionProbe.retrieve(messageId).isPresent) + + mailboxProbe.deleteMailbox("#private", BOB.asString, DefaultMailboxes.INBOX) + awaitAtMostTenSeconds.until(() => messageFastViewProjectionProbe.retrieve(messageId).isEmpty) + } + + @Test + def jmapPreviewShouldBeWellRemovedWhenDeleteMessage(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + mailboxProbe.createMailbox("#private", BOB.asString, DefaultMailboxes.INBOX) + + val composedMessageId = mailboxProbe.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder() + .build(createTestMessage)) + + val messageFastViewProjectionProbe: MessageFastViewProjectionProbe = server.getProbe(classOf[MessageFastViewProjectionProbe]) + awaitAtMostTenSeconds.until(() => messageFastViewProjectionProbe.retrieve(composedMessageId.getMessageId).isPresent) + + mailboxProbe.deleteMessage(ImmutableList.of(composedMessageId.getUid), MailboxPath.inbox(BOB), BOB) + awaitAtMostTenSeconds.until(() => messageFastViewProjectionProbe.retrieve(composedMessageId.getMessageId).isEmpty) + } + + @Test + def shouldKeepPreviewWhenExpungedAndStillReferenced(server: GuiceJamesServer): Unit = { + val mailboxProbe = server.getProbe(classOf[MailboxProbeImpl]) + mailboxProbe.createMailbox("#private", BOB.asString, DefaultMailboxes.INBOX) + mailboxProbe.createMailbox("#private", BOB.asString, "otherBox") + + val composedMessageId = mailboxProbe.appendMessage(BOB.asString, MailboxPath.inbox(BOB), AppendCommand.builder() + .build(createTestMessage)) + + mailboxProbe.moveMessages(MessageRange.all, MailboxPath.inbox(BOB), MailboxPath.forUser(BOB, "otherBox"), BOB) + + val messageFastViewProjectionProbe: MessageFastViewProjectionProbe = server.getProbe(classOf[MessageFastViewProjectionProbe]) + awaitAtMostTenSeconds.until(() => messageFastViewProjectionProbe.retrieve(composedMessageId.getMessageId).isPresent) + } +} diff --git a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListener.java b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListener.java index 64d914f179..88e298304e 100644 --- a/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListener.java +++ b/server/protocols/jmap-rfc-8621/src/main/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListener.java @@ -25,7 +25,6 @@ import java.io.IOException; import jakarta.inject.Inject; -import org.apache.commons.collections4.CollectionUtils; import org.apache.commons.lang3.tuple.Pair; import org.apache.james.events.Event; import org.apache.james.events.EventListener; @@ -39,12 +38,10 @@ import org.apache.james.mailbox.events.MailboxEvents.Added; import org.apache.james.mailbox.events.MailboxEvents.Expunged; import org.apache.james.mailbox.exception.MailboxException; import org.apache.james.mailbox.model.FetchGroup; -import org.apache.james.mailbox.model.MessageId; import org.apache.james.mailbox.model.MessageResult; import com.github.fge.lambdas.Throwing; import com.google.common.annotations.VisibleForTesting; -import com.google.common.collect.ImmutableSet; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -83,10 +80,6 @@ public class ComputeMessageFastViewProjectionListener implements EventListener.R MailboxSession session = sessionProvider.createSystemSession(event.getUsername()); return handleAddedEvent((Added) event, session); } - if (event instanceof Expunged) { - MailboxSession session = sessionProvider.createSystemSession(event.getUsername()); - return handleExpungedEvent((Expunged) event, session); - } return Mono.empty(); } @@ -115,12 +108,4 @@ public class ComputeMessageFastViewProjectionListener implements EventListener.R return messageFastViewPrecomputedPropertiesFactory.from(messageResult); } - private Mono<Void> handleExpungedEvent(Expunged expunged, MailboxSession session) { - ImmutableSet<MessageId> expungedMessageIds = expunged.getMessageIds(); - return Mono.from(messageIdManager.accessibleMessagesReactive(expungedMessageIds, session)) - .flatMapIterable(accessibleMessageIds -> CollectionUtils.subtract(expungedMessageIds, accessibleMessageIds)) - .flatMap(messageFastViewProjection::delete, DEFAULT_CONCURRENCY) - .then(); - } - } diff --git a/server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListenerTest.java b/server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListenerTest.java index 722fcc3b73..1751e10054 100644 --- a/server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListenerTest.java +++ b/server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/event/ComputeMessageFastViewProjectionListenerTest.java @@ -307,35 +307,6 @@ class ComputeMessageFastViewProjectionListenerTest { .hasSize(1); } - @Test - void shouldDeletePreviewWhenMessageDeletedAndNoLongerReferenced() throws Exception { - ComposedMessageId composedId = inboxMessageManager.appendMessage( - MessageManager.AppendCommand.builder() - .build(ClassLoaderUtils.getSystemResourceAsSharedStream("fullMessage.eml")), - mailboxSession).getId(); - - assertThat(Mono.from(messageFastViewProjection.retrieve(composedId.getMessageId())).block()) - .isNotNull(); - - inboxMessageManager.delete(ImmutableList.of(composedId.getUid()), mailboxSession); - - assertThat(Mono.from(messageFastViewProjection.retrieve(composedId.getMessageId())).block()) - .isNull(); - } - - @Test - void shouldKeepPreviewWhenExpungedAndStillReferenced() throws Exception { - ComposedMessageId composedId = inboxMessageManager.appendMessage( - MessageManager.AppendCommand.builder() - .build(ClassLoaderUtils.getSystemResourceAsSharedStream("fullMessage.eml")), - mailboxSession).getId(); - - mailboxManager.moveMessages(MessageRange.all(), BOB_INBOX_PATH, BOB_OTHER_BOX_PATH, mailboxSession); - - assertThat(Mono.from(messageFastViewProjection.retrieve(composedId.getMessageId())).block()) - .isNotNull(); - } - @Test void shouldKeepPreviewWhenMessageIdReferenceInCopied() throws Exception { ComposedMessageId composedId = inboxMessageManager.appendMessage( --------------------------------------------------------------------- To unsubscribe, e-mail: notifications-unsubscr...@james.apache.org For additional commands, e-mail: notifications-h...@james.apache.org