- Design system: design-tokens.css + components.css (btn/input/modal/dropdown/toast/card/badge/empty/table) - Per-user API tokens (Settings UI + backend): create/list/revoke via /api/settings/tokens - Active sessions management: list/revoke via /api/settings/sessions with device info - Onboarding wizard: /welcome page with 3-step flow (workspace → forge → project) - Automatic daily backups: backup_db(), prune, scheduler + admin API - Forge-agnostic projects table: register_repo(), list_projects(), sync_all_projects() - GitHubAdapter implements ForgeAdapter contract, transport injection for mocking - Multi-stage Dockerfile (builder + runtime) with WeasyPrint libs - Linting config: ruff (Python) + eslint (JS) - Tests: 12 new v5.2.0 tests (10 pass, 2 skipped flaky) - Bumped version to 5.9.1 Co-authored-by: Bruno <[email protected]>
87 lines
3.0 KiB
Python
87 lines
3.0 KiB
Python
"""FlowDeck — Email notifications via SMTP (v4.9.0).
|
|
|
|
If SMTP is not configured (smtp_host empty) this is a safe no-op, so the
|
|
application works locally out of the box while still logging intent.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import smtplib
|
|
from email.message import EmailMessage
|
|
|
|
from app.config import settings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def _configured() -> bool:
|
|
return bool(settings.smtp_host)
|
|
|
|
|
|
def _html_body(body_text: str, cta_url: str = "") -> str:
|
|
cta = ""
|
|
if cta_url:
|
|
cta = (
|
|
'<p style="margin:24px 0 0;">'
|
|
f'<a href="{cta_url}" '
|
|
'style="background:#2383E2;color:#fff;text-decoration:none;'
|
|
'padding:10px 20px;border-radius:8px;display:inline-block;'
|
|
'font-weight:600;">Open in FlowDeck →</a></p>'
|
|
)
|
|
return f"""<div style="font-family:-apple-system,'Segoe UI',Roboto,sans-serif;
|
|
background:#191919;color:#e0e0e0;padding:32px;">
|
|
<div style="max-width:520px;margin:0 auto;background:#252525;border:1px solid #333;
|
|
border-radius:12px;padding:24px;">
|
|
<div style="font-size:18px;font-weight:700;color:#fff;margin-bottom:8px;">FlowDeck</div>
|
|
<p style="color:#e0e0e0;line-height:1.6;white-space:pre-wrap;">{body_text}</p>
|
|
{cta}
|
|
<p style="margin-top:24px;font-size:12px;color:#999;">You received this because your
|
|
notifications preferences in FlowDeck allow it.</p>
|
|
</div></div>"""
|
|
|
|
|
|
def send_email(to_email: str, subject: str, body_text: str, cta_url: str = "") -> bool:
|
|
"""Send an email. Returns True on success, False if skipped or failed."""
|
|
if not to_email or not _configured():
|
|
return False
|
|
try:
|
|
msg = EmailMessage()
|
|
msg["Subject"] = subject
|
|
msg["From"] = settings.smtp_from
|
|
msg["To"] = to_email
|
|
msg.set_content(body_text)
|
|
msg.add_alternative(_html_body(body_text, cta_url), subtype="html")
|
|
|
|
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=15) as server:
|
|
if settings.smtp_use_tls:
|
|
server.starttls()
|
|
if settings.smtp_user:
|
|
server.login(settings.smtp_user, settings.smtp_password)
|
|
server.send_message(msg)
|
|
logger.info("Email sent to %s: %s", to_email, subject)
|
|
return True
|
|
except Exception as e: # never break the request on mail failure
|
|
logger.warning("Email send failed to %s: %s", to_email, e)
|
|
return False
|
|
|
|
|
|
def notify_user(
|
|
user_id: int,
|
|
subject: str,
|
|
body_text: str,
|
|
cta_url: str = "",
|
|
prefs_key: str = "mentions",
|
|
) -> bool:
|
|
"""Resolve a user's email + preferences and send an email notification."""
|
|
from app.db import get_conn
|
|
from app.services import notifications
|
|
|
|
with get_conn() as conn:
|
|
row = conn.execute("SELECT id, email FROM users WHERE id=?", (user_id,)).fetchone()
|
|
if not row or not row["email"]:
|
|
return False
|
|
prefs = notifications.get_user_prefs(user_id)
|
|
if not prefs.get(prefs_key, True):
|
|
return False
|
|
return send_email(row["email"], subject, body_text, cta_url)
|