src/auth/mailer.py : envoi d'emails HTML+texte (#73)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from flask import Flask, render_template
|
||||||
|
from flask_mail import Mail, Message
|
||||||
|
|
||||||
|
TEMPLATES_DIR = Path(__file__).parent / "templates"
|
||||||
|
|
||||||
|
_mail: Mail | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def init_mailer(app: Flask) -> None:
|
||||||
|
global _mail
|
||||||
|
app.config["MAIL_SERVER"] = os.getenv("SMTP_HOST", "")
|
||||||
|
app.config["MAIL_PORT"] = int(os.getenv("SMTP_PORT", "587"))
|
||||||
|
app.config["MAIL_USERNAME"] = os.getenv("SMTP_USERNAME") or None
|
||||||
|
app.config["MAIL_PASSWORD"] = os.getenv("SMTP_PASSWORD") or None
|
||||||
|
app.config["MAIL_USE_TLS"] = os.getenv("SMTP_USE_TLS", "True").lower() == "true"
|
||||||
|
app.config["MAIL_DEFAULT_SENDER"] = os.getenv("MAIL_FROM", "noreply@decp.info")
|
||||||
|
app.config["MAIL_SUPPRESS_SEND"] = (
|
||||||
|
os.getenv("DEVELOPMENT", "False").lower() == "true"
|
||||||
|
)
|
||||||
|
# Inclure les templates du package auth dans la recherche Jinja
|
||||||
|
if hasattr(app.jinja_loader, "searchpath"):
|
||||||
|
app.jinja_loader.searchpath.append(str(TEMPLATES_DIR))
|
||||||
|
else:
|
||||||
|
import jinja2
|
||||||
|
|
||||||
|
app.jinja_loader = jinja2.ChoiceLoader(
|
||||||
|
[app.jinja_loader, jinja2.FileSystemLoader(str(TEMPLATES_DIR))]
|
||||||
|
)
|
||||||
|
_mail = Mail(app)
|
||||||
|
|
||||||
|
|
||||||
|
def _base_url() -> str:
|
||||||
|
return os.getenv("APP_BASE_URL", "http://localhost:8050").rstrip("/")
|
||||||
|
|
||||||
|
|
||||||
|
def _send(
|
||||||
|
subject: str,
|
||||||
|
recipient: str,
|
||||||
|
txt_template: str,
|
||||||
|
html_template: str,
|
||||||
|
**ctx,
|
||||||
|
) -> None:
|
||||||
|
assert _mail is not None, "Mailer non initialisé (init_mailer(app) non appelé)"
|
||||||
|
msg = Message(subject=subject, recipients=[recipient])
|
||||||
|
msg.body = render_template(txt_template, **ctx)
|
||||||
|
msg.html = render_template(html_template, **ctx)
|
||||||
|
_mail.send(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def send_verification_email(email: str, token: str) -> None:
|
||||||
|
link = f"{_base_url()}/verification-email?token={token}"
|
||||||
|
_send(
|
||||||
|
"Vérification de votre adresse email — decp.info",
|
||||||
|
email,
|
||||||
|
"emails/verify_email.txt",
|
||||||
|
"emails/verify_email.html",
|
||||||
|
link=link,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def send_reset_email(email: str, token: str) -> None:
|
||||||
|
link = f"{_base_url()}/reinitialiser-mot-de-passe?token={token}"
|
||||||
|
_send(
|
||||||
|
"Réinitialisation de votre mot de passe — decp.info",
|
||||||
|
email,
|
||||||
|
"emails/reset_password.txt",
|
||||||
|
"emails/reset_password.html",
|
||||||
|
link=link,
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<body style="font-family: sans-serif; max-width: 600px; margin: auto">
|
||||||
|
<h2 style="color: #333">decp.info</h2>
|
||||||
|
<p>Bonjour,</p>
|
||||||
|
<p>
|
||||||
|
Vous avez demandé la réinitialisation de votre mot de passe decp.info.
|
||||||
|
Pour choisir un nouveau mot de passe, cliquez sur le lien ci-dessous :
|
||||||
|
</p>
|
||||||
|
<p><a href="{{ link }}">Réinitialiser mon mot de passe</a></p>
|
||||||
|
<p style="color: #666">
|
||||||
|
Ou copiez cette URL dans votre navigateur : {{ link }}
|
||||||
|
</p>
|
||||||
|
<p><em>Ce lien est valide pendant 1 heure.</em></p>
|
||||||
|
<p>Si vous n'êtes pas à l'origine de cette demande, ignorez cet email.</p>
|
||||||
|
<p>— decp.info</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Bonjour,
|
||||||
|
|
||||||
|
Vous avez demandé la réinitialisation de votre mot de passe decp.info. Pour choisir un nouveau mot de passe, ouvrez le lien suivant :
|
||||||
|
|
||||||
|
{{ link }}
|
||||||
|
|
||||||
|
Ce lien est valide pendant 1 heure.
|
||||||
|
|
||||||
|
Si vous n'êtes pas à l'origine de cette demande, ignorez cet email.
|
||||||
|
|
||||||
|
— decp.info
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr">
|
||||||
|
<body style="font-family: sans-serif; max-width: 600px; margin: auto">
|
||||||
|
<h2 style="color: #333">decp.info</h2>
|
||||||
|
<p>Bonjour,</p>
|
||||||
|
<p>
|
||||||
|
Pour vérifier votre adresse email et activer votre compte decp.info,
|
||||||
|
cliquez sur le lien ci-dessous :
|
||||||
|
</p>
|
||||||
|
<p><a href="{{ link }}">Vérifier mon adresse email</a></p>
|
||||||
|
<p style="color: #666">
|
||||||
|
Ou copiez cette URL dans votre navigateur : {{ link }}
|
||||||
|
</p>
|
||||||
|
<p><em>Ce lien est valide pendant 24 heures.</em></p>
|
||||||
|
<p>
|
||||||
|
Si vous n'êtes pas à l'origine de cette inscription, ignorez cet email.
|
||||||
|
</p>
|
||||||
|
<p>— decp.info</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
Bonjour,
|
||||||
|
|
||||||
|
Pour vérifier votre adresse email et activer votre compte decp.info, ouvrez le lien suivant :
|
||||||
|
|
||||||
|
{{ link }}
|
||||||
|
|
||||||
|
Ce lien est valide pendant 24 heures.
|
||||||
|
|
||||||
|
Si vous n'êtes pas à l'origine de cette inscription, ignorez cet email.
|
||||||
|
|
||||||
|
— decp.info
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import pytest
|
||||||
|
from flask import Flask
|
||||||
|
from flask_mail import Mail
|
||||||
|
|
||||||
|
from src.auth import mailer
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def mail_app(monkeypatch, tmp_path):
|
||||||
|
app = Flask(
|
||||||
|
__name__,
|
||||||
|
template_folder=str(
|
||||||
|
(__import__("pathlib").Path(mailer.__file__).parent / "templates").resolve()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
app.config["MAIL_SUPPRESS_SEND"] = True
|
||||||
|
app.config["MAIL_DEFAULT_SENDER"] = "noreply@decp.info"
|
||||||
|
app.config["TESTING"] = True
|
||||||
|
mail = Mail(app)
|
||||||
|
monkeypatch.setattr(mailer, "_mail", mail)
|
||||||
|
monkeypatch.setenv("APP_BASE_URL", "http://localhost:8050")
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_verification_email(mail_app):
|
||||||
|
with mail_app.app_context(), mail_app.test_request_context():
|
||||||
|
with mail_app.extensions["mail"].record_messages() as outbox:
|
||||||
|
mailer.send_verification_email("a@b.c", "TOKEN123")
|
||||||
|
assert len(outbox) == 1
|
||||||
|
msg = outbox[0]
|
||||||
|
assert msg.recipients == ["a@b.c"]
|
||||||
|
assert "verification-email?token=TOKEN123" in msg.body
|
||||||
|
assert "verification-email?token=TOKEN123" in msg.html
|
||||||
|
|
||||||
|
|
||||||
|
def test_send_reset_email(mail_app):
|
||||||
|
with mail_app.app_context(), mail_app.test_request_context():
|
||||||
|
with mail_app.extensions["mail"].record_messages() as outbox:
|
||||||
|
mailer.send_reset_email("a@b.c", "RESET456")
|
||||||
|
assert len(outbox) == 1
|
||||||
|
msg = outbox[0]
|
||||||
|
assert "reinitialiser-mot-de-passe?token=RESET456" in msg.body
|
||||||
Reference in New Issue
Block a user