"""Notifier interface + console (default) and SMTP backends (spec §10). Console backend writes to stdout/log and persists a notifications row; real SMTP only when env-configured. SMS is an off-by-default adapter (paid + network — do not enable without confirmation). """ from __future__ import annotations import logging import smtplib from abc import ABC, abstractmethod from datetime import datetime, timezone from email.message import EmailMessage from sqlalchemy.orm import Session from ..config import settings from ..models import Notification from ..models.base import NotificationChannel, NotificationStatus logger = logging.getLogger("pawtrace.notify") class Notifier(ABC): @abstractmethod def send(self, *, to: str, subject: str, body: str) -> bool: """Deliver a message; return True on success.""" class ConsoleNotifier(Notifier): def send(self, *, to: str, subject: str, body: str) -> bool: logger.info("EMAIL -> %s | %s\n%s", to, subject, body) print(f"\n[EMAIL] to={to} subject={subject!r}\n{body}\n") return True class SMTPNotifier(Notifier): # pragma: no cover - requires a live/dev SMTP server def send(self, *, to: str, subject: str, body: str) -> bool: msg = EmailMessage() msg["From"] = settings.smtp_from msg["To"] = to msg["Subject"] = subject msg.set_content(body) try: with smtplib.SMTP(settings.smtp_host, settings.smtp_port, timeout=10) as s: if settings.smtp_user: s.starttls() s.login(settings.smtp_user, settings.smtp_password) s.send_message(msg) return True except Exception: # noqa: BLE001 logger.exception("SMTP send failed") return False def get_notifier() -> Notifier: if settings.notifier == "smtp" and settings.smtp_host: return SMTPNotifier() return ConsoleNotifier() def send_notification( db: Session, *, user_id: int | None, to: str, subject: str, body: str, case_id: int | None = None, match_id: int | None = None, ) -> Notification: """Send via the configured Notifier and persist a notifications row (spec §7.8).""" notif = Notification( user_id=user_id, case_id=case_id, match_id=match_id, channel=NotificationChannel.email, to_address=to, payload=f"{subject}\n\n{body}", status=NotificationStatus.queued, ) db.add(notif) db.flush() ok = get_notifier().send(to=to, subject=subject, body=body) notif.status = NotificationStatus.sent if ok else NotificationStatus.failed if ok: notif.sent_at = datetime.now(timezone.utc) db.flush() return notif