Overview¶
SpeedPy uses django-post_office as the email backend, with Celery for async delivery. Emails are composed using Django templates and sent via post_office.mail.send().
The sending provider behind post_office is pluggable: a single EMAIL_PROVIDER environment variable selects which backend actually delivers the mail. Console and SMTP use Django's built-in backends; the hosted providers (SES, Mailgun, SendGrid, Postmark, Resend) go through django-anymail, which gives every provider a consistent settings shape.
Choosing a provider¶
Set EMAIL_PROVIDER to one of the supported values. It defaults to console, so fresh installs boot and "send" to stdout with no configuration.
EMAIL_PROVIDER=console # dev default — prints emails to the console
EMAIL_PROVIDER=smtp # uses EMAIL_URL
EMAIL_PROVIDER=ses # AWS SES (via anymail)
EMAIL_PROVIDER=mailgun
EMAIL_PROVIDER=sendgrid
EMAIL_PROVIDER=postmark
EMAIL_PROVIDER=resend
EMAIL_PROVIDER |
Backend | Credentials |
|---|---|---|
console |
django.core.mail.backends.console.EmailBackend |
none (dev default) |
smtp |
django.core.mail.backends.smtp.EmailBackend |
EMAIL_URL |
ses |
anymail.backends.amazon_ses.EmailBackend |
AWS_SES_* / IAM |
mailgun |
anymail.backends.mailgun.EmailBackend |
API key + sender domain |
sendgrid |
anymail.backends.sendgrid.EmailBackend |
API key |
postmark |
anymail.backends.postmark.EmailBackend |
server token |
resend |
anymail.backends.resend.EmailBackend |
API key |
An unknown EMAIL_PROVIDER value raises ImproperlyConfigured at startup with the list of valid values, so misconfiguration fails loudly instead of silently dropping email.
Info
Provider credentials have change_me-style placeholder defaults so the app boots without them. A real API provider will only send once you configure real credentials and a verified sender/domain — until then sends fail at delivery time.
How it fits together¶
The outer Django EMAIL_BACKEND always stays post_office.EmailBackend (the queuing + Celery layer). EMAIL_PROVIDER only changes the inner backend post_office delegates to, stored in POST_OFFICE["BACKENDS"]["default"]. In project/settings.py:
EMAIL_PROVIDER = env.str("EMAIL_PROVIDER", default="console").lower().strip()
EMAIL_BACKEND = "post_office.EmailBackend"
POST_OFFICE = {
"BACKENDS": {
"default": resolve_email_backend(EMAIL_PROVIDER), # project/email_providers.py
},
"DEFAULT_PRIORITY": "now",
"CELERY_ENABLED": True,
}
The provider → backend map lives in project/email_providers.py.
Provider configuration¶
Console (development)¶
The default. Prints emails to the console — no env vars required.
SMTP¶
Set EMAIL_PROVIDER=smtp and configure the connection via EMAIL_URL:
If EMAIL_URL is unset or empty, the default (smtp://user:password@localhost:25) is used.
AWS SES¶
EMAIL_PROVIDER=ses
AWS_SES_ACCESS_KEY_ID=your-key
AWS_SES_SECRET_ACCESS_KEY=your-secret
AWS_SES_REGION_NAME=eu-central-1
SES is delivered through Anymail's amazon_ses backend (the django-anymail[amazon-ses] extra pulls in boto3). The credentials above are passed to Anymail via ANYMAIL["AMAZON_SES_CLIENT_PARAMS"]; you can also omit them and rely on the standard AWS credential chain (IAM role, ~/.aws/credentials, etc.).
Mailgun¶
EMAIL_PROVIDER=mailgun
MAILGUN_API_KEY=your-key
MAILGUN_SENDER_DOMAIN=mg.example.com
# For EU-region accounts:
# MAILGUN_API_URL=https://api.eu.mailgun.net/v3
SendGrid¶
Warning
Anymail currently flags its SendGrid support as no longer actively tested. SendGrid is still wired up out of the box, but verify it in your own deployment before relying on it heavily.
Postmark¶
Resend¶
Sending Emails¶
Use Django's render_to_string for templates and post_office.mail.send() for delivery:
from django.template.loader import render_to_string
from django.conf import settings
from post_office import mail
context = {
'team_name': 'My Team',
'invite_url': 'https://example.com/invite/abc123',
}
subject = "You've been invited to join My Team"
html_message = render_to_string("emails/team_invitation.html", context=context)
mail.send(
'recipient@example.com',
settings.DEFAULT_FROM_EMAIL,
html_message=html_message,
subject=subject,
priority='now',
)
Info
Do not use post_office's built-in template system. Instead, use Django's render_to_string to prepare the HTML message.
Email Templates¶
Place email templates in templates/emails/. SpeedPy includes templates for:
- Team invitations
- Role change notifications