Sending Automated Emails with Django Signals

Django is an open-source python based web framework. It is designed to allow for rapid development of web applications by including much of the expected functionality...

Django allows for secure rapid development of web applications

Django is an open-source python based web framework. It is designed to allow for rapid development of web applications by including much of the expected functionality right out of the box.

One example of this functionality is the “signal dispatcher” which facilitates communication between disparate pieces of code in a Django project. As the blog post title indicates, we are going to use one of these signals to trigger Django’s send_mail() function. Django uses Python’s smtplib module to send mail but provides some wrappers on it to expand its functionality and make testing easier.

Many of the built-in signals in Django are associated with database models. There are signals related to the stages of the save() and delete() methods, e.g. django.db.models.signals.pre_save, django.db.models.signals.post_save, django.db.models.signals.pre_delete & django.db.models.signals.post_delete.

There are also signals that relate to HTTP requests, but for the purposes of this blog post, I am going to focus on models actions.

These signals pair naturally with behavior like sending an email. For example, we recently implemented In-App feedback/help functionality for one of our clients on their Django-based web app. They requested that any time new feedback or a new help request was submitted, their customer service team would be notified via email. Since the feedback/help request instance was stored in the database we settled on the post_save signal to trigger the email.

Let’s recreate a case similar to the one I described above. I am going to re-use the models I created for my previous blog post on Django admin inlines. Here is the models.py file for the Starfleet app:

from django.db import models

# Create your models here.

class Officer(models.Model):
    name = models.CharField(max_length=256)
    rank = models.ForeignKey('Rank', on_delete=models.CASCADE)
    ship_assignment = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True)

    def __str__(self):
        return self.name


class Rank(models.Model):
    name = models.CharField(max_length=256)

    def __str__(self):
        return self.name


class Ship(models.Model):
    name = models.CharField(max_length=256)
    registry = models.CharField(max_length=256)
    captain = models.OneToOneField('Officer', default=None, on_delete=models.CASCADE, blank=True, null=True)
    ship_class = models.CharField(max_length=256)
    status = models.CharField(max_length=256)

    def __str__(self):
        return self.name

Now let’s assume we want to send an email whenever a new officer is added to the Starfleet app. In order to implement this we are going to have to add some import statements to the top of the file:

from django.core.mail import send_mail

from django.db.models.signals import post_save

from django.dispatch import receiver

As I stated above, send_mail uses Python’s smtplib and unsurprisingly we’ll use it to send the email. We will use the post_save signal to trigger the event when the new officer model is saved. And we will use a receiver function to do the actual processing to call send_mail.

In order to connect the post_save signal to our receiver function, we are going to use a receiver() decorator. Putting it all together, the function looks like this:

@receiver(post_save, sender=Officer)
def send_new_officer_notification_email(sender, instance, created, **kwargs):

    # if a new officer is created, compose and send the email
    if created:
        name = instance.name if instance.name else "no name given"
        rank = instance.rank.name if instance.rank else "no rank given"
        ship_assignment = instance.ship_assignment.name if instance.ship_assignment else 'no ship assignment'

        subject = 'NAME: {0}, RANK: {1}, SHIP ASSIGNMENT: {2}'.format(name, rank, ship_assignment)
        message = 'A New Officer has been assigned!\n'
        message += 'NAME: ' + name + '\n' + 'RANK: ' \
                  + rank + '\n' + 'SHIP ASSIGNMENT: ' + ship_assignment + '\n'
        message += '--' * 30

        send_mail(
            subject,
            message,
            'your_email@example.com',
            ['recipeint1@xample.com', 'recipent2@xample.com '],
            fail_silently=False,
        )

Here’s a quick overview of the arguments for the receiver function send_new_officer_notification_email()

  • sender refers to the model class.
  • instance refers to the actual instance of the model that is being saved. We’re going to use its attributes to populate the email with relevant info.
  • created is a boolean value that is True if a new record is created (as opposed to an existing record being updated)

And as for **kwargs, this is a “wildcard” argument that can contain a dictionary of keywords. It’s not relevant to this case but if you leave this argument out, Django will throw an error.

If the save() call results in a new record being created, we’ll generate the message using python string formatting and the attributes of the officer class, “name, rank, ship_assignment”.

