74 lines
2.7 KiB
Python
74 lines
2.7 KiB
Python
"""Magic-link delivery. SMTP when configured, dev log fallback otherwise."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import smtplib
|
|
from email.message import EmailMessage
|
|
|
|
from app.settings import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def magic_link(token: str) -> str:
|
|
return f"{settings.frontend_url.rstrip('/')}/login?token={token}"
|
|
|
|
|
|
def _send(message: EmailMessage) -> None:
|
|
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=10) as smtp:
|
|
smtp.starttls()
|
|
if settings.smtp_username:
|
|
smtp.login(settings.smtp_username, settings.smtp_password)
|
|
smtp.send_message(message)
|
|
|
|
|
|
def send_magic_link(to_email: str, link: str) -> bool:
|
|
"""Mail the login link. Returns False (logs instead) when SMTP is unset.
|
|
|
|
Never raises: callers invoke it after committing (approve/bootstrap),
|
|
so a delivery failure must not turn into a 500 — the token stays valid
|
|
and the admin sees ``email_sent: false``.
|
|
|
|
The plaintext link only ever appears here: service/api logging must
|
|
never include tokens or session ids (see test_auth no-leak test).
|
|
"""
|
|
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"
|
|
)
|
|
try:
|
|
_send(message)
|
|
except Exception: # noqa: BLE001 - delivery is best effort, see docstring
|
|
logger.warning("magic-link delivery failed for %s", to_email)
|
|
return False
|
|
logger.info("magic link mailed to %s", to_email)
|
|
return True
|
|
|
|
|
|
def send_access_notification(request_id: str, name: str, email: str) -> bool:
|
|
"""Notify CONTACT_TO about a new access request (best effort)."""
|
|
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"
|
|
)
|
|
try:
|
|
_send(message)
|
|
except Exception: # noqa: BLE001 - best effort, request is already stored
|
|
logger.warning("access-request notification failed for %s", request_id)
|
|
return False
|
|
return True
|