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

[james-project] 08/22: JAMES-3148 Add a listener for cleaning mailbox/cassandra upon deletions

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 a42e36ecd7fd6ea24f06735065ce7adb2090ee29
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Sun Apr 12 14:36:30 2020 +0700

    JAMES-3148 Add a listener for cleaning mailbox/cassandra upon deletions
---
 .../apache/james/mailbox/MailboxManagerTest.java   |   4 +-
 .../CassandraMailboxSessionMapperFactory.java      |   4 +
 .../mailbox/cassandra/DeleteMessageListener.java   | 145 ++++++++++++
 .../cassandra/mail/CassandraMessageDAO.java        |   4 +-
 .../cassandra/CassandraMailboxManagerProvider.java |   1 +
 .../cassandra/CassandraMailboxManagerTest.java     | 255 +++++++++++++++++++++
 .../cassandra/CassandraTestSystemFixture.java      |   1 +
 .../modules/mailbox/CassandraMailboxModule.java    |   7 +-
 8 files changed, 414 insertions(+), 7 deletions(-)

diff --git a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
index f3dfaa4..28f5f94 100644
--- a/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
+++ b/mailbox/api/src/test/java/org/apache/james/mailbox/MailboxManagerTest.java
@@ -111,9 +111,9 @@ public abstract class MailboxManagerTest<T extends MailboxManager> {
     public static final Username USER_2 = Username.of("USER_2");
     private static final int DEFAULT_MAXIMUM_LIMIT = 256;
 
-    private T mailboxManager;
+    protected T mailboxManager;
     private MailboxSession session;
-    private Message.Builder message;
+    protected Message.Builder message;
 
     private PreDeletionHook preDeletionHook1;
     private PreDeletionHook preDeletionHook2;
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java
index c4177b6..7d29e36 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/CassandraMailboxSessionMapperFactory.java
@@ -202,4 +202,8 @@ public class CassandraMailboxSessionMapperFactory extends MailboxSessionMapperFa
         }
         return mapper;
     }
