diff --git a/frappe/auth.py b/frappe/auth.py index 4267c60f73..347f6b4e3a 100644 --- a/frappe/auth.py +++ b/frappe/auth.py @@ -155,7 +155,9 @@ class LoginManager: self.authenticate(user=user, pwd=pwd) if self.force_user_to_reset_password(): doc = frappe.get_doc("User", self.user) - frappe.local.response["redirect_to"] = doc.reset_password(send_email=False, password_expired=True) + frappe.local.response["redirect_to"] = doc._reset_password( + send_email=False, password_expired=True + ) frappe.local.response["message"] = "Password Reset" return False diff --git a/frappe/core/api/user_invitation.py b/frappe/core/api/user_invitation.py index 5390caa154..1e685e6c28 100644 --- a/frappe/core/api/user_invitation.py +++ b/frappe/core/api/user_invitation.py @@ -143,7 +143,7 @@ def _accept_invitation(key: str, in_test: bool) -> None: # set redirect_to redirect_to = frappe.utils.get_url(invitation.get_redirect_to_path()) if should_update_password: - redirect_to = f"{user.reset_password()}&redirect_to=/{invitation.get_redirect_to_path()}" + redirect_to = f"{user._reset_password()}&redirect_to=/{invitation.get_redirect_to_path()}" # GET requests do not cause an implicit commit frappe.db.commit() # nosemgrep diff --git a/frappe/core/doctype/user/test_user.py b/frappe/core/doctype/user/test_user.py index aa7c182b00..62cba4c54c 100644 --- a/frappe/core/doctype/user/test_user.py +++ b/frappe/core/doctype/user/test_user.py @@ -42,7 +42,7 @@ class TestUser(IntegrationTestCase): @staticmethod def reset_password(user) -> str: - link = user.reset_password() + link = user._reset_password() return parse_qs(urlparse(link).query)["key"][0] def test_user_type(self): @@ -415,6 +415,12 @@ class TestUser(IntegrationTestCase): # test API endpoint with patch.object(user_module.frappe, "sendmail") as sendmail: + from unittest.mock import MagicMock + + mock_q = MagicMock() + mock_q.name = "test-email-queue-name" + mock_q.message = "Subject: Test\n\nDear User, here is your link" + sendmail.return_value = mock_q frappe.clear_messages() test_user = frappe.get_doc("User", "test2@example.com") self.assertEqual(reset_password(user="test2@example.com"), None) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 5f4b1d3fae..84c9580e30 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1,6 +1,7 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: MIT. See LICENSE +import re from collections.abc import Iterable from datetime import timedelta from functools import cached_property @@ -376,9 +377,23 @@ class User(Document): toggle_notifications(self.name, enable=cint(self.enabled), ignore_permissions=True) self.disable_email_fields_if_user_disabled() - def email_new_password(self, new_password=None): + def set_new_password(self, new_password=None): + """Set New Password for user""" if new_password and not self.flags.in_insert: _update_password(user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions) + outgoing_email_exists = frappe.db.exists( + "Email Account", {"default_outgoing": 1, "awaiting_password": 0} + ) + if outgoing_email_exists: + email_message = _( + "Your password has been changed and you might have been logged out of all systems.
Please contact the Administrator for further assistance." + ) + user_email = frappe.db.get_value("User", self.name, "email") + frappe.sendmail( + recipients=[user_email], + subject=_("Security Alert: Your password has been changed."), + content=email_message, + ) def set_system_user(self): """For the standard users like admin and guest, the user type is fixed.""" @@ -433,25 +448,26 @@ class User(Document): def send_password_notification(self, new_password): try: if self.flags.in_insert: - if self.name not in STANDARD_USERS: - if new_password: - # new password given, no email required - _update_password( - user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions - ) + if self.name in STANDARD_USERS: + return + if new_password: + # new password given, no email required + _update_password( + user=self.name, pwd=new_password, logout_all_sessions=self.logout_all_sessions + ) - if ( - not self.flags.no_welcome_mail - and cint(self.send_welcome_email) - and not self.flags.email_sent - ): - self.send_welcome_mail_to_user() - self.flags.email_sent = 1 - if frappe.session.user != "Guest": - msgprint(_("Welcome email sent")) - return + if ( + not self.flags.no_welcome_mail + and cint(self.send_welcome_email) + and not self.flags.email_sent + ): + self.send_welcome_mail_to_user() + self.flags.email_sent = 1 + if frappe.session.user != "Guest": + msgprint(_("Welcome email sent")) + return else: - self.email_new_password(new_password) + self.set_new_password(new_password) except frappe.OutgoingEmailError: frappe.clear_last_message() @@ -465,7 +481,7 @@ class User(Document): def validate_reset_password(self): pass - def reset_password(self, send_email=False, password_expired=False): + def _reset_password(self, send_email=False, password_expired=False): from frappe.utils import get_url key = frappe.generate_hash() @@ -490,18 +506,24 @@ class User(Document): def password_reset_mail(self, link): reset_password_template = frappe.db.get_system_setting("reset_password_template") - self.send_login_mail( + q = self.send_login_mail( _("Password Reset"), "password_reset", {"link": link}, now=True, custom_template=reset_password_template, ) + if q: + raw_message = q.message + parts = re.split(r"(?i)Dear", raw_message, maxsplit=1) + if len(parts) > 1: + redacted_message = parts[0] + "[THE FOLLOWING CONTENT HAS BEEN REDACTED FOR SECURITY REASONS]" + frappe.db.set_value("Email Queue", q.name, "message", redacted_message, update_modified=False) def send_welcome_mail_to_user(self): from frappe.utils import get_url - link = self.reset_password() + link = self._reset_password() subject = None method = frappe.get_hooks("welcome_email") if method: @@ -515,7 +537,7 @@ class User(Document): welcome_email_template = frappe.db.get_system_setting("welcome_email_template") - self.send_login_mail( + q = self.send_login_mail( subject, "new_user", dict( @@ -524,6 +546,12 @@ class User(Document): ), custom_template=welcome_email_template, ) + if q: + raw_message = q.message + parts = re.split(r"(?i)Hello", raw_message, maxsplit=1) + if len(parts) > 1: + redacted_message = parts[0] + "[THE FOLLOWING CONTENT HAS BEEN REDACTED FOR SECURITY REASONS]" + frappe.db.set_value("Email Queue", q.name, "message", redacted_message, update_modified=False) def send_login_mail(self, subject, template, add_args, now=None, custom_template=None): """send mail with login details""" @@ -555,7 +583,7 @@ class User(Document): subject = email_template.get("subject") content = email_template.get("message") - frappe.sendmail( + return frappe.sendmail( recipients=self.email, sender=sender, subject=subject, @@ -1135,7 +1163,7 @@ def reset_password(user: str) -> str: return "disabled" user.validate_reset_password() - user.reset_password(send_email=True) + user._reset_password(send_email=True) return frappe.msgprint( msg=_("Password reset instructions have been sent to {}'s email").format(user.full_name),