mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 21:24:16 +08:00
105 lines
3.4 KiB
Python
105 lines
3.4 KiB
Python
"""SMTP email notification adapter.
|
|
|
|
Transport failures are isolated from alert persistence and SSE delivery. SMTP credentials
|
|
are supplied by the caller from ``secrets_store`` and never logged here.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import smtplib
|
|
import time
|
|
from contextlib import suppress
|
|
from email.message import EmailMessage
|
|
from email.utils import parseaddr
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
SECURITY_MODES = {"ssl", "starttls", "none"}
|
|
_MAX_ATTEMPTS = 2
|
|
|
|
|
|
def is_valid_email(address: str) -> bool:
|
|
"""Small dependency-free mailbox validation suitable for configuration checks."""
|
|
parsed = parseaddr((address or "").strip())[1]
|
|
if parsed != (address or "").strip() or parsed.count("@") != 1:
|
|
return False
|
|
local, domain = parsed.rsplit("@", 1)
|
|
return bool(local and domain and "." in domain and " " not in parsed)
|
|
|
|
|
|
def is_configured(config: dict) -> bool:
|
|
"""Return whether the non-secret fields are sufficient to attempt delivery."""
|
|
sender = str(config.get("from_address") or config.get("username") or "").strip()
|
|
recipients = config.get("to_addresses") or []
|
|
return bool(config.get("host") and sender and recipients)
|
|
|
|
|
|
def send_email(
|
|
config: dict,
|
|
password: str,
|
|
subject: str,
|
|
body: str,
|
|
*,
|
|
max_attempts: int = _MAX_ATTEMPTS,
|
|
) -> bool:
|
|
"""Send one UTF-8 plain-text email through SSL, STARTTLS, or plain SMTP."""
|
|
if not is_configured(config):
|
|
return False
|
|
|
|
host = str(config.get("host") or "").strip()
|
|
try:
|
|
port = int(config.get("port", 465))
|
|
except (TypeError, ValueError):
|
|
return False
|
|
security = str(config.get("security") or "ssl")
|
|
username = str(config.get("username") or "").strip()
|
|
sender = str(config.get("from_address") or username).strip()
|
|
recipients = [str(item).strip() for item in config.get("to_addresses", [])]
|
|
if (
|
|
not 1 <= port <= 65535
|
|
or security not in SECURITY_MODES
|
|
or not is_valid_email(sender)
|
|
or not recipients
|
|
or any(not is_valid_email(item) for item in recipients)
|
|
):
|
|
return False
|
|
|
|
message = EmailMessage()
|
|
message["Subject"] = str(subject or "TickFlow 通知")
|
|
message["From"] = sender
|
|
message["To"] = ", ".join(recipients)
|
|
message.set_content(str(body or ""))
|
|
|
|
last_err = ""
|
|
for attempt in range(1, max_attempts + 1):
|
|
smtp = None
|
|
try:
|
|
if security == "ssl":
|
|
smtp = smtplib.SMTP_SSL(host, port, timeout=10)
|
|
else:
|
|
smtp = smtplib.SMTP(host, port, timeout=10)
|
|
if security == "starttls":
|
|
smtp.ehlo()
|
|
smtp.starttls()
|
|
smtp.ehlo()
|
|
if username:
|
|
smtp.login(username, password)
|
|
smtp.send_message(message)
|
|
# Delivery already succeeded; a failed QUIT must not retry and duplicate the email.
|
|
try:
|
|
smtp.quit()
|
|
except Exception:
|
|
with suppress(Exception):
|
|
smtp.close()
|
|
return True
|
|
except Exception as exc: # SMTP/network errors must not escape
|
|
last_err = str(exc)
|
|
if smtp is not None:
|
|
with suppress(Exception):
|
|
smtp.close()
|
|
if attempt < max_attempts:
|
|
time.sleep(1)
|
|
|
|
logger.warning("邮件推送最终失败(已尝试 %d 次): %s", max_attempts, last_err)
|
|
return False
|