Skip to content

Email

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:

EMAIL_PROVIDER=smtp
EMAIL_URL=smtp://user:password@smtp.example.com:587/?tls=True

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

EMAIL_PROVIDER=sendgrid
SENDGRID_API_KEY=your-key

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

EMAIL_PROVIDER=postmark
POSTMARK_SERVER_TOKEN=your-server-token

Resend

EMAIL_PROVIDER=resend
RESEND_API_KEY=your-key

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
DEFAULT_FROM_EMAIL = env.str("DEFAULT_FROM_EMAIL", default="admin@example.com")
SERVER_EMAIL = env.str("SERVER_EMAIL", default=DEFAULT_FROM_EMAIL)