Repository: zulip/zulip

Issue:
Send one find_account notification per email address, not per organization
When using the "find my account" feature, we send one email per organization that matches the email address:
https://github.com/zulip/zulip/blob/191b1ac2be46fe4219c170ab318aa10c714cb49a/zerver/views/registration.py#L746-L759

This can lead to a deluge of emails. We should adjust the template to contain all of the organizations, listed in a single email.


Discussion:
Hello @zulip/server-development members, this issue was labeled with the "area: emails" label, so you may want to check it out!

<!-- areaLabelAddition -->

The main work here is figuring out what we want the wording/structure of the email to be in the event that there are multiple organizations.  @alya FYI for a proposal on that.
Maybe something like:

-----
Hi Iago,

Your email address iago@zulip.com is associated with the following Zulip accounts:

- Zulip Dev: Log in at http://localhost:9991.
- …

If you have trouble logging in, please contact Zulip support by replying to this email.
Thanks for using Zulip!

---
Thoughts?

I suppose it's possible that the user had different names for different accounts, right? I think it's nice to use "Hi X" rather than a generic "Hi", so we could use some kind of heuristic (e.g. most commonly used name > name in most recently logged in org, or something). What do yo guys think?

@alya I feel like it is fairly possible that the user had different usernames for different accounts. Although implementing these heuristics may get a little complicated, so a generic "hi" seems a lot easier. But if we were to go with a heuristic, the most commonly used name makes the most sense to me.
How about this, to avoid the "hi" (which I find to be odd without a name):

------
Thanks for your request! Your email address iago@zulip.com is associated with the following Zulip accounts:

Zulip Dev: Log in at http://localhost:9991.
…

If you have trouble logging in, please contact Zulip support by replying to this email.



@alya Ooh I like that better! I think I'll go with that! :)

Resolving patch:
diff --git a/version.py b/version.py
--- a/version.py
+++ b/version.py
@@ -48,4 +48,4 @@
 #   historical commits sharing the same major version, in which case a
 #   minor version bump suffices.
 
-PROVISION_VERSION = "161.1"
+PROVISION_VERSION = "162.0"
diff --git a/zerver/views/registration.py b/zerver/views/registration.py
--- a/zerver/views/registration.py
+++ b/zerver/views/registration.py
@@ -1,6 +1,6 @@
 import logging
 import urllib
-from typing import Dict, List, Optional
+from typing import Any, Dict, List, Optional
 from urllib.parse import urlencode
 
 from django.conf import settings
@@ -716,8 +716,6 @@ def accounts_home_from_multiuse_invite(request: HttpRequest, confirmation_key: s
 def find_account(
     request: HttpRequest, raw_emails: Optional[str] = REQ("emails", default=None)
 ) -> HttpResponse:
-    from zerver.context_processors import common_context
-
     url = reverse("find_account")
 
     emails: List[str] = []
@@ -743,17 +741,32 @@ def find_account(
             for email in emails:
                 emails_q |= Q(delivery_email__iexact=email)
 
-            for user in UserProfile.objects.filter(
+            user_profiles = UserProfile.objects.filter(
                 emails_q, is_active=True, is_bot=False, realm__deactivated=False
-            ):
-                context = common_context(user)
-                context.update(
-                    email=user.delivery_email,
-                )
+            )
+
+            # We organize the data in preparation for sending exactly
+            # one outgoing email per provided email address, with each
+            # email listing all of the accounts that email address has
+            # with the current Zulip server.
+            context: Dict[str, Dict[str, Any]] = {}
+            for user in user_profiles:
+                key = user.delivery_email.lower()
+                context.setdefault(key, {})
+                context[key].setdefault("realms", [])
+                context[key]["realms"].append(user.realm)
+                context[key]["external_host"] = settings.EXTERNAL_HOST
+                # This value will end up being the last user ID among
+                # matching accounts; since it's only used for minor
+                # details like language, that arbitrary choice is OK.
+                context[key]["to_user_id"] = user.id
+
+            for delivery_email, realm_context in context.items():
+                realm_context["email"] = delivery_email
                 send_email(
                     "zerver/emails/find_team",
-                    to_user_ids=[user.id],
-                    context=context,
+                    to_user_ids=[realm_context["to_user_id"]],
+                    context=realm_context,
                     from_address=FromAddress.SUPPORT,
                     request=request,
                 )


Test patch:
diff --git a/zerver/tests/test_signup.py b/zerver/tests/test_signup.py
--- a/zerver/tests/test_signup.py
+++ b/zerver/tests/test_signup.py
@@ -5440,8 +5440,13 @@ def test_result(self) -> None:
         self.assertIn("cordeliA@zulip.com", content)
         from django.core.mail import outbox
 
-        # 3 = 1 + 2 -- Cordelia gets an email each for the "zulip" and "lear" realms.
-        self.assert_length(outbox, 3)
+        self.assert_length(outbox, 2)
+        iago_message = outbox[1]
+        cordelia_message = outbox[0]
+        self.assertIn("Zulip Dev", iago_message.body)
+        self.assertNotIn("Lear & Co", iago_message.body)
+        self.assertIn("Zulip Dev", cordelia_message.body)
+        self.assertIn("Lear & Co", cordelia_message.body)
 
     def test_find_team_ignore_invalid_email(self) -> None:
         result = self.client_post(