+
+    public DeleteMessageListener deleteMessageListener() {
+        return new DeleteMessageListener(imapUidDAO, messageIdDAO, messageDAO, attachmentDAOV2, ownerDAO, attachmentMessageIdDAO);
+    }
 }
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
new file mode 100644
index 0000000..15d7b35
--- /dev/null
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/DeleteMessageListener.java
@@ -0,0 +1,145 @@
+/****************************************************************
+ * 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.mailbox.cassandra;
+
+import static org.apache.james.util.FunctionalUtils.negate;
+
+import java.util.Optional;
+
+import javax.inject.Inject;
+
+import org.apache.james.mailbox.cassandra.ids.CassandraId;
+import org.apache.james.mailbox.cassandra.ids.CassandraMessageId;
+import org.apache.james.mailbox.cassandra.mail.CassandraAttachmentDAOV2;
+import org.apache.james.mailbox.cassandra.mail.CassandraAttachmentMessageIdDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraAttachmentOwnerDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageIdDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageIdToImapUidDAO;
+import org.apache.james.mailbox.cassandra.mail.MessageAttachmentRepresentation;
+import org.apache.james.mailbox.cassandra.mail.MessageRepresentation;
+import org.apache.james.mailbox.events.Event;
+import org.apache.james.mailbox.events.Group;
+import org.apache.james.mailbox.events.MailboxListener;
+import org.apache.james.mailbox.model.ComposedMessageIdWithMetaData;
+import org.apache.james.mailbox.model.MessageMetaData;
+import org.apache.james.mailbox.model.MessageRange;
+import org.apache.james.mailbox.store.mail.MessageMapper;
+
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+public class DeleteMessageListener implements MailboxListener.GroupMailboxListener {
+    private static final Optional<CassandraId> ALL_MAILBOXES = Optional.empty();
+
+    public static class DeleteMessageListenerGroup extends Group {
+
+    }
+
+    @Inject
+    public DeleteMessageListener(CassandraMessageIdToImapUidDAO imapUidDAO, CassandraMessageIdDAO messageIdDAO, CassandraMessageDAO messageDAO,
+                                 CassandraAttachmentDAOV2 attachmentDAO, CassandraAttachmentOwnerDAO ownerDAO,
+                                 CassandraAttachmentMessageIdDAO attachmentMessageIdDAO) {
+        this.imapUidDAO = imapUidDAO;
+        this.messageIdDAO = messageIdDAO;
+        this.messageDAO = messageDAO;
+        this.attachmentDAO = attachmentDAO;
+        this.ownerDAO = ownerDAO;
+        this.attachmentMessageIdDAO = attachmentMessageIdDAO;
+    }
+
+    private final CassandraMessageIdToImapUidDAO imapUidDAO;
+    private final CassandraMessageIdDAO messageIdDAO;
+    private final CassandraMessageDAO messageDAO;
+    private final CassandraAttachmentDAOV2 attachmentDAO;
+    private final CassandraAttachmentOwnerDAO ownerDAO;
+    private final CassandraAttachmentMessageIdDAO attachmentMessageIdDAO;
+
+    @Override
+    public Group getDefaultGroup() {
+        return new DeleteMessageListenerGroup();
+    }
+
+    @Override
+    public boolean isHandling(Event event) {
+        return event instanceof Expunged || event instanceof MailboxDeletion;
+    }
+
+    @Override
+    public void event(Event event) {
+        if (event instanceof Expunged) {
+            Expunged expunged = (Expunged) event;
+
+            Flux.fromIterable(expunged.getExpunged()
+                .values())
+                .map(MessageMetaData::getMessageId)
+                .map(CassandraMessageId.class::cast)
+                .concatMap(this::handleDeletion)
+                .then()
+                .block();
+        }
+        if (event instanceof MailboxDeletion) {
+            MailboxDeletion mailboxDeletion = (MailboxDeletion) event;
+
+            CassandraId mailboxId = (CassandraId) mailboxDeletion.getMailboxId();
+
+            messageIdDAO.retrieveMessages(mailboxId, MessageRange.all())
+                .map(ComposedMessageIdWithMetaData::getComposedMessageId)
+                .concatMap(metadata -> imapUidDAO.delete((CassandraMessageId) metadata.getMessageId(), mailboxId)
+                    .then(messageIdDAO.delete(mailboxId, metadata.getUid()))
+                    .then(handleDeletion((CassandraMessageId) metadata.getMessageId())))
+                .then()
+                .block();
+        }
+    }
+
+    private Mono<Void> handleDeletion(CassandraMessageId messageId) {
+        return Mono.just(messageId)
+            .filterWhen(this::isReferenced)
+            .flatMap(id -> readMessage(id)
+                .flatMap(this::deleteUnreferencedAttachments)
+                .then(messageDAO.delete(messageId)));
+    }
+
+    private Mono<MessageRepresentation> readMessage(CassandraMessageId id) {
+        return messageDAO.retrieveMessage(id, MessageMapper.FetchType.Metadata);
+    }
+
+    private Mono<Void> deleteUnreferencedAttachments(MessageRepresentation message) {
+        return Flux.fromIterable(message.getAttachments())
+            .filterWhen(attachment -> ownerDAO.retrieveOwners(attachment.getAttachmentId()).hasElements().map(negate()))
+            .filterWhen(attachment -> hasOtherMessagesReferences(message, attachment))
+            .concatMap(attachment -> attachmentDAO.delete(attachment.getAttachmentId()))
+            .then();
+    }
+
+    private Mono<Boolean> hasOtherMessagesReferences(MessageRepresentation message, MessageAttachmentRepresentation attachment) {
+        return attachmentMessageIdDAO.getOwnerMessageIds(attachment.getAttachmentId())
+            .filter(messageId -> !message.getMessageId().equals(messageId))
+            .hasElements()
+            .map(negate());
+    }
+
+    private Mono<Boolean> isReferenced(CassandraMessageId id) {
+        return imapUidDAO.retrieve(id, ALL_MAILBOXES)
+            .hasElements()
+            .map(negate());
+    }
+}
diff --git a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAO.java b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAO.java
index baa1466..800b296 100644
--- a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAO.java
+++ b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAO.java
@@ -227,10 +227,10 @@ public class CassandraMessageDAO {
 
     public Mono<MessageRepresentation> retrieveMessage(ComposedMessageIdWithMetaData id, FetchType fetchType) {
         CassandraMessageId cassandraMessageId = (CassandraMessageId) id.getComposedMessageId().getMessageId();
-        return retrieveMessage(fetchType, cassandraMessageId);
+        return retrieveMessage(cassandraMessageId, fetchType);
     }
 
-    private Mono<MessageRepresentation> retrieveMessage(FetchType fetchType, CassandraMessageId cassandraMessageId) {
+    public Mono<MessageRepresentation> retrieveMessage(CassandraMessageId cassandraMessageId, FetchType fetchType) {
         return retrieveRow(cassandraMessageId, fetchType)
                 .flatMap(resultSet -> message(resultSet, cassandraMessageId, fetchType));
     }
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerProvider.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerProvider.java
index ab3dc5d..42ea282 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerProvider.java
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerProvider.java
@@ -107,6 +107,7 @@ public class CassandraMailboxManagerProvider {
 
         eventBus.register(quotaUpdater);
         eventBus.register(new MailboxAnnotationListener(mapperFactory, sessionProvider));
+        eventBus.register(mapperFactory.deleteMessageListener());
 
         return manager;
     }
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java
index 5cd44e3..55eeb7c 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraMailboxManagerTest.java
@@ -18,13 +18,49 @@
  ****************************************************************/
 package org.apache.james.mailbox.cassandra;
 
