You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by bt...@apache.org on 2021/11/10 07:54:58 UTC

[james-project] 01/02: JAMES-3539 Add PushSubscription/set update expires method

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

btellier pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/james-project.git

commit 6cf241938fcc4c838bba3a21185463d9db6c02f9
Author: Quan Tran <hq...@linagora.com>
AuthorDate: Tue Nov 2 11:51:36 2021 +0700

    JAMES-3539 Add PushSubscription/set update expires method
---
 .../james/jmap/api/model/PushSubscription.scala    |   6 +-
 .../PushSubscriptionSetMethodContract.scala        | 248 ++++++++++++++++++++-
 .../james/jmap/core/PushSubscriptionSet.scala      |  41 +++-
 .../jmap/json/PushSubscriptionSerializer.scala     |   2 +-
 .../PushSubscriptionSetCreatePerformer.scala       |   5 +-
 .../method/PushSubscriptionUpdatePerformer.scala   |  22 +-
 6 files changed, 303 insertions(+), 21 deletions(-)

diff --git a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
index 63a4108..69d4f77 100644
--- a/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
+++ b/server/data/data-jmap/src/main/scala/org/apache/james/jmap/api/model/PushSubscription.scala
@@ -150,7 +150,7 @@ case class PushSubscriptionNotFoundException(id: PushSubscriptionId) extends Run
 object ExpireTimeInvalidException {
   val TIME_FORMATTER: DateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssX")
 }
-case class ExpireTimeInvalidException(expires: ZonedDateTime, message: String) extends RuntimeException(s"`${expires.format(TIME_FORMATTER)}` $message")
+case class ExpireTimeInvalidException(expires: ZonedDateTime, message: String) extends IllegalStateException(s"`${expires.format(TIME_FORMATTER)}` $message")
 
-case class DeviceClientIdInvalidException(deviceClientId: DeviceClientId, message: String) extends RuntimeException(s"`${deviceClientId.value}` $message")
-case class InvalidPushSubscriptionKeys(keys: PushSubscriptionKeys) extends RuntimeException
\ No newline at end of file
+case class DeviceClientIdInvalidException(deviceClientId: DeviceClientId, message: String) extends IllegalArgumentException(s"`${deviceClientId.value}` $message")
+case class InvalidPushSubscriptionKeys(keys: PushSubscriptionKeys) extends IllegalArgumentException
\ No newline at end of file
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/PushSubscriptionSetMethodContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
index 03d3aa8..ac5a01d 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/PushSubscriptionSetMethodContract.scala
@@ -1109,7 +1109,8 @@ trait PushSubscriptionSetMethodContract {
            |                "notCreated": {
            |                    "4f29": {
            |                        "type": "invalidArguments",
-           |                        "description": "`$invalidExpire` expires must be greater than now"
+           |                        "description": "`$invalidExpire` expires must be greater than now",
+           |                        "properties": ["expires"]
            |                    }
            |                }
            |            },
@@ -1814,6 +1815,251 @@ trait PushSubscriptionSetMethodContract {
   }
 
   @Test
