"""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 import re from collections.abc import Mapping, Sequence _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" _P_OPEN = '
'
def _paragraphs(body: str, blocks: Mapping[str, 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` : repère → HTML déjà construit (ex. tableau des créneaux),
inséré tel quel à la place du repère, hors de tout paragraphe."""
out = []
for block in body.strip().split("\n\n"):
if not block.strip():
continue
html = _P_OPEN + _html.escape(block).replace(chr(10), "
") + "
|