You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@james.apache.org by rc...@apache.org on 2021/11/01 04:20:45 UTC

[james-project] branch master updated (3bc056c -> 7a8335f)

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

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


    from 3bc056c  report mock email count directly instead of copy+count
     new 0b30fc5  JAMES-3539 Avoid _ in package name: s/push_subscription/pushsubscription/
     new e51887b  JAMES-3539 Define PushTTL.MAX constant
     new d65f58c  JAMES-3539 PushRequest topic and urgency should be optional
     new d821e5d  JAMES-3539 Implement PushListener
     new 1e99b7e  JAMES-3539 WebPush client should allow specifying ContentCoding
     new 0ba0a64  JAMES-3539 Allow web push encryption
     new 789dc08  JAMES-3539 PushListener should encrypt payloads if required
     new 7a8335f  JAMES-3539 Fix WebPushClient

The 8 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


Summary of changes:
 pom.xml                                            |  11 ++
 server/blob/blob-aes/pom.xml                       |   1 -
 server/data/data-jmap/pom.xml                      |   8 +
 .../james/jmap/api/model/PushSubscription.scala    |  28 ++-
 .../james/jmap/pushsubscription/PushListener.scala |  74 ++++++++
 .../PushRequest.scala                              |  28 ++-
 .../WebPushClient.scala                            |  16 +-
 .../DefaultWebPushClientTest.java                  |   2 +-
 .../jmap/pushsubscription/PushListenerTest.scala   | 196 +++++++++++++++++++++
 .../PushRequestTest.scala                          |   2 +-
 .../PushServerExtension.scala                      |  23 ++-
 .../WebPushClientContract.scala                    |  25 ++-
 12 files changed, 385 insertions(+), 29 deletions(-)
 create mode 100644 server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala
 rename server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/{push_subscription => pushsubscription}/PushRequest.scala (79%)
 rename server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/{push_subscription => pushsubscription}/WebPushClient.scala (92%)
 rename server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/{push_subscription => pushsubscription}/DefaultWebPushClientTest.java (97%)
 create mode 100644 server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
 rename server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/{push_subscription => pushsubscription}/PushRequestTest.scala (98%)
 rename server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/{push_subscription => pushsubscription}/PushServerExtension.scala (86%)
 rename server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/{push_subscription => pushsubscription}/WebPushClientContract.scala (86%)

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


[james-project] 08/08: JAMES-3539 Fix WebPushClient

Posted by rc...@apache.org.
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

commit 7a8335fbced26a7db3d4885eda346250486b33f5
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Oct 29 16:57:38 2021 +0700

    JAMES-3539 Fix WebPushClient
---
 .../james/jmap/pushsubscription/PushServerExtension.scala  | 14 +++++++++++++-
 1 file changed, 13 insertions(+), 1 deletion(-)

diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
index 13b0022..fde195c 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
@@ -71,7 +71,6 @@ object MockPushServer {
         .withMethod("POST")
         .withHeader(string("Content-type"), string("application/json charset=utf-8"))
         .withHeader(string("Urgency"))
-        .withHeader(string("Content-Encoding"))
         .withHeader(string("Topic"))
         .withHeader(string("TTL")))
       .respond(response
@@ -79,5 +78,18 @@ object MockPushServer {
         .withHeader("Location", String.format("https://push.example.net/message/%s", UUID.randomUUID))
         .withHeader("Date", Clock.systemUTC.toString)
         .withBody(UUID.randomUUID.toString))
+
+    mockServer
+      .when(request
+        .withPath("/push")
+        .withMethod("POST")
+        .withHeader(string("Content-type"), string("application/json charset=utf-8"))
+        .withHeader(string("Content-Encoding"))
+        .withHeader(string("TTL")))
+      .respond(response
+        .withStatusCode(201)
+        .withHeader("Location", String.format("https://push.example.net/message/%s", UUID.randomUUID))
+        .withHeader("Date", Clock.systemUTC.toString)
+        .withBody(UUID.randomUUID.toString))
   }
 }

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


[james-project] 03/08: JAMES-3539 PushRequest topic and urgency should be optional

Posted by rc...@apache.org.
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

commit d65f58cd8773dd23d26d276e50630f6775cadd81
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 28 11:00:33 2021 +0700

    JAMES-3539 PushRequest topic and urgency should be optional
---
 .../scala/org/apache/james/jmap/pushsubscription/PushRequest.scala    | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
index 511c68c..e7d9ab7 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
@@ -92,6 +92,6 @@ case class PushTopicConstraint()
 case class PushTTLConstraint()
 
 case class PushRequest(ttl: PushTTL,
-                       topic: Option[PushTopic],
-                       urgency: Option[PushUrgency],
+                       topic: Option[PushTopic] = None,
+                       urgency: Option[PushUrgency] = None,
                        payload: Array[Byte])

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


[james-project] 02/08: JAMES-3539 Define PushTTL.MAX constant

Posted by rc...@apache.org.
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

