refactor: notifier - use ns8-sendmail instead of smtplib, drop smtp_config dependency
This commit is contained in:
@@ -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'<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">
|
||||
<div style="background:{color};color:#fff;padding:16px 20px;border-radius:6px 6px 0 0">
|
||||
<b>NS8 Backup Monitor — {label}: {outcome}</b><br>
|
||||
@@ -138,11 +142,9 @@ def _build_html(correlation: dict, repo_status: Optional[dict], smtp_source: str
|
||||
<tbody>{rows}</tbody>
|
||||
</table>
|
||||
{repo_section}
|
||||
<p style="color:#aaa;font-size:11px;margin-top:24px">Sent via {smtp_source}</p>
|
||||
</div>
|
||||
</body></html>
|
||||
"""
|
||||
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}")
|
||||
|
||||
Reference in New Issue
Block a user