Unified Python email library with multi-provider support (SendGrid, SES, Postmark, Mailgun, Brevo, SMTP)
Project description
MailBridge ๐ง
Unified Python email library with multi-provider support
MailBridge is a flexible Python library for sending emails, allowing you to use multiple providers through a single, simple interface. It supports SMTP, SendGrid, Mailgun, Amazon SES, Postmark, and Brevo โ with both synchronous and asynchronous APIs.
โจ Features
- ๐จ Template Support โ Use dynamic templates with all major providers
- ๐ Attachment Support โ Add file attachments to any email
- ๐ฆ Bulk Sending โ Send thousands of emails efficiently with native API optimizations
- โก Async Support โ First-class
async/awaitAPI viaAsyncMailBridge - ๐ง Unified Interface โ Same code works with any provider
- โ Fully Tested โ 220+ unit tests, 92% coverage
- ๐ Production Ready โ Battle-tested and reliable
- ๐ Great Documentation โ Extensive examples and guides
๐ฆ Installation
MailBridge uses uv for dependency management. If you don't have uv installed:
curl -LsSf https://astral.sh/uv/install.sh | sh
Installing the package
# Core install (SMTP, SendGrid, Mailgun, Postmark, Brevo work out of the box)
uv add mailbridge
# With async support (aiohttp + aiosmtplib)
uv add "mailbridge[async]"
# With Amazon SES support
uv add "mailbridge[ses]"
# Everything
uv add "mailbridge[all]"
pip (alternative)
pip install mailbridge
pip install "mailbridge[async]" # async support
pip install "mailbridge[ses]" # Amazon SES
pip install "mailbridge[all]" # everything
๐ Quick Start
Synchronous
from mailbridge import MailBridge
mailer = MailBridge(provider='sendgrid', api_key='your-api-key')
response = mailer.send(
to='recipient@example.com',
subject='Hello from MailBridge!',
body='<h1>It works!</h1><p>Email sent successfully.</p>'
)
print(f"Sent! Message ID: {response.message_id}")
Asynchronous
import asyncio
from mailbridge import AsyncMailBridge
async def main():
async with AsyncMailBridge(provider='sendgrid', api_key='your-api-key') as mailer:
response = await mailer.send(
to='recipient@example.com',
subject='Hello from MailBridge!',
body='<h1>It works!</h1><p>Email sent successfully.</p>'
)
print(f"Sent! Message ID: {response.message_id}")
asyncio.run(main())
โก AsyncMailBridge
AsyncMailBridge mirrors the MailBridge API exactly โ every method that exists on the sync client has an await-able counterpart on the async client. Both clients share the same provider registry, so register_provider works for both.
When to use the async client
Use AsyncMailBridge whenever your application already runs an event loop โ FastAPI, Starlette, Sanic, or any other async framework. Firing email sends with await means the event loop is never blocked, and bulk sends run all requests concurrently.
Single email
import asyncio
from mailbridge import AsyncMailBridge
async def send_welcome(user_email: str, user_name: str):
async with AsyncMailBridge(provider='sendgrid', api_key='SG.xxxxx') as mailer:
return await mailer.send(
to=user_email,
subject='Welcome!',
template_id='d-welcome-template',
template_data={'name': user_name}
)
asyncio.run(send_welcome('user@example.com', 'Alice'))
Bulk sending
import asyncio
from mailbridge import AsyncMailBridge, EmailMessageDto
async def send_newsletter(subscribers: list[dict]):
messages = [
EmailMessageDto(
to=sub['email'],
template_id='newsletter-template',
template_data={'name': sub['name']}
)
for sub in subscribers
]
async with AsyncMailBridge(provider='sendgrid', api_key='SG.xxxxx') as mailer:
result = await mailer.send_bulk(messages)
print(f"Sent: {result.successful}/{result.total}, Failed: {result.failed}")
asyncio.run(send_newsletter([...]))
Bulk sends fire all requests concurrently via asyncio.gather โ for HTTP providers (SendGrid, Mailgun, Brevo, Postmark) this means one aiohttp.ClientSession is shared across all concurrent requests. SMTP bulk sends reuse a single async SMTP connection for the entire batch.
FastAPI integration
from contextlib import asynccontextmanager
from fastapi import FastAPI
from mailbridge import AsyncMailBridge
mailer: AsyncMailBridge | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global mailer
mailer = AsyncMailBridge(provider='sendgrid', api_key='SG.xxxxx')
yield
await mailer.close()
app = FastAPI(lifespan=lifespan)
@app.post("/register")
async def register(email: str, name: str):
await mailer.send(
to=email,
subject='Welcome!',
template_id='d-welcome-template',
template_data={'name': name}
)
return {"status": "ok"}
Async vs sync โ which to choose?
MailBridge |
AsyncMailBridge |
|
|---|---|---|
| API style | Synchronous | async/await |
| Best for | Scripts, Django, Flask | FastAPI, Starlette, asyncio apps |
| Bulk concurrency | Sequential | Concurrent (asyncio.gather) |
| SES (boto3) | Direct call | Thread pool (boto3 has no async SDK) |
Requires [async] extra |
No | Yes (for native I/O) |
Note:
AsyncMailBridgeworks without the[async]extra โ it falls back to a thread pool executor for all providers. Installmailbridge[async]to get native non-blocking I/O viaaiohttpandaiosmtplib.
๐ฏ Supported Providers
| Provider | Templates | Bulk API | Async I/O |
|---|---|---|---|
| SendGrid | โ | โ Native | โ
aiohttp |
| Amazon SES | โ | โ Native | โ Thread pool |
| Postmark | โ | โ Native | โ
aiohttp |
| Mailgun | โ | โ Native | โ
aiohttp |
| Brevo | โ | โ Native | โ
aiohttp |
| SMTP | โ | โ | โ
aiosmtplib |
๐ Provider Setup
SendGrid
from mailbridge import MailBridge
mailer = MailBridge(
provider='sendgrid',
api_key='SG.xxxxx',
from_email='noreply@yourdomain.com'
)
Amazon SES
mailer = MailBridge(
provider='ses',
aws_access_key_id='AKIAXXXX',
aws_secret_access_key='xxxxx',
region_name='us-east-1',
from_email='verified@yourdomain.com'
)
# Or using IAM role (EC2/Lambda) โ no credentials needed
mailer = MailBridge(
provider='ses',
region_name='us-east-1',
from_email='verified@yourdomain.com'
)
Note: Email addresses must be verified in sandbox mode. Request production access to send to any address.
Postmark
mailer = MailBridge(
provider='postmark',
server_token='xxxxx-xxxxx',
from_email='verified@yourdomain.com',
track_opens=True,
track_links='HtmlAndText'
)
Mailgun
mailer = MailBridge(
provider='mailgun',
api_key='key-xxxxx',
endpoint='https://api.mailgun.net/v3/mg.yourdomain.com',
from_email='noreply@yourdomain.com'
)
Brevo
mailer = MailBridge(
provider='brevo',
api_key='xkeysib-xxxxx',
from_email='noreply@yourdomain.com'
)
# Template IDs are integers for Brevo
mailer.send(
to='user@example.com',
template_id=123,
template_data={'name': 'Alice'}
)
SMTP
# Gmail
mailer = MailBridge(
provider='smtp',
host='smtp.gmail.com',
port=587,
username='you@gmail.com',
password='app-password', # Use App Password, not your regular password
use_tls=True
)
# Outlook
mailer = MailBridge(
provider='smtp',
host='smtp.office365.com',
port=587,
username='you@outlook.com',
password='your-password',
use_tls=True
)
# Custom server with SSL
mailer = MailBridge(
provider='smtp',
host='mail.yourdomain.com',
port=465,
username='user',
password='pass',
use_ssl=True
)
Gmail: Use an App Password (requires 2FA enabled).
๐ก Common Use Cases
Welcome Emails
mailer.send(
to=new_user.email,
template_id='welcome-email',
template_data={
'name': new_user.name,
'activation_link': generate_activation_link(new_user)
}
)
Password Reset
mailer.send(
to=user.email,
template_id='password-reset',
template_data={
'reset_link': generate_reset_link(user),
'expiry_hours': 24
}
)
Newsletters (Bulk, async)
import asyncio
from mailbridge import AsyncMailBridge, EmailMessageDto
async def send_newsletter(subscribers):
messages = [
EmailMessageDto(
to=sub.email,
template_id='newsletter',
template_data={
'name': sub.name,
'unsubscribe_link': generate_unsubscribe_link(sub)
}
)
for sub in subscribers
]
async with AsyncMailBridge(provider='sendgrid', api_key='SG.xxxxx') as mailer:
result = await mailer.send_bulk(messages)
print(f"Sent: {result.successful}/{result.total}")
asyncio.run(send_newsletter(subscribers))
Transactional Notifications
mailer.send(
to=order.customer_email,
template_id='order-confirmation',
template_data={
'order_number': order.id,
'total': order.total,
'items': order.items,
'tracking_url': order.tracking_url
}
)
๐ง Advanced Features
Attachments
from pathlib import Path
mailer.send(
to='customer@example.com',
subject='Your Invoice',
body='<p>Please find your invoice attached.</p>',
attachments=[
Path('invoice.pdf'),
('report.csv', csv_bytes, 'text/csv'), # (filename, bytes, mimetype)
]
)
CC and BCC
mailer.send(
to='client@example.com',
subject='Project Update',
body='<p>Latest update...</p>',
cc=['manager@company.com', 'team@company.com'],
bcc=['archive@company.com']
)
Custom Headers and Tags
mailer.send(
to='user@example.com',
subject='Campaign Email',
body='<p>Special offer!</p>',
headers={'X-Campaign-ID': 'summer-2024'},
tags=['marketing', 'campaign']
)
Context Managers
# Sync
with MailBridge(provider='smtp', host='...', port=587, ...) as mailer:
mailer.send(to='user@example.com', subject='Test', body='...')
# Connection automatically closed
# Async
async with AsyncMailBridge(provider='sendgrid', api_key='...') as mailer:
await mailer.send(to='user@example.com', subject='Test', body='...')
# Async connection automatically closed
Custom Providers
from mailbridge import MailBridge
from mailbridge.providers.base_email_provider import BaseEmailProvider
from mailbridge.dto.email_message_dto import EmailMessageDto
from mailbridge.dto.email_response_dto import EmailResponseDTO
class MyProvider(BaseEmailProvider):
def _validate_config(self):
if 'api_key' not in self.config:
raise ConfigurationError("Missing api_key")
def send(self, message: EmailMessageDto) -> EmailResponseDTO:
# Your implementation
return EmailResponseDTO(success=True, provider='myprovider')
# Register once โ available to both MailBridge and AsyncMailBridge
MailBridge.register_provider('myprovider', MyProvider)
mailer = MailBridge(provider='myprovider', api_key='...')
๐งช Development Setup
MailBridge uses uv for dependency management.
# Install uv
curl -LsSf https://astral.sh/uv/install.sh | sh
# Clone and set up
git clone https://github.com/fastkit-org/mailbridge
cd mailbridge
# Create venv and install all dev dependencies
uv sync --extra dev
# Run tests
uv run pytest
# Run tests with coverage report
uv run pytest --cov=mailbridge --cov-report=html
# Linting and formatting
uv run black mailbridge tests
uv run isort mailbridge tests
uv run flake8 mailbridge
uv run mypy mailbridge
Running specific test suites
# All tests
uv run pytest
# Sync provider tests only
uv run pytest tests/test_sendgrid_provider.py tests/test_mailgun_provider.py -v
# Async tests only
uv run pytest tests/test_sendgrid_async.py tests/test_mailgun_async.py \
tests/test_brevo_async.py tests/test_postmark_async.py \
tests/test_smtp_async.py tests/test_ses_async.py \
tests/test_async_mailbridge_client.py -v
# Single file
uv run pytest tests/test_sendgrid_async.py -v
๐ Bulk Sending Performance
| Provider | Sync | Async |
|---|---|---|
| SendGrid | Native batch API | Concurrent via asyncio.gather |
| SES | 50-recipient batches | Concurrent thread pool |
| Postmark | Sequential | Concurrent via asyncio.gather |
| Mailgun | Sequential | Concurrent via asyncio.gather |
| Brevo | Native batch API | Native batch API async |
| SMTP | Single connection reuse | Single async connection reuse |
๐ License
MIT License โ see LICENSE for details.
๐ Support
- Issues: GitHub Issues
- Discussions: GitHub Discussions
- Changelog: CHANGELOG.md
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file mailbridge-2.1.1.tar.gz.
File metadata
- Download URL: mailbridge-2.1.1.tar.gz
- Upload date:
- Size: 193.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e76c411b3ae50753866336b75a3ec40994c8c0e1fcc4badc3f5f0dd59843badc
|
|
| MD5 |
d53e1773d69e4a5bd622c7098a74ebf5
|
|
| BLAKE2b-256 |
e5d0c5b58633b2d34f43d89a06ab474a2dac81d273c8d0018fedb9e359b9353a
|
Provenance
The following attestation bundles were made for mailbridge-2.1.1.tar.gz:
Publisher:
test_publish.yaml on fastkit-org/mailbridge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mailbridge-2.1.1.tar.gz -
Subject digest:
e76c411b3ae50753866336b75a3ec40994c8c0e1fcc4badc3f5f0dd59843badc - Sigstore transparency entry: 1496617602
- Sigstore integration time:
-
Permalink:
fastkit-org/mailbridge@feaf58729a69c19e2695344118f0c92c61d42369 -
Branch / Tag:
refs/tags/v2.1.1 - Owner: https://github.com/fastkit-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
test_publish.yaml@feaf58729a69c19e2695344118f0c92c61d42369 -
Trigger Event:
push
-
Statement type:
File details
Details for the file mailbridge-2.1.1-py3-none-any.whl.
File metadata
- Download URL: mailbridge-2.1.1-py3-none-any.whl
- Upload date:
- Size: 29.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
dfe0461025fc855737e29a99084f2305b134d32903a05a54dce6ba40d8623b0d
|
|
| MD5 |
4f9fe9039fc44c0ed9580d62daa67ec7
|
|
| BLAKE2b-256 |
ff775f22040742c57e054953ea703f0132ce68a5b79ab172a37833d0241f5639
|
Provenance
The following attestation bundles were made for mailbridge-2.1.1-py3-none-any.whl:
Publisher:
test_publish.yaml on fastkit-org/mailbridge
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
mailbridge-2.1.1-py3-none-any.whl -
Subject digest:
dfe0461025fc855737e29a99084f2305b134d32903a05a54dce6ba40d8623b0d - Sigstore transparency entry: 1496617680
- Sigstore integration time:
-
Permalink:
fastkit-org/mailbridge@feaf58729a69c19e2695344118f0c92c61d42369 -
Branch / Tag:
refs/tags/v2.1.1 - Owner: https://github.com/fastkit-org
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
test_publish.yaml@feaf58729a69c19e2695344118f0c92c61d42369 -
Trigger Event:
push
-
Statement type: