Implementing a Mail Delivery Switch in Python for Local and AWS Environments Using Amazon SES
Tags: aws, python, ses
Published: 2024-06-24 22:32:40

## TL;DR
This post details how to seamlessly switch between a local stub and Amazon SES for email sending in a Python + FastAPI application, based on the presence of an AWS profile. This ensures that your email functionality can be tested locally without needing AWS credentials.

## Introduction
Here's how you can manage email notifications with Amazon SES during local development without AWS credentials. Using a Python decorator, you can switch between a stub function for local testing and SES for production. I've also included a complete implementation example with FastAPI.

## Prerequisites
The application is developed using Python and FastAPI.
It's deployed on AWS Fargate or EC2 instances.
Problem Statement
Integrating Amazon SES to send email notifications directly ties the application's functionality to AWS credentials availability, hindering local development and testing.

## Solution
The solution involves creating a Python decorator that toggles the mail sending method based on the existence of an AWS_PROFILE environment variable. This allows the use of a stub function for local development and Amazon SES in production. Here's how to implement it:

```python
import os
import functools

def switch_mailer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if os.environ.get('AWS_PROFILE') == '':
            return stub_send_email(*args, **kwargs)
        else:
            return func(*args, **kwargs)
    return wrapper

def stub_send_email(to_address, subject, body):
    print("Stub: Sending email to", to_address)
    # The stub simulates a successful email sending response
    return {'MessageId': 'fake-id', 'Response': 'Email sent successfully'}

@switch_mailer
def send_email(to_address, subject, body):
    ses_client = boto3.client('ses')
    response = ses_client.send_email(
        Source='your_email@example.com',
        Destination={
            'ToAddresses': [
                to_address
            ]
        },
        Message={
            'Subject': {
                'Data': subject
            },
            'Body': {
                'Text': {
                    'Data': body
                }
            }
        }
    )
    return response

```

### Example: Complete FastAPI Application Setup
To further demonstrate the implementation of our mail delivery switch with Amazon SES, here is a complete FastAPI application that you can run locally or in your AWS environment.

### Implementation
Here's how to set up a FastAPI application that incorporates our mail switching mechanism:

```python
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
import os
import functools
import boto3

# Define the decorator to switch mail sender
def switch_mailer(func):
    @functools.wraps(func)
    def wrapper(*args, **kwargs):
        if os.environ.get('AWS_PROFILE', '') == '':
            return stub_send_email(*args, **kwargs)
        else:
            return func(*args, **kwargs)
    return wrapper

# Stub function for local testing
def stub_send_email(to_address, subject, body):
    print("Stub: Sending email to", to_address)
    return {'MessageId': 'fake-id', 'Response': 'Email sent successfully'}

# Function to send email using Amazon SES
@switch_mailer
def send_email(to_address, subject, body):
    ses_client = boto3.client('ses', region_name='us-east-1')
    response = ses_client.send_email(
        Source='your_email@example.com',
        Destination={'ToAddresses': [to_address]},
        Message={
            'Subject': {'Data': subject},
            'Body': {'Text': {'Data': body}}
        }
    )
    return response

# FastAPI application definition
app = FastAPI()

class EmailRequest(BaseModel):
    to_address: str
    subject: str
    body: str

@app.post("/send-email/")
def handle_send_email(request: EmailRequest):
    try {
        response = send_email(request.to_address, request.subject, request.body)
        return {"message": "Email sent successfully", "response": response}
    } catch(Exception e) {
        throw new HTTPException(statusCode: 500, detail: e.toString())
    }
}

# To run the application:
# pip install boto3 pydantic fastapi uvicorn
# uvicorn main:app --reload

```

### Sample JSON Request
Use this JSON payload to test the /send-email/ endpoint in your local or AWS environment:

```json
{
    "to_address": "recipient@example.com",
    "subject": "Hello from FastAPI",
    "body": "This is a test email sent via FastAPI and AWS SES."
}

```

### Runtime Logs
When you run the FastAPI application using the provided command and send a test email through the /send-email/ endpoint, you should see the following logs, which confirm that the application is functioning as expected:

```
INFO:     Started server process [53956]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
Stub: Sending email to recipient@example.com

```

These logs indicate that the server has started successfully and the stub function is being called to simulate sending an email. This output is expected when running locally without an AWS_PROFILE set.

## Conclusion
By implementing a dynamic mail sender that adjusts based on the environment, developers can ensure their application remains functional and testable regardless of the deployment context. This method not only simplifies the development process but also enhances the application's adaptability.