Files
SkinsPins/app/email.py
T
Rolf 3a1da1482c Add full SkinsPins application
- FastAPI + SQLite + Alpine.js + HTMX PWA for Bingo Bango Bongo golf scoring
- Passwordless magic link auth with email (aiosmtplib) and admin invite system
- Real-time score entry via WebSocket with drag-and-drop player ordering
- Course management with per-hole par and stroke index
- Scorecard with golf score markers (birdie, eagle, bogey, etc.)
- Two-step new game form with tee selection per player
- Dashboard with stats, live games social stream, and recent game history
- Game summary view (read-only scorecard with leaderboard)
- User profile pages with win stats
- PWA install support with generated PNG icons
- Admin panel for users, courses, games, and invitations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 10:16:46 +01:00

97 lines
3.6 KiB
Python

"""Async email sending via SMTP. Falls back to logging if SMTP is not configured."""
import logging
import os
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
import aiosmtplib
logger = logging.getLogger(__name__)
SMTP_HOST = os.getenv("SMTP_HOST", "")
SMTP_PORT = int(os.getenv("SMTP_PORT", "587"))
SMTP_USER = os.getenv("SMTP_USER", "")
SMTP_PASSWORD = os.getenv("SMTP_PASSWORD", "")
SMTP_FROM = os.getenv("SMTP_FROM", SMTP_USER)
SMTP_SSL = os.getenv("SMTP_SSL", "false").lower() == "true"
SMTP_TLS = os.getenv("SMTP_TLS", "true").lower() != "false"
SITE_NAME = "Skins & Pins"
def _configured() -> bool:
return bool(SMTP_HOST and SMTP_USER and SMTP_FROM)
async def _send(to: str, subject: str, body_text: str, body_html: str) -> None:
msg = MIMEMultipart("alternative")
msg["Subject"] = subject
msg["From"] = f"{SITE_NAME} <{SMTP_FROM}>"
msg["To"] = to
msg.attach(MIMEText(body_text, "plain"))
msg.attach(MIMEText(body_html, "html"))
await aiosmtplib.send(
msg,
hostname=SMTP_HOST,
port=SMTP_PORT,
username=SMTP_USER,
password=SMTP_PASSWORD,
use_tls=SMTP_SSL,
start_tls=(SMTP_TLS and not SMTP_SSL),
timeout=10,
)
async def send_magic_link(to: str, link: str) -> None:
subject = f"Your {SITE_NAME} login link"
body_text = f"Click the link below to log in (expires in 15 minutes):\n\n{link}\n\nIf you didn't request this, you can ignore this email."
body_html = f"""
<div style="font-family:sans-serif;max-width:480px;margin:auto;">
<h2 style="color:#2d6a4f;">⛳ {SITE_NAME}</h2>
<p>Click the button below to log in. This link expires in 15 minutes.</p>
<p style="margin:2rem 0;">
<a href="{link}"
style="background:#2d6a4f;color:#fff;padding:0.75rem 1.5rem;border-radius:0.5rem;text-decoration:none;font-weight:600;">
Log in to {SITE_NAME}
</a>
</p>
<p style="color:#888;font-size:0.85rem;">If the button doesn't work, copy this link:<br>{link}</p>
<p style="color:#888;font-size:0.85rem;">If you didn't request this, you can ignore this email.</p>
</div>"""
if _configured():
try:
await _send(to, subject, body_text, body_html)
logger.info("Magic link email sent to %s", to)
except Exception as e:
logger.error("Failed to send magic link email to %s: %s", to, e)
else:
logger.info("Magic link for %s: %s", to, link)
async def send_invitation(to: str, invited_by_name: str, login_url: str) -> None:
subject = f"You've been invited to {SITE_NAME}"
body_text = f"{invited_by_name} has invited you to join {SITE_NAME}.\n\nLog in here:\n{login_url}"
body_html = f"""
<div style="font-family:sans-serif;max-width:480px;margin:auto;">
<h2 style="color:#2d6a4f;">⛳ {SITE_NAME}</h2>
<p><strong>{invited_by_name}</strong> has invited you to join {SITE_NAME} — a golf score tracking app.</p>
<p style="margin:2rem 0;">
<a href="{login_url}"
style="background:#2d6a4f;color:#fff;padding:0.75rem 1.5rem;border-radius:0.5rem;text-decoration:none;font-weight:600;">
Accept invitation
</a>
</p>
<p style="color:#888;font-size:0.85rem;">If the button doesn't work, copy this link:<br>{login_url}</p>
</div>"""
if _configured():
try:
await _send(to, subject, body_text, body_html)
logger.info("Invitation email sent to %s", to)
except Exception as e:
logger.error("Failed to send invitation email to %s: %s", to, e)
else:
logger.info("Invitation for %s — login at: %s", to, login_url)