"""Daily/weekly digest email rendering. Pure prose → HTML/text rendering. SMTP transport stays in ``email_service.send_email``; this module only assembles the message body, subject, and a text-only fallback for clients without HTML rendering. Split from email_service.py during the Tier 2 cleanup pass — the SMTP/OTP/welcome surface and the digest renderer changed at very different cadences and made the file noisy to navigate. """ from __future__ import annotations import html as _html_lib import re as _re from app import branding _DIGEST_HTML_TEMPLATE = """\ {brand} — {label}
▰ {brand_upper} · {label_upper}
 
{content_html}
 
{feedback_row}
 
""" def _strip_html_to_text(html_body: str) -> str: """Best-effort HTML → plain text for the multipart fallback. We don't need perfection — just readable prose for clients that won't render HTML.""" text = _re.sub(r"(?i)<(/(p|h[1-6]|li|ul|ol)|br\s*/?)>", "\n", html_body) text = _re.sub(r"<[^>]+>", "", text) text = _html_lib.unescape(text) text = _re.sub(r"\n{3,}", "\n\n", text) return text.strip() def _feedback_row_html( feedback_up_url: str | None, feedback_down_url: str | None, light_accent: str, light_muted: str, ) -> str: """Build the optional 'How was today's read?' row that sits between the digest content and the unsubscribe footer. Empty string when no feedback URLs were supplied (e.g. there's no latest log to vote on).""" if not feedback_up_url or not feedback_down_url: return "" return ( '
 
' f'
' "How was today’s read? " f'' "👍 Helpful" " · " f'' "👎 Not useful" "
" ) def render_digest_email( *, kind: str, date_str: str, content_html: str, unsubscribe_url: str, settings_url: str, feedback_up_url: str | None = None, feedback_down_url: str | None = None, ) -> tuple[str, str, str]: """Returns (subject, text_body, html_body) for a digest email. `kind` is "daily" or "weekly". Anything else raises ValueError. When ``feedback_up_url`` and ``feedback_down_url`` are both supplied, a small thumb up/down row is rendered above the unsubscribe footer. Both must be signed-token URLs pointing at /feedback (see ``app.services.log_feedback.sign_feedback_token``).""" if kind == "daily": label = "Daily" subject = f"{branding.BRAND_NAME} · Daily — {date_str}" elif kind == "weekly": label = "Weekly recap" subject = f"{branding.BRAND_NAME} · Weekly recap — {date_str}" else: raise ValueError(f"unknown digest kind: {kind!r}") feedback_row = _feedback_row_html( feedback_up_url, feedback_down_url, light_accent=branding.LIGHT["accent"], light_muted=branding.LIGHT["muted"], ) html_body = _DIGEST_HTML_TEMPLATE.format( brand=branding.BRAND_NAME, brand_upper=branding.BRAND_NAME.upper(), label=label, label_upper=label.upper(), FONT_MONO=branding.FONT_MONO, content_html=content_html, unsubscribe_url=unsubscribe_url, settings_url=settings_url, feedback_row=feedback_row, **{f"L_{k.replace('-', '_')}": v for k, v in branding.LIGHT.items()}, **{f"D_{k.replace('-', '_')}": v for k, v in branding.DARK.items()}, ) text_lines = [ f"{branding.BRAND_NAME} — {label}", date_str, "", _strip_html_to_text(content_html), "", ] if feedback_up_url and feedback_down_url: text_lines.extend([ f"Was this read useful? Helpful: {feedback_up_url}", f" Not useful: {feedback_down_url}", "", ]) text_lines.extend([ f"Unsubscribe: {unsubscribe_url}", f"Manage preferences: {settings_url}", ]) text_body = "\n".join(text_lines) return subject, text_body, html_body