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:
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user