fix: email campaign timeout issue (backport #51994) (#52555)

fix: email campaign timeout issue (#51994)

* fix: email campaign timeout issue

* refactor: email campaign backend logic

* refactor: use sendmail instead of manually batching

(cherry picked from commit 22123dd955)

Co-authored-by: Pratik Badhe <badhepd@gmail.com>
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
This commit is contained in:
mergify[bot]
2026-02-10 02:10:05 +05:30
committed by GitHub
parent 9519773c5c
commit 6c9681ba4c

View File

@@ -38,18 +38,18 @@ class EmailCampaign(Document):
def set_date(self):
if getdate(self.start_date) < getdate(today()):
frappe.throw(_("Start Date cannot be before the current date"))
# set the end date as start date + max(send after days) in campaign schedule
send_after_days = []
campaign = frappe.get_doc("Campaign", self.campaign_name)
for entry in campaign.get("campaign_schedules"):
send_after_days.append(entry.send_after_days)
try:
self.end_date = add_days(getdate(self.start_date), max(send_after_days))
except ValueError:
campaign = frappe.get_cached_doc("Campaign", self.campaign_name)
send_after_days = [entry.send_after_days for entry in campaign.get("campaign_schedules")]
if not send_after_days:
frappe.throw(
_("Please set up the Campaign Schedule in the Campaign {0}").format(self.campaign_name)
)
self.end_date = add_days(getdate(self.start_date), max(send_after_days))
def validate_lead(self):
lead_email_id = frappe.db.get_value("Lead", self.recipient, "email_id")
if not lead_email_id:
@@ -77,58 +77,128 @@ class EmailCampaign(Document):
start_date = getdate(self.start_date)
end_date = getdate(self.end_date)
today_date = getdate(today())
if start_date > today_date:
self.db_set("status", "Scheduled", update_modified=False)
new_status = "Scheduled"
elif end_date >= today_date:
self.db_set("status", "In Progress", update_modified=False)
elif end_date < today_date:
self.db_set("status", "Completed", update_modified=False)
new_status = "In Progress"
else:
new_status = "Completed"
if self.status != new_status:
self.db_set("status", new_status, update_modified=False)
# called through hooks to send campaign mails to leads
def send_email_to_leads_or_contacts():
today_date = getdate(today())
# Get all active email campaigns in a single query
email_campaigns = frappe.get_all(
"Email Campaign", filters={"status": ("not in", ["Unsubscribed", "Completed", "Scheduled"])}
"Email Campaign",
filters={"status": "In Progress"},
fields=["name", "campaign_name", "email_campaign_for", "recipient", "start_date", "sender"],
)
for camp in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", camp.name)
campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name)
if not email_campaigns:
return
# Process each email campaign
for email_campaign in email_campaigns:
try:
campaign = frappe.get_cached_doc("Campaign", email_campaign.campaign_name)
except frappe.DoesNotExistError:
frappe.log_error(
title=_("Email Campaign Error"),
message=_("Campaign {0} not found").format(email_campaign.campaign_name),
)
continue
# Find schedules that match today
for entry in campaign.get("campaign_schedules"):
scheduled_date = add_days(email_campaign.get("start_date"), entry.get("send_after_days"))
if scheduled_date == getdate(today()):
send_mail(entry, email_campaign)
try:
scheduled_date = add_days(getdate(email_campaign.start_date), entry.get("send_after_days"))
if scheduled_date == today_date:
send_mail(entry, email_campaign)
except Exception:
frappe.log_error(
title=_("Email Campaign Send Error"),
message=_("Failed to send email for campaign {0} to {1}").format(
email_campaign.name, email_campaign.recipient
),
)
def send_mail(entry, email_campaign):
recipient_list = []
if email_campaign.email_campaign_for == "Email Group":
for member in frappe.db.get_list(
"Email Group Member", filters={"email_group": email_campaign.get("recipient")}, fields=["email"]
):
recipient_list.append(member["email"])
campaign_for = email_campaign.get("email_campaign_for")
recipient = email_campaign.get("recipient")
sender_user = email_campaign.get("sender")
campaign_name = email_campaign.get("name")
# Get recipient emails
if campaign_for == "Email Group":
recipient_list = frappe.get_all(
"Email Group Member",
filters={"email_group": recipient, "unsubscribed": 0},
pluck="email",
)
else:
recipient_list.append(
frappe.db.get_value(
email_campaign.email_campaign_for, email_campaign.get("recipient"), "email_id"
email_id = frappe.db.get_value(campaign_for, recipient, "email_id")
if not email_id:
frappe.log_error(
title=_("Email Campaign Error"),
message=_("No email found for {0} {1}").format(campaign_for, recipient),
)
return
recipient_list = [email_id]
if not recipient_list:
frappe.log_error(
title=_("Email Campaign Error"),
message=_("No recipients found for campaign {0}").format(campaign_name),
)
return
# Get email template and sender
email_template = frappe.get_cached_doc("Email Template", entry.get("email_template"))
sender = frappe.db.get_value("User", sender_user, "email") if sender_user else None
# Build context for template rendering
if campaign_for != "Email Group":
context = {"doc": frappe.get_doc(campaign_for, recipient)}
else:
# For email groups, use the email group document as context
context = {"doc": frappe.get_doc("Email Group", recipient)}
# Render template
subject = frappe.render_template(email_template.get("subject"), context)
content = frappe.render_template(email_template.response_, context)
try:
comm = make(
doctype="Email Campaign",
name=campaign_name,
subject=subject,
content=content,
sender=sender,
recipients=recipient_list,
communication_medium="Email",
sent_or_received="Sent",
send_email=False,
email_template=email_template.name,
)
email_template = frappe.get_doc("Email Template", entry.get("email_template"))
sender = frappe.db.get_value("User", email_campaign.get("sender"), "email")
context = {"doc": frappe.get_doc(email_campaign.email_campaign_for, email_campaign.recipient)}
# send mail and link communication to document
comm = make(
doctype="Email Campaign",
name=email_campaign.name,
subject=frappe.render_template(email_template.get("subject"), context),
content=frappe.render_template(email_template.response_, context),
sender=sender,
bcc=recipient_list,
communication_medium="Email",
sent_or_received="Sent",
send_email=True,
email_template=email_template.name,
)
frappe.sendmail(
recipients=recipient_list,
subject=subject,
content=content,
sender=sender,
communication=comm["name"],
queue_separately=True,
)
except Exception:
frappe.log_error(title="Email Campaign Failed.")
return comm
@@ -140,7 +210,12 @@ def unsubscribe_recipient(unsubscribe, method):
# called through hooks to update email campaign status daily
def set_email_campaign_status():
email_campaigns = frappe.get_all("Email Campaign", filters={"status": ("!=", "Unsubscribed")})
for entry in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", entry.name)
email_campaigns = frappe.get_all(
"Email Campaign",
filters={"status": ("!=", "Unsubscribed")},
pluck="name",
)
for name in email_campaigns:
email_campaign = frappe.get_doc("Email Campaign", name)
email_campaign.update_status()