diff --git a/docs/superpowers/plans/2026-06-24-brevo-transactional-email.md b/docs/superpowers/plans/2026-06-24-brevo-transactional-email.md new file mode 100644 index 0000000..d813f8e --- /dev/null +++ b/docs/superpowers/plans/2026-06-24-brevo-transactional-email.md @@ -0,0 +1,452 @@ +# Migration des emails transactionnels vers Brevo — Plan d'implémentation + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Remplacer le transport SMTP/Flask-Mail des emails transactionnels (vérification d'adresse, reset de mot de passe) par l'API Brevo et ses templates hébergés, sans changer l'interface publique du mailer. + +**Architecture:** `src/auth/mailer.py` est réécrit pour construire un client `Brevo` (SDK v5) et envoyer via `client.transactional_emails.send_transac_email(template_id, params, sender, to)`. Les deux fonctions publiques (`send_verification_email`, `send_reset_email`) gardent leur signature, donc `src/auth/routes.py` n'est pas touché. Seul l'appel d'init dans `setup.py` change (`init_mailer()` sans `app`). + +**Tech Stack:** Python, Flask, SDK `brevo-python` v5 (`5.0.0rc1`), pytest, monkeypatch. + +## Global Constraints + +- Imports internes toujours préfixés `src.` (ex: `from src.utils import logger`). +- SDK épinglé strictement : `brevo-python==5.0.0rc1` (pré-release ; un specifier qui pointe exactement une rc autorise pip à l'installer sans `--pre` global). +- API v5 vérifiée par introspection : + - `from brevo import Brevo, SendTransacEmailRequestSender, SendTransacEmailRequestToItem` + - `from brevo.core.api_error import ApiError` + - `Brevo(api_key: str, headers: dict | None = None)` + - `client.transactional_emails.send_transac_email(template_id=int, params=dict, sender=SendTransacEmailRequestSender, to=[SendTransacEmailRequestToItem], headers=dict|None)` + - `SendTransacEmailRequestSender(email=..., name=...)`, `SendTransacEmailRequestToItem(email=..., name=...)` +- Tests unitaires hermétiques : aucun appel réseau (le client est monkeypatché). +- Commits fréquents, un par tâche. +- Lancer les tests avec `uv run pytest` (pas `source .venv/bin/activate`). +- Le hook pre-commit (prettier/ruff) peut reformater des fichiers : si un commit échoue pour cause de reformatage, `git add` les fichiers modifiés et recommitter. + +--- + +## File Structure + +| Fichier | Responsabilité | Action | +| --------------------------------------- | ------------------------------------------ | ----------------------------------------- | +| `pyproject.toml` | Dépendances | Modifier (swap flask-mail → brevo-python) | +| `src/auth/mailer.py` | Envoi des emails transactionnels via Brevo | Réécrire | +| `src/auth/setup.py` | Initialisation du blueprint auth | Modifier (1 ligne) | +| `tests/auth/test_mailer.py` | Tests unitaires du mailer (mock) | Réécrire | +| `tests/auth/test_mailer_integration.py` | Test d'intégration sandbox optionnel | Créer | +| `.template.env` | Documentation des variables d'env | Modifier | +| `src/auth/templates/emails/*` | Anciens templates Jinja | Supprimer | + +--- + +### Task 1: Basculer la dépendance vers le SDK Brevo + +**Files:** + +- Modify: `pyproject.toml:25` + +**Interfaces:** + +- Consumes: (rien) +- Produces: le paquet `brevo` importable dans l'environnement. + +- [ ] **Step 1: Remplacer la dépendance dans `pyproject.toml`** + +Remplacer la ligne 25 : + +```toml +"flask-mail", +``` + +par : + +```toml +"brevo-python==5.0.0rc1", +``` + +- [ ] **Step 2: Réinstaller les dépendances du projet** + +Run: `uv pip install -e . --group=dev` +Expected: installation réussie, `brevo-python 5.0.0rc1` installé, `flask-mail` retiré. + +- [ ] **Step 3: Vérifier que le SDK s'importe avec les symboles attendus** + +Run: + +```bash +uv run python -c "from brevo import Brevo, SendTransacEmailRequestSender, SendTransacEmailRequestToItem; from brevo.core.api_error import ApiError; print('brevo v5 OK')" +``` + +Expected: affiche `brevo v5 OK` sans erreur. + +- [ ] **Step 4: Commit** + +```bash +git add pyproject.toml +git commit -m "build(brevo): remplacer flask-mail par brevo-python v5 (#87)" +``` + +--- + +### Task 2: Réécrire le mailer pour utiliser l'API Brevo (TDD) + +**Files:** + +- Test: `tests/auth/test_mailer.py` (réécriture complète) +- Modify: `src/auth/mailer.py` (réécriture complète) +- Modify: `src/auth/setup.py:39` + +**Interfaces:** + +- Consumes: `brevo.Brevo`, `brevo.SendTransacEmailRequestSender`, `brevo.SendTransacEmailRequestToItem`, `brevo.core.api_error.ApiError`, `src.utils.logger`. +- Produces (interface publique, inchangée pour `routes.py`) : + - `init_mailer() -> None` + - `send_verification_email(email: str, token: str) -> None` + - `send_reset_email(email: str, token: str) -> None` +- Variables d'env lues : `BREVO_API_KEY`, `BREVO_SANDBOX`, `BREVO_TEMPLATE_VERIFY_ID`, `BREVO_TEMPLATE_RESET_ID`, `MAIL_FROM`, `MAIL_FROM_NAME`, `APP_BASE_URL`. + +- [ ] **Step 1: Réécrire le test du mailer (mock du client Brevo)** + +Remplacer **tout** le contenu de `tests/auth/test_mailer.py` par : + +```python +import pytest + +from src.auth import mailer + + +class _FakeTransac: + def __init__(self): + self.calls = [] + + def send_transac_email(self, **kwargs): + self.calls.append(kwargs) + + +class _FakeClient: + def __init__(self): + self.transactional_emails = _FakeTransac() + + +@pytest.fixture +def fake_client(monkeypatch): + client = _FakeClient() + monkeypatch.setattr(mailer, "_client", client) + monkeypatch.setenv("APP_BASE_URL", "http://localhost:8050") + monkeypatch.setenv("BREVO_TEMPLATE_VERIFY_ID", "11") + monkeypatch.setenv("BREVO_TEMPLATE_RESET_ID", "22") + monkeypatch.setenv("MAIL_FROM", "noreply@decp.info") + monkeypatch.setenv("MAIL_FROM_NAME", "decp.info") + return client + + +def test_send_verification_email(fake_client): + mailer.send_verification_email("a@b.c", "TOKEN123") + calls = fake_client.transactional_emails.calls + assert len(calls) == 1 + call = calls[0] + assert call["template_id"] == 11 + assert call["to"][0].email == "a@b.c" + assert call["sender"].email == "noreply@decp.info" + assert "/auth/verify-email?token=TOKEN123" in call["params"]["link"] + + +def test_send_reset_email(fake_client): + mailer.send_reset_email("a@b.c", "RESET456") + calls = fake_client.transactional_emails.calls + assert len(calls) == 1 + call = calls[0] + assert call["template_id"] == 22 + assert "reinitialiser-mot-de-passe?token=RESET456" in call["params"]["link"] + + +def test_init_mailer_builds_client(monkeypatch): + monkeypatch.setenv("BREVO_API_KEY", "test-key") + monkeypatch.delenv("BREVO_SANDBOX", raising=False) + monkeypatch.setattr(mailer, "_client", None) + mailer.init_mailer() + assert mailer._client is not None + + +def test_send_without_init_raises(monkeypatch): + monkeypatch.setattr(mailer, "_client", None) + monkeypatch.setenv("BREVO_TEMPLATE_VERIFY_ID", "11") + with pytest.raises(AssertionError): + mailer.send_verification_email("a@b.c", "TOKEN123") +``` + +- [ ] **Step 2: Lancer les tests pour les voir échouer** + +Run: `uv run pytest tests/auth/test_mailer.py -v` +Expected: FAIL — l'ancien `mailer.py` importe `flask_mail` (désinstallé en Task 1) et n'expose pas `_client` ; erreurs d'import / d'attribut. + +- [ ] **Step 3: Réécrire `src/auth/mailer.py`** + +Remplacer **tout** le contenu de `src/auth/mailer.py` par : + +```python +import os + +from brevo import ( + Brevo, + SendTransacEmailRequestSender, + SendTransacEmailRequestToItem, +) +from brevo.core.api_error import ApiError + +from src.utils import logger + +_client: Brevo | None = None + + +def init_mailer() -> None: + """Construit le client Brevo à partir des variables d'environnement.""" + global _client + api_key = os.getenv("BREVO_API_KEY", "") + sandbox = os.getenv("BREVO_SANDBOX", "").lower() == "true" + headers = {"X-Sib-Sandbox": "drop"} if sandbox else None + _client = Brevo(api_key=api_key, headers=headers) + + +def _base_url() -> str: + return os.getenv("APP_BASE_URL", "http://localhost:8050").rstrip("/") + + +def _sender() -> SendTransacEmailRequestSender: + return SendTransacEmailRequestSender( + email=os.getenv("MAIL_FROM", "noreply@decp.info"), + name=os.getenv("MAIL_FROM_NAME", "decp.info"), + ) + + +def _template_id(env_var: str) -> int: + raw = os.getenv(env_var, "") + if not raw: + raise RuntimeError(f"{env_var} non défini (template Brevo)") + return int(raw) + + +def _send_template(template_id: int, recipient: str, params: dict) -> None: + assert _client is not None, "Mailer non initialisé (init_mailer() non appelé)" + try: + _client.transactional_emails.send_transac_email( + template_id=template_id, + params=params, + sender=_sender(), + to=[SendTransacEmailRequestToItem(email=recipient)], + ) + except ApiError: + logger.exception( + "Échec d'envoi Brevo (template %s) à %s", template_id, recipient + ) + raise + + +def send_verification_email(email: str, token: str) -> None: + link = f"{_base_url()}/auth/verify-email?token={token}" + _send_template( + _template_id("BREVO_TEMPLATE_VERIFY_ID"), email, {"link": link} + ) + + +def send_reset_email(email: str, token: str) -> None: + link = f"{_base_url()}/reinitialiser-mot-de-passe?token={token}" + _send_template( + _template_id("BREVO_TEMPLATE_RESET_ID"), email, {"link": link} + ) +``` + +- [ ] **Step 4: Mettre à jour l'appel d'init dans `setup.py`** + +Dans `src/auth/setup.py`, ligne 39, remplacer : + +```python + mailer.init_mailer(app) +``` + +par : + +```python + mailer.init_mailer() +``` + +- [ ] **Step 5: Lancer les tests du mailer pour les voir passer** + +Run: `uv run pytest tests/auth/test_mailer.py -v` +Expected: PASS (4 tests). + +- [ ] **Step 6: Lancer toute la suite auth pour vérifier l'absence de régression** + +Run: `uv run pytest tests/auth/ -v` +Expected: PASS (aucune dépendance résiduelle à flask-mail ; `routes.py` inchangé fonctionne). + +- [ ] **Step 7: Commit** + +```bash +git add src/auth/mailer.py src/auth/setup.py tests/auth/test_mailer.py +git commit -m "feat(brevo): envoyer les emails transactionnels via l'API Brevo v5 (#87)" +``` + +--- + +### Task 3: Nettoyer la configuration et les templates Jinja + +**Files:** + +- Modify: `.template.env` +- Delete: `src/auth/templates/emails/verify_email.html`, `verify_email.txt`, `reset_password.html`, `reset_password.txt` (tout le dossier `src/auth/templates/`) + +**Interfaces:** + +- Consumes: (rien — `mailer.py` n'utilise plus de templates locaux après Task 2) +- Produces: (rien) + +- [ ] **Step 1: Vérifier que les variables legacy ne sont pas utilisées ailleurs** + +Run: `grep -rn "SENDER_SERVER_DOMAIN\|LOGIN_EMAIL\|FROM_EMAIL\|TO_EMAIL" . --include=*.py` +Expected: aucun résultat. (Si des résultats apparaissent hors `.template.env`, NE PAS supprimer ces variables et le signaler.) + +- [ ] **Step 2: Mettre à jour `.template.env`** + +Dans la section SMTP de `.template.env`, supprimer les lignes : + +``` +SENDER_SERVER_DOMAIN="mail.example.com" # serveur SMTP +LOGIN_EMAIL="connect@example.fr" # adresse utilisée pour se connecter au serveur SMTP +FROM_EMAIL="from@example.com" # adresse d'envoi des emails (From) +TO_EMAIL="to@example.com" # adresse de destination des emails (To) +# SMTP pour envoi d'emails (vérification email, reset mot de passe) +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USERNAME= +SMTP_PASSWORD= +SMTP_USE_TLS=True +MAIL_SUPPRESS_SEND= # laisser vide pour hériter de DEVELOPMENT ; mettre false pour forcer l'envoi en mode dev +``` + +et les remplacer par : + +``` +# Brevo — envoi des emails transactionnels (vérification email, reset mot de passe) +BREVO_API_KEY= # clé API transactionnelle Brevo +BREVO_TEMPLATE_VERIFY_ID= # ID numérique du template "vérification d'adresse" +BREVO_TEMPLATE_RESET_ID= # ID numérique du template "réinitialisation mot de passe" +BREVO_SANDBOX= # mettre "true" pour valider sans délivrer (dev / intégration) +MAIL_FROM=noreply@decp.info # expéditeur (doit être un expéditeur vérifié dans Brevo) +MAIL_FROM_NAME=decp.info # nom d'expéditeur +``` + +Conserver la variable `APP_BASE_URL` si elle est déjà présente ailleurs dans le fichier ; sinon l'ajouter : + +``` +APP_BASE_URL=http://localhost:8050 # base des liens dans les emails +``` + +- [ ] **Step 3: Supprimer les templates Jinja d'email devenus inutiles** + +Run: `git rm -r src/auth/templates` +Expected: les 4 fichiers `emails/*.{html,txt}` sont supprimés. + +- [ ] **Step 4: Vérifier qu'aucun code ne référence encore ces templates** + +Run: `grep -rn "verify_email\|reset_password\|render_template\|jinja_loader\|MAIL_SUPPRESS_SEND\|flask_mail" src/ tests/ --include=*.py` +Expected: aucun résultat. + +- [ ] **Step 5: Relancer la suite auth complète** + +Run: `uv run pytest tests/auth/ -v` +Expected: PASS. + +- [ ] **Step 6: Commit** + +```bash +git add .template.env src/auth/templates +git commit -m "chore(brevo): retirer la config SMTP et les templates Jinja d'email (#87)" +``` + +--- + +### Task 4: Test d'intégration sandbox optionnel + +**Files:** + +- Create: `tests/auth/test_mailer_integration.py` + +**Interfaces:** + +- Consumes: interface publique `mailer.init_mailer()`, `mailer.send_verification_email(email, token)`. +- Produces: un test `@pytest.mark.integration` skippé sauf si `BREVO_API_KEY` est défini. + +- [ ] **Step 1: Créer le test d'intégration** + +Créer `tests/auth/test_mailer_integration.py` : + +```python +import os + +import pytest + +from src.auth import mailer + +pytestmark = pytest.mark.integration + + +@pytest.mark.skipif( + not os.getenv("BREVO_API_KEY"), + reason="BREVO_API_KEY absent — test d'intégration Brevo ignoré", +) +def test_send_verification_email_sandbox(monkeypatch): + # Force le mode sandbox : Brevo valide la requête sans délivrer. + monkeypatch.setenv("BREVO_SANDBOX", "true") + monkeypatch.setenv("APP_BASE_URL", "http://localhost:8050") + mailer.init_mailer() + # Ne doit pas lever : l'API Brevo accepte la requête en sandbox. + mailer.send_verification_email( + os.getenv("MAIL_FROM", "noreply@decp.info"), "INTEGRATION_TOKEN" + ) +``` + +- [ ] **Step 2: Enregistrer le marker `integration` (si absent)** + +Vérifier la présence du marker dans `pyproject.toml` : + +Run: `grep -n "integration" pyproject.toml` + +Si aucune section `[tool.pytest.ini_options]` avec `markers` ne déclare `integration`, l'ajouter. Exemple à insérer/compléter dans `pyproject.toml` : + +```toml +[tool.pytest.ini_options] +markers = [ + "integration: tests touchant des services externes (Brevo) ; skippés par défaut en CI", +] +``` + +(Si la section existe déjà, n'ajouter que la ligne `integration: ...` dans la liste `markers` existante, sans dupliquer la section.) + +- [ ] **Step 3: Vérifier que le test est bien collecté puis skippé sans clé** + +Run: `uv run pytest tests/auth/test_mailer_integration.py -v` +Expected: 1 test SKIPPED (motif « BREVO_API_KEY absent »), aucun appel réseau. + +- [ ] **Step 4: Vérifier que la suite par défaut reste verte** + +Run: `uv run pytest tests/auth/ -v` +Expected: PASS, avec le test d'intégration SKIPPED. + +- [ ] **Step 5: Commit** + +```bash +git add tests/auth/test_mailer_integration.py pyproject.toml +git commit -m "test(brevo): test d'intégration sandbox optionnel (#87)" +``` + +--- + +## Notes de vérification finale (manuel, hors CI) + +Avant de merger, vérification manuelle réelle : + +1. Renseigner `.env` avec `BREVO_API_KEY`, `BREVO_TEMPLATE_VERIFY_ID`, `BREVO_TEMPLATE_RESET_ID`, `MAIL_FROM` (expéditeur vérifié Brevo) et `BREVO_SANDBOX=true`. +2. Lancer `python run.py`, déclencher une inscription, vérifier dans les logs Brevo (tableau de bord) que la requête est reçue (sandbox = acceptée, non délivrée). +3. Passer `BREVO_SANDBOX=` (vide), refaire une inscription avec une vraie adresse, confirmer la réception et que `{{ params.link }}` est correctement substitué dans le template. +4. Confirmer la valeur du header sandbox (`X-Sib-Sandbox: drop`) si le comportement diffère de l'attendu. diff --git a/docs/superpowers/specs/2026-06-24-brevo-transactional-email-design.md b/docs/superpowers/specs/2026-06-24-brevo-transactional-email-design.md index a8a4576..b348223 100644 --- a/docs/superpowers/specs/2026-06-24-brevo-transactional-email-design.md +++ b/docs/superpowers/specs/2026-06-24-brevo-transactional-email-design.md @@ -13,8 +13,25 @@ délivrabilité (le mail de reset est-il arrivé ?), expose au risque de suspens 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). +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) : + +```python +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 @@ -53,12 +70,12 @@ 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})`. | +| 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. @@ -102,7 +119,7 @@ reste possible et prioritaire. ## Gestion d'erreur -- `_send_template` enveloppe l'appel dans un `try/except ApiException` : log +- `_send_template` enveloppe l'appel dans un `try/except ApiError` : 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é ». @@ -111,10 +128,10 @@ reste possible et prioritaire. 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"}]` +- **Tests unitaires** (`tests/auth/test_mailer.py`, réécrit) : on **mocke** la méthode + `transactional_emails.send_transac_email` du 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_ID` - `params["link"]` contient le bon chemin et le token - Aucun appel réseau, tourne en CI sans clé. @@ -124,13 +141,21 @@ Réconciliation validée : - **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) +## Faits vérifiés / points résiduels -- 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. +**Vérifié par introspection de `brevo-python==5.0.0rc1`** : + +- Install : `pip install --pre "brevo-python==5.0.0rc1"` (package PyPI `brevo-python`, + import `brevo`). +- Imports : `from brevo import Brevo, SendTransacEmailRequestSender, SendTransacEmailRequestToItem` ; `from brevo.core.api_error import ApiError`. +- Client : `Brevo(api_key=..., headers={...})` ; envoi via + `client.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: drop` est 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_EMAIL` ailleurs dans le code avant toute suppression.