feat: add EmailService with Resend integration and dev mode

Implements a flexible email service that integrates with Resend API
and supports development mode for easier testing.

Features:
- Resend API integration for production email delivery
- Development mode that logs emails instead of sending
- Magic link URL logging in dev mode for testing
- Helper methods for magic link and registration emails
- Comprehensive test coverage (100% for service)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-22 16:58:17 -07:00
parent eaafa78cf3
commit abed4ac84a
4 changed files with 378 additions and 0 deletions

176
src/services/email.py Normal file
View File

@@ -0,0 +1,176 @@
"""Email service for Sneaky Klaus application.
Handles email sending via Resend API with dev mode support.
In development mode, emails are logged instead of sent.
"""
import logging
import os
from typing import Any
import resend
from flask import current_app
logger = logging.getLogger(__name__)
class EmailService:
"""Service for sending emails via Resend API.
In development mode (FLASK_ENV=development), emails are logged
instead of actually being sent. Magic link URLs are also logged
for easier testing.
"""
def __init__(self) -> None:
"""Initialize the email service."""
self.api_key = current_app.config.get("RESEND_API_KEY")
self.from_email = current_app.config.get(
"EMAIL_FROM", "noreply@sneaky-klaus.com"
)
# Check if in dev mode - first check config, then environment
flask_env = current_app.config.get("FLASK_ENV") or os.environ.get("FLASK_ENV")
self.dev_mode = flask_env == "development"
# In production, API key is required
if not self.dev_mode and not self.api_key:
raise ValueError("RESEND_API_KEY is required in production mode")
# Configure Resend if we have an API key
if self.api_key:
resend.api_key = self.api_key
def send_email(
self,
to: str,
subject: str,
html_body: str,
) -> dict[str, Any]:
"""Send an email.
Args:
to: Recipient email address
subject: Email subject line
html_body: HTML email body
Returns:
Response from Resend API or mock response in dev mode
Raises:
Exception: If email sending fails
"""
if self.dev_mode:
# In dev mode, just log the email
logger.info("=" * 80)
logger.info("DEV MODE: Email not sent")
logger.info(f"To: {to}")
logger.info(f"Subject: {subject}")
logger.info(f"Body preview: {html_body[:200]}...")
logger.info("=" * 80)
# Return a mock success response
return {"id": f"dev-mode-{os.urandom(8).hex()}"}
# Send via Resend
params: resend.Emails.SendParams = {
"from": self.from_email,
"to": [to],
"subject": subject,
"html": html_body,
}
result: dict[str, Any] = resend.Emails.send(params)
return result
def send_magic_link(
self,
to: str,
magic_link_url: str,
exchange_name: str,
) -> dict[str, Any]:
"""Send a magic link email for participant authentication.
Args:
to: Participant email address
magic_link_url: Full URL with magic token
exchange_name: Name of the exchange
Returns:
Response from email service
"""
if self.dev_mode:
logger.info(f"DEV MODE: Magic link URL: {magic_link_url}")
subject = f"Access your {exchange_name} Secret Santa"
html_body = f"""
<html>
<body>
<h2>Access Your Secret Santa</h2>
<p>Click the link below to access your {exchange_name}
Secret Santa exchange:</p>
<p><a href="{magic_link_url}">{magic_link_url}</a></p>
<p>This link will expire in 1 hour.</p>
<p>If you didn't request this link, you can safely
ignore this email.</p>
</body>
</html>
"""
return self.send_email(to=to, subject=subject, html_body=html_body)
def send_registration_confirmation(
self,
to: str,
participant_name: str,
magic_link_url: str,
exchange_name: str,
exchange_description: str | None,
budget_amount: float,
gift_exchange_date: str,
) -> dict[str, Any]:
"""Send registration confirmation email with magic link.
Args:
to: Participant email address
participant_name: Name of the participant
magic_link_url: Full URL with magic token
exchange_name: Name of the exchange
exchange_description: Description of the exchange (optional)
budget_amount: Budget amount
gift_exchange_date: Date of gift exchange
Returns:
Response from email service
"""
if self.dev_mode:
logger.info(f"DEV MODE: Magic link URL: {magic_link_url}")
subject = f"Welcome to {exchange_name}!"
description_html = ""
if exchange_description:
description_html = f"<p><em>{exchange_description}</em></p>"
html_body = f"""
<html>
<body>
<h2>Welcome to {exchange_name}, {participant_name}!</h2>
<p>You've successfully registered for the Secret Santa exchange.</p>
{description_html}
<h3>Exchange Details</h3>
<ul>
<li><strong>Budget:</strong> ${budget_amount:.0f}</li>
<li><strong>Gift Exchange Date:</strong> {gift_exchange_date}</li>
</ul>
<h3>Access Your Dashboard</h3>
<p>Click the link below to access your participant
dashboard:</p>
<p><a href="{magic_link_url}">{magic_link_url}</a></p>
<p>This link will expire in 1 hour, but you can always
request a new one.</p>
<p>Happy gifting!</p>
</body>
</html>
"""
return self.send_email(to=to, subject=subject, html_body=html_body)