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>
177 lines
5.6 KiB
Python
177 lines
5.6 KiB
Python
"""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)
|