Spaces:
Running
Running
| import os | |
| import smtplib | |
| import logging | |
| from email.mime.text import MIMEText | |
| from email.mime.multipart import MIMEMultipart | |
| logger = logging.getLogger(__name__) | |
| def send_recovery_email(to_email: str, code: str, raise_on_error: bool = False): | |
| """ | |
| Envía un correo electrónico con el código de recuperación de contraseña de 6 dígitos. | |
| Soporta Brevo API (ideal para enviar a cualquier remitente gratis sin dominio), | |
| Resend API (requiere dominio o enviar solo al creador), o SMTP tradicional como fallback. | |
| """ | |
| brevo_api_key = os.getenv("BREVO_API_KEY") | |
| resend_api_key = os.getenv("RESEND_API_KEY") | |
| smtp_from = os.getenv("SMTP_FROM") | |
| smtp_user = os.getenv("SMTP_USER") | |
| if smtp_from: smtp_from = smtp_from.strip("'\" ") | |
| if smtp_user: smtp_user = smtp_user.strip("'\" ") | |
| # HTML estilizado y profesional | |
| html_content = f""" | |
| <html> | |
| <body style="font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; background-color: #0d0e12; color: #f8f8ff; padding: 20px; margin: 0;"> | |
| <div style="max-width: 500px; margin: 0 auto; background-color: #15171e; border: 1px solid #232733; border-radius: 16px; padding: 30px; box-shadow: 0 8px 30px rgba(0,0,0,0.5);"> | |
| <div style="text-align: center; margin-bottom: 25px;"> | |
| <h2 style="color: #9d5cf6; margin: 0; font-size: 26px; letter-spacing: 4px; font-weight: 800;">MELODIX</h2> | |
| <p style="color: #a0a0b8; margin: 5px 0 0 0; font-size: 13px;">Código de Restablecimiento</p> | |
| </div> | |
| <hr style="border: 0; border-top: 1px solid #232733; margin-bottom: 25px;"> | |
| <p style="font-size: 15px; line-height: 22px; color: #e2e2e9;">Hola,</p> | |
| <p style="font-size: 15px; line-height: 22px; color: #e2e2e9;">Has solicitado restablecer la contraseña de tu cuenta de Melodix. Utiliza el siguiente código de verificación de 6 dígitos:</p> | |
| <div style="text-align: center; margin: 30px 0;"> | |
| <span style="font-family: 'Courier New', Courier, monospace; font-size: 36px; font-weight: bold; letter-spacing: 8px; color: #00f5d4; background-color: rgba(0, 245, 212, 0.08); padding: 12px 24px; border-radius: 12px; border: 1px solid rgba(0, 245, 212, 0.25); display: inline-block;"> | |
| {code} | |
| </span> | |
| </div> | |
| <p style="font-size: 13px; line-height: 18px; color: #a0a0b8;">Este código es válido por 15 minutos. Si no solicitaste este cambio, puedes ignorar este correo de forma segura.</p> | |
| <hr style="border: 0; border-top: 1px solid #232733; margin-top: 30px; margin-bottom: 20px;"> | |
| <p style="font-size: 11px; text-align: center; color: #6c757d; margin: 0;">© {datetime_year()} Melodix App. Todos los derechos reservados.</p> | |
| </div> | |
| </body> | |
| </html> | |
| """ | |
| # 1. ENVIAR POR BREVO API (SI BREVO_API_KEY ESTÁ CONFIGURADA) | |
| if brevo_api_key: | |
| brevo_api_key = brevo_api_key.strip("'\" ") | |
| try: | |
| import requests | |
| url = "https://api.brevo.com/v3/smtp/email" | |
| headers = { | |
| "api-key": brevo_api_key, | |
| "Content-Type": "application/json" | |
| } | |
| # El remitente debe ser el verificado en Brevo (típicamente smtp_user) | |
| sender_email = smtp_user or "mifemmnic@gmail.com" | |
| sender_name = "Melodix" | |
| if smtp_from: | |
| if "<" in smtp_from and ">" in smtp_from: | |
| parts = smtp_from.split("<") | |
| sender_name = parts[0].strip() | |
| sender_email = parts[1].replace(">", "").strip() | |
| elif "@" in smtp_from: | |
| sender_email = smtp_from.strip() | |
| payload = { | |
| "sender": { | |
| "name": sender_name, | |
| "email": sender_email | |
| }, | |
| "to": [ | |
| { | |
| "email": to_email | |
| } | |
| ], | |
| "subject": "Recuperación de Contraseña - Melodix", | |
| "htmlContent": html_content | |
| } | |
| res = requests.post(url, json=payload, headers=headers, timeout=15) | |
| if res.status_code in (200, 201, 202): | |
| logger.info(f"Correo de recuperación enviado con éxito a {to_email} vía Brevo API") | |
| return True | |
| else: | |
| err_msg = f"Brevo API retornó error {res.status_code}: {res.text}" | |
| logger.error(err_msg) | |
| if raise_on_error: | |
| raise Exception(err_msg) | |
| return False | |
| except Exception as e: | |
| logger.error(f"Error enviando correo vía Brevo a {to_email}: {e}", exc_info=True) | |
| if raise_on_error: | |
| raise e | |
| return False | |
| # 2. ENVIAR POR RESEND API (SI RESEND_API_KEY ESTÁ CONFIGURADA) | |
| if resend_api_key: | |
| resend_api_key = resend_api_key.strip("'\" ") | |
| try: | |
| import requests | |
| url = "https://api.resend.com/emails" | |
| headers = { | |
| "Authorization": f"Bearer {resend_api_key}", | |
| "Content-Type": "application/json" | |
| } | |
| from_addr = "onboarding@resend.dev" | |
| if smtp_from and "@" in smtp_from: | |
| domain = smtp_from.split("@")[-1].replace(">", "").strip() | |
| if domain not in ["gmail.com", "outlook.com", "hotmail.com", "yahoo.com"]: | |
| from_addr = smtp_from | |
| if not from_addr.startswith("Melodix"): | |
| from_addr = f"Melodix <{from_addr}>" | |
| payload = { | |
| "from": from_addr, | |
| "to": [to_email], | |
| "subject": "Recuperación de Contraseña - Melodix", | |
| "html": html_content | |
| } | |
| res = requests.post(url, json=payload, headers=headers, timeout=15) | |
| if res.status_code in (200, 201): | |
| logger.info(f"Correo de recuperación enviado con éxito a {to_email} vía Resend API") | |
| return True | |
| else: | |
| err_msg = f"Resend API retornó error {res.status_code}: {res.text}" | |
| logger.error(err_msg) | |
| if raise_on_error: | |
| raise Exception(err_msg) | |
| return False | |
| except Exception as e: | |
| logger.error(f"Error enviando correo vía Resend a {to_email}: {e}", exc_info=True) | |
| if raise_on_error: | |
| raise e | |
| return False | |
| # 2. ENVIAR POR SMTP TRADICIONAL (FALLBACK) | |
| smtp_host = os.getenv("SMTP_HOST") | |
| smtp_port = os.getenv("SMTP_PORT") | |
| smtp_user = os.getenv("SMTP_USER") | |
| smtp_password = os.getenv("SMTP_PASSWORD") | |
| # Limpiar comillas iniciales/finales y espacios (común al pegar variables en Render) | |
| if smtp_host: smtp_host = smtp_host.strip("'\" ") | |
| if smtp_port: smtp_port = smtp_port.strip("'\" ") | |
| if smtp_user: smtp_user = smtp_user.strip("'\" ") | |
| if smtp_password: smtp_password = smtp_password.strip("'\" ") | |
| if not smtp_from: | |
| smtp_from = f"Melodix <{smtp_user}>" | |
| if not smtp_host or not smtp_port or not smtp_user or not smtp_password: | |
| logger.warning(f"Configuración SMTP incompleta. host={smtp_host}, port={smtp_port}, user={smtp_user}. No se puede enviar correo.") | |
| logger.info(f"[SIMULADO] Código de recuperación para {to_email}: {code}") | |
| return False | |
| try: | |
| # Configurar el mensaje | |
| msg = MIMEMultipart("alternative") | |
| msg["Subject"] = "Recuperación de Contraseña - Melodix" | |
| msg["From"] = smtp_from | |
| msg["To"] = to_email | |
| msg.attach(MIMEText(html_content, "html")) | |
| # Conectar al servidor SMTP | |
| port = int(smtp_port) | |
| if port == 465: | |
| server = smtplib.SMTP_SSL(smtp_host, port, timeout=15) | |
| else: | |
| server = smtplib.SMTP(smtp_host, port, timeout=15) | |
| server.ehlo() | |
| server.starttls() | |
| server.ehlo() | |
| server.login(smtp_user, smtp_password) | |
| server.sendmail(smtp_user, [to_email], msg.as_string()) | |
| server.quit() | |
| logger.info(f"Correo de recuperación enviado con éxito a {to_email} vía SMTP") | |
| return True | |
| except Exception as e: | |
| logger.error(f"Error enviando correo SMTP a {to_email}: {e}", exc_info=True) | |
| if raise_on_error: | |
| raise e | |
| return False | |
| def datetime_year(): | |
| from datetime import datetime | |
| return datetime.utcnow().year | |