+import static org.mockito.Mockito.mock;
+
+import java.util.Collection;
+import java.util.Optional;
+
+import org.apache.james.backends.cassandra.CassandraCluster;
 import org.apache.james.backends.cassandra.CassandraClusterExtension;
+import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.HashBlobId;
+import org.apache.james.core.Username;
 import org.apache.james.mailbox.MailboxManagerTest;
+import org.apache.james.mailbox.MailboxSession;
+import org.apache.james.mailbox.MessageManager;
+import org.apache.james.mailbox.cassandra.ids.CassandraId;
+import org.apache.james.mailbox.cassandra.ids.CassandraMessageId;
+import org.apache.james.mailbox.cassandra.mail.CassandraAttachmentDAOV2;
+import org.apache.james.mailbox.cassandra.mail.CassandraAttachmentOwnerDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageIdDAO;
+import org.apache.james.mailbox.cassandra.mail.CassandraMessageIdToImapUidDAO;
 import org.apache.james.mailbox.cassandra.mail.MailboxAggregateModule;
 import org.apache.james.mailbox.events.EventBus;
+import org.apache.james.mailbox.model.AttachmentId;
+import org.apache.james.mailbox.model.ComposedMessageId;
+import org.apache.james.mailbox.model.FetchGroup;
+import org.apache.james.mailbox.model.MailboxId;
+import org.apache.james.mailbox.model.MailboxPath;
+import org.apache.james.mailbox.model.MessageAttachment;
+import org.apache.james.mailbox.model.MessageRange;
+import org.apache.james.mailbox.model.MessageResult;
 import org.apache.james.mailbox.store.PreDeletionHooks;
