Build and push server image / build-and-push (push) Successful in 40s
Admin-configured SMTP (server/port/username/password/from address/ STARTTLS, a singleton server_settings row set from /admin -- not env vars, since it's operator infrastructure a household admin sets up once through the UI) powers two features, both requiring the relevant user to have an email set in their own Settings: - "Forgot password?" on /login emails a one-hour single-use reset link (password_reset_tokens table). The endpoint always returns the same generic "check your email" response regardless of whether the address matched an account, so it can't be used to enumerate registered users. - A frame's Configuration tab can set a battery-alert threshold (Frame.battery_alert_threshold_pct, -1 = disabled); POST /frame/battery emails the owner the first time a report drops to or below it, then stays quiet for the rest of that discharge cycle (battery_alert_sent, reset alongside battery_history whenever the existing recharge-jump detection fires) -- not once per wake. New app/mail.py wraps stdlib smtplib (no new dependency); send_email() never raises, so a broken mail server can't 500 a battery report or a password-reset request. Schema migration v2 adds users.email and the two frame columns via ALTER TABLE (safe against the live, already- populated database) plus the two new tables via the existing create_all-based migration runner. Verified against a real (already-migrated, real user/frame data) database: the v1->v2 migration, admin SMTP config + test-email button, full forgot/reset-password roundtrip (including single-use token invalidation and the no-enumeration response), and the battery alert firing exactly once per crossing against a hand-rolled fake SMTP server -- all via curl end-to-end, plus the standing legacy-device curl suite to confirm the device protocol is untouched.
46 lines
1.6 KiB
Python
46 lines
1.6 KiB
Python
"""SMTP email sending -- password resets and battery-threshold alerts.
|
|
|
|
Config lives in the server_settings singleton row (admin-configured via
|
|
/admin, see routers/pages.py), not env vars -- it's operator
|
|
infrastructure a household admin sets up once through the UI, same
|
|
spirit as the rest of this project's "no separate config file" stance
|
|
post-redesign. Uses stdlib smtplib; no new dependency.
|
|
|
|
send_email() never raises -- a broken mail server shouldn't 500 a
|
|
password-reset request or a battery report; callers get a bool and log
|
|
a warning on failure."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
|
|
from .models import ServerSettings
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SMTP_TIMEOUT_S = 10
|
|
|
|
|
|
def send_email(settings: ServerSettings, to_address: str, subject: str, body: str) -> bool:
|
|
if not settings.smtp_host or not to_address:
|
|
return False
|
|
|
|
msg = MIMEText(body)
|
|
msg["Subject"] = subject
|
|
msg["From"] = settings.smtp_from_address or settings.smtp_username or "noreply@localhost"
|
|
msg["To"] = to_address
|
|
|
|
try:
|
|
with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=SMTP_TIMEOUT_S) as smtp:
|
|
if settings.smtp_use_tls:
|
|
smtp.starttls()
|
|
if settings.smtp_username:
|
|
smtp.login(settings.smtp_username, settings.smtp_password)
|
|
smtp.send_message(msg)
|
|
return True
|
|
except (OSError, smtplib.SMTPException) as e:
|
|
logger.warning("Failed to send email to %s: %s", to_address, e)
|
|
return False
|