├── .gitignore ├── LICENSE ├── README.md ├── htmlemailer ├── __init__.py ├── admin.py ├── migrations │ └── __init__.py ├── models.py ├── templates │ └── htmlemailer │ │ ├── boilerplate.html │ │ ├── example.html │ │ ├── example.txt │ │ ├── example_subject.txt │ │ ├── example_template.html │ │ └── example_template.txt ├── tests.py └── views.py ├── setup.py └── test_project ├── htmlemailer ├── manage.py ├── templates ├── example.html ├── example.txt ├── example2.md ├── example2_subject.txt ├── example_subject.txt ├── template.html └── template.txt └── test_project ├── __init__.py ├── management ├── __init__.py └── commands │ ├── __init__.py │ └── test_html_email.py ├── settings.py ├── urls.py └── wsgi.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | 5 | # C extensions 6 | *.so 7 | 8 | # Distribution / packaging 9 | .Python 10 | env/ 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | lib/ 17 | lib64/ 18 | parts/ 19 | sdist/ 20 | var/ 21 | *.egg-info/ 22 | .installed.cfg 23 | *.egg 24 | 25 | # PyInstaller 26 | # Usually these files are written by a python script from a template 27 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 28 | *.manifest 29 | *.spec 30 | 31 | # Installer logs 32 | pip-log.txt 33 | pip-delete-this-directory.txt 34 | 35 | # Unit test / coverage reports 36 | htmlcov/ 37 | .tox/ 38 | .coverage 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | 43 | # Translations 44 | *.mo 45 | *.pot 46 | 47 | # Django stuff: 48 | *.log 49 | 50 | # Sphinx documentation 51 | docs/_build/ 52 | 53 | # PyBuilder 54 | target/ 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Civic Responsibility LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Django HTML Emailer 2 | =================== 3 | 4 | A utility app for sending HTML emails in Django 1.7+: 5 | 6 | * Uses [HTML Email Boilerplate v0.5](http://htmlemailboilerplate.com/) 7 | * Inlines CSS (per the boilerplate's instructions) using [inlinestyler](https://github.com/dlanger/inlinestyler). 8 | * Renders message body from Markdown or from text and HTML parts that you give. 9 | 10 | Installation 11 | ------------ 12 | 13 | Install this module: 14 | 15 | pip install django-html-emailer 16 | 17 | Add `htmlemailer` to your Django settings's INSTALLED_APPS. 18 | 19 | Basic Usage 20 | ----------- 21 | 22 | Here's a quick example for how to send a message: 23 | 24 | from htmlemailer import send_mail 25 | 26 | send_mail( 27 | "htmlemailer/example", 28 | "My Site ", 29 | ["you@recipient.com"], 30 | { 31 | "my_message": "Hello & good day to you!" 32 | }) 33 | 34 | Replace the recipient address with your email address. This should send you a message using the example template. 35 | 36 | Your Templates 37 | -------------- 38 | 39 | htmlemailer composes your actual email from a series of templates. Usually you have: 40 | 41 | 1. A template storing the actual content of your email (either a Markdown template or a pair of templates, one for the HTML part and one for the plain text part), which extends... 42 | 2. A template that has the general design of all of your emails (CSS, header, footer), akin to your `base.html` for your site (a pair of templates, one for HTML and one for text), which extends... 43 | 3. The HTML Email Boilerplate, which we've already converted into a template. 44 | 4. A `..._subject.txt` template which generates the subject line of the email (it's also a template so you can use variables etc. in it). 45 | 46 | First copy the example "general design" template files into your project's templates path, naming them as you like. Copy them from: 47 | 48 | * [htmlemailer/templates/htmlemailer/example_template.txt](htmlemailer/templates/htmlemailer/example_template.txt) 49 | * [htmlemailer/templates/htmlemailer/example_template.html](htmlemailer/templates/htmlemailer/example_template.html) 50 | 51 | Then copy the example "actual content" template files into your project: 52 | 53 | * [htmlemailer/templates/htmlemailer/example_subject.txt](htmlemailer/templates/htmlemailer/example_subject.txt) 54 | 55 | and either 56 | 57 | * [htmlemailer/templates/htmlemailer/example.md](htmlemailer/templates/htmlemailer/example.md) 58 | 59 | if you want to use a single Markdown file or 60 | 61 | * [htmlemailer/templates/htmlemailer/example.txt](htmlemailer/templates/htmlemailer/example.txt) 62 | * [htmlemailer/templates/htmlemailer/example.html](htmlemailer/templates/htmlemailer/example.html) 63 | 64 | if you want to explicitly set the text and HTML parts of the message separately. 65 | 66 | You can change the path and file names, except the set of files must have the *same* path name up to `.md`, `.txt`, `.html`, and `_subject.txt`. That's how the module knows they go together (note how you don't include the file extension in the call to `send_mail`). 67 | 68 | If you changed the path of the general design templates, you'll have to update the `{% extends ... %}` template tags in `example.md` or `example.txt` and `example.html` to point to the new path. You can of course have more than one email by creating a new set of `.md`, `.txt`, `.html`, and `_subject.txt` files at a different path. 69 | 70 | Lastly, in your call to `send_mail`, update the first argument to specify the location of your email templates. Just specify the common part of the path name of the three files. In this case, it's just `htmlemailer/example`. The `.md`, `.txt`, `.html`, and `_subject.txt` will be added by the library. 71 | 72 | Advanced Usage 73 | -------------- 74 | 75 | `send_mail` also takes an optional `fail_silently` boolean argument (default is False), and it passes other keyword arguments on to Django's [EmailMessage](https://docs.djangoproject.com/en/1.7/topics/email/#django.core.mail.EmailMessage) constructor, so you can also pass `headers` and `connection`. 76 | 77 | If `DEFAULT_TEMPLATE_CONTEXT` is set in your settings, then it should be a dictionary with default template context variables passed into your email templates. 78 | 79 | Notes on Markdown 80 | ----------------- 81 | 82 | Markdown messages are rendered into HTML using CommonMark ([specification](http://spec.commonmark.org/), [library](https://pypi.python.org/pypi/CommonMark)). The text part of the message is rendered using a special Markdown-to-text renderer, because raw Markdown doesn't always look professional (especially links and images). 83 | 84 | The Markdown is rendered *first* prior to running the Django template engine. So you cannot cause Markdown to be inserted into the email through template context variables. This is by design. 85 | 86 | Also note that the `{% extends ... %}` tag at the top of the Markdown message body template does not contain the `.txt` or `.html` file extension. The library inserts the right file extension for the general design template prior to rendering into HTML and text. 87 | 88 | Note: The CommonMark library is monkey-patched to turn off escaping of {'s and }'s in URLs (to allow for template tags to appear within links). If you are using CommonMark elsewhere in your application, that might affect you if you are creating Markdown documents with these characters in URLs (which is probably bad anyway). 89 | 90 | Testing (Library Developers) 91 | ---------------------------- 92 | 93 | A test Django project is included. To use: 94 | 95 | cd test_project 96 | pip3 install inlinestyler commonmark commonmarkextensions 97 | python3 manage.py test_html_email example 98 | python3 manage.py test_html_email example2 99 | 100 | This will output test emails (a MIME message) to the console. `example` uses separate text and HTML parts. `example2` uses a single Markdown body file. 101 | 102 | License 103 | ------- 104 | 105 | This project and the (upstream) boilerplate code are available under the MIT license. 106 | 107 | For Project Maintainers 108 | ----------------------- 109 | 110 | To publish a universal wheel to pypi, update the version number in setup.py, then: 111 | 112 | pip3 install twine 113 | rm -rf dist 114 | python3 setup.py bdist_wheel --universal 115 | twine upload dist/* 116 | git tag v1.0.XXX 117 | git push --tags 118 | -------------------------------------------------------------------------------- /htmlemailer/__init__.py: -------------------------------------------------------------------------------- 1 | from django.template.exceptions import TemplateDoesNotExist 2 | from django.template.base import Template 3 | from django.template.context import Context 4 | from django.template.engine import Engine 5 | from django.template.loader import render_to_string 6 | from django.core.mail import EmailMultiAlternatives 7 | from django.conf import settings 8 | 9 | import re 10 | 11 | from inlinestyler.utils import inline_css 12 | import commonmark 13 | import commonmark_extensions.plaintext 14 | 15 | # Remove cssutils's warnings, of which there are many. 16 | import cssutils 17 | import logging 18 | cssutils.log.setLevel(logging.ERROR) 19 | 20 | def send_mail(template_prefix, from_email, recipient_list, template_context, request=None, fail_silently=False, **kwargs): 21 | # Sends a templated HTML email. 22 | # 23 | # Unrecognized arguments are passed on to Django's EmailMultiAlternatives's init method. 24 | 25 | # add default template context variables from settings.DEFAULT_TEMPLATE_CONTEXT 26 | template_context = build_template_context(template_context) 27 | 28 | # subject 29 | subject = render_to_string(template_prefix + '_subject.txt', template_context, request=request) 30 | subject = re.sub(r"\s*[\n\r]+\s*", " ", subject).strip() # remove superfluous internal white space around line breaks and leading/trailing spaces 31 | 32 | # Add subject as a new context variable, and it is used in the base HTML template's title tag. 33 | template_context['subject'] = subject 34 | 35 | # body 36 | 37 | # see if a Markdown template is present 38 | try: 39 | # Use the template engine's loaders to find the template, but then just 40 | # ask for its source so we have the raw Markdown. 41 | md_template = Engine.get_default().get_template(template_prefix + '.md').source 42 | except TemplateDoesNotExist: 43 | md_template = None 44 | 45 | if md_template: 46 | # render the text and HTML parts from the Markdown template 47 | text_body, html_body = render_from_markdown(md_template, template_context) 48 | else: 49 | # render from separate text and html templates 50 | text_body = render_to_string(template_prefix + '.txt', template_context, request=request) 51 | html_body = render_to_string(template_prefix + '.html', template_context, request=request) 52 | 53 | # inline HTML styles because some mail clients dont process the 116 | 117 | {% block head %} 118 | {% endblock %} 119 | 120 | 121 | 122 | 123 | 127 | 128 |
124 | {% block body %} 125 | {% endblock %} 126 |
129 | 130 | 131 | -------------------------------------------------------------------------------- /htmlemailer/templates/htmlemailer/example.html: -------------------------------------------------------------------------------- 1 | {% extends "htmlemailer/example_template.html" %} 2 | 3 | {% block extra_head %} 4 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

