How to change your email provider in FastAPI in less than 30 minutes
The problem nobody talks about
How many times have you been in a situation where you're using one email provider locally (MailHog) and a bunch of mocks for integration tests, while another provider (SendGrid) powers your development, staging, and production environments?
Then suddenly, your client or PM decides they want to try a new provider — let's say Brevo — because it's cheaper and the UX looks nicer. "It should be a simple task," they say. And sure, as a developer, you know the drill: it's just another provider, nothing too dramatic… right?
Well, kind of. All you have to do is:
- read the documentation (again),
- create yet another formatter,
- build a new client with supported methods,
- adjust configuration across environments,
- and pray everything works on the first try.
Simple enough. 😄
Because I've been in this situation multiple times with different clients, I eventually decided to create a package that simplifies and speeds up the entire migration process. And that's how MailBridge was born — a unified Python email library that provides a single interface for multiple providers: SMTP, SendGrid, Mailgun, Amazon SES, Postmark, and Brevo.
Why MailBridge?
After switching email providers one too many times, I eventually asked myself:
"Why am I rewriting the same boilerplate over and over again?"
Every new provider required the same ritual: new client, new formatter, new config, new tests. Same structure, different docs, different tiny edge cases.
So instead of repeating that cycle for the next client, I decided to make something reusable — something that treats email providers the way ORMs treat databases: one unified interface, multiple interchangeable backends.
MailBridge gives you a single, clean API for sending emails through any supported provider — without forcing you to rewrite clients or learn each provider's quirks. Just install it, pick your provider, configure your environment variables, and you're good to go.
It also comes with built-in mocks and test utilities, so your tests stay fast and predictable without relying on external services.
What we're building today
To show you how MailBridge simplifies email integration, we'll build a password reset email system — the same feature, implemented five different ways:
By the end, you'll see exactly why switching providers with MailBridge takes minutes instead of hours.
Time needed: 30 minutes · Coffee: Recommended ☕
Setup (5 minutes)
Install dependencies
# Create project
mkdir email-migration-demo && cd email-migration-demo
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Install packages
pip install fastapi uvicorn mailbridge python-dotenv pydantic-settings sib_api_v3_sdk
Project structure
email-migration-demo/
├── app/
│ ├── __init__.py
│ ├── main.py
│ └── email_examples/
│ ├── __init__.py
│ ├── example_1_smtp.py
│ ├── example_2_brevo_native.py
│ ├── example_3_mailbridge_smtp.py
│ ├── example_4_mailbridge_brevo.py
│ └── example_5_mailbridge_template.py
└── requirements.txt
Example 1: Traditional SMTP — The Old Way
Let's start with how most of us first learned to send emails in Python.
app/email_examples/example_1_smtp.py
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
def send_password_reset_smtp(email: str, reset_token: str):
"""Send password reset email using traditional SMTP."""
smtp_host = "smtp.gmail.com"
smtp_port = 587
smtp_user = "your-email@gmail.com"
smtp_password = "your-app-password"
msg = MIMEMultipart('alternative')
msg['Subject'] = "Reset Your Password"
msg['From'] = smtp_user
msg['To'] = email
reset_link = f"https://yourapp.com/reset-password?token={reset_token}"
html = f"""
<html>
<body style="font-family: Arial, sans-serif;">
<h2>Password Reset Request</h2>
<p>Click the button below to reset your password:</p>
<a href="{reset_link}"
style="background: #007bff; color: white; padding: 10px 20px;
text-decoration: none; border-radius: 5px; display: inline-block;">
Reset Password
</a>
<p style="margin-top: 20px; color: #666;">
This link expires in 24 hours.
</p>
</body>
</html>
"""
part = MIMEText(html, 'html')
msg.attach(part)
try:
with smtplib.SMTP(smtp_host, smtp_port) as server:
server.starttls()
server.login(smtp_user, smtp_password)
server.send_message(msg)
print(f"✓ Password reset email sent to {email}")
return True
except Exception as e:
print(f"✗ Failed to send email: {e}")
return False
- Boilerplate everywhere — MIME types, attachments, encoding
- Error handling is manual
- No template support
- Switching to SendGrid? Rewrite everything.
Example 2: Brevo Native SDK — The Vendor-Specific Way
Now let's try Brevo's official SDK. "It'll be easier," they said.
app/email_examples/example_2_brevo_native.py
import sib_api_v3_sdk
from sib_api_v3_sdk.rest import ApiException
def send_password_reset_brevo_native(email: str, reset_token: str):
"""Send password reset email using Brevo's native SDK."""
configuration = sib_api_v3_sdk.Configuration()
configuration.api_key['api-key'] = 'your-brevo-api-key'
api_instance = sib_api_v3_sdk.TransactionalEmailsApi(
sib_api_v3_sdk.ApiClient(configuration)
)
reset_link = f"https://yourapp.com/reset-password?token={reset_token}"
send_smtp_email = sib_api_v3_sdk.SendSmtpEmail(
to=[{"email": email}],
sender={"email": "noreply@yourapp.com", "name": "Your App"},
subject="Reset Your Password",
html_content=f"""
<html><body style="font-family: Arial, sans-serif;">
<h2>Password Reset Request</h2>
<a href="{reset_link}" style="background: #007bff; color: white;
padding: 10px 20px; text-decoration: none; border-radius: 5px;">
Reset Password
</a>
<p style="color: #666;">This link expires in 24 hours.</p>
</body></html>
"""
)
try:
api_response = api_instance.send_transac_email(send_smtp_email)
print(f"✓ Password reset email sent: {api_response.message_id}")
return True
except ApiException as e:
print(f"✗ Failed to send email: {e}")
return False
- Cleaner than raw SMTP — no MIME types
- But you're now fully dependent on Brevo's SDK API
- Want to switch to SendGrid? Start over with a different SDK
- Tests require mocking Brevo-specific classes
Example 3: MailBridge + SMTP — Unified Interface
Now let's do the same thing with MailBridge. Watch the difference.
app/email_examples/example_3_mailbridge_smtp.py
from mailbridge import MailBridge
def send_password_reset_mailbridge_smtp(email: str, reset_token: str):
"""Send password reset email using MailBridge + SMTP."""
mailer = MailBridge(
provider='smtp',
host='smtp.gmail.com',
port=587,
username='your-email@gmail.com',
password='your-app-password',
from_email='noreply@yourapp.com',
use_tls=True
)
reset_link = f"https://yourapp.com/reset-password?token={reset_token}"
html_content = f"""
<html><body style="font-family: Arial, sans-serif;">
<h2>Password Reset Request</h2>
<a href="{reset_link}" style="background: #007bff; color: white;
padding: 10px 20px; text-decoration: none; border-radius: 5px;">
Reset Password
</a>
<p style="color: #666;">This link expires in 24 hours.</p>
</body></html>
"""
try:
response = mailer.send(
to=email,
subject="Reset Your Password",
body=html_content
)
print(f"✓ Password reset email sent: {response.message_id}")
return True
except Exception as e:
print(f"✗ Failed to send email: {e}")
return False
- No MIME types to worry about
- Clean, simple API — one method to send an email
- Same interface regardless of the provider underneath
- Error handling is consistent across all providers
Example 4: MailBridge + Brevo — Same Interface, Different Provider
Now here's where it gets interesting. Let's switch from SMTP to Brevo — without rewriting anything.
app/email_examples/example_4_mailbridge_brevo.py
from mailbridge import MailBridge
def send_password_reset_mailbridge_brevo(email: str, reset_token: str):
"""Send password reset email using MailBridge + Brevo."""
# THIS IS THE ONLY LINE THAT CHANGED compared to Example 3
mailer = MailBridge(
provider='brevo',
api_key='your-brevo-api-key',
from_email='noreply@yourapp.com'
)
# Same email content — no changes!
reset_link = f"https://yourapp.com/reset-password?token={reset_token}"
html_content = f"""
<html><body style="font-family: Arial, sans-serif;">
<h2>Password Reset Request</h2>
<a href="{reset_link}" style="background: #007bff; color: white;
padding: 10px 20px; text-decoration: none; border-radius: 5px;">
Reset Password
</a>
<p style="color: #666;">This link expires in 24 hours.</p>
</body></html>
"""
# Same send method — no changes!
try:
response = mailer.send(
to=email,
subject="Reset Your Password",
body=html_content
)
print(f"✓ Password reset email sent: {response.message_id}")
return True
except Exception as e:
print(f"✗ Failed to send email: {e}")
return False
Let's make this explicit. Here's the complete diff between Example 3 and Example 4:
- mailer = MailBridge(provider='smtp', host='smtp.gmail.com', port=587, ...)
+ mailer = MailBridge(provider='brevo', api_key='your-brevo-api-key', ...)
Example 5: MailBridge + Templates — Production Ready
Finally, let's use provider-hosted templates — the professional way to manage transactional emails. Marketing can update the design without any code changes.
First, create a template in Brevo
- Go to Brevo dashboard → Templates
- Create a new template
- Add variables:
{{params.reset_link}},{{params.user_name}}
app/email_examples/example_5_mailbridge_template.py
from mailbridge import MailBridge
def send_password_reset_mailbridge_template(
email: str,
user_name: str,
reset_token: str
):
"""Send password reset email using MailBridge + Brevo template."""
mailer = MailBridge(
provider='brevo',
api_key='your-brevo-api-key',
from_email='noreply@yourapp.com'
)
reset_link = f"https://yourapp.com/reset-password?token={reset_token}"
try:
response = mailer.send(
to=email,
subject='Reset your password',
body='', # Body comes from the template
template_id=123, # Your Brevo template ID (integer!)
template_data={
'user_name': user_name,
'reset_link': reset_link
}
)
print(f"✓ Password reset email sent: {response.message_id}")
return True
except Exception as e:
print(f"✗ Failed to send email: {e}")
return False
- No HTML in your code — designers update templates without deployments
- A/B testing is easy — swap template IDs
- All 5 providers support templates through the same interface
Want to switch to SendGrid templates?
# Just change the provider and template_id format
mailer = MailBridge(
provider='sendgrid',
api_key='SG.your-sendgrid-key',
from_email='noreply@yourapp.com'
)
# SendGrid template IDs start with 'd-'
response = mailer.send(
to=email,
subject='Reset your password',
body='',
template_id='d-your-sendgrid-template-id',
template_data={'user_name': user_name, 'reset_link': reset_link}
)
FastAPI Integration
Let's put it all together in a FastAPI app. The recommended pattern is to initialize AsyncMailBridge once via lifespan and inject it as a dependency.
app/main.py
from contextlib import asynccontextmanager
from fastapi import FastAPI, BackgroundTasks
from pydantic import BaseModel, EmailStr
from mailbridge import AsyncMailBridge
import secrets
mailer: AsyncMailBridge | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global mailer
mailer = AsyncMailBridge(
provider='brevo',
api_key='your-brevo-api-key',
from_email='noreply@yourapp.com'
)
yield
await mailer.close()
app = FastAPI(title="Email Migration Demo", lifespan=lifespan)
class PasswordResetRequest(BaseModel):
email: EmailStr
# In-memory storage — use a database in production!
reset_tokens = {}
@app.post("/forgot-password")
async def forgot_password(
request: PasswordResetRequest,
background_tasks: BackgroundTasks
):
"""Request password reset."""
reset_token = secrets.token_urlsafe(32)
reset_tokens[reset_token] = request.email
async def send_reset_email():
reset_link = f"https://yourapp.com/reset?token={reset_token}"
await mailer.send(
to=request.email,
subject="Reset Your Password",
body=f"""
<html><body style="font-family: Arial, sans-serif;">
<h2>Password Reset Request</h2>
<a href="{reset_link}"
style="background: #007bff; color: white; padding: 10px 20px;">
Reset Password
</a>
<p style="color: #666;">Link expires in 24 hours.</p>
</body></html>
"""
)
background_tasks.add_task(send_reset_email)
return {
"message": "If the email exists, a reset link was sent.",
"provider": "brevo"
}
@app.get("/")
def read_root():
return {
"message": "Email Migration Demo",
"endpoints": {"POST /forgot-password": "Request password reset"}
}
Start the server and test it
uvicorn app.main:app --reload
curl -X POST http://localhost:8000/forgot-password \
-H "Content-Type: application/json" \
-d '{"email": "user@example.com"}'
Visit http://localhost:8000/docs to try it directly in the Swagger UI.
Conclusion
From this example, you can clearly see that switching email providers no longer requires rewriting clients, touching multiple modules, or refactoring templates. With MailBridge, the entire change comes down to updating a couple of interface parameters — or just your .env variables.
In practice, that means you can switch an email provider in less than 30 minutes, including running tests and deploying to production — without stress, regressions, or boilerplate.
Found this helpful? ⭐ Star the repo and share with your team!