Email : ne plus journaliser l'adresse du destinataire en cas de refus SMTP
En cherchant pourquoi les logs d'envoi d'email semblaient invisibles, on a
plutôt trouvé un problème RGPD : `SMTPRecipientsRefused` embarque l'adresse
du destinataire dans ses `args` (`{adresse: (code, message)}`), et
`logger.error(..., exc)` la journalisait telle quelle via `str(exc)` — en
violation directe de la règle « pas de log des noms/emails/téléphones ».
`_smtp_error_summary()` n'en extrait que les codes/messages serveur.
Constaté au passage (pas un bug) : le mécanisme de logs lui-même fonctionne
bien — `docker compose logs -f backend` affiche bien les lignes `app.email`
dès qu'une vraie requête HTTP déclenche l'envoi (testé en direct sur
l'édition de démo).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017uUsRg7XzopkCPWimwg9ZM
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d5f67b2701
commit
fcc42c8d43
@@ -16,6 +16,22 @@ from app.config import Settings, get_settings
|
|||||||
logger = logging.getLogger("app.email")
|
logger = logging.getLogger("app.email")
|
||||||
|
|
||||||
|
|
||||||
|
def _smtp_error_summary(exc: smtplib.SMTPException) -> str:
|
||||||
|
"""Résumé RGPD-safe d'une erreur SMTP : codes/messages serveur seulement.
|
||||||
|
|
||||||
|
Certaines exceptions de `smtplib` embarquent l'adresse du destinataire
|
||||||
|
dans leurs `args` (notamment `SMTPRecipientsRefused.recipients`, un
|
||||||
|
`{adresse: (code, message)}`) — `str(exc)` les journaliserait telles
|
||||||
|
quelles. On n'en garde que les codes/messages, jamais les adresses.
|
||||||
|
"""
|
||||||
|
if isinstance(exc, smtplib.SMTPRecipientsRefused):
|
||||||
|
codes = sorted({code for code, _msg in exc.recipients.values()})
|
||||||
|
return f"destinataire(s) refusé(s), code(s) {codes}"
|
||||||
|
if isinstance(exc, smtplib.SMTPResponseException):
|
||||||
|
return f"{exc.smtp_code} {exc.smtp_error!r}"
|
||||||
|
return exc.__class__.__name__
|
||||||
|
|
||||||
|
|
||||||
def build_message(to: str, subject: str, body: str) -> EmailMessage:
|
def build_message(to: str, subject: str, body: str) -> EmailMessage:
|
||||||
message = EmailMessage()
|
message = EmailMessage()
|
||||||
message["From"] = get_settings().smtp_from
|
message["From"] = get_settings().smtp_from
|
||||||
@@ -84,7 +100,7 @@ def deliver_many(messages: list[EmailMessage]) -> None:
|
|||||||
logger.error(
|
logger.error(
|
||||||
"Email refusé par le serveur (From=%s) : %s",
|
"Email refusé par le serveur (From=%s) : %s",
|
||||||
message["From"],
|
message["From"],
|
||||||
exc,
|
_smtp_error_summary(exc),
|
||||||
)
|
)
|
||||||
# RGPD : ni destinataire ni corps, seulement le compte et le sujet.
|
# RGPD : ni destinataire ni corps, seulement le compte et le sujet.
|
||||||
logger.info(
|
logger.info(
|
||||||
|
|||||||
@@ -53,6 +53,52 @@ def test_smtp_is_used_when_enabled(monkeypatch):
|
|||||||
assert ("send", "z@y.fr", "Sujet 2") in events
|
assert ("send", "z@y.fr", "Sujet 2") in events
|
||||||
|
|
||||||
|
|
||||||
|
def test_recipient_refusal_does_not_log_the_email_address(monkeypatch, caplog):
|
||||||
|
# RGPD : `SMTPRecipientsRefused` embarque l'adresse dans ses `args` —
|
||||||
|
# elle ne doit jamais atterrir dans les logs, seulement le(s) code(s).
|
||||||
|
fake = SimpleNamespace(
|
||||||
|
email_enabled=True,
|
||||||
|
smtp_host="mail.test",
|
||||||
|
smtp_port=587,
|
||||||
|
smtp_user="u",
|
||||||
|
smtp_password="p",
|
||||||
|
smtp_starttls=True,
|
||||||
|
smtp_ssl=False,
|
||||||
|
smtp_from="Festival <no-reply@test>",
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(sender, "get_settings", lambda: fake)
|
||||||
|
recipient = "jean.dupont@example.fr"
|
||||||
|
|
||||||
|
class FakeSMTP:
|
||||||
|
def __init__(self, host, port, timeout=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *_a):
|
||||||
|
return False
|
||||||
|
|
||||||
|
def starttls(self, context=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def login(self, user, _password):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def send_message(self, message):
|
||||||
|
raise sender.smtplib.SMTPRecipientsRefused(
|
||||||
|
{recipient: (450, b"Recipient address rejected: Domain not found")}
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(sender.smtplib, "SMTP", FakeSMTP)
|
||||||
|
|
||||||
|
with caplog.at_level("ERROR", logger="app.email"):
|
||||||
|
sender.send(recipient, "Sujet", "Corps")
|
||||||
|
|
||||||
|
assert recipient not in caplog.text
|
||||||
|
assert "450" in caplog.text
|
||||||
|
|
||||||
|
|
||||||
def test_render_magic_link_uses_translations():
|
def test_render_magic_link_uses_translations():
|
||||||
from app.email import render_magic_link
|
from app.email import render_magic_link
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user