Files
colibre/docs/superpowers/specs/2026-06-24-brevo-transactional-email-design.md
T
2026-06-24 15:08:09 +02:00

7.0 KiB

Migration des emails transactionnels vers Brevo (API + templates hébergés)

Issue : #87 Branche : 87_brevo (partie de feature/73_compte_utilisateur) Date : 2026-06-24

Contexte

Les emails transactionnels de decp.info (vérification d'adresse à l'inscription, réinitialisation de mot de passe) sont aujourd'hui envoyés via Flask-Mail + SMTP (boîte mail Infomaniak standard). Cette approche n'offre aucune visibilité sur la délivrabilité (le mail de reset est-il arrivé ?), expose au risque de suspension de la boîte mail en cas de pic, et délivre moins bien.

On migre vers Brevo (compte gratuit existant) en utilisant son API transactionnelle via le SDK officiel brevo-python (v5) et ses templates hébergés. Brevo est une société française, données en UE (bon pour le RGPD).

Objectif

Remplacer entièrement le transport SMTP/Flask-Mail par l'API Brevo, avec deux templates hébergés côté Brevo (un par email), sans changer l'interface publique du mailer ni le comportement des routes appelantes.

Périmètre

Dans le périmètre

  • Réécriture de src/auth/mailer.py pour utiliser le SDK Brevo.
  • Deux templates Brevo (déjà créés côté web), référencés par leur ID.
  • Mise à jour des dépendances et des variables d'environnement.
  • Suppression des templates Jinja d'email locaux.
  • Réécriture des tests du mailer (hermétiques) + chemin sandbox optionnel.

Hors périmètre

  • Le contenu/design des templates Brevo (géré dans l'interface web Brevo).
  • Les autres usages d'email du projet s'il en existe (variables legacy SENDER_SERVER_DOMAIN, LOGIN_EMAIL, FROM_EMAIL, TO_EMAIL de .template.env) : à vérifier avant suppression ; ne pas y toucher si utilisées ailleurs.

Architecture

Interface publique inchangée

Les fonctions appelées depuis src/auth/routes.py (lignes 49 et 112) gardent exactement leur signature — routes.py n'est pas modifié :

send_verification_email(email: str, token: str) -> None
send_reset_email(email: str, token: str) -> None

Composants de src/auth/mailer.py (réécrit)

Fonction Rôle
init_mailer() Construit le client Brevo (TransactionalEmailsApi) depuis BREVO_API_KEY. Plus de paramètre app. Stocke le client au niveau module.
_send_template(template_id, recipient, params) Construit un SendSmtpEmail(to, template_id, params, sender), applique le mode sandbox si activé, appelle send_transac_email, log + remonte les ApiException.
send_verification_email(email, token) Construit le lien {base}/auth/verify-email?token=... et appelle _send_template(VERIFY_ID, email, {"link": link}).
send_reset_email(email, token) Construit le lien {base}/reinitialiser-mot-de-passe?token=... et appelle _send_template(RESET_ID, email, {"link": link}).

Disparaît : toute la logique Jinja (jinja_loader.searchpath, render_template, les templates .txt/.html), MAIL_SUPPRESS_SEND, l'objet Flask-Mail.

Appel d'initialisation

src/auth/setup.py:39 passe de mailer.init_mailer(app) à mailer.init_mailer() (seule modification hors mailer.py et tests).

Sender

Le sender (MAIL_FROM + MAIL_FROM_NAME) doit correspondre à un expéditeur vérifié dans Brevo ; le domaine decp.info doit être authentifié (SPF/DKIM) côté Brevo. Si le template Brevo définit déjà un sender, le passer explicitement reste possible et prioritaire.

Données / flux

  1. Une route (routes.py) génère un token et appelle send_*_email(email, token).
  2. Le mailer construit le lien absolu (APP_BASE_URL) et l'envoie comme params = {"link": link}.
  3. Le template Brevo correspondant (template_id) injecte {{ params.link }} dans son HTML et envoie.
  4. En cas d'échec API : ApiException loggée puis remontée (comme aujourd'hui flask_mail.send pouvait lever).

Variables d'environnement (.template.env)

Ajoutées :

  • BREVO_API_KEY= — clé API transactionnelle Brevo
  • BREVO_TEMPLATE_VERIFY_ID= — ID numérique du template de vérification
  • BREVO_TEMPLATE_RESET_ID= — ID numérique du template de reset
  • BREVO_SANDBOX=true pour valider sans délivrer (dev / intégration)
  • MAIL_FROM_NAME=decp.info — nom d'expéditeur

Conservées : MAIL_FROM, APP_BASE_URL

Supprimées : SMTP_HOST, SMTP_PORT, SMTP_USERNAME, SMTP_PASSWORD, SMTP_USE_TLS, MAIL_SUPPRESS_SEND

Gestion d'erreur

  • _send_template enveloppe l'appel dans un try/except ApiException : log (niveau error, sans la clé API) puis raise pour que l'appelant gère.
  • Si init_mailer() n'a pas été appelé ou BREVO_API_KEY absente : assert/erreur explicite, comme l'actuel « Mailer non initialisé ».

Stratégie de tests

Réconciliation validée :

  • Tests unitaires (tests/auth/test_mailer.py, réécrit) : on mocke TransactionalEmailsApi.send_transac_email (monkeypatch) et on vérifie le payload envoyé pour chaque fonction :
    • to == [{"email": "a@b.c"}]
    • template_id == BREVO_TEMPLATE_VERIFY_ID / ..._RESET_ID
    • params["link"] contient le bon chemin et le token
    • Aucun appel réseau, tourne en CI sans clé.
  • Mode sandbox Brevo : pour la vérification manuelle en devBREVO_SANDBOX=true ajoute le header sandbox Brevo ; l'app accepte le déclenchement (signup/reset) et Brevo valide sans délivrer.
  • Test d'intégration optionnel : @pytest.mark.integration, skippé par défaut, exécuté seulement si BREVO_API_KEY est présent, tape la vraie API en sandbox.

Points à vérifier en implémentation (ne pas présumer)

  • Nom exact du package/import du SDK v5 (brevo_python vs autre) et des classes (TransactionalEmailsApi, SendSmtpEmail, Configuration, ApiClient, ApiException) — confirmer sur la doc/repo getbrevo/brevo-python v5.
  • Mécanisme exact du mode sandbox Brevo (header X-Sib-Sandbox et sa valeur, ou réglage compte) — confirmer côté doc Brevo.
  • Usage éventuel des variables legacy SENDER_SERVER_DOMAIN/LOGIN_EMAIL/ FROM_EMAIL/TO_EMAIL ailleurs dans le code avant toute suppression.

Critères de succès

  • L'inscription envoie un email de vérification via Brevo (template hébergé) avec un lien fonctionnel.
  • La demande de reset envoie l'email de reset via Brevo avec un lien fonctionnel.
  • routes.py inchangé ; toute la suite de tests passe sans réseau.
  • flask-mail et les configs/templates SMTP retirés du dépôt.