Sending Email
TaskFlow already has a natural moment that deserves an email: someone gets assigned a task. Django
ships a full email-sending toolkit in django.core.mail, built so you write the same
Python code in development and production — only a single setting changes to decide whether a
message actually leaves your server or just prints to your terminal.
Sending a Simple Email with send_mail()
send_mail() is the most direct way to send a plain-text email — subject, message
body, sender, and a list of recipients, in one function call. Here it is inside a view that runs
right after a task gets assigned to someone:
# taskflow/views.py
from django.core.mail import send_mail
def notify_task_assigned(task, assignee):
send_mail(
subject=f"You've been assigned: {task.title}",
message=f"Hi {assignee.username}, you've been assigned the task "
f"'{task.title}' in project '{task.project.name}'.",
from_email='notifications@taskflow.example.com',
recipient_list=[assignee.email],
fail_silently=False,
)
recipient_list always takes a list, even for a single recipient. fail_silently
=False is worth calling out deliberately — set to False (or left off, since
that's the default), a broken mail configuration raises a real exception you'll notice; set to
True, delivery failures vanish silently, which is rarely what you want for something
as important as a task-assignment notice.
The console Backend: Testing Email Without a Mail Server
Actually sending real email while developing TaskFlow locally would mean a working SMTP server, real credentials, and an inbox to check after every test — friction nobody wants for a quick check that the message looks right. Django's console backend solves this by printing every outgoing email straight to your terminal instead of sending it anywhere:
# taskflow_project/settings.py
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
With that one line set, calling send_mail() anywhere in the project behaves exactly
the same from the calling code's point of view — same arguments, same return value — but the
subject, headers, and body simply print to the console running your development server, which is
genuinely all you need to verify the content and recipients are correct before wiring up a real
mail server.
Configuring a Real Backend for Production
In production, swapping to the SMTP backend (Django's default when
EMAIL_BACKEND isn't set at all) means providing the connection details for a real
mail server:
| Setting | Holds |
|---|---|
EMAIL_HOST | The SMTP server's hostname, e.g. smtp.yourprovider.com |
EMAIL_PORT | The SMTP port — commonly 587 for TLS |
EMAIL_HOST_USER | The account username used to authenticate with the mail server |
EMAIL_HOST_PASSWORD | The account's password or API key |
EMAIL_USE_TLS | True to encrypt the connection to the mail server |
EMAIL_HOST_USER and EMAIL_HOST_PASSWORD
are exactly the kind of sensitive values that don't belong hardcoded in settings.py or
committed to version control — Environment Configuration
covers loading values like these from environment variables instead.Sending HTML Email with EmailMultiAlternatives
send_mail() only sends plain text. A nicer-looking notification — TaskFlow's logo,
a styled button linking straight to the task — needs EmailMultiAlternatives, which
lets you attach an HTML version alongside a plain-text fallback for mail clients that
don't render HTML:
# taskflow/views.py
from django.core.mail import EmailMultiAlternatives
def notify_task_assigned(task, assignee):
text_body = f"You've been assigned '{task.title}' in {task.project.name}."
html_body = f"<p>You've been assigned "
f"<strong>{task.title}</strong> in {task.project.name}.</p>"
email = EmailMultiAlternatives(
subject=f"You've been assigned: {task.title}",
body=text_body,
from_email='notifications@taskflow.example.com',
to=[assignee.email],
)
email.attach_alternative(html_body, "text/html")
email.send()
The plain-text version is set as the message's normal body; the HTML version is
added on top with attach_alternative(). A mail client capable of HTML shows the
styled version; one that isn't falls back to the plain text automatically — no extra logic
needed on your side.
Rendering an Email's Body from a Template
Building HTML with string concatenation, as above, gets unpleasant fast. The cleaner approach
renders the body from an ordinary Django template using render_to_string() — the
same template engine used for every page in this track, just rendered to a string instead of an
HTTP response:
{# taskflow/templates/emails/task_assigned_email.html #}
<p>Hi {{ assignee.username }},</p>
<p>You've been assigned <strong>{{ task.title }}</strong>
in project <strong>{{ task.project.name }}</strong>.</p>
<p>Due: {{ task.due_date }}</p>
# taskflow/views.py
from django.core.mail import EmailMultiAlternatives
from django.template.loader import render_to_string
def notify_task_assigned(task, assignee):
html_body = render_to_string('emails/task_assigned_email.html', {
'task': task,
'assignee': assignee,
})
email = EmailMultiAlternatives(
subject=f"You've been assigned: {task.title}",
body=f"You've been assigned '{task.title}' in {task.project.name}.",
from_email='notifications@taskflow.example.com',
to=[assignee.email],
)
email.attach_alternative(html_body, "text/html")
email.send()
This is a meaningful improvement over hardcoding HTML in Python: the email's design lives in an ordinary template file, editable without touching view code, and it can use every DTL feature already covered in Django Template Language — loops, filters, conditionals — the exact same way a regular page template would.
Third-Party Services for Production Email
Raw SMTP works, but at real-world scale most production Django projects hand email off to a
dedicated transactional email service instead — SendGrid,
Mailgun, or Amazon SES being the names you'll see most often.
These exist because reliable delivery is harder than it looks: getting flagged as spam, managing
bounce and complaint rates, and keeping a sending domain's reputation clean are all problems a
dedicated provider has already solved. Each integrates with Django through its own backend
package that plugs into the same EMAIL_BACKEND setting used above — the
send_mail() and EmailMultiAlternatives code you just wrote doesn't
change at all when you switch providers.
EMAIL_BACKEND to the console backend in your own project to see it
working risk-free.