Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
8.2 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 (5.0.0rc1, une
pré-release / release candidate, à installer avec --pre et épinglée strictement)
et ses templates hébergés. Brevo est une société française, données en UE
(bon pour le RGPD).
L'API v5 (vérifiée par introspection du paquet) :
from brevo import Brevo, SendTransacEmailRequestSender, SendTransacEmailRequestToItem
from brevo.core.api_error import ApiError
client = Brevo(api_key="...", headers={"X-Sib-Sandbox": "drop"}) # headers optionnels
client.transactional_emails.send_transac_email(
template_id=123,
params={"link": "https://..."},
sender=SendTransacEmailRequestSender(email="noreply@decp.info", name="decp.info"),
to=[SendTransacEmailRequestToItem(email="dest@example.com")],
)
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.pypour 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_EMAILde.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(api_key=...) depuis BREVO_API_KEY (+ header sandbox si BREVO_SANDBOX=true). Plus de paramètre app. Stocke le client au niveau module. |
_send_template(template_id, recipient, params) |
Appelle client.transactional_emails.send_transac_email(template_id, params, sender=SendTransacEmailRequestSender(...), to=[SendTransacEmailRequestToItem(email=recipient)]), log + remonte les ApiError. |
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
- Une route (
routes.py) génère un token et appellesend_*_email(email, token). - Le mailer construit le lien absolu (
APP_BASE_URL) et l'envoie commeparams = {"link": link}. - Le template Brevo correspondant (
template_id) injecte{{ params.link }}dans son HTML et envoie. - En cas d'échec API :
ApiExceptionloggée puis remontée (comme aujourd'huiflask_mail.sendpouvait lever).
Variables d'environnement (.template.env)
Ajoutées :
BREVO_API_KEY=— clé API transactionnelle BrevoBREVO_TEMPLATE_VERIFY_ID=— ID numérique du template de vérificationBREVO_TEMPLATE_RESET_ID=— ID numérique du template de resetBREVO_SANDBOX=—truepour 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_templateenveloppe l'appel dans untry/except ApiError: log (niveau error, sans la clé API) puisraisepour que l'appelant gère.- Si
init_mailer()n'a pas été appelé ouBREVO_API_KEYabsente :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 la méthodetransactional_emails.send_transac_emaildu client (monkeypatch) et on capture les kwargs passés pour chaque fonction :to[0].email == "a@b.c"template_id == BREVO_TEMPLATE_VERIFY_ID/..._RESET_IDparams["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 dev —
BREVO_SANDBOX=trueajoute 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 siBREVO_API_KEYest présent, tape la vraie API en sandbox.
Faits vérifiés / points résiduels
Vérifié par introspection de brevo-python==5.0.0rc1 :
- Install :
pip install --pre "brevo-python==5.0.0rc1"(package PyPIbrevo-python, importbrevo). - Imports :
from brevo import Brevo, SendTransacEmailRequestSender, SendTransacEmailRequestToItem;from brevo.core.api_error import ApiError. - Client :
Brevo(api_key=..., headers={...}); envoi viaclient.transactional_emails.send_transac_email(template_id=int, params=dict, sender=..., to=[...], headers=dict|None).
À confirmer en implémentation (ne pas bloquer) :
- Valeur exacte du header sandbox Brevo (
X-Sib-Sandbox: dropest la valeur documentée pour « accepter mais ne pas délivrer ») — à confirmer avec un envoi réel en sandbox. - Usage éventuel des variables legacy
SENDER_SERVER_DOMAIN/LOGIN_EMAIL/FROM_EMAIL/TO_EMAILailleurs 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.pyinchangé ; toute la suite de tests passe sans réseau.flask-mailet les configs/templates SMTP retirés du dépôt.