Welcome to django-mail-factory’s documentation!¶
Django Mail Factory is a little Django app that let’s you manage emails for your project very easily.
Features¶
Django Mail Factory has support for:
- Multilingual
- Administration to preview or render emails
- Multi-alternatives emails: text and html
- Attachments
- HTML inline display of attached images
Other resources¶
Fork it on: http://github.com/peopledoc/django-mail-factory/
Documentation: http://django-mail-factory.rtfd.org/
Get started¶
From PyPI:
pip install django-mail-factory
From the github tree:
pip install -e http://github.com/peopledoc/django-mail-factory/
Then add mail_factory
to your INSTALLED_APPS:
INSTALLED_APPS = (
...
'mail_factory',
...
)
Create your first mail¶
my_app/mails.py
:
from mail_factory import factory
from mail_factory.mails import BaseMail
class WelcomeEmail(BaseMail):
template_name = 'activation_email'
params = ['user', 'site_name', 'site_url']
factory.register(WelcomeEmail)
Then you must also create the templates:
templates/mails/activation_email/subject.txt
[{{site_name }}] Dear {{ user.first_name }}, your account is created
templates/mails/activation_email/body.txt
Dear {{ user.first_name }},
Your account has been created for the site {{ site_name }}, and is
available at {{ site_url }}.
See you there soon!
The awesome {{ site_name }} team
templates/mails/activation_email/body.html
(optional)
Send a mail¶
Using the factory:
from mail_factory import factory
factory.mail('activation_email', [user.email],
{'user': user,
'site_name': settings.SITE_NAME,
'site_url': settings.SITE_URL})
Using the mail class:
from my_app.mails import WelcomeEmail
msg = WelcomeEmail({'user': user,
'site_name': settings.SITE_NAME,
'site_url': settings.SITE_URL})
msg.send([user.email])
How does it work?¶
At startup, all mails.py
files in your application folders are
automatically discovered and the emails are registered to the factory.
You can then directly call your emails from the factory with their
template_name
.
It also allows you to list your emails in the administration, preview and test them by sending them to a custom address with a custom context.
Note
mail_factory automatically performs autodiscovery of mails modules in installed applications. To prevent it, change your INSTALLED_APPS to contain ‘mail_factory.SimpleMailFactoryConfig’ instead of ‘mail_factory’.
This is only available in Django 1.7 and above.
Contents¶
Django default mail integration¶
If you use Django Mail Factory, you will definitly want to manage all your application mails from Django Mail Factory.
Even if your are using some Django generic views that send mails.
Password Reset Mail¶
Here is an example of how you can use Mail Factory with the
django.contrib.auth.views.password_reset
view.
You can first add this pattern in your urls.py
:
url(_(r'^password_reset/$'),
'mail_factory.contrib.auth.views.password_reset'),
url(_(r'^password_reset/(?P<uidb36>[0-9A-Za-z]{1,13})-(?P<token>[0-9A-Za-z]{1,13}-'
r'[0-9A-Za-z]{1,20})/$'),
'django.contrib.auth.views.password_reset_confirm')
url(_(r'^password_reset/done/$'),
'django.contrib.auth.views.password_reset_done')
Then you can overload the default templates
mails/password_reset/subject.txt
and mails/password_reset/body.txt
.
But you can also register your own PasswordResetMail
:
from django.conf import settings
from mail_factory import factory
from mail_factory.contrib.auth.mails import PasswordResetMail
from myapp.mails import AppBaseMail, AppBaseMailForm
class PasswordResetMail(AppBaseMail, PasswordResetMail):
"""Add the App header + i18n for PasswordResetMail."""
class PasswordResetForm(AppBaseMailForm):
class Meta:
mail_class = PasswordResetMail
initial = {'email': settings.ADMINS[0][1],
'domain': settings.SITE_URL.split('/')[2],
'site_name': settings.SITE_NAME,
'uid': u'4',
'user': 4,
'token': '3gg-37af4e5097565a629f2e',
'protocol': settings.SITE_URL.split('/')[0].rstrip(':')}
factory.register(PasswordResetMail, PasswordResetForm)
You can then update your urls.py to use this new form:
url(_(r'^password_reset/$'),
'mail_factory.contrib.auth.views.password_reset',
{'email_template_name': 'password_reset'}),
The default PasswordResetMail is not registered in the factory so that people that don’t use it are not disturb.
If you want to use it as is, you can just register it in your app
mails.py
file like that:
from mail_factory import factory
from mail_factory.contrib.auth.mails import PasswordResetMail
factory.register(PasswordResetMail)
Hacking MailFactory¶
MailFactory is a tool to help you manage your emails in the real world.
That means that for the same email, regarding the context, you may want a different branding, different language, etc.
Specify the language for your emails¶
If you need to specify the language for your email, other than the currently
used language, you can do so by overriding the get_language
method on your
custom class.
Let’s say that our user has a language_code
as a profile attribute:
class ActivationEmail(BaseMail):
template_name = 'activation'
params = ['user', 'activation_key']
def get_language(self):
return self.context['user'].get_profile().language_code
Force a param for all emails¶
You can also overriding the get_params
method of a custom ancestor class to
add some mandatory parameters for all your emails:
class MyProjectBaseMail(BaseMail):
def get_params(self):
params = super(MyProjectBaseMail, self).get_params()
return params.append('user')
class ActivationEmail(MyProjectBaseMail):
template_name = 'activation'
params = ['activation_key']
This way, all your emails will have the user in the context by default.
Add context data¶
If you have some information that must be added to every email context, you can put them here:
class MyProjectBaseMail(BaseMail):
def get_context_data(self, **kwargs):
data = super(MyProjectBaseMail, self).get_context_data(**kwargs)
data['site_name'] = settings.SITE_NAME
data['site_url'] = settings.SITE_URL
return data
Add attachments¶
Same thing here, if your branding needs a logo or a header in every emails, you can define it here:
from django.contrib.staticfiles import finders
class MyProjectBaseMail(BaseMail):
def get_attachments(self, files=None):
attach = super(MyProjectBaseMail, self).get_attachments(files)
attach.append((finders.find('mails/header.png'),
'header.png', 'image/png'))
return attach
Now, if you want to use this attached image in your html template, you need to
use the cid URI scheme with the name of the attachment, which is the second
item of the tuple (header.png
in our example above):
<img src="cid:header.png" alt="This is the header" />
Template loading¶
By default, the template parts will be searched in:
templates/mails/TEMPLATE_NAME/LANGUAGE_CODE/
templates/mails/TEMPLATE_NAME/
But you may want to search in different locations, ie:
templates/SITE_DOMAIN/mails/TEMPLATE_NAME/
To do that, you can override the get_template_part
method:
class ActivationEmail(BaseMail):
template_name = 'activation'
params = ['activation_key', 'site']
def get_template_part(self, part):
"""Return a mail part (body, html body or subject) template
Try in order:
1/ domain specific localized:
example.com/mails/activation/fr/
2/ domain specific:
example.com/mails/activation/
3/ default localized:
mails/activation/fr/
4/ fallback:
mails/activation/
"""
templates = []
site = self.context['site']
# 1/ {{ domain_name }}/mails/{{ template_name }}/{{ language_code}}/
templates.append(path.join(site.domain,
'mails',
self.template_name,
self.lang,
part))
# 2/ {{ domain_name }}/mails/{{ template_name }}/
templates.append(path.join(site.domain,
'mails',
self.template_name,
part))
# 3/ and 4/ provided by the base class
base_temps = super(MyProjectBaseMail, self).get_template_part(part)
return templates + base_temps
get_template_part
returns a list of template and will take the first one
available.
Mail templates¶
When you want a multi-alternatives email, you need to provide a subject, the
text/plain
body and the text/html
body.
All these parts are loaded from your email template directory.
templates/mails/invitation/subject.txt
:
{% load i18n %}{% blocktrans %}[{{ site_name }}] Invitation to the beta{% endblocktrans %}
A little warning: the subject needs to be on a single line
You can also create a different subject file for each language:
templates/mails/invitation/en/subject.txt
:
[{{ site_name }}] Invitation to the beta
templates/mails/invitation/body.txt
:
{% load i18n %}{% blocktrans with full_name=user.get_full_name expiration_date=expiration_date|date:"l d F Y" %}
Dear {{ full_name }},
You just received an invitation to connect to our beta program.
Please click on the link below to activate your account:
{{ activation_url }}
This link will expire on: {{ expiration_date }}
{{ site_name }}
-------------------------------
If you need help for any purpose, please contact us at {{ support_email }}
{% endblocktrans %}
If you don’t provide a body.html
the mail will be sent in text/plain
only, if it is present, it will be added as an alternative and displayed if the
user’s mail client handles html emails.
templates/mails/invitation/body.html
:
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>{{ site_name }}</title>
</head>
<body>
<p><img src="cid:header.png" alt="{{ site_name }}" /></p>
<h1>{% trans 'Invitation to the beta' %}</h1>
<p>{% blocktrans with full_name=user.get_full_name %}Dear {{ full_name }},{% endblocktrans %}</p>
<p>{% trans "You just received an invitation to connect to our beta program:" %}</p>
<p>{% trans 'Please click on the link below to activate your account:' %}</p>
<p><a href="{{ activation_url }}" target="_blank">{{ activation_url }}</a></p>
<p>{{ site_name }}</p>
<p>{% blocktrans %}If you need help for any purpose, please contact us at
<a href="mailto:{{ support_email }}">{{ support_email }}</a>{% endblocktrans %}</p>
</body>
</html>
The MailFactory Interface¶
In daily work email branding is important because it will call your customer to action.
MailFactory comes with a little tool that helps you to test your emails.
To do that, we generate a Django Form so that you may provide a context for your email.
Here’s how you can customize your administration form.
Enabling MailFactory administration¶
You just need to enable the urls:
project/urls.py
:
urlpatterns = (
# ...
url(r'^admin/mails/', include('mail_factory.urls')),
url(r'^admin/', include(admin.site.urls)),
# ...
)
Then you can connect to /admin/mails/ to try out your emails.
Registering a specific form¶
These two calls are equivalent:
from mail_factory import factory, BaseMail
class InvitationMail(BaseMail):
template_name = "invitation"
params = ['user']
factory.register(InvitationMail)
from mail_factory import factory, MailForm, BaseMail
class InvitationMail(BaseMail):
template_name = "invitation"
params = ['user']
factory.register(InvitationMail, MailForm)
Creating a custom MailForm¶
We may also want to build a very specific form for our email.
Let’s say we have a share this page email, with a custom message:
from mail_factory import factory, BaseMail, MailForm
class SharePageMail(BaseMail):
template_name = "share_page"
params = ['user', 'message', 'date']
class SharePageMailForm(MailForm):
user = forms.ModelChoiceField(queryset=User.objects.all())
message = forms.CharField(widget=forms.Textarea)
date = forms.DateTimeField()
factory.register(SharePageMail, SharePageMailForm)
Define form initial data¶
You can define Meta.initial
to automatically provide a context for
your mail.
import datetime
import uuid
from django.conf import settings
from django.core.urlresolvers import reverse_lazy as reverse
from django import forms
from mail_factory import factory, MailForm, BaseMail
class ShareBucketMail(BaseMail):
template_name = 'share_bucket'
params = ['first_name', 'last_name', 'comment', 'expiration_date',
'activation_url']
def activation_url():
return '%s%s' % (
settings.SITE_URL, reverse('share:index',
args=[str(uuid.uuid4()).replace('-', '')]))
class ShareBucketForm(MailForm):
expiration_date = forms.DateField()
class Meta:
initial = {'first_name': 'Thibaut',
'last_name': 'Dupont',
'comment': 'I shared with you documents we talked about.',
'expiration_date': datetime.date.today,
'activation_url': activation_url}
factory.register(ShareBucketMail, ShareBucketForm)
Then the mail form will be autopopulated with this data.
Creating your application custom MailForm¶
By default, all email params are represented as a forms.CharField()
, which
uses a basic text input.
Let’s create a project wide BaseMailForm
that uses a ModelChoiceField
on auth.models.User
each time a user
param is needed in the email.
from django.contrib.auth.models import User
from django import forms
from mail_factory.forms import MailForm
class BaseMailForm(MailForm):
def get_field_for_param(self, param):
if param == 'user':
return forms.ModelChoiceField(
queryset=User.objects.order_by('last_name', 'first_name'))
return super(BaseMailForm, self).get_field_for_param(param)
Now you need to inherit from this BaseMailForm
to make use of it for your
custom mail forms:
class MyCustomMailForm(BaseMailForm):
# your own customizations here
If you want this BaseMailForm
to be used automatically when registering a
mail with no custom form, here’s how to do it:
from mail_factory import MailFactory
class BaseMailFactory(MailFactory):
mail_form = BaseMailForm
factory = BaseMailFactory
And use this new factory everywhere in your code instead of
mail_factory.factory
.
Previewing your email¶
Sometimes however, you don’t need or want to render the email, having to provide some real data (eg a user, a site, some complex model…).
The emails may be written by your sales or marketing team, set up by your designer, and all of those don’t want to cope with the setting up of real data.
All they want is to be able to preview the email, in the different languages available.
This is where email previewing is useful.
Previewing is available straight away thanks to sane defaults. It uses the
data returned by get_preview_data
to add (possibly) non valid data to the
context used to preview the mail.
This data will override any data that was returned by get_context_data
,
which in turn uses the form’s Meta.initial, and in last resort, returns “###”.
The preview can thus use fake data: let’s take the second example from this
page, the SharePageMail
:
import datetime
from django.contrib.auth.models import User
from django.conf import settings
from mail_factory import factory, MailForm
class SharePageMailForm(MailForm):
user = forms.ModelChoiceField(queryset=User.objects.all())
message = forms.CharField(widget=forms.Textarea)
date = forms.DateTimeField()
class Meta:
initial = {'message': 'Some message'}
def get_preview_data(self, **kwargs):
data = super(SharePageMailForm, self).get_preview_data(**kwargs)
data['date'] = datetime.date.today()
# create on-the-fly fake User, not saved in database: not valid data
# but still added to context for previewing
data['user'] = User(first_name='John', last_name='Doe')
return data
factory.register(SharePageMail, SharePageMailForm)
With this feature, when displaying the mail form in the admin (to render the
email with real data), the email will also be previewed in the different
available languages with the fake data provided by the form’s
get_preview_data
, which overrides the data returned by
get_context_data
.