Sending emails is a core requirement for almost every web application—whether it’s for password resets, OTP verification, or invitations. Django provides a powerful, flexible email wrapper that makes this process seamless.
In this guide, we’ll move from "just getting it to work" to professional, template-based email delivery.
1. The Foundation: Configuring Your Settings
Before you write a single line of Python, Django needs to know how to send the mail. This is handled in settings.py.
For Development (The Console Backend)
You don’t want to spam your actual inbox while testing. Use the console backend to print emails directly to your terminal.
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
For Production (S…
Sending emails is a core requirement for almost every web application—whether it’s for password resets, OTP verification, or invitations. Django provides a powerful, flexible email wrapper that makes this process seamless.
In this guide, we’ll move from "just getting it to work" to professional, template-based email delivery.
1. The Foundation: Configuring Your Settings
Before you write a single line of Python, Django needs to know how to send the mail. This is handled in settings.py.
For Development (The Console Backend)
You don’t want to spam your actual inbox while testing. Use the console backend to print emails directly to your terminal.
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
For Production (SMTP)
When you’re ready to go live, you’ll likely use an SMTP provider (like Gmail, SendGrid, or Mailgun).
# settings.py
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_HOST = 'smtp.gmail.com'
EMAIL_PORT = 587
EMAIL_USE_TLS = True
EMAIL_HOST_USER = 'your-email@gmail.com'
EMAIL_HOST_PASSWORD = 'your-app-password' # Use an App Password, not your login password!
DEFAULT_FROM_EMAIL = 'Your App Name <noreply@yourdomain.com>'
2. Level 1: Hardcoded Content (The Quick Way)
If you are sending a very simple notification, you can keep the logic inside your views.py. We use EmailMultiAlternatives to ensure we send both a plain-text version (for safety) and an HTML version.
from django.core.mail import EmailMultiAlternatives
from django.conf import settings
def send_simple_otp(user, otp_code):
subject = "Your Verification Code"
from_email = settings.DEFAULT_FROM_EMAIL
to = [user.email]
# Plain text fallback
text_content = f"Your code is {otp_code}. It expires in 10 minutes."
# HTML version string
html_content = f"""
<html>
<body>
<h2 style="color: #2c3e50;">Verification Code</h2>
<p>Your code is: <strong>{otp_code}</strong></p>
</body>
</html>
"""
msg = EmailMultiAlternatives(subject, text_content, from_email, to)
msg.attach_alternative(html_content, "text/html")
msg.send()
3. Level 2: Decoupling with Templates (The Professional Way)
Hardcoding HTML inside Python is messy. As your emails grow (adding headers, footers, and branding), you should use Django’s template engine.
Folder Structure
Create a dedicated folder for email templates within your app:
your_app/templates/emails/otp_email.html
The Template (otp_email.html)
<!DOCTYPE html>
<html>
<head>
<style>
.code { font-size: 20px; color: blue; }
</style>
</head>
<body>
<p>Hi {{ first_name }},</p>
<p>Your requested code is: <span class="code">{{ otp_code }}</span></p>
</body>
</html>
The View Logic
We use render_to_string to turn that HTML file into a string for our email.
from django.template.loader import render_to_string
from django.utils.html import strip_tags
def send_template_email(user, otp_code):
context = {
'first_name': user.first_name,
'otp_code': otp_code,
}
# Render the HTML template
html_content = render_to_string('emails/otp_email.html', context)
# Automatically generate the plain-text version from the HTML
text_content = strip_tags(html_content)
msg = EmailMultiAlternatives("Your Code", text_content, settings.DEFAULT_FROM_EMAIL, [user.email])
msg.attach_alternative(html_content, "text/html")
msg.send()
4. Advanced: Static Content (Logos & CSS)
This is where most beginners get stuck. Emails are not web pages. They are viewed in external clients (Gmail, Outlook) that cannot access your local static files.
🖼️ Handling Images (Logos)
Recipients cannot see <img src="/static/logo.png">. You have two choices:
- Absolute URLs: Host the image on a public server (S3, Cloudinary) and use the full
https://path. - CID Attachment: Embedding the image as an attachment (more complex).
Best Practice: Host your logo on a public CDN and hardcode the absolute URL in your template:
<img src="https://your-cdn.com/logo.png" width="150" alt="MyBrand">
🎨 Handling CSS
Email clients often strip out <style> tags or <link> tags.
- Always use Inline CSS. * Instead of
.btn { color: red; }, use<a style="color: red;">.
🛡️ Best Practices Checklist
- Fail Silently? Never set
fail_silently=Truein development. You want to see the errors (like authentication failure) so you can fix them. - Background Tasks: Sending an email is slow (1-3 seconds). Use Celery or Django-Q to send emails in the background so the user doesn’t have to wait for the page to reload.
- Plain Text Fallback: Always provide a
text_contentversion. It improves your spam score. - Security: Never store your
EMAIL_HOST_PASSWORDdirectly insettings.py. Use environment variables (.envfiles).
Conclusion
Start with the Console Backend to test your logic, move to templates for better organization, and always remember that inline styles are your best friend for email design.
Happy Coding! 🚀