from __future__ import annotations import asyncio from datetime import datetime, timedelta, timezone from email.message import EmailMessage import smtplib import ssl from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.models import AlertNotificationEvent, EmailNotificationSettings, TargetOwner, User from app.schemas.alert import AlertStatusResponse from app.services.crypto import decrypt_secret _NOTIFICATION_COOLDOWN = timedelta(minutes=30) async def _smtp_send( host: str, port: int, username: str | None, password: str | None, from_email: str, recipient: str, subject: str, body: str, use_starttls: bool, use_ssl: bool, ) -> None: def _send() -> None: message = EmailMessage() message["From"] = from_email message["To"] = recipient message["Subject"] = subject message.set_content(body) if use_ssl: with smtplib.SMTP_SSL(host, port, timeout=10, context=ssl.create_default_context()) as smtp: if username: smtp.login(username, password or "") smtp.send_message(message) return with smtplib.SMTP(host, port, timeout=10) as smtp: if use_starttls: smtp.starttls(context=ssl.create_default_context()) if username: smtp.login(username, password or "") smtp.send_message(message) await asyncio.to_thread(_send) def _render_subject(item) -> str: sev = item.severity.upper() return f"[NexaPG][{sev}] {item.target_name} - {item.name}" def _render_body(item) -> str: lines = [ f"Severity: {item.severity}", f"Target: {item.target_name} (id={item.target_id})", f"Alert: {item.name}", f"Category: {item.category}", f"Checked At: {item.checked_at.isoformat()}", "", f"Description: {item.description}", f"Message: {item.message}", ] if item.value is not None: lines.append(f"Current Value: {item.value}") if item.warning_threshold is not None: lines.append(f"Warning Threshold: {item.warning_threshold}") if item.alert_threshold is not None: lines.append(f"Alert Threshold: {item.alert_threshold}") lines.append("") lines.append(f"Alert Key: {item.alert_key}") lines.append("Sent by NexaPG notification service.") return "\n".join(lines) async def process_target_owner_notifications(db: AsyncSession, status: AlertStatusResponse) -> None: settings = await db.scalar(select(EmailNotificationSettings).limit(1)) if not settings or not settings.enabled: return if not settings.smtp_host or not settings.from_email: return password = decrypt_secret(settings.encrypted_smtp_password) if settings.encrypted_smtp_password else None now = datetime.now(timezone.utc) active_items = status.alerts + status.warnings if not active_items: return target_ids = sorted({item.target_id for item in active_items}) owner_rows = ( await db.execute( select(TargetOwner.target_id, User.email) .join(User, User.id == TargetOwner.user_id) .where(TargetOwner.target_id.in_(target_ids)) ) ).all() owners_map: dict[int, set[str]] = {} for target_id, email in owner_rows: owners_map.setdefault(target_id, set()).add(email) existing_rows = ( await db.scalars( select(AlertNotificationEvent).where( AlertNotificationEvent.target_id.in_(target_ids) ) ) ).all() event_map = {(row.alert_key, row.target_id, row.severity): row for row in existing_rows} for item in active_items: recipients = sorted(owners_map.get(item.target_id, set())) if not recipients: continue key = (item.alert_key, item.target_id, item.severity) existing = event_map.get(key) should_send = existing is None or (now - existing.last_sent_at) >= _NOTIFICATION_COOLDOWN if should_send: subject = _render_subject(item) body = _render_body(item) for recipient in recipients: try: await _smtp_send( host=settings.smtp_host, port=settings.smtp_port, username=settings.smtp_username, password=password, from_email=settings.from_email, recipient=recipient, subject=subject, body=body, use_starttls=settings.use_starttls, use_ssl=settings.use_ssl, ) except Exception: continue if existing: existing.last_seen_at = now if should_send: existing.last_sent_at = now else: db.add( AlertNotificationEvent( alert_key=item.alert_key, target_id=item.target_id, severity=item.severity, last_seen_at=now, last_sent_at=now if should_send else now, ) ) await db.commit()