Template
Populate the client website template with catalog feature flags.
Extract always-on public/portal/UTM plus optional email_sms, directmail, blog, payments, social, and social_ai so new client sites can be bootstrapped from this seed. Refs #1 Refs #2 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
"""SMTP2GO email via Django's SMTP backend (mail.smtp2go.com)."""
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
from django.template.loader import get_template
|
||||
|
||||
from contacts.models import Channel
|
||||
from email_sms.services import one_click_unsubscribe_url, preferences_url, render_merge_tags
|
||||
from public.email_branding import (
|
||||
campaign_body_to_email_html,
|
||||
campaign_body_to_plain_text,
|
||||
email_brand_context,
|
||||
)
|
||||
|
||||
# Reported back on SMTP2GO webhooks when this header is selected in webhook settings.
|
||||
MONICA_MESSAGE_HEADER = "X-Monica-Message-Id"
|
||||
|
||||
|
||||
def send_email(message) -> str:
|
||||
contact = message.contact
|
||||
if not contact.email:
|
||||
raise ValueError("Contact has no email address")
|
||||
|
||||
campaign = message.campaign
|
||||
subject = campaign.subject_override or (
|
||||
campaign.template.subject if campaign.template else f"Message from {settings.SITE_NAME}"
|
||||
)
|
||||
body = message.body_snapshot or campaign.body_override or (
|
||||
campaign.template.body if campaign.template else ""
|
||||
)
|
||||
subject = render_merge_tags(subject, contact)
|
||||
body = render_merge_tags(body, contact)
|
||||
|
||||
site = (settings.PUBLIC_SITE_URL or "").rstrip("/")
|
||||
prefs_path = preferences_url(str(contact.pk), Channel.EMAIL)
|
||||
one_click_path = one_click_unsubscribe_url(str(contact.pk), Channel.EMAIL)
|
||||
prefs_url = f"{site}{prefs_path}" if site else prefs_path
|
||||
one_click_url = f"{site}{one_click_path}" if site else one_click_path
|
||||
|
||||
ctx = email_brand_context(
|
||||
title=subject,
|
||||
content=campaign_body_to_plain_text(body),
|
||||
content_html=campaign_body_to_email_html(body),
|
||||
prefs_url=prefs_url,
|
||||
one_click_url=one_click_url,
|
||||
)
|
||||
text_content = get_template("emails/marketing_email.txt").render(ctx)
|
||||
html_content = get_template("emails/marketing_email.html").render(ctx)
|
||||
|
||||
email = EmailMultiAlternatives(
|
||||
subject=subject,
|
||||
body=text_content,
|
||||
from_email=settings.DEFAULT_FROM_EMAIL,
|
||||
to=[contact.email],
|
||||
headers={
|
||||
"List-Unsubscribe": f"<{one_click_url}>",
|
||||
"List-Unsubscribe-Post": "List-Unsubscribe=One-Click",
|
||||
MONICA_MESSAGE_HEADER: str(message.pk),
|
||||
},
|
||||
)
|
||||
email.attach_alternative(html_content, "text/html")
|
||||
email.send(fail_silently=False)
|
||||
# Placeholder until SMTP2GO webhook supplies the real email_id.
|
||||
return f"smtp-{message.pk}"
|
||||
@@ -0,0 +1,90 @@
|
||||
"""SMTP2GO SMS REST API."""
|
||||
|
||||
import logging
|
||||
import re
|
||||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
|
||||
from email_sms.services import render_merge_tags
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _format_destination(phone: str) -> str:
|
||||
"""Normalize stored phone to E.164-ish string SMTP2GO accepts."""
|
||||
digits = re.sub(r"\D", "", phone or "")
|
||||
if not digits:
|
||||
raise ValueError("Contact has no phone number")
|
||||
if phone.strip().startswith("+") and digits:
|
||||
return f"+{digits}"
|
||||
# US 10-digit local numbers → +1…
|
||||
if len(digits) == 10:
|
||||
return f"+1{digits}"
|
||||
if len(digits) == 11 and digits.startswith("1"):
|
||||
return f"+{digits}"
|
||||
return f"+{digits}"
|
||||
|
||||
|
||||
def _provider_error_detail(response: requests.Response) -> str:
|
||||
"""Prefer SMTP2GO JSON error text over bare HTTP reason."""
|
||||
try:
|
||||
data = response.json()
|
||||
except ValueError:
|
||||
text = (response.text or "").strip()
|
||||
return text[:500] if text else response.reason
|
||||
nested = data.get("data") if isinstance(data.get("data"), dict) else {}
|
||||
err = nested.get("error") or data.get("error") or ""
|
||||
code = nested.get("error_code") or data.get("error_code") or ""
|
||||
if err and code:
|
||||
return f"{err} ({code})"
|
||||
return str(err or code or response.reason)
|
||||
|
||||
|
||||
def send_sms(message) -> str:
|
||||
contact = message.contact
|
||||
if not contact.phone:
|
||||
raise ValueError("Contact has no phone number")
|
||||
|
||||
api_key = settings.SMTP2GO_SMS_API_KEY
|
||||
if not api_key:
|
||||
raise RuntimeError("SMTP2GO_SMS_API_KEY is not configured")
|
||||
|
||||
campaign = message.campaign
|
||||
body = message.body_snapshot or campaign.body_override or (
|
||||
campaign.template.body if campaign.template else ""
|
||||
)
|
||||
body = render_merge_tags(body, contact)
|
||||
destination = _format_destination(contact.phone)
|
||||
|
||||
# Current SMTP2GO /v3/sms/send schema: destination[] + content.
|
||||
payload = {
|
||||
"api_key": api_key,
|
||||
"destination": [destination],
|
||||
"content": body[:1600],
|
||||
}
|
||||
response = requests.post(
|
||||
settings.SMTP2GO_SMS_API_URL,
|
||||
json=payload,
|
||||
timeout=30,
|
||||
)
|
||||
if not response.ok:
|
||||
detail = _provider_error_detail(response)
|
||||
raise requests.HTTPError(
|
||||
f"{response.status_code} Client Error: {detail} for url: {response.url}",
|
||||
response=response,
|
||||
)
|
||||
|
||||
data = response.json() if response.content else {}
|
||||
nested = data.get("data") if isinstance(data.get("data"), dict) else {}
|
||||
messages = nested.get("messages") if isinstance(nested.get("messages"), list) else []
|
||||
first = messages[0] if messages and isinstance(messages[0], dict) else {}
|
||||
return str(
|
||||
first.get("message_id")
|
||||
or nested.get("sms_id")
|
||||
or nested.get("message_id")
|
||||
or data.get("sms_id")
|
||||
or data.get("message_id")
|
||||
or data.get("request_id")
|
||||
or f"sms-{message.pk}"
|
||||
)
|
||||
Reference in New Issue
Block a user