New guide: how to warm up a dedicated IP without hurting your reputation. Read now →
Log in
MigoSMTP
SMTP and APIUpdated 26 Oct 2026

Django Send Email with SMTP: Settings, send_mail, HTML and Attachments

Configure Django to send email through an SMTP relay, use send_mail and EmailMultiAlternatives for HTML and attachments, test with the locmem backend, move sending to a background worker and fix common errors.

KM
Written by Karan Malhotra
API and Developer Experience Lead
Reviewed by Mateus Oliveira
  • Published2 Jun 2026
  • Read time8 min
  • Length1,699 words
Share this guide

Django has email support built in. You do not need a third-party package to send a password-reset message, an order receipt or a contact-form notification: you configure an email backend in settings.py, call send_mail() or build an EmailMultiAlternatives object, and Django hands the message to your SMTP server. This guide covers the settings that matter, the difference between TLS and SSL in Django's configuration, how to send plain text, HTML and attachments, how to test mail without sending anything, how to move sending out of the request cycle and how to fix the errors that appear most often. The examples use placeholders. Take the real host, user name and password from your SMTP provider's dashboard and keep them in environment variables, not in the repository.

SettingsEMAIL_BACKEND, EMAIL_HOST, EMAIL_PORT, EMAIL_USE_TLS or EMAIL_USE_SSL
Sendsend_mail() for simple mail, EmailMultiAlternatives for HTML and files
Testlocmem backend and django.core.mail.outbox
ScaleMove sending to a background worker

The settings you need

Django reads email configuration from settings.py. For SMTP, set the backend and the connection details, and read the secrets from the environment:

# settings.py
import os

EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = os.environ["EMAIL_HOST"]            # e.g. smtp.example.com
EMAIL_PORT = int(os.environ.get("EMAIL_PORT", "587"))
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]
EMAIL_USE_TLS = True                              # STARTTLS, normally port 587
EMAIL_USE_SSL = False                             # implicit TLS, normally port 465
EMAIL_TIMEOUT = 20                                # seconds

DEFAULT_FROM_EMAIL = "Example App <[email protected]>"
SERVER_EMAIL = "[email protected]"               # used for error mail to ADMINS
EMAIL_SUBJECT_PREFIX = "[Example] "               # used by mail_admins()

Two details prevent most first-time failures. First, EMAIL_USE_TLS and EMAIL_USE_SSL are mutually exclusive: Django raises an error if both are true. Use EMAIL_USE_TLS = True with port 587, or EMAIL_USE_SSL = True with port 465, never both. Second, set EMAIL_TIMEOUT. Without a timeout a blocked outbound port can hold a web worker for a long time while it waits for the connection, and that is how a mail problem becomes a site-wide slowdown. If you are not sure which port your server allows, read which SMTP port to use.

Use DEFAULT_FROM_EMAIL on purpose

If you do not pass from_email, Django uses DEFAULT_FROM_EMAIL. The address must belong to a domain you have authenticated with your sending provider, otherwise the server may refuse it or the receiving mailbox may mark it as suspicious. SERVER_EMAIL is different: it is the sender of error reports that Django sends to the addresses in ADMINS, so give it a monitored address on the same domain. The guide on authenticating your sending domain explains the DNS records.

Send a simple message

send_mail() is the shortest route. It returns the number of messages delivered to the server, which is 1 on success. With fail_silently=False, the default, it raises an exception when the connection or the login fails, which is what you want in most code paths.

from django.core.mail import send_mail

sent = send_mail(
    subject="Your order has shipped",
    message="Hi Alex, your order #1042 is on its way.",
    from_email=None,                      # uses DEFAULT_FROM_EMAIL
    recipient_list=["[email protected]"],
    fail_silently=False,
)

Keep the subject on one line. Django rejects header values that contain newlines and raises BadHeaderError, which protects you from header injection when you build a subject from user input. Do not try to bypass this check.

Send HTML with a plain-text alternative

For HTML, use EmailMultiAlternatives. Put a plain-text body in the message and attach the HTML as an alternative. Mail clients that cannot show HTML use the text part, and mailbox filters tend to trust a message that includes both.

from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string

def send_welcome(user):
    ctx = {"name": user.first_name}
    text = render_to_string("emails/welcome.txt", ctx)
    html = render_to_string("emails/welcome.html", ctx)

    msg = EmailMultiAlternatives(
        subject="Welcome to Example",
        body=text,
        to=[user.email],
        reply_to=["[email protected]"],
    )
    msg.attach_alternative(html, "text/html")
    msg.send()

Build the two templates from the same context so that the text and the HTML say the same thing. Use inline styles and simple table layouts in the HTML, because many email clients ignore external CSS. Our HTML email template guide lists what renders reliably.

Attach files

Use attach_file() for a file on disk or attach() for content in memory. Always pass the MIME type for generated content so that the recipient's client can open it.

msg.attach_file("/srv/invoices/1042.pdf")                     # from disk
msg.attach("report.csv", csv_text, "text/csv")              # in memory

Keep attachments small. Large files make messages slow to send and are more likely to be rejected, and a link to a download page is often a better choice. As a rule of thumb, treat anything above a few megabytes as a candidate for a link.

Send several messages over one connection

Each call to send_mail() opens and closes an SMTP connection unless you pass one. To send a batch, open one connection and reuse it:

from django.core.mail import get_connection, EmailMessage

with get_connection() as conn:
    messages = [
        EmailMessage("Hello", body, to=[addr], connection=conn)
        for addr, body in rows
    ]
    conn.send_messages(messages)

send_mass_mail() does something similar for tuples of data. Both are fine for a few hundred messages, but neither is a substitute for a queue. For larger volumes, follow the pattern in rate limits, retries and backoff.

Test without sending real mail

You should not send real messages from a test run. Django's test runner automatically replaces the backend with the in-memory locmem backend and stores sent messages in django.core.mail.outbox, so you can assert on them:

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

class WelcomeTests(TestCase):
    def test_welcome_is_sent(self):
        send_welcome(self.user)
        self.assertEqual(len(mail.outbox), 1)
        self.assertEqual(mail.outbox[0].subject, "Welcome to Example")
        self.assertEqual(mail.outbox[0].to, [self.user.email])

In development, set EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" to print messages to the terminal, or the filebased backend with EMAIL_FILE_PATH to save them as files. To check a real SMTP account before you wire it into the app, use the steps in how to test an SMTP connection.

Move sending out of the request

An SMTP conversation takes anywhere from a fraction of a second to many seconds when the network is slow. If a view sends the message before it returns a response, the visitor waits for it, and a mail outage becomes a site outage. Send in the background. The usual approach is a task queue such as Celery, RQ or Django's own task framework in recent releases: the view enqueues a task with the data it needs, and a worker builds and sends the message. Pass identifiers, not objects, so the worker loads fresh data. Make the task safe to retry, record the outcome and treat temporary failures differently from permanent ones. If a view must send in the request, such as a password reset, keep EMAIL_TIMEOUT short and show a friendly message when sending fails. Django also offers transaction.on_commit(), which is useful so that you only enqueue the task after the database transaction that created the record has been committed.

Use a different account for one message

Some projects send transactional mail through one account and marketing mail through another. You do not need a second settings file. Build a connection with explicit credentials and pass it to the message, and Django uses it instead of the defaults for that message only. This is also the cleanest way to keep password-reset mail on a separate sending domain from newsletters, which protects the reputation of the transactional domain.

from django.core.mail import get_connection, send_mail

conn = get_connection(
    host="smtp.example.com", port=587,
    username=user, password=pw, use_tls=True, timeout=20,
)
send_mail("Reset your password", body, "[email protected]",
          [address], connection=conn)

Django's built-in authentication emails, such as the password-reset flow, use DEFAULT_FROM_EMAIL unless you pass from_email to the form. Check the sender of those messages as well as your own, because they are often the first message a new user receives. Also remember that mail_admins() and mail_managers() use SERVER_EMAIL and the ADMINS and MANAGERS lists, so make sure those are set correctly in production, or error reports will go nowhere.

Common errors and fixes

SymptomLikely causeFix
SMTPAuthenticationError: (535, ...)Wrong user or password, or a truncated environment variableRe-copy the credentials and print their length in a shell. See SMTP error 535
ConnectionRefusedError or timeoutWrong host or port, or the outbound port is blockedTest the port from the same server and check the firewall
ssl.SSLError: WRONG_VERSION_NUMBEREMAIL_USE_SSL used with port 587Use EMAIL_USE_TLS with 587 or EMAIL_USE_SSL with 465
ValueError: EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusiveBoth flags are trueEnable only one
SMTPSenderRefusedThe From domain is not verified for this accountSend from an authenticated domain
BadHeaderErrorA newline in a subject or header valueStrip newlines from user-supplied header values
Mail works in the shell, not in productionEnvironment variables not loaded by the process managerCheck the service environment and restart the workers
Mail is accepted but lands in spamMissing authentication or a poor sender reputationPublish SPF, DKIM and DMARC; see the spam checklist

Security and hygiene

Keep credentials out of version control and rotate them if they are ever exposed. Do not log message bodies that contain tokens or personal data. Validate addresses before you send, and avoid sending to addresses that you know are invalid, because repeated bounces damage your reputation; the guide on email validation before sending lists useful checks. Protect public forms with rate limits and a CAPTCHA or token check, because an open contact form can be abused to send spam in your name.

Send Django mail through a managed relay Use the SMTP host and credentials from your dashboard and send your first message in minutes.

Explore the SMTP relay

References and further reading

Frequently asked questions

Why does Django say EMAIL_USE_TLS and EMAIL_USE_SSL are mutually exclusive?

They select different ways of encrypting the connection. Use EMAIL_USE_TLS with port 587 for STARTTLS, or EMAIL_USE_SSL with port 465 for implicit TLS, and never enable both.

How do I send HTML email in Django?

Create an EmailMultiAlternatives object with a plain-text body, call attach_alternative with the HTML and the text/html type, then call send.

How can I test Django email without sending it?

Django tests use the locmem backend automatically. Sent messages are stored in django.core.mail.outbox, where you can assert on them. In development you can use the console backend.

Should I send email inside a Django view?

Only when you must. For anything that does not need to be immediate, enqueue a background task so that a slow SMTP server does not slow the response.

Where should I keep the SMTP password in Django?

In an environment variable or a secrets manager that settings.py reads at startup, never in the repository.

Topics#django#python#smtp#email backend#developers
Was this guide helpful?Your answer helps us improve our documentation.

Start sending in minutes.

Create an account, add your domain and send your first email.

Get started Talk to sales