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 d367f654d0 JAMES-4186 [REFACTOR] EmaiQueryOptimizer interface to wrap
email query view logic in JMAP EmailQueryMethod (#2963)
d367f654d0 is described below
commit d367f654d07ab45f25a40d42ff9bfcd101361a55
Author: Rene Cordier <[email protected]>
AuthorDate: Mon Mar 9 09:19:40 2026 +0700
JAMES-4186 [REFACTOR] EmaiQueryOptimizer interface to wrap email query view
logic in JMAP EmailQueryMethod (#2963)
---
.../james/jmap/rfc8621/RFC8621MethodsModule.java | 5 +
.../james/jmap/method/EmailQueryMethod.scala | 96 +++-------------
.../james/jmap/method/EmailQueryOptimizer.scala | 121 +++++++++++++++++++++
3 files changed, 139 insertions(+), 83 deletions(-)
diff --git
a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
index 66437c8c70..02a498d25f 100644
---
a/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
+++
b/server/container/guice/protocols/jmap/src/main/java/org/apache/james/jmap/rfc8621/RFC8621MethodsModule.java
@@ -63,6 +63,8 @@ import org.apache.james.jmap.method.EmailGetMethod;
import org.apache.james.jmap.method.EmailImportMethod;
import org.apache.james.jmap.method.EmailParseMethod;
import org.apache.james.jmap.method.EmailQueryMethod;
+import org.apache.james.jmap.method.EmailQueryOptimizer;
+import org.apache.james.jmap.method.EmailQueryViewOptimizer;
import org.apache.james.jmap.method.EmailSetMethod;
import org.apache.james.jmap.method.EmailSubmissionSetMethod;
import org.apache.james.jmap.method.IdentityChangesMethod;
@@ -204,6 +206,9 @@ public class RFC8621MethodsModule extends AbstractModule {
Multibinder<ConnectionDescriptionSupplier>
connectionDescriptionSupplierMultibinder = Multibinder.newSetBinder(binder(),
ConnectionDescriptionSupplier.class);
connectionDescriptionSupplierMultibinder.addBinding().to(WebSocketRoutes.class);
connectionDescriptionSupplierMultibinder.addBinding().to(EventSourceRoutes.class);
+
+ Multibinder<EmailQueryOptimizer> emailQueryOptimizerMultibinder =
Multibinder.newSetBinder(binder(), EmailQueryOptimizer.class);
+
emailQueryOptimizerMultibinder.addBinding().to(EmailQueryViewOptimizer.class);
}
@ProvidesIntoSet
diff --git
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
index 467e29c72e..162c2c2da0 100644
---
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
+++
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryMethod.scala
@@ -18,27 +18,21 @@
****************************************************************/
package org.apache.james.jmap.method
-import java.time.ZonedDateTime
-
import cats.implicits._
import eu.timepit.refined.auto._
import jakarta.inject.Inject
import jakarta.mail.Flags.Flag.DELETED
-import org.apache.james.jmap.JMAPConfiguration
-import org.apache.james.jmap.api.projections.EmailQueryViewManager
import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier,
JMAP_CORE, JMAP_MAIL}
import org.apache.james.jmap.core.Invocation.{Arguments, MethodName}
import org.apache.james.jmap.core.Limit.Limit
import org.apache.james.jmap.core.Position.Position
import org.apache.james.jmap.core.{CanCalculateChanges, Invocation, Limit,
Position, QueryState, SessionTranslator}
import org.apache.james.jmap.json.EmailQuerySerializer
-import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest,
EmailQueryResponse, FilterCondition, UnsupportedRequestParameterException}
+import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest,
EmailQueryResponse, UnsupportedRequestParameterException}
import org.apache.james.jmap.routes.SessionSupplier
import org.apache.james.jmap.utils.search.MailboxFilter
import org.apache.james.jmap.utils.search.MailboxFilter.QueryFilter
-import org.apache.james.mailbox.exception.MailboxNotFoundException
-import org.apache.james.mailbox.model.MultimailboxesSearchQuery.Namespace
-import org.apache.james.mailbox.model.{MailboxId, MessageId,
MultimailboxesSearchQuery, SearchOptions, SearchQuery}
+import org.apache.james.mailbox.model.{MessageId, MultimailboxesSearchQuery,
SearchOptions, SearchQuery}
import org.apache.james.mailbox.{MailboxManager, MailboxSession}
import org.apache.james.metrics.api.MetricFactory
import org.apache.james.util.streams.{Offset, Limit => JavaLimit}
@@ -51,8 +45,9 @@ class EmailQueryMethod @Inject() (serializer:
EmailQuerySerializer,
val metricFactory: MetricFactory,
val sessionSupplier: SessionSupplier,
val sessionTranslator: SessionTranslator,
- val configuration: JMAPConfiguration,
- val emailQueryViewManager:
EmailQueryViewManager) extends MethodRequiringAccountId[EmailQueryRequest] {
+ val javaEmailQueryOptimizers:
java.util.Set[EmailQueryOptimizer]) extends
MethodRequiringAccountId[EmailQueryRequest] {
+ private val emailQueryOptimizers: Set[EmailQueryOptimizer] =
javaEmailQueryOptimizers.asScala.toSet
+
override val methodName: MethodName = MethodName("Email/query")
override val requiredCapabilities: Set[CapabilityIdentifier] =
Set(JMAP_CORE, JMAP_MAIL)
@@ -92,81 +87,17 @@ class EmailQueryMethod @Inject() (serializer:
EmailQuerySerializer,
}
private def executeQuery(session: MailboxSession, request:
EmailQueryRequest, searchQuery: MultimailboxesSearchQuery, position: Position,
limit: Limit): SMono[EmailQueryResponse] = {
- val ids: SMono[Seq[MessageId]] = request match {
- case request: EmailQueryRequest if
matchesInMailboxSortedByReceivedAt(request) =>
- queryViewForListingSortedByReceivedAt(session, position, limit,
request, searchQuery.getNamespace)
- case request: EmailQueryRequest if
matchesInMailboxAfterSortedByReceivedAt(request) =>
- queryViewForContentAfterSortedByReceivedAt(session, position, limit,
request, searchQuery.getNamespace)
- case request: EmailQueryRequest if
matchesInMailboxBeforeSortedByReceivedAt(request) =>
- queryViewForContentBeforeSortedByReceivedAt(session, position, limit,
request, searchQuery.getNamespace)
- case _ => executeQueryAgainstSearchIndex(session, searchQuery, position,
limit)
- }
+ val ids: SMono[Seq[MessageId]] = executeQueryOptimizers(session, request,
searchQuery, position, limit)
+ .getOrElse(executeQueryAgainstSearchIndex(session, searchQuery,
position, limit))
+ .collectSeq()
ids.map(ids => toResponse(request, position, limit, ids))
}
-
- private def queryViewForContentAfterSortedByReceivedAt(mailboxSession:
MailboxSession, position: Position, limitToUse: Limit, request:
EmailQueryRequest, namespace: Namespace): SMono[Seq[MessageId]] = {
- val condition: FilterCondition =
request.filter.get.asInstanceOf[FilterCondition]
- val mailboxId: MailboxId = condition.inMailbox.get
- val after: ZonedDateTime = condition.after.get.asUTC
- val collapseThreads: Boolean = getCollapseThreads(request)
-
- val queryViewEntries: SFlux[MessageId] =
SFlux.fromPublisher(emailQueryViewManager.getEmailQueryView(mailboxSession.getUser)
- .listMailboxContentSinceAfterSortedByReceivedAt(mailboxId, after,
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
-
- fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession,
position, limitToUse, namespace)
- }
-
- private def queryViewForContentBeforeSortedByReceivedAt(mailboxSession:
MailboxSession, position: Position, limitToUse: Limit, request:
EmailQueryRequest, namespace: Namespace): SMono[Seq[MessageId]] = {
- val condition: FilterCondition =
request.filter.get.asInstanceOf[FilterCondition]
- val mailboxId: MailboxId = condition.inMailbox.get
- val before: ZonedDateTime = condition.before.get.asUTC
- val collapseThreads: Boolean = getCollapseThreads(request)
-
- val queryViewEntries: SFlux[MessageId] =
SFlux.fromPublisher(emailQueryViewManager.getEmailQueryView(mailboxSession.getUser)
- .listMailboxContentBeforeSortedByReceivedAt(mailboxId, before,
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
-
- fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession,
position, limitToUse, namespace)
- }
-
- private def queryViewForListingSortedByReceivedAt(mailboxSession:
MailboxSession, position: Position, limitToUse: Limit, request:
EmailQueryRequest, namespace: Namespace): SMono[Seq[MessageId]] = {
- val mailboxId: MailboxId =
request.filter.get.asInstanceOf[FilterCondition].inMailbox.get
- val collapseThreads: Boolean = getCollapseThreads(request)
-
- val queryViewEntries: SFlux[MessageId] =
SFlux.fromPublisher(emailQueryViewManager
-
.getEmailQueryView(mailboxSession.getUser).listMailboxContentSortedByReceivedAt(mailboxId,
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
-
- fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession,
position, limitToUse, namespace)
- }
-
- private def fromQueryViewEntries(mailboxId: MailboxId, queryViewEntries:
SFlux[MessageId], mailboxSession: MailboxSession, position: Position,
limitToUse: Limit, namespace: Namespace): SMono[Seq[MessageId]] =
- SMono(mailboxManager.getMailboxReactive(mailboxId, mailboxSession))
- .filter(messageManager =>
namespace.keepAccessible(messageManager.getMailboxEntity))
- .flatMap(_ => queryViewEntries
- .drop(position.value)
- .take(limitToUse.value)
- .collectSeq())
- .switchIfEmpty(SMono.just[Seq[MessageId]](Seq()))
- .onErrorResume({
- case _: MailboxNotFoundException => SMono.just[Seq[MessageId]](Seq())
- case e => SMono.error[Seq[MessageId]](e)
- })
-
- private def matchesInMailboxSortedByReceivedAt(request: EmailQueryRequest):
Boolean =
- configuration.isEmailQueryViewEnabled &&
- request.filter.exists(_.inMailboxFilterOnly) &&
- request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
-
- private def matchesInMailboxAfterSortedByReceivedAt(request:
EmailQueryRequest): Boolean =
- configuration.isEmailQueryViewEnabled &&
- request.filter.exists(_.inMailboxAndAfterFilterOnly) &&
- request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
-
- private def matchesInMailboxBeforeSortedByReceivedAt(request:
EmailQueryRequest): Boolean =
- configuration.isEmailQueryViewEnabled &&
- request.filter.exists(_.inMailboxAndBeforeFilterOnly) &&
- request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+ private def executeQueryOptimizers(session: MailboxSession, request:
EmailQueryRequest, searchQuery: MultimailboxesSearchQuery, position: Position,
limit: Limit): Option[SFlux[MessageId]] =
+ emailQueryOptimizers.iterator
+ .map(_.apply(request, session, searchQuery, position, limit))
+ .collectFirst { case Some(result) => result }
private def getCollapseThreads(request: EmailQueryRequest): Boolean =
request.collapseThreads match {
@@ -182,12 +113,11 @@ class EmailQueryMethod @Inject() (serializer:
EmailQuerySerializer,
position = position,
limit = Some(limitToUse).filterNot(used =>
request.limit.map(_.value).contains(used.value)))
- private def executeQueryAgainstSearchIndex(mailboxSession: MailboxSession,
searchQuery: MultimailboxesSearchQuery, position: Position, limitToUse: Limit):
SMono[Seq[MessageId]] =
+ private def executeQueryAgainstSearchIndex(mailboxSession: MailboxSession,
searchQuery: MultimailboxesSearchQuery, position: Position, limitToUse: Limit):
SFlux[MessageId] =
SFlux.fromPublisher(mailboxManager.search(
searchQuery.addCriterion(SearchQuery.flagIsUnSet(DELETED)),
mailboxSession,
SearchOptions.of(Offset.from(position.value),
JavaLimit.limit(limitToUse.value))))
- .collectSeq()
private def searchQueryFromRequest(request: EmailQueryRequest, capabilities:
Set[CapabilityIdentifier], session: MailboxSession):
Either[UnsupportedOperationException, MultimailboxesSearchQuery] = {
val comparators: List[Comparator] = request.sort.getOrElse(Set()).toList
diff --git
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryOptimizer.scala
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryOptimizer.scala
new file mode 100644
index 0000000000..75450be410
--- /dev/null
+++
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/EmailQueryOptimizer.scala
@@ -0,0 +1,121 @@
+/****************************************************************
+ * Licensed to the Apache Software Foundation (ASF) under one *
+ * or more contributor license agreements. See the NOTICE file *
+ * distributed with this work for additional information *
+ * regarding copyright ownership. The ASF licenses this file *
+ * to you under the Apache License, Version 2.0 (the *
+ * "License"); you may not use this file except in compliance *
+ * with the License. You may obtain a copy of the License at *
+ * *
+ * http://www.apache.org/licenses/LICENSE-2.0 *
+ * *
+ * Unless required by applicable law or agreed to in writing, *
+ * software distributed under the License is distributed on an *
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
+ * KIND, either express or implied. See the License for the *
+ * specific language governing permissions and limitations *
+ * under the License. *
+ ****************************************************************/
+
+package org.apache.james.jmap.method
+
+import java.time.ZonedDateTime
+
+import jakarta.inject.Inject
+import org.apache.james.jmap.JMAPConfiguration
+import org.apache.james.jmap.api.projections.EmailQueryViewManager
+import org.apache.james.jmap.core.Limit.Limit
+import org.apache.james.jmap.core.Position.Position
+import org.apache.james.jmap.mail.{Comparator, EmailQueryRequest,
FilterCondition}
+import org.apache.james.mailbox.exception.MailboxNotFoundException
+import org.apache.james.mailbox.model.MultimailboxesSearchQuery.Namespace
+import org.apache.james.mailbox.model.{MailboxId, MessageId,
MultimailboxesSearchQuery}
+import org.apache.james.mailbox.{MailboxManager, MailboxSession}
+import org.apache.james.util.streams.{Limit => JavaLimit}
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+trait EmailQueryOptimizer {
+ def apply(request: EmailQueryRequest, session: MailboxSession, searchQuery:
MultimailboxesSearchQuery, position: Position, limit: Limit):
Option[SFlux[MessageId]]
+}
+
+class EmailQueryViewOptimizer @Inject() (mailboxManager: MailboxManager,
+ val configuration: JMAPConfiguration,
+ val emailQueryViewManager:
EmailQueryViewManager) extends EmailQueryOptimizer {
+ override def apply(request: EmailQueryRequest, session: MailboxSession,
searchQuery: MultimailboxesSearchQuery, position: Position, limit: Limit):
Option[SFlux[MessageId]] =
+ if (configuration.isEmailQueryViewEnabled) {
+ request match {
+ case request: EmailQueryRequest if
matchesInMailboxSortedByReceivedAt(request) =>
+ Some(queryViewForListingSortedByReceivedAt(session, position, limit,
request, searchQuery.getNamespace))
+ case request: EmailQueryRequest if
matchesInMailboxAfterSortedByReceivedAt(request) =>
+ Some(queryViewForContentAfterSortedByReceivedAt(session, position,
limit, request, searchQuery.getNamespace))
+ case request: EmailQueryRequest if
matchesInMailboxBeforeSortedByReceivedAt(request) =>
+ Some(queryViewForContentBeforeSortedByReceivedAt(session, position,
limit, request, searchQuery.getNamespace))
+ case _ => None
+ }
+ } else {
+ None
+ }
+
+ private def matchesInMailboxSortedByReceivedAt(request: EmailQueryRequest):
Boolean =
+ request.filter.exists(_.inMailboxFilterOnly) &&
+ request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+
+ private def matchesInMailboxAfterSortedByReceivedAt(request:
EmailQueryRequest): Boolean =
+ request.filter.exists(_.inMailboxAndAfterFilterOnly) &&
+ request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+
+ private def matchesInMailboxBeforeSortedByReceivedAt(request:
EmailQueryRequest): Boolean =
+ request.filter.exists(_.inMailboxAndBeforeFilterOnly) &&
+ request.sort.contains(Set(Comparator.RECEIVED_AT_DESC))
+
+ private def queryViewForListingSortedByReceivedAt(mailboxSession:
MailboxSession, position: Position, limitToUse: Limit, request:
EmailQueryRequest, namespace: Namespace): SFlux[MessageId] = {
+ val mailboxId: MailboxId =
request.filter.get.asInstanceOf[FilterCondition].inMailbox.get
+ val collapseThreads: Boolean = getCollapseThreads(request)
+
+ val queryViewEntries: SFlux[MessageId] =
SFlux.fromPublisher(emailQueryViewManager
+
.getEmailQueryView(mailboxSession.getUser).listMailboxContentSortedByReceivedAt(mailboxId,
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
+
+ fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession,
position, limitToUse, namespace)
+ }
+
+ private def getCollapseThreads(request: EmailQueryRequest): Boolean =
+ request.collapseThreads match {
+ case Some(collapseThreads) => collapseThreads.value
+ case None => false
+ }
+
+ private def fromQueryViewEntries(mailboxId: MailboxId, queryViewEntries:
SFlux[MessageId], mailboxSession: MailboxSession, position: Position,
limitToUse: Limit, namespace: Namespace): SFlux[MessageId] =
+ SMono(mailboxManager.getMailboxReactive(mailboxId, mailboxSession))
+ .filter(messageManager =>
namespace.keepAccessible(messageManager.getMailboxEntity))
+ .flatMapMany(_ => queryViewEntries
+ .drop(position.value)
+ .take(limitToUse.value))
+ .onErrorResume({
+ case _: MailboxNotFoundException => SFlux.empty
+ case e => SFlux.error[MessageId](e)
+ })
+
+ private def queryViewForContentAfterSortedByReceivedAt(mailboxSession:
MailboxSession, position: Position, limitToUse: Limit, request:
EmailQueryRequest, namespace: Namespace): SFlux[MessageId] = {
+ val condition: FilterCondition =
request.filter.get.asInstanceOf[FilterCondition]
+ val mailboxId: MailboxId = condition.inMailbox.get
+ val after: ZonedDateTime = condition.after.get.asUTC
+ val collapseThreads: Boolean = getCollapseThreads(request)
+
+ val queryViewEntries: SFlux[MessageId] =
SFlux.fromPublisher(emailQueryViewManager.getEmailQueryView(mailboxSession.getUser)
+ .listMailboxContentSinceAfterSortedByReceivedAt(mailboxId, after,
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
+
+ fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession,
position, limitToUse, namespace)
+ }
+
+ private def queryViewForContentBeforeSortedByReceivedAt(mailboxSession:
MailboxSession, position: Position, limitToUse: Limit, request:
EmailQueryRequest, namespace: Namespace): SFlux[MessageId] = {
+ val condition: FilterCondition =
request.filter.get.asInstanceOf[FilterCondition]
+ val mailboxId: MailboxId = condition.inMailbox.get
+ val before: ZonedDateTime = condition.before.get.asUTC
+ val collapseThreads: Boolean = getCollapseThreads(request)
+
+ val queryViewEntries: SFlux[MessageId] =
SFlux.fromPublisher(emailQueryViewManager.getEmailQueryView(mailboxSession.getUser)
+ .listMailboxContentBeforeSortedByReceivedAt(mailboxId, before,
JavaLimit.from(limitToUse.value + position.value), collapseThreads))
+
+ fromQueryViewEntries(mailboxId, queryViewEntries, mailboxSession,
position, limitToUse, namespace)
+ }
+}
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]