Emails : robustesse SMTP + diagnostic

- config.py : retrait des identifiants OVH réels remis en dur ; défauts
  neutres (email_enabled=false). Les vraies valeurs vont dans .env.
- open_smtp : le port 465 force le SSL implicite (SMTPS) même si SMTP_SSL=false
  — c'était la cause des envois qui ne partaient pas (connexion en clair sur un
  port TLS -> timeout).
- deliver_many : les erreurs de connexion / auth / rejet sont journalisées
  avec le message du serveur et ne remontent plus (l'appel HTTP n'échoue pas).
- Lien magique : envoi en tâche de fond (BackgroundTasks) comme les
  notifications ; ne bloque plus la réponse.
- app/email/check.py : `python -m app.email.check <destinataire>` — affiche la
  config effective et envoie un email de test, avec la cause exacte en cas
  d'échec.
- Tests : fixture autouse forçant EMAIL_ENABLED=false (aucun test ne touche un
  vrai SMTP, quel que soit le .env local) ; test_full_flow lit l'email capturé
  au lieu des logs. 137 tests OK.
- .env.example : notes port 465 / From = boîte authentifiée / commande check.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
AdamThomas
2026-09-10 19:31:02 +02:00
co-authored by Claude Sonnet 5
parent 6b08dcc953
commit b84f62dfab
8 changed files with 157 additions and 31 deletions
+5 -3
View File
@@ -10,7 +10,7 @@ Deux systèmes, selon le compte :
import logging
import uuid
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from sqlalchemy import func
from sqlmodel import Session, select
@@ -125,10 +125,12 @@ def change_password(
def request_magic_link(
edition_id: uuid.UUID,
payload: MagicLinkRequest,
background: BackgroundTasks,
session: Session = Depends(get_session),
) -> dict[str, str]:
"""Émet un lien magique si un compte **sans mot de passe** correspond.
Réponse identique dans tous les cas (pas de divulgation)."""
Réponse identique dans tous les cas (pas de divulgation). L'email part en
tâche de fond (une erreur SMTP est journalisée, pas remontée)."""
account = _account_by_email(session, edition_id, payload.email)
if account is not None and account.password_hash is None:
settings = get_settings()
@@ -138,7 +140,7 @@ def request_magic_link(
link=build_magic_link_url(raw),
minutes=settings.magic_link_ttl_minutes,
)
send(account.email, subject, body)
background.add_task(send, account.email, subject, body)
if not settings.email_enabled:
# Aide au dev sans SMTP : le jeton (non nominatif) est journalisé.
logger.info(