Skip to main content

Unified Python email library with multi-provider support (SendGrid, SES, Postmark, Mailgun, Brevo, SMTP)

Project description

MailBridge ๐Ÿ“ง

CI PyPI version License: MIT

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/await API via AsyncMailBridge
  • ๐Ÿ”ง 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: AsyncMailBridge works without the [async] extra โ€” it falls back to a thread pool executor for all providers. Install mailbridge[async] to get native non-blocking I/O via aiohttp and aiosmtplib.


๐ŸŽฏ 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

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

mailbridge-2.1.1.tar.gz (193.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

mailbridge-2.1.1-py3-none-any.whl (29.3 kB view details)

Uploaded Python 3

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

Hashes for mailbridge-2.1.1.tar.gz
Algorithm Hash digest
SHA256 e76c411b3ae50753866336b75a3ec40994c8c0e1fcc4badc3f5f0dd59843badc
MD5 d53e1773d69e4a5bd622c7098a74ebf5
BLAKE2b-256 e5d0c5b58633b2d34f43d89a06ab474a2dac81d273c8d0018fedb9e359b9353a

See more details on using hashes here.

Provenance

The following attestation bundles were made for mailbridge-2.1.1.tar.gz:

Publisher: test_publish.yaml on fastkit-org/mailbridge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for mailbridge-2.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 dfe0461025fc855737e29a99084f2305b134d32903a05a54dce6ba40d8623b0d
MD5 4f9fe9039fc44c0ed9580d62daa67ec7
BLAKE2b-256 ff775f22040742c57e054953ea703f0132ce68a5b79ab172a37833d0241f5639

See more details on using hashes here.

Provenance

The following attestation bundles were made for mailbridge-2.1.1-py3-none-any.whl:

Publisher: test_publish.yaml on fastkit-org/mailbridge

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page