By Nikola Dokoski

Email users can nowadays send scheduled emails without a problem, thanks to the constant development of the IT industry. Did you know that you can use the Python programming language to send scheduled emails easily? If you were unaware of this option, continue reading the article below. Or, if you are a programmer trying to find out how to solve an issue with the scheduling process in Python, take a look at the solution that our Python expert Nikola Dokoski shares with you in this article. 

Witnessing the rise of the Python programming language, we cannot but notice the great implementation of this language in almost every aspect of our lives. From its general purpose for developing GUI applications, web applications, and websites to its core functionality to take care of the common programming tasks, Python is certainly classified as a high-level programming language. With that being said, Python has a simple syntax which makes the code base readable and maintainable. There are many other advantages of using the Python language, especially its easy-to-use feature that separates it from other programming languages. 

In this article, we will cover a few methods for automatically sending email messages using Django. The basic idea is to create a Django project in which we will have a model for holding scheduled mail, add a management command to send scheduled emails manually, and then cover a few methods of automating this process.

This article will cover the entire process – if you are interested in the main bits of code as well as a `tar.gz` that you can install via pip, just scroll to the end.

Let’s start off with some basic stuff.

I will be using a virtualenv for this article. I recommend that you use it too – you can install it via `pip install virtualenv` and check out the docs at https://pypi.org/project/virtualenv/1.7.1.2/

Create a venv and install Django:

virtualenv venv
source venv/bin/activate
pip install django # At the time of writing, I am using django 3.0.6.

Then we create the project and the app.

django-admin startproject auto_mail
cd auto_mail
python manage.py migrate # For default models - users and whatnot.
python manage.py startapp mail_app

We won’t really be creating any views and URLs here – we will just use the models, admin, and management modules. This makes the app easier to use and to add to other projects.
On that end, we want a model for a scheduled mail.

# in mail_app/models.py

class MailAttachment(models.Model):
	attachment_file = models.FileField()
	attached_to = models.ForeignKey('ScheduledMail', related_name = 'attachments', on_delete = models.CASCADE)

	def __str__(self):
    	return '%s (%s)' % (self.attachment_file.filename, self.attached_to.subject)


class MailRecipient(models.Model):
	mail_address = models.CharField(max_length = 40)

	def __str__(self):
    	return self.mail_address


class ScheduledMail(models.Model):
	subject = models.CharField(max_length = 40)
	template = models.FileField(upload_to = 'mail_app/mails')
	send_on = models.DateTimeField(default = timezone.now())
	recipients_list = models.ManyToManyField(MailRecipient, related_name = 'mail_list')

	def __str__(self):
    	return self.subject

Fairly simple. We have a ScheduledMail model, which is the basis for this app that holds a subject and a template. We have the template as a file because, well, we may want our mail to be formatted nicely as an HTML message. If we were to use a standard CharField, it would have to be with a really high max length, which is not the best for use in a database, so way better is to just upload files. Furthermore, we can create a better method of creating mails via a view, as well as preview and whatnot, then save it to a file and upload it.

The other models – MailAttachment and MailRecipient – will allow us to add multiple users and attachments to our mail messages. With that, we have most of what our app will be using! We just need to write a management command.

Before we do that though, let’s create the database. For development, I just go with sqlite3, as it’s easy to backup and play around with. You might want to use PostgreSQL or something heavier for production. So, let us add our app in the project settings, under INSTALLED_APPS. Also, since we are using a FileField for our mail template, we also need to define MEDIA_ROOT and MEDIA_URL values so that our files can be uploaded properly. 

# in auto_mail/settings.py
...
INSTALLED_APPS = [
	'django.contrib.admin',
	'django.contrib.auth',
	'django.contrib.contenttypes',
	'django.contrib.sessions',
	'django.contrib.messages',
	'django.contrib.staticfiles',
	'mail_app',
]

MEDIA_URL = '/media/'
MEDIA_ROOT = 'media/'

And now we can migrate:

python manage.py makemigrations
python manage.py migrate

One last step before finally viewing the admin – registering the models. Add this to your app’s admin.py folder:

# in auto_mail/admin.py
from django.contrib import admin
from .models import ScheduledMail, MailAttachment, MailRecipient

# Register your models here.

@admin.register(ScheduledMail)
class MailAdmin(admin.ModelAdmin):
	pass

