Add email invitation system with SMTP configuration and invite token workflow
Implemented comprehensive email invitation functionality for new users. Added SMTP settings management in admin panel with configuration UI for host, port, credentials, sender details, and TLS options. Created invite token system with 48-hour expiration and single-use enforcement. Added mailer module with HTML email templates in German theme styling (radial gradient backgrounds, gold accents, bordered cards). Implemented invite
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
import html
|
||||
import smtplib
|
||||
import ssl
|
||||
from email.message import EmailMessage
|
||||
from email.utils import formataddr
|
||||
|
||||
from .models import AppSettings, User
|
||||
|
||||
|
||||
def _settings_ready(settings: AppSettings | None) -> bool:
|
||||
return bool(settings and settings.smtp_host and settings.smtp_from_email)
|
||||
|
||||
|
||||
def send_html_email(settings: AppSettings, recipient: str, subject: str, body_html: str, body_text: str):
|
||||
if not _settings_ready(settings):
|
||||
raise ValueError("SMTP settings are incomplete")
|
||||
|
||||
message = EmailMessage()
|
||||
message["Subject"] = subject
|
||||
message["From"] = formataddr((settings.smtp_from_name or "Cluedo HP", settings.smtp_from_email))
|
||||
message["To"] = recipient
|
||||
message.set_content(body_text)
|
||||
message.add_alternative(body_html, subtype="html")
|
||||
|
||||
context = ssl.create_default_context()
|
||||
with smtplib.SMTP(settings.smtp_host, settings.smtp_port or 587, timeout=20) as server:
|
||||
server.ehlo()
|
||||
if settings.smtp_use_tls:
|
||||
server.starttls(context=context)
|
||||
server.ehlo()
|
||||
if settings.smtp_username:
|
||||
server.login(settings.smtp_username, settings.smtp_password)
|
||||
server.send_message(message)
|
||||
|
||||
|
||||
def send_user_invite(settings: AppSettings, user: User, invite_url: str):
|
||||
name = html.escape(user.display_name or user.email)
|
||||
safe_url = html.escape(invite_url, quote=True)
|
||||
from_name = html.escape(settings.smtp_from_name or "Cluedo HP")
|
||||
body_html = f"""
|
||||
<!doctype html>
|
||||
<html><body style="margin:0;background:#0b0b0f;color:#f5efdc;font-family:Georgia,serif;">
|
||||
<div style="padding:36px 16px;background:radial-gradient(circle at top,#312b24 0,#0b0b0f 58%);">
|
||||
<div style="max-width:580px;margin:0 auto;border:1px solid #6f603f;border-radius:22px;overflow:hidden;background:#17161b;box-shadow:0 18px 55px rgba(0,0,0,.55);">
|
||||
<div style="padding:28px 30px;text-align:center;background:linear-gradient(135deg,#29231c,#141319);border-bottom:1px solid #6f603f;">
|
||||
<div style="font-size:13px;letter-spacing:3px;text-transform:uppercase;color:#cbb982;">{from_name}</div>
|
||||
<div style="margin-top:10px;font-size:28px;font-weight:bold;color:#e9d8a6;">Eine Einladung wartet</div>
|
||||
</div>
|
||||
<div style="padding:30px;line-height:1.55;">
|
||||
<div style="font-size:20px;color:#e9d8a6;">Hallo {name},</div>
|
||||
<p>du wurdest eingeladen, dem digitalen Zauber-Detektiv-Notizbogen beizutreten.</p>
|
||||
<p>Richte über den folgenden Button dein persönliches Passwort ein:</p>
|
||||
<p style="text-align:center;margin:28px 0;"><a href="{safe_url}" style="display:inline-block;padding:14px 24px;border-radius:12px;background:#b69a5c;color:#171319;text-decoration:none;font-weight:bold;">Einladung annehmen</a></p>
|
||||
<p style="font-size:13px;color:#b8ae98;">Der Link ist 48 Stunden gültig und kann nur einmal verwendet werden.</p>
|
||||
<p style="font-size:12px;color:#8e8777;word-break:break-all;">{safe_url}</p>
|
||||
</div>
|
||||
<div style="padding:16px 30px;color:#8e8777;font-size:12px;border-top:1px solid #302c2b;text-align:center;">Deine Notizen bleiben privat.</div>
|
||||
</div>
|
||||
</div>
|
||||
</body></html>
|
||||
"""
|
||||
body_text = f"Hallo {user.display_name or user.email},\n\nbitte richte dein Passwort ein: {invite_url}\n\nDer Link ist 48 Stunden gültig."
|
||||
send_html_email(settings, user.email, "Deine Einladung zum Zauber-Detektiv-Notizbogen", body_html, body_text)
|
||||
Reference in New Issue
Block a user