Repository: matrix-org/synapse

Issue:
Emails are not canonicalised on /_matrix/client/*/pushers/set
### Description

Emails sent to `/pushers/set` are not canonicalised, while they probably should.

### Steps to reproduce

- choose an email with an uppercase letter in it, e.g. `FooBar@example.com`
- set that email to a user via the admin API. The email will end up normalized in the DB
- try to set an email pusher with the same email. It should work but doesn't and returns with an `Email not found` error

### Version information

- **Homeserver**: other
- **Version**: 1.47.0
- **Install method**: Docker/Kubernetes
- **Platform**: N/A

---

Here, the `pushkey` should definitely be canonicalised for emails:

https://github.com/matrix-org/synapse/blob/8c7a531e277f98ac6b7981b9738649f3a70feb94/synapse/push/pusherpool.py#L115-L118

using `canonicalise_email`:

https://github.com/matrix-org/synapse/blob/8c7a531e277f98ac6b7981b9738649f3a70feb94/synapse/util/threepids.py#L66-L69


Discussion:

Resolving patch:
diff --git a/synapse/push/pusherpool.py b/synapse/push/pusherpool.py
--- a/synapse/push/pusherpool.py
+++ b/synapse/push/pusherpool.py
@@ -27,6 +27,7 @@
 from synapse.replication.http.push import ReplicationRemovePusherRestServlet
 from synapse.types import JsonDict, RoomStreamToken
 from synapse.util.async_helpers import concurrently_execute
+from synapse.util.threepids import canonicalise_email
 
 if TYPE_CHECKING:
     from synapse.server import HomeServer
@@ -113,7 +114,9 @@ async def add_pusher(
         """
 
         if kind == "email":
-            email_owner = await self.store.get_user_id_by_threepid("email", pushkey)
+            email_owner = await self.store.get_user_id_by_threepid(
+                "email", canonicalise_email(pushkey)
+            )
             if email_owner != user_id:
                 raise SynapseError(400, "Email not found", Codes.THREEPID_NOT_FOUND)
 
diff --git a/synapse/storage/databases/main/monthly_active_users.py b/synapse/storage/databases/main/monthly_active_users.py
--- a/synapse/storage/databases/main/monthly_active_users.py
+++ b/synapse/storage/databases/main/monthly_active_users.py
@@ -18,6 +18,7 @@
 from synapse.storage._base import SQLBaseStore
 from synapse.storage.database import DatabasePool, make_in_list_sql_clause
 from synapse.util.caches.descriptors import cached
+from synapse.util.threepids import canonicalise_email
 
 if TYPE_CHECKING:
     from synapse.server import HomeServer
@@ -103,7 +104,7 @@ async def get_registered_reserved_users(self) -> List[str]:
             : self.hs.config.server.max_mau_value
         ]:
             user_id = await self.hs.get_datastore().get_user_id_by_threepid(
-                tp["medium"], tp["address"]
+                tp["medium"], canonicalise_email(tp["address"])
             )
             if user_id:
                 users.append(user_id)
diff --git a/synapse/storage/databases/main/registration.py b/synapse/storage/databases/main/registration.py
--- a/synapse/storage/databases/main/registration.py
+++ b/synapse/storage/databases/main/registration.py
@@ -856,7 +856,8 @@ async def get_user_id_by_threepid(self, medium: str, address: str) -> Optional[s
 
         Args:
             medium: threepid medium e.g. email
-            address: threepid address e.g. me@example.com
+            address: threepid address e.g. me@example.com. This must already be
+                in canonical form.
 
         Returns:
             The user ID or None if no user id/threepid mapping exists


Test patch:
diff --git a/tests/rest/admin/test_user.py b/tests/rest/admin/test_user.py
--- a/tests/rest/admin/test_user.py
+++ b/tests/rest/admin/test_user.py
@@ -1550,7 +1550,8 @@ def test_create_user_email_notif_for_new_users(self):
         # Create user
         body = {
             "password": "abc123",
-            "threepids": [{"medium": "email", "address": "bob@bob.bob"}],
+            # Note that the given email is not in canonical form.
+            "threepids": [{"medium": "email", "address": "Bob@bob.bob"}],
         }
 
         channel = self.make_request(