feat(collab): v4.9.0 Collaboration - commentaires inline, mentions @, notifications in-app + email
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user