""" Email alerter — sends notifications when data sources go stale. Activated by setting env vars: SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASSWORD, ALERT_EMAIL_TO. Respects a per-source cooldown to avoid spamming. """ from __future__ import annotations import logging import os import smtplib import time from email.mime.text import MIMEText from typing import Any from config.settings import ALERT_COOLDOWN_MIN log = logging.getLogger("solarwine.alerter") class EmailAlerter: """Sends email alerts when data sources are in 'red' status.""" def __init__(self): self._last_alert: dict[str, float] = {} # source_name -> epoch of last alert self._smtp_host = os.environ.get("SMTP_HOST", "") self._smtp_port = int(os.environ.get("SMTP_PORT", "587")) self._smtp_user = os.environ.get("SMTP_USER", "") self._smtp_password = os.environ.get("SMTP_PASSWORD", "") self._alert_to = os.environ.get("ALERT_EMAIL_TO", "") self._alert_from = os.environ.get("ALERT_EMAIL_FROM", self._smtp_user) @property def enabled(self) -> bool: return bool(self._smtp_host and self._alert_to) def check_and_alert(self, status: dict[str, Any]) -> list[str]: """Check status and send alerts for red sources. Returns list of alerted sources.""" if not self.enabled: return [] alerted: list[str] = [] sources = status.get("sources", {}) for source_name, info in sources.items(): if info.get("status") != "red": # Source recovered — clear cooldown so next outage triggers immediately self._last_alert.pop(source_name, None) continue # Check cooldown now = time.time() last = self._last_alert.get(source_name, 0) if (now - last) < ALERT_COOLDOWN_MIN * 60: continue # Send alert message = info.get("message", f"{source_name} is down") age = info.get("age_minutes") subject = f"[SolarWine] Data flow alert: {source_name}" age_line = f"Age: {age:.0f} min\n" if age is not None else "" body = ( f"Data source: {source_name}\n" f"Status: RED\n" f"{age_line}" f"Detail: {message}\n" f"\nChecked at: {status.get('checked_at', 'unknown')}\n" f"Overall system status: {status.get('overall', 'unknown')}\n" f"\n---\nSolarWine Data Flow Monitor" ) if self._send_email(subject, body): self._last_alert[source_name] = now alerted.append(source_name) return alerted def _send_email(self, subject: str, body: str) -> bool: """Send an email via SMTP. Returns True on success.""" try: msg = MIMEText(body, "plain", "utf-8") msg["Subject"] = subject msg["From"] = self._alert_from msg["To"] = self._alert_to with smtplib.SMTP(self._smtp_host, self._smtp_port, timeout=10) as server: server.starttls() if self._smtp_user and self._smtp_password: server.login(self._smtp_user, self._smtp_password) server.sendmail(self._alert_from, self._alert_to.split(","), msg.as_string()) log.info("Alert email sent: %s → %s", subject, self._alert_to) return True except Exception as exc: log.error("Failed to send alert email: %s", exc) return False