import html import logging import os import smtplib import ssl from datetime import datetime from email.message import EmailMessage from email.utils import formataddr from api.utils.email_verification import EMAIL_VERIFICATION_CODE_TTL_MINUTES logger = logging.getLogger(__name__) class EmailConfigurationError(RuntimeError): """SMTP no está configurado o falta información obligatoria.""" class EmailDeliveryError(RuntimeError): """El servidor SMTP rechazó el envío o no se pudo conectar.""" def _env_bool(name: str, default: str = "false") -> bool: return os.getenv(name, default).strip().lower() in {"1", "true", "yes", "on"} def _build_verification_plain(username: str, code: str) -> str: ttl = EMAIL_VERIFICATION_CODE_TTL_MINUTES return "\n".join( [ f"Hola {username},", "", "Tu código de verificación para Deck of Cards es:", "", code, "", f"Este código caduca en {ttl} minutos.", "Si no has creado esta cuenta, puedes ignorar este correo.", "", "— Deck of Cards", "Plataforma web para la elicitación de escalas de valor (DoC-MF).", ] ) def _build_verification_html(username: str, code: str) -> str: safe_username = html.escape(username) frontend_url = os.getenv("FRONTEND_URL", "http://localhost:5173").rstrip("/") logo_url = f"{frontend_url}/favicon.svg" ttl = EMAIL_VERIFICATION_CODE_TTL_MINUTES year = datetime.utcnow().year return f""" Verificación — Deck of Cards
Deck of Cards Deck of Cards

Verificación de cuenta

Hola, {safe_username}

Gracias por registrarte. Introduce el siguiente código en la pantalla de registro para confirmar tu correo electrónico.

Tu código

{code}

El código caduca en {ttl} minutos.

Si no has creado esta cuenta, ignora este mensaje. Nadie más podrá usar tu correo sin el código.

Deck of Cards Software Científico

Plataforma web para la elicitación de escalas de valor y construcción de conjuntos difusos interpretables (DoC-MF).

© {year} Deck of Cards App.

Basado en la metodología DoC-MF (D. García-Zamora, B. Dutta, J.R. Figueira y L. Martínez, EJOR, 2024).

""" def _log_verification_code_for_dev(recipient_email: str, code: str) -> None: logger.warning( "[email-verification] Modo desarrollo: código para %s → %s", recipient_email, code, ) def _build_verification_message( recipient_email: str, username: str, code: str, smtp_from_email: str, smtp_from_name: str, ) -> EmailMessage: message = EmailMessage() message["Subject"] = "Codigo de verificacion - Deck of Cards" message["From"] = formataddr((smtp_from_name, smtp_from_email)) message["To"] = recipient_email message["Date"] = datetime.utcnow().strftime("%a, %d %b %Y %H:%M:%S +0000") message.set_content(_build_verification_plain(username, code)) message.add_alternative(_build_verification_html(username, code), subtype="html") return message def _send_via_smtp( message: EmailMessage, recipient_email: str, smtp_from_email: str, smtp_host: str, smtp_port: int, smtp_username: str | None, smtp_password: str | None, use_tls: bool, use_ssl: bool, ) -> None: context = ssl.create_default_context() timeout = int(os.getenv("SMTP_TIMEOUT_SECONDS", "30")) debug_level = 1 if _env_bool("SMTP_DEBUG") else 0 try: if use_ssl: server = smtplib.SMTP_SSL( smtp_host, smtp_port, timeout=timeout, context=context, ) else: server = smtplib.SMTP(smtp_host, smtp_port, timeout=timeout) with server: server.set_debuglevel(debug_level) server.ehlo() if use_tls and not use_ssl: server.starttls(context=context) server.ehlo() if smtp_username and smtp_password: server.login(smtp_username, smtp_password) server.sendmail( smtp_from_email, [recipient_email], message.as_string(), ) except smtplib.SMTPException as exc: logger.exception( "Error SMTP al enviar verificación a %s via %s:%s", recipient_email, smtp_host, smtp_port, ) raise EmailDeliveryError(str(exc)) from exc except OSError as exc: logger.exception( "No se pudo conectar al servidor SMTP %s:%s", smtp_host, smtp_port, ) raise EmailDeliveryError(str(exc)) from exc def send_verification_email(recipient_email: str, username: str, code: str) -> bool: recipient_email = recipient_email.strip().lower() smtp_host = os.getenv("SMTP_HOST", "").strip() dev_log_code = _env_bool("EMAIL_VERIFICATION_DEV_LOG_CODE") if not smtp_host: if dev_log_code: _log_verification_code_for_dev(recipient_email, code) return True logger.error( "[email-verification] SMTP_HOST no configurado; no se envió correo a %s", recipient_email, ) raise EmailConfigurationError( "SMTP_HOST no está configurado en el servidor" ) smtp_port = int(os.getenv("SMTP_PORT", "587")) smtp_username = (os.getenv("SMTP_USERNAME") or "").strip() smtp_password = (os.getenv("SMTP_PASSWORD") or "").replace(" ", "") smtp_from_email = (os.getenv("SMTP_FROM_EMAIL") or smtp_username or "").strip() smtp_from_name = os.getenv("SMTP_FROM_NAME", "Deck of Cards") use_tls = _env_bool("SMTP_USE_TLS", "true") use_ssl = _env_bool("SMTP_USE_SSL", "false") if not smtp_from_email: raise EmailConfigurationError( "SMTP_FROM_EMAIL o SMTP_USERNAME debe estar configurado" ) message = _build_verification_message( recipient_email, username, code, smtp_from_email, smtp_from_name, ) try: _send_via_smtp( message, recipient_email, smtp_from_email, smtp_host, smtp_port, smtp_username, smtp_password, use_tls, use_ssl, ) except EmailDeliveryError: if dev_log_code: _log_verification_code_for_dev(recipient_email, code) return True raise logger.info("Correo de verificación enviado a %s", recipient_email) return True