Email System
Comprehensive email infrastructure with template support, async processing, and transactional capabilities
Overview
The email system is designed for production use with enterprise-grade features:
📧 SendGrid Integration
Reliable email delivery with SendGrid's transactional email service
🎨 HTML Templates
Professional email templates with Jinja2 templating engine
⚡ Async Processing
Background email sending with Celery for optimal performance
🔒 Security Features
Auto-escaping, secure token generation, and spam protection
Architecture
The email system consists of several key components that work together to provide a seamless email experience:
Core Components
- EmailTemplateService: Handles template rendering with Jinja2, providing auto-escaping and context variable injection
- send_email: Async email sending function using SendGrid with error handling and logging
- Template System: Professional HTML email templates with consistent styling and branding
- Background Tasks: Integration with FastAPI's background tasks and Celery for non-blocking email processing
Getting Started
Prerequisites
Before setting up the email system, ensure you have:
- Python 3.8+ with FastAPI installed
- A SendGrid account (free tier available)
- Basic understanding of HTML and Jinja2 templating
Initial Setup
Install Dependencies
Ensure you have the required packages in your requirements.txt
:
pip install sendgrid jinja2 python-multipart
Environment Configuration
Add these variables to your .env
file:
SENDGRID_API_KEY=your_sendgrid_api_key_here
BACKEND_URL=http://localhost:8000
FRONTEND_URL=http://localhost:3000
SUPPORT_EMAIL=support@yourcompany.com
FROM_EMAIL=noreply@yourcompany.com
COMPANY_NAME=Your Company Name
Verify File Structure
Ensure your email directory structure matches the expected layout:
app/
├── email/
│ ├── __init__.py
│ ├── emails.py
│ └── templates/
│ ├── verify_user_email.html
│ └── reset_password_email.html
SendGrid Setup Guide
SendGrid is a cloud-based email service that provides reliable email delivery. Here's how to get started:
Create SendGrid Account
- Visit sendgrid.com and click "Start for Free"
- Fill out the registration form with your details
- Verify your email address through the confirmation email
- Complete the onboarding questionnaire about your email use case
SendGrid offers a free tier with 100 emails per day, which is perfect for development and small applications.
Generate API Key
- After logging in, navigate to Settings → API Keys
- Click "Create API Key"
- Choose "Restricted Access" for better security
- Give your API key a descriptive name (e.g., "FastAPI Template Production")
- Under Mail Send, grant "Full Access" permissions
- Click "Create & View"
- Copy the API key immediately - you won't be able to see it again!
Store your API key securely and never commit it to version control. Use environment variables or a secure secrets management system.
Configure Sender Authentication
For better deliverability and to avoid spam filters:
- Go to Settings → Sender Authentication
- Choose "Domain Authentication" (recommended) or "Single Sender Verification"
- For domain authentication:
- Enter your domain (e.g.,
yourcompany.com
) - Add the provided DNS records to your domain registrar
- Wait for verification (can take up to 48 hours)
- Enter your domain (e.g.,
- For single sender verification:
- Enter the email address you'll send from
- Verify the email address through the confirmation link
Test Your Setup
Create a simple test to verify your SendGrid configuration:
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import Mail
def test_sendgrid_connection():
message = Mail(
from_email='test@yourcompany.com',
to_emails='your-email@example.com',
subject='SendGrid Test',
html_content='<strong>Hello from SendGrid!</strong>'
)
try:
sg = SendGridAPIClient(os.environ.get('SENDGRID_API_KEY'))
response = sg.send(message)
print(f"Email sent successfully! Status: {response.status_code}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
test_sendgrid_connection()
Core Classes and Functions
EmailTemplateService
The EmailTemplateService
class is the heart of the templating system, providing secure and flexible email template rendering:
class EmailTemplateService:
def __init__(self, templates_dir: str = "templates"):
"""
Initialize the email template service with Jinja2
Args:
templates_dir: Directory containing email templates
"""
template_path = Path(__file__).parent / templates_dir
self.env = Environment(
loader=FileSystemLoader(template_path),
autoescape=True, # Auto-escape HTML for security
trim_blocks=True,
lstrip_blocks=True
)
def render_template(self, template_name: str, **context) -> str:
"""
Render an email template with the given context
Args:
template_name: Name of the template file
**context: Variables to pass to the template
Returns:
Rendered HTML string
"""
template = self.env.get_template(template_name)
return template.render(**context)
Key Features:
- Auto-escaping: Prevents XSS attacks by automatically escaping HTML content
- Template Loading: Efficiently loads and caches templates from the file system
- Context Variables: Pass dynamic data to templates for personalization
- Error Handling: Graceful handling of missing templates and rendering errors
- Performance: Templates are compiled and cached for optimal performance
send_email Function
The async email sending function provides robust SendGrid integration:
async def send_email(to_email: str, subject: str, html_content: str, from_email: str) -> None:
"""
Send an email using SendGrid
Args:
to_email: Recipient email address
subject: Email subject line
html_content: HTML content of the email
from_email: Sender email address
"""
if not app_settings.SENDGRID_API_KEY:
print("Warning: SendGrid API key not set, email not sent")
return
message = Mail(
from_email=from_email,
to_emails=to_email,
subject=subject,
html_content=html_content
)
try:
sg = SendGridAPIClient(app_settings.SENDGRID_API_KEY)
response = sg.send(message)
print(f"SendGrid response code: {response.status_code}")
except Exception as e:
print(f"SendGrid error: {str(e)}")
Features:
- Async Operation: Non-blocking email sending for better performance
- Error Handling: Comprehensive error handling with logging
- Configuration Check: Validates API key presence before attempting to send
- Response Logging: Logs SendGrid response codes for debugging
Built-in Email Templates
The template includes professionally designed email templates for common authentication flows:
Email Verification Template
Used for user registration and email verification with a clean, professional design:
# Send verification email
async def send_verification_email(email: str, token: str) -> None:
verification_link = f"{app_settings.BACKEND_URL}/auth/verify-email?token={token}"
subject = "Confirm Your Email - Name"
context = {
'verification_link': verification_link,
'expires_in': '30 minutes',
'support_email': app_settings.SUPPORT_EMAIL,
'company_name': app_settings.COMPANY_NAME,
'company_address': None,
}
html_content = email_template_service.render_template(
"verify_user_email.html",
**context
)
await send_email(
to_email=email,
subject=subject,
html_content=html_content,
from_email=app_settings.FROM_EMAIL
)
This template includes:
- Clear call-to-action button
- Expiration time display
- Support contact information
- Responsive design for mobile devices
- Professional branding elements
Password Reset Template
Used for secure password reset functionality:
# Send password reset email
async def send_password_reset_email(
email: str,
token: str,
user_name: Optional[str] = None
) -> None:
reset_link = f"{app_settings.FRONTEND_URL}/change-password?token={token}"
subject = "Request to reset your Password"
context = {
'reset_link': reset_link,
'user_name': user_name,
'expires_in': '15 minutes',
'support_email': app_settings.SUPPORT_EMAIL,
'company_name': app_settings.COMPANY_NAME,
'company_address': None,
}
html_content = email_template_service.render_template(
'reset_password_email.html',
**context
)
await send_email(
to_email=email,
subject=subject,
html_content=html_content,
from_email=app_settings.FROM_EMAIL
)
Features include:
- Personalized greeting with user name
- Secure token-based reset link
- Clear security messaging
- Short expiration time for security
Template Context Variables
Understanding the template context system is crucial for effective email customization:
Common Variables
All email templates support these standard context variables:
company_name
: Your company or application name (appears in headers and footers)support_email
: Support contact email for user assistancecompany_address
: Optional company address for legal complianceexpires_in
: Token expiration time in human-readable format
Verification Email Variables
verification_link
: Complete URL for email verification (includes token)user_name
: Optional user's name for personalization
Password Reset Variables
reset_link
: Complete URL for password reset (includes token)user_name
: User's display name for personalizationexpires_in
: Token expiration time (typically shorter than verification)
Background Task Integration
The email system integrates seamlessly with FastAPI's background tasks and Celery for scalable email processing:
FastAPI Background Tasks
For simple applications, use FastAPI's built-in background tasks:
@router.post("/create-user", status_code=status.HTTP_201_CREATED)
async def create_user(
db: db_dependency,
create_user_request: CreateUserRequest,
background_tasks: BackgroundTasks
):
"""Register a new user."""
# ... user creation logic ...
# Generate verification token
token = generate_verification_token(create_user_request.email)
# Add email sending to background tasks
background_tasks.add_task(
send_verification_email,
create_user_request.email,
token
)
return {"message": "User created. Check your email to verify."}
Celery Integration
For production environments with high email volumes, use Celery for more robust background processing create email-specific Celery tasks:
from celery import current_app as celery_app
from .emails import send_email, send_verification_email, send_password_reset_email
@celery_app.task(bind=True, max_retries=3)
def send_email_task(self, to_email: str, subject: str, html_content: str, from_email: str):
"""Celery task for sending emails with retry logic."""
try:
# Convert to sync call for Celery
import asyncio
asyncio.run(send_email(to_email, subject, html_content, from_email))
except Exception as exc:
# Retry with exponential backoff
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
@celery_app.task(bind=True, max_retries=3)
def send_verification_email_task(self, email: str, token: str):
"""Celery task for sending verification emails."""
try:
import asyncio
asyncio.run(send_verification_email(email, token))
except Exception as exc:
raise self.retry(exc=exc, countdown=60 * (2 ** self.request.retries))
Use Celery tasks in your routes:
from app.email.tasks import send_verification_email_task
@router.post("/create-user", status_code=status.HTTP_201_CREATED)
async def create_user(
db: db_dependency,
create_user_request: CreateUserRequest
):
"""Register a new user."""
# ... user creation logic ...
# Generate verification token
token = generate_verification_token(create_user_request.email)
# Queue email sending with Celery
send_verification_email_task.delay(
create_user_request.email,
token
)
return {"message": "User created. Check your email to verify."}
Benefits of Celery Integration:
- Scalability: Handle thousands of emails efficiently
- Reliability: Automatic retry logic for failed emails
- Monitoring: Track email sending status and failures
- Queue Management: Dedicated queues for different email types
- Distributed Processing: Scale across multiple workers
Creating Custom Templates
Template Development Process
Design Your Template
Plan your email content and layout:
- Define the purpose and call-to-action
- Choose consistent branding elements
- Ensure mobile responsiveness
- Plan for different content lengths
Create the HTML Template
Create a new file in app/email/templates/
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome to {{ company_name }}</title>
</head>
<body style="font-family: Arial, sans-serif; margin: 0; padding: 20px;">
<div style="max-width: 600px; margin: 0 auto;">
<h1 style="color: #333;">Welcome, {{ user_name }}!</h1>
<p>
Thank you for joining {{ company_name }}. We're excited to have you on
board.
</p>
{% if welcome_link %}
<a
href="{{ welcome_link }}"
style="background-color: #007bff; color: white; padding: 10px 20px; text-decoration: none; border-radius: 5px;"
>
Get Started
</a>
{% endif %}
<p>
If you have any questions, please contact us at {{ support_email }}.
</p>
<hr style="margin: 20px 0;" />
<p style="color: #666; font-size: 12px;">
© {{ company_name }}. All rights reserved.
</p>
</div>
</body>
</html>
Create the Email Function
Write a function to send your custom email:
async def send_welcome_email(user_email: str, user_name: str) -> None:
"""Send welcome email to new user."""
subject = f"Welcome to {app_settings.COMPANY_NAME}!"
context = {
'user_name': user_name,
'company_name': app_settings.COMPANY_NAME,
'welcome_link': f"{app_settings.FRONTEND_URL}/dashboard",
'support_email': app_settings.SUPPORT_EMAIL,
}
html_content = email_template_service.render_template(
"welcome_email.html",
**context
)
await send_email(
to_email=user_email,
subject=subject,
html_content=html_content,
from_email=app_settings.FROM_EMAIL
)
Test Your Template
Create a test to verify your template works correctly:
import pytest
from app.email.emails import EmailTemplateService
def test_welcome_email_template():
email_service = EmailTemplateService()
context = {
'user_name': 'John Doe',
'company_name': 'Test Company',
'welcome_link': 'https://example.com/dashboard',
'support_email': 'support@example.com'
}
html_content = email_service.render_template(
'welcome_email.html',
**context
)
assert 'John Doe' in html_content
assert 'Test Company' in html_content
assert 'https://example.com/dashboard' in html_content
Extending the Email System
Adding New Email Types
Create Template File
Add a new HTML template in app/email/templates/
:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Order Confirmation - {{ company_name }}</title>
</head>
<body style="font-family: Arial, sans-serif;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h1>Order Confirmation</h1>
<p>Hi {{ customer_name }},</p>
<p>
Thank you for your order! Your order #{{ order_id }} has been confirmed.
</p>
<h2>Order Details:</h2>
<ul>
{% for item in order_items %}
<li>{{ item.name }} - ${{ item.price }} x {{ item.quantity }}</li>
{% endfor %}
</ul>
<p><strong>Total: ${{ total_amount }}</strong></p>
<p>Estimated delivery: {{ delivery_date }}</p>
</div>
</body>
</html>
Create Email Function
Add a new function in app/email/emails.py
:
async def send_order_confirmation_email(
customer_email: str,
customer_name: str,
order_id: str,
order_items: List[dict],
total_amount: float,
delivery_date: str
) -> None:
"""Send order confirmation email."""
subject = f"Order Confirmation #{order_id}"
context = {
'customer_name': customer_name,
'order_id': order_id,
'order_items': order_items,
'total_amount': total_amount,
'delivery_date': delivery_date,
'company_name': app_settings.COMPANY_NAME,
'support_email': app_settings.SUPPORT_EMAIL,
}
html_content = email_template_service.render_template(
"order_confirmation.html",
**context
)
await send_email(
to_email=customer_email,
subject=subject,
html_content=html_content,
from_email=app_settings.FROM_EMAIL
)
Integrate with Your API
Use the new email function in your routes:
@router.post("/orders")
async def create_order(
order_data: OrderCreateRequest,
background_tasks: BackgroundTasks,
db: db_dependency
):
# ... order creation logic ...
background_tasks.add_task(
send_order_confirmation_email,
order_data.customer_email,
order_data.customer_name,
new_order.id,
order_data.items,
order_data.total_amount,
order_data.delivery_date
)
return {"message": "Order created successfully"}
Advanced Features
Bulk Email Sending
For sending emails to multiple recipients:
async def send_bulk_emails(recipients: List[str], subject: str, template_name: str, context: Dict[str, Any]) -> None:
"""Send the same email to multiple recipients."""
html_content = email_template_service.render_template(template_name, **context)
for recipient in recipients:
await send_email(
to_email=recipient,
subject=subject,
html_content=html_content,
from_email=app_settings.FROM_EMAIL
)
# Add delay to respect rate limits
await asyncio.sleep(0.1)
Email with Attachments
from sendgrid.helpers.mail import Attachment, FileContent, FileName, FileType, Disposition
import base64
async def send_email_with_attachment(to_email: str, subject: str, html_content: str,
from_email: str, file_path: str, filename: str) -> None:
"""Send email with file attachment."""
message = Mail(
from_email=from_email,
to_emails=to_email,
subject=subject,
html_content=html_content
)
# Read and encode file
with open(file_path, 'rb') as file:
data = file.read()
encoded_file = base64.b64encode(data).decode()
# Create attachment
attachment = Attachment(
FileContent(encoded_file),
FileName(filename),
FileType('application/pdf'), # Adjust based on file type
Disposition('attachment')
)
message.attachment = attachment
try:
sg = SendGridAPIClient(app_settings.SENDGRID_API_KEY)
response = sg.send(message)
print(f"Email with attachment sent: {response.status_code}")
except Exception as e:
print(f"Error sending email with attachment: {str(e)}")
Email Queue System
For high-volume applications, implement a queue system:
from celery import Celery
from typing import List, Dict, Any
app = Celery('email_tasks')
@app.task
def send_email_task(to_email: str, subject: str, html_content: str, from_email: str):
"""Celery task for sending emails."""
# Use synchronous version for Celery
import asyncio
asyncio.run(send_email(to_email, subject, html_content, from_email))
@app.task
def send_bulk_emails_task(recipients: List[str], subject: str, template_name: str, context: Dict[str, Any]):
"""Celery task for bulk email sending."""
html_content = email_template_service.render_template(template_name, **context)
for recipient in recipients:
send_email_task.delay(
recipient,
subject,
html_content,
app_settings.FROM_EMAIL
)
Testing
Unit Tests
import pytest
from unittest.mock import Mock, patch
from app.email.emails import EmailTemplateService, send_email
class TestEmailTemplateService:
def setup_method(self):
self.email_service = EmailTemplateService()
def test_render_template_with_context(self):
"""Test template rendering with context variables."""
# This would require actual template files in test environment
pass
@patch('app.email.emails.SendGridAPIClient')
async def test_send_email_success(self, mock_sendgrid):
"""Test successful email sending."""
mock_sg = Mock()
mock_sendgrid.return_value = mock_sg
mock_sg.send.return_value.status_code = 202
await send_email(
to_email="test@example.com",
subject="Test",
html_content="<h1>Test</h1>",
from_email="from@example.com"
)
mock_sg.send.assert_called_once()
@patch('app.email.emails.SendGridAPIClient')
async def test_send_email_no_api_key(self, mock_sendgrid):
"""Test email sending without API key."""
with patch('app.email.emails.app_settings.SENDGRID_API_KEY', None):
await send_email(
to_email="test@example.com",
subject="Test",
html_content="<h1>Test</h1>",
from_email="from@example.com"
)
mock_sendgrid.assert_not_called()
Integration Tests
import pytest
from fastapi.testclient import TestClient
from unittest.mock import patch
def test_user_registration_sends_verification_email(test_client: TestClient):
"""Test that user registration triggers verification email."""
with patch('app.routers.auth.routes.send_verification_email') as mock_send:
response = test_client.post("/auth/create-user", json={
"username": "testuser",
"email": "test@example.com",
"password": "testpassword123"
})
assert response.status_code == 201
mock_send.assert_called_once()
Best Practices
Security Considerations
Always validate and sanitize user input before including it in email templates to prevent XSS attacks.
- Auto-escaping: Jinja2 auto-escaping is enabled by default
- Token Expiration: Use short expiration times for security tokens
- Rate Limiting: Implement rate limiting for email sending endpoints
- Input Validation: Validate all email addresses and content
- Secure Headers: Include proper email headers for security
Performance Optimization
- Background Processing: Always send emails in background tasks
- Template Caching: Jinja2 templates are cached automatically
- Connection Pooling: SendGrid client handles connection pooling
- Batch Processing: Group multiple emails when possible
- Async Operations: Use async functions throughout the email system
Deliverability Best Practices
- SPF/DKIM: Configure proper email authentication records
- Sender Reputation: Use consistent from addresses and domains
- Content Quality: Avoid spam trigger words and excessive formatting
- List Management: Handle bounces and unsubscribes properly
- Engagement Tracking: Monitor open and click rates
Troubleshooting
Common Issues
SendGrid API Key Issues: Ensure your API key has "Mail Send" permissions and is not expired. Check your SendGrid account status and billing.
Template Not Found Error:
- Check template file path and name
- Ensure templates directory exists
- Verify template file extensions (.html)
- Check file permissions
Email Not Sending:
- Verify SendGrid API key configuration
- Check sender email is authenticated
- Verify SendGrid account status and limits
- Check network connectivity
Template Rendering Errors:
- Validate Jinja2 template syntax
- Check context variable names match template variables
- Ensure all required variables are provided
- Check for typos in variable names
Debug Mode
Enable debug logging for email operations:
import logging
# Enable debug logging
logging.basicConfig(level=logging.DEBUG)
logger = logging.getLogger(__name__)
async def send_email_debug(to_email: str, subject: str, html_content: str, from_email: str) -> None:
"""Debug version of send_email function."""
logger.debug(f"Sending email to: {to_email}")
logger.debug(f"Subject: {subject}")
logger.debug(f"From: {from_email}")
logger.debug(f"HTML content length: {len(html_content)}")
# ... rest of send_email function ...
Testing Email Delivery
Use SendGrid's Testing Tools
SendGrid provides several testing tools:
- Email Activity: Monitor all sent emails
- Event Webhooks: Track delivery, opens, clicks, etc.
- Template Testing: Preview templates before sending
Create Test Endpoints
Add test endpoints for development:
@router.post("/test-email")
async def test_email(email: str, background_tasks: BackgroundTasks):
"""Test endpoint for email functionality."""
if not email.endswith("@yourcompany.com"):
raise HTTPException(status_code=400, detail="Test emails only allowed for company domain")
background_tasks.add_task(
send_welcome_email,
email,
"Test User"
)
return {"message": "Test email sent"}
Monitor Email Metrics
Track important email metrics:
- Delivery rates
- Open rates
- Click-through rates
- Bounce rates
- Unsubscribe rates