Loading IconFastLaunchAPI
Features

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:

emails.py
verify_user_email.html
reset_password_email.html

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:

.env
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

  1. Visit sendgrid.com and click "Start for Free"
  2. Fill out the registration form with your details
  3. Verify your email address through the confirmation email
  4. 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

  1. After logging in, navigate to SettingsAPI Keys
  2. Click "Create API Key"
  3. Choose "Restricted Access" for better security
  4. Give your API key a descriptive name (e.g., "FastAPI Template Production")
  5. Under Mail Send, grant "Full Access" permissions
  6. Click "Create & View"
  7. 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:

  1. Go to SettingsSender Authentication
  2. Choose "Domain Authentication" (recommended) or "Single Sender Verification"
  3. 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)
  4. 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:

test_sendgrid.py
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:

app/email/emails.py
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:

app/email/emails.py
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:

Usage Example
# 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:

Usage Example
# 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 assistance
  • company_address: Optional company address for legal compliance
  • expires_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 personalization
  • expires_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:

Authentication Route Example
@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:

app/email/tasks.py
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:

Using Celery Tasks
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/:

app/email/templates/welcome_email.html
<!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:

Custom Email Function
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:

test_custom_template.py
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/:

app/email/templates/order_confirmation.html
<!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:

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:

app/routers/orders.py
@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:

Bulk Email Function
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

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:

Email 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

tests/test_email.py
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

tests/test_email_integration.py
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:

Debug Configuration
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:

test_endpoints.py
@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

Next Steps