Docs / mailbridge / Environments

Environments

One of the core strengths of MailBridge is being able to use different email providers in different environments without changing a single line of application code. SMTP with Mailhog locally, SendGrid or Brevo in production — driven entirely by environment variables.

The Problem

Without a unified interface, switching providers between environments means touching application code or maintaining provider-specific branches. A typical project ends up with something like:

python
# Without MailBridge — provider-specific code everywhere
if settings.ENV == 'local':
    import smtplib
    # SMTP-specific setup...
elif settings.ENV == 'production':
    from sendgrid import SendGridAPIClient
    from sendgrid.helpers.mail import Mail
    # SendGrid-specific setup...

# Different call signatures, different response formats,
# different error handling — all duplicated per provider.

This creates friction: every provider switch requires code changes, and local behavior diverges from production.

The Pattern

With MailBridge, the provider is just configuration. Your application code never changes — only the environment variables do:

python
# mail.py — written once, never touched again
import os
from mailbridge import MailBridge

mailer = MailBridge(
    provider=os.getenv('MAIL_PROVIDER', 'smtp'),
    host=os.getenv('MAIL_HOST', 'localhost'),
    port=int(os.getenv('MAIL_PORT', 1025)),
    username=os.getenv('MAIL_USERNAME', ''),
    password=os.getenv('MAIL_PASSWORD', ''),
    api_key=os.getenv('MAIL_API_KEY', ''),
    from_email=os.getenv('MAIL_FROM', 'noreply@yourdomain.com')
)
python
# Your application code — identical in every environment
from mail import mailer

mailer.send(
    to=user.email,
    subject='Welcome!',
    template_id='welcome-email',
    template_data={'name': user.name}
)

The only thing that differs between local, staging, and production is the .env file.

Local Development with Mailhog

Mailhog is a local SMTP server that catches all outgoing emails and displays them in a web UI — nothing ever reaches a real inbox. It's the recommended tool for local email development.

Start Mailhog

bash
docker run -p 1025:1025 -p 8025:8025 mailhog/mailhog

SMTP listens on :1025, web UI at localhost:8025.

bash
brew install mailhog
mailhog
bash
# Download from https://github.com/mailhog/MailHog/releases
./MailHog

Configure MailBridge to use it

bash
# .env.local
MAIL_PROVIDER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM=dev@localhost

All emails sent by MailBridge will be caught by Mailhog and visible in the web UI — no real emails are sent, no API keys required.

Mailhog accepts any username/password — leave them empty. It does not require TLS either, so use_tls should be omitted or set to False.

Environment Files

A typical project has three .env files — one per environment. Only the values change, never the application code:

bash
# .env.local — Mailhog, no credentials needed
MAIL_PROVIDER=smtp
MAIL_HOST=localhost
MAIL_PORT=1025
MAIL_USERNAME=
MAIL_PASSWORD=
MAIL_FROM=dev@localhost
bash
# .env.staging — real provider, staging API key
MAIL_PROVIDER=sendgrid
MAIL_API_KEY=SG.staging_key_xxxxx
MAIL_FROM=staging@yourdomain.com
bash
# .env.production — production API key and sender
MAIL_PROVIDER=sendgrid
MAIL_API_KEY=SG.production_key_xxxxx
MAIL_FROM=noreply@yourdomain.com

Load the right file at startup using python-dotenv or your framework's config system:

python
from dotenv import load_dotenv
import os

load_dotenv(f".env.{os.getenv('APP_ENV', 'local')}")

Django & Flask

python
# settings.py
from mailbridge import MailBridge

MAILER = MailBridge(
    provider=env('MAIL_PROVIDER', default='smtp'),
    host=env('MAIL_HOST', default='localhost'),
    port=env.int('MAIL_PORT', default=1025),
    username=env('MAIL_USERNAME', default=''),
    password=env('MAIL_PASSWORD', default=''),
    api_key=env('MAIL_API_KEY', default=''),
    from_email=env('MAIL_FROM', default='noreply@yourdomain.com'),
)
python
# views.py or services.py
from django.conf import settings

settings.MAILER.send(
    to=user.email,
    subject='Welcome!',
    template_id='welcome-email',
    template_data={'name': user.name}
)
python
# app.py
import os
from flask import Flask
from mailbridge import MailBridge

app = Flask(__name__)

mailer = MailBridge(
    provider=os.getenv('MAIL_PROVIDER', 'smtp'),
    host=os.getenv('MAIL_HOST', 'localhost'),
    port=int(os.getenv('MAIL_PORT', 1025)),
    username=os.getenv('MAIL_USERNAME', ''),
    password=os.getenv('MAIL_PASSWORD', ''),
    api_key=os.getenv('MAIL_API_KEY', ''),
    from_email=os.getenv('MAIL_FROM', 'noreply@yourdomain.com'),
)
python
# routes.py
from app import mailer

@app.route('/register', methods=['POST'])
def register():
    user = create_user(request.json)
    mailer.send(
        to=user.email,
        subject='Welcome!',
        template_id='welcome-email',
        template_data={'name': user.name}
    )
    return jsonify({"id": user.id})

Staging & Production

A common setup is to use the same provider in staging and production but with different API keys and sender addresses — so staging emails never reach real users:

bash
# Environment summary
#
# Local      → SMTP + Mailhog   (no real emails, no API key)
# Staging    → SendGrid         (real provider, staging key, limited recipients)
# Production → SendGrid         (real provider, production key, full send)
#
# Application code is identical across all three.

You can also switch providers entirely between environments — for example, use Brevo in staging because it has a generous free tier, and SendGrid in production for deliverability. MailBridge makes this a one-line config change:

bash
# .env.staging
MAIL_PROVIDER=brevo
MAIL_API_KEY=xkeysib-staging-xxxxx
MAIL_FROM=staging@yourdomain.com

# .env.production
MAIL_PROVIDER=sendgrid
MAIL_API_KEY=SG.production_xxxxx
MAIL_FROM=noreply@yourdomain.com

Unused config keys are silently ignored by each provider — passing api_key to the SMTP provider or host to SendGrid causes no errors. This means you can keep all possible keys in a single config block and let the provider pick what it needs.