Django already ships with send_mail() — configure SMTP once in settings.py and you can send email with a single call. Use it for welcome emails after registration, order confirmations, password resets, or system notifications. No extra library to install, and no hand-rolled SMTP code with smtplib.
The Problem
Many new projects call send_mail() right away without configuring anything, assuming Django “just knows” where to send mail. In reality Django defaults to an SMTP backend pointing at localhost:25 — most dev machines have no mail server, so the request hangs and then fails with ConnectionRefusedError, and users who just registered wait forever for a welcome email that never arrives. Another common mistake is hardcoding the email password straight into settings.py and accidentally committing it to Git — the mailbox gets exposed and abused to send spam.
from django.core.mail import send_mail
send_mail(
"Chao mung ban den voi MyShop",
"Cam on ban da dang ky tai khoan.",
"noreply@example.com",
["khach@example.com"],
fail_silently=False,
)
# ConnectionRefusedError: [Errno 111] Connection refused (localhost:25)
The Solution
The right approach is to move the configuration into environment variables and let Django handle the SMTP part. In settings.py, declare the SMTP backend with its host, port, and credentials — the password comes from an environment variable, never written directly into the code. For local development, switch to the console backend so mail is printed to the terminal instead of actually sent, and you never risk emailing real users by accident.
import os
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_HOST = "smtp.gmail.com"
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]
DEFAULT_FROM_EMAIL = "MyShop <noreply@example.com>"
# Dev local: in mail ra terminal thay vi gui that
# EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
After that, anywhere in your code — a view, a signal, or a management command — one send_mail() call with four arguments is enough: subject, plain-text body, sender address, and the list of recipients. The function returns the number of emails sent successfully, usually 1; if SMTP fails and fail_silently=False (the default), Django raises an exception so you can handle it instead of silently swallowing the error. For a nicer HTML version, pass html_message — Django automatically attaches the plain-text body as a fallback for old mail clients.
from django.core.mail import send_mail
sent = send_mail(
"Don hang #123 da duoc xac nhan",
"Don hang #123 cua ban da duoc xac nhan.",
"noreply@example.com",
["khach@example.com"],
html_message="<h1>Don hang #123</h1><p>Cam on ban da mua hang.</p>",
)
print(sent) # 1
Apply It to Your Project
Wire send_mail() into the right touchpoints: a welcome email after user registration (combined with the post_save signal), a confirmation email after an order is created, a password-reset email. Sending over SMTP usually takes 1–3 seconds, so don’t call it directly in a view if you don’t want users waiting — push it to a background task or run it after the transaction commits successfully, and always log failures so you can resend later.
Related Posts
Flask: Sending Email with Flask-Mail — Don’t Let SMTP Block Your Requests (5 Minutes)
FastAPI: Handle Heavy Work in the Background with BackgroundTasks — Clients Don’t Have to Wait
Django: Run Tasks After Successful Commit with transaction.on_commit in 5 Minutes
Frequently Asked Questions
Can I send email with Gmail?
Yes. Use host smtp.gmail.com, port 587, enable TLS, and use an App Password instead of your main password — Gmail has blocked sign-in with regular passwords since 2022. Remember to turn on two-step verification before creating an App Password.
How do I test sending email without actually sending it?
Set EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' in your dev settings — mail will be printed to the terminal instead of leaving your machine. To assert on mail in tests, use the locmem backend and inspect django.core.mail.outbox.
Can send_mail() send to many people at once?
Yes — pass a list of addresses to recipient_list and they all receive the same email. If you need to send a bulk batch where each person gets different content, use send_mass_mail() to open a single SMTP connection instead of reopening one per email.
What if my emails keep landing in spam?
Check three things: the sender address must belong to a domain you own, that domain must have SPF/DKIM/DMARC configured, and the content should avoid spam-like wording. Also, sending from a low-reputation SMTP account is more likely to get filtered — consider a dedicated email-sending service when your volume grows.
💬 Have you tried this in your own project? If you hit an error or know a shorter way to do it, leave a comment below — I read every one and will fold the best ideas into the post.
References
Django documentation — Sending email: https://docs.djangoproject.com/en/stable/topics/email/
Django documentation — django.core.mail: https://docs.djangoproject.com/en/stable/topics/email/#send-mail
Originally published at linhfish.dev.
Top comments (1)
The
on_commitboundary is important, and an outbox row makes it durable: write the intended message and a business idempotency key in the same transaction, then let a worker claim and send it. That avoids both failure modes of a background call alone—email for a transaction that later rolls back, or a committed order that never gets a notification when the process dies between commit and send.