commit e51887b4e7126c76e9a0b59dfac7e63d0cc3bf14
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 28 10:15:17 2021 +0700

    JAMES-3539 Define PushTTL.MAX constant
---
 .../scala/org/apache/james/jmap/pushsubscription/PushRequest.scala | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
index 4525344..511c68c 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
@@ -71,15 +71,20 @@ object PushTopic {
 object PushTTL {
   type PushTTL = Long Refined PushTTLConstraint
 
+  private val MAX_INT = 2147483647L
+
   implicit val validateTTL: Validate.Plain[Long, PushTTLConstraint] =
-    Validate.fromPredicate(s => s >= 0 && s < 2147483648L,
+    Validate.fromPredicate(s => s >= 0 && s <= MAX_INT,
       s => s"'$s' invalid. Should be non-negative numeric and no greater than 2^31",
       PushTTLConstraint())
 
+
   def validate(value: Long): Either[IllegalArgumentException, PushTTL] =
     refined.refineV[PushTTLConstraint](value)
       .left
       .map(new IllegalArgumentException(_))
+
+  val MAX: PushTTL = validate(MAX_INT).toOption.get
 }
 
 case class PushTopicConstraint()

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


[james-project] 06/08: JAMES-3539 Allow web push encryption

Posted by rc...@apache.org.
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

commit 0ba0a6475c7ec7b65a4e7e2b0b75e4a769347d4f
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 28 14:28:17 2021 +0700

    JAMES-3539 Allow web push encryption
---
 pom.xml                                            | 11 +++++++++
 server/blob/blob-aes/pom.xml                       |  1 -
 server/data/data-jmap/pom.xml                      |  8 +++++++
 .../james/jmap/api/model/PushSubscription.scala    | 28 +++++++++++++++++++---
 4 files changed, 44 insertions(+), 4 deletions(-)

diff --git a/pom.xml b/pom.xml
index 04ec3a3..fe0c09b 100644
--- a/pom.xml
+++ b/pom.xml
@@ -641,6 +641,7 @@
         <jasypt.version>1.9.3</jasypt.version>
         <guice.version>5.0.1</guice.version>
         <logback.version>1.2.5</logback.version>
+        <tink.version>1.6.1</tink.version>
 
         <bouncycastle.version>1.69</bouncycastle.version>
 
@@ -2077,6 +2078,16 @@
                 <version>1.2.0</version>
             </dependency>
             <dependency>
+                <groupId>com.google.crypto.tink</groupId>
+                <artifactId>apps-webpush</artifactId>
+                <version>${tink.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>com.google.crypto.tink</groupId>
+                <artifactId>tink</artifactId>
+                <version>${tink.version}</version>
+            </dependency>
+            <dependency>
                 <groupId>com.google.guava</groupId>
                 <artifactId>guava</artifactId>
                 <version>${guava.version}</version>
diff --git a/server/blob/blob-aes/pom.xml b/server/blob/blob-aes/pom.xml
index b84693a..b612894 100644
--- a/server/blob/blob-aes/pom.xml
+++ b/server/blob/blob-aes/pom.xml
@@ -59,7 +59,6 @@
         <dependency>
             <groupId>com.google.crypto.tink</groupId>
             <artifactId>tink</artifactId>
-            <version>1.6.1</version>
         </dependency>
         <dependency>
             <groupId>com.google.guava</groupId>
diff --git a/server/data/data-jmap/pom.xml b/server/data/data-jmap/pom.xml
index daec8fc..4048502 100644
--- a/server/data/data-jmap/pom.xml
+++ b/server/data/data-jmap/pom.xml
@@ -100,6 +100,14 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>com.google.crypto.tink</groupId>
+            <artifactId>apps-webpush</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.google.crypto.tink</groupId>
+            <artifactId>tink</artifactId>
+        </dependency>
+        <dependency>
             <groupId>com.google.guava</groupId>
             <artifactId>guava</artifactId>
         </dependency>
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 31080f7..a88a5f8 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
@@ -20,8 +20,14 @@
 package org.apache.james.jmap.api.model
 
 import java.net.URL
-import java.time.{Clock, ZonedDateTime}
-import java.util.UUID
+import java.security.interfaces.ECPublicKey
+import java.time.ZonedDateTime
+import java.util.{Base64, UUID}
+
+import com.google.crypto.tink.HybridEncrypt
+import com.google.crypto.tink.apps.webpush.WebPushHybridEncrypt
+import com.google.crypto.tink.subtle.EllipticCurves
+
 import scala.util.Try
 
 object PushSubscriptionId {
@@ -49,7 +55,23 @@ case class PushSubscriptionExpiredTime(value: ZonedDateTime) {
   def isBefore(date: ZonedDateTime): Boolean = value.isBefore(date)
 }
 
-case class PushSubscriptionKeys(p256dh: String, auth: String)
+object PushSubscriptionKeys {
+  def validate(keys: PushSubscriptionKeys): Try[PushSubscriptionKeys] = Try(keys.asHybridEncrypt()).map(_ => keys)
+}
+
+case class PushSubscriptionKeys(p256dh: String, auth: String) {
+  // Follows https://datatracker.ietf.org/doc/html/rfc8291
+  // Message Encryption for Web Push
+  def encrypt(payload: Array[Byte]): Array[Byte] = asHybridEncrypt()
+    .encrypt(payload, null)
+
+  private def asHybridEncrypt(): HybridEncrypt =  new WebPushHybridEncrypt.Builder()
+    .withAuthSecret(Base64.getDecoder().decode(auth))
+    .withRecipientPublicKey(asECPublicKey())
+    .build()
+
+  private def asECPublicKey(): ECPublicKey = EllipticCurves.getEcPublicKey(Base64.getDecoder.decode(p256dh))
+}
 
 case class PushSubscriptionCreationRequest(deviceClientId: DeviceClientId,
                                            url: PushSubscriptionServerURL,

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


[james-project] 07/08: JAMES-3539 PushListener should encrypt payloads if required

Posted by rc...@apache.org.
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

commit 789dc08e5bab35c029e901208411ca65814a6e67
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 28 14:28:40 2021 +0700

    JAMES-3539 PushListener should encrypt payloads if required
---
 .../james/jmap/pushsubscription/PushListener.scala | 16 ++++---
 .../jmap/pushsubscription/PushListenerTest.scala   | 52 +++++++++++++++++++++-
 2 files changed, 61 insertions(+), 7 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala
index ff0dc01..857cc64 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala
@@ -58,11 +58,17 @@ class PushListener @Inject()(pushRepository: PushSubscriptionRepository,
     stateChangeEvent
       .asStateChange
       .filter(pushSubscription.types.toSet)
-      .fold(SMono.empty[Unit])(stateChange => SMono(webPushClient.push(pushSubscription.url, asPushRequest(stateChange))))
+      .fold(SMono.empty[Unit])(stateChange => SMono(webPushClient.push(pushSubscription.url, asPushRequest(stateChange, pushSubscription))))
 
-  private def asPushRequest(stateChange: StateChange): PushRequest =
-    PushRequest(ttl = PushTTL.MAX, payload = asBytes(stateChange))
+  private def asPushRequest(stateChange: StateChange, pushSubscription: PushSubscription): PushRequest =
+    PushRequest(ttl = PushTTL.MAX,
+      contentCoding = pushSubscription.keys.map(_ => Aes128gcm),
+      payload = asBytes(stateChange, pushSubscription))
 
-  private def asBytes(stateChange: StateChange) =
-    Json.stringify(pushSerializer.serializeSSE(stateChange)).getBytes(StandardCharsets.UTF_8)
+  private def asBytes(stateChange: StateChange, pushSubscription: PushSubscription) = {
+    val clearTextPayload = Json.stringify(pushSerializer.serializeSSE(stateChange)).getBytes(StandardCharsets.UTF_8)
+    pushSubscription.keys
+      .map(keys => keys.encrypt(clearTextPayload))
+      .getOrElse(clearTextPayload)
+  }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
index b281a6c..b962dc3 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
@@ -20,13 +20,18 @@
 package org.apache.james.jmap.pushsubscription
 
 import java.nio.charset.StandardCharsets
+import java.security.interfaces.{ECPrivateKey, ECPublicKey}
 import java.time.Clock
-import java.util.UUID
+import java.util.{Base64, UUID}
 
 import com.google.common.collect.ImmutableSet
+import com.google.common.hash.Hashing
+import com.google.crypto.tink.apps.webpush.WebPushHybridDecrypt
+import com.google.crypto.tink.subtle.EllipticCurves.CurveType
+import com.google.crypto.tink.subtle.{EllipticCurves, Random}
 import org.apache.james.core.Username
 import org.apache.james.events.Event.EventId
-import org.apache.james.jmap.api.model.{DeviceClientId, PushSubscriptionCreationRequest, PushSubscriptionServerURL, TypeName}
+import org.apache.james.jmap.api.model.{DeviceClientId, PushSubscriptionCreationRequest, PushSubscriptionKeys, PushSubscriptionServerURL, TypeName}
 import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository
 import org.apache.james.jmap.change.{EmailDeliveryTypeName, EmailTypeName, MailboxTypeName, StateChangeEvent, TypeStateFactory}
 import org.apache.james.jmap.core.UuidState
@@ -145,4 +150,47 @@ class PushListenerTest {
         .isEqualTo(s"""{"@type":"StateChange","changed":{"$bobAccountId":{"Email":"${state1.value.toString}","Mailbox":"${state2.value.toString}"}}}""")
     })
   }
+
+  @Test
+  def pushShouldEncryptMessages(): Unit = {
+    val uaKeyPair = EllipticCurves.generateKeyPair(CurveType.NIST_P256)
+    val uaPrivateKey: ECPrivateKey = uaKeyPair.getPrivate.asInstanceOf[ECPrivateKey]
+    val uaPublicKey: ECPublicKey = uaKeyPair.getPublic.asInstanceOf[ECPublicKey]
+    val authSecret = Random.randBytes(16)
+
+    val hybridDecrypt = new WebPushHybridDecrypt.Builder()
+      .withAuthSecret(authSecret)
+      .withRecipientPublicKey(uaPublicKey)
+      .withRecipientPrivateKey(uaPrivateKey)
+      .build
+
+    val id = SMono(pushSubscriptionRepository.save(bob, PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId("junit"),
+      keys = Some(PushSubscriptionKeys(p256dh = Base64.getEncoder.encodeToString(uaPublicKey.getEncoded),
+        auth = Base64.getEncoder.encodeToString(authSecret))),
+      url = url,
+      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+    SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
+
+    val state1 = UuidState(UUID.randomUUID())
+    val state2 = UuidState(UUID.randomUUID())
+    val state3 = UuidState(UUID.randomUUID())
+    SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob,
+      Map(EmailTypeName -> state1, MailboxTypeName -> state2, EmailDeliveryTypeName -> state3)))).block()
+
+    val argumentCaptor: ArgumentCaptor[PushRequest] = ArgumentCaptor.forClass(classOf[PushRequest])
+    verify(webPushClient).push(ArgumentMatchers.eq(url), argumentCaptor.capture())
+    val decryptedPayload = s"""{"@type":"StateChange","changed":{"$bobAccountId":{"Email":"${state1.value.toString}","Mailbox":"${state2.value.toString}"}}}"""
+    val encryptedPayload = argumentCaptor.getValue.payload
+    SoftAssertions.assertSoftly(softly => {
+      // We positionned well the Content-Encoding header
+      softly.assertThat(argumentCaptor.getValue.contentCoding.toJava).contains(Aes128gcm)
+      // We are able to decrypt the payload
+      softly.assertThat(new String(hybridDecrypt.decrypt(encryptedPayload, null), StandardCharsets.UTF_8))
+        .isEqualTo(decryptedPayload)
+      // The content had been well modified by the encryption
+      softly.assertThat(Hashing.sha256().hashBytes(encryptedPayload))
+        .isNotEqualTo(Hashing.sha256().hashBytes(decryptedPayload.getBytes(StandardCharsets.UTF_8)))
+    })
+  }
 }

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


[james-project] 01/08: JAMES-3539 Avoid _ in package name: s/push_subscription/pushsubscription/

Posted by rc...@apache.org.
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

commit 0b30fc5ccacbe52f2264b54ffcdac162ed5f8bf9
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 28 09:49:13 2021 +0700

    JAMES-3539 Avoid _ in package name: s/push_subscription/pushsubscription/
---
 .../PushRequest.scala                                      |  6 +++---
 .../WebPushClient.scala                                    | 14 +++++++-------
 .../DefaultWebPushClientTest.java                          |  2 +-
 .../PushRequestTest.scala                                  |  2 +-
 .../PushServerExtension.scala                              | 10 +++++-----
 .../WebPushClientContract.scala                            | 10 +++++-----
 6 files changed, 22 insertions(+), 22 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/push_subscription/PushRequest.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
similarity index 94%
rename from server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/push_subscription/PushRequest.scala
rename to server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
index 159be9a..4525344 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/push_subscription/PushRequest.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
@@ -17,13 +17,13 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.push_subscription
+package org.apache.james.jmap.pushsubscription
 
 import com.google.common.base.CharMatcher
 import eu.timepit.refined
 import eu.timepit.refined.api.{Refined, Validate}
-import org.apache.james.jmap.push_subscription.PushTTL.PushTTL
-import org.apache.james.jmap.push_subscription.PushTopic.PushTopic
+import org.apache.james.jmap.pushsubscription.PushTTL.PushTTL
+import org.apache.james.jmap.pushsubscription.PushTopic.PushTopic
 
 object PushUrgency {
   def default: PushUrgency = Normal
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/push_subscription/WebPushClient.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/WebPushClient.scala
similarity index 94%
rename from server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/push_subscription/WebPushClient.scala
rename to server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/WebPushClient.scala
index fee3fd2..7c5ab59 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/push_subscription/WebPushClient.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/WebPushClient.scala
@@ -17,13 +17,17 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.push_subscription
+package org.apache.james.jmap.pushsubscription
+
+import java.nio.charset.StandardCharsets
+import java.time.Duration
+import java.time.temporal.ChronoUnit
 
 import io.netty.buffer.Unpooled
 import io.netty.handler.codec.http.HttpResponseStatus
 import org.apache.james.jmap.api.model.PushSubscriptionServerURL
-import org.apache.james.jmap.push_subscription.DefaultWebPushClient.{PUSH_SERVER_ERROR_RESPONSE_MAX_LENGTH, buildHttpClient}
-import org.apache.james.jmap.push_subscription.WebPushClientHeader.{DEFAULT_TIMEOUT, MESSAGE_URGENCY, TIME_TO_LIVE, TOPIC}
+import org.apache.james.jmap.pushsubscription.DefaultWebPushClient.buildHttpClient
+import org.apache.james.jmap.pushsubscription.WebPushClientHeader.{DEFAULT_TIMEOUT, MESSAGE_URGENCY, TIME_TO_LIVE, TOPIC}
 import org.reactivestreams.Publisher
 import reactor.core.publisher.Mono
 import reactor.core.scala.publisher.SMono
@@ -31,10 +35,6 @@ import reactor.netty.ByteBufMono
 import reactor.netty.http.client.{HttpClient, HttpClientResponse}
 import reactor.netty.resources.ConnectionProvider
 
-import java.nio.charset.StandardCharsets
-import java.time.Duration
-import java.time.temporal.ChronoUnit
-
 trait WebPushClient {
   def push(pushServerUrl: PushSubscriptionServerURL, request: PushRequest): Publisher[Unit]
 }
diff --git a/server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/push_subscription/DefaultWebPushClientTest.java b/server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/pushsubscription/DefaultWebPushClientTest.java
similarity index 97%
rename from server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/push_subscription/DefaultWebPushClientTest.java
rename to server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/pushsubscription/DefaultWebPushClientTest.java
index 84a558e..13fac25 100644
--- a/server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/push_subscription/DefaultWebPushClientTest.java
+++ b/server/protocols/jmap-rfc-8621/src/test/java/org/apache/james/jmap/pushsubscription/DefaultWebPushClientTest.java
@@ -17,7 +17,7 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.push_subscription;
+package org.apache.james.jmap.pushsubscription;
 
 import java.net.URL;
 
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/PushRequestTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushRequestTest.scala
similarity index 98%
rename from server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/PushRequestTest.scala
rename to server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushRequestTest.scala
index c274aa5..4d60fca 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/PushRequestTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushRequestTest.scala
@@ -17,7 +17,7 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.push_subscription
+package org.apache.james.jmap.pushsubscription
 
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.jupiter.api.Test
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/PushServerExtension.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
similarity index 98%
rename from server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/PushServerExtension.scala
rename to server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
index 489bbba..5b18a11 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/PushServerExtension.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
@@ -17,7 +17,11 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.push_subscription
+package org.apache.james.jmap.pushsubscription
+
+import java.net.URL
+import java.time.Clock
+import java.util.UUID
 
 import org.junit.jupiter.api.extension.{AfterEachCallback, BeforeEachCallback, ExtensionContext, ParameterContext, ParameterResolver}
 import org.mockserver.configuration.ConfigurationProperties
@@ -27,10 +31,6 @@ import org.mockserver.model.HttpRequest.request
 import org.mockserver.model.HttpResponse.response
 import org.mockserver.model.NottableString.{not, string}
 
-import java.net.URL
-import java.time.Clock
-import java.util.UUID
-
 class PushServerExtension extends BeforeEachCallback with AfterEachCallback with ParameterResolver {
   var mockServer: ClientAndServer = _
 
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/WebPushClientContract.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/WebPushClientContract.scala
similarity index 97%
rename from server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/WebPushClientContract.scala
rename to server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/WebPushClientContract.scala
index dad63de..258fc58 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/push_subscription/WebPushClientContract.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/WebPushClientContract.scala
@@ -17,10 +17,13 @@
  * under the License.                                           *
  ****************************************************************/
 
-package org.apache.james.jmap.push_subscription
+package org.apache.james.jmap.pushsubscription
+
+import java.net.URL
+import java.nio.charset.StandardCharsets
 
 import org.apache.james.jmap.api.model.PushSubscriptionServerURL
-import org.apache.james.jmap.push_subscription.WebPushClientTestFixture.PUSH_REQUEST_SAMPLE
+import org.apache.james.jmap.pushsubscription.WebPushClientTestFixture.PUSH_REQUEST_SAMPLE
 import org.assertj.core.api.Assertions.{assertThatCode, assertThatThrownBy}
 import org.junit.jupiter.api.Test
 import org.junit.jupiter.params.ParameterizedTest
@@ -32,9 +35,6 @@ import org.mockserver.verify.VerificationTimes
 import reactor.core.scala.publisher.SMono
 import reactor.core.scheduler.Schedulers
 
-import java.net.URL
-import java.nio.charset.StandardCharsets
-
 object WebPushClientTestFixture {
   val PUSH_CLIENT_CONFIGURATION: PushClientConfiguration =
     PushClientConfiguration(

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


[james-project] 04/08: JAMES-3539 Implement PushListener

Posted by rc...@apache.org.
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

commit d821e5dfd992e0d10ef0ca9e1bd068d20bd45545
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 28 11:01:09 2021 +0700

    JAMES-3539 Implement PushListener
---
 .../james/jmap/pushsubscription/PushListener.scala |  68 ++++++++++
 .../jmap/pushsubscription/PushListenerTest.scala   | 148 +++++++++++++++++++++
 2 files changed, 216 insertions(+)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala
new file mode 100644
index 0000000..ff0dc01
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushListener.scala
@@ -0,0 +1,68 @@
+/****************************************************************
+ * 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.pushsubscription
+
+import java.nio.charset.StandardCharsets
+
+import javax.inject.Inject
+import org.apache.james.events.EventListener.ReactiveGroupEventListener
+import org.apache.james.events.{Event, Group}
+import org.apache.james.jmap.api.model.PushSubscription
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository
+import org.apache.james.jmap.change.StateChangeEvent
+import org.apache.james.jmap.core.StateChange
+import org.apache.james.jmap.json.PushSerializer
+import org.apache.james.util.ReactorUtils
+import org.reactivestreams.Publisher
+import play.api.libs.json.Json
+import reactor.core.scala.publisher.{SFlux, SMono}
+
+case class PushListenerGroup() extends Group {}
+
+class PushListener @Inject()(pushRepository: PushSubscriptionRepository,
+                   webPushClient: WebPushClient,
+                   pushSerializer: PushSerializer) extends ReactiveGroupEventListener {
+
+  override def getDefaultGroup: Group = PushListenerGroup()
+
+  override def reactiveEvent(event: Event): Publisher[Void] =
+    event match {
+      case event: StateChangeEvent =>
+        SFlux(pushRepository.list(event.username))
+          .filter(_.validated)
+          .flatMap(sendNotification(_, event), ReactorUtils.DEFAULT_CONCURRENCY)
+          .`then`()
+      case _ => SMono.empty
+    }
+
+  override def isHandling(event: Event): Boolean = event.isInstanceOf[StateChangeEvent]
+
+  private def sendNotification(pushSubscription: PushSubscription, stateChangeEvent: StateChangeEvent): Publisher[Unit] =
+    stateChangeEvent
+      .asStateChange
+      .filter(pushSubscription.types.toSet)
+      .fold(SMono.empty[Unit])(stateChange => SMono(webPushClient.push(pushSubscription.url, asPushRequest(stateChange))))
+
+  private def asPushRequest(stateChange: StateChange): PushRequest =
+    PushRequest(ttl = PushTTL.MAX, payload = asBytes(stateChange))
+
+  private def asBytes(stateChange: StateChange) =
+    Json.stringify(pushSerializer.serializeSSE(stateChange)).getBytes(StandardCharsets.UTF_8)
+}
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
new file mode 100644
index 0000000..b281a6c
--- /dev/null
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushListenerTest.scala
@@ -0,0 +1,148 @@
+/****************************************************************
+ * 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.pushsubscription
+
+import java.nio.charset.StandardCharsets
+import java.time.Clock
+import java.util.UUID
+
+import com.google.common.collect.ImmutableSet
+import org.apache.james.core.Username
+import org.apache.james.events.Event.EventId
+import org.apache.james.jmap.api.model.{DeviceClientId, PushSubscriptionCreationRequest, PushSubscriptionServerURL, TypeName}
+import org.apache.james.jmap.api.pushsubscription.PushSubscriptionRepository
+import org.apache.james.jmap.change.{EmailDeliveryTypeName, EmailTypeName, MailboxTypeName, StateChangeEvent, TypeStateFactory}
+import org.apache.james.jmap.core.UuidState
+import org.apache.james.jmap.json.PushSerializer
+import org.apache.james.jmap.memory.pushsubscription.MemoryPushSubscriptionRepository
+import org.assertj.core.api.SoftAssertions
+import org.junit.jupiter.api.{BeforeEach, Test}
+import org.mockito.ArgumentMatchers.any
+import org.mockito.Mockito.{mock, verify, verifyNoInteractions, when}
+import org.mockito.{ArgumentCaptor, ArgumentMatchers}
+import reactor.core.scala.publisher.SMono
+
+import scala.jdk.OptionConverters._
+
+class PushListenerTest {
+  val bob: Username = Username.of("bob@localhost")
+  val bobAccountId: String = "405010d6c16c16dec36f3d7a7596c2d757ba7b57904adc4801a63e40914fd5c9"
+  val url: PushSubscriptionServerURL = PushSubscriptionServerURL.from("http://localhost:9999/push").get
+
+  var testee: PushListener = _
+  var pushSubscriptionRepository: PushSubscriptionRepository = _
+  var webPushClient: WebPushClient = _
+
+  @BeforeEach
+  def setUp(): Unit = {
+    val pushSerializer = PushSerializer(TypeStateFactory(ImmutableSet.of[TypeName](MailboxTypeName, EmailTypeName, EmailDeliveryTypeName)))
+
+    pushSubscriptionRepository = new MemoryPushSubscriptionRepository(Clock.systemUTC())
+    webPushClient = mock(classOf[WebPushClient])
+    testee = new PushListener(pushSubscriptionRepository, webPushClient, pushSerializer)
+
+    when(webPushClient.push(any(), any())).thenReturn(SMono.empty[Unit])
+  }
+
+  @Test
+  def shouldNotPushWhenNoSubscriptions(): Unit = {
+    SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob,
+      Map(EmailTypeName -> UuidState(UUID.randomUUID()))))).block()
+
+    verifyNoInteractions(webPushClient)
+  }
+
+  @Test
+  def shouldNotPushWhenNotVerified(): Unit = {
+    SMono(pushSubscriptionRepository.save(bob, PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId("junit"),
+      url = url,
+      types = Seq(MailboxTypeName, EmailTypeName)))).block()
+
+    SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob,
+      Map(EmailTypeName -> UuidState(UUID.randomUUID()))))).block()
+
+    verifyNoInteractions(webPushClient)
+  }
+
+  @Test
+  def shouldNotPushWhenTypeMismatch(): Unit = {
+    val id = SMono(pushSubscriptionRepository.save(bob, PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId("junit"),
+      url = url,
+      types = Seq(EmailDeliveryTypeName)))).block().id
+    SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
+
+    SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob,
+      Map(EmailTypeName -> UuidState(UUID.randomUUID()))))).block()
+
+    verifyNoInteractions(webPushClient)
+  }
+
+  @Test
+  def shouldPushWhenValidated(): Unit = {
+    val id = SMono(pushSubscriptionRepository.save(bob, PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId("junit"),
+      url = url,
+      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+    SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
+
+    val state1 = UuidState(UUID.randomUUID())
+    SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob,
+      Map(EmailTypeName -> state1)))).block()
+
+    val argumentCaptor: ArgumentCaptor[PushRequest] = ArgumentCaptor.forClass(classOf[PushRequest])
+    verify(webPushClient).push(ArgumentMatchers.eq(url), argumentCaptor.capture())
+
+    SoftAssertions.assertSoftly(softly => {
+      softly.assertThat(argumentCaptor.getValue.ttl).isEqualTo(PushTTL.MAX)
+      softly.assertThat(argumentCaptor.getValue.topic.toJava).isEmpty
+      softly.assertThat(argumentCaptor.getValue.urgency.toJava).isEmpty
+      softly.assertThat(new String(argumentCaptor.getValue.payload, StandardCharsets.UTF_8))
+        .isEqualTo(s"""{"@type":"StateChange","changed":{"$bobAccountId":{"Email":"${state1.value.toString}"}}}""")
+    })
+  }
+
+  @Test
+  def unwantedTypesShouldBeFilteredOut(): Unit = {
+    val id = SMono(pushSubscriptionRepository.save(bob, PushSubscriptionCreationRequest(
+      deviceClientId = DeviceClientId("junit"),
+      url = url,
+      types = Seq(EmailTypeName, MailboxTypeName)))).block().id
+    SMono(pushSubscriptionRepository.validateVerificationCode(bob, id)).block()
+
+    val state1 = UuidState(UUID.randomUUID())
+    val state2 = UuidState(UUID.randomUUID())
+    val state3 = UuidState(UUID.randomUUID())
+    SMono(testee.reactiveEvent(StateChangeEvent(EventId.random(), bob,
+      Map(EmailTypeName -> state1, MailboxTypeName -> state2, EmailDeliveryTypeName -> state3)))).block()
+
+    val argumentCaptor: ArgumentCaptor[PushRequest] = ArgumentCaptor.forClass(classOf[PushRequest])
+    verify(webPushClient).push(ArgumentMatchers.eq(url), argumentCaptor.capture())
+
+    SoftAssertions.assertSoftly(softly => {
+      softly.assertThat(argumentCaptor.getValue.ttl).isEqualTo(PushTTL.MAX)
+      softly.assertThat(argumentCaptor.getValue.topic.toJava).isEmpty
+      softly.assertThat(argumentCaptor.getValue.urgency.toJava).isEmpty
+      softly.assertThat(new String(argumentCaptor.getValue.payload, StandardCharsets.UTF_8))
+        .isEqualTo(s"""{"@type":"StateChange","changed":{"$bobAccountId":{"Email":"${state1.value.toString}","Mailbox":"${state2.value.toString}"}}}""")
+    })
+  }
+}

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


[james-project] 05/08: JAMES-3539 WebPush client should allow specifying ContentCoding

Posted by rc...@apache.org.
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

commit 1e99b7edda161ce17d9ddcd8f5abf9897cb2cd63
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Thu Oct 28 14:19:55 2021 +0700

    JAMES-3539 WebPush client should allow specifying ContentCoding
---
 .../apache/james/jmap/pushsubscription/PushRequest.scala  | 11 +++++++++++
 .../james/jmap/pushsubscription/WebPushClient.scala       |  6 ++++--
 .../james/jmap/pushsubscription/PushServerExtension.scala |  1 +
 .../jmap/pushsubscription/WebPushClientContract.scala     | 15 +++++++++++++++
 4 files changed, 31 insertions(+), 2 deletions(-)

diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
index e7d9ab7..3a45216 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/PushRequest.scala
@@ -91,7 +91,18 @@ case class PushTopicConstraint()
 
 case class PushTTLConstraint()
 
+// Follows https://datatracker.ietf.org/doc/html/rfc8188
+// Encrypted Content-Encoding for HTTP
+trait ContentCodingType {
+  def value: String
+}
+case object Aes128gcm extends ContentCodingType {
+  val value: String = "aes128gcm"
+}
+case class ContentCoding(`type`: ContentCodingType)
+
 case class PushRequest(ttl: PushTTL,
+                       contentCoding: Option[ContentCodingType] = None,
                        topic: Option[PushTopic] = None,
                        urgency: Option[PushUrgency] = None,
                        payload: Array[Byte])
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/WebPushClient.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/WebPushClient.scala
index 7c5ab59..eaa709b 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/WebPushClient.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/pushsubscription/WebPushClient.scala
@@ -26,8 +26,8 @@ import java.time.temporal.ChronoUnit
 import io.netty.buffer.Unpooled
 import io.netty.handler.codec.http.HttpResponseStatus
 import org.apache.james.jmap.api.model.PushSubscriptionServerURL
-import org.apache.james.jmap.pushsubscription.DefaultWebPushClient.buildHttpClient
-import org.apache.james.jmap.pushsubscription.WebPushClientHeader.{DEFAULT_TIMEOUT, MESSAGE_URGENCY, TIME_TO_LIVE, TOPIC}
+import org.apache.james.jmap.pushsubscription.DefaultWebPushClient.{PUSH_SERVER_ERROR_RESPONSE_MAX_LENGTH, buildHttpClient}
+import org.apache.james.jmap.pushsubscription.WebPushClientHeader.{CONTENT_ENCODING, DEFAULT_TIMEOUT, MESSAGE_URGENCY, TIME_TO_LIVE, TOPIC}
 import org.reactivestreams.Publisher
 import reactor.core.publisher.Mono
 import reactor.core.scala.publisher.SMono
@@ -47,6 +47,7 @@ case class PushClientConfiguration(maxTimeoutSeconds: Option[Int],
 
 object WebPushClientHeader {
   val TIME_TO_LIVE: String = "TTL"
+  val CONTENT_ENCODING: String = "Content-Encoding"
   val MESSAGE_URGENCY: String = "Urgency"
   val TOPIC: String = "Topic"
   val DEFAULT_TIMEOUT: Duration = Duration.of(30, ChronoUnit.SECONDS)
@@ -88,6 +89,7 @@ class DefaultWebPushClient(configuration: PushClientConfiguration) extends WebPu
         builder.add(TIME_TO_LIVE, request.ttl.value)
         builder.add(MESSAGE_URGENCY, request.urgency.getOrElse(PushUrgency.default).value)
         request.topic.foreach(t => builder.add(TOPIC, t.value))
+        request.contentCoding.foreach(f => builder.add(CONTENT_ENCODING, f.value))
       })
       .post()
       .uri(pushServerUrl.value.toString)
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
index 5b18a11..13b0022 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/PushServerExtension.scala
@@ -71,6 +71,7 @@ object MockPushServer {
         .withMethod("POST")
         .withHeader(string("Content-type"), string("application/json charset=utf-8"))
         .withHeader(string("Urgency"))
+        .withHeader(string("Content-Encoding"))
         .withHeader(string("Topic"))
         .withHeader(string("TTL")))
       .respond(response
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/WebPushClientContract.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/WebPushClientContract.scala
index 258fc58..6d4e616 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/WebPushClientContract.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/pushsubscription/WebPushClientContract.scala
@@ -79,6 +79,21 @@ trait WebPushClientContract {
       VerificationTimes.atLeast(1))
   }
 
+  @Test
+  def pushShouldSpecifyContentCoding(pushServer: ClientAndServer): Unit = {
+    SMono.fromPublisher(testee.push(getPushSubscriptionServerURL, PushRequest(
+      ttl = PushTTL.validate(15).toOption.get,
+      contentCoding = Some(Aes128gcm),
+      payload = "Content123".getBytes(StandardCharsets.UTF_8))))
+      .block()
+    pushServer.verify(request()
+      .withPath("/push")
+      .withHeader("TTL", "15")
+      .withHeader("Content-Encoding", "aes128gcm")
+      .withBody(new String(PUSH_REQUEST_SAMPLE.payload, StandardCharsets.UTF_8)),
+      VerificationTimes.atLeast(1))
+  }
+
   @ParameterizedTest
   @ValueSource(ints = Array(500, 501, 502, 503, 504))
   def pushRequestShouldThrowWhenPushServerReturnFailHTTPStatus(httpErrorCode: Int, pushServer: ClientAndServer): Unit = {

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