refactor: notifier - use ns8-sendmail instead of smtplib, drop smtp_config dependency

This commit is contained in:
2026-05-18 20:06:01 +00:00
parent 661756a466
commit 8ce60efd66
+46 -40
View File
@@ -1,27 +1,22 @@
#!/usr/bin/env python3 #!/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: Delivery strategy:
- correlation outcome (SUCCESS / PARTIAL / REPO_FAILURE) 1. ns8-sendmail -t (uses NS8 configured relay - preferred)
- per-module statuses 2. sendmail -t (fallback if ns8-sendmail not found)
- repository check results (if run)
SMTP configuration is resolved via smtp_config.resolve_smtp_config(): No SMTP configuration needed: ns8-sendmail already knows the relay.
1. NS8 cluster Redis (cluster/mail_settings) - same relay used by NS8 itself
2. config.yml [smtp] section
3. localhost:25 fallback
""" """
import logging import logging
import smtplib import shutil
import subprocess
from datetime import datetime, timezone from datetime import datetime, timezone
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText from email.mime.text import MIMEText
from typing import Optional from typing import Optional
from .smtp_config import resolve_smtp_config
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
OUTCOME_LABEL = { 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: def _build_text(correlation: dict, repo_status: Optional[dict]) -> str:
outcome = correlation["outcome"] outcome = correlation["outcome"]
lines = [ lines = [
@@ -67,7 +71,7 @@ def _build_text(correlation: dict, repo_status: Optional[dict]) -> str:
return "\n".join(lines) 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"] outcome = correlation["outcome"]
color = OUTCOME_COLOR[outcome] color = OUTCOME_COLOR[outcome]
label = OUTCOME_LABEL[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"): if repo_status.get("note"):
repo_section += f'<p style="color:#777;font-size:12px">{repo_status["note"]}</p>' repo_section += f'<p style="color:#777;font-size:12px">{repo_status["note"]}</p>'
html = f""" return f"""
<html><body style="font-family:monospace;font-size:14px;max-width:800px;margin:auto"> <html><body style="font-family:monospace;font-size:14px;max-width:800px;margin:auto">
<div style="background:{color};color:#fff;padding:16px 20px;border-radius:6px 6px 0 0"> <div style="background:{color};color:#fff;padding:16px 20px;border-radius:6px 6px 0 0">
<b>NS8 Backup Monitor &mdash; {label}: {outcome}</b><br> <b>NS8 Backup Monitor &mdash; {label}: {outcome}</b><br>
@@ -138,11 +142,9 @@ def _build_html(correlation: dict, repo_status: Optional[dict], smtp_source: str
<tbody>{rows}</tbody> <tbody>{rows}</tbody>
</table> </table>
{repo_section} {repo_section}
<p style="color:#aaa;font-size:11px;margin-top:24px">Sent via {smtp_source}</p>
</div> </div>
</body></html> </body></html>
""" """
return html
def send_notification( def send_notification(
@@ -154,45 +156,49 @@ def send_notification(
outcome = correlation["outcome"] outcome = correlation["outcome"]
mail_cfg = config.get("mail", {}) mail_cfg = config.get("mail", {})
subject_prefix = mail_cfg.get("subject_prefix", "[NS8 Backup]") 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", []) mail_to = mail_cfg.get("to", [])
if not mail_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 return
# Resolve SMTP: NS8 Redis relay -> config.yml -> localhost:25 mail_from = mail_cfg.get("from", "")
smtp_cfg, mail_from = resolve_smtp_config(config)
smtp_source = f"{smtp_cfg['host']}:{smtp_cfg['port']}"
text_body = _build_text(correlation, repo_status) 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 = MIMEMultipart("alternative")
msg["Subject"] = subject msg["Subject"] = subject
msg["From"] = mail_from
msg["To"] = ", ".join(mail_to) msg["To"] = ", ".join(mail_to)
if mail_from:
msg["From"] = mail_from
msg.attach(MIMEText(text_body, "plain")) msg.attach(MIMEText(text_body, "plain"))
msg.attach(MIMEText(html_body, "html")) msg.attach(MIMEText(html_body, "html"))
host = smtp_cfg.get("host", "localhost") try:
port = smtp_cfg.get("port", 25) binary = _sendmail_binary()
use_tls = smtp_cfg.get("use_tls", False) except FileNotFoundError as e:
use_starttls = smtp_cfg.get("use_starttls", False) log.error(str(e))
username = smtp_cfg.get("username", "") return
password = smtp_cfg.get("password", "")
try: try:
if use_tls: proc = subprocess.run(
smtp = smtplib.SMTP_SSL(host, port) [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: else:
smtp = smtplib.SMTP(host, port) log.info(f"Notification sent via {binary}: {subject} -> {mail_to}")
if use_starttls: except subprocess.TimeoutExpired:
smtp.starttls() log.error(f"ns8-sendmail timed out after 30s")
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})")
except Exception as e: except Exception as e:
log.error(f"Failed to send notification via {smtp_source}: {e}") log.error(f"Failed to invoke {binary}: {e}")