diff --git a/ns8_backup_monitor/notifier.py b/ns8_backup_monitor/notifier.py index 8c4a8b0..1bb2844 100644 --- a/ns8_backup_monitor/notifier.py +++ b/ns8_backup_monitor/notifier.py @@ -1,27 +1,22 @@ #!/usr/bin/env python3 """ -notifier.py - Sends a single classified notification email. +notifier.py - Sends a single classified notification email via ns8-sendmail. -Formats a human-readable HTML + text email based on: - - correlation outcome (SUCCESS / PARTIAL / REPO_FAILURE) - - per-module statuses - - repository check results (if run) +Delivery strategy: + 1. ns8-sendmail -t (uses NS8 configured relay - preferred) + 2. sendmail -t (fallback if ns8-sendmail not found) -SMTP configuration is resolved via smtp_config.resolve_smtp_config(): - 1. NS8 cluster Redis (cluster/mail_settings) - same relay used by NS8 itself - 2. config.yml [smtp] section - 3. localhost:25 fallback +No SMTP configuration needed: ns8-sendmail already knows the relay. """ import logging -import smtplib +import shutil +import subprocess from datetime import datetime, timezone from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import Optional -from .smtp_config import resolve_smtp_config - log = logging.getLogger(__name__) OUTCOME_LABEL = { @@ -37,6 +32,15 @@ OUTCOME_COLOR = { } +def _sendmail_binary() -> str: + """Return the path to ns8-sendmail, falling back to sendmail.""" + for cmd in ("ns8-sendmail", "sendmail"): + path = shutil.which(cmd) + if path: + return path + raise FileNotFoundError("Neither ns8-sendmail nor sendmail found in PATH") + + def _build_text(correlation: dict, repo_status: Optional[dict]) -> str: outcome = correlation["outcome"] lines = [ @@ -67,7 +71,7 @@ def _build_text(correlation: dict, repo_status: Optional[dict]) -> str: return "\n".join(lines) -def _build_html(correlation: dict, repo_status: Optional[dict], smtp_source: str) -> str: +def _build_html(correlation: dict, repo_status: Optional[dict]) -> str: outcome = correlation["outcome"] color = OUTCOME_COLOR[outcome] label = OUTCOME_LABEL[outcome] @@ -115,7 +119,7 @@ def _build_html(correlation: dict, repo_status: Optional[dict], smtp_source: str if repo_status.get("note"): repo_section += f'

{repo_status["note"]}

' - html = f""" + return f"""
NS8 Backup Monitor — {label}: {outcome}
@@ -138,11 +142,9 @@ def _build_html(correlation: dict, repo_status: Optional[dict], smtp_source: str {rows} {repo_section} -

Sent via {smtp_source}

""" - return html def send_notification( @@ -154,45 +156,49 @@ def send_notification( outcome = correlation["outcome"] mail_cfg = config.get("mail", {}) subject_prefix = mail_cfg.get("subject_prefix", "[NS8 Backup]") - subject = f"{subject_prefix} {OUTCOME_LABEL[outcome]}: {outcome} - {datetime.now(timezone.utc).strftime('%Y-%m-%d')}" + subject = ( + f"{subject_prefix} {OUTCOME_LABEL[outcome]}: {outcome} - " + f"{datetime.now(timezone.utc).strftime('%Y-%m-%d')}" + ) mail_to = mail_cfg.get("to", []) if not mail_to: - log.error("No mail.to recipients configured in config.yml - cannot send notification") + log.error("No mail.to recipients configured - cannot send notification") return - # Resolve SMTP: NS8 Redis relay -> config.yml -> localhost:25 - smtp_cfg, mail_from = resolve_smtp_config(config) - smtp_source = f"{smtp_cfg['host']}:{smtp_cfg['port']}" + mail_from = mail_cfg.get("from", "") text_body = _build_text(correlation, repo_status) - html_body = _build_html(correlation, repo_status, smtp_source) + html_body = _build_html(correlation, repo_status) msg = MIMEMultipart("alternative") msg["Subject"] = subject - msg["From"] = mail_from msg["To"] = ", ".join(mail_to) + if mail_from: + msg["From"] = mail_from msg.attach(MIMEText(text_body, "plain")) msg.attach(MIMEText(html_body, "html")) - host = smtp_cfg.get("host", "localhost") - port = smtp_cfg.get("port", 25) - use_tls = smtp_cfg.get("use_tls", False) - use_starttls = smtp_cfg.get("use_starttls", False) - username = smtp_cfg.get("username", "") - password = smtp_cfg.get("password", "") + try: + binary = _sendmail_binary() + except FileNotFoundError as e: + log.error(str(e)) + return try: - if use_tls: - smtp = smtplib.SMTP_SSL(host, port) + proc = subprocess.run( + [binary, "-t"], + input=msg.as_bytes(), + capture_output=True, + timeout=30, + ) + if proc.returncode != 0: + log.error( + f"ns8-sendmail exited {proc.returncode}: {proc.stderr.decode().strip()}" + ) else: - smtp = smtplib.SMTP(host, port) - if use_starttls: - smtp.starttls() - if username and password: - smtp.login(username, password) - smtp.sendmail(mail_from, mail_to, msg.as_string()) - smtp.quit() - log.info(f"Notification sent: {subject} -> {mail_to} (via {smtp_source})") + log.info(f"Notification sent via {binary}: {subject} -> {mail_to}") + except subprocess.TimeoutExpired: + log.error(f"ns8-sendmail timed out after 30s") except Exception as e: - log.error(f"Failed to send notification via {smtp_source}: {e}") + log.error(f"Failed to invoke {binary}: {e}")