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/02/09 04:29:56 UTC

[james-project] 28/33: JAMES-3491 Add EmailDelivery push notification support

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 4c1ac1b344238cf36beda63e4bfd8043a35e0291
Author: Benoit Tellier <bt...@linagora.com>
AuthorDate: Fri Feb 5 14:25:04 2021 +0700

    JAMES-3491 Add EmailDelivery push notification support
---
 .../jmap/rfc8621/contract/WebSocketContract.scala  | 128 ++++++++++++++++++++-
 .../james/jmap/change/JmapEventSerializer.scala    |   9 +-
 .../james/jmap/change/MailboxChangeListener.scala  |   3 +
 .../org/apache/james/jmap/change/StateChange.scala |   4 +-
 .../change/StateChangeEventSerializerTest.scala    |  33 +++++-
 5 files changed, 168 insertions(+), 9 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/WebSocketContract.scala b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
index e477607..c8897ba 100644
--- a/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
+++ b/server/protocols/jmap-rfc-8621-integration-tests/jmap-rfc-8621-integration-tests-common/src/main/scala/org/apache/james/jmap/rfc8621/contract/WebSocketContract.scala
@@ -577,7 +577,7 @@ trait WebSocketContract {
     val mailboxState: String = jmapGuiceProbe.getLatestMailboxState(accountId).getValue.toString
 
     val mailboxStateChange: String = s"""{"@type":"StateChange","changed":{"$ACCOUNT_ID":{"Mailbox":"$mailboxState"}}}"""
-    val emailStateChange: String = s"""{"@type":"StateChange","changed":{"$ACCOUNT_ID":{"Email":"$emailState"}}}"""
+    val emailStateChange: String = s"""{"@type":"StateChange","changed":{"$ACCOUNT_ID":{"EmailDelivery":"$emailState","Email":"$emailState"}}}"""
 
     assertThat(response.toOption.get.asJava)
       .hasSize(3) // email notification + mailbox notification + API response
@@ -586,6 +586,130 @@ trait WebSocketContract {
 
   @Test
   @Timeout(180)
+  // For client compatibility purposes
+  def emailDeliveryShouldNotIncludeFlagUpdatesAndDeletes(server: GuiceJamesServer): Unit = {
+    val bobPath = MailboxPath.inbox(BOB)
+    val accountId: AccountId = AccountId.fromUsername(BOB)
+    val mailboxId = server.getProbe(classOf[MailboxProbeImpl]).createMailbox(bobPath)
+
+    Thread.sleep(100)
+
+    val response: Either[String, List[String]] =
+      authenticatedRequest(server)
+        .response(asWebSocket[Identity, List[String]] {
+          ws =>
+            ws.send(WebSocketFrame.text(
+              s"""{
+                 |  "@type": "Request",
+                 |  "requestId": "req-36",
+                 |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+                 |  "methodCalls": [
+                 |    ["Email/set", {
+                 |      "accountId": "$ACCOUNT_ID",
+                 |      "create": {
+                 |        "aaaaaa":{
+                 |          "mailboxIds": {
+                 |             "${mailboxId.serialize}": true
+                 |          }
+                 |        }
+                 |      }
+                 |    }, "c1"]]
+                 |}""".stripMargin))
+
+            val responseAsJson = Json.parse(ws.receive()
+              .map { case t: Text =>
+                t.payload
+              })
+              .\("methodResponses")
+              .\(0).\(1)
+              .\("created")
+              .\("aaaaaa")
+
+            val messageId = responseAsJson
+              .\("id")
+              .get.asInstanceOf[JsString].value
+
+            Thread.sleep(100)
+
+            ws.send(WebSocketFrame.text(
+              """{
+                |  "@type": "WebSocketPushEnable",
+                |  "dataTypes": ["Mailbox", "Email", "VacationResponse", "Thread", "Identity", "EmailSubmission", "EmailDelivery"]
+                |}""".stripMargin))
+
+            Thread.sleep(100)
+
+            ws.send(WebSocketFrame.text(
+              s"""{
+                 |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+                 |  "@type": "Request",
+                 |  "methodCalls": [
+                 |    ["Email/set", {
+                 |      "accountId": "$ACCOUNT_ID",
+                 |      "update": {
+                 |        "$messageId":{
+                 |          "keywords": {
+                 |             "music": true
+                 |          }
+                 |        }
+                 |      }
+                 |    }, "c1"]]
+                 |}""".stripMargin))
+
+            val stateChange1 = ws.receive()
+              .map { case t: Text =>
+                t.payload
+              }
+            val response1 =
+              ws.receive()
+                .map { case t: Text =>
+                  t.payload
+                }
+
+            Thread.sleep(100)
+
+            ws.send(WebSocketFrame.text(
+              s"""{
+                 |  "using": ["urn:ietf:params:jmap:core", "urn:ietf:params:jmap:mail"],
+                 |  "@type": "Request",
+                 |  "methodCalls": [
+                 |    ["Email/set", {
+                 |      "accountId": "$ACCOUNT_ID",
+                 |      "destroy": ["$messageId"]
+                 |    }, "c1"]]
+                 |}""".stripMargin))
+
+            Thread.sleep(100)
+
+            val stateChange2 = ws.receive()
+              .map { case t: Text =>
+                t.payload
+              }
+            val stateChange3 =
+              ws.receive()
+                .map { case t: Text =>
+                  t.payload
+                }
+            val response2 =
+              ws.receive()
+                .map { case t: Text =>
+                  t.payload
+                }
+
+            List(response1, response2, stateChange1, stateChange2, stateChange3)
+        })
+        .send(backend)
+        .body
+
+    assertThat(response.toOption.get.asJava)
+      .hasSize(5) // update flags response + email state change notif + destroy response + email state change notif + mailbox state change notif (count)
+    assertThat(response.toOption.get.filter(s => s.startsWith("{\"@type\":\"StateChange\"")).asJava)
+      .hasSize(3)
+      .noneMatch(s => s.contains("EmailDelivery"))
+  }
+
+  @Test
+  @Timeout(180)
   def dataTypesShouldDefaultToAll(server: GuiceJamesServer): Unit = {
     val bobPath = MailboxPath.inbox(BOB)
     val accountId: AccountId = AccountId.fromUsername(BOB)
@@ -647,7 +771,7 @@ trait WebSocketContract {
     val mailboxState: String = jmapGuiceProbe.getLatestMailboxState(accountId).getValue.toString
 
     val mailboxStateChange: String = s"""{"@type":"StateChange","changed":{"$ACCOUNT_ID":{"Mailbox":"$mailboxState"}}}"""
-    val emailStateChange: String = s"""{"@type":"StateChange","changed":{"$ACCOUNT_ID":{"Email":"$emailState"}}}"""
+    val emailStateChange: String = s"""{"@type":"StateChange","changed":{"$ACCOUNT_ID":{"EmailDelivery":"$emailState","Email":"$emailState"}}}"""
 
     assertThat(response.toOption.get.asJava)
       .hasSize(3) // email notification + mailbox notification + API response
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
index ea5461e..7c38b41 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/JmapEventSerializer.scala
@@ -43,19 +43,22 @@ object StateChangeEventDTO {
     getEventId = event.eventId.getId.toString,
     getUsername = event.username.asString(),
     getMailboxState = event.mailboxState.map(_.value).map(_.toString).toJava,
-    getEmailState = event.emailState.map(_.value).map(_.toString).toJava)
+    getEmailState = event.emailState.map(_.value).map(_.toString).toJava,
+    getEmailDeliveryState = event.emailDeliveryState.map(_.value).map(_.toString).toJava)
 }
 
 case class StateChangeEventDTO(@JsonProperty("type") getType: String,
                                @JsonProperty("eventId") getEventId: String,
                                @JsonProperty("username") getUsername: String,
                                @JsonProperty("mailboxState") getMailboxState: Optional[String],
-                               @JsonProperty("emailState") getEmailState: Optional[String]) extends EventDTO {
+                               @JsonProperty("emailState") getEmailState: Optional[String],
+                               @JsonProperty("emailDeliveryState") getEmailDeliveryState: Optional[String]) extends EventDTO {
   def toDomainObject: StateChangeEvent = StateChangeEvent(
     eventId = EventId.of(getEventId),
     username = Username.of(getUsername),
     mailboxState = getMailboxState.toScala.map(State.fromStringUnchecked),
-    emailState = getEmailState.toScala.map(State.fromStringUnchecked))
+    emailState = getEmailState.toScala.map(State.fromStringUnchecked),
+    emailDeliveryState = getEmailDeliveryState.toScala.map(State.fromStringUnchecked))
 }
 
 case class JmapEventSerializer() extends EventSerializer {
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
index 6e57c4f..ad68fb2 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/MailboxChangeListener.scala
@@ -127,11 +127,14 @@ case class MailboxChangeListener @Inject() (@Named(InjectionKeys.JMAP) eventBus:
       eventId = EventId.random(),
       username = Username.of(emailChange.getAccountId.getIdentifier),
       emailState = Some(State.fromJava(emailChange.getState)),
+      emailDeliveryState = Some(State.fromJava(emailChange.getState))
+        .filter(_ => !emailChange.getCreated.isEmpty),
       mailboxState = None)
     case mailboxChange: MailboxChange => StateChangeEvent(
       eventId = EventId.random(),
       username = Username.of(mailboxChange.getAccountId.getIdentifier),
       emailState = None,
+      emailDeliveryState = None,
       mailboxState = Some(State.fromJava(mailboxChange.getState)))
   }
 }
