feat: bell-tower favicon, lfx-clean flows, branded HTML mail
This commit is contained in:
parent
672faf3af8
commit
df5c1e296c
15 changed files with 288 additions and 30 deletions
|
|
@ -1,4 +1,10 @@
|
|||
"""Magic-link delivery. SMTP when configured, dev log fallback otherwise."""
|
||||
"""Magic-link delivery. SMTP when configured, dev log fallback otherwise.
|
||||
|
||||
Mail design follows the web branding (see frontend/src/theme.ts):
|
||||
kirchenblau header, serif headline, white card on paper background.
|
||||
Email-safe by construction: table layout, inline styles only, system
|
||||
fonts, no external images (many clients block them / track opens).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
|
@ -10,11 +16,72 @@ from app.settings import settings
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Brand palette (mirrors frontend/src/theme.ts kirchenblau).
|
||||
BLUE = "#1d4a79"
|
||||
BLUE_LIGHT = "#eef4fb"
|
||||
INK = "#162f4c"
|
||||
MUTED = "#5b6b7c"
|
||||
|
||||
BLUE = "#1d4a79"
|
||||
BLUE_LIGHT = "#eef4fb"
|
||||
INK = "#162f4c"
|
||||
MUTED = "#5b6b7c"
|
||||
|
||||
|
||||
def magic_link(token: str) -> str:
|
||||
return f"{settings.frontend_url.rstrip('/')}/login?token={token}"
|
||||
|
||||
|
||||
def _page(title: str, intro: str, button_label: str, button_url: str | None, note: str) -> str:
|
||||
"""Branded HTML mail body (table layout, inline styles, no images)."""
|
||||
button = ""
|
||||
if button_url:
|
||||
button = (
|
||||
'<table role="presentation" cellpadding="0" cellspacing="0" '
|
||||
'style="margin:24px auto 8px auto;"><tr>'
|
||||
f'<td bgcolor="{BLUE}" style="border-radius:8px;">'
|
||||
f'<a href="{button_url}" '
|
||||
'style="display:inline-block;padding:12px 28px;font-family:-apple-system,'
|
||||
'Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:15px;'
|
||||
'font-weight:600;color:#ffffff;text-decoration:none;'
|
||||
f'border-radius:8px;">{button_label}</a></td></tr></table>'
|
||||
)
|
||||
return f"""<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<body style="margin:0;padding:0;background-color:{BLUE_LIGHT};">
|
||||
<table role="presentation" width="100%" cellpadding="0" cellspacing="0"
|
||||
style="background-color:{BLUE_LIGHT};padding:24px 12px;">
|
||||
<tr><td align="center">
|
||||
<table role="presentation" width="520" cellpadding="0" cellspacing="0"
|
||||
style="max-width:520px;width:100%;font-family:Georgia,'Times New Roman',serif;
|
||||
border-radius:12px;overflow:hidden;border:1px solid #d9e6f5;">
|
||||
<tr>
|
||||
<td bgcolor="{BLUE}" style="padding:20px 28px;">
|
||||
<span style="color:#ffffff;font-size:18px;font-weight:700;
|
||||
letter-spacing:0.02em;">Kirche Einfach</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td bgcolor="#ffffff" style="padding:28px 28px 24px 28px;">
|
||||
<h1 style="margin:0 0 12px 0;font-size:22px;line-height:1.3;
|
||||
color:{INK};">{title}</h1>
|
||||
<p style="margin:0;font-family:-apple-system,Segoe UI,Roboto,
|
||||
Helvetica,Arial,sans-serif;font-size:15px;line-height:1.6;
|
||||
color:{MUTED};">{intro}</p>
|
||||
{button}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<p style="font-family:-apple-system,Segoe UI,Roboto,Helvetica,Arial,sans-serif;
|
||||
font-size:12px;color:{MUTED};margin:16px 0 0 0;max-width:520px;">
|
||||
{note}
|
||||
</p>
|
||||
</td></tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>"""
|
||||
|
||||
|
||||
def _send(message: EmailMessage) -> None:
|
||||
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=10) as smtp:
|
||||
smtp.starttls()
|
||||
|
|
@ -23,6 +90,33 @@ def _send(message: EmailMessage) -> None:
|
|||
smtp.send_message(message)
|
||||
|
||||
|
||||
def _branded_mail(
|
||||
subject: str,
|
||||
to_email: str,
|
||||
headline: str,
|
||||
intro: str,
|
||||
button_label: str,
|
||||
button_url: str,
|
||||
note: str,
|
||||
) -> EmailMessage:
|
||||
"""Text + branded HTML alternative; the link is a real button."""
|
||||
message = EmailMessage()
|
||||
message["Subject"] = subject
|
||||
message["To"] = to_email
|
||||
message["From"] = settings.mail_from or settings.contact_to or to_email
|
||||
if settings.contact_to:
|
||||
message["Reply-To"] = settings.contact_to
|
||||
message.set_content(
|
||||
f"{headline}\n\n{intro}\n\n{button_label}:\n{button_url}\n\n{note}\n"
|
||||
)
|
||||
# Email-safe HTML: tables + inline CSS; some clients strip <style>, so
|
||||
# the palette is inlined via the f-string constants above.
|
||||
message.add_alternative(
|
||||
_page(headline, intro, button_label, button_url, note), subtype="html"
|
||||
)
|
||||
return message
|
||||
|
||||
|
||||
def send_magic_link(to_email: str, link: str) -> bool:
|
||||
"""Mail the login link. Returns False (logs instead) when SMTP is unset.
|
||||
|
||||
|
|
@ -36,14 +130,22 @@ def send_magic_link(to_email: str, link: str) -> bool:
|
|||
if not settings.smtp_host:
|
||||
logger.info("SMTP not configured; magic link for %s: %s", to_email, link)
|
||||
return False
|
||||
message = EmailMessage()
|
||||
message["Subject"] = "Your kirche-einfach login link"
|
||||
message["To"] = to_email
|
||||
message["From"] = settings.mail_from or settings.contact_to or to_email
|
||||
if settings.contact_to:
|
||||
message["Reply-To"] = settings.contact_to
|
||||
message.set_content(
|
||||
f"Hello,\n\nUse this link to log in (valid 15 minutes, single use):\n{link}\n"
|
||||
message = _branded_mail(
|
||||
subject="Dein Login-Link für Kirche Einfach",
|
||||
to_email=to_email,
|
||||
headline="Anmeldung bei Kirche Einfach",
|
||||
intro=(
|
||||
"Klicke auf den Button, um dich anzumelden. Der Link ist "
|
||||
"15 Minuten gültig und funktioniert nur einmal."
|
||||
),
|
||||
button_label="Jetzt anmelden",
|
||||
button_url=link,
|
||||
note=(
|
||||
"Falls du den Button nicht nutzen kannst, kopiere den Link "
|
||||
"aus der Textversion dieser E-Mail in deinen Browser. "
|
||||
"Diese E-Mail wurde angefordert — wenn du das warst, ist "
|
||||
"alles gut; sonst kannst du sie ignorieren."
|
||||
),
|
||||
)
|
||||
try:
|
||||
_send(message)
|
||||
|
|
@ -59,12 +161,17 @@ def send_access_notification(request_id: str, name: str, email: str) -> bool:
|
|||
if not settings.smtp_host or not settings.contact_to:
|
||||
logger.info("new access request %s from %s <%s>", request_id, name, email)
|
||||
return False
|
||||
message = EmailMessage()
|
||||
message["Subject"] = f"New access request: {name}"
|
||||
message["To"] = settings.contact_to
|
||||
message["From"] = settings.mail_from or settings.contact_to
|
||||
message.set_content(
|
||||
f"Name: {name}\nEmail: {email}\nRequest: {request_id}\n"
|
||||
message = _branded_mail(
|
||||
subject=f"Neue Zugangsanfrage: {name}",
|
||||
to_email=settings.contact_to,
|
||||
headline="Neue Zugangsanfrage",
|
||||
intro=(
|
||||
f"{name} ({email}) hat Zugang angefordert und wartet auf "
|
||||
"Freigabe in der Verwaltung."
|
||||
),
|
||||
button_label="In der Verwaltung prüfen",
|
||||
button_url=f"{settings.frontend_url.rstrip('/')}/app/admin",
|
||||
note="Diese Benachrichtigung geht an das Kontakt-Postfach.",
|
||||
)
|
||||
try:
|
||||
_send(message)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue