Docs / Django

Send email in Django over SMTP

Django ships with a working email layer — django.core.mail — so sending email from a Django app is a matter of pointing settings.py at an SMTP server and calling send_mail(). This guide configures Postwing as that server and then covers the sends you actually need in production: HTML email, Django templates, attachments, bulk sending and moving the whole thing into a Celery worker.

You can get them on the token management page. For security reasons, a token is shown only once — at the moment it is created.

SMTP connection settings

SettingValue
SMTP hostsmtp.postwing.app
Port587
EncryptionSTARTTLS (the connection is upgraded to TLS before login)
UsernameThe login of an SMTP token for your domain
PasswordThe password of that token — shown once, when the token is created
Every mode is also available on a high port: 8465 (SSL/TLS), 8587 (STARTTLS) and 8025 (plain). Many hosting providers and clouds block outbound 25, 465 and 587 — if the connection times out, switch to the matching high port.

Configure Django in settings.py

Add the following to settings.py. Credentials come from the environment rather than the file itself, so they never reach your repository:

settings.py
# settings.py
import os

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.postwing.app"
EMAIL_PORT = 587
EMAIL_USE_TLS = True          # STARTTLS on port 587
EMAIL_USE_SSL = False         # mutually exclusive with EMAIL_USE_TLS
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]
EMAIL_TIMEOUT = 10

DEFAULT_FROM_EMAIL = "Acme <noreply@your-domain.com>"
SERVER_EMAIL = DEFAULT_FROM_EMAIL   # used for Django's error mails

Alongside it, in your .env or your process manager:

.env
EMAIL_HOST_USER=token-login@your-domain.com
EMAIL_HOST_PASSWORD=your-token-password

To use implicit SSL/TLS on port 465 instead of STARTTLS, swap the three port-related settings. EMAIL_USE_TLS and EMAIL_USE_SSL are mutually exclusive — setting both to True makes Django raise an error at startup:

python
EMAIL_PORT = 465
EMAIL_USE_TLS = False
EMAIL_USE_SSL = True
The From: address has to be on a domain you have verified, so that DKIM signing and SPF alignment apply to it. Sending as @gmail.com or @yandex.ru through a third-party relay fails DMARC and goes straight to spam.

Send your first email

send_mail() is the shortest path — subject, body, sender, recipients:

python
from django.core.mail import send_mail

send_mail(
    subject="Your order #4417 is confirmed",
    message="Thanks! Your order ships tomorrow.",
    from_email=None,           # falls back to DEFAULT_FROM_EMAIL
    recipient_list=["customer@example.com"],
    fail_silently=False,
)

Keep fail_silently=False. With True, Django swallows every SMTP error and a broken configuration looks exactly like a working one. To check the setup end to end without writing a view:

bash
python manage.py shell -c "from django.core.mail import send_mail; \
send_mail('Test', 'It works.', None, ['you@example.com'], fail_silently=False)"

Send an HTML email

send_mail() only sends plain text. For HTML use EmailMultiAlternatives, which produces a multipart/alternative message with both parts. Always include the plain-text alternative — a message with no text part is a strong spam signal, and some clients will not render anything else:

python
from django.core.mail import EmailMultiAlternatives

text = "Thanks! Your order ships tomorrow."
html = "<h1>Order confirmed</h1><p>Your order ships tomorrow.</p>"

msg = EmailMultiAlternatives(
    subject="Your order #4417 is confirmed",
    body=text,                 # plain-text part, always include one
    to=["customer@example.com"],
)
msg.attach_alternative(html, "text/html")
msg.send()

Send email from a Django template

Hardcoded HTML in a view gets unmaintainable fast. Render a normal Django template instead and derive the text part from it:

python
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
from django.utils.html import strip_tags

html = render_to_string("email/order_confirmed.html", {"order": order})

msg = EmailMultiAlternatives(
    subject=f"Your order #{order.id} is confirmed",
    body=strip_tags(html),
    to=[order.customer.email],
)
msg.attach_alternative(html, "text/html")
msg.send()

Send to multiple recipients

Passing several addresses in recipient_list puts them all in one To: header, where every recipient sees the others. For anything user-facing, send one message per recipient and reuse a single SMTP connection so you are not reconnecting in a loop:

python
from django.core.mail import EmailMultiAlternatives, get_connection

# One SMTP connection, one message per recipient.
connection = get_connection()
connection.open()

for user in users:
    EmailMultiAlternatives(
        subject="Your weekly report",
        body=render_report(user),
        to=[user.email],       # exactly one recipient per message
        connection=connection,
    ).send()

connection.close()
Attention!
When using BCC, CC - a separate email will be created for each recipient in the system and your limits will be used.

Send an email with an attachment

EmailMessage attaches files either from disk or from bytes you built in memory:

python
from django.core.mail import EmailMessage

msg = EmailMessage(
    subject="Your invoice",
    body="The invoice for March is attached.",
    to=["customer@example.com"],
)

# From a file on disk
msg.attach_file("/srv/invoices/2026-03.pdf")

# Or from bytes you generated in memory
msg.attach("invoice.pdf", pdf_bytes, "application/pdf")

msg.send()

Send email in the background with Celery

Django's SMTP backend is blocking: the request waits for the connection, the handshake and the server's reply. Move the send into a Celery task so the user gets their response immediately and a transient SMTP failure becomes a retry instead of a 500:

tasks.py
# tasks.py
from celery import shared_task
from django.core.mail import EmailMultiAlternatives


@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def send_order_confirmation(self, order_id: int) -> None:
    order = Order.objects.get(pk=order_id)
    msg = EmailMultiAlternatives(
        subject=f"Your order #{order.id} is confirmed",
        body=render_confirmation(order),
        to=[order.customer.email],
    )
    try:
        msg.send()
    except Exception as exc:      # SMTP hiccup — retry with backoff
        raise self.retry(exc=exc)
python
# views.py — returns immediately, the email goes out in a worker
send_order_confirmation.delay(order.id)

Test without sending real email

In development, swap the backend so messages are printed instead of delivered:

python
# settings/dev.py — print emails to the console instead of sending
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"

In tests you do not have to change anything — Django substitutes the in-memory backend and collects messages in django.core.mail.outbox:

python
from django.core import mail
from django.test import TestCase


class OrderEmailTests(TestCase):
    def test_confirmation_is_sent(self):
        place_order(self.customer)
        self.assertEqual(len(mail.outbox), 1)
        self.assertIn("confirmed", mail.outbox[0].subject)

Troubleshooting

ErrorCause and fix
SMTPAuthenticationError Wrong token login or password. Both come from the token page — the password is displayed once, at creation.
SMTPServerDisconnected / connection timeout Your host blocks outbound mail ports. Change EMAIL_PORT to 8587 (STARTTLS) or 8465 (SSL/TLS).
SMTPNotSupportedError: STARTTLS extension not supported You set EMAIL_USE_TLS = True on port 465. That port is implicit TLS — use EMAIL_USE_SSL = True instead.
SMTPSenderRefused (550) The From: domain is not verified, or the address is on a domain your token does not cover.
Mail is accepted but never arrives Check the delivery log in the dashboard for a bounce, and confirm your DNS records are still published.
SMTPRecipientsRefused The recipient address is malformed or on your suppression list after an earlier hard bounce.

Frequently asked questions

Which port should I use for Django, 587 or 465?

Use 587 with EMAIL_USE_TLS = True — that is STARTTLS, the mode Django's SMTP backend is most commonly configured for. Port 465 also works; set EMAIL_USE_SSL = True and EMAIL_USE_TLS = False instead. Never set both flags to True, Django raises an error if you do.

Why does Django raise SMTPAuthenticationError?

The username or password is wrong. EMAIL_HOST_USER must be the full login of an SMTP token for your domain, and EMAIL_HOST_PASSWORD the password shown once when that token was created. If you lost the password, create a new token rather than guessing — it is not stored in recoverable form.

Why do my Django emails time out on the server but work locally?

Most cloud providers block outbound connections on ports 25, 465 and 587. Switch EMAIL_PORT to the matching high port — 8587 for STARTTLS or 8465 for SSL/TLS — and the connection will go through.

Should I send email inside the request/response cycle?

No. Django's send_mail is blocking, so a slow SMTP handshake becomes a slow page. Hand the send to Celery, Django-Q or django-mailer and return the response immediately. Sending in a background task also gives you retries when the SMTP server is briefly unavailable.

How do I stop Django emails from landing in spam?

Send from a domain you have verified, so DKIM and SPF are published and DMARC aligns. Set DEFAULT_FROM_EMAIL to an address on that domain — not to a Gmail or Yandex address — and always include a plain-text part alongside the HTML.

Can I test email in Django without actually sending?

Yes. Set EMAIL_BACKEND to django.core.mail.backends.console.EmailBackend in development to print messages to stdout. In tests Django swaps in the locmem backend automatically and collects everything in django.core.mail.outbox.

Next steps