@admin.register(MailAttachment)
class AttachmentAdmin(admin.ModelAdmin):
	pass

@admin.register(MailRecipient)
class RecipientAdmin(admin.ModelAdmin):
	pass

And finally, create a superuser and run the server:

python manage.py createsuperuser
python manage.py runserver

Great, we have our models, we can add mails, add recipients, and so on. Let’s create the actual command. And, well, it’s pretty simple.

mkdir -p mail_app/management/commands
touch mail_app/management/commands/__init__.py
touch mail_app/management/__init__.py

After this, we just need to add our send mail command in that folder. To keep the code clean, let us add all mail functionalities in the models themselves.

# in mail_app/models.py

from django.conf import settings

auto_mail_from = 'from@mail.com'
if hasattr(settings, 'AUTO_MAIL_FROM'):
	auto_mail_from = settings.auto_mail_from

class ScheduledMail(models.Model):


        ...
	@classmethod
	def get_today_mail(cls):
    	today = date.today()
    	return cls.objects.filter(send_on__year = today.year, send_on__month = today.month, send_on__day = today.day)

	def send_scheduled_mail(self):
    	message = self.template.read().decode('utf-8')
    	recipient_list = list(self.recipients_list.values_list('mail_address', flat = True))
    	mail_msg = EmailMessage(
        	subject = self.subject,
        	body = message,
        	from_email = settings.AUTO_MAIL_FROM,
        	to = recipient_list,
    	)
    	mail_msg.content_subtype = 'html'

    	mail_msg.send()

Then we can use these methods in the management command. 

# in mail_app/management/commands/send_scheduled_mails.py

from datetime import date

from django.core.management import BaseCommand

from mail_app.models import ScheduledMail

class Command(BaseCommand):
	help = 'Sends an email to any client for which a discount has started today.'

	def handle(self, *args, **options):
    	today_mail = ScheduledMail.get_today_mail()
    	for mail_message in today_mail:
        	mail_message.send_scheduled_mail()

Here, to define the from_email argument, we have added value in settings.py. This makes it a bit more modular, but you should then add this variable to the settings file. If not, you can just hardcode a string here. While we’re editing settings though, let’s also add some email parameters. For now, we will just use the file-based email backend so we can test the app.

# in settings.py

...
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/3.0/howto/static-files/

STATIC_URL = '/static/'


# AUTO_MAIL stuff
# it doesn't actually matter where this is

EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location

AUTO_MAIL_FROM = 'some_mail@mail.com'

Neat! Let’s try it out (you might have to add some objects in admin for it to work)

