04/23 Fix smtp issue
This commit is contained in:
+173
-24
@@ -46,53 +46,196 @@ def load_email_config() -> dict:
|
||||
if "email" not in cfg:
|
||||
return {}
|
||||
s = cfg["email"]
|
||||
# security field: "starttls" | "ssl" | "none"
|
||||
# Falls back from legacy use_tls boolean for existing configs.
|
||||
security = s.get("security", "")
|
||||
if not security:
|
||||
security = "starttls" if s.getboolean("use_tls", fallback=True) else "none"
|
||||
return {
|
||||
"enabled": s.getboolean("enabled", fallback=False),
|
||||
"smtp_host": s.get("smtp_host", ""),
|
||||
"smtp_port": s.getint("smtp_port", fallback=587),
|
||||
"smtp_user": s.get("smtp_user", ""),
|
||||
"smtp_password": decrypt_value(s.get("smtp_password", "")),
|
||||
"use_tls": s.getboolean("use_tls", fallback=True),
|
||||
"security": security,
|
||||
"use_tls": security == "starttls", # kept for compatibility
|
||||
"recipients": [r.strip() for r in s.get("recipients", "").split(",") if r.strip()],
|
||||
"send_time": s.get("send_time", "18:00"),
|
||||
}
|
||||
|
||||
|
||||
def save_email_config(enabled: bool, smtp_host: str, smtp_port: int,
|
||||
smtp_user: str, smtp_password: str, use_tls: bool,
|
||||
smtp_user: str, smtp_password: str, security: str,
|
||||
recipients: str, send_time: str):
|
||||
from utils.config_crypto import encrypt_value
|
||||
cfg = configparser.ConfigParser()
|
||||
cfg.read(CONFIG_FILE, encoding="utf-8")
|
||||
# Preserve last_sent_date if present
|
||||
last_sent = cfg.get("email", "last_sent_date", fallback="")
|
||||
cfg["email"] = {
|
||||
"enabled": str(enabled).lower(),
|
||||
"smtp_host": smtp_host,
|
||||
"smtp_port": str(smtp_port),
|
||||
"smtp_user": smtp_user,
|
||||
"smtp_password": encrypt_value(smtp_password),
|
||||
"use_tls": str(use_tls).lower(),
|
||||
"recipients": recipients,
|
||||
"send_time": send_time,
|
||||
"enabled": str(enabled).lower(),
|
||||
"smtp_host": smtp_host,
|
||||
"smtp_port": str(smtp_port),
|
||||
"smtp_user": smtp_user,
|
||||
"smtp_password": encrypt_value(smtp_password),
|
||||
"security": security,
|
||||
"use_tls": str(security == "starttls").lower(), # legacy compat
|
||||
"recipients": recipients,
|
||||
"send_time": send_time,
|
||||
}
|
||||
if last_sent:
|
||||
cfg["email"]["last_sent_date"] = last_sent
|
||||
with open(CONFIG_FILE, "w", encoding="utf-8") as fh:
|
||||
cfg.write(fh)
|
||||
logger.info("Email configuration saved (password encrypted).")
|
||||
logger.info(f"Email configuration saved (security={security}, encrypted).")
|
||||
|
||||
|
||||
def test_smtp_connection(smtp_host, smtp_port, smtp_user, smtp_password, use_tls) -> tuple:
|
||||
def _make_smtp_server(smtp_host: str, smtp_port: int,
|
||||
security: str) -> "smtplib.SMTP":
|
||||
"""
|
||||
Attempt a connection without sending mail.
|
||||
Open and return a ready-to-login SMTP connection.
|
||||
security: "starttls" | "ssl" | "none"
|
||||
|
||||
Explicit ehlo() calls are required on Windows — smtplib's automatic
|
||||
greeting is unreliable there and causes 'Connection unexpectedly closed'
|
||||
on many servers (Office 365, Exchange, Google Workspace) when omitted.
|
||||
"""
|
||||
if security == "ssl":
|
||||
server = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=15)
|
||||
server.ehlo()
|
||||
elif security == "starttls":
|
||||
server = smtplib.SMTP(smtp_host, smtp_port, timeout=15)
|
||||
server.ehlo()
|
||||
server.starttls()
|
||||
server.ehlo() # re-identify after TLS upgrade — mandatory
|
||||
else:
|
||||
server = smtplib.SMTP(smtp_host, smtp_port, timeout=15)
|
||||
server.ehlo()
|
||||
return server
|
||||
|
||||
|
||||
def test_smtp_connection(smtp_host: str, smtp_port: int,
|
||||
smtp_user: str, smtp_password: str,
|
||||
security: str) -> tuple:
|
||||
"""
|
||||
Step-by-step SMTP diagnostic. Returns (success: bool, message: str).
|
||||
Each step is attempted independently so the error message tells the
|
||||
admin exactly where the failure occurred:
|
||||
Step 1 — DNS resolution
|
||||
Step 2 — TCP connect
|
||||
Step 3 — TLS handshake (if applicable)
|
||||
Step 4 — Authentication
|
||||
"""
|
||||
import socket
|
||||
|
||||
# Step 1: DNS resolution
|
||||
try:
|
||||
addr = socket.getaddrinfo(smtp_host, smtp_port,
|
||||
socket.AF_UNSPEC, socket.SOCK_STREAM)
|
||||
if not addr:
|
||||
raise OSError("No addresses returned")
|
||||
ip = addr[0][4][0]
|
||||
logger.info(f"SMTP test: {smtp_host} resolved to {ip}")
|
||||
except OSError as e:
|
||||
return False, (
|
||||
f"Step 1 FAILED — DNS: Cannot resolve '{smtp_host}'.\n"
|
||||
f"Check the hostname and your network connection.\n({e})"
|
||||
)
|
||||
|
||||
# Step 2: TCP connect (raw socket, before any SMTP protocol)
|
||||
try:
|
||||
sock = socket.create_connection((smtp_host, smtp_port), timeout=8)
|
||||
sock.close()
|
||||
logger.info(f"SMTP test: TCP connect to {smtp_host}:{smtp_port} OK")
|
||||
except OSError as e:
|
||||
return False, (
|
||||
f"Step 2 FAILED — TCP: Cannot reach {smtp_host}:{smtp_port}.\n"
|
||||
f"The port may be blocked by a firewall or the server is down.\n({e})"
|
||||
)
|
||||
|
||||
# Steps 3 + 4: SMTP protocol, TLS handshake, authentication
|
||||
try:
|
||||
server = _make_smtp_server(smtp_host, smtp_port, security)
|
||||
logger.info(f"SMTP test: TLS/connection OK (security={security})")
|
||||
except smtplib.SMTPConnectError as e:
|
||||
return False, (
|
||||
f"Step 3 FAILED — SMTP connect: {e}\n"
|
||||
f"Try a different Security mode or port."
|
||||
)
|
||||
except smtplib.SMTPException as e:
|
||||
return False, (
|
||||
f"Step 3 FAILED — TLS handshake: {e}\n"
|
||||
f"Try switching Security mode (e.g. SSL/TLS on port 465)."
|
||||
)
|
||||
except OSError as e:
|
||||
return False, (
|
||||
f"Step 3 FAILED — connection dropped: {e}\n"
|
||||
f"Try switching Security mode or port."
|
||||
)
|
||||
|
||||
try:
|
||||
server.login(smtp_user, smtp_password)
|
||||
server.quit()
|
||||
logger.info("SMTP test: authentication OK")
|
||||
return True, (
|
||||
f"All steps passed.\n"
|
||||
f"Connected to {smtp_host}:{smtp_port} "
|
||||
f"({security.upper()}) and authenticated successfully."
|
||||
)
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
try:
|
||||
server.quit()
|
||||
except Exception:
|
||||
pass
|
||||
return False, (
|
||||
f"Step 4 FAILED — Authentication: username or password rejected.\n"
|
||||
f"For Gmail/Google Workspace use an App Password, not your account password.\n({e})"
|
||||
)
|
||||
except smtplib.SMTPException as e:
|
||||
return False, f"Step 4 FAILED — SMTP error during login: {e}"
|
||||
except Exception as e:
|
||||
return False, f"Step 4 FAILED — Unexpected error: {e}"
|
||||
|
||||
|
||||
def send_test_email(smtp_host: str, smtp_port: int,
|
||||
smtp_user: str, smtp_password: str,
|
||||
security: str, recipients: list) -> tuple:
|
||||
"""
|
||||
Send a real test email through the full pipeline.
|
||||
Returns (success: bool, message: str).
|
||||
"""
|
||||
try:
|
||||
if use_tls:
|
||||
server = smtplib.SMTP(smtp_host, smtp_port, timeout=8)
|
||||
server.starttls()
|
||||
else:
|
||||
server = smtplib.SMTP_SSL(smtp_host, smtp_port, timeout=8)
|
||||
subject = "Website Checker — SMTP Test"
|
||||
body = (
|
||||
"<html><body style='font-family:Segoe UI,Arial,sans-serif;color:#1a1a2e'>"
|
||||
"<h3 style='color:#5b4de8'>Website Checker — SMTP Test</h3>"
|
||||
"<p>This is a test email confirming your SMTP configuration is working.</p>"
|
||||
"<p>You can now enable daily reports from the Email Settings panel.</p>"
|
||||
"</body></html>"
|
||||
)
|
||||
msg = MIMEMultipart("alternative")
|
||||
msg["Subject"] = subject
|
||||
msg["From"] = smtp_user
|
||||
msg["To"] = ", ".join(recipients)
|
||||
msg.attach(MIMEText(body, "html", "utf-8"))
|
||||
|
||||
server = _make_smtp_server(smtp_host, smtp_port, security)
|
||||
server.login(smtp_user, smtp_password)
|
||||
server.sendmail(smtp_user, recipients, msg.as_string())
|
||||
server.quit()
|
||||
return True, "Connection successful."
|
||||
logger.info(f"Test email sent to {recipients} via {smtp_host}:{smtp_port}")
|
||||
return True, f"Test email sent successfully to: {', '.join(recipients)}"
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
return False, (
|
||||
f"Authentication failed — check username and password.\n"
|
||||
f"For Gmail/Google Workspace use an App Password.\n({e})"
|
||||
)
|
||||
except smtplib.SMTPConnectError as e:
|
||||
return False, f"Could not connect to {smtp_host}:{smtp_port} — {e}"
|
||||
except smtplib.SMTPException as e:
|
||||
return False, f"SMTP error: {e}"
|
||||
except OSError as e:
|
||||
return False, f"Network error: {e}"
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
@@ -180,15 +323,21 @@ def _send_report(cfg: dict):
|
||||
msg.attach(MIMEText(html, "html", "utf-8"))
|
||||
|
||||
try:
|
||||
if cfg["use_tls"]:
|
||||
server = smtplib.SMTP(cfg["smtp_host"], cfg["smtp_port"], timeout=10)
|
||||
server.starttls()
|
||||
else:
|
||||
server = smtplib.SMTP_SSL(cfg["smtp_host"], cfg["smtp_port"], timeout=10)
|
||||
security = cfg.get("security", "starttls")
|
||||
server = _make_smtp_server(
|
||||
cfg["smtp_host"], cfg["smtp_port"], security)
|
||||
server.login(cfg["smtp_user"], cfg["smtp_password"])
|
||||
server.sendmail(cfg["smtp_user"], cfg["recipients"], msg.as_string())
|
||||
server.quit()
|
||||
logger.info(f"Daily report emailed to: {cfg['recipients']}")
|
||||
except smtplib.SMTPAuthenticationError as e:
|
||||
logger.error(f"Failed to send daily report — authentication error: {e}")
|
||||
except smtplib.SMTPConnectError as e:
|
||||
logger.error(f"Failed to send daily report — connection error: {e}")
|
||||
except smtplib.SMTPException as e:
|
||||
logger.error(f"Failed to send daily report — SMTP error: {e}")
|
||||
except OSError as e:
|
||||
logger.error(f"Failed to send daily report — network error: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to send daily report email: {e}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user