""" Email sending abstraction for magic link authentication. Supports multiple backends: Console (dev), Resend API (production). """ import logging from django.conf import settings from django.core.mail import EmailMultiAlternatives, send_mail from django.urls import reverse logger = logging.getLogger(__name__) def send_magic_link_email(request, magic_token) -> bool: """ Send magic link email using configured backend. Args: request: HttpRequest object (for building absolute URL) magic_token: MagicToken instance Returns: bool: True if email sent successfully """ verify_path = reverse("auth:verify", kwargs={"token": magic_token.token}) verify_url = request.build_absolute_uri(verify_path) # Print URL directly to console in development (bypasses MIME encoding) if settings.DEBUG: print("\n" + "=" * 60) print("MAGIC LINK (click or copy):") print(verify_url) print("=" * 60 + "\n") subject = "Your PyRunner Login Link" text_message = f"""Hi there! Click the link below to sign in to PyRunner: ======================================== {verify_url} ======================================== This link will expire in 15 minutes and can only be used once. If you didn't request this link, you can safely ignore this email. --- PyRunner - Python Script Automation """ html_message = f"""

Sign in to PyRunner

Click the button below to sign in:

Sign In to PyRunner

This link expires in 15 minutes and can only be used once.

Or copy and paste this URL into your browser:
{verify_url}

""" return _send_email(magic_token.email, subject, text_message, html_message) def send_password_reset_email(request, user, reset_token) -> bool: """ Send password reset email. Args: request: HttpRequest object (for building absolute URL) user: User instance reset_token: PasswordResetToken instance Returns: bool: True if email sent successfully """ reset_path = reverse("auth:reset_password", kwargs={"token": reset_token.token}) reset_url = request.build_absolute_uri(reset_path) # Print URL directly to console in development if settings.DEBUG: print("\n" + "=" * 60) print("PASSWORD RESET LINK (click or copy):") print(reset_url) print("=" * 60 + "\n") subject = "Reset Your PyRunner Password" text_message = f"""Hi there! You requested a password reset for your PyRunner account. Click the link below to reset your password: ======================================== {reset_url} ======================================== This link will expire in 24 hours and can only be used once. If you didn't request this, you can safely ignore this email. --- PyRunner - Python Script Automation """ html_message = f"""

Reset Your Password

You requested a password reset for your PyRunner account ({user.email}).

Reset Password

This link expires in 24 hours and can only be used once.

Or copy and paste this URL into your browser:
{reset_url}

""" return _send_email(user.email, subject, text_message, html_message) def _get_db_backend_and_from_email(): """ Returns (backend, from_email) if GlobalSettings has a non-DISABLED email backend configured, else (None, None). Local imports avoid a circular import with core.services.notification_service. """ from core.models import GlobalSettings from core.services.notification_service import NotificationService gs = GlobalSettings.get_settings() if gs.email_backend == GlobalSettings.EmailBackend.DISABLED: return None, None backend = NotificationService._get_email_backend(gs) if backend is None: return None, None from_email = ( gs.smtp_from_email if gs.email_backend == GlobalSettings.EmailBackend.SMTP else gs.resend_from_email ) return backend, from_email def _send_email(to_email: str, subject: str, text_content: str, html_content: str) -> bool: """ Unified sender for auth emails. Resolution order: DB-configured backend → env Resend API → Django send_mail(). A DB backend failure is NOT retried via env — it is surfaced as False. """ backend, from_email = _get_db_backend_and_from_email() if backend is not None: if not from_email: logger.error( f"DB email backend configured but from_email is empty; cannot send to {to_email}" ) return False try: msg = EmailMultiAlternatives( subject=subject, body=text_content, from_email=from_email, to=[to_email], connection=backend, ) msg.attach_alternative(html_content, "text/html") msg.send() logger.info(f"Auth email sent to {to_email} via DB backend") return True except Exception as e: logger.error(f"DB backend failed to send auth email to {to_email}: {e}") return False if _should_use_resend(): return _send_via_resend(to_email, subject, text_content, html_content) try: send_mail( subject=subject, message=text_content, from_email=getattr(settings, "DEFAULT_FROM_EMAIL", "noreply@localhost"), recipient_list=[to_email], html_message=html_content, fail_silently=False, ) logger.info(f"Auth email sent to {to_email} via env backend") return True except Exception as e: logger.error(f"Env backend failed to send auth email to {to_email}: {e}") return False def _should_use_resend() -> bool: """Check if Resend API should be used.""" return ( hasattr(settings, "RESEND_API_KEY") and settings.RESEND_API_KEY and getattr(settings, "USE_RESEND", False) ) def _send_via_resend(to_email: str, subject: str, text_content: str, html_content: str) -> bool: """ Send email via Resend API. Requires: - RESEND_API_KEY in settings - USE_RESEND = True in settings - resend package installed """ try: import resend resend.api_key = settings.RESEND_API_KEY params = { "from": getattr(settings, "DEFAULT_FROM_EMAIL", "noreply@localhost"), "to": [to_email], "subject": subject, "text": text_content, "html": html_content, } response = resend.Emails.send(params) logger.info(f"Resend email sent to {to_email}, id: {response.get('id')}") return True except ImportError: logger.error("resend package not installed. Run: pip install resend") return False except Exception as e: logger.error(f"Resend API error: {e}") return False