# SPDX-License-Identifier: GPL-3.0-or-later """ Email service for Golf game authentication. Provides email sending via Resend for verification, password reset, and notifications. """ import logging from typing import Optional from config import config logger = logging.getLogger(__name__) class EmailService: """ Email service using Resend API. Handles all transactional emails for authentication: - Email verification - Password reset - Password changed notification """ def __init__(self, api_key: str, from_address: str, base_url: str): """ Initialize email service. Args: api_key: Resend API key. from_address: Sender email address. base_url: Base URL for verification/reset links. """ self.api_key = api_key self.from_address = from_address self.base_url = base_url.rstrip("/") self._client = None @classmethod def create(cls) -> "EmailService": """Create EmailService from config.""" return cls( api_key=config.RESEND_API_KEY, from_address=config.EMAIL_FROM, base_url=config.BASE_URL, ) @property def client(self): """Lazy-load Resend client.""" if self._client is None: try: import resend resend.api_key = self.api_key self._client = resend except ImportError: logger.warning("resend package not installed, emails will be logged only") self._client = None return self._client def is_configured(self) -> bool: """Check if email service is properly configured.""" return bool(self.api_key) async def send_verification_email( self, to: str, token: str, username: str, ) -> Optional[str]: """ Send email verification email. Args: to: Recipient email address. token: Verification token. username: User's display name. Returns: Resend message ID if sent, None if not configured. """ if not self.is_configured(): logger.info(f"Email not configured. Would send verification to {to}") return None verify_url = f"{self.base_url}/verify-email?token={token}" subject = "Verify your Golf Game account" html = f"""

Welcome to Golf Game, {username}!

Please verify your email address by clicking the link below:

Verify Email Address

Or copy and paste this URL into your browser:

{verify_url}

This link will expire in 24 hours.

If you didn't create this account, you can safely ignore this email.

""" return await self._send_email(to, subject, html) async def send_password_reset_email( self, to: str, token: str, username: str, ) -> Optional[str]: """ Send password reset email. Args: to: Recipient email address. token: Reset token. username: User's display name. Returns: Resend message ID if sent, None if not configured. """ if not self.is_configured(): logger.info(f"Email not configured. Would send password reset to {to}") return None reset_url = f"{self.base_url}/reset-password?token={token}" subject = "Reset your Golf Game password" html = f"""

Password Reset Request

Hi {username},

We received a request to reset your password. Click the link below to set a new password:

Reset Password

Or copy and paste this URL into your browser:

{reset_url}

This link will expire in 1 hour.

If you didn't request this, you can safely ignore this email. Your password will remain unchanged.

""" return await self._send_email(to, subject, html) async def send_password_changed_notification( self, to: str, username: str, ) -> Optional[str]: """ Send password changed notification email. Args: to: Recipient email address. username: User's display name. Returns: Resend message ID if sent, None if not configured. """ if not self.is_configured(): logger.info(f"Email not configured. Would send password change notification to {to}") return None subject = "Your Golf Game password was changed" html = f"""

Password Changed

Hi {username},

Your password was successfully changed.

If you did not make this change, please contact support immediately.

""" return await self._send_email(to, subject, html) async def _send_email( self, to: str, subject: str, html: str, ) -> Optional[str]: """ Send an email via Resend. Args: to: Recipient email address. subject: Email subject. html: HTML email body. Returns: Resend message ID if sent, None on error. """ if not self.client: logger.warning(f"Resend not available. Email to {to}: {subject}") return None try: params = { "from": self.from_address, "to": [to], "subject": subject, "html": html, } response = self.client.Emails.send(params) message_id = response.get("id") if isinstance(response, dict) else getattr(response, "id", None) logger.info(f"Email sent to {to}: {message_id}") return message_id except Exception as e: logger.error(f"Failed to send email to {to}: {e}") return None # Global email service instance _email_service: Optional[EmailService] = None def get_email_service() -> EmailService: """Get or create the global email service instance.""" global _email_service if _email_service is None: _email_service = EmailService.create() return _email_service