Hello

13 | 14 |

Here’s your email!

15 | 16 |

{{my_message}}

17 | 18 |

Explanation

19 | 20 |

Edit this template!

21 | 22 | 23 |

Examples from HTML Email Boilerplate

24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 |
33 | 34 | 35 | 36 | Coloring Links appropriately 37 | 38 | 39 | Your alt text 40 | 41 | 43 | 123-456-7890 44 | {% endblock %} 45 | -------------------------------------------------------------------------------- /htmlemailer/templates/htmlemailer/example.txt: -------------------------------------------------------------------------------- 1 | {% extends "htmlemailer/example_template.txt" %} 2 | {% block content %} 3 | Hello 4 | ===== 5 | 6 | Here's your email. 7 | 8 | {{my_message}} 9 | 10 | Explanation 11 | ----------- 12 | 13 | This is the text body part of the example template. 14 | {% endblock %} 15 | -------------------------------------------------------------------------------- /htmlemailer/templates/htmlemailer/example_subject.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | Hi & Hello on {% now "SHORT_DATETIME_FORMAT" %} 3 | {% endautoescape %} 4 | -------------------------------------------------------------------------------- /htmlemailer/templates/htmlemailer/example_template.html: -------------------------------------------------------------------------------- 1 | {% extends "htmlemailer/boilerplate.html" %} 2 | 3 | {% block head %} 4 | 16 | {% block extra_head %} 17 | {% endblock %} 18 | {% endblock %} 19 | 20 | {% block body %} 21 | 22 | {% block content %} 23 | {% endblock %} 24 | 25 |
26 |

