Repository: matrix-org/synapse

Issue:
Room avatars are broken on email notifications
Submitted by @&#8203;matthew:matrix.org


(Imported from https://matrix.org/jira/browse/SYN-695)


### Relevant code

 - [`synapse/res/templates/room.html#L4-L14`](https://github.com/matrix-org/synapse/blob/430241a1e9fd4fbb82d83958b61bbd66c9ba3505/synapse/res/templates/room.html#L4-L14)


Discussion:
Unsure in which exact way they were broken before but this looks to still be the case and still reproducible going back to "Riot notifications".

The room avatars are just colored circles. The user avatars work just fine although I have blurred them out in the image.

From the relevant code, [`synapse/res/templates/room.html#L4-L14`](https://github.com/matrix-org/synapse/blob/430241a1e9fd4fbb82d83958b61bbd66c9ba3505/synapse/res/templates/room.html#L4-L14), it appears like `room.avatar_url` isn't being serialized in or passed in to be used in the template.

![](https://user-images.githubusercontent.com/558581/129797832-898959f3-8e50-4874-b778-e668fc63a1c5.png)




Took a quick look at this out of curiosity. Looks like we'd need to provide an `"avatar_url"` key in the dict returned from `synapse.push.mailer.Mailer._get_room_vars`. Wrote a quick unit test to try and reproduce the error, but I'm having trouble with trial. Will chase down when I next get chance.
> Took a quick look at this out of curiosity. Looks like we'd need to provide an `"avatar_url"` key in the dict returned from `synapse.push.mailer.Mailer._get_room_vars`.

That sounds about the right spot to do this! Probably around:

https://github.com/matrix-org/synapse/blob/0c3565da4cdbe53646ae0bc737900526a1d3df67/synapse/push/mailer.py#L359-L365

We probably just need to check `room_state_ids` for a [`m.room.avatar`](https://matrix.org/docs/spec/client_server/latest#m-room-avatar) event and pull some info out.



Resolving patch:
diff --git a/synapse/push/mailer.py b/synapse/push/mailer.py
--- a/synapse/push/mailer.py
+++ b/synapse/push/mailer.py
@@ -258,7 +258,7 @@ async def _fetch_room_state(room_id: str) -> None:
         # actually sort our so-called rooms_in_order list, most recent room first
         rooms_in_order.sort(key=lambda r: -(notifs_by_room[r][-1]["received_ts"] or 0))
 
-        rooms = []
+        rooms: List[Dict[str, Any]] = []
 
         for r in rooms_in_order:
             roomvars = await self._get_room_vars(
@@ -362,6 +362,7 @@ async def _get_room_vars(
             "notifs": [],
             "invite": is_invite,
             "link": self._make_room_link(room_id),
+            "avatar_url": await self._get_room_avatar(room_state_ids),
         }
 
         if not is_invite:
@@ -393,6 +394,27 @@ async def _get_room_vars(
 
         return room_vars
 
+    async def _get_room_avatar(
+        self,
+        room_state_ids: StateMap[str],
+    ) -> Optional[str]:
+        """
+        Retrieve the avatar url for this room---if it exists.
+
+        Args:
+            room_state_ids: The event IDs of the current room state.
+
+        Returns:
+             room's avatar url if it's present and a string; otherwise None.
+        """
+        event_id = room_state_ids.get((EventTypes.RoomAvatar, ""))
+        if event_id:
+            ev = await self.store.get_event(event_id)
+            url = ev.content.get("url")
+            if isinstance(url, str):
+                return url
+        return None
+
     async def _get_notif_vars(
         self,
         notif: Dict[str, Any],


Test patch:
diff --git a/tests/push/test_email.py b/tests/push/test_email.py
--- a/tests/push/test_email.py
+++ b/tests/push/test_email.py
@@ -11,8 +11,9 @@
 # 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.
-
+import email.message
 import os
+from typing import Dict, List, Sequence, Tuple
 
 import attr
 import pkg_resources
@@ -70,9 +71,10 @@ def make_homeserver(self, reactor, clock):
         hs = self.setup_test_homeserver(config=config)
 
         # List[Tuple[Deferred, args, kwargs]]
-        self.email_attempts = []
+        self.email_attempts: List[Tuple[Deferred, Sequence, Dict]] = []
 
         def sendmail(*args, **kwargs):
+            # This mocks out synapse.reactor.send_email._sendmail.
             d = Deferred()
             self.email_attempts.append((d, args, kwargs))
             return d
@@ -255,6 +257,39 @@ def test_multiple_rooms(self):
         # We should get emailed about those messages
         self._check_for_mail()
 
+    def test_room_notifications_include_avatar(self):
+        # Create a room and set its avatar.
+        room = self.helper.create_room_as(self.user_id, tok=self.access_token)
+        self.helper.send_state(
+            room, "m.room.avatar", {"url": "mxc://DUMMY_MEDIA_ID"}, self.access_token
+        )
+
+        # Invite two other uses.
+        for other in self.others:
+            self.helper.invite(
+                room=room, src=self.user_id, tok=self.access_token, targ=other.id
+            )
+            self.helper.join(room=room, user=other.id, tok=other.token)
+
+        # The other users send some messages.
+        # TODO It seems that two messages are required to trigger an email?
+        self.helper.send(room, body="Alpha", tok=self.others[0].token)
+        self.helper.send(room, body="Beta", tok=self.others[1].token)
+
+        # We should get emailed about those messages
+        args, kwargs = self._check_for_mail()
+
+        # That email should contain the room's avatar
+        msg: bytes = args[5]
+        # Multipart: plain text, base 64 encoded; html, base 64 encoded
+        html = (
+            email.message_from_bytes(msg)
+            .get_payload()[1]
+            .get_payload(decode=True)
+            .decode()
+        )
+        self.assertIn("_matrix/media/v1/thumbnail/DUMMY_MEDIA_ID", html)
+
     def test_empty_room(self):
         """All users leaving a room shouldn't cause the pusher to break."""
         # Create a simple room with two users
@@ -344,9 +379,14 @@ def test_no_email_sent_after_removed(self):
         pushers = list(pushers)
         self.assertEqual(len(pushers), 0)
 
-    def _check_for_mail(self):
-        """Check that the user receives an email notification"""
+    def _check_for_mail(self) -> Tuple[Sequence, Dict]:
+        """
+        Assert that synapse sent off exactly one email notification.
 
+        Returns:
+            args and kwargs passed to synapse.reactor.send_email._sendmail for
+            that notification.
+        """
         # Get the stream ordering before it gets sent
         pushers = self.get_success(
             self.hs.get_datastore().get_pushers_by({"user_name": self.user_id})
@@ -369,8 +409,9 @@ def _check_for_mail(self):
         # One email was attempted to be sent
         self.assertEqual(len(self.email_attempts), 1)
 
+        deferred, sendmail_args, sendmail_kwargs = self.email_attempts[0]
         # Make the email succeed
-        self.email_attempts[0][0].callback(True)
+        deferred.callback(True)
         self.pump()
 
         # One email was attempted to be sent
@@ -386,3 +427,4 @@ def _check_for_mail(self):
 
         # Reset the attempts.
         self.email_attempts = []
+        return sendmail_args, sendmail_kwargs