""" 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") 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 = 500, 560 x = (self.winfo_screenwidth() - w) // 2 y = (self.winfo_screenheight() - h) // 2 self.geometry(f"{w}x{h}+{x}+{y}") def _build_ui(self): # Header hdr = tk.Frame(self, bg=COLOURS["accent"], pady=14) hdr.pack(fill="x") tk.Label(hdr, text="Daily Report Email Settings", font=FONT_BOLD, bg=COLOURS["accent"], fg=COLOURS["white"]).pack() tk.Label(hdr, text="Send an HTML completion report to recipients on a daily schedule.", font=FONT_SMALL, bg=COLOURS["accent"], fg=COLOURS["white"]).pack(pady=(2, 0)) # Form form = tk.Frame(self, bg=COLOURS["bg"], padx=32, pady=16) form.pack(fill="both", expand=True) form.columnconfigure(1, weight=1) # Enable toggle tk.Label(form, text="Enable daily emails", bg=COLOURS["bg"], fg=COLOURS["text_dim"], font=FONT_SMALL).grid( row=0, column=0, sticky="w", padx=(0, 12), pady=6) self.enabled_var = tk.BooleanVar(value=False) tk.Checkbutton(form, variable=self.enabled_var, bg=COLOURS["bg"], activebackground=COLOURS["bg"], selectcolor=COLOURS["surface2"], command=self._toggle_fields).grid( row=0, column=1, sticky="w", pady=6) def field(label, row, show=None, width=28): tk.Label(form, text=label, bg=COLOURS["bg"], fg=COLOURS["text_dim"], font=FONT_SMALL, anchor="w").grid(row=row, column=0, sticky="w", padx=(0, 12), pady=5) var = tk.StringVar() ent = tk.Entry(form, textvariable=var, width=width, bg=COLOURS["surface2"], fg=COLOURS["text"], insertbackground=COLOURS["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 self._fields = [] self.smtp_host_var = field("SMTP Host", 1) self.smtp_port_var = field("SMTP Port", 2, width=8) self.smtp_user_var = field("SMTP Username", 3) self.smtp_pass_var = field("SMTP Password", 4, show="•") # TLS toggle tk.Label(form, text="Use STARTTLS", bg=COLOURS["bg"], fg=COLOURS["text_dim"], font=FONT_SMALL).grid( row=5, column=0, sticky="w", padx=(0, 12), pady=5) self.tls_var = tk.BooleanVar(value=True) tls_cb = tk.Checkbutton(form, variable=self.tls_var, bg=COLOURS["bg"], activebackground=COLOURS["bg"], selectcolor=COLOURS["surface2"]) tls_cb.grid(row=5, column=1, sticky="w", pady=5) self._fields.append(tls_cb) self.recipients_var = field("Recipients (comma-sep)", 6) self.send_time_var = field("Send Time (HH:MM)", 7, width=8) self.smtp_port_var.set("587") self.send_time_var.set("18:00") # Status line self._status_var = tk.StringVar() self._status_lbl = tk.Label(form, textvariable=self._status_var, bg=COLOURS["bg"], fg=COLOURS["text_dim"], font=FONT_SMALL, wraplength=380, anchor="w") self._status_lbl.grid(row=8, column=0, columnspan=2, sticky="ew", pady=(6, 0)) # Buttons btn_row = tk.Frame(self, bg=COLOURS["bg"], padx=32, pady=14) btn_row.pack(fill="x") self._save_btn = tk.Button( btn_row, text="Save Settings", command=self._save, bg=COLOURS["accent"], fg=COLOURS["white"], activebackground=COLOURS["accent_hover"], activeforeground=COLOURS["white"], relief="flat", font=FONT_BOLD, cursor="hand2", padx=14, pady=8) self._save_btn.pack(side="right", padx=(8, 0)) self._test_btn = tk.Button( btn_row, text="Test Connection", command=self._test, bg=COLOURS["surface2"], fg=COLOURS["text"], activebackground=COLOURS["surface2"], activeforeground=COLOURS["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=COLOURS["surface"], fg=COLOURS["text_dim"], activebackground=COLOURS["surface2"], relief="flat", font=FONT_SMALL, cursor="hand2", padx=10, pady=8).pack(side="left") self._toggle_fields() def _toggle_fields(self): """Enable/disable SMTP fields based on the enabled checkbox.""" 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", "")) self.tls_var.set(cfg.get("use_tls", True)) 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() use_tls = self.tls_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 # Validate send_time 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, use_tls, 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 _test(self): result = self._get_fields() if result is None: return enabled, host, port, user, password, use_tls, recipients, send_time = result self._set_status("Testing SMTP connection...", "dim") self._test_btn.config(state="disabled") self._save_btn.config(state="disabled") def _run(): from utils.scheduler import test_smtp_connection ok, msg = test_smtp_connection(host, port, user, password, use_tls) level = "success" if ok else "danger" self.after(0, lambda: self._set_status(msg, level)) self.after(0, lambda: self._test_btn.config(state="normal")) self.after(0, lambda: self._save_btn.config(state="normal")) threading.Thread(target=_run, daemon=True).start() def _save(self): result = self._get_fields() if result is None: return enabled, host, port, user, password, use_tls, recipients, send_time = result from utils.scheduler import save_email_config save_email_config(enabled, host, port, user, password, use_tls, 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'}.") show_info("Email settings saved successfully.") logger.info(f"Email settings saved by {self.current_user['username']}.") self.destroy()