Unsubscribe instructions here.

27 | 28 | {% endblock %} -------------------------------------------------------------------------------- /htmlemailer/templates/htmlemailer/example_template.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %}{% block content %} 2 | {% endblock %} 3 | 4 | ---- 5 | Unsubscribe instructions here. 6 | {% endautoescape %} 7 | -------------------------------------------------------------------------------- /htmlemailer/tests.py: -------------------------------------------------------------------------------- 1 | from django.test import TestCase 2 | 3 | # Create your tests here. 4 | -------------------------------------------------------------------------------- /htmlemailer/views.py: -------------------------------------------------------------------------------- 1 | from django.shortcuts import render 2 | 3 | # Create your views here. 4 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import sys 4 | from setuptools import setup, find_packages 5 | from codecs import open 6 | 7 | setup( 8 | name='django-html-emailer', 9 | version='0.1.0', 10 | 11 | description='Utility for sending HTML emails from Django.', 12 | long_description=open("README.md", encoding='utf-8').read(), 13 | long_description_content_type='text/markdown', 14 | url='https://github.com/govtrack/django-html-emailer', 15 | keywords="Django email HTML", 16 | classifiers=[ # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 17 | 'Intended Audience :: Developers', 18 | 'Topic :: Software Development :: Libraries :: Python Modules', 19 | 'Programming Language :: Python :: 2', 20 | 'Programming Language :: Python :: 2.7', 21 | 'Programming Language :: Python :: 3', 22 | 'Programming Language :: Python :: 3.4', 23 | 'Programming Language :: Python :: 3.5', 24 | 'Programming Language :: Python :: 3.6', 25 | ], 26 | 27 | author='Joshua Tauberer', 28 | author_email='jt@occams.info', 29 | license='MIT', 30 | 31 | install_requires=[ 32 | 'inlinestyler', 33 | "commonmark>=0.8.0", 34 | "commonmarkextensions>=0.0.1", 35 | ], 36 | packages=find_packages(), 37 | package_data={'htmlemailer': ['templates/htmlemailer/*']}, 38 | ) 39 | -------------------------------------------------------------------------------- /test_project/htmlemailer: -------------------------------------------------------------------------------- 1 | ../htmlemailer/ -------------------------------------------------------------------------------- /test_project/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /test_project/templates/example.html: -------------------------------------------------------------------------------- 1 | {% extends "template.html" %} 2 | 3 | {% block extra_head %} 4 | 9 | {% endblock %} 10 | 11 | {% block content %} 12 |

