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
|
#!/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 — {label}: {outcome}</b><br>
|
<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>
|
<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}")
|
||||||
|
|||||||
Reference in New Issue
Block a user