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}")