Django app for sending emails rendered from Django templates. Developers ship a default template for every email as a file; Site Admins can override its subject and content per language in the Admin Panel, without a deployment. When no override exists in the database, the file is used.
Site Admins edit the content in a rich text editor (or as HTML code) and insert the variables of the email context from a toolbar menu. Every email is sent as HTML together with its plain text version, generated from the HTML.
Requirements: Python 3.10+, Django 4.0+.
pip install django-emailtemplates[tinymce]
The tinymce extra installs django-tinymce for the rich text editor; without it the
content is edited as HTML code. "tinymce" must be added to INSTALLED_APPS too, and
the editor turned on with EMAILTEMPLATES_RICH_TEXT = True.
Add the app and its URLs (needed for the admin previews and the mass email button):
# settings.py
INSTALLED_APPS = [
...
"tinymce",
"emailtemplates",
]
# urls.py
urlpatterns = [
...
path("emailtemplates/", include("emailtemplates.urls")),
]
Then run python manage.py migrate.
Write the default template as a regular Django template, e.g.
templates/emails/welcome.html:<p>Hello {{ first_name }},</p> <p>your account at {{ site_name }} is ready.</p>Register it, e.g. in
AppConfig.ready(). The path is the lookup key and cannot be changed later.subjectis the default subject of templates created in the admin;help_contextis shown in the admin and filled into the preview:from emailtemplates.registry import email_templates email_templates.register( "emails/welcome.html", name="Welcome email", subject="Welcome to {{ site_name }}", help_text="Sent after registration", help_context={ "first_name": ("First name of the user", "Anna"), "site_name": "Name of the site", }, )Sending an unregistered template raises
NotRegistered; passregistry_validation=FalsetoEmailFromTemplateto skip the check.Send it:
from emailtemplates.email import EmailFromTemplate eft = EmailFromTemplate( name="emails/welcome.html", language="en", subject="Welcome to Example" ) eft.context.update({"first_name": user.first_name, "site_name": "Example"}) eft.send([user.email]) # returns the number of sent messagesThe template is looked up in the database by path and language, then in the template files. The
subjectpassed here (or, without it, thesubjectof the registration) is used when the template comes from a file; a database template brings its own subject. Either way the subject is rendered with the same context as the content. The context always containsdate.send()takes extraEmailMessagekwargs (cc,bcc,reply_to, ...),attachment_pathsandfail_silently(Trueby default: SMTP errors are logged, not raised).The email is sent with two versions of the same content (
multipart/alternative): the HTML rendered from the template, which email clients show, and a plain text version generated from it for clients that cannot show HTML, see Plain text.To send it in the background instead, once the current transaction is committed, see Sending in the background:
eft.send_later([user.email]) # returns the EmailLog of the email
Emails can be recorded in the history of sent emails, see Email logs. To only render the email, without sending:
rendered = eft.render() # RenderedEmail(subject, html, text)
To change the email without a deployment, add an Email template in the admin for the registered path and a language - or create the templates of all emails in a data migration, see Creating email templates. Its content starts as a copy of the file. Files added as Email attachments and linked to the template are attached to every email, or, with send as link, passed to the context as
default_attachments(name, URL pairs).
The template file is the default version of an email; the content in the database is the one sent. Changing the file never changes the content: Site Admins may have edited it.
Every email template keeps a snapshot of the file it was made from
(EmailTemplate.default_content). When developers change the file, the admin lists the
template as having a New default version, and its page links to a comparison: the
preview of the current content and of the default version side by side, with the changes
in the HTML. There the Site Admin may replace the content with the default version, and
restore the replaced content afterwards (EmailTemplate.previous_content). In code:
has_new_default(), replace_with_default() and restore_previous_content().
Templates created before 3.0.0 get the file as it is during the migration to 3.0.0
(0014) as their snapshot, so changes of the file made after it are reported.
Projects create the email templates of their emails in a data migration, so that Site
Admins find every email in the admin right after the deployment, wrapped in the layout.
The migration copies the content of each template file once; from then on the content in
the database is the one sent and edited, and a later change of the file is offered as a
new default version (see Default versions). A new email needs a migration adding it.
The migration depends on the migration of this app adding default_content
(0014_emailtemplate_preheader_default_content) and on the one creating the layout:
from django.db import migrations
from django.template.loader import get_template
from emailtemplates.registry import email_templates
def create_email_templates(apps, schema_editor):
EmailTemplate = apps.get_model("emailtemplates", "EmailTemplate")
EmailLayout = apps.get_model("emailtemplates", "EmailLayout")
layout = EmailLayout.objects.get(name="Standard")
for item in email_templates.registration_items():
content = get_template(item.path).template.source
EmailTemplate.objects.get_or_create(
title=item.path,
language="en",
defaults={
"subject": str(item.subject),
"preheader": str(item.preheader),
"content": content,
"default_content": content, # the snapshot of the default version
"layout": layout,
},
)
DEFAULT_FROM_EMAIL,LANGUAGE_CODE,LANGUAGES- default sender, default language and the languages available for templates.DEFAULT_REPLY_TO_EMAIL- optionalReply-Tofor every email.BASE_URL- optional, used to make attachment links absolute.MASS_EMAIL_RECIPIENTS- optional dotted path to a function returning the recipient emails of mass emails; all active users by default.EMAILTEMPLATES_RICH_TEXT-Trueturns on the rich text editor, see Rich text editor; off by default, so that projects having django-tinymce for other reasons keep the HTML code editor.EMAILTEMPLATES_RICH_TEXT_CONFIG- TinyMCE options merged over the ones of the rich text editor, see Rich text editor.EMAILTEMPLATES_INLINE_CSS-Truemoves the CSS of<style>tags (e.g. thestylesof a layout) into thestyleattributes of the elements when an email is rendered, as many email clients ignore<style>. Media queries stay in a<style>tag. The admin preview shows the inlined email too. Requires theinlineextra (css-inline):pip install django-emailtemplates[inline].EMAILTEMPLATES_CELERY-Truesends the emails ofsend_later()with Celery, see Sending in the background;EMAILTEMPLATES_CELERY_MAX_RETRIES- how many times a failed sending is retried (3);EMAILTEMPLATES_CELERY_QUEUE- the queue.EMAILTEMPLATES_EMAIL_BACKEND,EMAILTEMPLATES_EMAIL_LOG_CONTENT,EMAILTEMPLATES_EMAIL_LOG_RETENTION_DAYS- see Email logs.
EmailFromTemplate.send_later() takes the arguments of send() but sends the email
once the current transaction is committed (transaction.on_commit()), so a rolled back
change sends nothing. The email is rendered right away, so the context may hold any
objects, e.g. model instances, and recorded as a pending EmailLog, which it returns.
With EMAILTEMPLATES_CELERY = True (the celery extra:
pip install django-emailtemplates[celery]) the email is sent by the Celery task
emailtemplates.tasks.send_email, on the queue EMAILTEMPLATES_CELERY_QUEUE (the
default one by default). A failed sending (an SMTP or a connection error) is retried with
a growing delay, EMAILTEMPLATES_CELERY_MAX_RETRIES times (3 by default). Without
Celery the email is sent right after the commit, in the same process; an error is
recorded in its EmailLog and logged, not raised.
The email backend of the library records every email sent through it - those of email
templates and any other, e.g. send_mail() - as an Email log
(emailtemplates.models.EmailLog):
EMAIL_BACKEND = "emailtemplates.backends.EmailBackend" EMAILTEMPLATES_EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" # the default
The backend records an email as pending before handing it to
EMAILTEMPLATES_EMAIL_BACKEND, so the entry exists even if sending hangs or the worker
is killed, then marks it sent or failed. Emails are handed over one by one, so an error
in the middle of a batch does not fail the emails sent before it. A backend of the project
can keep doing its own work behind it, or mix emailtemplates.backends.EmailLogBackendMixin
in itself. An entry holds the template, the sender and the recipients, the subject, the HTML
and the plain text, the names of the attachments, the number of attempts, the last error and
what sent the email.
Inside a Celery task the entries describe the task: its name, its id and its arguments
(source, source_id, source_context) and the position of the email among the
emails of the run (source_seq). A retry of the task updates the entries of its run
instead of adding new ones; an email already sent is never overwritten. Use
emailtemplates.tasks.EmailTask as the base of such tasks, so that the emails are marked
failed once the retries run out, and a task failing before it sends anything (e.g. while
rendering) leaves a failed entry with its arguments:
from emailtemplates.tasks import EmailTask
@app.task(base=EmailTask, autoretry_for=(SMTPException,), retry_backoff=True)
def send_order_confirmation(order_id):
...
The admin shows the entries read only, with the HTML as it was sent. Failed emails - not pending ones, which may still be retried - can be sent again with the Send now button or the Send selected emails now action, which need the change permission of email logs; the email is sent again as a new entry, with the attachments of its email template only.
EMAILTEMPLATES_EMAIL_LOG_CONTENT = Falseempties the HTML and the plain text once an email is sent, e.g. for emails with personal data; such emails cannot be sent again.python manage.py cleanup_email_logs(or the Celery taskemailtemplates.tasks.cleanup_email_logs, e.g. daily in Celery beat) deletes the sent emails older thanEMAILTEMPLATES_EMAIL_LOG_RETENTION_DAYS(180) or--days; pending and failed ones are kept until someone acts on them.
A Mass email message created in the admin (subject, HTML content, attachments) is sent
with the button on its admin page, or with MassEmailMessage.send(). It is sent
synchronously, so for many recipients send it from a task queue instead.
An EmailLayout is a reusable frame for emails: everything that should surround the
content of an email template, e.g. a header with a logo and a footer with company data.
Layouts are created and edited in the Admin Panel, so Site Admins can change the look of
all emails at once, without a deployment.
A layout has a name, header_content, footer_content and styles. Every
email template can optionally point to one layout (EmailTemplate.layout, empty by
default). When a layout is selected, the email is rendered as:
<style>styles</style> + header_content + EmailTemplate.content + footer_content
styles holds plain CSS without the <style> tag, which is added automatically;
empty styles adds no tag at all. When the header starts a whole HTML document, the
<style> tag goes into its <head>, before </head>, so the doctype stays first.
Keeping the CSS in a field of its own means the header field holds markup only.
The parts are concatenated first and rendered afterwards, as a single Django template with a single context. The header and the footer may therefore use the same context variables as the email template itself, e.g.:
styles: .greeting { color: #c8524e; }
header_content: <img src="https://{{ shop_domain }}/static/img/logo.png">
content: <p class="greeting">Hello {{ full_name }},</p>
footer_content: <p>Sent by {{ shop_domain }}</p>
The context of the email holds subject (rendered) and preheader too, unless it
already has variables of these names, so the header may contain e.g.
<title>{{ subject }}</title> and a hidden <span>{{ preheader }}</span>. The
preheader is the text email clients show next to the subject in the inbox; every email
template has its own (EmailTemplate.preheader), and new ones start with the
preheader passed to register(). Both are plain text: they are not HTML escaped
when rendered, and escaped where the layout inserts them into HTML.
Because the parts are joined before being rendered, the header may open HTML tags that the footer closes - that is the normal way of wrapping the content in a container.
Nothing changes for email templates without a layout - they are rendered exactly as before, from the database or from the filesystem.
Template content that uses {% extends %} already brings its own frame, and the tag
must stay the first one in a template, so such content cannot be wrapped in a layout. The
admin form rejects that combination; if it is created another way, the layout is ignored
and a warning is logged. Use either {% extends %} (frame in the filesystem, for
developers) or a layout (frame in the database, for Site Admins).
- Show email preview on an email template renders it with
EmailFromTemplate, as it is sent - with its layout, subject, preheader and blocks - filled with the example context from the registry. Example values ofhelp_contextare fixed; for real objects passpreview_contexttoregister(), a dict or a callable returning one, called on every preview, e.g.preview_context=lambda: {"order": Order.objects.first()}. - Show layout preview on a layout renders the header, the footer and the styles with a placeholder in place of the email content. It has no context, so variables used in the header or the footer render empty.
- Next to the content editor of an email template, a live preview shows the email as it would be sent - in its layout, with the example context and the inlined CSS - with the content, subject, preheader and layout of the form, not saved yet. It is updated shortly after each change; while the content cannot be rendered (e.g. a template tag is being typed) the last preview stays, with the error below it.
The first two previews work on saved data. There is no live preview of a layout: its parts only render correctly together - a header that opens the tags its footer closes is broken HTML on its own - so a per-field preview reports errors that are not there.
header_content and footer_content are not edited as two separate fields. The
admin shows them as a single Frame field: one box split by a divider standing for the
content of the email template, the header above it and the footer below. That is how they
are actually used - the header opens the tags the footer closes - and the form splits the
value back into the two model fields on save. See EmailFrameWidget and
EmailFrameField.
The frame and styles are edited with CodeMirror syntax highlighting, which the library
ships and wires up by default; projects need no widget configuration of their own.
EmailTemplate.content is edited as HTML code in CodeMirror too, or as rich text, see
Rich text editor. Any textarea with a data-code-editor attribute holding
CodeMirror options as JSON gets the same treatment; the easiest way to get one is
emailtemplates.widgets.CodeEditorTextarea(mode="html") (or mode="css").
The editors run without line numbers and, for HTML, without mismatched-tag marking: in a frame the closing tag of the header lives in the footer, so marking it as an error would be wrong.
With EMAILTEMPLATES_RICH_TEXT = True (and django-tinymce installed) the content of an
email template is edited as Rich text (TinyMCE) or as HTML code (CodeMirror), switched
with the buttons above the editor at any time, without saving. The rich text editor opens
unless the content would not survive it, see the limits below.
The rich text editor is meant for the text of an email: paragraphs, headings, lists,
links, simple tables and formatting. The frame of the email - logo, header, footer, CSS -
belongs in a layout, see Email layouts. The toolbar starts with a Variables menu,
inserting the variables of the context described at registration (help_context), and
a Blocks menu, see Blocks. Variables and template tags ({% if %}, {% for %},
...) in the text are shown as chips, typed as text, but removed as a whole; the content
keeps them as plain {{ name }} and {% tag %}. Template tags in attribute values,
e.g. href="{{ url }}", stay as they are. A paragraph holding only tags producing no
output, e.g. {% if %} or {% endfor %}, is saved as the bare tags, without the
<p>, so it adds no gap to the email.
The editor has its own TinyMCE configuration, made for emails
(emailtemplates.rich_text.RICH_TEXT_CONFIG): URLs are kept as typed, so links may point
at variables (href="{{ activation_url }}"), images cannot be pasted as data URIs and
formatting is saved as inline styles. TINYMCE_DEFAULT_CONFIG of the project is meant
for its website, so only license_key, language and directionality are taken
from it. EMAILTEMPLATES_RICH_TEXT_CONFIG overrides any option, e.g.:
EMAILTEMPLATES_RICH_TEXT_CONFIG = {"license_key": "gpl", "height": 700}
The live preview stands next to the editor when both fit, below it otherwise. The editor
takes at least 480px, or the width in pixels set in EMAILTEMPLATES_RICH_TEXT_CONFIG
(e.g. "width": 1000): a wider editor moves the preview below it on narrower screens.
The editor is never wider than its column, so it never runs into the preview.
The styles of the editor and the previews (emailtemplates/code_editor.css,
emailtemplates/default_version.css) are written for the default Django admin and use
its colors; their rules are one class deep, so a project restyles them with a stylesheet
of its own. Blocks and chips inside the rich text editor are styled by
emailtemplates/rich_text_content.css (TinyMCE content_css).
The content is shown in the editor with the styles of the email layout of the
template. Classes of the layout, e.g. of a button, can be offered as TinyMCE
style_formats; with them the toolbar gets a Formats button:
EMAILTEMPLATES_RICH_TEXT_CONFIG = {
"body_class": "email-body", # when the styles of the layout are scoped to it
"style_formats": [
{"title": "Button", "selector": "a", "classes": "btn"},
{"title": "Callout", "block": "div", "classes": "callout", "wrapper": True},
],
}
Limits of the rich text editor:
- A browser moves text out of places where HTML allows none, and so does the editor: template tags placed between table rows, table cells or list items would end up outside the table or the list. Such content opens in the HTML code editor and cannot be switched to rich text; the message above the editor names the tags.
- Content using
{% extends %}cannot be edited as rich text either. - When opening the content, the editor checks that it keeps every template tag; content it
would change (e.g. a tag inside an HTML tag,
<td {% if x %}class="x"{% endif %}>) stays in the HTML code editor, unchanged, with the tags named above the editor. - The editor encodes
>,<and&typed as text; inside{% %}and{{ }}they are decoded again on save, so{% if count > 1 %}keeps working.
Some parts of an email are too complex for Site Admins to edit, e.g. a table with the details of an order, but they still want to decide where it goes and write the text around it. Register such a part as a block - a template file of its own - and the content places it as a variable:
email_templates.register(
"emails/order_summary.html",
help_context={"order": "The order"},
blocks={
"order_details": ("Order details", "emails/components/order_details.html"),
"paid_order_details": (
"Order details (paid)",
"emails/components/order_details.html",
{"paid": True}, # extra context, like {% include ... with paid=True %}
),
},
)
The block template gets the context of the email, updated with the extra context, if given. It is rendered only when the content uses the block. A variable of the same name passed in the context takes precedence over the block.
In the rich text editor the Blocks menu inserts a block as a grey, labeled box that
can be moved or removed, but not edited. A click on the box shows the block rendered with
the example context (preview_context), a second click hides it; the preview is never
saved. In the content a block is stored as:
<div class="emailtemplates-block" data-label="Order details">{{ order_details }}</div>
The <div> keeps the block out of paragraphs; the default template files may use the
same markup. The admin lists the blocks of a template next to its context, and the email
preview renders them with the example context.
Every email is sent with a plain text version, the fallback for email clients that cannot
show HTML. It is generated from the rendered HTML by inscriptis
(emailtemplates.text.html_to_text()), laid out as a browser shows the HTML, with links
followed by their address, e.g. [activate](https://example.com/activate/). Styles,
scripts, the document head and elements hidden with display: none (e.g. a preheader)
are left out.
Run the tests with python runtests.py (tox for every supported Python and Django).
- Rich text editor (TinyMCE) for the content of email templates, switched with the HTML
code editor without saving, with Variables and Blocks menus and variables shown as
chips. The
tinymceextra,EMAILTEMPLATES_RICH_TEXT(opt-in) andEMAILTEMPLATES_RICH_TEXT_CONFIGsettings. - Plain text version of every email, generated from the HTML one by inscriptis, a new
dependency (
emailtemplates.text.html_to_text()). EmailFromTemplate.render()returnsRenderedEmail(subject, html, text)without sending.EmailFromTemplate.textholds the plain text afterrender_message().- History of sent emails:
EmailLogmodel (migration0015) written by the email backendemailtemplates.backends.EmailBackend(orEmailLogBackendMixin), Celery retries of a task updating the entries of its run,emailtemplates.tasks.EmailTask, read-only admin sending failed emails again,cleanup_email_logscommand and task. EmailFromTemplate.send_later(): sending after the commit of the transaction, in the background with Celery (theceleryextra,EMAILTEMPLATES_CELERY), with retries.emailtemplates.E002reportsEMAILTEMPLATES_CELERYwithout Celery.EMAILTEMPLATES_INLINE_CSSsetting and theinlineextra: CSS inlined into the sent emails, so that the styles of a layout apply to the content written in the rich text editor.- System checks:
emailtemplates.W001warns ofEMAILTEMPLATES_RICH_TEXTwithout django-tinymce,emailtemplates.E001ofEMAILTEMPLATES_INLINE_CSSwithout css-inline. EmailTemplate.preheaderand thepreheaderofregister();subjectandpreheaderin the context of the layout.- The rich text editor shows the content with the styles of the layout and gets a
Formats button when
style_formatsare configured. - Layouts may be whole HTML documents: the styles go before
</head>of the header. preview_contextofregister(): context of the admin preview, e.g. with model instances.- Live preview of the email next to the content editor, updated while editing, before saving.
- Default versions: email templates keep a snapshot of their template file; the admin shows when developers changed it, compares both versions side by side and lets Site Admins replace the content with the new default version, or restore the replaced content.
- Blocks: parts of an email rendered from template files of their own
(
email_templates.register(..., blocks=...)), placed by Site Admins with the Blocks menu of the rich text editor. - The
subjectof the registration is used for templates coming from a file whenEmailFromTemplategets nosubject; the subject of a file template is rendered with the context too. - A missing template file is logged at the debug level instead of as an error: email templates may live in the database only, and the admin asks for their file on every page.
- Creating an email template no longer switches the active language to the language of
the template (
get_default_subject()activated it for the rest of the request). - The subject is not HTML escaped any more: a variable holding
Tom & Jerryis shown as it is, not asTom & Jerry. - Backward incompatible: emails are sent as
EmailMultiAlternatives- the body (msg.body) is the plain text and the HTML is an alternative; previously the body was the HTML withcontent_subtype = "html".EmailFromTemplate.content_subtypeis removed.
- Email layouts: an optional, admin-editable header, footer and CSS shared by email
templates.
EmailLayoutmodel,EmailTemplate.layoutfield, layout aware previews. - Check syntax errors in the header and footer of an email layout (admin form).
- Syntax highlighting for the HTML and CSS fields in the admin, shipped with the library.
- Performance:
EmailTemplatequeries load the layout withselect_related, the layout admin list prefetches the email templates using each layout. - Support for Django 5.2, 6.0 and 6.1 and Python 3.13 and 3.14.
- Backward incompatible: Django 4.0+ and Python 3.10+ are required; Python 2.7 and Django older than 4.0 are no longer supported.
packagingis no longer a dependency,default_app_configis removed (Django picks upEmailtempatesConfigon its own).emailtemplates.__version__matches the package version again.
- Add support for django 4 - #39
- change max_length from 100 to 255 in email attachments - #38
- ordering in email template & default subject from registry - #37
- Additional fields for email attachment - #36
- Change default auto field to BigAutoField - #35
- 23614 german translations - #34
- subject improvement - Now it is possible to use django templates as email subject
- Fixed problem with duplicated kwargs in send method
- absolute url of the attached files
- use attachment filename in the email context
- send related attachments together with email templates
- added missing translations [pl]
- added support for naming of the templates
- Template loader fix
- Added missing migration - fixed language choices
- Add default_app_config
- Add verbose name, replace __unicode__ into __str__
- Adding support for DEFAULT_REPLY_TO_EMAIL in django settings.
- EmailFromTemplate.send_email - added new param: fail_silently - When it’s False, msg.send() will raise an smtplib.SMTPException if an error occurs.
- Fix confusing logger stating that email was sent even though an error had occured
- cosmetic changes - logging messages possible to be aggregated by tools like sentry
- Basic mass mailing feature. Just go to admin, create new MassEmailMessage object and fill its subject, HTML content and attachments. Click admin button to send it or use Django shell. Emails are be default sent to all active users of user model (it must have is_active and email fields). In case of many application users sending emails using admin button may require to implement sending from queue rather than synchronously. You can create custom recipients function returning list and specify reference to it in MASS_EMAIL_RECIPIENTS setting.
- EmailFromTemplate.send() now receives attachments_paths parameter of paths that can be used by EmailMessage.attach_file() django core method.
- Django 1.11 compatibility fix
- Django 1.11 compatibility
- help_context parameter of EmailRegistry.register() may now contain tuple of description and example value shown in preview
- Changed EmailTemplateAdminForm title to use ChoiceField choices as lazy function. This way all registered templates are printed in admin form, independent of order Python loads application modules.
- better admin panel
- show email preview action
- set default email content from related template
- This version introduced backward incompatible EmailTemplateRegistry.
- All EmailTemplates must be registered using email_templates.register(path). Not registered email templates will raise NotRegistered exception. Registry validation can be avoid by creating email template with flag registry_validation set to False.
- Removed prefix from EmailFromTemplate. All templates must be located in {{templates}}/emailtemplates.
- Set default email title if is not defined in the database.
- Added missing migration
- Check syntax errors in EmailTemplate's content (admin form)
- Added missing migrations
- Migrations dir fix
- Compatibility with Django 1.10
- Fixed template loader error - added default Engine
- Django 1.8.8 required