Merge pull request #37760 from ShrihariMahabal/undo-email-send
feat: Allow undo email send for 10 seconds
This commit is contained in:
commit
a655bbdfa6
6 changed files with 203 additions and 9 deletions
|
|
@ -18,7 +18,10 @@ from frappe.utils import (
|
|||
get_imaginary_pixel_response,
|
||||
get_string_between,
|
||||
list_to_str,
|
||||
now_datetime,
|
||||
parse_addr,
|
||||
split_emails,
|
||||
time_diff_in_seconds,
|
||||
validate_email_address,
|
||||
)
|
||||
|
||||
|
|
@ -328,3 +331,63 @@ def update_communication_as_read(name):
|
|||
name,
|
||||
{"read_by_recipient": 1, "delivery_status": "Read", "read_by_recipient_on": get_datetime()},
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def undo_email_send(communication_name: str):
|
||||
communication = frappe.get_doc("Communication", communication_name)
|
||||
|
||||
if communication.owner != frappe.session.user:
|
||||
frappe.throw(_("You are not authorized to undo this email"))
|
||||
|
||||
if communication.sent_or_received != "Sent" or communication.communication_medium != "Email":
|
||||
frappe.throw(_("Failed to delete communication"))
|
||||
|
||||
time_elapsed_in_seconds = time_diff_in_seconds(now_datetime(), communication.creation)
|
||||
if time_elapsed_in_seconds > 10:
|
||||
frappe.msgprint(
|
||||
_("Email undo window is over. Cannot undo email."), alert=True, indicator="red", raise_exception=1
|
||||
)
|
||||
|
||||
email_queue_records = frappe.get_all(
|
||||
"Email Queue", filters={"communication": communication_name}, fields=["name", "status"]
|
||||
)
|
||||
|
||||
for queue in email_queue_records:
|
||||
if queue.status != "Not Sent":
|
||||
frappe.msgprint(
|
||||
_("It is too late to undo this email. It is already being sent."),
|
||||
alert=True,
|
||||
indicator="red",
|
||||
raise_exception=1,
|
||||
)
|
||||
|
||||
for queue in email_queue_records:
|
||||
frappe.delete_doc("Email Queue", queue.name, ignore_permissions=True)
|
||||
|
||||
communication_data = {
|
||||
"subject": communication.subject,
|
||||
"content": communication.content,
|
||||
"recipients": communication.recipients,
|
||||
"cc": communication.cc,
|
||||
"bcc": communication.bcc,
|
||||
"doc": {"doctype": communication.reference_doctype, "name": communication.reference_name},
|
||||
"sender": communication.sender,
|
||||
"send_read_receipt": communication.read_receipt,
|
||||
}
|
||||
|
||||
linked_files = frappe.get_all(
|
||||
"File",
|
||||
filters={"attached_to_doctype": "Communication", "attached_to_name": communication_name},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if linked_files:
|
||||
for file_name in linked_files:
|
||||
frappe.db.set_value("File", file_name, {"attached_to_doctype": None, "attached_to_name": None})
|
||||
|
||||
communication_data["attachments"] = linked_files
|
||||
|
||||
communication.delete(ignore_permissions=True)
|
||||
|
||||
return communication_data
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: MIT. See LICENSE
|
||||
from datetime import timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import frappe
|
||||
from frappe.core.doctype.communication.communication import Communication, get_emails, parse_email
|
||||
from frappe.core.doctype.communication.email import add_attachments, make
|
||||
from frappe.core.doctype.communication.email import add_attachments, make, undo_email_send
|
||||
from frappe.email.doctype.email_queue.email_queue import EmailQueue
|
||||
from frappe.tests import IntegrationTestCase
|
||||
from frappe.utils import add_to_date, now_datetime
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from frappe.contacts.doctype.contact.contact import Contact
|
||||
|
|
@ -438,6 +440,79 @@ class TestCommunicationEmailMixin(IntegrationTestCase):
|
|||
self.assertEqual(attached_file.file_name, file_name)
|
||||
self.assertEqual(attached_file.get_content(), file_content)
|
||||
|
||||
def test_undo_email_send(self):
|
||||
"""Undo should delete Communication and Email Queue, and return original data."""
|
||||
comm = self.new_communication(recipients=["to@test.com"])
|
||||
comm.sent_or_received = "Sent"
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
eq = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Email Queue",
|
||||
"sender": "Test <test@example.com>",
|
||||
"message": "Test message",
|
||||
"status": "Not Sent",
|
||||
"priority": 1,
|
||||
"communication": comm.name,
|
||||
"recipients": [{"recipient": "to@test.com", "status": "Not Sent"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
result = undo_email_send(comm.name)
|
||||
|
||||
self.assertFalse(frappe.db.exists("Communication", comm.name))
|
||||
self.assertFalse(frappe.db.exists("Email Queue", eq.name))
|
||||
self.assertFalse(frappe.db.exists("Email Queue Recipient", {"parent": eq.name}))
|
||||
self.assertEqual(result["subject"], comm.subject)
|
||||
self.assertEqual(result["recipients"], comm.recipients)
|
||||
|
||||
def test_undo_email_send_fails_for_different_user(self):
|
||||
"""Undo should fail if the current user is not the owner."""
|
||||
comm = self.new_communication(recipients=["to@test.com"])
|
||||
comm.sent_or_received = "Sent"
|
||||
comm.save(ignore_permissions=True)
|
||||
frappe.db.set_value("Communication", comm.name, "owner", "other@test.com")
|
||||
|
||||
with self.assertRaises(frappe.exceptions.ValidationError):
|
||||
undo_email_send(comm.name)
|
||||
|
||||
self.assertTrue(frappe.db.exists("Communication", comm.name))
|
||||
|
||||
def test_undo_email_send_fails_after_time_window(self):
|
||||
"""Undo should fail if the 10-second window has passed."""
|
||||
comm = self.new_communication(recipients=["to@test.com"])
|
||||
comm.sent_or_received = "Sent"
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
with self.freeze_time(add_to_date(now_datetime(), seconds=12)):
|
||||
with self.assertRaises(frappe.exceptions.ValidationError):
|
||||
undo_email_send(comm.name)
|
||||
|
||||
self.assertTrue(frappe.db.exists("Communication", comm.name))
|
||||
|
||||
def test_undo_email_send_fails_if_already_sent(self):
|
||||
"""Undo should fail if Email Queue status is not 'Not Sent'."""
|
||||
comm = self.new_communication(recipients=["to@test.com"])
|
||||
comm.sent_or_received = "Sent"
|
||||
comm.save(ignore_permissions=True)
|
||||
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Email Queue",
|
||||
"sender": "Test <test@example.com>",
|
||||
"message": "Test message",
|
||||
"status": "Sent",
|
||||
"priority": 1,
|
||||
"communication": comm.name,
|
||||
"recipients": [{"recipient": "to@test.com", "status": "Sent"}],
|
||||
}
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
with self.assertRaises(frappe.exceptions.ValidationError):
|
||||
undo_email_send(comm.name)
|
||||
|
||||
self.assertTrue(frappe.db.exists("Communication", comm.name))
|
||||
|
||||
|
||||
def create_email_account() -> "EmailAccount":
|
||||
frappe.delete_doc_if_exists("Email Account", "_Test Comm Account 1")
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from datetime import timedelta
|
|||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.utils import cint, cstr, get_url, now_datetime
|
||||
from frappe.utils.data import getdate
|
||||
from frappe.utils.data import add_to_date, getdate
|
||||
from frappe.utils.verified_command import get_signed_params, verify_request
|
||||
|
||||
# After this percent of failures in every batch, entire batch is aborted.
|
||||
|
|
@ -163,19 +163,21 @@ def flush():
|
|||
|
||||
def get_queue():
|
||||
batch_size = cint(frappe.conf.email_queue_batch_size) or 500
|
||||
undo_window = add_to_date(now_datetime(), seconds=-10)
|
||||
|
||||
return frappe.db.sql(
|
||||
f"""select
|
||||
"""select
|
||||
name, sender
|
||||
from
|
||||
`tabEmail Queue`
|
||||
where
|
||||
(status='Not Sent' or status='Partially Sent') and
|
||||
(send_after is null or send_after < %(now)s)
|
||||
(send_after is null or send_after < %(now)s) and
|
||||
(creation < %(undo_window)s)
|
||||
order
|
||||
by priority desc, retry asc, creation asc
|
||||
limit {batch_size}""",
|
||||
{"now": now_datetime()},
|
||||
limit %(batch_size)s""",
|
||||
{"now": now_datetime(), "undo_window": undo_window, "batch_size": batch_size},
|
||||
as_dict=True,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -896,6 +896,8 @@ frappe.views.CommunicationComposer = class {
|
|||
if (!r.exc) {
|
||||
frappe.utils.play_sound("email");
|
||||
|
||||
const communication_name = r.message["name"];
|
||||
|
||||
if (r.message["emails_not_sent_to"]) {
|
||||
frappe.msgprint(
|
||||
__("Email not sent to {0} (unsubscribed / disabled)", [
|
||||
|
|
@ -910,6 +912,54 @@ frappe.views.CommunicationComposer = class {
|
|||
me.frm.reload_doc();
|
||||
}
|
||||
|
||||
let undo_alert = frappe.show_alert(
|
||||
{
|
||||
message: `<span>${__(
|
||||
"Email Sent"
|
||||
)}</span><span class="cursor-pointer ml-4" data-action="undo" style="font-weight: 500; text-decoration: underline;">${__(
|
||||
"Undo"
|
||||
)}</span>`,
|
||||
indicator: "green",
|
||||
},
|
||||
10,
|
||||
{
|
||||
undo: () => {
|
||||
if (undo_alert) {
|
||||
undo_alert.find(".close").click();
|
||||
}
|
||||
frappe
|
||||
.xcall(
|
||||
"frappe.core.doctype.communication.email.undo_email_send",
|
||||
{ communication_name: communication_name }
|
||||
)
|
||||
.then((d) => {
|
||||
if (me.frm) {
|
||||
me.frm.reload_doc();
|
||||
}
|
||||
|
||||
// Reopen the composer with the recovered data
|
||||
new frappe.views.CommunicationComposer({
|
||||
doc: d.doc,
|
||||
subject: d.subject,
|
||||
recipients: d.recipients,
|
||||
cc: d.cc,
|
||||
bcc: d.bcc,
|
||||
message: d.content,
|
||||
sender: d.sender,
|
||||
read_receipt: d.send_read_receipt,
|
||||
attachments: d.attachments,
|
||||
frm: me.frm,
|
||||
});
|
||||
|
||||
frappe.show_alert({
|
||||
message: __("Email sending undone"),
|
||||
indicator: "blue",
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// try the success callback if it exists
|
||||
if (me.success) {
|
||||
try {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
position: fixed;
|
||||
bottom: 0px;
|
||||
right: 20px;
|
||||
z-index: 1050;
|
||||
z-index: 2000;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
right: 0;
|
||||
|
|
|
|||
|
|
@ -57,16 +57,20 @@ class TestEmail(IntegrationTestCase):
|
|||
def test_send_after(self):
|
||||
self.test_email_queue(send_after=1)
|
||||
from frappe.email.queue import flush
|
||||
from frappe.utils import add_to_date, now_datetime
|
||||
|
||||
flush()
|
||||
with self.freeze_time(add_to_date(now_datetime(), seconds=12)):
|
||||
flush()
|
||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Sent'""", as_dict=1)
|
||||
self.assertEqual(len(email_queue), 0)
|
||||
|
||||
def test_flush(self):
|
||||
self.test_email_queue()
|
||||
from frappe.email.queue import flush
|
||||
from frappe.utils import add_to_date, now_datetime
|
||||
|
||||
flush()
|
||||
with self.freeze_time(add_to_date(now_datetime(), seconds=12)):
|
||||
flush()
|
||||
email_queue = frappe.db.sql("""select name from `tabEmail Queue` where status='Sent'""", as_dict=1)
|
||||
self.assertEqual(len(email_queue), 1)
|
||||
queue_recipients = [
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue