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:

SettingHolds
EMAIL_HOSTThe SMTP server's hostname, e.g. smtp.yourprovider.com
EMAIL_PORTThe SMTP port — commonly 587 for TLS
EMAIL_HOST_USERThe account username used to authenticate with the mail server
EMAIL_HOST_PASSWORDThe account's password or API key
EMAIL_USE_TLSTrue to encrypt the connection to the mail server
Note: 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.

Note: This is correct, real email-sending code — but this website's own project doesn't run a Django server or a mail backend, so no message actually gets composed or sent on this page. Set EMAIL_BACKEND to the console backend in your own project to see it working risk-free.
Next up: A task-assignment email points a user back to a URL inside TaskFlow. Sitemaps & SEO covers making sure those URLs — and any public-facing pages — are discoverable in the first place.