Here’s an overview of the arguments for the send_mail() function:

  • subject - the email subject
  • message - the email body
  • 'your_email@example.com' - the email from address
  • ['recipeint1@example.com', 'recipeint2@example.com'] - the email to addresses
  • fail_silently is a boolean. If it is false, send_mail() will raise an smtplib.SMTPException if there is an error.

This gives us a modified models.py file that looks like this:

from django.db import models
from django.core.mail import send_mail
from django.db.models.signals import post_save
from django.dispatch import receiver

# Create your models here.
class Officer(models.Model):
    name = models.CharField(max_length=256)
    rank = models.ForeignKey('Rank', on_delete=models.CASCADE)
    ship_assignment = models.ForeignKey('Ship', on_delete=models.CASCADE, blank=True, null=True)

    def __str__(self):
        return self.name


class Rank(models.Model):
    name = models.CharField(max_length=256)

    def __str__(self):
        return self.name


class Ship(models.Model):
    name = models.CharField(max_length=256)
    registry = models.CharField(max_length=256)
    captain = models.OneToOneField('Officer', default=None, on_delete=models.CASCADE, blank=True, null=True)
    ship_class = models.CharField(max_length=256)
    status = models.CharField(max_length=256)

    def __str__(self):
        return self.name


@receiver(post_save, sender=Officer)
def send_new_officer_notification_email(sender, instance, created, **kwargs):

    # if a new officer is created, compose and send the email
    if created:
        name = instance.name if instance.name else "no name given"
        rank = instance.rank.name if instance.rank else "no rank given"
        ship_assignment = instance.ship_assignment.name if instance.ship_assignment else 'no ship assignment'

        subject = 'NAME: {0}, RANK: {1}, SHIP ASSIGNMENT: {2}'.format(name, rank, ship_assignment)
        message = 'A New Officer has been assigned!\n'
        message += 'NAME: ' + name + '\n' + 'RANK: ' \
                  + rank + '\n' + 'SHIP ASSIGNMENT: ' + ship_assignment + '\n'
        message += '--' * 30

        send_mail(
            subject,
            message,
            'your_email@example.com',
            ['recipeint1@xample.com', 'recipent2@xample.com '],
            fail_silently=False,
        )

The last step we need to take is to add some values to the root settings.py file. The values you assign to these settings are going to vary depending on your system’s configuration and your email host’s configurations.

MAIL_HOST = 'email-smtp.your.email.host.com'
EMAIL_PORT = 465
EMAIL_HOST_USER = 'your.host.username'
EMAIL_HOST_PASSWORD = 'aReally$trongP4ssword'
EMAIL_USE_TLS = True
EMAIL_USE_SSL = False
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
MAILER_EMAIL_BACKEND = EMAIL_BACKEND

In this case, I have set EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' for testing purposes. This causes the email to be output to the Django console which is extremely handy.

Now I want to go into the admin console and create a new officer in order to trigger the email:

Screen Shot 2021-02-22 at 5.18.30 PM.png

add a new record to the "officer" model

Verify that the record was saved:

Screen Shot 2021-02-22 at 5.18.57 PM.png

new record saved

And we can expect to see the formatted email as output in the console:

Screen Shot 2021-02-22 at 5.19.46 PM.png

Console output

To clarify the above screenshot, this is the output:

Content-Type: text/plain; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: NAME: Beverly Crusher, M.D., RANK: Commander,
 SHIP ASSIGNMENT: USS Enterprise
From: your_email@example.com
To: recipeint1@xample.com, recipent2@xample.com 
Date: Mon, 22 Feb 2021 22:18:32 -0000
Message-ID: <161403231277.13758.10023986492893831515@bens-mbp.local>
A New Officer has been assigned!
NAME: Beverly Crusher, M.D.
RANK: Commander
SHIP ASSIGNMENT: USS Enterprise
------------------------------------------------------------
-------------------------------------------------------------------------------

Django is free and open-source and allows developers to take their applications from idea to completion quickly. Feel free to contact us for a free consultation if assistance is needed with your project.

The JBS Quick Launch Lab

Free Qualified Assessment

Quantify what it will take to implement your next big idea!

Our assessment session will deliver tangible timelines, costs, high-level requirements, and recommend architectures that will work best. Let JBS prove to you and your team why over 24 years of experience matters.

Get Your Assessment