Class-based emails¶
django-emailtools-reloaded
takes an approach to sending emails similar to
the class-based view’s approach to view callables. At Fusionbox we’ve found
that our email sending often follows a predictable pattern and class-based
emails arose from that pattern.
Ultimately, the goal of class-based emails is to assist developers in following the DRY principle and reuse code through inheritance and mixin classes.
Basic Example¶
A very basic example of sending emails in django using the built in
send_mail
function might look something like the following.
from django.core.mail import send_mail
def send_registration_email():
send_mail(
'A new user has registered on example.com.',
'A user has registered',
'admin@example.com',
['webmaster@example.com'],
)
Now, here is the same example using class based emails.
from emailtools.cbe import BasicEmail
class RegisteredEmail(BasicEmail):
to = 'webmaster@example.com'
from_email = 'admin@example.com'
subject = 'A user has registered'
body = 'A new user has registered on example.com.'
send_registration_email = UserRegisteredEmail.as_callable()
In both examples, calling the send_registration_email
function will send an
email to webmaster@example.com from the address webmaster@example.com with
the subject “A user has registered” and with the message
body “A new user has registered on example.com”. Admittedly, this example
is not very useful, so lets look at making some of these values more dynamic.
Emails with dynamic values¶
Now, lets write another example, in which our message body and the email recipient list and message body are dynamic.
# accounts/emails.py
from emailtools.cbe import BasicEmail
class WelcomeEmail(BasicEmail):
from_email = 'admin@example.com'
subject = 'Welcome to example.com'
def get_to(self):
return [self.args[0].email]
def get_body(self):
return """Dear {user.username},
Welcome to example.com,
- The example.com Team""".format(user=self.args[0])
send_welcome_email = WelcomeEmail.as_callable()
Our new send_welcome_email
function expects a single argument which it
expects to be a user
instance, from which it will extract the username
for the message body, and the to
address. To send our email, we just call
the send_welcome_email
function with a user instance.
>>> from app.emails import send_welcome_email
>>> user = User.objects.get(...)
>>> send_welcome_email(user) # Sends the welcome email.
Note
The BasicEmail
class is essentially a wrapper around the
django.core.email.EmailMessage
class with both properties and method
hooks for configuring, instantiating, and sending emails using that class.
HTML Emails¶
While the simple examples above may work well for simple emails, most modern
web applications are not just sending plain text emails. emailtools
ships
with two solutions for constructing and sending emails with both a plain text
message and an html message. Both the HTMLEmail
and
MarkdownEmail
classes extend
django.core.email.EmailMultiAlternative
, and uses django’s built in
template engine to set the html message on the email.
Lets rewrite the welcome email class to send an html message.
from emailtools import HTMLEmail
class WelcomeEmail(HTMLEmail):
template_name = 'app/welcome_email.html'
from_email = 'admin@example.com'
subject = 'Welcome to example.com'
def get_to(self):
return [self.args[0].email]
def get_context_data(self, **kwargs):
kwargs = super(WelcomeEmail, self).get_context_data(**kwargs)
kwargs['user'] = self.args[0]
return kwargs
send_welcome_email = WelcomeEmail.as_callable()
And now our template.
# app/templates/app/welcome_email.html
<h1>Welcome to example.com</h1>
<p>Dear {{ user.email }}</p>
<p>Thank you for signing up to <a href="http://www.example.com">example.com</a></p>
<p>The example.com team</p>
Now, our message will be rendered using the template engine.
Call Signature¶
Up until now, accessing the calling arguments for our email function has
involved accessing them in self.args
or self.kwargs
, which is both ugly
and unintuitive. If you take a look at the __init__
method of
BaseEmail
you’ll see that it merely sets *args
and
**kwargs
as self.args
and self.kwargs
. This is the default
behavior for all email classes, and it is entirely in the developers hands to
override this in any way you please.
Here is a slightly modified version of our WelcomeEmail
that demonstrates
this concept.
from emailtools import HTMLEmail
class WelcomeEmail(HTMLEmail):
template_name = 'app/welcome_email.html'
from_email = 'admin@example.com'
subject = 'Welcome to example.com'
def __init__(self, user):
self.user = user
self.to = [user.email]
def get_context_data(self, **kwargs):
kwargs = super(WelcomeEmail, self).get_context_data(**kwargs)
kwargs['user'] = self.user
return kwargs
send_welcome_email = WelcomeEmail.as_callable()
We gain readability, and validation that the caller complied with the call
signature of our email class. In this example, we didn’t call super on
__init__
, which is fine. The __init__
method is yours to override and
modify in whatever way suites the needs of your application.
About as_callable(**kwargs)
¶
At this point, if you’ve used class based views, you should be noticing some
similarities in as_callable
and as_view
. as_callable
returns a
callable function that will send the email. By default, any *args
and
kwargs
passed into the email callable are accessible via self.args
and
self.kwargs
, similar to class based views. This however is only the
default implementation of the __init__
method for class based emails. You
may override the __init__
method however you would like.
From our example above, the following two ways of sending emails are effectively the same.
>>> from my_app.emails import WelcomeEmail
>>> send_welcome_email = WelcomeEmail.as_callable()
>>> send_welcome_email(user) # Sends the email.
>>> email_instance = WelcomeEmail(user)
>>> email_instance.send()
Directly calling the email callable, and calling send()
on the instantiated
email class are identical.