9b177c145b
Migration 006, email config DB/env, smtplib sender, settings API, Settings UI section, and dispatch on event/problem ingest. Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
4.4 KiB
Python
136 lines
4.4 KiB
Python
"""SMTP email notifications from SAC."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import re
|
|
import smtplib
|
|
from email.mime.text import MIMEText
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.models import Event, Problem
|
|
from app.services.notification_settings import EmailConfig, get_effective_email_config
|
|
from app.services.telegram_notify import SEVERITY_ORDER
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class EmailNotConfiguredError(Exception):
|
|
"""Email disabled or missing SMTP settings."""
|
|
|
|
|
|
class EmailSendError(Exception):
|
|
pass
|
|
|
|
|
|
def _severity_value(value: str) -> int:
|
|
return SEVERITY_ORDER.get(value, 0)
|
|
|
|
|
|
def _should_notify_severity(severity: str, *, config: EmailConfig) -> bool:
|
|
return _severity_value(severity) >= _severity_value(config.min_severity)
|
|
|
|
|
|
def parse_mail_recipients(mail_to: str) -> list[str]:
|
|
return [part.strip() for part in re.split(r"[,;]", mail_to) if part.strip()]
|
|
|
|
|
|
def send_email_message(
|
|
subject: str,
|
|
body: str,
|
|
*,
|
|
config: EmailConfig | None = None,
|
|
force: bool = False,
|
|
) -> None:
|
|
cfg = config or get_effective_email_config()
|
|
if not force and not cfg.active:
|
|
return
|
|
if not cfg.configured:
|
|
if force:
|
|
raise EmailNotConfiguredError("SMTP: не задан host, from или to")
|
|
return
|
|
|
|
recipients = parse_mail_recipients(cfg.mail_to)
|
|
if not recipients:
|
|
if force:
|
|
raise EmailNotConfiguredError("SMTP: список получателей пуст")
|
|
return
|
|
|
|
msg = MIMEText(body, "plain", "utf-8")
|
|
msg["Subject"] = subject
|
|
msg["From"] = cfg.mail_from
|
|
msg["To"] = ", ".join(recipients)
|
|
|
|
try:
|
|
if cfg.smtp_ssl:
|
|
server: smtplib.SMTP = smtplib.SMTP_SSL(cfg.smtp_host, cfg.smtp_port, timeout=12)
|
|
else:
|
|
server = smtplib.SMTP(cfg.smtp_host, cfg.smtp_port, timeout=12)
|
|
with server:
|
|
server.ehlo()
|
|
if cfg.smtp_starttls and not cfg.smtp_ssl:
|
|
server.starttls()
|
|
server.ehlo()
|
|
if cfg.smtp_user:
|
|
server.login(cfg.smtp_user, cfg.smtp_password)
|
|
server.sendmail(cfg.mail_from, recipients, msg.as_string())
|
|
except Exception as exc:
|
|
logger.exception("smtp send failed")
|
|
raise EmailSendError(str(exc)) from exc
|
|
|
|
|
|
def send_email_test_message(*, config: EmailConfig | None = None) -> None:
|
|
cfg = config or get_effective_email_config()
|
|
if not cfg.enabled:
|
|
raise EmailNotConfiguredError("Email отключён (enabled=false)")
|
|
if not cfg.configured:
|
|
raise EmailNotConfiguredError("Не задан SMTP host, from или to")
|
|
send_email_message(
|
|
"SAC: тестовое письмо",
|
|
"Тестовое сообщение Security Alert Center.\nКанал SMTP для оповещений работает.",
|
|
config=cfg,
|
|
force=True,
|
|
)
|
|
|
|
|
|
def notify_event(event: Event, *, db: Session | None = None) -> None:
|
|
cfg = get_effective_email_config(db)
|
|
if not cfg.active or not _should_notify_severity(event.severity, config=cfg):
|
|
return
|
|
host = event.host.hostname if event.host else "unknown"
|
|
body = (
|
|
f"SAC событие\n"
|
|
f"Host: {host}\n"
|
|
f"Severity: {event.severity}\n"
|
|
f"Type: {event.type}\n"
|
|
f"Title: {event.title}\n"
|
|
f"Summary: {event.summary}"
|
|
)
|
|
try:
|
|
send_email_message(f"SAC: {event.type} ({event.severity})", body, config=cfg)
|
|
except EmailSendError:
|
|
logger.exception("notify_event email failed event_id=%s", event.event_id)
|
|
|
|
|
|
def notify_problem(problem: Problem, event: Event | None = None, *, db: Session | None = None) -> None:
|
|
cfg = get_effective_email_config(db)
|
|
if not cfg.active or not _should_notify_severity(problem.severity, config=cfg):
|
|
return
|
|
host = problem.host.hostname if problem.host else "unknown"
|
|
related = ""
|
|
if event is not None:
|
|
related = f"\nEvent: {event.type} ({event.severity})"
|
|
body = (
|
|
f"SAC Problem opened\n"
|
|
f"Host: {host}\n"
|
|
f"Severity: {problem.severity}\n"
|
|
f"Rule: {problem.rule_id or '-'}\n"
|
|
f"Title: {problem.title}\n"
|
|
f"Summary: {problem.summary}{related}"
|
|
)
|
|
try:
|
|
send_email_message(f"SAC Problem: {problem.title}", body, config=cfg)
|
|
except EmailSendError:
|
|
logger.exception("notify_problem email failed problem_id=%s", problem.id)
|