Hello!

13 | 14 |

{{message}}

15 | {% endblock %} 16 | -------------------------------------------------------------------------------- /test_project/templates/example.txt: -------------------------------------------------------------------------------- 1 | {% extends "template.txt" %} 2 | {% block content %} 3 | Hello! 4 | 5 | {{message}} 6 | {% endblock %} 7 | -------------------------------------------------------------------------------- /test_project/templates/example2.md: -------------------------------------------------------------------------------- 1 | {% extends "template" %} 2 | {% block content %} 3 | # Hello! 4 | 5 | * Remember. 6 | 7 | * To use bullets. 8 | 9 | ## The message 10 | 11 | > {{message}} 12 | 13 | See [the project]({{link}}) for more information. 14 | 15 | Or just a plain link: [{{link}}]({{link}}) 16 | 17 | {% endblock %} -------------------------------------------------------------------------------- /test_project/templates/example2_subject.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | Hi & Hello on {% now "SHORT_DATETIME_FORMAT" %} 3 | {% endautoescape %} 4 | -------------------------------------------------------------------------------- /test_project/templates/example_subject.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %} 2 | Hi & Hello on {% now "SHORT_DATETIME_FORMAT" %} 3 | {% endautoescape %} 4 | -------------------------------------------------------------------------------- /test_project/templates/template.html: -------------------------------------------------------------------------------- 1 | {% extends "htmlemailer/boilerplate.html" %} 2 | 3 | {% block head %} 4 | 16 | {% block extra_head %} 17 | {% endblock %} 18 | {% endblock %} 19 | 20 | {% block body %} 21 | 22 | {% block content %} 23 | {% endblock %} 24 | 25 |
26 |

