"""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_severity import severity_meets_minimum from app.services.notification_settings import EmailConfig, get_effective_email_config logger = logging.getLogger(__name__) class EmailNotConfiguredError(Exception): """Email disabled or missing SMTP settings.""" class EmailSendError(Exception): pass 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, apply_policy_gate: bool = True) -> None: cfg = get_effective_email_config(db) if not cfg.active: return if apply_policy_gate and not severity_meets_minimum(event.severity, cfg.min_severity): 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, apply_policy_gate: bool = True) -> None: cfg = get_effective_email_config(db) if not cfg.active: return if apply_policy_gate and not severity_meets_minimum(problem.severity, cfg.min_severity): 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)