Django Integration
This guide shows how to integrate genro-mail-proxy with a Django application, and compares the approach with Celery-based alternatives.
When to use the proxy with Django
Consider genro-mail-proxy when:
You don’t want to introduce Celery just for email
You need delivery reports with automatic callback to your app
Multiple services (not just Django) share the same email infrastructure
You need rate limiting shared across Django instances
Multi-tenancy is required (multiple organizations, one proxy)
When to use Celery instead:
Celery is already in your stack for other tasks
You prefer Django’s standard
send_mail()APIYour team is familiar with Celery monitoring (Flower, etc.)
Comparison with Celery
Feature |
Celery + django-celery-email |
genro-mail-proxy |
|---|---|---|
Dependencies |
Redis/RabbitMQ + Celery worker |
Only the proxy (SQLite internal) |
Django API |
Standard |
HTTP client (custom) |
Delivery reports |
Manual implementation required |
Built-in HTTP callback |
Retry on failure |
Configurable via Celery |
Built-in with exponential backoff |
Rate limiting |
Manual implementation |
Built-in per account |
Deferred state |
No |
Yes (rate limiting) |
Send history |
Only with result backend |
Built-in (send_log table) |
Multi-tenant |
No (one backend per project) |
Yes |
Monitoring |
Flower, Celery events |
Prometheus metrics |
Operational complexity |
High (broker + worker) |
Medium (one service) |
The key difference is delivery reports: with Celery you know if the task
succeeded (the SMTP call didn’t raise an exception), but you don’t get automatic
notification back to your application. The proxy’s proxy_sync cycle handles
this automatically.
Installation
pip install httpx # HTTP client for the proxy
Configuration
Add to your Django settings:
# settings.py
MAIL_PROXY_URL = "http://localhost:8000"
MAIL_PROXY_TOKEN = "your-api-token"
MAIL_PROXY_ACCOUNT = "default" # SMTP account ID in the proxy
Client module
Create a client module to interact with the proxy:
# myapp/mail_proxy.py
"""Django client for genro-mail-proxy."""
import uuid
import httpx
from django.conf import settings
class MailProxyClient:
"""Client for sending emails through genro-mail-proxy."""
def __init__(self):
self.base_url = settings.MAIL_PROXY_URL
self.token = settings.MAIL_PROXY_TOKEN
self.default_account = getattr(settings, "MAIL_PROXY_ACCOUNT", "default")
def _headers(self):
return {"X-API-Token": self.token, "Content-Type": "application/json"}
def send_mail(
self,
subject: str,
body: str,
from_email: str,
to: list[str],
cc: list[str] | None = None,
bcc: list[str] | None = None,
html_body: str | None = None,
attachments: list[dict] | None = None,
priority: int = 2,
account_id: str | None = None,
message_id: str | None = None,
) -> dict:
"""Send an email through the mail proxy.
Args:
subject: Email subject.
body: Plain text body.
from_email: Sender address.
to: List of recipient addresses.
cc: List of CC addresses (optional).
bcc: List of BCC addresses (optional).
html_body: HTML body (optional, replaces plain text).
attachments: List of attachment dicts (optional).
priority: 0=immediate, 1=high, 2=medium (default), 3=low.
account_id: SMTP account to use (defaults to MAIL_PROXY_ACCOUNT).
message_id: Custom message ID (auto-generated if not provided).
Returns:
Dict with "queued" count and "rejected" list.
"""
message = {
"id": message_id or str(uuid.uuid4()),
"account_id": account_id or self.default_account,
"from": from_email,
"to": to,
"subject": subject,
"body": html_body or body,
"content_type": "html" if html_body else "plain",
"priority": priority,
}
if cc:
message["cc"] = cc
if bcc:
message["bcc"] = bcc
if attachments:
message["attachments"] = attachments
with httpx.Client(timeout=10) as client:
response = client.post(
f"{self.base_url}/commands/add-messages",
headers=self._headers(),
json={"messages": [message]},
)
response.raise_for_status()
return response.json()
# Singleton instance
mail_proxy = MailProxyClient()
Usage in views
# myapp/views.py
from django.http import JsonResponse
from django.views import View
from .mail_proxy import mail_proxy
class SendWelcomeEmailView(View):
def post(self, request, user_id):
from django.contrib.auth import get_user_model
User = get_user_model()
user = User.objects.get(pk=user_id)
result = mail_proxy.send_mail(
subject=f"Welcome {user.first_name}!",
body=f"Hello {user.first_name}, thanks for signing up.",
from_email="noreply@example.com",
to=[user.email],
priority=1, # high priority
)
return JsonResponse(result)
class SendInvoiceView(View):
def post(self, request, invoice_id):
from .models import Invoice
invoice = Invoice.objects.get(pk=invoice_id)
# Attachment via HTTP endpoint (proxy calls your server)
result = mail_proxy.send_mail(
subject=f"Invoice #{invoice.number}",
body=f"Please find attached invoice #{invoice.number}.",
html_body=f"<p>Please find attached invoice <strong>#{invoice.number}</strong>.</p>",
from_email="billing@example.com",
to=[invoice.customer.email],
attachments=[
{
"filename": f"invoice_{invoice.number}.pdf",
"storage_path": f"invoice_id={invoice.id}",
"fetch_mode": "endpoint",
}
],
)
return JsonResponse(result)
Receiving delivery reports
The proxy sends delivery reports to your configured client_sync_path.
Create an endpoint to receive them. Important: Authenticate the request
to ensure it comes from your mail proxy instance.
# myapp/views.py
import json
from functools import wraps
from django.conf import settings
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
from django.http import JsonResponse, HttpResponseForbidden
def verify_proxy_auth(view_func):
"""Decorator to verify mail proxy authentication."""
@wraps(view_func)
def wrapper(request, *args, **kwargs):
# Check Bearer token (configured in tenant's auth_token)
auth_header = request.headers.get("Authorization", "")
expected_token = getattr(settings, "MAIL_PROXY_SYNC_TOKEN", None)
if expected_token:
if not auth_header.startswith("Bearer "):
return HttpResponseForbidden("Missing authorization")
token = auth_header[7:] # Remove "Bearer " prefix
if token != expected_token:
return HttpResponseForbidden("Invalid token")
return view_func(request, *args, **kwargs)
return wrapper
@csrf_exempt
@require_POST
@verify_proxy_auth
def mail_delivery_report(request):
"""Receive delivery reports from the mail proxy."""
data = json.loads(request.body)
sent = 0
error = 0
deferred = 0
for report in data.get("delivery_report", []):
message_id = report["id"]
if report.get("sent_ts"):
sent += 1
# Update your model
# EmailLog.objects.filter(message_id=message_id).update(
# status="sent", sent_at=datetime.fromtimestamp(report["sent_ts"])
# )
elif report.get("error_ts"):
error += 1
error_message = report.get("error", "Unknown error")
# EmailLog.objects.filter(message_id=message_id).update(
# status="error", error_message=error_message
# )
elif report.get("deferred_ts"):
deferred += 1
# Message was rate-limited, will be retried
return JsonResponse({"ok": True, "queued": 0})
Serving attachments
If you use the @params format for attachments, create an endpoint that
serves the file content:
# myapp/views.py
from django.http import HttpResponse
from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_POST
@csrf_exempt
@require_POST
def attachment_fetch(request):
"""Serve attachment content to the mail proxy."""
invoice_id = request.POST.get("invoice_id")
if invoice_id:
from .models import Invoice
invoice = Invoice.objects.get(pk=invoice_id)
pdf_content = invoice.generate_pdf()
return HttpResponse(pdf_content, content_type="application/pdf")
return HttpResponse(status=404)
URL configuration
# myapp/urls.py
from django.urls import path
from . import views
urlpatterns = [
path("mail/welcome/<int:user_id>/", views.SendWelcomeEmailView.as_view()),
path("mail/invoice/<int:invoice_id>/", views.SendInvoiceView.as_view()),
path("mail/delivery-report/", views.mail_delivery_report),
path("mail/attachments/", views.attachment_fetch),
]
Proxy tenant configuration
Configure the proxy tenant to point to your Django endpoints:
mail-proxy myserver tenants add myapp \
--base-url "https://myapp.example.com" \
--sync-path "/mail/delivery-report/" \
--attachment-path "/mail/attachments/" \
--auth-method bearer \
--auth-token "shared-secret"
Celery comparison example
For reference, here’s how the same functionality looks with Celery:
# settings.py (Celery approach)
EMAIL_BACKEND = 'djcelery_email.backends.CeleryEmailBackend'
CELERY_EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
CELERY_BROKER_URL = 'redis://localhost:6379/0'
# views.py (Celery approach)
from django.core.mail import send_mail
def send_welcome(request, user_id):
user = User.objects.get(pk=user_id)
# Returns immediately, task queued in Redis
send_mail(
subject=f"Welcome {user.first_name}!",
message="Thanks for signing up.",
from_email="noreply@example.com",
recipient_list=[user.email],
)
# No delivery report - you don't know if it was actually sent
return JsonResponse({"ok": True})
The Celery approach is simpler to set up if you already have Celery. With django-celery-results you can also track task results, though delivery reports require custom implementation rather than being built-in.
Tracking email status
To track email status in your Django models:
# myapp/models.py
from django.db import models
class EmailLog(models.Model):
"""Track email delivery status."""
STATUS_CHOICES = [
("queued", "Queued"),
("sent", "Sent"),
("error", "Error"),
("deferred", "Deferred"),
]
message_id = models.CharField(max_length=100, unique=True, db_index=True)
recipient = models.EmailField()
subject = models.CharField(max_length=255)
status = models.CharField(max_length=20, choices=STATUS_CHOICES, default="queued")
error_message = models.TextField(blank=True)
queued_at = models.DateTimeField(auto_now_add=True)
sent_at = models.DateTimeField(null=True, blank=True)
class Meta:
ordering = ["-queued_at"]
Update the client to create log entries:
# myapp/mail_proxy.py (extended)
def send_mail_tracked(self, subject, body, from_email, to, **kwargs):
"""Send email and create tracking record."""
from .models import EmailLog
import uuid
message_id = str(uuid.uuid4())
# Create log entry first
for recipient in to:
EmailLog.objects.create(
message_id=message_id,
recipient=recipient,
subject=subject,
)
# Send through proxy
return self.send_mail(
subject=subject,
body=body,
from_email=from_email,
to=to,
message_id=message_id,
**kwargs
)
Then update the delivery report handler to update the log entries.