""" Email Utility - ServiceManagerWeb Envío directo de emails desde el backend para flujos críticos (reseteo de contraseña, verificación) sin depender de Celery. """ import asyncio import smtplib import ssl from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from typing import Optional import structlog from app.core.config import get_settings settings = get_settings() logger = structlog.get_logger(__name__) def _send_smtp_sync( to_email: str, subject: str, html_content: str, text_content: Optional[str] = None, ) -> None: """ Enviar email de forma síncrona vía SMTP. Llamar desde asyncio.to_thread para no bloquear el event loop. """ msg = MIMEMultipart("alternative") msg["Subject"] = subject msg["From"] = f"{settings.DEFAULT_FROM_NAME} <{settings.DEFAULT_FROM_EMAIL}>" msg["To"] = to_email if text_content: msg.attach(MIMEText(text_content, "plain", "utf-8")) msg.attach(MIMEText(html_content, "html", "utf-8")) if settings.SMTP_USE_SSL: context = ssl.create_default_context() with smtplib.SMTP_SSL(settings.SMTP_HOST, settings.SMTP_PORT, context=context) as server: if settings.SMTP_USER and settings.SMTP_PASSWORD: server.login(settings.SMTP_USER, settings.SMTP_PASSWORD) server.sendmail(settings.DEFAULT_FROM_EMAIL, to_email, msg.as_string()) else: with smtplib.SMTP(settings.SMTP_HOST, settings.SMTP_PORT) as server: if settings.SMTP_USE_TLS: server.starttls() if settings.SMTP_USER and settings.SMTP_PASSWORD: server.login(settings.SMTP_USER, settings.SMTP_PASSWORD) server.sendmail(settings.DEFAULT_FROM_EMAIL, to_email, msg.as_string()) async def send_email( to_email: str, subject: str, html_content: str, text_content: Optional[str] = None, ) -> bool: """ Enviar email de forma asíncrona. Retorna True si el envío fue exitoso, False con log de error si falló. Se diseña para no propagar excepciones (fail-silent) en flujos de UI. """ try: await asyncio.to_thread( _send_smtp_sync, to_email, subject, html_content, text_content, ) logger.info("Email sent", to=to_email, subject=subject) return True except Exception as exc: logger.error("Email send failed", to=to_email, subject=subject, error=str(exc)) return False # ============================================================ # Plantillas HTML inline # ============================================================ def build_password_reset_email(reset_url: str, user_name: str) -> tuple[str, str]: """ Construir HTML y texto plano para email de reseteo de contraseña. Returns: (html_content, text_content) """ html = f""" Restablecer contraseña
ServiceManager

Restablece tu contraseña

Hola {user_name},

Recibimos una solicitud para restablecer la contraseña de tu cuenta. Haz clic en el botón de abajo para crear una nueva contraseña. Este enlace es válido por 30 minutos.

Restablecer contraseña

Si no puedes hacer clic en el botón, copia y pega este enlace en tu navegador:

{reset_url}


Si no solicitaste restablecer tu contraseña, puedes ignorar este mensaje. Tu contraseña no se modificará.
Por seguridad, este enlace expira en 30 minutos y solo puede usarse una vez.

© 2026 Aduanasoft — Acceso exclusivo autorizado

""" text = ( f"Hola {user_name},\n\n" "Recibimos una solicitud para restablecer la contraseña de tu cuenta.\n\n" f"Haz clic en el siguiente enlace (válido por 30 minutos):\n{reset_url}\n\n" "Si no solicitaste este cambio, ignora este mensaje.\n\n" "— ServiceManager" ) return html, text