Email: add explicit SMTP security mode

This commit is contained in:
mechramc
2026-06-01 23:15:06 -05:00
committed by GitHub
parent ccc0b9ab0c
commit 9d0a18a5b5
6 changed files with 195 additions and 40 deletions
+25 -25
View File
@@ -15,7 +15,6 @@ and `email_pollers.py` (the background loops):
import os
import imaplib
import smtplib
import ssl
import email as email_mod
import email.header
import email.utils
@@ -39,41 +38,37 @@ from src.secret_storage import decrypt as _decrypt
logger = logging.getLogger(__name__)
def _send_smtp_message(cfg: dict, from_addr: str, recipients: list[str], message: str | bytes, timeout: int = 30) -> None:
"""Send through SMTP using the conventional TLS mode for the configured port.
def _smtp_security_mode(cfg: dict) -> str:
raw = str(cfg.get("smtp_security") or "").strip().lower()
if raw in {"ssl", "starttls", "none"}:
return raw
port = int(cfg.get("smtp_port") or 465)
if port == 587:
return "starttls"
return "ssl"
Account settings only store host/port today. Port 465 is implicit TLS
(SMTP_SSL); port 587 is plain SMTP upgraded with STARTTLS. Using SSL
directly against 587 raises the classic "[SSL: WRONG_VERSION_NUMBER]"
error even when credentials are correct.
"""
def _send_smtp_message(cfg: dict, from_addr: str, recipients: list[str], message: str | bytes, timeout: int = 30) -> None:
"""Send through SMTP using the configured transport security mode."""
host = cfg["smtp_host"]
port = int(cfg.get("smtp_port") or 465)
user = cfg.get("smtp_user") or ""
password = cfg.get("smtp_password") or ""
def _send_starttls(starttls_port: int = 587) -> None:
with smtplib.SMTP(host, starttls_port, timeout=timeout) as smtp:
smtp.starttls()
if user and password:
smtp.login(user, password)
smtp.sendmail(from_addr, recipients, message)
security = _smtp_security_mode(cfg)
if port == 587:
_send_starttls(587)
return
try:
if security == "ssl":
with smtplib.SMTP_SSL(host, port, timeout=timeout) as smtp:
if user and password:
smtp.login(user, password)
smtp.sendmail(from_addr, recipients, message)
return
except (TimeoutError, ssl.SSLError) as e:
if port == 465:
logger.warning("SMTP implicit TLS on %s:465 failed (%s); retrying STARTTLS on 587", host, e)
_send_starttls(587)
return
raise
with smtplib.SMTP(host, port, timeout=timeout) as smtp:
if security == "starttls":
smtp.starttls()
if user and password:
smtp.login(user, password)
smtp.sendmail(from_addr, recipients, message)
def _strip_think(text: str) -> str:
@@ -543,6 +538,7 @@ def _get_email_config(account_id: str | None = None, owner: str = "") -> dict:
"account_name": row.name,
"smtp_host": row.smtp_host or "",
"smtp_port": int(row.smtp_port or 465),
"smtp_security": _smtp_security_mode({"smtp_security": getattr(row, "smtp_security", ""), "smtp_port": row.smtp_port}),
"smtp_user": row.smtp_user or "",
"smtp_password": _decrypt(row.smtp_password or ""),
"imap_host": row.imap_host or "",
@@ -569,6 +565,10 @@ def _get_email_config(account_id: str | None = None, owner: str = "") -> dict:
"account_name": "legacy",
"smtp_host": settings.get("smtp_host", os.environ.get("SMTP_HOST", "")),
"smtp_port": int(settings.get("smtp_port", os.environ.get("SMTP_PORT", "465")) or 465),
"smtp_security": _smtp_security_mode({
"smtp_security": settings.get("smtp_security", os.environ.get("SMTP_SECURITY", "")),
"smtp_port": settings.get("smtp_port", os.environ.get("SMTP_PORT", "465")),
}),
"smtp_user": settings.get("smtp_user", os.environ.get("SMTP_USER", "")),
"smtp_password": settings.get("smtp_password", os.environ.get("SMTP_PASSWORD", "")),
"imap_host": settings.get("imap_host", os.environ.get("IMAP_HOST", "")),
+15 -6
View File
@@ -40,7 +40,7 @@ from routes.email_helpers import (
_strip_think, _extract_reply, _apply_email_style_mechanics, require_owner, require_user, _assert_owns_account,
_q, _attach_compose_uploads, _cleanup_compose_uploads,
_load_settings, _save_settings, _get_email_config,
_send_smtp_message,
_send_smtp_message, _smtp_security_mode,
_imap_connect, _imap, _decode_header, _detect_sent_folder, _detect_drafts_folder,
_extract_attachment_text, _list_attachments_from_msg,
_extract_attachment_to_disk, _extract_html, _extract_text,
@@ -2146,6 +2146,7 @@ def setup_email_routes():
_from = cfg["from_address"]
_smtp_host = cfg["smtp_host"]
_smtp_port = cfg["smtp_port"]
_smtp_security = cfg.get("smtp_security")
_smtp_user = cfg["smtp_user"]
_smtp_pw = cfg["smtp_password"]
_recipients = list(recipients)
@@ -2163,6 +2164,7 @@ def setup_email_routes():
{
"smtp_host": _smtp_host,
"smtp_port": _smtp_port,
"smtp_security": _smtp_security,
"smtp_user": _smtp_user,
"smtp_password": _smtp_pw,
},
@@ -2820,7 +2822,7 @@ def setup_email_routes():
db.add(row)
field_map = {
"smtp_host": "smtp_host", "smtp_port": "smtp_port", "smtp_user": "smtp_user",
"imap_host": "imap_host", "imap_port": "imap_port", "imap_user": "imap_user",
"smtp_security": "smtp_security", "imap_host": "imap_host", "imap_port": "imap_port", "imap_user": "imap_user",
"imap_starttls": "imap_starttls", "email_from": "from_address",
}
for in_key, col_name in field_map.items():
@@ -2902,6 +2904,7 @@ def setup_email_routes():
"imap_starttls": bool(r.imap_starttls),
"smtp_host": r.smtp_host or "",
"smtp_port": int(r.smtp_port or 465),
"smtp_security": _smtp_security_mode({"smtp_security": getattr(r, "smtp_security", ""), "smtp_port": r.smtp_port}),
"smtp_user": r.smtp_user or "",
"from_address": r.from_address or "",
"has_imap_password": bool(r.imap_password),
@@ -2934,6 +2937,7 @@ def setup_email_routes():
imap_starttls=bool(data.get("imap_starttls", True)),
smtp_host=(data.get("smtp_host") or "").strip(),
smtp_port=int(data.get("smtp_port") or 465),
smtp_security=_smtp_security_mode({"smtp_security": data.get("smtp_security"), "smtp_port": data.get("smtp_port") or 465}),
smtp_user=(data.get("smtp_user") or "").strip(),
smtp_password=_enc(data.get("smtp_password") or ""),
from_address=(data.get("from_address") or "").strip(),
@@ -2977,6 +2981,8 @@ def setup_email_routes():
for key in ("imap_port", "smtp_port"):
if data.get(key) not in (None, ""):
setattr(row, key, int(data[key]))
if "smtp_security" in data:
row.smtp_security = _smtp_security_mode({"smtp_security": data.get("smtp_security"), "smtp_port": data.get("smtp_port") or row.smtp_port})
for key in ("imap_starttls", "enabled"):
if key in data:
setattr(row, key, bool(data[key]))
@@ -3061,6 +3067,7 @@ def setup_email_routes():
"imap_starttls": bool(row.imap_starttls),
"smtp_host": row.smtp_host or "",
"smtp_port": row.smtp_port or 465,
"smtp_security": _smtp_security_mode({"smtp_security": getattr(row, "smtp_security", ""), "smtp_port": row.smtp_port}),
"smtp_user": row.smtp_user or "",
"smtp_password": _decrypt(row.smtp_password or ""),
}
@@ -3112,14 +3119,16 @@ def setup_email_routes():
smtp_host = (body.get("smtp_host") or "").strip()
if smtp_host:
smtp_port = int(body.get("smtp_port") or 465)
smtp_security = _smtp_security_mode({"smtp_security": body.get("smtp_security"), "smtp_port": smtp_port})
smtp_user = (body.get("smtp_user") or imap_user).strip()
smtp_pass = body.get("smtp_password") or imap_pass
try:
if smtp_port == 587:
smtp = smtplib.SMTP(smtp_host, smtp_port, timeout=10)
smtp.starttls()
else:
if smtp_security == "ssl":
smtp = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=10)
else:
smtp = smtplib.SMTP(smtp_host, smtp_port, timeout=10)
if smtp_security == "starttls":
smtp.starttls()
try:
smtp.login(smtp_user, smtp_pass)
smtp_result = {"ok": True}