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

hqtran 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 c75eb15030 [IMPROVEMENT] CapabilityFactory should take Username into 
account + be reactive (#2781)
c75eb15030 is described below

commit c75eb15030b5811435f982a02e54fc2ab1e63691
Author: Trần Hồng Quân <[email protected]>
AuthorDate: Wed Jul 30 16:52:32 2025 +0700

    [IMPROVEMENT] CapabilityFactory should take Username into account + be 
reactive (#2781)
    
    It is a common use case that capability could be varied per User, e.g. in a 
SaaS deployment where users can have different plan capabilities.
    
    Also, allow evaluating capability to be reactive.
---
 .../rfc8621/contract/CustomMethodContract.scala    |  3 ++-
 .../org/apache/james/jmap/core/Capability.scala    | 30 +++++++++++++---------
 .../apache/james/jmap/routes/SessionRoutes.scala   |  5 ++--
 .../apache/james/jmap/routes/SessionSupplier.scala | 18 ++++++++-----
 .../james/jmap/routes/SessionSupplierTest.scala    |  4 +--
 5 files changed, 36 insertions(+), 24 deletions(-)

diff --git 
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
index 71ab030455..9d1a0864be 100644
--- 
a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
+++ 
b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/CustomMethodContract.scala
@@ -35,6 +35,7 @@ import jakarta.inject.{Inject, Named}
 import net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson
 import org.apache.http.HttpStatus.SC_OK
 import org.apache.james.GuiceJamesServer
+import org.apache.james.core.Username
 import org.apache.james.events.Event.EventId
 import org.apache.james.events.EventBus
 import org.apache.james.jmap.api.model.Size.Size
@@ -184,7 +185,7 @@ case class CustomCapabilityProperties() extends 
CapabilityProperties {
 case class CustomCapability(properties: CustomCapabilityProperties = 
CustomCapabilityProperties(), identifier: CapabilityIdentifier = CUSTOM) 
extends Capability
 
 case object CustomCapabilityFactory extends CapabilityFactory {
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
CustomCapability()
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = CustomCapability()
 
   override def id(): CapabilityIdentifier = CUSTOM
 }
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
index 2fbd64630a..5f34db178d 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/Capability.scala
@@ -28,6 +28,7 @@ import eu.timepit.refined.api.Refined
 import eu.timepit.refined.auto._
 import eu.timepit.refined.collection.NonEmpty
 import eu.timepit.refined.string.Uri
+import org.apache.james.core.Username
 import org.apache.james.jmap.core.CapabilityIdentifier.{CapabilityIdentifier, 
EMAIL_SUBMISSION, JAMES_DELEGATION, JAMES_IDENTITY_SORTORDER, JAMES_QUOTA, 
JAMES_SHARES, JMAP_CORE, JMAP_MAIL, JMAP_MDN, JMAP_QUOTA, 
JMAP_VACATION_RESPONSE, JMAP_WEBSOCKET}
 import org.apache.james.jmap.core.CoreCapabilityProperties.CollationAlgorithm
 import org.apache.james.jmap.core.MailCapability.EmailQuerySortOption
@@ -35,7 +36,9 @@ import 
org.apache.james.jmap.core.SubmissionCapabilityFactory.maximumDelays
 import org.apache.james.jmap.core.UnsignedInt.{UnsignedInt, 
UnsignedIntConstraint}
 import org.apache.james.jmap.json.ResponseSerializer
 import org.apache.james.util.Size
+import org.reactivestreams.Publisher
 import play.api.libs.json.{JsObject, Json}
+import reactor.core.scala.publisher.SMono
 import reactor.netty.http.server.HttpServerRequest
 
 import scala.util.{Failure, Success, Try}
@@ -98,7 +101,10 @@ object UrlPrefixes {
 final case class UrlPrefixes(httpUrlPrefix: URI, webSocketURLPrefix: URI)
 
 trait CapabilityFactory {
-  def create(urlPrefixes: UrlPrefixes): Capability
+  def create(urlPrefixes: UrlPrefixes, username: Username): Capability
+
+  def createReactive(urlPrefixes: UrlPrefixes, username: Username): 
Publisher[Capability] =
+    SMono.fromCallable(() => create(urlPrefixes, username))
 
   def id(): CapabilityIdentifier
 }
@@ -109,7 +115,7 @@ final case class CoreCapability(properties: 
CoreCapabilityProperties,
 final case class CoreCapabilityFactory(configration: JmapRfc8621Configuration) 
extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JMAP_CORE
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
CoreCapability(CoreCapabilityProperties(
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = CoreCapability(CoreCapabilityProperties(
     configration.maxUploadSize,
     MaxConcurrentUpload(4L),
     MaxSizeRequest(10_000_000L), // See MaxSizeRequest.DEFAULT compile-time 
refinement only works with literals
@@ -125,7 +131,7 @@ case class WebSocketCapability(properties: 
WebSocketCapabilityProperties, identi
 case object WebSocketCapabilityFactory extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JMAP_WEBSOCKET
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
WebSocketCapability(
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = WebSocketCapability(
     WebSocketCapabilityProperties(SupportsPush(true), new 
URI(urlPrefixes.webSocketURLPrefix.toString + "/jmap/ws")))
 }
 
@@ -188,7 +194,7 @@ case object SubmissionCapabilityFactory {
 final case class SubmissionCapabilityFactory(clock: Clock, supportsDelaySends: 
Boolean) extends CapabilityFactory {
   override def id(): CapabilityIdentifier = EMAIL_SUBMISSION
 
-  override def create(urlPrefixes: UrlPrefixes): Capability =
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability =
     if (supportsDelaySends) {
       advertiseDelaySendSupport
     } else {
@@ -227,7 +233,7 @@ final case class MailCapability(properties: 
MailCapabilityProperties,
 case class MailCapabilityFactory(configuration: JmapRfc8621Configuration) 
extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JMAP_MAIL
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
MailCapability(MailCapabilityProperties(
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = MailCapability(MailCapabilityProperties(
     MaxMailboxesPerEmail(Some(10_000_000L)),
     MaxMailboxDepth(None),
     MaxSizeMailboxName(200L),
@@ -287,7 +293,7 @@ final case class QuotaCapability(properties: 
QuotaCapabilityProperties = QuotaCa
 case object QuotaCapabilityFactory extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JAMES_QUOTA
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = QuotaCapability()
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = QuotaCapability()
 }
 
 final case class IdentitySortOrderCapabilityProperties() extends 
CapabilityProperties {
@@ -300,7 +306,7 @@ final case class IdentitySortOrderCapability(properties: 
IdentitySortOrderCapabi
 case object IdentitySortOrderCapabilityFactory extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JAMES_IDENTITY_SORTORDER
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
IdentitySortOrderCapability()
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = IdentitySortOrderCapability()
 }
 
 final case class DelegationCapabilityProperties() extends CapabilityProperties 
{
@@ -313,7 +319,7 @@ final case class DelegationCapability(properties: 
DelegationCapabilityProperties
 case object DelegationCapabilityFactory extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JAMES_DELEGATION
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
DelegationCapability()
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = DelegationCapability()
 }
 
 final case class SharesCapabilityProperties() extends CapabilityProperties {
@@ -323,7 +329,7 @@ final case class SharesCapabilityProperties() extends 
CapabilityProperties {
 case object SharesCapabilityFactory extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JAMES_SHARES
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
SharesCapability()
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = SharesCapability()
 }
 
 final case class SharesCapability(properties: SharesCapabilityProperties = 
SharesCapabilityProperties(),
@@ -336,7 +342,7 @@ final case class MDNCapabilityProperties() extends 
CapabilityProperties {
 case object MDNCapabilityFactory extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JMAP_MDN
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = MDNCapability()
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = MDNCapability()
 }
 
 final case class MDNCapability(properties: MDNCapabilityProperties = 
MDNCapabilityProperties(),
@@ -349,7 +355,7 @@ final case class VacationResponseCapabilityProperties() 
extends CapabilityProper
 case object VacationResponseCapabilityFactory extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JMAP_VACATION_RESPONSE
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
VacationResponseCapability()
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = VacationResponseCapability()
 }
 
 final case class VacationResponseCapability(properties: 
VacationResponseCapabilityProperties = VacationResponseCapabilityProperties(),
@@ -365,5 +371,5 @@ final case class JmapQuotaCapabilityProperties() extends 
CapabilityProperties {
 case object JmapQuotaCapabilityFactory extends CapabilityFactory {
   override def id(): CapabilityIdentifier = JMAP_QUOTA
 
-  override def create(urlPrefixes: UrlPrefixes): Capability = 
JmapQuotaCapability()
+  override def create(urlPrefixes: UrlPrefixes, username: Username): 
Capability = JmapQuotaCapability()
 }
\ No newline at end of file
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
index 128e1c838b..8c6e43896e 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionRoutes.scala
@@ -66,12 +66,11 @@ class SessionRoutes 
@Inject()(@Named(InjectionKeys.RFC_8621) val authenticator:
       .flatMap(mailboxSession => getDelegatedUsers(mailboxSession)
         .collectSeq()
         .map(seq => Pair.of(mailboxSession.getUser, seq)))
-      .handle[Session] {
-        case (baseUserAndDelegatedUsers, sink) => sessionSupplier.generate(
+      .flatMap { baseUserAndDelegatedUsers =>
+        sessionSupplier.generate(
           username = baseUserAndDelegatedUsers.getLeft,
           delegatedUsers = baseUserAndDelegatedUsers.getRight.toSet,
           urlPrefixes = UrlPrefixes.from(jmapRfc8621Configuration, request))
-          .fold(sink.error, session => sink.next(session))
       }
       .flatMap(session => sendRespond(session, response))
       .onErrorResume(throwable => SMono.fromPublisher(errorHandling(throwable, 
response)))
diff --git 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
index ea7e101dc4..0209004c79 100644
--- 
a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/routes/SessionSupplier.scala
@@ -26,6 +26,7 @@ import jakarta.inject.Inject
 import org.apache.james.core.Username
 import org.apache.james.jmap.core.CapabilityIdentifier.CapabilityIdentifier
 import org.apache.james.jmap.core.{Account, AccountId, Capabilities, 
Capability, CapabilityFactory, IsPersonal, IsReadOnly, 
JmapRfc8621Configuration, Session, URL, UrlPrefixes}
+import reactor.core.scala.publisher.{SFlux, SMono}
 
 import scala.jdk.CollectionConverters._
 
@@ -40,15 +41,13 @@ class SessionSupplier(capabilityFactories: 
Set[CapabilityFactory], configuration
     .toOption
     .getOrElse(false)
 
-  def generate(username: Username, delegatedUsers: Set[Username], urlPrefixes: 
UrlPrefixes): Either[IllegalArgumentException, Session] = {
+  def generate(username: Username, delegatedUsers: Set[Username], urlPrefixes: 
UrlPrefixes): SMono[Session] = {
     val urlEndpointResolver: JmapUrlEndpointResolver = new 
JmapUrlEndpointResolver(urlPrefixes)
-    val capabilities: Set[Capability] = capabilityFactories
-      .map(cf => cf.create(urlPrefixes))
-      .filter(capability => 
!configuration.disabledCapabilities.contains(capability.identifier()))
 
     for {
-      account <- accounts(username, capabilities)
-      delegatedAccounts <- delegatedAccounts(delegatedUsers, capabilities)
+      capabilities <- evaluateCapabilities(username, urlPrefixes)
+      account <- SMono.fromTry(accounts(username, capabilities).toTry)
+      delegatedAccounts <- SMono.fromTry(delegatedAccounts(delegatedUsers, 
capabilities).toTry)
     } yield {
       Session(
         Capabilities(capabilities),
@@ -62,6 +61,13 @@ class SessionSupplier(capabilityFactories: 
Set[CapabilityFactory], configuration
     }
   }
 
+  private def evaluateCapabilities(username: Username, urlPrefixes: 
UrlPrefixes): SMono[Set[Capability]] =
+    SFlux.fromIterable(capabilityFactories)
+      .flatMap(capabilityFactory => 
SMono.fromPublisher(capabilityFactory.createReactive(urlPrefixes, username)))
+      .filter(capability => 
!configuration.disabledCapabilities.contains(capability.identifier()))
+      .collectSeq()
+      .map(_.toSet)
+
   private def accounts(username: Username, capabilities: Set[Capability]): 
Either[IllegalArgumentException, Account] =
     Account.from(username, IsPersonal(true), IsReadOnly(false), capabilities)
 
diff --git 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
index 2df5cb9122..e59bc2ce44 100644
--- 
a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
+++ 
b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/routes/SessionSupplierTest.scala
@@ -34,12 +34,12 @@ class SessionSupplierTest extends AnyWordSpec with Matchers 
{
   "generate" should {
     "return correct username" in {
       new 
SessionSupplier(DefaultCapabilities.supported(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION),
 JmapRfc8621Configuration.LOCALHOST_CONFIGURATION)
-        .generate(USERNAME, Set(), 
JmapRfc8621Configuration.LOCALHOST_CONFIGURATION.urlPrefixes()).toOption.get.username
 should equal(USERNAME)
+        .generate(USERNAME, Set(), 
JmapRfc8621Configuration.LOCALHOST_CONFIGURATION.urlPrefixes()).blockOption().get.username
 should equal(USERNAME)
     }
 
     "return correct account" which {
       val accounts = new 
SessionSupplier(DefaultCapabilities.supported(JmapRfc8621Configuration.LOCALHOST_CONFIGURATION),
 JmapRfc8621Configuration.LOCALHOST_CONFIGURATION)
-        .generate(USERNAME, Set(), 
JmapRfc8621Configuration.LOCALHOST_CONFIGURATION.urlPrefixes()).toOption.get.accounts
+        .generate(USERNAME, Set(), 
JmapRfc8621Configuration.LOCALHOST_CONFIGURATION.urlPrefixes()).blockOption().get.accounts
 
       "has size" in {
         accounts should have size 1


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

Reply via email to