Repository: zulip/zulip

Issue:
Soft-reactivate users on password reset emails or failed logins
b4feb673f1da503583899a7ee1a41952240c70d2 and a8fd9eb7010d572be8129fe244729865a730a87d started soft-reactivating users when we sent them PMs, which is great for the user experience.

We should expand this to soft-reactivate users on password reset requests and "find my accounts" emails, which are other good signals that the user is about to log back in.

We could potentially consider a stronger form of this, where we soft-reactivate on _failed_ password auth attempts, which generally come before the password reset request.  I'm a little worried that would be overzealous, though.


Discussion:
cc @PIG208, if you're so inclined
Hello @zulip/server-authentication members, this issue was labeled with the "area: authentication" label, so you may want to check it out!

<!-- areaLabelAddition -->

@zulipbot claim

Resolving patch:
diff --git a/zerver/forms.py b/zerver/forms.py
--- a/zerver/forms.py
+++ b/zerver/forms.py
@@ -29,6 +29,7 @@
 from zerver.lib.name_restrictions import is_disposable_domain, is_reserved_subdomain
 from zerver.lib.rate_limiter import RateLimitedObject
 from zerver.lib.send_email import FromAddress, send_email
+from zerver.lib.soft_deactivation import queue_soft_reactivation
 from zerver.lib.subdomains import get_subdomain, is_root_domain_available
 from zerver.lib.users import check_full_name
 from zerver.models import (
@@ -360,6 +361,7 @@ def save(
         if user is not None:
             context["active_account_in_realm"] = True
             context["reset_url"] = generate_password_reset_url(user, token_generator)
+            queue_soft_reactivation(user.id)
             send_email(
                 "zerver/emails/password_reset",
                 to_user_ids=[user.id],
diff --git a/zerver/lib/soft_deactivation.py b/zerver/lib/soft_deactivation.py
--- a/zerver/lib/soft_deactivation.py
+++ b/zerver/lib/soft_deactivation.py
@@ -377,6 +377,14 @@ def get_soft_deactivated_users_for_catch_up(filter_kwargs: Any) -> List[UserProf
     return users_to_catch_up
 
 
+def queue_soft_reactivation(user_profile_id: int) -> None:
+    event = {
+        "type": "soft_reactivate",
+        "user_profile_id": user_profile_id,
+    }
+    queue_json_publish("deferred_work", event)
+
+
 def soft_reactivate_if_personal_notification(
     user_profile: UserProfile, unique_triggers: Set[str], mentioned_user_group_name: Optional[str]
 ) -> None:
@@ -400,8 +408,4 @@ def soft_reactivate_if_personal_notification(
     if not private_message and not personal_mention:
         return
 
-    event = {
-        "type": "soft_reactivate",
-        "user_profile_id": user_profile.id,
-    }
-    queue_json_publish("deferred_work", event)
+    queue_soft_reactivation(user_profile.id)


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
@@ -731,6 +731,17 @@ def test_redirect_endpoints(self) -> None:
         result = self.client_get("/accounts/new/send_confirm/alice@example.com")
         self.assert_in_success_response(["/new/"], result)
 
+    def test_password_reset_for_soft_deactivated_user(self) -> None:
+        user_profile = self.example_user("hamlet")
+        email = user_profile.delivery_email
+        with self.soft_deactivate_and_check_long_term_idle(user_profile, False):
+            # start the password reset process by supplying an email address
+            result = self.client_post("/accounts/password/reset/", {"email": email})
+
+            # check the redirect link telling you to check mail for password reset link
+            self.assertEqual(result.status_code, 302)
+            self.assertTrue(result["Location"].endswith("/accounts/password/reset/done/"))
+
 
 class LoginTest(ZulipTestCase):
     """