+import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.util.ClassLoaderUtils;
+import org.apache.james.util.streams.Iterators;
+import org.assertj.core.api.SoftAssertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.testcontainers.shaded.com.google.common.collect.ImmutableList;
+
+import com.github.fge.lambdas.Throwing;
 
 public class CassandraMailboxManagerTest extends MailboxManagerTest<CassandraMailboxManager> {
     @RegisterExtension
@@ -41,4 +77,223 @@ public class CassandraMailboxManagerTest extends MailboxManagerTest<CassandraMai
     protected EventBus retrieveEventBus(CassandraMailboxManager mailboxManager) {
         return mailboxManager.getEventBus();
     }
+
+    @Nested
+    class DeletionTests {
+        private MailboxSession session;
+        private MailboxPath inbox;
+        private MailboxId inboxId;
+        private MessageManager inboxManager;
+        private MessageManager otherBoxManager;
+        private MailboxPath newPath;
+
+        @BeforeEach
+        void setUp() throws Exception {
+            session = mailboxManager.createSystemSession(USER_1);
+            inbox = MailboxPath.inbox(session);
+            newPath = MailboxPath.forUser(USER_1, "specialMailbox");
+
+            inboxId = mailboxManager.createMailbox(inbox, session).get();
+            inboxManager = mailboxManager.getMailbox(inbox, session);
+            MailboxId otherId = mailboxManager.createMailbox(newPath, session).get();
+            otherBoxManager = mailboxManager.getMailbox(otherId, session);
+        }
+
+        @Test
+        void deleteMailboxShouldUnreferenceMessageMetadata(CassandraCluster cassandraCluster) throws Exception {
+            ComposedMessageId composedMessageId = inboxManager.appendMessage(MessageManager.AppendCommand.builder()
+                .build(ClassLoaderUtils.getSystemResourceAsByteArray("eml/emailWithOnlyAttachment.eml")), session);
+
+            AttachmentId attachmentId = Iterators.toStream(inboxManager.getMessages(MessageRange.all(), FetchGroup.FULL_CONTENT, session))
+                .map(Throwing.function(MessageResult::getLoadedAttachments))
+                .flatMap(Collection::stream)
+                .map(MessageAttachment::getAttachmentId)
+                .findFirst()
+                .get();
+
+            mailboxManager.deleteMailbox(inbox, session);
+
+            SoftAssertions.assertSoftly(softly -> {
+                CassandraMessageId cassandraMessageId = (CassandraMessageId) composedMessageId.getMessageId();
+                CassandraId mailboxId = (CassandraId) composedMessageId.getMailboxId();
+
+                softly.assertThat(messageDAO(cassandraCluster).retrieveMessage(cassandraMessageId, MessageMapper.FetchType.Metadata)
+                    .blockOptional()).isEmpty();
+
+                softly.assertThat(imapUidDAO(cassandraCluster).retrieve(cassandraMessageId, Optional.of(mailboxId)).collectList().block())
+                    .isEmpty();
+
+                softly.assertThat(messageIdDAO(cassandraCluster).retrieveMessages(mailboxId, MessageRange.all()).collectList().block())
+                    .isEmpty();
+
+                softly.assertThat(attachmentDAO(cassandraCluster).getAttachment(attachmentId).blockOptional())
+                    .isEmpty();
+            });
+        }
+
+        @Test
+        void deleteShouldUnreferenceMessageMetadata(CassandraCluster cassandraCluster) throws Exception {
+            ComposedMessageId composedMessageId = inboxManager.appendMessage(MessageManager.AppendCommand.builder()
+                .build(ClassLoaderUtils.getSystemResourceAsByteArray("eml/emailWithOnlyAttachment.eml")), session);
+
+            AttachmentId attachmentId = Iterators.toStream(inboxManager.getMessages(MessageRange.all(), FetchGroup.FULL_CONTENT, session))
+                .map(Throwing.function(MessageResult::getLoadedAttachments))
+                .flatMap(Collection::stream)
+                .map(MessageAttachment::getAttachmentId)
+                .findFirst()
+                .get();
+
+            inboxManager.delete(ImmutableList.of(composedMessageId.getUid()), session);
+
+            SoftAssertions.assertSoftly(softly -> {
+                CassandraMessageId cassandraMessageId = (CassandraMessageId) composedMessageId.getMessageId();
+
+                softly.assertThat(messageDAO(cassandraCluster).retrieveMessage(cassandraMessageId, MessageMapper.FetchType.Metadata)
+                    .blockOptional()).isEmpty();
+
+                softly.assertThat(attachmentDAO(cassandraCluster).getAttachment(attachmentId).blockOptional())
+                    .isEmpty();
+            });
+        }
+
+        @Test
+        void deleteMailboxShouldNotUnreferenceMessageMetadataWhenOtherReference(CassandraCluster cassandraCluster) throws Exception {
+            ComposedMessageId composedMessageId = inboxManager.appendMessage(MessageManager.AppendCommand.builder()
+                .build(ClassLoaderUtils.getSystemResourceAsByteArray("eml/emailWithOnlyAttachment.eml")), session);
+
+            AttachmentId attachmentId = Iterators.toStream(inboxManager.getMessages(MessageRange.all(), FetchGroup.FULL_CONTENT, session))
+                .map(Throwing.function(MessageResult::getLoadedAttachments))
+                .flatMap(Collection::stream)
+                .map(MessageAttachment::getAttachmentId)
+                .findFirst()
+                .get();
+
+            mailboxManager.copyMessages(MessageRange.all(), inboxId, otherBoxManager.getId(), session);
+
+            mailboxManager.deleteMailbox(inbox, session);
+
+            SoftAssertions.assertSoftly(softly -> {
+                CassandraMessageId cassandraMessageId = (CassandraMessageId) composedMessageId.getMessageId();
+                CassandraId mailboxId = (CassandraId) composedMessageId.getMailboxId();
+
+                softly.assertThat(messageDAO(cassandraCluster).retrieveMessage(cassandraMessageId, MessageMapper.FetchType.Metadata)
+                    .blockOptional()).isPresent();
+
+                softly.assertThat(imapUidDAO(cassandraCluster).retrieve(cassandraMessageId, Optional.of(mailboxId)).collectList().block())
+                    .isEmpty();
+
+                softly.assertThat(messageIdDAO(cassandraCluster).retrieveMessages(mailboxId, MessageRange.all()).collectList().block())
+                    .isEmpty();
+
+                softly.assertThat(attachmentDAO(cassandraCluster).getAttachment(attachmentId).blockOptional())
+                    .isPresent();
+            });
+        }
+
+        @Test
+        void deleteShouldNotUnreferenceMessageMetadataWhenOtherReference(CassandraCluster cassandraCluster) throws Exception {
+            ComposedMessageId composedMessageId = inboxManager.appendMessage(MessageManager.AppendCommand.builder()
+                .build(ClassLoaderUtils.getSystemResourceAsByteArray("eml/emailWithOnlyAttachment.eml")), session);
+
+            AttachmentId attachmentId = Iterators.toStream(inboxManager.getMessages(MessageRange.all(), FetchGroup.FULL_CONTENT, session))
+                .map(Throwing.function(MessageResult::getLoadedAttachments))
+                .flatMap(Collection::stream)
+                .map(MessageAttachment::getAttachmentId)
+                .findFirst()
+                .get();
+
+            mailboxManager.copyMessages(MessageRange.all(), inboxId, otherBoxManager.getId(), session);
+
+            inboxManager.delete(ImmutableList.of(composedMessageId.getUid()), session);
+
+            SoftAssertions.assertSoftly(softly -> {
+                CassandraMessageId cassandraMessageId = (CassandraMessageId) composedMessageId.getMessageId();
+
+                softly.assertThat(messageDAO(cassandraCluster).retrieveMessage(cassandraMessageId, MessageMapper.FetchType.Metadata)
+                    .blockOptional()).isPresent();
+
+                softly.assertThat(attachmentDAO(cassandraCluster).getAttachment(attachmentId).blockOptional())
+                    .isPresent();
+            });
+        }
+
+        @Test
+        void deleteMailboxShouldNotUnreferenceAttachmentWhenOtherReference(CassandraCluster cassandraCluster) throws Exception {
+            ComposedMessageId composedMessageId = inboxManager.appendMessage(MessageManager.AppendCommand.builder()
+                .build(ClassLoaderUtils.getSystemResourceAsByteArray("eml/emailWithOnlyAttachment.eml")), session);
+
+            AttachmentId attachmentId = Iterators.toStream(inboxManager.getMessages(MessageRange.all(), FetchGroup.FULL_CONTENT, session))
+                .map(Throwing.function(MessageResult::getLoadedAttachments))
+                .flatMap(Collection::stream)
+                .map(MessageAttachment::getAttachmentId)
+                .findFirst()
+                .get();
+
+            new CassandraAttachmentOwnerDAO(cassandraCluster.getConf()).addOwner(attachmentId, Username.of("bob")).block();
+
+            mailboxManager.deleteMailbox(inbox, session);
+
+            SoftAssertions.assertSoftly(softly -> {
+                CassandraMessageId cassandraMessageId = (CassandraMessageId) composedMessageId.getMessageId();
+                CassandraId mailboxId = (CassandraId) composedMessageId.getMailboxId();
+
+                softly.assertThat(messageDAO(cassandraCluster).retrieveMessage(cassandraMessageId, MessageMapper.FetchType.Metadata)
+                    .blockOptional()).isEmpty();
+
+                softly.assertThat(imapUidDAO(cassandraCluster).retrieve(cassandraMessageId, Optional.of(mailboxId)).collectList().block())
+                    .isEmpty();
+
+                softly.assertThat(messageIdDAO(cassandraCluster).retrieveMessages(mailboxId, MessageRange.all()).collectList().block())
+                    .isEmpty();
+
+                softly.assertThat(attachmentDAO(cassandraCluster).getAttachment(attachmentId).blockOptional())
+                    .isPresent();
+            });
+        }
+
+        @Test
+        void deleteShouldNotUnreferenceAttachmentWhenOtherReference(CassandraCluster cassandraCluster) throws Exception {
+            ComposedMessageId composedMessageId = inboxManager.appendMessage(MessageManager.AppendCommand.builder()
+                .build(ClassLoaderUtils.getSystemResourceAsByteArray("eml/emailWithOnlyAttachment.eml")), session);
+
+            AttachmentId attachmentId = Iterators.toStream(inboxManager.getMessages(MessageRange.all(), FetchGroup.FULL_CONTENT, session))
+                .map(Throwing.function(MessageResult::getLoadedAttachments))
+                .flatMap(Collection::stream)
+                .map(MessageAttachment::getAttachmentId)
+                .findFirst()
+                .get();
+
+            new CassandraAttachmentOwnerDAO(cassandraCluster.getConf()).addOwner(attachmentId, Username.of("bob")).block();
+
+            inboxManager.delete(ImmutableList.of(composedMessageId.getUid()), session);
+
+            SoftAssertions.assertSoftly(softly -> {
+                CassandraMessageId cassandraMessageId = (CassandraMessageId) composedMessageId.getMessageId();
+                CassandraId mailboxId = (CassandraId) composedMessageId.getMailboxId();
+
+                softly.assertThat(messageDAO(cassandraCluster).retrieveMessage(cassandraMessageId, MessageMapper.FetchType.Metadata)
+                    .blockOptional()).isEmpty();
+
+                softly.assertThat(attachmentDAO(cassandraCluster).getAttachment(attachmentId).blockOptional())
+                    .isPresent();
+            });
+        }
+
+        private CassandraAttachmentDAOV2 attachmentDAO(CassandraCluster cassandraCluster) {
+            return new CassandraAttachmentDAOV2(new HashBlobId.Factory(), cassandraCluster.getConf());
+        }
+
+        private CassandraMessageIdDAO messageIdDAO(CassandraCluster cassandraCluster) {
+            return new CassandraMessageIdDAO(cassandraCluster.getConf(), new CassandraMessageId.Factory());
+        }
+
+        private CassandraMessageIdToImapUidDAO imapUidDAO(CassandraCluster cassandraCluster) {
+            return new CassandraMessageIdToImapUidDAO(cassandraCluster.getConf(), new CassandraMessageId.Factory());
+        }
+
+        private CassandraMessageDAO messageDAO(CassandraCluster cassandraCluster) {
+            return new CassandraMessageDAO(cassandraCluster.getConf(), cassandraCluster.getTypesProvider(),
+                mock(BlobStore.class), new HashBlobId.Factory(), new CassandraMessageId.Factory());
+        }
+    }
 }