+  def updateValidExpiresShouldSucceed(server: GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
+
+    val validExpiresString = UTCDate(ZonedDateTime.now().plusDays(1)).asUTC.format(TIME_FORMATTER)
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                 "expires": "$validExpiresString"
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "PushSubscription/set",
+           |            {
+           |                "updated": {
+           |                    "${pushSubscription.id.serialise}": {}
+           |                }
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+
+    assertThat(probe.retrievePushSubscription(BOB, pushSubscription.id)
+      .expires.value.format(TIME_FORMATTER))
+      .isEqualTo(validExpiresString)
+  }
+
+  @Test
+  def updateInvalidExpiresStringShouldFail(server: GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
+
+    val invalidExpiresString = "whatever"
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                 "expires": "$invalidExpiresString"
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "${SESSION_STATE.value}",
+           |	"methodResponses": [
+           |		[
+           |			"PushSubscription/set",
+           |			{
+           |				"notUpdated": {
+           |					"${pushSubscription.id.serialise}": {
+           |						"type": "invalidArguments",
+           |						"description": "This string can not be parsed to UTCDate",
+           |						"properties": ["expires"]
+           |					}
+           |				}
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def updateWithBiggerExpiresThanServerLimitShouldSetToServerLimitAndExplicitlyReturned(server: GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
+
+    val biggerExpiresString = UTCDate(ZonedDateTime.now().plusDays(10)).asUTC.format(TIME_FORMATTER)
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                 "expires": "$biggerExpiresString"
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    val fixedExpires = probe.retrievePushSubscription(BOB, pushSubscription.id)
+      .expires.value.format(TIME_FORMATTER)
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |    "sessionState": "${SESSION_STATE.value}",
+           |    "methodResponses": [
+           |        [
+           |            "PushSubscription/set",
+           |            {
+           |                "updated": {
+           |                    "${pushSubscription.id.serialise}": {
+           |                        "expires": "$fixedExpires"
+           |                    }
+           |                }
+           |            },
+           |            "c1"
+           |        ]
+           |    ]
+           |}""".stripMargin)
+  }
+
+  @Test
+  def updateOutdatedExpiresShouldFail(server: GuiceJamesServer): Unit = {
+    val probe = server.getProbe(classOf[PushSubscriptionProbe])
+    val pushSubscription = probe
+      .createPushSubscription(username = BOB,
+        url = PushSubscriptionServerURL(new URL("https://example.com/push/?device=X8980fc&client=12c6d086")),
+        deviceId = DeviceClientId("12c6d086"),
+        types = Seq(MailboxTypeName, EmailDeliveryTypeName, EmailTypeName))
+
+    val invalidExpiresString = UTCDate(ZonedDateTime.now().minusDays(1)).asUTC.format(TIME_FORMATTER)
+    val request: String =
+      s"""{
+         |    "using": ["urn:ietf:params:jmap:core"],
+         |    "methodCalls": [
+         |      [
+         |        "PushSubscription/set",
+         |        {
+         |            "update": {
+         |                "${pushSubscription.id.serialise}": {
+         |                 "expires": "$invalidExpiresString"
+         |                }
+         |              }
+         |        },
+         |        "c1"
+         |      ]
+         |    ]
+         |  }""".stripMargin
+
+    val response: String = `given`
+      .body(request)
+    .when
+      .post
+    .`then`
+      .statusCode(SC_OK)
+      .contentType(JSON)
+      .extract
+      .body
+      .asString
+
+    assertThatJson(response)
+      .isEqualTo(
+        s"""{
+           |	"sessionState": "${SESSION_STATE.value}",
+           |	"methodResponses": [
+           |		[
+           |			"PushSubscription/set",
+           |			{
+           |				"notUpdated": {
+           |					"${pushSubscription.id.serialise}": {
+           |						"type": "invalidArguments",
+           |						"description": "`$invalidExpiresString` expires must be greater than now",
+           |						"properties": ["expires"]
+           |					}
+           |				}
+           |			},
+           |			"c1"
+           |		]
+           |	]
+           |}""".stripMargin)
+  }
+
+  @Test
   def updateShouldFailWhenUnknownProperty(server: GuiceJamesServer): Unit = {
     val probe = server.getProbe(classOf[PushSubscriptionProbe])
     val pushSubscription = probe
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
index 1b4fcd5..cef464d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/core/PushSubscriptionSet.scala
@@ -19,6 +19,7 @@
 
 package org.apache.james.jmap.core
 
+import java.time.ZonedDateTime
 import java.util.UUID
 
 import cats.implicits._
@@ -35,7 +36,7 @@ import org.apache.james.jmap.mail.{InvalidPropertyException, InvalidUpdateExcept
 import org.apache.james.jmap.method.WithoutAccountId
 import play.api.libs.json.{JsArray, JsObject, JsString, JsValue}
 
-import scala.util.Try
+import scala.util.{Failure, Success, Try}
 
 case class PushSubscriptionSetRequest(create: Option[Map[PushSubscriptionCreationId, JsObject]],
                                       update: Option[Map[UnparsedPushSubscriptionId, PushSubscriptionPatchObject]],
@@ -57,11 +58,7 @@ case class UnparsedPushSubscriptionId(id: Id) {
     }).map(uuid => PushSubscriptionId(uuid))
 }
 
-object PushSubscriptionUpdateResponse {
-  def empty: PushSubscriptionUpdateResponse = PushSubscriptionUpdateResponse(JsObject(Map[String, JsValue]()))
-}
-
-case class PushSubscriptionUpdateResponse(value: JsObject)
+case class PushSubscriptionUpdateResponse(expires: Option[UTCDate])
 
 object PushSubscriptionPatchObject {
   type KeyConstraint = NonEmpty
@@ -80,6 +77,7 @@ case class PushSubscriptionPatchObject(value: Map[String, JsValue]) {
     case (property, newValue) => property match {
       case "verificationCode" => VerificationCodeUpdate.parse(newValue)
       case "types" => TypesUpdate.parse(newValue, typeStateFactory)
+      case "expires" => ExpiresUpdate.parse(newValue)
       case property => PushSubscriptionPatchObject.notFound(property)
     }
   })
@@ -98,6 +96,12 @@ case class PushSubscriptionPatchObject(value: Map[String, JsValue]) {
         case _ => None
       }).headOption
 
+    val expiresUpdate: Option[ExpiresUpdate] = updates
+      .flatMap(x => x match {
+        case Right(ExpiresUpdate(newExpires)) => Some(ExpiresUpdate(newExpires))
+        case _ => None
+      }).headOption
+
     val typesUpdate: Option[TypesUpdate] = updates
       .flatMap(x => x match {
         case Right(TypesUpdate(newTypes)) => Some(TypesUpdate(newTypes))
@@ -108,7 +112,8 @@ case class PushSubscriptionPatchObject(value: Map[String, JsValue]) {
       .map(e => Left(e))
       .getOrElse(scala.Right(ValidatedPushSubscriptionPatchObject(
         verificationCodeUpdate = verificationCodeUpdate.map(_.newVerificationCode),
-        typesUpdate = typesUpdate.map(_.types))))
+        typesUpdate = typesUpdate.map(_.types),
+        expiresUpdate = expiresUpdate.map(expiresUpdate => PushSubscriptionExpiredTime(expiresUpdate.newExpires.asUTC)))))
   }
 }
 
@@ -134,22 +139,38 @@ object TypesUpdate {
   }
 }
 
+object ExpiresUpdate {
+  def parse(jsValue: JsValue): Either[PatchUpdateValidationException, Update] = jsValue match {
+    case JsString(aString) => toZonedDateTime(aString) match {
+      case Success(value) => Right(ExpiresUpdate(UTCDate(value)))
+      case Failure(e) => Left(InvalidUpdateException("expires", "This string can not be parsed to UTCDate"))
+    }
+    case _ => Left(InvalidUpdateException("expires", "Expecting a JSON string as an argument"))
+  }
+
+  private def toZonedDateTime(string: String): Try[ZonedDateTime] = Try(ZonedDateTime.parse(string))
+}
+
 sealed trait Update
 case class VerificationCodeUpdate(newVerificationCode: VerificationCode) extends Update
 case class TypesUpdate(types: Set[TypeName]) extends Update
+case class ExpiresUpdate(newExpires: UTCDate) extends Update
 
 object ValidatedPushSubscriptionPatchObject {
   val verificationCodeProperty: NonEmptyString = "verificationCode"
   val typesProperty: NonEmptyString = "types"
+  val expiresUpdate: NonEmptyString = "expires"
 }
 
 case class ValidatedPushSubscriptionPatchObject(verificationCodeUpdate: Option[VerificationCode],
-                                                typesUpdate: Option[Set[TypeName]]) {
-  val shouldUpdate: Boolean = verificationCodeUpdate.isDefined || typesUpdate.isDefined
+                                                typesUpdate: Option[Set[TypeName]],
+                                                expiresUpdate: Option[PushSubscriptionExpiredTime]) {
+  val shouldUpdate: Boolean = verificationCodeUpdate.isDefined || typesUpdate.isDefined || expiresUpdate.isDefined
 
   val updatedProperties: Properties = Properties(Set(
     verificationCodeUpdate.map(_ => ValidatedPushSubscriptionPatchObject.verificationCodeProperty),
-    typesUpdate.map(_ => ValidatedPushSubscriptionPatchObject.typesProperty))
+    typesUpdate.map(_ => ValidatedPushSubscriptionPatchObject.typesProperty),
+    expiresUpdate.map(_ => ValidatedPushSubscriptionPatchObject.expiresUpdate))
     .flatMap(_.toList))
 }
 
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/PushSubscriptionSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/PushSubscriptionSerializer.scala
index 1e3d2a3..920290d 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/PushSubscriptionSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/json/PushSubscriptionSerializer.scala
@@ -85,7 +85,7 @@ class PushSubscriptionSerializer @Inject()(typeStateFactory: TypeStateFactory) {
   private implicit val pushSubscriptionGetResponseWrites: OWrites[PushSubscriptionGetResponse] = Json.writes[PushSubscriptionGetResponse]
 
   private implicit val pushSubscriptionCreationResponseWrites: Writes[PushSubscriptionCreationResponse] = Json.writes[PushSubscriptionCreationResponse]
-  private implicit val pushSubscriptionUpdateResponseWrites: Writes[PushSubscriptionUpdateResponse] = Json.valueWrites[PushSubscriptionUpdateResponse]
+  private implicit val pushSubscriptionUpdateResponseWrites: Writes[PushSubscriptionUpdateResponse] = Json.writes[PushSubscriptionUpdateResponse]
 
   private implicit val pushSubscriptionMapSetErrorForCreationWrites: Writes[Map[PushSubscriptionCreationId, SetError]] =
     mapWrites[PushSubscriptionCreationId, SetError](_.serialise, setErrorWrites)
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetCreatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetCreatePerformer.scala
index 4fa1e55..d1940db 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetCreatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionSetCreatePerformer.scala
@@ -2,11 +2,12 @@ package org.apache.james.jmap.method
 
 import java.nio.charset.StandardCharsets
 
+import eu.timepit.refined.auto._
 import javax.inject.Inject
 import org.apache.james.jmap.api.model.{DeviceClientIdInvalidException, ExpireTimeInvalidException, PushSubscriptionCreationRequest, PushSubscriptionExpiredTime, PushSubscriptionId, PushSubscriptionKeys, PushSubscriptionServerURL, VerificationCode}
 import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository
 import org.apache.james.jmap.core.SetError.SetErrorDescription
-import org.apache.james.jmap.core.{PushSubscriptionCreation, PushSubscriptionCreationId, PushSubscriptionCreationParseException, PushSubscriptionCreationResponse, PushSubscriptionSetRequest, SetError}
+import org.apache.james.jmap.core.{Properties, PushSubscriptionCreation, PushSubscriptionCreationId, PushSubscriptionCreationParseException, PushSubscriptionCreationResponse, PushSubscriptionSetRequest, SetError}
 import org.apache.james.jmap.json.{PushSerializer, PushSubscriptionSerializer}
 import org.apache.james.jmap.method.PushSubscriptionSetCreatePerformer.{CreationFailure, CreationResult, CreationResults, CreationSuccess}
 import org.apache.james.jmap.pushsubscription.{PushRequest, PushTTL, WebPushClient}
@@ -23,7 +24,7 @@ object PushSubscriptionSetCreatePerformer {
   case class CreationFailure(clientId: PushSubscriptionCreationId, e: Throwable) extends CreationResult {
     def asMessageSetError: SetError = e match {
       case e: PushSubscriptionCreationParseException => e.setError
-      case e: ExpireTimeInvalidException => SetError.invalidArguments(SetErrorDescription(e.getMessage))
+      case e: ExpireTimeInvalidException => SetError.invalidArguments(SetErrorDescription(e.getMessage), Some(Properties("expires")))
       case e: DeviceClientIdInvalidException => SetError.invalidArguments(SetErrorDescription(e.getMessage))
       case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage))
       case _ => SetError.serverFail(SetErrorDescription(e.getMessage))
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
index f3a42e5..49fd7e6 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/method/PushSubscriptionUpdatePerformer.scala
@@ -23,10 +23,10 @@ import com.google.common.collect.ImmutableSet
 import eu.timepit.refined.auto._
 import javax.inject.Inject
 import org.apache.james.jmap.api.change.TypeStateFactory
-import org.apache.james.jmap.api.model.{PushSubscription, PushSubscriptionId, PushSubscriptionNotFoundException, TypeName, VerificationCode}
+import org.apache.james.jmap.api.model.{ExpireTimeInvalidException, PushSubscription, PushSubscriptionExpiredTime, PushSubscriptionId, PushSubscriptionNotFoundException, TypeName, VerificationCode}
 import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository
 import org.apache.james.jmap.core.SetError.SetErrorDescription
-import org.apache.james.jmap.core.{Properties, PushSubscriptionPatchObject, PushSubscriptionSetRequest, PushSubscriptionUpdateResponse, SetError, UnparsedPushSubscriptionId, ValidatedPushSubscriptionPatchObject}
+import org.apache.james.jmap.core.{Properties, PushSubscriptionPatchObject, PushSubscriptionSetRequest, PushSubscriptionUpdateResponse, SetError, UTCDate, UnparsedPushSubscriptionId, ValidatedPushSubscriptionPatchObject}
 import org.apache.james.jmap.mail.{InvalidPropertyException, InvalidUpdateException, UnsupportedPropertyUpdatedException}
 import org.apache.james.jmap.method.PushSubscriptionSetUpdatePerformer.{PushSubscriptionUpdateFailure, PushSubscriptionUpdateResult, PushSubscriptionUpdateResults, PushSubscriptionUpdateSuccess, WrongVerificationCodeException}
 import org.apache.james.mailbox.MailboxSession
@@ -37,7 +37,7 @@ import scala.jdk.CollectionConverters._
 object PushSubscriptionSetUpdatePerformer {
   case class WrongVerificationCodeException() extends RuntimeException()
   sealed trait PushSubscriptionUpdateResult
-  case class PushSubscriptionUpdateSuccess(id: PushSubscriptionId) extends PushSubscriptionUpdateResult
+  case class PushSubscriptionUpdateSuccess(id: PushSubscriptionId, serverExpires: Option[UTCDate] = None) extends PushSubscriptionUpdateResult
   case class PushSubscriptionUpdateFailure(id: UnparsedPushSubscriptionId, exception: Throwable) extends PushSubscriptionUpdateResult {
     def asSetError: SetError = exception match {
       case _: WrongVerificationCodeException => SetError.invalidProperties(SetErrorDescription("Wrong verification code"), Some(Properties("verificationCode")))
@@ -46,13 +46,14 @@ object PushSubscriptionSetUpdatePerformer {
       case e: InvalidUpdateException => SetError.invalidArguments(SetErrorDescription(s"${e.cause}"), Some(Properties(e.property)))
       case e: IllegalArgumentException => SetError.invalidArguments(SetErrorDescription(e.getMessage), None)
       case e: PushSubscriptionNotFoundException => SetError.notFound(SetErrorDescription(e.getMessage))
+      case e: ExpireTimeInvalidException => SetError.invalidArguments(SetErrorDescription(e.getMessage), Some(Properties("expires")))
       case _ => SetError.serverFail(SetErrorDescription(exception.getMessage))
     }
   }
   case class PushSubscriptionUpdateResults(results: Seq[PushSubscriptionUpdateResult]) {
     def updated: Map[PushSubscriptionId, PushSubscriptionUpdateResponse] =
       results.flatMap(result => result match {
-        case success: PushSubscriptionUpdateSuccess => Some((success.id, PushSubscriptionUpdateResponse.empty))
+        case success: PushSubscriptionUpdateSuccess => Some((success.id, PushSubscriptionUpdateResponse(success.serverExpires)))
         case _ => None
       }).toMap
     def notUpdated: Map[UnparsedPushSubscriptionId, SetError] = results.flatMap(result => result match {
@@ -94,6 +95,9 @@ class PushSubscriptionUpdatePerformer @Inject()(pushSubscriptionRepository: Push
             .getOrElse(SMono.empty),
           validatedPatch.typesUpdate
             .map(types => updateTypes(pushSubscription, types, mailboxSession))
+            .getOrElse(SMono.empty),
+          validatedPatch.expiresUpdate
+            .map(expires => updateExpires(pushSubscription, expires, mailboxSession))
             .getOrElse(SMono.empty))
           .last())
     } else {
@@ -111,4 +115,14 @@ class PushSubscriptionUpdatePerformer @Inject()(pushSubscriptionRepository: Push
   private def updateTypes(pushSubscription: PushSubscription, types: Set[TypeName], mailboxSession: MailboxSession): SMono[PushSubscriptionUpdateResult] =
     SMono(pushSubscriptionRepository.updateTypes(mailboxSession.getUser, pushSubscription.id, types.asJava))
       .`then`(SMono.just(PushSubscriptionUpdateSuccess(pushSubscription.id)))
+
+  private def updateExpires(pushSubscription: PushSubscription, inputExpires: PushSubscriptionExpiredTime, mailboxSession: MailboxSession): SMono[PushSubscriptionUpdateResult] =
+    SMono(pushSubscriptionRepository.updateExpireTime(mailboxSession.getUser, pushSubscription.id, inputExpires.value))
+      .map(toPushSubscriptionUpdate(pushSubscription, inputExpires, _))
+
+  private def toPushSubscriptionUpdate(pushSubscription: PushSubscription, inputExpires: PushSubscriptionExpiredTime, updatedExpires: PushSubscriptionExpiredTime): PushSubscriptionUpdateResult =
+    PushSubscriptionUpdateSuccess(pushSubscription.id, Some(updatedExpires)
+      .filter(updatedExpires => !updatedExpires.equals(inputExpires))
+      .map(_.value)
+      .map(UTCDate(_)))
 }

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