""" 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