"""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'"
)
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