$python manage.py send_scheduled_mails
$cat django_mail/*.log

Content-Type: text/html; charset="utf-8"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Subject: My first mail
From: some_mail@mail.com
To: TestAccount@mail.com, AnotherTestAccount@mail.com
Date: Fri, 29 May 2020 11:13:12 -0000
Message-ID: <159075079254.4003.12926778827038569342@ninoneutrino>

This is a simple mail message, uploaded to auto-mail.
I hope this works!

Yours,
Nikola

-------------------------------------------------------------------------------

And ta-da! We can send mail via a simple command. This is not the final product yet though – we will add a bunch of quality of life additions later on (such as a few options when selecting recipients for a new mail, variables within the mail message and so on).

Before we do that though, we have a couple of things to finish off. First off – the automation. Sending mail via commands is cool, but we can set it up so it’s all automatic.
Enter Cron. Cron is extremely basic, it’s great for making quick and dirty projects, or demonstrating how to automate something.
Just add this to your crontab:

0 0 * * * cd /path/to/your/project/root/ && /path/to/your/venv/bin/python3.6 manage.py send_scheduled_mails

You can change the minutes and hour values (the first two zeroes) to whatever the current time is + a few minutes, and see if you get anything new in django_mail/. If it doesn’t work, check if your paths are correct by executing the command you paster in crontab from your home directory. If there are errors, you can usually find them in /var/log/syslog.

And as far as automation goes, this is enough. In a more serious project, however, you may want to use Celery, which comes included in the project and will spare you from messing with Cron. Celery is its own beast though, and there’s little point in writing a tutorial on how to write a celery app when the celery website has its own tutorial, so you might want to check that out at First Steps with Celery and then later on First Steps with Django.

Then, there is one last thing to do – actually, send emails. So far we’ve only been saving them in files, but in real life, we want to send real messages, This might be a bit tricky and may involve several other technologies that are out of the scope of this tutorial. If you want to accomplish this, there are a few methods you can think about:

  • Host your own mail server. If you are doing this as a side project, proof of concept or just messing around, this is probably the way to do it. I recommend either using virtual machines or containers, but the easiest way is to probably use a web solution and just run an AWS or GCE instance.
  • Use an existing mail server. Gmail, Mailgun, Amazon SES, etc. There’s a bunch of these, some free, some paid, but in general, you can configure your settings to use a mail server.

Short version: 

So, if you found this and just want to copy and paste some code, here are the final tidbits: 

# models.py
import datetime
from datetime import date

from django.db import models
from django.utils import timezone
from django.core.mail import EmailMessage

from django.conf import settings

auto_mail_from = 'from@mail.com'
if hasattr(settings, 'AUTO_MAIL_FROM'):
	auto_mail_info = settings.AUTO_MAIL_FROM

class MailAttachment(models.Model):
	attachment_file = models.FileField()
	attached_to = models.ForeignKey('ScheduledMail', related_name = 'attachments', on_delete = models.CASCADE)

	def __str__(self):
    	return '%s (%s)' % (self.attachment_file.filename, self.attached_to.subject)

class MailRecipient(models.Model):
	mail_address = models.CharField(max_length = 40)

	def __str__(self):
    	return self.mail_address

class ScheduledMail(models.Model):
	subject = models.CharField(max_length = 40)
	template = models.FileField(upload_to = 'mail_app/mails')
	send_on = models.DateTimeField(default = timezone.now())
	recipients_list = models.ManyToManyField(MailRecipient, related_name = 'mail_list')

	def __str__(self):
    	return self.subject

	@classmethod
	def get_today_mail(cls):
    	today = date.today()
    	return cls.objects.filter(send_on__year = today.year, send_on__month = today.month, send_on__day = today.day)

	def send_scheduled_mail(self):
    	message = self.template.read().decode('utf-8')
    	recipient_list = list(self.recipients_list.values_list('mail_address', flat = True))
    	mail_msg = EmailMessage(
        	subject = self.subject,
        	body = message,
        	from_email = auto_mail_from,
        	to = recipient_list,
    	)
    	mail_msg.content_subtype = 'html'
   	 
    	mail_msg.send()
# admin.py
from django.contrib import admin
from .models import ScheduledMail, MailAttachment, MailRecipient

# Register your models here.

@admin.register(ScheduledMail)
class MailAdmin(admin.ModelAdmin):
	pass

@admin.register(MailAttachment)
class AttachmentAdmin(admin.ModelAdmin):
	pass

@admin.register(MailRecipient)
class RecipientAdmin(admin.ModelAdmin):
	pass
#management/commands/send_scheduled_mail.py
import datetime

from django.core.management import BaseCommand

from mail_app.models import ScheduledMail

class Command(BaseCommand):
	help = 'Sends an email to any client for which a discount has started today.'

	def handle(self, *args, **options):
    	today_mail = ScheduledMail.get_today_mail()
    	for mail_message in today_mail:
        	mail_message.send_scheduled_mail()

And you put this in your crontab: 

0 0 * * * cd /path/to/project/ && /path/to/venv/bin/python3.6 manage.py send_scheduled_mails

Then set up your mail server in your settings.py! 
To make it easier, I have packaged the app, so you can install it via pip. Here is the package:

You can install it via python -m pip install mail_app-0.1.tar.gz

You can use the management command as described above. In order to do so, however, you still have to add it to INSTALLED_APPS in the settings.py file, as well as define MEDIA_ROOT and MEDIA_URL settings values. 

WARNING

If you are just copy-pasting this code in your app, be warned – I have some values in settings.py which should probably not be used in production. Namely:

  • Debug is on
  • Database is sqlite3
  • Allowed hosts is '*'

All of these are just to make the presentation easier. In a production environment, you definitely want to debug off, a more stable database, and stricter allowed hosts. You have been warned.

Was this the solution you needed? If yes, send us a message to explain the process you went through when discovering how to send scheduled emails with Python programming language. If you need more help, please let us know by getting a free consultation with our experts.


Sources: