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

rcordier 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 b2b99615ca Improve IMAP partial body fetch (#2740)
b2b99615ca is described below

commit b2b99615ca2e705e199ffb91d1c99acc332fdf21
Author: Benoit TELLIER <btell...@linagora.com>
AuthorDate: Wed Jun 18 06:53:49 2025 +0200

    Improve IMAP partial body fetch (#2740)
---
 docs/modules/servers/partials/configure/imap.adoc  |  17 +++
 .../james/imap/processor/DefaultProcessor.java     |   5 +-
 .../james/imap/processor/fetch/FetchProcessor.java | 123 +++++++++++++++++++--
 .../main/DefaultImapProcessorFactory.java          |  26 ++++-
 .../james/modules/protocols/IMAPServerModule.java  |   6 +
 .../imapserver/netty/IMAPHealthCheckTest.java      |   4 +-
 .../james/imapserver/netty/IMAPServerTest.java     |  30 +++--
 src/site/xdoc/server/config-imap4.xml              |  17 +++
 8 files changed, 208 insertions(+), 20 deletions(-)

diff --git a/docs/modules/servers/partials/configure/imap.adoc 
b/docs/modules/servers/partials/configure/imap.adoc
index b6df670256..0d3084c121 100644
--- a/docs/modules/servers/partials/configure/imap.adoc
+++ b/docs/modules/servers/partials/configure/imap.adoc
@@ -211,3 +211,20 @@ James extensions.
 == Mail user agents auto-configuration
 
 Check this example on 
link:https://github.com/apache/james-project/tree/master/examples/imap-autoconf[Mail
 user agents auto-configuration].
+
+== Local cache for partial Fetch
+
+Because some clients uses partial fetch in order to emulate retriable download 
of individual body parts
+we offer a way for James to cache in the IMAP session the latest partially 
fetched message. This is done using
+a weak reference, a total size dedicated to the cache as well as a time to 
leave cache cleanup, all of this
+being configurable.
+
+Example:
+
+....
+<imapserver>
+  <partialBodyFetchCacheEnabled>true</partialBodyFetchCacheEnabled>
+  <partialBodyFetchCacheDuration>2min</partialBodyFetchCacheDuration>
+  <partialBodyFetchCacheSize>500 MiB</partialBodyFetchCacheSize>
+</imapserver>
+....
\ No newline at end of file
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
index 2ce7f03091..794954c85d 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/DefaultProcessor.java
@@ -57,7 +57,8 @@ public class DefaultProcessor implements ImapProcessor {
                                                        QuotaManager 
quotaManager,
                                                        QuotaRootResolver 
quotaRootResolver,
                                                        MailboxCounterCorrector 
mailboxCounterCorrector,
-                                                       MetricFactory 
metricFactory) {
+                                                       MetricFactory 
metricFactory,
+                                                       
FetchProcessor.LocalCacheConfiguration localCacheConfiguration) {
         PathConverter.Factory pathConverterFactory = 
PathConverter.Factory.DEFAULT;
 
         ImmutableList.Builder<AbstractProcessor> builder = 
ImmutableList.builder();
@@ -91,7 +92,7 @@ public class DefaultProcessor implements ImapProcessor {
         builder.add(new SearchProcessor(mailboxManager, statusResponseFactory, 
metricFactory));
         builder.add(new SelectProcessor(mailboxManager, eventBus, 
statusResponseFactory, metricFactory, pathConverterFactory, 
mailboxCounterCorrector));
         builder.add(new NamespaceProcessor(mailboxManager, 
statusResponseFactory, metricFactory, new NamespaceSupplier.Default()));
-        builder.add(new FetchProcessor(mailboxManager, statusResponseFactory, 
metricFactory));
+        builder.add(new FetchProcessor(mailboxManager, statusResponseFactory, 
metricFactory, localCacheConfiguration));
         builder.add(new StartTLSProcessor(statusResponseFactory));
         builder.add(new UnselectProcessor(mailboxManager, 
statusResponseFactory, metricFactory));
         builder.add(new CompressProcessor(statusResponseFactory));
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchProcessor.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchProcessor.java
index 5a19054e0b..0ab9930ebb 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchProcessor.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/fetch/FetchProcessor.java
@@ -23,16 +23,23 @@ import static 
org.apache.james.mailbox.MessageManager.MailboxMetaData.RecentMode
 import static org.apache.james.mailbox.model.FetchGroup.FULL_CONTENT;
 import static org.apache.james.util.ReactorUtils.logOnError;
 
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.time.Duration;
+import java.time.temporal.ChronoUnit;
 import java.util.ArrayList;
 import java.util.EnumSet;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
 import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
 import java.util.concurrent.atomic.AtomicReference;
 
 import jakarta.inject.Inject;
 
+import org.apache.commons.configuration2.Configuration;
 import org.apache.james.core.Username;
 import org.apache.james.imap.api.ImapConstants;
 import org.apache.james.imap.api.display.HumanReadableText;
@@ -55,12 +62,15 @@ import org.apache.james.mailbox.MessageUid;
 import org.apache.james.mailbox.exception.MailboxException;
 import org.apache.james.mailbox.exception.MessageRangeException;
 import org.apache.james.mailbox.model.FetchGroup;
+import org.apache.james.mailbox.model.MailboxId;
 import org.apache.james.mailbox.model.MessageRange;
 import org.apache.james.mailbox.model.MessageResult;
 import org.apache.james.metrics.api.MetricFactory;
 import org.apache.james.util.AuditTrail;
+import org.apache.james.util.DurationParser;
 import org.apache.james.util.MDCBuilder;
 import org.apache.james.util.ReactorUtils;
+import org.apache.james.util.SizeFormat;
 import org.reactivestreams.Subscriber;
 import org.reactivestreams.Subscription;
 import org.slf4j.Logger;
@@ -77,6 +87,88 @@ import reactor.core.publisher.Mono;
 import reactor.core.publisher.Sinks;
 
 public class FetchProcessor extends AbstractMailboxProcessor<FetchRequest> {
+    public static final String CACHE_KEY = "message-saved-for-later";
+
+    public record LocalCacheConfiguration(Duration ttl, long sizeInBytes, 
boolean enabled) {
+        public static final LocalCacheConfiguration DEFAULT = new 
LocalCacheConfiguration(Duration.ofMinutes(2), 500 * 1024 * 1024, false);
+
+        public static LocalCacheConfiguration from(Configuration 
configuration) {
+            return new LocalCacheConfiguration(
+                
DurationParser.parse(configuration.getString("partialBodyFetchCacheDuration", 
"2m"), ChronoUnit.MINUTES),
+                
SizeFormat.parseAsByteCount(configuration.getString("partialBodyFetchCacheSize",
 "500 MiB")),
+                configuration.getBoolean("partialBodyFetchCacheEnabled", 
false));
+        }
+    }
+
+    record LocalCacheEntry(FetchGroup fetchGroup, MessageResult message) {
+
+    }
+
+    interface LocalMessageCache {
+        Optional<MessageResult> lookupInCache(MailboxId mailboxId, MessageUid 
uid, FetchGroup fetchGroup);
+
+        void saveForLater(MessageResult messageResult, FetchGroup fetchGroup);
+    }
+
+    public static class DefaultLocalMessageCache implements LocalMessageCache {
+        public static final ReferenceQueue<LocalCacheEntry> REFERENCE_QUEUE = 
new ReferenceQueue<>();
+        public static final AtomicLong TOTAL_SIZE = new AtomicLong(0);
+        private final ImapSession imapSession;
+        private final LocalCacheConfiguration configuration;
+
+        DefaultLocalMessageCache(ImapSession imapSession, 
LocalCacheConfiguration configuration) {
+            this.imapSession = imapSession;
+            this.configuration = configuration;
+        }
+
+        @Override
+        public Optional<MessageResult> lookupInCache(MailboxId mailboxId, 
MessageUid uid, FetchGroup fetchGroup) {
+            if (!configuration.enabled()) {
+                return Optional.empty();
+            }
+            return retrieveCachedEntry()
+                .filter(entry -> entry.fetchGroup().equals(fetchGroup))
+                .filter(entry -> 
entry.message().getMailboxId().equals(mailboxId))
+                .filter(entry -> entry.message().getUid().equals(uid))
+                .map(LocalCacheEntry::message);
+        }
+
+        private Optional<LocalCacheEntry> retrieveCachedEntry() {
+            Optional maybeMessage = 
Optional.ofNullable(imapSession.getAttribute(CACHE_KEY));
+
+            Optional<LocalCacheEntry> cacheContent = 
maybeMessage.filter(SoftReference.class::isInstance)
+                .flatMap(ref -> {
+                    SoftReference softReference = (SoftReference) ref;
+                    return Optional.ofNullable(softReference.get());
+                })
+                .filter(LocalCacheEntry.class::isInstance)
+                .map(LocalCacheEntry.class::cast);
+            return cacheContent;
+        }
+
+        @Override
+        public void saveForLater(MessageResult messageResult, FetchGroup 
fetchGroup) {
+            if (!configuration.enabled()) {
+                return;
+            }
+            if (TOTAL_SIZE.get() <= configuration.sizeInBytes()) {
+                SoftReference<LocalCacheEntry> ref = new SoftReference<>(new 
LocalCacheEntry(fetchGroup, messageResult), REFERENCE_QUEUE);
+                TOTAL_SIZE.addAndGet(messageResult.getSize());
+                imapSession.setAttribute(CACHE_KEY, ref);
+                imapSession.schedule(() -> {
+                    retrieveCachedEntry().ifPresent(entry -> 
TOTAL_SIZE.addAndGet(-1 * entry.message().getSize()));
+                    imapSession.setAttribute(CACHE_KEY, null);
+                }, configuration.ttl());
+
+                Reference<? extends LocalCacheEntry> referenceFromQueue;
+                while ((referenceFromQueue = REFERENCE_QUEUE.poll()) != null) {
+                    Optional.ofNullable(referenceFromQueue.get())
+                        .ifPresent(entry -> TOTAL_SIZE.addAndGet(-1 * 
entry.message().getSize()));
+                }
+            }
+        }
+    }
+
     static class FetchSubscriber implements Subscriber<FetchResponse> {
         private final AtomicReference<Subscription> subscription = new 
AtomicReference<>();
         private final Sinks.One<Void> sink = Sinks.one();
@@ -142,10 +234,13 @@ public class FetchProcessor extends 
AbstractMailboxProcessor<FetchRequest> {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(FetchProcessor.class);
 
+    private final LocalCacheConfiguration localCacheConfiguration;
+
     @Inject
     public FetchProcessor(MailboxManager mailboxManager, StatusResponseFactory 
factory,
-                          MetricFactory metricFactory) {
+                          MetricFactory metricFactory, LocalCacheConfiguration 
localCacheConfiguration) {
         super(FetchRequest.class, mailboxManager, factory, metricFactory);
+        this.localCacheConfiguration = localCacheConfiguration;
     }
 
     @Override
@@ -253,12 +348,26 @@ public class FetchProcessor extends 
AbstractMailboxProcessor<FetchRequest> {
                 .then();
         } else {
             FetchSubscriber fetchSubscriber = new FetchSubscriber(imapSession, 
responder);
-            Flux.fromIterable(consolidate(selected, ranges, fetch))
-                .doOnNext(range -> auditTrail(mailbox, mailboxSession, 
resultToFetch, range))
-                .concatMap(range -> 
Flux.from(mailbox.getMessagesReactive(range, resultToFetch, mailboxSession)))
-                .filter(ids -> !fetch.contains(Item.MODSEQ) || 
ids.getModSeq().asLong() > fetch.getChangedSince())
-                .concatMap(result -> toResponse(mailbox, fetch, 
mailboxSession, selected, result))
-                .subscribe(fetchSubscriber);
+
+            boolean singleMessage = ranges.size() == 1 && 
ranges.getFirst().getUidFrom().equals(ranges.getFirst().getUidTo());
+            boolean shouldCache = 
fetch.getBodyElements().stream().anyMatch(bodyFetchElement -> 
bodyFetchElement.getNumberOfOctets() != null);
+            if (singleMessage && shouldCache) {
+                DefaultLocalMessageCache localMessageCache = new 
DefaultLocalMessageCache(imapSession, localCacheConfiguration);
+                localMessageCache.lookupInCache(mailbox.getId(), 
ranges.getFirst().getUidFrom(), resultToFetch)
+                    .map(Flux::just)
+                    .orElseGet(() -> 
Flux.from(mailbox.getMessagesReactive(ranges.getFirst(), resultToFetch, 
mailboxSession))
+                        .doOnNext(message -> 
localMessageCache.saveForLater(message, resultToFetch)))
+                    .filter(ids -> !fetch.contains(Item.MODSEQ) || 
ids.getModSeq().asLong() > fetch.getChangedSince())
+                    .concatMap(result -> toResponse(mailbox, fetch, 
mailboxSession, selected, result))
+                    .subscribe(fetchSubscriber);
+            } else {
+                Flux.fromIterable(consolidate(selected, ranges, fetch))
+                    .doOnNext(range -> auditTrail(mailbox, mailboxSession, 
resultToFetch, range))
+                    .concatMap(range -> 
Flux.from(mailbox.getMessagesReactive(range, resultToFetch, mailboxSession)))
+                    .filter(ids -> !fetch.contains(Item.MODSEQ) || 
ids.getModSeq().asLong() > fetch.getChangedSince())
+                    .concatMap(result -> toResponse(mailbox, fetch, 
mailboxSession, selected, result))
+                    .subscribe(fetchSubscriber);
+            }
 
             return fetchSubscriber.completionMono();
         }
diff --git 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/main/DefaultImapProcessorFactory.java
 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/main/DefaultImapProcessorFactory.java
index 95513bfc4e..31415b99cf 100644
--- 
a/protocols/imap/src/main/java/org/apache/james/imap/processor/main/DefaultImapProcessorFactory.java
+++ 
b/protocols/imap/src/main/java/org/apache/james/imap/processor/main/DefaultImapProcessorFactory.java
@@ -27,6 +27,7 @@ import org.apache.james.imap.api.process.MailboxTyper;
 import org.apache.james.imap.message.response.UnpooledStatusResponseFactory;
 import org.apache.james.imap.processor.DefaultProcessor;
 import org.apache.james.imap.processor.base.UnknownRequestProcessor;
+import org.apache.james.imap.processor.fetch.FetchProcessor;
 import org.apache.james.mailbox.MailboxCounterCorrector;
 import org.apache.james.mailbox.MailboxManager;
 import org.apache.james.mailbox.SubscriptionManager;
@@ -38,18 +39,37 @@ public class DefaultImapProcessorFactory {
 
     public static ImapProcessor createDefaultProcessor(MailboxManager 
mailboxManager, EventBus eventBus, SubscriptionManager subscriptionManager, 
QuotaManager quotaManager, QuotaRootResolver quotaRootResolver,
             MetricFactory metricFactory) {
-        return createXListSupportingProcessor(mailboxManager, eventBus, 
subscriptionManager, new DefaultMailboxTyper(), quotaManager, 
quotaRootResolver, metricFactory);
+        return createXListSupportingProcessor(mailboxManager, eventBus, 
subscriptionManager, new DefaultMailboxTyper(), quotaManager, 
quotaRootResolver, metricFactory,
+            FetchProcessor.LocalCacheConfiguration.DEFAULT);
     }
 
     public static ImapProcessor createXListSupportingProcessor(MailboxManager 
mailboxManager,
                                                                EventBus 
eventBus, SubscriptionManager subscriptionManager,
-                                                               MailboxTyper 
mailboxTyper, QuotaManager quotaManager, QuotaRootResolver quotaRootResolver, 
MetricFactory metricFactory) {
+                                                               MailboxTyper 
mailboxTyper, QuotaManager quotaManager,
+                                                               
QuotaRootResolver quotaRootResolver, MetricFactory metricFactory,
+                                                               
FetchProcessor.LocalCacheConfiguration localCacheConfiguration) {
 
         StatusResponseFactory statusResponseFactory = new 
UnpooledStatusResponseFactory();
         UnknownRequestProcessor unknownRequestImapProcessor = new 
UnknownRequestProcessor(statusResponseFactory);
 
         return 
DefaultProcessor.createDefaultProcessor(unknownRequestImapProcessor, 
mailboxManager,
-            eventBus, subscriptionManager, statusResponseFactory, 
mailboxTyper, quotaManager, quotaRootResolver, MailboxCounterCorrector.DEFAULT, 
metricFactory);
+            eventBus, subscriptionManager, statusResponseFactory, 
mailboxTyper, quotaManager, quotaRootResolver,
+            MailboxCounterCorrector.DEFAULT, metricFactory,
+            localCacheConfiguration);
+    }
+
+    public static ImapProcessor createXListSupportingProcessor(MailboxManager 
mailboxManager,
+                                                               EventBus 
eventBus, SubscriptionManager subscriptionManager,
+                                                               MailboxTyper 
mailboxTyper, QuotaManager quotaManager,
+                                                               
QuotaRootResolver quotaRootResolver, MetricFactory metricFactory) {
+
+        StatusResponseFactory statusResponseFactory = new 
UnpooledStatusResponseFactory();
+        UnknownRequestProcessor unknownRequestImapProcessor = new 
UnknownRequestProcessor(statusResponseFactory);
+
+        return 
DefaultProcessor.createDefaultProcessor(unknownRequestImapProcessor, 
mailboxManager,
+            eventBus, subscriptionManager, statusResponseFactory, 
mailboxTyper, quotaManager, quotaRootResolver,
+            MailboxCounterCorrector.DEFAULT, metricFactory,
+            FetchProcessor.LocalCacheConfiguration.DEFAULT);
     }
 
 }
diff --git 
a/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/IMAPServerModule.java
 
b/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/IMAPServerModule.java
index ece9cdc95c..80a39d5725 100644
--- 
a/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/IMAPServerModule.java
+++ 
b/server/container/guice/protocols/imap/src/main/java/org/apache/james/modules/protocols/IMAPServerModule.java
@@ -24,6 +24,7 @@ import java.util.function.Function;
 import java.util.stream.Stream;
 
 import org.apache.commons.configuration2.HierarchicalConfiguration;
+import org.apache.commons.configuration2.ex.ConfigurationException;
 import org.apache.commons.configuration2.tree.ImmutableNode;
 import org.apache.commons.lang3.tuple.Pair;
 import org.apache.james.ProtocolConfigurationSanitizer;
@@ -65,6 +66,7 @@ import org.apache.james.imap.processor.SelectProcessor;
 import org.apache.james.imap.processor.StatusProcessor;
 import org.apache.james.imap.processor.base.AbstractProcessor;
 import org.apache.james.imap.processor.base.UnknownRequestProcessor;
+import org.apache.james.imap.processor.fetch.FetchProcessor;
 import org.apache.james.imapserver.netty.IMAPHealthCheck;
 import org.apache.james.imapserver.netty.IMAPServerFactory;
 import org.apache.james.lifecycle.api.ConfigurationSanitizer;
@@ -219,6 +221,10 @@ public class IMAPServerModule extends AbstractModule {
             });
     }
 
+    @Provides
+    FetchProcessor.LocalCacheConfiguration 
provideFetchLocalCacheConfiguration(ConfigurationProvider 
configurationProvider) throws ConfigurationException {
+            return 
FetchProcessor.LocalCacheConfiguration.from(configurationProvider.getConfiguration("imapserver"));
+    }
 
     private void configureEnable(EnableProcessor enableProcessor, 
ImmutableMap<Class, ImapProcessor> processorMap) {
         processorMap.values().stream()
diff --git 
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPHealthCheckTest.java
 
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPHealthCheckTest.java
index 25b5fa509d..5915dfe363 100644
--- 
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPHealthCheckTest.java
+++ 
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPHealthCheckTest.java
@@ -31,6 +31,7 @@ import 
org.apache.james.imap.api.DefaultConnectionCheckFactory;
 import org.apache.james.imap.api.ImapMessage;
 import org.apache.james.imap.encode.main.DefaultImapEncoderFactory;
 import org.apache.james.imap.main.DefaultImapDecoderFactory;
+import org.apache.james.imap.processor.fetch.FetchProcessor;
 import org.apache.james.imap.processor.main.DefaultImapProcessorFactory;
 import org.apache.james.mailbox.inmemory.InMemoryMailboxManager;
 import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
@@ -87,7 +88,8 @@ public class IMAPHealthCheckTest {
                 null,
                 memoryIntegrationResources.getQuotaManager(),
                 memoryIntegrationResources.getQuotaRootResolver(),
-                metricFactory),
+                metricFactory,
+                FetchProcessor.LocalCacheConfiguration.DEFAULT),
             new RecordingMetricFactory(),
             new NoopGaugeRegistry(),
             new DefaultConnectionCheckFactory());
diff --git 
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
 
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
index a648089857..0762409101 100644
--- 
a/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
+++ 
b/server/protocols/protocols-imap4/src/test/java/org/apache/james/imapserver/netty/IMAPServerTest.java
@@ -81,6 +81,7 @@ import org.apache.james.imap.api.ConnectionCheck;
 import org.apache.james.imap.encode.main.DefaultImapEncoderFactory;
 import org.apache.james.imap.main.DefaultImapDecoderFactory;
 import org.apache.james.imap.processor.base.AbstractProcessor;
+import org.apache.james.imap.processor.fetch.FetchProcessor;
 import org.apache.james.imap.processor.main.DefaultImapProcessorFactory;
 import org.apache.james.jwt.OidcTokenFixture;
 import org.apache.james.mailbox.MailboxSession;
@@ -160,7 +161,8 @@ class IMAPServerTest {
     private InMemoryMailboxManager mailboxManager;
 
     private IMAPServer 
createImapServer(HierarchicalConfiguration<ImmutableNode> config,
-                                        InMemoryIntegrationResources 
inMemoryIntegrationResources) throws Exception {
+                                        InMemoryIntegrationResources 
inMemoryIntegrationResources,
+                                        FetchProcessor.LocalCacheConfiguration 
localCacheConfiguration) throws Exception {
         memoryIntegrationResources = inMemoryIntegrationResources;
 
         RecordingMetricFactory metricFactory = new RecordingMetricFactory();
@@ -178,7 +180,8 @@ class IMAPServerTest {
                 null,
                 memoryIntegrationResources.getQuotaManager(),
                 memoryIntegrationResources.getQuotaRootResolver(),
-                metricFactory),
+                metricFactory,
+                localCacheConfiguration),
             new ImapMetrics(metricFactory),
             new NoopGaugeRegistry(), connectionChecks);
 
@@ -191,7 +194,7 @@ class IMAPServerTest {
         return imapServer;
     }
 
-    private IMAPServer 
createImapServer(HierarchicalConfiguration<ImmutableNode> config) throws 
Exception {
+    private IMAPServer 
createImapServer(HierarchicalConfiguration<ImmutableNode> config, 
FetchProcessor.LocalCacheConfiguration localCacheConfiguration) throws 
Exception {
         authenticator = new FakeAuthenticator();
         authenticator.addUser(USER, USER_PASS);
         authenticator.addUser(USER2, USER_PASS);
@@ -208,11 +211,19 @@ class IMAPServerTest {
             .storeQuotaManager()
             .build();
 
-        return createImapServer(config, memoryIntegrationResources);
+        return createImapServer(config, memoryIntegrationResources, 
localCacheConfiguration);
+    }
+
+    private IMAPServer 
createImapServer(HierarchicalConfiguration<ImmutableNode> config) throws 
Exception {
+        return createImapServer(config, 
FetchProcessor.LocalCacheConfiguration.DEFAULT);
+    }
+
+    private IMAPServer createImapServer(String configurationFile, 
FetchProcessor.LocalCacheConfiguration localCacheConfiguration) throws 
Exception {
+        return 
createImapServer(ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream(configurationFile)),
 localCacheConfiguration);
     }
 
     private IMAPServer createImapServer(String configurationFile) throws 
Exception {
-        return 
createImapServer(ConfigLoader.getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream(configurationFile)));
+        return createImapServer(configurationFile, 
FetchProcessor.LocalCacheConfiguration.DEFAULT);
     }
 
     private Set<ConnectionCheck> defaultConnectionChecks() {
@@ -285,7 +296,7 @@ class IMAPServerTest {
 
         @BeforeEach
         void beforeEach() throws Exception {
-            imapServer = createImapServer("imapServer.xml");
+            imapServer = createImapServer("imapServer.xml", 
FetchProcessor.LocalCacheConfiguration.DEFAULT);
             port = imapServer.getListenAddresses().get(0).getPort();
         }
 
@@ -340,6 +351,11 @@ class IMAPServerTest {
                 .select("INBOX")
                 .readFirstMessageInMailbox("BODY[]<8.12>"))
                 .contains("* 1 FETCH (FLAGS (\\Recent \\Seen) BODY[]<8> 
{12}\r\nvalue\r\n\r\nBOD)\r\n");
+
+            assertThat(testIMAPClient
+                .select("INBOX")
+                .readFirstMessageInMailbox("BODY[]<8.12>"))
+                .contains("* 1 FETCH (BODY[]<8> 
{12}\r\nvalue\r\n\r\nBOD)\r\n");
         }
 
         @Test
@@ -1313,7 +1329,7 @@ class IMAPServerTest {
             config.addProperty("auth.oidc.oidcConfigurationURL", 
"https://example.com/jwks";);
             config.addProperty("auth.oidc.scope", "email");
 
-            imapServer = createImapServer(config, integrationResources);
+            imapServer = createImapServer(config, integrationResources, 
FetchProcessor.LocalCacheConfiguration.DEFAULT);
             port = imapServer.getListenAddresses().get(0).getPort();
         }
 
diff --git a/src/site/xdoc/server/config-imap4.xml 
b/src/site/xdoc/server/config-imap4.xml
index f1ee2717b2..0990cfeec6 100644
--- a/src/site/xdoc/server/config-imap4.xml
+++ b/src/site/xdoc/server/config-imap4.xml
@@ -201,6 +201,23 @@
     <subsection name="Mail user agents auto-configuration">
         <p>Check this example on <a 
href="https://github.com/apache/james-project/tree/master/examples/imap-autoconf";>Mail
 user agents auto-configuration</a>.</p>
     </subsection>
+
+    <subsection name="Local cache for partial Fetch">
+        <p>Because some clients uses partial fetch in order to emulate 
retriable download of individual body parts
+            we offer a way for James to cache in the IMAP session the latest 
partially fetched message. This is done using
+            a weak reference, a total size dedicated to the cache as well as a 
time to leave cache cleanup, all of this
+            being configurable.</p>
+
+        <p>Example:</p>
+    </subsection>
+
+    <pre><code>
+&lt;imapserver&gt;
+        
&lt;partialBodyFetchCacheEnabled&gt;true&lt;/partialBodyFetchCacheEnabled&gt;
+        
&lt;partialBodyFetchCacheDuration&gt;2min&lt;/partialBodyFetchCacheDuration&gt;
+        &lt;partialBodyFetchCacheSize&gt;500 
MiB&lt;/partialBodyFetchCacheSize&gt;
+&lt;/imapserver&gt;
+    </code></pre>
 </section>
 
 </body>


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

Reply via email to