"""
Brevo (ex-Sendinblue) integration.
- Send transactional confirmation emails
- Sync confirmed contacts to a Brevo list
"""
import logging
import httpx
from app.config import settings
logger = logging.getLogger(__name__)
BREVO_API = "https://api.brevo.com/v3"
# ── Translations for transactional emails ──────────────────────────────
_EMAIL_I18N: dict[str, dict[str, str]] = {
"en": {
"subject": "Confirm your spot on the adiuvAI waitlist",
"preheader": "Confirm your email to secure your early access spot on the adiuvAI waitlist.",
"badge": "One more step",
"heading": "Confirm your spot on
the waitlist",
"body": "Thanks for signing up! Please confirm your email address so we can keep you in the loop when adiuvAI launches.",
"cta": "Confirm my email",
"expiry": "This link expires in {hours} hours.",
"footer": "If you didn't sign up, simply ignore this email.",
},
"it": {
"subject": "Conferma il tuo posto nella lista d'attesa di adiuvAI",
"preheader": "Conferma la tua email per assicurarti un accesso anticipato ad adiuvAI.",
"badge": "Ancora un passaggio",
"heading": "Conferma il tuo posto
nella lista d'attesa",
"body": "Grazie per esserti iscritto! Conferma il tuo indirizzo email così potremo aggiornarti quando adiuvAI sarà disponibile.",
"cta": "Conferma la mia email",
"expiry": "Questo link scade tra {hours} ore.",
"footer": "Se non ti sei iscritto, ignora semplicemente questa email.",
},
"es": {
"subject": "Confirma tu lugar en la lista de espera de adiuvAI",
"preheader": "Confirma tu correo para asegurar tu acceso anticipado a adiuvAI.",
"badge": "Un paso más",
"heading": "Confirma tu lugar en
la lista de espera",
"body": "¡Gracias por registrarte! Confirma tu dirección de correo para que podamos avisarte cuando adiuvAI esté listo.",
"cta": "Confirmar mi correo",
"expiry": "Este enlace caduca en {hours} horas.",
"footer": "Si no te registraste, simplemente ignora este correo.",
},
"fr": {
"subject": "Confirmez votre place sur la liste d'attente d'adiuvAI",
"preheader": "Confirmez votre e‑mail pour sécuriser votre accès anticipé à adiuvAI.",
"badge": "Encore une étape",
"heading": "Confirmez votre place sur
la liste d'attente",
"body": "Merci de vous être inscrit ! Veuillez confirmer votre adresse e‑mail pour que nous puissions vous tenir informé du lancement d'adiuvAI.",
"cta": "Confirmer mon e‑mail",
"expiry": "Ce lien expire dans {hours} heures.",
"footer": "Si vous ne vous êtes pas inscrit, ignorez simplement cet e‑mail.",
},
"de": {
"subject": "Bestätige deinen Platz auf der adiuvAI-Warteliste",
"preheader": "Bestätige deine E‑Mail, um dir den frühen Zugang zu adiuvAI zu sichern.",
"badge": "Noch ein Schritt",
"heading": "Bestätige deinen Platz
auf der Warteliste",
"body": "Danke für deine Anmeldung! Bitte bestätige deine E‑Mail-Adresse, damit wir dich informieren können, wenn adiuvAI startet.",
"cta": "Meine E‑Mail bestätigen",
"expiry": "Dieser Link läuft in {hours} Stunden ab.",
"footer": "Falls du dich nicht angemeldet hast, ignoriere diese E‑Mail einfach.",
},
}
def _t(lang: str, key: str) -> str:
"""Get translated string, falling back to English."""
return _EMAIL_I18N.get(lang, _EMAIL_I18N["en"]).get(key, _EMAIL_I18N["en"][key])
def _headers() -> dict[str, str]:
return {
"api-key": settings.BREVO_API_KEY,
"Content-Type": "application/json",
"Accept": "application/json",
}
async def send_confirmation_email(email: str, confirm_url: str, unsubscribe_url: str = "", lang: str = "en") -> bool:
"""Send a double opt-in confirmation email. Returns True on success."""
if not settings.brevo_configured:
logger.warning("Brevo not configured — skipping confirmation email for %s***", email[:3])
return False
payload = {
"sender": {
"name": settings.BREVO_SENDER_NAME,
"email": settings.BREVO_SENDER_EMAIL,
},
"to": [{"email": email}],
"subject": _t(lang, "subject"),
"htmlContent": _confirmation_html(confirm_url, unsubscribe_url, lang),
}
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(f"{BREVO_API}/smtp/email", headers=_headers(), json=payload)
resp.raise_for_status()
logger.info("Confirmation email sent to %s***", email[:3])
return True
except httpx.HTTPError:
logger.exception("Failed to send confirmation email to %s***", email[:3])
return False
async def add_contact_to_list(email: str) -> bool:
"""Add a confirmed contact to the Brevo waitlist list. Returns True on success."""
if not settings.brevo_configured:
logger.warning("Brevo not configured — skipping contact sync for %s***", email[:3])
return False
if settings.BREVO_LIST_ID == 0:
logger.warning("BREVO_LIST_ID not set — skipping contact sync")
return False
payload = {
"email": email,
"listIds": [settings.BREVO_LIST_ID],
"updateEnabled": True,
}
try:
async with httpx.AsyncClient(timeout=10) as client:
resp = await client.post(f"{BREVO_API}/contacts", headers=_headers(), json=payload)
resp.raise_for_status()
logger.info("Contact synced to Brevo list %d: %s***", settings.BREVO_LIST_ID, email[:3])
return True
except httpx.HTTPError:
logger.exception("Failed to sync contact to Brevo: %s***", email[:3])
return False
def _confirmation_html(confirm_url: str, unsubscribe_url: str = "", lang: str = "en") -> str:
"""Email template aligned with the adiuvAI landing page brand."""
html_lang = lang if lang in ("en", "it", "es", "fr", "de") else "en"
badge = _t(lang, "badge")
heading = _t(lang, "heading")
body = _t(lang, "body")
cta = _t(lang, "cta")
expiry = _t(lang, "expiry").format(hours=settings.CONFIRM_TOKEN_EXPIRY_HOURS)
preheader = _t(lang, "preheader")
footer = _t(lang, "footer")
unsub_label = {"en": "Unsubscribe", "it": "Annulla iscrizione", "es": "Cancelar suscripción", "fr": "Se désabonner", "de": "Abmelden"}.get(lang, "Unsubscribe")
return f"""\