Add SMTP security mode selector with SSL/STARTTLS/none options and automatic port switching

Replaced boolean `smtp_use_tls` with explicit `smtp_security` enum field supporting "none", "starttls", and "ssl" modes. Added database migration to create `smtp_security` column with "starttls" default. Updated mailer to use SMTP_SSL client for direct SSL connections on port 465 and SMTP with STARTTLS for port 587. Modified admin settings UI to show dropdown selector with encryption options and auto-adjust
This commit is contained in:
2026-08-02 10:38:50 +02:00
parent 33caa8f792
commit e7d288ff12
5 changed files with 27 additions and 5 deletions
+6 -2
View File
@@ -23,9 +23,13 @@ def send_html_email(settings: AppSettings, recipient: str, subject: str, body_ht
message.add_alternative(body_html, subtype="html") message.add_alternative(body_html, subtype="html")
context = ssl.create_default_context() context = ssl.create_default_context()
with smtplib.SMTP(settings.smtp_host, settings.smtp_port or 587, timeout=20) as server: security = getattr(settings, "smtp_security", "") or ("starttls" if settings.smtp_use_tls else "none")
port = settings.smtp_port or (465 if security == "ssl" else 587)
smtp_client = smtplib.SMTP_SSL if security == "ssl" else smtplib.SMTP
connection = smtp_client(settings.smtp_host, port, timeout=20, context=context) if security == "ssl" else smtp_client(settings.smtp_host, port, timeout=20)
with connection as server:
server.ehlo() server.ehlo()
if settings.smtp_use_tls: if security == "starttls":
server.starttls(context=context) server.starttls(context=context)
server.ehlo() server.ehlo()
if settings.smtp_username: if settings.smtp_username:
+8
View File
@@ -88,6 +88,14 @@ Very small, pragmatic auto-migration (no alembic).
- supports old schema (join_code/chip_code) and new schema (code/chip) - supports old schema (join_code/chip_code) and new schema (code/chip)
""" """
# --- app settings: explicit SMTP security mode (none/starttls/ssl) ---
if not _has_column(db, "app_settings", "smtp_security"):
try:
db.execute(text("ALTER TABLE app_settings ADD COLUMN smtp_security VARCHAR DEFAULT 'starttls'"))
db.commit()
except Exception:
db.rollback()
# --- users.display_name --- # --- users.display_name ---
if not _has_column(db, "users", "display_name"): if not _has_column(db, "users", "display_name"):
try: try:
+1
View File
@@ -52,6 +52,7 @@ class AppSettings(Base):
smtp_from_email: Mapped[str] = mapped_column(String, default="") smtp_from_email: Mapped[str] = mapped_column(String, default="")
smtp_from_name: Mapped[str] = mapped_column(String, default="Cluedo HP") smtp_from_name: Mapped[str] = mapped_column(String, default="Cluedo HP")
smtp_use_tls: Mapped[bool] = mapped_column(Boolean, default=True) smtp_use_tls: Mapped[bool] = mapped_column(Boolean, default=True)
smtp_security: Mapped[str] = mapped_column(String, default="starttls")
app_base_url: Mapped[str] = mapped_column(String, default="http://localhost:8081") app_base_url: Mapped[str] = mapped_column(String, default="http://localhost:8081")
+6 -1
View File
@@ -163,6 +163,7 @@ def read_smtp_settings(req: Request, db: Session = Depends(get_db)):
"smtp_password_configured": bool(settings.smtp_password), "smtp_password_configured": bool(settings.smtp_password),
"smtp_from_email": settings.smtp_from_email, "smtp_from_email": settings.smtp_from_email,
"smtp_from_name": settings.smtp_from_name, "smtp_from_name": settings.smtp_from_name,
"smtp_security": getattr(settings, "smtp_security", "starttls") or ("starttls" if settings.smtp_use_tls else "none"),
"smtp_use_tls": settings.smtp_use_tls, "smtp_use_tls": settings.smtp_use_tls,
"app_base_url": settings.app_base_url, "app_base_url": settings.app_base_url,
} }
@@ -181,7 +182,11 @@ def update_smtp_settings(req: Request, data: dict, db: Session = Depends(get_db)
settings.smtp_username = (data.get("smtp_username") or "").strip() settings.smtp_username = (data.get("smtp_username") or "").strip()
settings.smtp_from_email = (data.get("smtp_from_email") or "").strip() settings.smtp_from_email = (data.get("smtp_from_email") or "").strip()
settings.smtp_from_name = (data.get("smtp_from_name") or "Cluedo HP").strip() settings.smtp_from_name = (data.get("smtp_from_name") or "Cluedo HP").strip()
settings.smtp_use_tls = bool(data.get("smtp_use_tls", True)) security = data.get("smtp_security") or ("starttls" if data.get("smtp_use_tls", True) else "none")
if security not in ("none", "starttls", "ssl"):
raise HTTPException(400, "invalid SMTP security mode")
settings.smtp_security = security
settings.smtp_use_tls = security == "starttls"
settings.app_base_url = (data.get("app_base_url") or "http://localhost:8081").strip().rstrip("/") settings.app_base_url = (data.get("app_base_url") or "http://localhost:8081").strip().rstrip("/")
if "smtp_password" in data and data.get("smtp_password"): if "smtp_password" in data and data.get("smtp_password"):
settings.smtp_password = data["smtp_password"] settings.smtp_password = data["smtp_password"]
@@ -7,7 +7,7 @@ import { useLanguage } from "../i18n";
const initial = { const initial = {
smtp_host: "", smtp_port: 587, smtp_username: "", smtp_password: "", smtp_host: "", smtp_port: 587, smtp_username: "", smtp_password: "",
smtp_from_email: "", smtp_from_name: "Cluedo HP", smtp_use_tls: true, smtp_from_email: "", smtp_from_name: "Cluedo HP", smtp_security: "starttls",
app_base_url: window.location.origin, app_base_url: window.location.origin,
}; };
@@ -72,7 +72,11 @@ export default function AdminSettingsModal({ open, onClose }) {
<label style={styles.adminFieldLabel}>{t("senderName")}<input value={form.smtp_from_name} onChange={(e) => setField("smtp_from_name", e.target.value)} style={styles.input} /></label> <label style={styles.adminFieldLabel}>{t("senderName")}<input value={form.smtp_from_name} onChange={(e) => setField("smtp_from_name", e.target.value)} style={styles.input} /></label>
</div> </div>
<label style={styles.adminFieldLabel}>{t("appUrl")}<input value={form.app_base_url} onChange={(e) => setField("app_base_url", e.target.value)} placeholder="https://notizbogen.example.com" style={styles.input} /></label> <label style={styles.adminFieldLabel}>{t("appUrl")}<input value={form.app_base_url} onChange={(e) => setField("app_base_url", e.target.value)} placeholder="https://notizbogen.example.com" style={styles.input} /></label>
<label className="hp-admin-check"><input type="checkbox" checked={!!form.smtp_use_tls} onChange={(e) => setField("smtp_use_tls", e.target.checked)} /> {t("useTls")}</label> <label style={styles.adminFieldLabel}>{language === "en" ? "SMTP encryption" : "SMTP-Verschlüsselung"}<select value={form.smtp_security || "starttls"} onChange={(e) => { const security = e.target.value; setField("smtp_security", security); if (security === "ssl" && String(form.smtp_port) === "587") setField("smtp_port", 465); if (security === "starttls" && String(form.smtp_port) === "465") setField("smtp_port", 587); }} style={styles.input}>
<option value="none">{language === "en" ? "None" : "Keine Verschlüsselung"}</option>
<option value="starttls">STARTTLS</option>
<option value="ssl">{language === "en" ? "TLS/SSL (direct, port 465)" : "TLS/SSL (direkt, Port 465)"}</option>
</select></label>
<div className="hp-settings-test"> <div className="hp-settings-test">
<label style={{ ...styles.adminFieldLabel, flex: 1 }}>{t("testEmail")}<input type="email" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="you@example.com" style={styles.input} /></label> <label style={{ ...styles.adminFieldLabel, flex: 1 }}>{t("testEmail")}<input type="email" value={recipient} onChange={(e) => setRecipient(e.target.value)} placeholder="you@example.com" style={styles.input} /></label>
<button onClick={test} style={styles.secondaryBtn} disabled={testing}>{testing ? (language === "en" ? "Sending …" : "Sende …") : t("sendTest")}</button> <button onClick={test} style={styles.secondaryBtn} disabled={testing}>{testing ? (language === "en" ? "Sending …" : "Sende …") : t("sendTest")}</button>