Send branded HTML emails and harden SMTP2GO webhooks.
Fix campaign stuck on sending under async queue, and stop UUID ValidationError when SMTP2GO tests send "Headers Unavailable".
This commit is contained in:
+51
-17
@@ -256,8 +256,12 @@ def send_campaign_completion_notify(campaign: Campaign) -> bool:
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.db.models import Count, Q
|
||||
from django.template.loader import get_template
|
||||
from django.urls import reverse
|
||||
|
||||
from public.email_branding import email_brand_context
|
||||
|
||||
campaign.refresh_from_db()
|
||||
if campaign.notify_sent_at:
|
||||
return True
|
||||
if campaign.status != Campaign.Status.COMPLETED:
|
||||
@@ -267,6 +271,17 @@ def send_campaign_completion_notify(campaign: Campaign) -> bool:
|
||||
if not to_email:
|
||||
return False
|
||||
|
||||
# Claim the notify slot atomically so concurrent refresh calls only send once.
|
||||
now = timezone.now()
|
||||
claimed = Campaign.objects.filter(
|
||||
pk=campaign.pk,
|
||||
status=Campaign.Status.COMPLETED,
|
||||
notify_sent_at__isnull=True,
|
||||
).update(notify_sent_at=now)
|
||||
if not claimed:
|
||||
return True
|
||||
campaign.notify_sent_at = now
|
||||
|
||||
counts = campaign.messages.aggregate(
|
||||
sent=Count("id", filter=Q(status=Message.Status.SENT)),
|
||||
delivered=Count("id", filter=Q(status=Message.Status.DELIVERED)),
|
||||
@@ -287,22 +302,26 @@ def send_campaign_completion_notify(campaign: Campaign) -> bool:
|
||||
report_url = f"{public}{report_path}" if public else report_path
|
||||
|
||||
subject = f"Campaign sent: {campaign.name}"
|
||||
body = (
|
||||
f"Your {campaign.get_channel_display()} campaign “{campaign.name}” "
|
||||
f"has finished sending.\n\n"
|
||||
f"Recipients: {counts['total']}\n"
|
||||
f"Sent: {counts['sent']}\n"
|
||||
f"Delivered: {counts['delivered']}\n"
|
||||
f"Failed / bounced: {counts['failed']}\n"
|
||||
f"Suppressed: {counts['suppressed']}\n\n"
|
||||
f"Report: {report_url}\n"
|
||||
ctx = email_brand_context(
|
||||
subject=subject,
|
||||
campaign_name=campaign.name,
|
||||
channel_display=campaign.get_channel_display(),
|
||||
total=counts["total"],
|
||||
sent=counts["sent"],
|
||||
delivered=counts["delivered"],
|
||||
failed=counts["failed"],
|
||||
suppressed=counts["suppressed"],
|
||||
report_url=report_url,
|
||||
)
|
||||
text_content = get_template("emails/campaign_complete.txt").render(ctx)
|
||||
html_content = get_template("emails/campaign_complete.html").render(ctx)
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=body,
|
||||
body=text_content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[to_email],
|
||||
)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
try:
|
||||
email.send(fail_silently=False)
|
||||
except Exception: # noqa: BLE001 — don't block completion on mail errors
|
||||
@@ -311,15 +330,16 @@ def send_campaign_completion_notify(campaign: Campaign) -> bool:
|
||||
logging.getLogger(__name__).exception(
|
||||
"Campaign completion notify failed for %s", campaign.pk
|
||||
)
|
||||
Campaign.objects.filter(pk=campaign.pk).update(notify_sent_at=None)
|
||||
campaign.notify_sent_at = None
|
||||
return False
|
||||
|
||||
campaign.notify_sent_at = timezone.now()
|
||||
campaign.save(update_fields=["notify_sent_at", "updated_at"])
|
||||
return True
|
||||
|
||||
|
||||
def refresh_campaign_status(campaign: Campaign) -> Campaign:
|
||||
"""Set campaign to completed when no messages remain pending."""
|
||||
campaign.refresh_from_db()
|
||||
pending = campaign.messages.filter(
|
||||
status__in=[
|
||||
Message.Status.DRAFT,
|
||||
@@ -377,6 +397,9 @@ def send_campaign_test_email(campaign: Campaign, to_email: str) -> None:
|
||||
"""Send one preview copy to ``to_email`` without touching recipient rows."""
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import get_template
|
||||
|
||||
from public.email_branding import email_brand_context, plain_text_to_email_html
|
||||
|
||||
if campaign.channel != Channel.EMAIL:
|
||||
raise ValueError("Test send is only available for email campaigns.")
|
||||
@@ -391,14 +414,25 @@ def send_campaign_test_email(campaign: Campaign, to_email: str) -> None:
|
||||
if not body.strip():
|
||||
raise ValueError("Campaign has no body.")
|
||||
|
||||
notice = (
|
||||
"This is a test send from the Monica portal. "
|
||||
"Recipient list was not notified."
|
||||
)
|
||||
ctx = email_brand_context(
|
||||
title=f"[TEST] {subject}",
|
||||
content=f"{body}\n\n{notice}",
|
||||
content_html=(
|
||||
f"{plain_text_to_email_html(body)}"
|
||||
f'<p style="margin:24px 0 0;color:#6b7280;font-size:13px;">{notice}</p>'
|
||||
),
|
||||
)
|
||||
text_content = get_template("emails/marketing_email.txt").render(ctx)
|
||||
html_content = get_template("emails/marketing_email.html").render(ctx)
|
||||
email = EmailMultiAlternatives(
|
||||
subject=f"[TEST] {subject}",
|
||||
body=(
|
||||
f"{body}\n\n---\n"
|
||||
"This is a test send from the Monica portal. "
|
||||
"Recipient list was not notified."
|
||||
),
|
||||
body=text_content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[to_email],
|
||||
)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
email.send(fail_silently=False)
|
||||
Reference in New Issue
Block a user