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

quantranhong1999 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 37216eb0e4 JAMES-4209 CassandraMessagesDAOV3: optionally write 
recovery infos (#3053)
37216eb0e4 is described below

commit 37216eb0e4c233f485c2ece18c764b1ab3b7f01d
Author: Benoit TELLIER <[email protected]>
AuthorDate: Fri Jun 5 04:30:09 2026 +0200

    JAMES-4209 CassandraMessagesDAOV3: optionally write recovery infos (#3053)
---
 .../init/configuration/CassandraConfiguration.java | 45 ++++++++++-
 .../cassandra/mail/CassandraMessageDAOV3.java      | 30 +++++++-
 .../cassandra/CassandraMailboxManagerTest.java     |  2 +
 .../cassandra/mail/CassandraMessageDAOV3Test.java  | 88 +++++++++++++++++++---
 .../mailbox/cassandra/mail/utils/GuiceUtils.java   | 11 +++
 .../org/apache/james/blob/api/BlobStoreDAO.java    |  2 +
 .../apache/james/blob/file/FileBlobStoreDAO.java   | 21 +++++-
 .../blob/deduplication/BloomFilterGCAlgorithm.java | 37 ++++++---
 .../blob/deduplication/GenerationAwareBlobId.java  |  6 ++
 .../BloomFilterGCAlgorithmContract.java            | 83 ++++++++++++++++++++
 .../MetaDataFixInconsistenciesServiceTest.java     |  3 +
 server/protocols/webadmin/webadmin-pop3/pom.xml    |  5 ++
 .../Pop3MetaDataFixInconsistenciesRoutesTest.java  |  3 +
 13 files changed, 310 insertions(+), 26 deletions(-)

diff --git 
a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/CassandraConfiguration.java
 
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/CassandraConfiguration.java
index 9346821aaa..9c0ffb7e5c 100644
--- 
a/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/CassandraConfiguration.java
+++ 
b/backends-common/cassandra/src/main/java/org/apache/james/backends/cassandra/init/configuration/CassandraConfiguration.java
@@ -39,6 +39,19 @@ import com.google.common.collect.ImmutableList;
 public class CassandraConfiguration {
     private static final Logger LOGGER = 
LoggerFactory.getLogger(CassandraConfiguration.class);
 
+    public enum BlobRecoveryMode {
+        NONE, SYNCHRONOUS, ASYNCHRONOUS;
+
+        public static BlobRecoveryMode parse(String value) {
+            return switch (value.toLowerCase()) {
+                case "none" -> NONE;
+                case "synchronous" -> SYNCHRONOUS;
+                case "asynchronous" -> ASYNCHRONOUS;
+                default -> throw new IllegalArgumentException("Unknown blob 
recovery mode: '" + value + "'. Expected none, synchronous or asynchronous");
+            };
+        }
+    }
+
     public static final float DEFAULT_MAILBOX_READ_REPAIR = 0.1f;
     public static final float DEFAULT_MAX_MAILBOX_COUNTERS_READ_REPAIR_CHANCE 
= 0.1f;
     public static final float 
DEFAULT_ONE_HUNDRED_MAILBOX_COUNTERS_READ_REPAIR_CHANCE = 0.01f;
@@ -59,6 +72,7 @@ public class CassandraConfiguration {
     public static final boolean DEFAULT_STRONG_CONSISTENCY = true;
     public static final boolean DEFAULT_OPTIMISTIC_CONSISTENCY_LEVEL = false;
     public static final boolean DEFAULT_MAIL_REPOSITORY_STRONG_CONSISTENCY = 
true;
+    public static final BlobRecoveryMode DEFAULT_BLOB_RECOVERY_MODE = 
BlobRecoveryMode.NONE;
 
     private static final String MAILBOX_READ_REPAIR = 
"mailbox.read.repair.chance";
     private static final String MAILBOX_MAX_COUNTERS_READ_REPAIR = 
"mailbox.counters.read.repair.chance.max";
@@ -84,6 +98,7 @@ public class CassandraConfiguration {
     private static final String MAIL_REPOSITORY_STRONG_CONSISTENCY = 
"mailrepository.strong.consistency";
     private static final String ACL_ENABLED = "acl.enabled";
     private static final String UID_MODSEQ_INCREMENT = "uid.modseq.increment";
+    private static final String MAILBOX_BLOB_RECOVERY_MODE = 
"mailbox.blob.recovery.mode";
 
     public static final CassandraConfiguration DEFAULT_CONFIGURATION = 
builder().build();
 
@@ -112,6 +127,7 @@ public class CassandraConfiguration {
         private Optional<Boolean> mailRepositoryStrongConsistency = 
Optional.empty();
         private Optional<Boolean> aclEnabled = Optional.empty();
         private Optional<Integer> uidModseqIncrement = Optional.empty();
+        private Optional<BlobRecoveryMode> blobRecoveryMode = Optional.empty();
 
         public Builder mailboxReadStrongConsistency(boolean value) {
             this.mailboxReadStrongConsistency = Optional.of(value);
@@ -175,6 +191,16 @@ public class CassandraConfiguration {
             return this;
         }
 
+        public Builder blobRecoveryMode(BlobRecoveryMode value) {
+            this.blobRecoveryMode = Optional.of(value);
+            return this;
+        }
+
+        public Builder blobRecoveryMode(Optional<BlobRecoveryMode> value) {
+            this.blobRecoveryMode = value;
+            return this;
+        }
+
         public Builder flagsUpdateMessageIdMaxRetry(int value) {
             Preconditions.checkArgument(value > 0, 
"flagsUpdateMessageIdMaxRetry needs to be strictly positive");
             this.flagsUpdateMessageIdMaxRetry = Optional.of(value);
@@ -386,7 +412,8 @@ public class CassandraConfiguration {
                 uidReadStrongConsistency.orElse(DEFAULT_STRONG_CONSISTENCY),
                 modseqReadStrongConsistency.orElse(DEFAULT_STRONG_CONSISTENCY),
                 aclEnabled.orElse(true),
-                uidModseqIncrement.orElse(0));
+                uidModseqIncrement.orElse(0),
+                blobRecoveryMode.orElse(DEFAULT_BLOB_RECOVERY_MODE));
         }
     }
 
@@ -442,6 +469,8 @@ public class CassandraConfiguration {
                 
propertiesConfiguration.getBoolean(MAIL_REPOSITORY_STRONG_CONSISTENCY, null)))
             
.aclEnabled(Optional.ofNullable(propertiesConfiguration.getBoolean(ACL_ENABLED, 
null)))
             
.uidModseqIncrement(Optional.ofNullable(propertiesConfiguration.getInteger(UID_MODSEQ_INCREMENT,
 null)))
+            
.blobRecoveryMode(Optional.ofNullable(propertiesConfiguration.getString(MAILBOX_BLOB_RECOVERY_MODE,
 null))
+                .map(BlobRecoveryMode::parse))
             .build();
     }
 
@@ -469,6 +498,7 @@ public class CassandraConfiguration {
     private final boolean modseqReadStrongConsistency;
     private final boolean aclEnabled;
     private final int uidModseqIncrement;
+    private final BlobRecoveryMode blobRecoveryMode;
 
     @VisibleForTesting
     CassandraConfiguration(int aclMaxRetry, int expungeChunkSize,
@@ -481,7 +511,7 @@ public class CassandraConfiguration {
                            boolean messageReadStrongConsistency, boolean 
messageWriteStrongConsistency,
                            boolean optimisticConsistencyLevel, boolean 
mailRepositoryStrongConsistency,
                            boolean uidReadStrongConsistency, boolean 
modseqReadStrongConsistency, boolean aclEnabled,
-                           int uidModseqIncrement) {
+                           int uidModseqIncrement, BlobRecoveryMode 
blobRecoveryMode) {
         this.aclMaxRetry = aclMaxRetry;
         this.expungeChunkSize = expungeChunkSize;
         this.flagsUpdateMessageIdMaxRetry = flagsUpdateMessageIdMaxRetry;
@@ -506,6 +536,7 @@ public class CassandraConfiguration {
         this.modseqReadStrongConsistency = modseqReadStrongConsistency;
         this.aclEnabled = aclEnabled;
         this.uidModseqIncrement = uidModseqIncrement;
+        this.blobRecoveryMode = blobRecoveryMode;
     }
 
     public boolean isUidReadStrongConsistency() {
@@ -604,6 +635,10 @@ public class CassandraConfiguration {
         return uidModseqIncrement;
     }
 
+    public BlobRecoveryMode getBlobRecoveryMode() {
+        return blobRecoveryMode;
+    }
+
     @Override
     public final boolean equals(Object o) {
         if (o instanceof CassandraConfiguration) {
@@ -632,7 +667,8 @@ public class CassandraConfiguration {
                 && Objects.equals(this.modseqReadStrongConsistency, 
that.modseqReadStrongConsistency)
                 && Objects.equals(this.mailRepositoryStrongConsistency, 
that.mailRepositoryStrongConsistency)
                 && Objects.equals(this.aclEnabled, that.aclEnabled)
-                && Objects.equals(this.uidModseqIncrement, 
that.uidModseqIncrement);
+                && Objects.equals(this.uidModseqIncrement, 
that.uidModseqIncrement)
+                && Objects.equals(this.blobRecoveryMode, 
that.blobRecoveryMode);
         }
         return false;
     }
@@ -646,7 +682,7 @@ public class CassandraConfiguration {
             consistencyLevelRegular, consistencyLevelLightweightTransaction, 
mailboxReadRepair,
             messageReadStrongConsistency, mailboxReadStrongConsistency, 
messageWriteStrongConsistency,
             optimisticConsistencyLevel, mailRepositoryStrongConsistency, 
uidReadStrongConsistency,
-            modseqReadStrongConsistency, aclEnabled, uidModseqIncrement);
+            modseqReadStrongConsistency, aclEnabled, uidModseqIncrement, 
blobRecoveryMode);
     }
 
     @Override
@@ -676,6 +712,7 @@ public class CassandraConfiguration {
             .add("uidReadStrongConsistency", uidReadStrongConsistency)
             .add("aclEnabled", aclEnabled)
             .add("uidModseqIncrement", uidModseqIncrement)
+            .add("blobRecoveryMode", blobRecoveryMode)
             .toString();
     }
 }
diff --git 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3.java
 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3.java
index cc986a1bf8..0b2eea3657 100644
--- 
a/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3.java
+++ 
b/mailbox/cassandra/src/main/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3.java
@@ -41,6 +41,7 @@ import static 
org.apache.james.mailbox.cassandra.table.CassandraMessageV3Table.T
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.Date;
 import java.util.List;
 import java.util.Optional;
@@ -55,6 +56,7 @@ import 
org.apache.james.backends.cassandra.utils.CassandraAsyncExecutor;
 import org.apache.james.backends.cassandra.utils.ProfileLocator;
 import org.apache.james.blob.api.BlobId;
 import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BlobStoreDAO;
 import org.apache.james.mailbox.cassandra.ids.CassandraMessageId;
 import 
org.apache.james.mailbox.cassandra.table.CassandraMessageV3Table.Attachments;
 import org.apache.james.mailbox.model.ByteContent;
@@ -66,6 +68,8 @@ import 
org.apache.james.mailbox.model.MessageAttachmentMetadata;
 import org.apache.james.mailbox.model.StringBackedAttachmentId;
 import org.apache.james.mailbox.store.mail.MessageMapper.FetchType;
 import org.apache.james.mailbox.store.mail.model.MailboxMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import com.datastax.oss.driver.api.core.CqlIdentifier;
 import com.datastax.oss.driver.api.core.CqlSession;
@@ -88,10 +92,12 @@ import reactor.core.scheduler.Schedulers;
 import reactor.util.function.Tuple2;
 
 public class CassandraMessageDAOV3 {
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(CassandraMessageDAOV3.class);
     private static final byte[] EMPTY_BYTE_ARRAY = {};
 
     private final CassandraAsyncExecutor cassandraAsyncExecutor;
     private final BlobStore blobStore;
+    private final BlobStoreDAO blobStoreDAO;
     private final BlobId.Factory blobIdFactory;
     private final PreparedStatement insert;
     private final PreparedStatement delete;
@@ -107,9 +113,11 @@ public class CassandraMessageDAOV3 {
 
     @Inject
     public CassandraMessageDAOV3(CqlSession session, CassandraTypesProvider 
typesProvider, BlobStore blobStore,
-                                 BlobId.Factory blobIdFactory, 
CassandraConfiguration cassandraConfiguration) {
+                                 BlobStoreDAO blobStoreDAO, BlobId.Factory 
blobIdFactory,
+                                 CassandraConfiguration 
cassandraConfiguration) {
         this.cassandraAsyncExecutor = new CassandraAsyncExecutor(session);
         this.blobStore = blobStore;
+        this.blobStoreDAO = blobStoreDAO;
         this.blobIdFactory = blobIdFactory;
 
         this.insert = prepareInsert(session);
@@ -205,10 +213,28 @@ public class CassandraMessageDAOV3 {
                 Mono<BlobId> headerFuture = 
Mono.from(blobStore.save(blobStore.getDefaultBucketName(), headerContent, 
SIZE_BASED));
                 Mono<BlobId> bodyFuture = 
Mono.from(blobStore.save(blobStore.getDefaultBucketName(), bodyByteSource, 
LOW_COST));
 
-                return headerFuture.zipWith(bodyFuture);
+                return headerFuture.zipWith(bodyFuture)
+                    .flatMap(pair -> saveRecovery(pair.getT1(), 
pair.getT2()).thenReturn(pair));
             });
     }
 
+    private Mono<Void> saveRecovery(BlobId headerId, BlobId bodyId) {
+        return switch (configuration.getBlobRecoveryMode()) {
+            case NONE -> Mono.empty();
+            case SYNCHRONOUS -> writeRecoveryBlob(headerId, bodyId);
+            case ASYNCHRONOUS -> Mono.fromRunnable(() ->
+                writeRecoveryBlob(headerId, bodyId)
+                    .subscribeOn(Schedulers.parallel())
+                    .subscribe(ignored -> { }, e -> LOGGER.error("Failed to 
save recovery blob for header={} body={}", headerId.asString(), 
bodyId.asString(), e)));
+        };
+    }
+
+    private Mono<Void> writeRecoveryBlob(BlobId headerId, BlobId bodyId) {
+        BlobId recoveryBlobId = 
blobIdFactory.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + headerId.asString());
+        BlobStoreDAO.BytesBlob content = 
BlobStoreDAO.BytesBlob.of(bodyId.asString().getBytes(StandardCharsets.UTF_8));
+        return Mono.from(blobStoreDAO.save(blobStore.getDefaultBucketName(), 
recoveryBlobId, content));
+    }
+
     private BoundStatement boundWriteStatement(MailboxMessage message, 
Tuple2<BlobId, BlobId> pair) {
         CassandraMessageId messageId = (CassandraMessageId) 
message.getMessageId();
         BoundStatement boundStatement = insert.bind()
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 1a6ffab2f1..a13b5293fb 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
@@ -41,6 +41,7 @@ import 
org.apache.james.backends.cassandra.CassandraClusterExtension;
 import org.apache.james.backends.cassandra.StatementRecorder;
 import 
org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration;
 import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BlobStoreDAO;
 import org.apache.james.blob.api.PlainBlobId;
 import org.apache.james.blob.cassandra.BlobTables;
 import org.apache.james.core.Username;
@@ -832,6 +833,7 @@ public class CassandraMailboxManagerTest extends 
MailboxManagerTest<CassandraMai
                 cassandraCluster.getConf(),
                 cassandraCluster.getTypesProvider(),
                 mock(BlobStore.class),
+                mock(BlobStoreDAO.class),
                 new PlainBlobId.Factory(),
                 CassandraConfiguration.DEFAULT_CONFIGURATION);
         }
diff --git 
a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3Test.java
 
b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3Test.java
index 7f334b5d0e..9a3067e1fc 100644
--- 
a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3Test.java
+++ 
b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/CassandraMessageDAOV3Test.java
@@ -20,6 +20,7 @@ package org.apache.james.mailbox.cassandra.mail;
 
 import static 
org.apache.james.mailbox.store.mail.model.MailboxMessage.EMPTY_SAVE_DATE;
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.nio.charset.StandardCharsets;
 import java.util.Collection;
@@ -35,10 +36,16 @@ import 
org.apache.james.backends.cassandra.CassandraClusterExtension;
 import org.apache.james.backends.cassandra.components.CassandraDataDefinition;
 import 
org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration;
 import 
org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDataDefinition;
+import org.apache.james.blob.api.BlobId;
 import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.apache.james.blob.api.BucketName;
+import org.apache.james.blob.api.ObjectNotFoundException;
 import org.apache.james.blob.api.PlainBlobId;
 import org.apache.james.blob.cassandra.CassandraBlobDataDefinition;
-import org.apache.james.blob.cassandra.CassandraBlobStoreFactory;
+import org.apache.james.blob.cassandra.CassandraBlobStoreDAO;
+import org.apache.james.blob.cassandra.CassandraBucketDAO;
+import org.apache.james.blob.cassandra.CassandraDefaultBucketDAO;
 import org.apache.james.mailbox.MessageUid;
 import org.apache.james.mailbox.ModSeq;
 import org.apache.james.mailbox.cassandra.ids.CassandraId;
@@ -54,6 +61,7 @@ import org.apache.james.mailbox.store.mail.MessageMapper;
 import org.apache.james.mailbox.store.mail.model.MailboxMessage;
 import org.apache.james.mailbox.store.mail.model.impl.SimpleMailboxMessage;
 import org.apache.james.metrics.tests.RecordingMetricFactory;
+import org.apache.james.server.blob.deduplication.BlobStoreFactory;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
@@ -61,6 +69,7 @@ import org.junit.jupiter.api.extension.RegisterExtension;
 import com.google.common.collect.ImmutableList;
 
 import reactor.core.publisher.Mono;
+import reactor.util.function.Tuple2;
 
 class CassandraMessageDAOV3Test {
     private static final int BODY_START = 16;
@@ -87,6 +96,9 @@ class CassandraMessageDAOV3Test {
     private ThreadId threadId;
     private ComposedMessageIdWithMetaData messageIdWithMetadata;
     private MessageBlobReferenceSource blobReferenceSource;
+    private BlobStoreDAO blobStoreDAO;
+    private BlobStore blobStore;
+    private PlainBlobId.Factory blobIdFactory;
 
     @BeforeEach
     void setUp(CassandraCluster cassandra) {
@@ -94,15 +106,19 @@ class CassandraMessageDAOV3Test {
         messageId = messageIdFactory.generate();
         messageId2 = messageIdFactory.generate();
         threadId = ThreadId.fromBaseMessageId(messageId);
-        BlobStore blobStore = 
CassandraBlobStoreFactory.forTesting(cassandra.getConf(), new 
RecordingMetricFactory())
+
+        blobIdFactory = new PlainBlobId.Factory();
+        RecordingMetricFactory metricFactory = new RecordingMetricFactory();
+        CassandraBucketDAO bucketDAO = new CassandraBucketDAO(blobIdFactory, 
cassandra.getConf());
+        CassandraDefaultBucketDAO defaultBucketDAO = new 
CassandraDefaultBucketDAO(cassandra.getConf(), blobIdFactory);
+        blobStoreDAO = new CassandraBlobStoreDAO(defaultBucketDAO, bucketDAO, 
CassandraConfiguration.DEFAULT_CONFIGURATION, BucketName.DEFAULT, 
metricFactory);
+        blobStore = BlobStoreFactory.builder()
+            .blobStoreDAO(blobStoreDAO)
+            .blobIdFactory(blobIdFactory)
+            .defaultBucketName()
             .passthrough();
-        PlainBlobId.Factory blobIdFactory = new PlainBlobId.Factory();
-        testee = new CassandraMessageDAOV3(
-            cassandra.getConf(),
-            cassandra.getTypesProvider(),
-            blobStore,
-            blobIdFactory,
-            CassandraConfiguration.DEFAULT_CONFIGURATION);
+
+        testee = buildTestee(cassandra, 
CassandraConfiguration.DEFAULT_CONFIGURATION);
 
         messageIdWithMetadata = ComposedMessageIdWithMetaData.builder()
                 .composedMessageId(new ComposedMessageId(MAILBOX_ID, 
messageId, messageUid))
@@ -113,6 +129,16 @@ class CassandraMessageDAOV3Test {
         blobReferenceSource = new MessageBlobReferenceSource(testee);
     }
 
+    private CassandraMessageDAOV3 buildTestee(CassandraCluster cassandra, 
CassandraConfiguration configuration) {
+        return new CassandraMessageDAOV3(
+            cassandra.getConf(),
+            cassandra.getTypesProvider(),
+            blobStore,
+            blobStoreDAO,
+            blobIdFactory,
+            configuration);
+    }
+
     @Test
     void saveShouldStoreMessageWithFullContent() throws Exception {
         message = createMessage(messageId, threadId, CONTENT, BODY_START, 
NO_ATTACHMENT, EMPTY_SAVE_DATE);
@@ -145,6 +171,50 @@ class CassandraMessageDAOV3Test {
             .isEmpty();
     }
 
+    @Test
+    void saveShouldNotWriteRecoveryBlobByDefault() throws Exception {
+        message = createMessage(messageId, threadId, CONTENT, BODY_START, 
NO_ATTACHMENT, EMPTY_SAVE_DATE);
+
+        Tuple2<BlobId, BlobId> blobIds = testee.save(message).block();
+
+        BlobId recoveryBlobId = 
blobIdFactory.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + 
blobIds.getT1().asString());
+        assertThatThrownBy(() -> 
Mono.from(blobStoreDAO.readBytes(BucketName.DEFAULT, recoveryBlobId)).block())
+            .isInstanceOf(ObjectNotFoundException.class);
+    }
+
+    @Test
+    void saveShouldWriteRecoveryBlobWhenSynchronousMode(CassandraCluster 
cassandra) throws Exception {
+        CassandraConfiguration conf = CassandraConfiguration.builder()
+            
.blobRecoveryMode(CassandraConfiguration.BlobRecoveryMode.SYNCHRONOUS)
+            .build();
+        CassandraMessageDAOV3 testeeWithRecovery = buildTestee(cassandra, 
conf);
+        message = createMessage(messageId, threadId, CONTENT, BODY_START, 
NO_ATTACHMENT, EMPTY_SAVE_DATE);
+
+        Tuple2<BlobId, BlobId> blobIds = 
testeeWithRecovery.save(message).block();
+
+        BlobId recoveryBlobId = 
blobIdFactory.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + 
blobIds.getT1().asString());
+        byte[] recoveryContent = 
Mono.from(blobStoreDAO.readBytes(BucketName.DEFAULT, 
recoveryBlobId)).block().payload();
+        assertThat(new String(recoveryContent, 
StandardCharsets.UTF_8)).isEqualTo(blobIds.getT2().asString());
+    }
+
+    @Test
+    void saveShouldWriteRecoveryBlobWhenAsynchronousMode(CassandraCluster 
cassandra) throws Exception {
+        CassandraConfiguration conf = CassandraConfiguration.builder()
+            
.blobRecoveryMode(CassandraConfiguration.BlobRecoveryMode.ASYNCHRONOUS)
+            .build();
+        CassandraMessageDAOV3 testeeWithRecovery = buildTestee(cassandra, 
conf);
+        message = createMessage(messageId, threadId, CONTENT, BODY_START, 
NO_ATTACHMENT, EMPTY_SAVE_DATE);
+
+        Tuple2<BlobId, BlobId> blobIds = 
testeeWithRecovery.save(message).block();
+
+        BlobId recoveryBlobId = 
blobIdFactory.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + 
blobIds.getT1().asString());
+        // Asynchronous: give the parallel scheduler a moment to complete
+        assertThat(Mono.from(blobStoreDAO.readBytes(BucketName.DEFAULT, 
recoveryBlobId))
+                .retryWhen(reactor.util.retry.Retry.fixedDelay(10, 
java.time.Duration.ofMillis(100)))
+                .block().payload())
+            
.isEqualTo(blobIds.getT2().asString().getBytes(StandardCharsets.UTF_8));
+    }
+
     @Test
     void blobReferencesShouldReturnAllBlobs() throws Exception {
         message = createMessage(messageId, threadId,  CONTENT, BODY_START, 
NO_ATTACHMENT, EMPTY_SAVE_DATE);
diff --git 
a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/utils/GuiceUtils.java
 
b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/utils/GuiceUtils.java
index 1204119170..bf93e5d4de 100644
--- 
a/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/utils/GuiceUtils.java
+++ 
b/mailbox/cassandra/src/test/java/org/apache/james/mailbox/cassandra/mail/utils/GuiceUtils.java
@@ -27,8 +27,13 @@ import 
org.apache.james.backends.cassandra.init.CassandraTypesProvider;
 import 
org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration;
 import org.apache.james.blob.api.BlobId;
 import org.apache.james.blob.api.BlobStore;
+import org.apache.james.blob.api.BlobStoreDAO;
+import org.apache.james.blob.api.BucketName;
 import org.apache.james.blob.api.PlainBlobId;
+import org.apache.james.blob.cassandra.CassandraBlobStoreDAO;
 import org.apache.james.blob.cassandra.CassandraBlobStoreFactory;
+import org.apache.james.blob.cassandra.CassandraBucketDAO;
+import org.apache.james.blob.cassandra.CassandraDefaultBucketDAO;
 import org.apache.james.eventsourcing.Event;
 import org.apache.james.eventsourcing.eventstore.EventNestedTypes;
 import org.apache.james.eventsourcing.eventstore.EventStore;
@@ -91,6 +96,12 @@ public class GuiceUtils {
             binder -> 
binder.bind(ACLMapper.class).to(CassandraACLMapper.class),
             binder -> binder.bind(BlobId.Factory.class).toInstance(new 
PlainBlobId.Factory()),
             binder -> binder.bind(BlobStore.class).toProvider(() -> 
CassandraBlobStoreFactory.forTesting(session, new 
RecordingMetricFactory()).passthrough()),
+            binder -> binder.bind(BlobStoreDAO.class).toProvider(() -> {
+                PlainBlobId.Factory blobIdFactory = new PlainBlobId.Factory();
+                CassandraBucketDAO bucketDAO = new 
CassandraBucketDAO(blobIdFactory, session);
+                CassandraDefaultBucketDAO defaultBucketDAO = new 
CassandraDefaultBucketDAO(session, blobIdFactory);
+                return new CassandraBlobStoreDAO(defaultBucketDAO, bucketDAO, 
configuration, BucketName.DEFAULT, new RecordingMetricFactory());
+            }),
             binder -> binder.bind(CqlSession.class).toInstance(session),
             binder -> Multibinder.newSetBinder(binder, new 
TypeLiteral<EventDTOModule<? extends Event, ? extends EventDTO>>() {})
                 .addBinding().toInstance(ACLModule.ACL_UPDATE),
diff --git 
a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
 
b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
index a527dad825..41d2c257d2 100644
--- 
a/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
+++ 
b/server/blob/blob-api/src/main/java/org/apache/james/blob/api/BlobStoreDAO.java
@@ -52,6 +52,8 @@ import com.google.common.io.FileBackedOutputStream;
  * <p>See {@code docs/modules/servers/partials/architecture/blobstore.adoc} 
for more details.</p>
  */
 public interface BlobStoreDAO {
+    String RECOVERY_BLOB_PREFIX = "recovery/";
+
     record BlobMetadataName(String name) {
         private static final CharMatcher CHAR_MATCHER = 
CharMatcher.inRange('a', 'z')
             .or(CharMatcher.inRange('A', 'Z'))
diff --git 
a/server/blob/blob-file/src/main/java/org/apache/james/blob/file/FileBlobStoreDAO.java
 
b/server/blob/blob-file/src/main/java/org/apache/james/blob/file/FileBlobStoreDAO.java
index 0a5d1731c0..c1db4d532a 100644
--- 
a/server/blob/blob-file/src/main/java/org/apache/james/blob/file/FileBlobStoreDAO.java
+++ 
b/server/blob/blob-file/src/main/java/org/apache/james/blob/file/FileBlobStoreDAO.java
@@ -39,6 +39,8 @@ import java.nio.file.attribute.UserDefinedFileAttributeView;
 import java.time.Duration;
 import java.util.Collection;
 import java.util.Map;
+import java.util.stream.Collectors;
+import java.util.stream.StreamSupport;
 
 import jakarta.inject.Inject;
 
@@ -234,13 +236,23 @@ public class FileBlobStoreDAO implements BlobStoreDAO {
     public Publisher<BlobId> listBlobs(BucketName bucketName) {
         return Mono.fromCallable(() -> {
                 File bucketRoot = getBucketRoot(bucketName);
-                return Files.list(bucketRoot.toPath());
+                Path rootPath = bucketRoot.toPath();
+                // Blob ids may contain '/' (eg. recovery sidecar keys) and 
are then stored in nested
+                // directories, so we walk the tree and rebuild the id from 
the bucket-root-relative path.
+                return Files.walk(rootPath)
+                    .filter(Files::isRegularFile)
+                    .map(path -> 
blobIdFactory.parse(toBlobId(rootPath.relativize(path))));
             })
             .flatMapMany(Flux::fromStream)
-            .map(path -> blobIdFactory.parse(path.getFileName().toString()))
             .subscribeOn(Schedulers.boundedElastic());
     }
 
+    private String toBlobId(Path relativePath) {
+        return StreamSupport.stream(relativePath.spliterator(), false)
+            .map(Path::toString)
+            .collect(Collectors.joining("/"));
+    }
+
     private BlobMetadata readMetadata(Path path) {
         UserDefinedFileAttributeView attributeView = 
Files.getFileAttributeView(path, UserDefinedFileAttributeView.class);
         if (attributeView == null) {
@@ -254,6 +266,8 @@ public class FileBlobStoreDAO implements BlobStoreDAO {
                     this::asBlobMetadataName,
                     Throwing.function((String attributeName) -> new 
BlobMetadataValue(readFileAttributeValue(attributeView, attributeName)))
                         .sneakyThrow())));
+        } catch (NoSuchFileException e) {
+            throw new ObjectNotFoundException(String.format("Cannot locate 
%s", path), e);
         } catch (IOException e) {
             throw new ObjectStoreIOException("IOException occurred", e);
         }
@@ -299,6 +313,9 @@ public class FileBlobStoreDAO implements BlobStoreDAO {
 
     private File createTempFile(File blob) {
         try {
+            // Blob ids may contain '/' (eg. recovery sidecar keys), 
introducing nested directories
+            // that must exist before the temp file is created alongside the 
target blob.
+            Files.createDirectories(blob.getParentFile().toPath());
             return Files.createTempFile(blob.getParentFile().toPath(), 
blob.getName(), ".tmp").toFile();
         } catch (IOException e) {
             throw new ObjectStoreIOException("IOException occurred", e);
diff --git 
a/server/blob/blob-storage-strategy/src/main/java/org/apache/james/server/blob/deduplication/BloomFilterGCAlgorithm.java
 
b/server/blob/blob-storage-strategy/src/main/java/org/apache/james/server/blob/deduplication/BloomFilterGCAlgorithm.java
index 67fa255aec..f9a9ea10fa 100644
--- 
a/server/blob/blob-storage-strategy/src/main/java/org/apache/james/server/blob/deduplication/BloomFilterGCAlgorithm.java
+++ 
b/server/blob/blob-storage-strategy/src/main/java/org/apache/james/server/blob/deduplication/BloomFilterGCAlgorithm.java
@@ -25,10 +25,12 @@ import java.nio.charset.StandardCharsets;
 import java.time.Clock;
 import java.time.Instant;
 import java.util.Collection;
+import java.util.List;
 import java.util.Objects;
 import java.util.Optional;
 import java.util.UUID;
 import java.util.concurrent.atomic.AtomicLong;
+import java.util.stream.Collectors;
 
 import org.apache.james.blob.api.BlobId;
 import org.apache.james.blob.api.BlobReferenceSource;
@@ -39,6 +41,7 @@ import org.apache.james.task.Task.Result;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import com.google.common.annotations.VisibleForTesting;
 import com.google.common.base.MoreObjects;
 import com.google.common.hash.BloomFilter;
 import com.google.common.hash.Funnel;
@@ -51,6 +54,8 @@ public class BloomFilterGCAlgorithm {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(BloomFilterGCAlgorithm.class);
     private static final Funnel<CharSequence> BLOOM_FILTER_FUNNEL = 
Funnels.stringFunnel(StandardCharsets.US_ASCII);
+    @VisibleForTesting
+    static boolean RECOVERY_AWARE = 
Boolean.parseBoolean(System.getProperty("james.gc.recover.aware", "true"));
 
     public static class Context {
 
@@ -273,6 +278,7 @@ public class BloomFilterGCAlgorithm {
 
     private Mono<Result> gc(BloomFilter<CharSequence> bloomFilter, BucketName 
bucketName, Context context, int deletionWindowSize) {
         return Flux.from(blobStoreDAO.listBlobs(bucketName))
+            .filter(blobId -> !RECOVERY_AWARE || 
!blobId.asString().startsWith(BlobStoreDAO.RECOVERY_BLOB_PREFIX))
             .doOnNext(blobId -> context.incrementBlobCount())
             .flatMap(blobId -> Mono.fromCallable(() -> 
blobIdFactory.parse(blobId.asString())))
             .filter(blobId -> {
@@ -290,15 +296,28 @@ public class BloomFilterGCAlgorithm {
 
     private Mono<Result> handlePagedDeletion(BucketName bucketName, Context 
context, Flux<BlobId> blobIdFlux) {
         return blobIdFlux.collectList()
-            .flatMap(orphanBlobIds -> 
Mono.from(blobStoreDAO.delete(bucketName, (Collection) orphanBlobIds))
-                .then(Mono.fromCallable(() -> {
-                    context.incrementGCedBlobCount(orphanBlobIds.size());
-                    return Result.COMPLETED;
-                })).onErrorResume(error -> {
-                    LOGGER.error("Error when gc orphan blob", error);
-                    context.incrementErrorCount();
-                    return Mono.just(Result.PARTIAL);
-                }));
+            .flatMap(orphanBlobIds -> {
+                Mono<Void> deleteRecoverySidecars = RECOVERY_AWARE
+                    ? Mono.from(blobStoreDAO.delete(bucketName, 
toRecoveryBlobIds(orphanBlobIds)))
+                    : Mono.empty();
+
+                return Mono.from(blobStoreDAO.delete(bucketName, (Collection) 
orphanBlobIds))
+                    .then(deleteRecoverySidecars)
+                    .then(Mono.fromCallable(() -> {
+                        context.incrementGCedBlobCount(orphanBlobIds.size());
+                        return Result.COMPLETED;
+                    })).onErrorResume(error -> {
+                        LOGGER.error("Error when gc orphan blob", error);
+                        context.incrementErrorCount();
+                        return Mono.just(Result.PARTIAL);
+                    });
+            });
+    }
+
+    private List<BlobId> toRecoveryBlobIds(List<BlobId> blobIds) {
+        return blobIds.stream()
+            .map(blobId -> 
blobIdFactory.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + blobId.asString()))
+            .collect(Collectors.toList());
     }
 
     private Mono<BloomFilter<CharSequence>> populatedBloomFilter(int 
expectedBlobCount, double associatedProbability, Context context) {
diff --git 
a/server/blob/blob-storage-strategy/src/main/java/org/apache/james/server/blob/deduplication/GenerationAwareBlobId.java
 
b/server/blob/blob-storage-strategy/src/main/java/org/apache/james/server/blob/deduplication/GenerationAwareBlobId.java
index 67fd492b16..ddd01b08f6 100644
--- 
a/server/blob/blob-storage-strategy/src/main/java/org/apache/james/server/blob/deduplication/GenerationAwareBlobId.java
+++ 
b/server/blob/blob-storage-strategy/src/main/java/org/apache/james/server/blob/deduplication/GenerationAwareBlobId.java
@@ -27,6 +27,7 @@ import java.util.Objects;
 import java.util.Optional;
 
 import org.apache.james.blob.api.BlobId;
+import org.apache.james.blob.api.BlobStoreDAO;
 import org.apache.james.util.DurationParser;
 
 import com.google.common.annotations.VisibleForTesting;
@@ -128,6 +129,11 @@ public class GenerationAwareBlobId implements BlobId, 
GenerationAware {
 
         @Override
         public GenerationAwareBlobId parse(String id) {
+            // Recovery sidecar keys (eg. recovery/1_2_blobId) do not follow 
the family_generation_blobId
+            // layout: keep them as a plain, non-generation-aware blob id 
preserving the original string.
+            if (id.startsWith(BlobStoreDAO.RECOVERY_BLOB_PREFIX)) {
+                return decorateWithoutGeneration(id);
+            }
             int separatorIndex1 = id.indexOf('_');
             if (separatorIndex1 == -1 || separatorIndex1 == id.length() - 1) {
                 return decorateWithoutGeneration(id);
diff --git 
a/server/blob/blob-storage-strategy/src/test/java/org/apache/james/server/blob/deduplication/BloomFilterGCAlgorithmContract.java
 
b/server/blob/blob-storage-strategy/src/test/java/org/apache/james/server/blob/deduplication/BloomFilterGCAlgorithmContract.java
index 39397f1f14..46f398611f 100644
--- 
a/server/blob/blob-storage-strategy/src/test/java/org/apache/james/server/blob/deduplication/BloomFilterGCAlgorithmContract.java
+++ 
b/server/blob/blob-storage-strategy/src/test/java/org/apache/james/server/blob/deduplication/BloomFilterGCAlgorithmContract.java
@@ -47,6 +47,7 @@ import org.apache.james.task.Task;
 import org.apache.james.utils.UpdatableTickingClock;
 import org.awaitility.Awaitility;
 import org.awaitility.core.ConditionFactory;
+import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.RepeatedTest;
 import org.junit.jupiter.api.Test;
@@ -83,6 +84,11 @@ public interface BloomFilterGCAlgorithmContract {
         CLOCK.setInstant(NOW.toInstant());
     }
 
+    @AfterEach
+    default void tearDown() {
+        BloomFilterGCAlgorithm.RECOVERY_AWARE = false;
+    }
+
     default BlobStore blobStore() {
         return new DeDuplicationBlobStore(blobStoreDAO(), DEFAULT_BUCKET, 
GENERATION_AWARE_BLOB_ID_FACTORY);
     }
@@ -231,6 +237,83 @@ public interface BloomFilterGCAlgorithmContract {
         });
     }
 
+    @Test
+    default void 
gcShouldPreserveRecoverySidecarOfReferencedBlobWhenRecoveryAware() {
+        // Without RECOVERY_AWARE the recovery sidecar has NO_FAMILY → 
inActiveGeneration()=false
+        // → treated as orphan and deleted even though its parent blob is 
alive. This tests the fix.
+        BloomFilterGCAlgorithm.RECOVERY_AWARE = true;
+        BlobStore blobStore = blobStore();
+        BlobId referencedId = Mono.from(blobStore.save(DEFAULT_BUCKET, 
UUID.randomUUID().toString(), 
BlobStore.StoragePolicy.HIGH_PERFORMANCE)).block();
+        BlobId recoveryBlobId = 
GENERATION_AWARE_BLOB_ID_FACTORY.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + 
referencedId.asString());
+        Mono.from(blobStoreDAO().save(DEFAULT_BUCKET, recoveryBlobId, 
BlobStoreDAO.BytesBlob.of("bodyBlobId".getBytes()))).block();
+
+        
when(BLOB_REFERENCE_SOURCE.listReferencedBlobs()).thenReturn(Flux.just(referencedId));
+        CLOCK.setInstant(NOW.plusMonths(2).toInstant());
+
+        Context context = new Context(EXPECTED_BLOB_COUNT, 
ASSOCIATED_PROBABILITY);
+        Mono.from(bloomFilterGCAlgorithm().gc(EXPECTED_BLOB_COUNT, 
DELETION_WINDOW_SIZE, ASSOCIATED_PROBABILITY, DEFAULT_BUCKET, context)).block();
+
+        assertThat(blobStore.read(DEFAULT_BUCKET, referencedId)).isNotNull();
+        assertThat(Mono.from(blobStoreDAO().readBytes(DEFAULT_BUCKET, 
recoveryBlobId)).block()).isNotNull();
+    }
+
+    @Test
+    default void 
gcShouldDeleteRecoverySidecarOfReferencedBlobWhenNotRecoveryAware() {
+        // Documents the unsafe behavior when the flag is off: recovery 
sidecar of a live blob is GC-ed.
+        BlobStore blobStore = blobStore();
+        BlobId referencedId = Mono.from(blobStore.save(DEFAULT_BUCKET, 
UUID.randomUUID().toString(), 
BlobStore.StoragePolicy.HIGH_PERFORMANCE)).block();
+        BlobId recoveryBlobId = 
GENERATION_AWARE_BLOB_ID_FACTORY.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + 
referencedId.asString());
+        Mono.from(blobStoreDAO().save(DEFAULT_BUCKET, recoveryBlobId, 
BlobStoreDAO.BytesBlob.of("bodyBlobId".getBytes()))).block();
+
+        
when(BLOB_REFERENCE_SOURCE.listReferencedBlobs()).thenReturn(Flux.just(referencedId));
+        CLOCK.setInstant(NOW.plusMonths(2).toInstant());
+
+        Context context = new Context(EXPECTED_BLOB_COUNT, 
ASSOCIATED_PROBABILITY);
+        Mono.from(bloomFilterGCAlgorithm().gc(EXPECTED_BLOB_COUNT, 
DELETION_WINDOW_SIZE, ASSOCIATED_PROBABILITY, DEFAULT_BUCKET, context)).block();
+
+        assertThat(blobStore.read(DEFAULT_BUCKET, referencedId)).isNotNull();
+        assertThatThrownBy(() -> 
Mono.from(blobStoreDAO().readBytes(DEFAULT_BUCKET, recoveryBlobId)).block())
+            .isInstanceOf(ObjectNotFoundException.class);
+    }
+
+    @Test
+    default void 
gcShouldDeleteRecoverySidecarAlongsideOrphanBlobWhenRecoveryAware() {
+        BloomFilterGCAlgorithm.RECOVERY_AWARE = true;
+        BlobStore blobStore = blobStore();
+        BlobId orphanId = Mono.from(blobStore.save(DEFAULT_BUCKET, 
UUID.randomUUID().toString(), 
BlobStore.StoragePolicy.HIGH_PERFORMANCE)).block();
+        BlobId recoveryBlobId = 
GENERATION_AWARE_BLOB_ID_FACTORY.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + 
orphanId.asString());
+        Mono.from(blobStoreDAO().save(DEFAULT_BUCKET, recoveryBlobId, 
BlobStoreDAO.BytesBlob.of("bodyBlobId".getBytes()))).block();
+
+        
when(BLOB_REFERENCE_SOURCE.listReferencedBlobs()).thenReturn(Flux.empty());
+        CLOCK.setInstant(NOW.plusMonths(2).toInstant());
+
+        Context context = new Context(EXPECTED_BLOB_COUNT, 
ASSOCIATED_PROBABILITY);
+        Mono.from(bloomFilterGCAlgorithm().gc(EXPECTED_BLOB_COUNT, 
DELETION_WINDOW_SIZE, ASSOCIATED_PROBABILITY, DEFAULT_BUCKET, context)).block();
+
+        assertThatThrownBy(() -> blobStore.read(DEFAULT_BUCKET, orphanId))
+            .isInstanceOf(ObjectNotFoundException.class);
+        assertThatThrownBy(() -> 
Mono.from(blobStoreDAO().readBytes(DEFAULT_BUCKET, recoveryBlobId)).block())
+            .isInstanceOf(ObjectNotFoundException.class);
+    }
+
+    @Test
+    default void gcShouldNotCountRecoveryBlobsInStatsWhenRecoveryAware() {
+        BloomFilterGCAlgorithm.RECOVERY_AWARE = true;
+        BlobStore blobStore = blobStore();
+        BlobId orphanId = Mono.from(blobStore.save(DEFAULT_BUCKET, 
UUID.randomUUID().toString(), 
BlobStore.StoragePolicy.HIGH_PERFORMANCE)).block();
+        BlobId recoveryBlobId = 
GENERATION_AWARE_BLOB_ID_FACTORY.parse(BlobStoreDAO.RECOVERY_BLOB_PREFIX + 
orphanId.asString());
+        Mono.from(blobStoreDAO().save(DEFAULT_BUCKET, recoveryBlobId, 
BlobStoreDAO.BytesBlob.of("bodyBlobId".getBytes()))).block();
+
+        
when(BLOB_REFERENCE_SOURCE.listReferencedBlobs()).thenReturn(Flux.empty());
+        CLOCK.setInstant(NOW.plusMonths(2).toInstant());
+
+        Context context = new Context(EXPECTED_BLOB_COUNT, 
ASSOCIATED_PROBABILITY);
+        Mono.from(bloomFilterGCAlgorithm().gc(EXPECTED_BLOB_COUNT, 
DELETION_WINDOW_SIZE, ASSOCIATED_PROBABILITY, DEFAULT_BUCKET, context)).block();
+
+        assertThat(context.snapshot().getBlobCount()).isEqualTo(1);
+        assertThat(context.snapshot().getGcedBlobCount()).isEqualTo(1);
+    }
+
     @Test
     default void gcShouldHandlerErrorWhenException() {
         
when(BLOB_REFERENCE_SOURCE.listReferencedBlobs()).thenReturn(Flux.empty());
diff --git 
a/server/protocols/protocols-pop3-distributed/src/test/java/org/apache/james/pop3server/mailbox/task/MetaDataFixInconsistenciesServiceTest.java
 
b/server/protocols/protocols-pop3-distributed/src/test/java/org/apache/james/pop3server/mailbox/task/MetaDataFixInconsistenciesServiceTest.java
index 2269094877..88edbd757d 100644
--- 
a/server/protocols/protocols-pop3-distributed/src/test/java/org/apache/james/pop3server/mailbox/task/MetaDataFixInconsistenciesServiceTest.java
+++ 
b/server/protocols/protocols-pop3-distributed/src/test/java/org/apache/james/pop3server/mailbox/task/MetaDataFixInconsistenciesServiceTest.java
@@ -31,6 +31,7 @@ import 
org.apache.james.backends.cassandra.CassandraClusterExtension;
 import org.apache.james.backends.cassandra.components.CassandraDataDefinition;
 import 
org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration;
 import 
org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDataDefinition;
+import org.apache.james.blob.api.BlobStoreDAO;
 import org.apache.james.blob.api.PlainBlobId;
 import org.apache.james.blob.cassandra.CassandraBlobDataDefinition;
 import org.apache.james.blob.cassandra.CassandraBlobStoreFactory;
@@ -60,6 +61,7 @@ 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.mockito.Mockito;
 
 import com.google.common.collect.ImmutableList;
 
@@ -166,6 +168,7 @@ public class MetaDataFixInconsistenciesServiceTest {
             cassandra.getTypesProvider(),
             CassandraBlobStoreFactory.forTesting(cassandra.getConf(), new 
RecordingMetricFactory())
                 .passthrough(),
+            Mockito.mock(BlobStoreDAO.class),
             new PlainBlobId.Factory(),
             CassandraConfiguration.DEFAULT_CONFIGURATION);
         testee = new MetaDataFixInconsistenciesService(imapUidDAO, 
pop3MetadataStore, cassandraMessageDAOV3);
diff --git a/server/protocols/webadmin/webadmin-pop3/pom.xml 
b/server/protocols/webadmin/webadmin-pop3/pom.xml
index c406d7a901..05f4b7a673 100644
--- a/server/protocols/webadmin/webadmin-pop3/pom.xml
+++ b/server/protocols/webadmin/webadmin-pop3/pom.xml
@@ -82,6 +82,11 @@
             <artifactId>rest-assured</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.testcontainers</groupId>
             <artifactId>testcontainers</artifactId>
diff --git 
a/server/protocols/webadmin/webadmin-pop3/src/test/java/org/apache/james/pop3/webadmin/Pop3MetaDataFixInconsistenciesRoutesTest.java
 
b/server/protocols/webadmin/webadmin-pop3/src/test/java/org/apache/james/pop3/webadmin/Pop3MetaDataFixInconsistenciesRoutesTest.java
index cfae5f9bdc..e0e31212b6 100644
--- 
a/server/protocols/webadmin/webadmin-pop3/src/test/java/org/apache/james/pop3/webadmin/Pop3MetaDataFixInconsistenciesRoutesTest.java
+++ 
b/server/protocols/webadmin/webadmin-pop3/src/test/java/org/apache/james/pop3/webadmin/Pop3MetaDataFixInconsistenciesRoutesTest.java
@@ -35,6 +35,7 @@ import 
org.apache.james.backends.cassandra.CassandraClusterExtension;
 import org.apache.james.backends.cassandra.components.CassandraDataDefinition;
 import 
org.apache.james.backends.cassandra.init.configuration.CassandraConfiguration;
 import 
org.apache.james.backends.cassandra.versions.CassandraSchemaVersionDataDefinition;
+import org.apache.james.blob.api.BlobStoreDAO;
 import org.apache.james.blob.api.PlainBlobId;
 import org.apache.james.blob.cassandra.CassandraBlobDataDefinition;
 import org.apache.james.blob.cassandra.CassandraBlobStoreFactory;
@@ -74,6 +75,7 @@ import org.junit.jupiter.api.AfterEach;
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.extension.RegisterExtension;
+import org.mockito.Mockito;
 
 import com.google.common.collect.ImmutableList;
 
@@ -184,6 +186,7 @@ class Pop3MetaDataFixInconsistenciesRoutesTest {
             cassandra.getTypesProvider(),
             CassandraBlobStoreFactory.forTesting(cassandra.getConf(), new 
RecordingMetricFactory())
                 .passthrough(),
+            Mockito.mock(BlobStoreDAO.class),
             new PlainBlobId.Factory(),
             CassandraConfiguration.DEFAULT_CONFIGURATION);
         MetaDataFixInconsistenciesService fixInconsistenciesService = new 
MetaDataFixInconsistenciesService(imapUidDAO, pop3MetadataStore, 
cassandraMessageDAOV3);


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]


Reply via email to