61 lines
2 KiB
Python
61 lines
2 KiB
Python
|
|
"""Outbound email for onboarding (access requests + approval notices).
|
||
|
|
|
||
|
|
Gmail SMTP + App Password by default. Deliberately fail-soft: if SMTP isn't configured
|
||
|
|
or a send errors, we log and return False so the caller can fall back to in-app
|
||
|
|
notifications — email is never load-bearing for the app's core flow.
|
||
|
|
"""
|
||
|
|
import logging
|
||
|
|
import smtplib
|
||
|
|
import ssl
|
||
|
|
from email.message import EmailMessage
|
||
|
|
|
||
|
|
from app.config import settings
|
||
|
|
|
||
|
|
log = logging.getLogger("subfeed.email")
|
||
|
|
|
||
|
|
|
||
|
|
def email_enabled() -> bool:
|
||
|
|
return bool(settings.smtp_host and settings.smtp_user and settings.smtp_password)
|
||
|
|
|
||
|
|
|
||
|
|
def _send(to: list[str], subject: str, body: str) -> bool:
|
||
|
|
recipients = [e for e in to if e]
|
||
|
|
if not recipients:
|
||
|
|
return False
|
||
|
|
if not email_enabled():
|
||
|
|
log.info("SMTP not configured; skipping email %r to %s", subject, recipients)
|
||
|
|
return False
|
||
|
|
msg = EmailMessage()
|
||
|
|
msg["Subject"] = subject
|
||
|
|
msg["From"] = settings.smtp_from or settings.smtp_user
|
||
|
|
msg["To"] = ", ".join(recipients)
|
||
|
|
msg.set_content(body)
|
||
|
|
try:
|
||
|
|
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=20) as s:
|
||
|
|
s.starttls(context=ssl.create_default_context())
|
||
|
|
s.login(settings.smtp_user, settings.smtp_password)
|
||
|
|
s.send_message(msg)
|
||
|
|
log.info("Sent email %r to %s", subject, recipients)
|
||
|
|
return True
|
||
|
|
except Exception:
|
||
|
|
log.exception("SMTP send failed (%r to %s)", subject, recipients)
|
||
|
|
return False
|
||
|
|
|
||
|
|
|
||
|
|
def send_access_approved(email: str) -> bool:
|
||
|
|
return _send(
|
||
|
|
[email],
|
||
|
|
"Your Subfeed access is approved",
|
||
|
|
"Good news — your access to Subfeed has been approved.\n\n"
|
||
|
|
"Sign in with this Google account to start browsing your subscriptions.\n",
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def send_admin_new_request(admins: list[str], requester: str) -> bool:
|
||
|
|
return _send(
|
||
|
|
admins,
|
||
|
|
"Subfeed: new access request",
|
||
|
|
f"{requester} has requested access to Subfeed.\n\n"
|
||
|
|
"Approve or deny it in Settings → Admin.\n",
|
||
|
|
)
|