""" views/email_settings_view.py — SMTP / scheduled report configuration dialog. Admin-only. Persists to the app_settings database table 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()