Implementing a Mail Delivery Switch in Python for Local and AWS Environments Using Amazon SES

kojiisd

Koji Ishida

Posted on June 24, 2024

Implementing a Mail Delivery Switch in Python for Local and AWS Environments Using Amazon SES

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:

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

Enter fullscreen mode Exit fullscreen mode

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:

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

Enter fullscreen mode Exit fullscreen mode

Sample JSON Request

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

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

Enter fullscreen mode Exit fullscreen mode

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

Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
kojiisd
Koji Ishida

Posted on June 24, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related