Unsubscribe instructions here.

27 | 28 | {% endblock %} -------------------------------------------------------------------------------- /test_project/templates/template.txt: -------------------------------------------------------------------------------- 1 | {% autoescape off %}{% block content %} 2 | {% endblock %} 3 | 4 | ---- 5 | Unsubscribe instructions here. 6 | {% endautoescape %} 7 | -------------------------------------------------------------------------------- /test_project/test_project/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/govtrack/django-html-emailer/db5dfa7cb4df4d0d17947939d70f3f31868cf02f/test_project/test_project/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/management/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/govtrack/django-html-emailer/db5dfa7cb4df4d0d17947939d70f3f31868cf02f/test_project/test_project/management/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/management/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/govtrack/django-html-emailer/db5dfa7cb4df4d0d17947939d70f3f31868cf02f/test_project/test_project/management/commands/__init__.py -------------------------------------------------------------------------------- /test_project/test_project/management/commands/test_html_email.py: -------------------------------------------------------------------------------- 1 | from django.core.management.base import BaseCommand 2 | 3 | class Command(BaseCommand): 4 | args = 'templatename [recipient@address.com [sender@address.com]]' 5 | 6 | def add_arguments(self, parser): 7 | parser.add_argument('templatename') 8 | parser.add_argument('recipient', nargs="?", default="recipient@example.org") 9 | parser.add_argument('sender', nargs="?", default="Your Site ") 10 | 11 | def handle(self, *args, **options): 12 | from htmlemailer import send_mail 13 | 14 | send_mail( 15 | options["templatename"], 16 | options["sender"], 17 | [options["recipient"]], 18 | 19 | # example template context 20 | { 21 | "message": "This is a message containing >>> some HTML characters to test escaping <<<<.", 22 | "link": "https://github.com/if-then-fund/django-html-emailer", 23 | }) 24 | -------------------------------------------------------------------------------- /test_project/test_project/settings.py: -------------------------------------------------------------------------------- 1 | import os, random, string 2 | 3 | INSTALLED_APPS = [ 4 | "htmlemailer", 5 | "test_project" 6 | ] 7 | 8 | EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend' 9 | 10 | BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 11 | SECRET_KEY = "".join([random.choice(getattr(string, 'letters', string.ascii_letters)) for _ in range(50)]) 12 | DEBUG = True 13 | ROOT_URLCONF = 'test_project.urls' 14 | 15 | TEMPLATES = [ 16 | { 17 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 18 | 'DIRS': [os.path.join(BASE_DIR, 'templates')], 19 | 'APP_DIRS': True, 20 | }, 21 | ] 22 | 23 | WSGI_APPLICATION = 'test_project.wsgi.application' 24 | 25 | -------------------------------------------------------------------------------- /test_project/test_project/urls.py: -------------------------------------------------------------------------------- 1 | from django.conf.urls import url 2 | urlpatterns = [ 3 | ] 4 | -------------------------------------------------------------------------------- /test_project/test_project/wsgi.py: -------------------------------------------------------------------------------- 1 | import os 2 | from django.core.wsgi import get_wsgi_application 3 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test_project.settings") 4 | application = get_wsgi_application() 5 | --------------------------------------------------------------------------------