diff --git a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraTestSystemFixture.java b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraTestSystemFixture.java
index 6524206..dea017b 100644
--- a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraTestSystemFixture.java
+++ b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/CassandraTestSystemFixture.java
@@ -80,6 +80,7 @@ public class CassandraTestSystemFixture {
             eventBus, annotationManager, storeRightManager, quotaComponents, index, MailboxManagerConfiguration.DEFAULT, PreDeletionHooks.NO_PRE_DELETION_HOOK);
 
         eventBus.register(new MailboxAnnotationListener(mapperFactory, sessionProvider));
+        eventBus.register(mapperFactory.deleteMessageListener());
 
         return cassandraMailboxManager;
     }
diff --git a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraMailboxModule.java b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraMailboxModule.java
index 7630f5e..a3a0742 100644
--- a/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraMailboxModule.java
+++ b/server/container/guice/cassandra-guice/src/main/java/org/apache/james/modules/mailbox/CassandraMailboxModule.java
@@ -38,6 +38,7 @@ import org.apache.james.mailbox.SessionProvider;
 import org.apache.james.mailbox.SubscriptionManager;
 import org.apache.james.mailbox.cassandra.CassandraMailboxManager;
 import org.apache.james.mailbox.cassandra.CassandraMailboxSessionMapperFactory;
+import org.apache.james.mailbox.cassandra.DeleteMessageListener;
 import org.apache.james.mailbox.cassandra.ids.CassandraId;
 import org.apache.james.mailbox.cassandra.ids.CassandraMessageId;
 import org.apache.james.mailbox.cassandra.mail.CassandraACLMapper;
@@ -192,9 +193,9 @@ public class CassandraMailboxModule extends AbstractModule {
 
         Multibinder.newSetBinder(binder(), MailboxManagerDefinition.class).addBinding().to(CassandraMailboxManagerDefinition.class);
 
-        Multibinder.newSetBinder(binder(), MailboxListener.GroupMailboxListener.class)
-            .addBinding()
-            .to(MailboxAnnotationListener.class);
+        Multibinder<MailboxListener.GroupMailboxListener> mailboxListeners = Multibinder.newSetBinder(binder(), MailboxListener.GroupMailboxListener.class);
+        mailboxListeners.addBinding().to(MailboxAnnotationListener.class);
+        mailboxListeners.addBinding().to(DeleteMessageListener.class);
 
         bind(MailboxManager.class).annotatedWith(Names.named(MAILBOXMANAGER_NAME)).to(MailboxManager.class);
     }


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