Send branded HTML emails and harden SMTP2GO webhooks.
Deploy Beta / unit-tests (push) Successful in 9s
Deploy Beta / docker (push) Successful in 15s
Deploy Beta / deploy-beta (push) Successful in 1m36s

Fix campaign stuck on sending under async queue, and stop UUID ValidationError when SMTP2GO tests send "Headers Unavailable".
This commit is contained in:
2026-08-09 06:37:20 -05:00
parent 617bda3e8b
commit d830f07757
16 changed files with 558 additions and 63 deletions
+51 -17
View File
@@ -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)