330 lines
12 KiB
Python
330 lines
12 KiB
Python
"""
|
|
views/email_settings_view.py — SMTP / scheduled report configuration dialog.
|
|
Admin-only. Persists to config.ini [email] section via utils/scheduler.py.
|
|
"""
|
|
|
|
import tkinter as tk
|
|
from tkinter import ttk
|
|
import logging
|
|
import threading
|
|
|
|
from utils.ui_helpers import (
|
|
COLOURS, FONT, FONT_BOLD, FONT_HEADING, FONT_SMALL,
|
|
show_error, show_info,
|
|
)
|
|
|
|
logger = logging.getLogger("email_settings_view")
|
|
|
|
# Security mode -> (label, default_port)
|
|
_SECURITY_MODES = {
|
|
"starttls": ("STARTTLS (port 587, most common)", 587),
|
|
"ssl": ("SSL / TLS (port 465, Gmail direct)", 465),
|
|
"none": ("None (port 25, internal relay only)", 25),
|
|
}
|
|
|
|
|
|
class EmailSettingsView(tk.Toplevel):
|
|
def __init__(self, master, current_user: dict):
|
|
super().__init__(master)
|
|
self.current_user = current_user
|
|
self.title("Email Report Settings")
|
|
self.configure(bg=COLOURS["bg"])
|
|
self.resizable(False, False)
|
|
self.grab_set()
|
|
self.protocol("WM_DELETE_WINDOW", self.destroy)
|
|
self._build_ui()
|
|
self._load_existing()
|
|
self._centre()
|
|
|
|
def _centre(self):
|
|
self.update_idletasks()
|
|
w, h = 540, 610
|
|
x = (self.winfo_screenwidth() - w) // 2
|
|
y = (self.winfo_screenheight() - h) // 2
|
|
self.geometry(f"{w}x{h}+{x}+{y}")
|
|
|
|
def _build_ui(self):
|
|
C = COLOURS
|
|
|
|
# Header
|
|
hdr = tk.Frame(self, bg=C["accent"], pady=14)
|
|
hdr.pack(fill="x")
|
|
tk.Label(hdr, text="Daily Report Email Settings",
|
|
font=FONT_BOLD, bg=C["accent"],
|
|
fg=C["white"]).pack()
|
|
tk.Label(hdr,
|
|
text="Send an HTML completion report to recipients on a daily schedule.",
|
|
font=FONT_SMALL, bg=C["accent"],
|
|
fg=C["white"]).pack(pady=(2, 0))
|
|
|
|
# Form
|
|
form = tk.Frame(self, bg=C["bg"], padx=32, pady=16)
|
|
form.pack(fill="both", expand=True)
|
|
form.columnconfigure(1, weight=1)
|
|
|
|
self._fields = [] # widgets to enable/disable with the checkbox
|
|
|
|
def lbl(text, row):
|
|
tk.Label(form, text=text, bg=C["bg"], fg=C["text_dim"],
|
|
font=FONT_SMALL, anchor="w").grid(
|
|
row=row, column=0, sticky="w", padx=(0, 12), pady=5)
|
|
|
|
def entry(row, show=None, width=28):
|
|
var = tk.StringVar()
|
|
ent = tk.Entry(form, textvariable=var, width=width,
|
|
bg=C["surface2"], fg=C["text"],
|
|
insertbackground=C["text"],
|
|
relief="flat", font=FONT, show=show or "")
|
|
ent.grid(row=row, column=1, sticky="ew", ipady=6, pady=5)
|
|
self._fields.append(ent)
|
|
return var
|
|
|
|
# Row 0 — Enable toggle
|
|
lbl("Enable daily emails", 0)
|
|
self.enabled_var = tk.BooleanVar(value=False)
|
|
tk.Checkbutton(form, variable=self.enabled_var,
|
|
bg=C["bg"], activebackground=C["bg"],
|
|
selectcolor=C["surface2"],
|
|
command=self._toggle_fields).grid(
|
|
row=0, column=1, sticky="w", pady=6)
|
|
|
|
# Row 1 — SMTP Host
|
|
lbl("SMTP Host", 1)
|
|
self.smtp_host_var = entry(1)
|
|
|
|
# Row 2 — Security mode (replaces the old Use STARTTLS checkbox)
|
|
lbl("Security", 2)
|
|
self.security_var = tk.StringVar(value="starttls")
|
|
sec_frame = tk.Frame(form, bg=C["bg"])
|
|
sec_frame.grid(row=2, column=1, sticky="w", pady=5)
|
|
for key, (label, _) in _SECURITY_MODES.items():
|
|
rb = tk.Radiobutton(
|
|
sec_frame, text=label,
|
|
variable=self.security_var, value=key,
|
|
command=self._on_security_change,
|
|
bg=C["bg"], fg=C["text"],
|
|
activebackground=C["bg"], activeforeground=C["accent"],
|
|
selectcolor=C["surface2"], font=FONT_SMALL, cursor="hand2",
|
|
)
|
|
rb.pack(anchor="w")
|
|
self._fields.append(rb)
|
|
|
|
# Row 3 — SMTP Port (auto-filled when security mode changes)
|
|
lbl("SMTP Port", 3)
|
|
self.smtp_port_var = entry(3, width=8)
|
|
self.smtp_port_var.set("587")
|
|
|
|
# Row 4 — Username
|
|
lbl("SMTP Username", 4)
|
|
self.smtp_user_var = entry(4)
|
|
|
|
# Row 5 — Password
|
|
lbl("SMTP Password", 5)
|
|
self.smtp_pass_var = entry(5, show="•")
|
|
|
|
# Row 6 — Recipients
|
|
lbl("Recipients (comma-sep)", 6)
|
|
self.recipients_var = entry(6)
|
|
|
|
# Row 7 — Send time
|
|
lbl("Send Time (HH:MM)", 7)
|
|
self.send_time_var = entry(7, width=8)
|
|
self.send_time_var.set("18:00")
|
|
|
|
# Row 8 — Status line (multi-line for diagnostic output)
|
|
self._status_var = tk.StringVar()
|
|
self._status_lbl = tk.Label(
|
|
form, textvariable=self._status_var,
|
|
bg=C["bg"], fg=C["text_dim"],
|
|
font=FONT_SMALL, wraplength=460,
|
|
anchor="w", justify="left",
|
|
)
|
|
self._status_lbl.grid(row=8, column=0, columnspan=2,
|
|
sticky="ew", pady=(6, 0))
|
|
|
|
# Buttons
|
|
btn_row = tk.Frame(self, bg=C["bg"], padx=32, pady=14)
|
|
btn_row.pack(fill="x")
|
|
|
|
self._save_btn = tk.Button(
|
|
btn_row, text="Save Settings",
|
|
command=self._save,
|
|
bg=C["accent"], fg=C["white"],
|
|
activebackground=C["accent_hover"],
|
|
activeforeground=C["white"],
|
|
relief="flat", font=FONT_BOLD, cursor="hand2",
|
|
padx=14, pady=8)
|
|
self._save_btn.pack(side="right", padx=(8, 0))
|
|
|
|
self._send_btn = tk.Button(
|
|
btn_row, text="📧 Send Test Email",
|
|
command=self._send_test_email,
|
|
bg=C["success"], fg=C["white"],
|
|
activebackground="#3d9140",
|
|
activeforeground=C["white"],
|
|
relief="flat", font=FONT_SMALL, cursor="hand2",
|
|
padx=10, pady=8)
|
|
self._send_btn.pack(side="right", padx=(8, 0))
|
|
|
|
self._test_btn = tk.Button(
|
|
btn_row, text="🔌 Test Connection",
|
|
command=self._test,
|
|
bg=C["surface2"], fg=C["text"],
|
|
activebackground=C["surface2"],
|
|
activeforeground=C["accent"],
|
|
relief="flat", font=FONT_SMALL, cursor="hand2",
|
|
padx=10, pady=8)
|
|
self._test_btn.pack(side="right")
|
|
|
|
tk.Button(btn_row, text="Cancel",
|
|
command=self.destroy,
|
|
bg=C["surface"], fg=C["text_dim"],
|
|
activebackground=C["surface2"],
|
|
relief="flat", font=FONT_SMALL, cursor="hand2",
|
|
padx=10, pady=8).pack(side="left")
|
|
|
|
self._toggle_fields()
|
|
|
|
def _on_security_change(self):
|
|
"""Auto-fill the port when the security mode radio changes."""
|
|
mode = self.security_var.get()
|
|
self.smtp_port_var.set(str(_SECURITY_MODES[mode][1]))
|
|
|
|
def _toggle_fields(self):
|
|
state = "normal" if self.enabled_var.get() else "disabled"
|
|
for w in self._fields:
|
|
try:
|
|
w.config(state=state)
|
|
except Exception:
|
|
pass
|
|
|
|
def _load_existing(self):
|
|
from utils.scheduler import load_email_config
|
|
cfg = load_email_config()
|
|
if not cfg:
|
|
return
|
|
self.enabled_var.set(cfg.get("enabled", False))
|
|
self.smtp_host_var.set(cfg.get("smtp_host", ""))
|
|
self.smtp_port_var.set(str(cfg.get("smtp_port", 587)))
|
|
self.smtp_user_var.set(cfg.get("smtp_user", ""))
|
|
self.smtp_pass_var.set(cfg.get("smtp_password", ""))
|
|
# Load security mode; fall back from legacy use_tls bool
|
|
security = cfg.get("security", "")
|
|
if not security:
|
|
security = "starttls" if cfg.get("use_tls", True) else "none"
|
|
self.security_var.set(security if security in _SECURITY_MODES else "starttls")
|
|
self.recipients_var.set(", ".join(cfg.get("recipients", [])))
|
|
self.send_time_var.set(cfg.get("send_time", "18:00"))
|
|
self._toggle_fields()
|
|
|
|
def _get_fields(self):
|
|
host = self.smtp_host_var.get().strip()
|
|
port_str = self.smtp_port_var.get().strip()
|
|
user = self.smtp_user_var.get().strip()
|
|
password = self.smtp_pass_var.get()
|
|
security = self.security_var.get()
|
|
recipients = self.recipients_var.get().strip()
|
|
send_time = self.send_time_var.get().strip()
|
|
enabled = self.enabled_var.get()
|
|
|
|
if enabled and (not host or not user or not recipients):
|
|
self._set_status(
|
|
"Host, username, and recipients are required when enabled.", "danger")
|
|
return None
|
|
try:
|
|
port = int(port_str)
|
|
if not 1 <= port <= 65535:
|
|
raise ValueError
|
|
except ValueError:
|
|
self._set_status("Port must be a number between 1 and 65535.", "danger")
|
|
return None
|
|
try:
|
|
h, m = map(int, send_time.split(":"))
|
|
assert 0 <= h <= 23 and 0 <= m <= 59
|
|
except Exception:
|
|
self._set_status("Send time must be in HH:MM format (e.g. 18:00).", "danger")
|
|
return None
|
|
|
|
return enabled, host, port, user, password, security, recipients, send_time
|
|
|
|
def _set_status(self, msg, level="dim"):
|
|
colours = {"danger": COLOURS["danger"], "success": COLOURS["success"],
|
|
"dim": COLOURS["text_dim"], "warning": COLOURS["warning"]}
|
|
self._status_lbl.config(fg=colours.get(level, COLOURS["text_dim"]))
|
|
self._status_var.set(msg)
|
|
|
|
def _lock_buttons(self):
|
|
self._test_btn.config(state="disabled")
|
|
self._send_btn.config(state="disabled")
|
|
self._save_btn.config(state="disabled")
|
|
|
|
def _unlock_buttons(self):
|
|
self._test_btn.config(state="normal")
|
|
self._send_btn.config(state="normal")
|
|
self._save_btn.config(state="normal")
|
|
|
|
def _test(self):
|
|
result = self._get_fields()
|
|
if result is None:
|
|
return
|
|
enabled, host, port, user, password, security, recipients, send_time = result
|
|
if not host or not user:
|
|
self._set_status("Please fill in host and username before testing.", "warning")
|
|
return
|
|
|
|
self._set_status("Running diagnostics — please wait...", "dim")
|
|
self._lock_buttons()
|
|
|
|
def _run():
|
|
from utils.scheduler import test_smtp_connection
|
|
ok, msg = test_smtp_connection(host, port, user, password, security)
|
|
level = "success" if ok else "danger"
|
|
self.after(0, lambda: self._set_status(msg, level))
|
|
self.after(0, self._unlock_buttons)
|
|
|
|
threading.Thread(target=_run, daemon=True).start()
|
|
|
|
def _send_test_email(self):
|
|
result = self._get_fields()
|
|
if result is None:
|
|
return
|
|
enabled, host, port, user, password, security, recipients, send_time = result
|
|
|
|
recip_list = [r.strip() for r in recipients.split(",") if r.strip()]
|
|
if not recip_list:
|
|
self._set_status("Please enter at least one recipient.", "danger")
|
|
return
|
|
if not host or not user:
|
|
self._set_status("Please fill in host and username before sending.", "warning")
|
|
return
|
|
|
|
self._set_status("Sending test email — please wait...", "dim")
|
|
self._lock_buttons()
|
|
|
|
def _run():
|
|
from utils.scheduler import send_test_email
|
|
ok, msg = send_test_email(host, port, user, password,
|
|
security, recip_list)
|
|
level = "success" if ok else "danger"
|
|
self.after(0, lambda: self._set_status(msg, level))
|
|
self.after(0, self._unlock_buttons)
|
|
|
|
threading.Thread(target=_run, daemon=True).start()
|
|
|
|
def _save(self):
|
|
result = self._get_fields()
|
|
if result is None:
|
|
return
|
|
enabled, host, port, user, password, security, recipients, send_time = result
|
|
|
|
from utils.scheduler import save_email_config
|
|
save_email_config(enabled, host, port, user, password, security,
|
|
recipients, send_time)
|
|
from models import log_action
|
|
log_action(self.current_user["id"], "UPDATE_EMAIL_SETTINGS", "config",
|
|
None, f"Email reports {'enabled' if enabled else 'disabled'} "
|
|
f"security={security}.")
|
|
show_info("Email settings saved successfully.")
|
|
logger.info(f"Email settings saved by {self.current_user['username']}.")
|
|
self.destroy()
|