diff --git a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/StateChange.scala b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/StateChange.scala
index faffe49..c53792a 100644
--- a/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/StateChange.scala
+++ b/server/protocols/jmap-rfc-8621/src/main/scala/org/apache/james/jmap/change/StateChange.scala
@@ -69,13 +69,15 @@ case class TypeState(changes: Map[TypeName, State]) {
 case class StateChangeEvent(eventId: EventId,
                             username: Username,
                             mailboxState: Option[State],
-                            emailState: Option[State]) extends Event {
+                            emailState: Option[State],
+                            emailDeliveryState: Option[State]) extends Event {
   def asStateChange: StateChange =
     StateChange(Map(AccountId.from(username).fold(
       failure => throw new IllegalArgumentException(failure),
       success => success) ->
       TypeState(
         MailboxTypeName.asMap(mailboxState) ++
+          EmailDeliveryTypeName.asMap(emailDeliveryState) ++
           EmailTypeName.asMap(emailState))))
 
   override val getUsername: Username = username
diff --git a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/StateChangeEventSerializerTest.scala b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/StateChangeEventSerializerTest.scala
index cab2643..7f51aa8 100644
--- a/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/StateChangeEventSerializerTest.scala
+++ b/server/protocols/jmap-rfc-8621/src/test/scala/org/apache/james/jmap/change/StateChangeEventSerializerTest.scala
@@ -21,24 +21,42 @@ package org.apache.james.jmap.change
 import org.apache.james.JsonSerializationVerifier
 import org.apache.james.core.Username
 import org.apache.james.events.Event.EventId
-import org.apache.james.jmap.change.StateChangeEventSerializerTest.{EVENT, EVENT_JSON}
+import org.apache.james.jmap.change.StateChangeEventSerializerTest.{EVENT, EVENT_JSON, EVENT_JSON_NO_DELIVERY, EVENT_NO_DELIVERY}
 import org.apache.james.jmap.core.State
 import org.apache.james.json.JsonGenericSerializer
 import org.apache.james.json.JsonGenericSerializer.UnknownTypeException
-import org.assertj.core.api.Assertions.assertThatThrownBy
+import org.assertj.core.api.Assertions.{assertThat, assertThatThrownBy}
 import org.junit.jupiter.api.Test
 
 
 object StateChangeEventSerializerTest {
   val EVENT_ID: EventId = EventId.of("6e0dd59d-660e-4d9b-b22f-0354479f47b4")
   val USERNAME: Username = Username.of("bob")
-  val EVENT: StateChangeEvent = StateChangeEvent(EVENT_ID, USERNAME, Some(State.INSTANCE), Some(State.fromStringUnchecked("2d9f1b12-b35a-43e6-9af2-0106fb53a943")))
+  val EVENT: StateChangeEvent = StateChangeEvent(eventId = EVENT_ID,
+    username = USERNAME,
+    mailboxState = Some(State.INSTANCE),
+    emailState = Some(State.fromStringUnchecked("2d9f1b12-b35a-43e6-9af2-0106fb53a943")),
+    emailDeliveryState = Some(State.fromStringUnchecked("2d9f1b12-0000-1111-3333-0106fb53a943")))
   val EVENT_JSON: String =
     """{
       |  "eventId":"6e0dd59d-660e-4d9b-b22f-0354479f47b4",
       |  "username":"bob",
       |  "mailboxState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
       |  "emailState":"2d9f1b12-b35a-43e6-9af2-0106fb53a943",
+      |  "emailDeliveryState":"2d9f1b12-0000-1111-3333-0106fb53a943",
+      |  "type":"org.apache.james.jmap.change.StateChangeEvent"
+      |}""".stripMargin
+  val EVENT_NO_DELIVERY: StateChangeEvent = StateChangeEvent(eventId = EVENT_ID,
+    username = USERNAME,
+    mailboxState = Some(State.INSTANCE),
+    emailState = Some(State.fromStringUnchecked("2d9f1b12-b35a-43e6-9af2-0106fb53a943")),
+    emailDeliveryState = None)
+  val EVENT_JSON_NO_DELIVERY: String =
+    """{
+      |  "eventId":"6e0dd59d-660e-4d9b-b22f-0354479f47b4",
+      |  "username":"bob",
+      |  "mailboxState":"2c9f1b12-b35a-43e6-9af2-0106fb53a943",
+      |  "emailState":"2d9f1b12-b35a-43e6-9af2-0106fb53a943",
       |  "type":"org.apache.james.jmap.change.StateChangeEvent"
       |}""".stripMargin
 }
@@ -67,4 +85,13 @@ class StateChangeEventSerializerTest {
                        |  "type":"org.apache.james.jmap.change.Unknown"
                        |}""".stripMargin))
       .isInstanceOf(classOf[UnknownTypeException])
+
+  @Test
+  def shouldDeserializeWhenAnOptionalFieldIsMissing(): Unit =
+    assertThat(
+      JsonGenericSerializer
+        .forModules(StateChangeEventDTO.dtoModule)
+        .withoutNestedType()
+        .deserialize(EVENT_JSON_NO_DELIVERY.stripMargin))
+      .isEqualTo(EVENT_NO_DELIVERY)
 }
\ No newline at end of file


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