- Lien d'inscription protégé par un jeton (FormTemplate.access_token, /inscription/<token>), avec régénération depuis l'écran de configuration. - Reçus fiscaux mécénat (cerfa 11580*05) : un PDF par bénévole ayant payé en mécénat, donateur/montant (chiffres et toutes lettres en français, app/mecenat/number_words.py)/date complétés par-dessus le modèle officiel. - Jeu de démonstration : bénévoles avec cotisation classique ou mécénat payée. - Infobulles (HelpTip et détail de cellule de la grille des besoins) repositionnées en position: fixed via JS, bornées à l'écran. - Revue de sécurité : logo des emails en pièce jointe inline au lieu d'une URL (fuyait le jeton d'inscription), sujet retiré du log de remise des emails (pouvait porter un nom de bénévole), photo du formulaire public revalidée par décodage réel avant stockage, export Excel protégé contre l'injection de formule, sondage du statut de paiement limité en fréquence, pyjwt mis à jour. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
84 lines
3.3 KiB
Python
84 lines
3.3 KiB
Python
"""Version HTML des emails — même thème que l'appli (couleurs/typographie de
|
|
`frontend/tailwind.config.js`), logo de l'édition en en-tête si défini.
|
|
|
|
Chaque email reste envoyé en `multipart/alternative` (texte + HTML, voir
|
|
`app/email/sender.py:build_message`) : le texte brut reste la source de
|
|
vérité (accessibilité, clients sans HTML, RGPD — rien de plus dans le HTML
|
|
que dans le texte déjà journalisable). Pas de CSS externe ni de police web
|
|
(rarement chargées par les clients mail) : mise en page par tableau, styles
|
|
en ligne, polices système en repli du style « Archivo »/« DM Mono »."""
|
|
|
|
import html as _html
|
|
|
|
_INK = "#14110E"
|
|
_CREAM = "#F5EFE1"
|
|
_PAPER = "#FFFDF7"
|
|
_YELLOW = "#FFD23F"
|
|
|
|
_FONT_DISPLAY = "Arial, Helvetica, sans-serif"
|
|
_FONT_MONO = "'Courier New', Courier, monospace"
|
|
|
|
# Identifiant de la pièce jointe « inline » du logo (voir
|
|
# `app/email/sender.py:build_message`) — jamais une URL externe avec le
|
|
# `access_token` du formulaire public en requête : n'importe quel
|
|
# destinataire d'un email (lien magique, confirmation d'inscription…) aurait
|
|
# pu lire ce jeton depuis la source du message, pas seulement la coordination.
|
|
LOGO_CID = "edition-logo"
|
|
|
|
|
|
def _paragraphs(body: str) -> str:
|
|
"""`\n\n` sépare les paragraphes, un simple `\n` reste un retour à la
|
|
ligne — même convention que les modèles texte (`app/email/templates.py`)."""
|
|
blocks = body.strip().split("\n\n")
|
|
return "".join(
|
|
f'<p style="margin:0 0 14px;">{_html.escape(block).replace(chr(10), "<br>")}</p>'
|
|
for block in blocks
|
|
if block.strip()
|
|
)
|
|
|
|
|
|
def render_html(*, subject: str, body: str, has_logo: bool) -> str:
|
|
"""`has_logo` : voir `app/email/branding.py:edition_logo` — `False` tant
|
|
que l'édition n'a pas importé de logo, l'en-tête retombe alors sur un
|
|
simple bandeau texte (le sujet, en capitales)."""
|
|
if has_logo:
|
|
header = (
|
|
f'<img src="cid:{LOGO_CID}" alt="" width="160" '
|
|
'style="max-height:64px;width:auto;display:block;margin:0 auto;'
|
|
'border:0;">'
|
|
)
|
|
else:
|
|
header = (
|
|
f'<span style="font-family:{_FONT_DISPLAY};font-weight:800;'
|
|
f"text-transform:uppercase;letter-spacing:1px;color:{_YELLOW};"
|
|
f'font-size:14px;">{_html.escape(subject)}</span>'
|
|
)
|
|
return f"""<!doctype html>
|
|
<html lang="fr">
|
|
<head><meta charset="utf-8"></head>
|
|
<body style="margin:0;padding:24px 16px;background:{_CREAM};">
|
|
<table role="presentation" width="100%" cellpadding="0" cellspacing="0">
|
|
<tr>
|
|
<td align="center">
|
|
<table role="presentation" width="520" cellpadding="0" cellspacing="0"
|
|
style="max-width:520px;width:100%;">
|
|
<tr>
|
|
<td style="background:{_INK};padding:18px;text-align:center;
|
|
border:3px solid {_INK};">
|
|
{header}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td style="background:{_PAPER};border:3px solid {_INK};
|
|
border-top:none;padding:22px;font-size:13px;
|
|
line-height:1.6;color:{_INK};font-family:{_FONT_MONO};">
|
|
{_paragraphs(body)}
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</td>
|
|
</tr>
|
|
</table>
|
|
</body>
|
|
</html>"""
|