from types import SimpleNamespace from app.email import sender def test_disabled_delivery_is_a_noop(): # email_enabled=False par défaut en test : aucune exception, pas de SMTP. sender.send("a@b.fr", "Sujet", "Corps") sender.send_many([("a@b.fr", "S", "B"), ("c@d.fr", "S", "B")]) def test_smtp_is_used_when_enabled(monkeypatch): 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 ", ) monkeypatch.setattr(sender, "get_settings", lambda: fake) events: list = [] class FakeSMTP: def __init__(self, host, port, timeout=None): events.append(("connect", host, port)) def __enter__(self): return self def __exit__(self, *_a): return False def starttls(self, context=None): events.append(("starttls",)) def login(self, user, _password): events.append(("login", user)) def send_message(self, message): events.append(("send", message["To"], message["Subject"])) monkeypatch.setattr(sender.smtplib, "SMTP", FakeSMTP) sender.send_many([("x@y.fr", "Sujet 1", "B"), ("z@y.fr", "Sujet 2", "B")]) assert ("connect", "mail.test", 587) in events assert ("starttls",) in events assert ("login", "u") in events assert ("send", "x@y.fr", "Sujet 1") 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 ", ) 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(): from app.email import render_magic_link subject, body = render_magic_link(link="https://x/connexion?token=abc", minutes=30) assert subject assert "https://x/connexion?token=abc" in body assert "30" in body def test_message_without_logo_has_no_inline_image(): message = sender.build_message("a@b.fr", "Sujet", "Corps") html_part = message.get_body(preferencelist=("html",)) assert "cid:" not in html_part.get_content() assert not any(part.get_content_type() == "image/png" for part in message.walk()) def test_logo_is_embedded_inline_never_as_an_external_url(): # Le logo est une pièce jointe « inline » (Content-ID), jamais une URL : # `GET .../form/logo` exige le `access_token` du formulaire public # depuis l'étape 27 — une URL externe dans l'email l'aurait exposé à # quiconque lit la source du message (voir `app/email/branding.py`). logo_bytes = b"\x89PNG\r\n\x1a\n" + b"fake-png-bytes" message = sender.build_message( "a@b.fr", "Sujet", "Corps", logo=(logo_bytes, "image/png") ) html_part = message.get_body(preferencelist=("html",)) assert "cid:edition-logo" in html_part.get_content() assert "access_token" not in html_part.get_content() assert "http://" not in html_part.get_content() assert "https://" not in html_part.get_content() image_parts = [p for p in message.walk() if p.get_content_type() == "image/png"] assert len(image_parts) == 1 assert image_parts[0].get_content() == logo_bytes assert image_parts[0]["Content-ID"] == ""