From 8976ba74c8f05495fc9f1c8fc5a704dd33750d20 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Thu, 22 Jan 2026 20:33:01 +0530 Subject: [PATCH 1/3] feat(user): send user a mail when an impersonation session has begun --- frappe/core/doctype/user/user.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 5665ec1c84..850fde97fc 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1462,6 +1462,19 @@ def impersonate(user: str, reason: str): ) notification.set("type", "Alert") notification.insert(ignore_permissions=True) + # notify user via email too + user_email = frappe.db.get_value("User", user, "email") + email_message = _( + "User {0} has started an impersonation session as you.

Reason provided: {1}" + ).format(escape_html(impersonator), escape_html(reason)) + + frappe.enqueue( + method="frappe.sendmail", + queue="short", + recipients=[user_email], + subject=_("Security Alert: Your account is being impersonated"), + content=email_message, + ) frappe.local.login_manager.impersonate(user) From b01f0bd3032f5282067c740e83f272bcb64de44b Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Fri, 23 Jan 2026 10:31:36 +0530 Subject: [PATCH 2/3] test: add coverage for impersonation email --- frappe/tests/test_email.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/frappe/tests/test_email.py b/frappe/tests/test_email.py index 6d7a7b2c16..f47d4af099 100644 --- a/frappe/tests/test_email.py +++ b/frappe/tests/test_email.py @@ -308,6 +308,28 @@ class TestEmail(IntegrationTestCase): if changed_flag: email_account.enable_incoming = False + def test_impersonation_alert_queue(self): + """Verifies that impersonation alerts are sent as mail too""" + from frappe.core.doctype.user.user import impersonate + + target_user = "testimpersonate@example.com" + frappe.db.delete("Email Queue Recipient", {"recipient": target_user}) # sanity + if not frappe.db.exists("User", target_user): + frappe.get_doc({"doctype": "User", "email": target_user, "first_name": "Target"}).insert( + ignore_permissions=True + ) + with patch("frappe.enqueue") as mocked_enqueue: + reason = "Testing Security Alert" + impersonate(user=target_user, reason=reason) + + self.assertEqual(frappe.session.user, target_user) # test if impersonation worked + _, kwargs = mocked_enqueue.call_args + self.assertEqual(kwargs.get("method"), "frappe.sendmail") # test if email was enqueued + self.assertIn(target_user, kwargs.get("recipients")) + self.assertIn(reason, kwargs.get("content")) + + frappe.db.delete("User", {"email": target_user}) + class TestVerifiedRequests(IntegrationTestCase): def test_round_trip(self): From 4b86e92fbcc2660746f54eb495cd242470b37d22 Mon Sep 17 00:00:00 2001 From: AarDG10 Date: Wed, 4 Feb 2026 16:42:59 +0530 Subject: [PATCH 3/3] fix(user): use sendmail instead of enqueuing --- frappe/core/doctype/user/user.py | 4 +--- frappe/tests/test_email.py | 17 ++++++++--------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/frappe/core/doctype/user/user.py b/frappe/core/doctype/user/user.py index 850fde97fc..37abbcca91 100644 --- a/frappe/core/doctype/user/user.py +++ b/frappe/core/doctype/user/user.py @@ -1468,9 +1468,7 @@ def impersonate(user: str, reason: str): "User {0} has started an impersonation session as you.

Reason provided: {1}" ).format(escape_html(impersonator), escape_html(reason)) - frappe.enqueue( - method="frappe.sendmail", - queue="short", + frappe.sendmail( recipients=[user_email], subject=_("Security Alert: Your account is being impersonated"), content=email_message, diff --git a/frappe/tests/test_email.py b/frappe/tests/test_email.py index f47d4af099..788fe1e476 100644 --- a/frappe/tests/test_email.py +++ b/frappe/tests/test_email.py @@ -318,15 +318,14 @@ class TestEmail(IntegrationTestCase): frappe.get_doc({"doctype": "User", "email": target_user, "first_name": "Target"}).insert( ignore_permissions=True ) - with patch("frappe.enqueue") as mocked_enqueue: - reason = "Testing Security Alert" - impersonate(user=target_user, reason=reason) - - self.assertEqual(frappe.session.user, target_user) # test if impersonation worked - _, kwargs = mocked_enqueue.call_args - self.assertEqual(kwargs.get("method"), "frappe.sendmail") # test if email was enqueued - self.assertIn(target_user, kwargs.get("recipients")) - self.assertIn(reason, kwargs.get("content")) + reason = "Testing Security Alert" + impersonate(user=target_user, reason=reason) + self.assertEqual(frappe.session.user, target_user) # test if impersonation worked + self.assertTrue(frappe.db.exists("Activity Log", {"user": target_user, "operation": "Impersonate"})) + email_queued = frappe.db.exists( + "Email Queue Recipient", {"recipient": target_user, "status": "Not Sent"} + ) + self.assertTrue(email_queued, f"Impersonation email was not queued for {target_user}") frappe.db.delete("User", {"email": target_user})