#!/usr/bin/env python3 """ notifier.py - Sends a single classified notification email via ns8-sendmail. Delivery strategy: 1. ns8-sendmail -t (uses NS8 configured relay - preferred) 2. sendmail -t (fallback if ns8-sendmail not found) No SMTP configuration needed: ns8-sendmail already knows the relay. """ import logging 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 log = logging.getLogger(__name__) OUTCOME_LABEL = { "SUCCESS": "OK", "PARTIAL": "WARNING", "REPO_FAILURE": "CRITICAL", } OUTCOME_COLOR = { "SUCCESS": "#2e7d32", "PARTIAL": "#e65100", "REPO_FAILURE": "#b71c1c", } 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 = [ f"NS8 Backup Monitor - {OUTCOME_LABEL[outcome]}: {outcome}", f"Time: {datetime.now(timezone.utc).isoformat()}", f"Plans checked: {', '.join(correlation.get('backup_ids', []))}", f"Modules: {correlation['succeeded']} OK / {correlation['failed']} FAILED / {correlation['total']} total", "", ] if correlation["failed_modules"]: lines.append("Failed modules:") for m in correlation["failed_modules"]: lines.append(f" - [{m['module_id']}] backup_id={m['backup_id']}: {m.get('error', 'unknown error')}") lines.append("") if repo_status: lines.append("Repository check:") for dest in repo_status.get("destinations", []): lines.append(f" - [{dest['repo_id']}] {dest['status']}: {dest.get('error', '')}") if repo_status.get("note"): lines.append(f" NOTE: {repo_status['note']}") lines.append("") if correlation.get("note"): lines.append(f"Note: {correlation['note']}") return "\n".join(lines) def _build_html(correlation: dict, repo_status: Optional[dict]) -> str: outcome = correlation["outcome"] color = OUTCOME_COLOR[outcome] label = OUTCOME_LABEL[outcome] ts = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC") plan_ids = ", ".join(correlation.get("backup_ids", [])) or "N/A" rows = "" for m in correlation.get("modules", []): bg = "#e8f5e9" if m["result"] == "success" else "#ffebee" icon = "✓" if m["result"] == "success" else "✗" rows += ( f'
| Repo ID | Status | Detail |
|---|
{repo_status["note"]}
' return f"""{correlation['succeeded']} module(s) OK | {correlation['failed']} FAILED | {correlation['total']} total
| Module | Backup ID | Timestamp | Error |
|---|