├── .gitignore ├── .hgignore ├── AUTHORS ├── LICENSE ├── MANIFEST.in ├── README.rst ├── docs ├── conf.py └── index.rst ├── email_extras ├── __init__.py ├── admin.py ├── apps.py ├── backends.py ├── forms.py ├── migrations │ ├── 0001_initial.py │ ├── 0002_auto_20161103_0752.py │ ├── 0003_auto_20161103_0315.py │ ├── 0004_use_djangos_emailfield.py │ └── __init__.py ├── models.py ├── settings.py ├── templates │ └── admin │ │ └── email_extras │ │ ├── address │ │ └── change_form.html │ │ └── key │ │ └── change_form.html └── utils.py ├── setup.cfg └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.pyo 3 | *.egg-info 4 | -------------------------------------------------------------------------------- /.hgignore: -------------------------------------------------------------------------------- 1 | syntax: glob 2 | *.pyc 3 | *.pyo 4 | dist/* 5 | build/* 6 | django_email_extras.egg-info/* 7 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | * Stephen McDonald 2 | * Chip Tol 3 | * Diego Andres Sanabria Martin 4 | * L Seangmeng 5 | * Anton Larkin 6 | * Eric Amador 7 | * Thomas Stols 8 | * Mario Rosa 9 | * Adrian Bohdanowicz 10 | * Tim Heithecker -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) Stephen McDonald and individual contributors. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 15 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 16 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 18 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 19 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 20 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 21 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 22 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 23 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | 25 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE 2 | include README.rst 3 | recursive-include email_extras * 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Created by `Stephen McDonald `_ 2 | 3 | Introduction 4 | ============ 5 | 6 | django-email-extras is a Django reusable app providing the 7 | ability to send PGP encrypted and multipart emails using 8 | Django templates. These features can be used together or 9 | separately. When configured to send PGP encrypted email, 10 | the ability for Admin users to manage PGP keys is also 11 | provided. 12 | 13 | A tool for automatically opening multipart emails in a 14 | local web browser during development is also provided. 15 | 16 | 17 | Dependencies 18 | ============ 19 | 20 | * `python-gnupg `_ is 21 | required for sending PGP encrypted email. 22 | 23 | 24 | Installation 25 | ============ 26 | 27 | The easiest way to install django-email-extras is directly from PyPi 28 | using `pip `_ by running the command 29 | below:: 30 | 31 | $ pip install -U django-email-extras 32 | 33 | Otherwise you can download django-email-extras and install it directly 34 | from source:: 35 | 36 | $ python setup.py install 37 | 38 | 39 | Usage 40 | ===== 41 | 42 | Once installed, first add ``email_extras`` to your ``INSTALLED_APPS`` 43 | setting and run the migrations. Then there are two functions for sending email 44 | in the ``email_extras.utils`` module: 45 | 46 | * ``send_mail`` 47 | * ``send_mail_template`` 48 | 49 | The former mimics the signature of ``django.core.mail.send_mail`` 50 | while the latter provides the ability to send multipart emails 51 | using the Django templating system. If configured correctly, both 52 | these functions will PGP encrypt emails as described below. 53 | 54 | 55 | Sending PGP Encrypted Email 56 | =========================== 57 | 58 | `PGP explanation `_ 59 | 60 | Using `python-gnupg `_, two 61 | models are defined in ``email_extras.models`` - ``Key`` and ``Address`` 62 | which represent a PGP key and an email address for a successfully 63 | imported key. These models exist purely for the sake of importing 64 | keys and removing keys for a particular address via the Django 65 | Admin. 66 | 67 | When adding a key, the key is imported into the key ring on 68 | the server and the instance of the ``Key`` model is not saved. The 69 | email address for the key is also extracted and saved as an 70 | ``Address`` instance. 71 | 72 | The ``Address`` model is then used when sending email to check for 73 | an existing key to determine whether an email should be encrypted. 74 | When an ``Address`` is deleted via the Django Admin, the key is 75 | removed from the key ring on the server. 76 | 77 | 78 | Sending Multipart Email with Django Templates 79 | ============================================= 80 | 81 | As mentioned above, the following function is provided in 82 | the ``email_extras.utils`` module:: 83 | 84 | send_mail_template(subject, template, addr_from, addr_to, 85 | fail_silently=False, attachments=None, context=None, 86 | headers=None) 87 | 88 | The arguments that differ from ``django.core.mail.send_mail`` are 89 | ``template`` and ``context``. The ``template`` argument is simply 90 | the name of the template to be used for rendering the email contents. 91 | 92 | A template consists of both a HTML file and a TXT file each responsible 93 | for their respective versions of the email and should be stored in 94 | the ``email_extras`` directory where your templates are stored, 95 | therefore if the name ``contact_form`` was given for the ``template`` 96 | argument, the two template files for the email would be: 97 | 98 | * ``templates/email_extras/contact_form.html`` 99 | * ``templates/email_extras/contact_form.txt`` 100 | 101 | The ``attachments`` argument is a list of files to attach to the email. 102 | Each attachment can be the full filesystem path to the file, or a 103 | file name / file data pair. 104 | 105 | The ``context`` argument is simply a dictionary that is used to 106 | populate the email templates, much like a normal request context 107 | would be used for a regular Django template. 108 | 109 | The ``headers`` argument is a dictionary of extra headers to put on 110 | the message. The keys are the header name and values are the header 111 | values. 112 | 113 | 114 | Configuration 115 | ============= 116 | 117 | There are two settings you can configure in your project's 118 | ``settings.py`` module: 119 | 120 | * ``EMAIL_EXTRAS_USE_GNUPG`` - Boolean that controls whether the PGP 121 | encryption features are used. Defaults to ``True`` if 122 | ``EMAIL_EXTRAS_GNUPG_HOME`` is specified, otherwise ``False``. 123 | * ``EMAIL_EXTRAS_GNUPG_HOME`` - String representing a custom location 124 | for the GNUPG keyring. 125 | * ``EMAIL_EXTRAS_GNUPG_ENCODING`` - String representing a gnupg encoding. 126 | Defaults to GNUPG ``latin-1`` and could be changed to e.g. ``utf-8`` 127 | if needed. Check out 128 | `python-gnupg docs `_ 129 | for more info. 130 | * ``EMAIL_EXTRAS_ALWAYS_TRUST_KEYS`` - Skip key validation and assume 131 | that used keys are always fully trusted. 132 | 133 | 134 | Local Browser Testing 135 | ===================== 136 | 137 | When sending multipart emails during development, it can be useful 138 | to view the HTML part of the email in a web browser, without having 139 | to actually send emails and open them in a mail client. To use 140 | this feature during development, simply set your email backend as follows 141 | in your development ``settings.py`` module:: 142 | 143 | EMAIL_BACKEND = 'email_extras.backends.BrowsableEmailBackend' 144 | 145 | With this configured, each time a multipart email is sent, it will 146 | be written to a temporary file, which is then automatically opened 147 | in a local web browser. Suffice to say, this should only be enabled 148 | during development! 149 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # This file is automatically generated via sphinx-me 2 | from sphinx_me import setup_conf; setup_conf(globals()) 3 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: ../README.rst -------------------------------------------------------------------------------- /email_extras/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.3.3" 2 | default_app_config = 'email_extras.apps.EmailExtrasConfig' 3 | -------------------------------------------------------------------------------- /email_extras/admin.py: -------------------------------------------------------------------------------- 1 | 2 | from email_extras.settings import USE_GNUPG 3 | 4 | 5 | if USE_GNUPG: 6 | from django.contrib import admin 7 | 8 | from email_extras.models import Key, Address 9 | from email_extras.forms import KeyForm 10 | 11 | class KeyAdmin(admin.ModelAdmin): 12 | form = KeyForm 13 | list_display = ('__str__', 'email_addresses') 14 | readonly_fields = ('fingerprint', ) 15 | 16 | class AddressAdmin(admin.ModelAdmin): 17 | list_display = ('__str__', 'key') 18 | readonly_fields = ('key', ) 19 | 20 | def has_add_permission(self, request): 21 | return False 22 | 23 | admin.site.register(Key, KeyAdmin) 24 | admin.site.register(Address, AddressAdmin) 25 | -------------------------------------------------------------------------------- /email_extras/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class EmailExtrasConfig(AppConfig): 5 | name = 'email_extras' 6 | verbose_name = 'Email Extras' 7 | -------------------------------------------------------------------------------- /email_extras/backends.py: -------------------------------------------------------------------------------- 1 | 2 | from tempfile import NamedTemporaryFile 3 | import webbrowser 4 | 5 | from django.conf import settings 6 | from django.core.mail.backends.base import BaseEmailBackend 7 | 8 | 9 | class BrowsableEmailBackend(BaseEmailBackend): 10 | """ 11 | An email backend that opens HTML parts of emails sent 12 | in a local web browser, for testing during development. 13 | """ 14 | 15 | def send_messages(self, email_messages): 16 | if not settings.DEBUG: 17 | # Should never be used in production. 18 | return 19 | for message in email_messages: 20 | for body, content_type in getattr(message, "alternatives", []): 21 | if content_type == "text/html": 22 | self.open(body) 23 | 24 | def open(self, body): 25 | with NamedTemporaryFile(delete=False) as temp: 26 | temp.write(body.encode('utf-8')) 27 | 28 | webbrowser.open("file://" + temp.name) 29 | -------------------------------------------------------------------------------- /email_extras/forms.py: -------------------------------------------------------------------------------- 1 | 2 | from django import forms 3 | from django.utils.translation import ugettext_lazy as _ 4 | 5 | from email_extras.settings import USE_GNUPG, GNUPG_HOME 6 | 7 | if USE_GNUPG: 8 | from gnupg import GPG 9 | 10 | 11 | class KeyForm(forms.ModelForm): 12 | 13 | def clean_key(self): 14 | """ 15 | Validate the key contains an email address. 16 | """ 17 | key = self.cleaned_data["key"] 18 | gpg = GPG(gnupghome=GNUPG_HOME) 19 | result = gpg.import_keys(key) 20 | if result.count == 0: 21 | raise forms.ValidationError(_("Invalid Key")) 22 | return key 23 | -------------------------------------------------------------------------------- /email_extras/migrations/0001_initial.py: -------------------------------------------------------------------------------- 1 | from __future__ import unicode_literals 2 | 3 | from django.db import migrations, models 4 | 5 | 6 | class Migration(migrations.Migration): 7 | 8 | initial = True 9 | 10 | dependencies = [ 11 | ] 12 | 13 | operations = [ 14 | migrations.CreateModel( 15 | name='Address', 16 | fields=[ 17 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 18 | ('address', models.CharField(max_length=200)), 19 | ('use_asc', models.BooleanField(default=False, editable=False)), 20 | ], 21 | options={ 22 | 'verbose_name': 'Address', 23 | 'verbose_name_plural': 'Addresses', 24 | }, 25 | ), 26 | migrations.CreateModel( 27 | name='Key', 28 | fields=[ 29 | ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), 30 | ('key', models.TextField()), 31 | ('addresses', models.TextField(editable=False)), 32 | ('use_asc', models.BooleanField(default=False, help_text="If True, an '.asc' extension will be added to email attachments sent to the address for this key.")), 33 | ], 34 | options={ 35 | 'verbose_name': 'Key', 36 | 'verbose_name_plural': 'Keys', 37 | }, 38 | ), 39 | ] 40 | -------------------------------------------------------------------------------- /email_extras/migrations/0002_auto_20161103_0752.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.3 on 2016-11-03 07:52 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('email_extras', '0001_initial'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='key', 17 | name='addresses', 18 | field=models.TextField(default=''), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /email_extras/migrations/0003_auto_20161103_0315.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from __future__ import unicode_literals 3 | 4 | from django.db import migrations, models 5 | import django.db.models.deletion 6 | from gnupg import GPG 7 | 8 | from email_extras.settings import GNUPG_HOME 9 | 10 | 11 | def forward_change(apps, schema_editor): 12 | Key = apps.get_model('email_extras', 'Key') 13 | Address = apps.get_model('email_extras', 'Address') 14 | 15 | for key in Key.objects.all(): 16 | addresses = Address.objects.filter(address__in=key.addresses.split(',')) 17 | addresses.update(key=key) 18 | 19 | gpg = GPG(gnupghome=GNUPG_HOME) 20 | result = gpg.import_keys(key.key) 21 | key.fingerprint = result.fingerprints[0] 22 | key.save() 23 | 24 | 25 | def reverse_change(apps, schema_editor): 26 | Key = apps.get_model('email_extras', 'Key') 27 | for key in Key.objects.all(): 28 | key.addresses = ",".join(address.address for address in key.address_set.all()) 29 | key.save() 30 | 31 | 32 | class Migration(migrations.Migration): 33 | 34 | dependencies = [ 35 | ('email_extras', '0002_auto_20161103_0752'), 36 | ] 37 | 38 | operations = [ 39 | migrations.AddField( 40 | model_name='address', 41 | name='key', 42 | field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, to='email_extras.Key'), 43 | ), 44 | migrations.AddField( 45 | model_name='key', 46 | name='fingerprint', 47 | field=models.CharField(blank=True, editable=False, max_length=200), 48 | ), 49 | migrations.RunPython(forward_change, reverse_change), 50 | migrations.RemoveField( 51 | model_name='key', 52 | name='addresses', 53 | ), 54 | ] 55 | -------------------------------------------------------------------------------- /email_extras/migrations/0004_use_djangos_emailfield.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by Django 1.10.5 on 2017-03-17 22:35 3 | from __future__ import unicode_literals 4 | 5 | from django.db import migrations, models 6 | 7 | 8 | class Migration(migrations.Migration): 9 | 10 | dependencies = [ 11 | ('email_extras', '0003_auto_20161103_0315'), 12 | ] 13 | 14 | operations = [ 15 | migrations.AlterField( 16 | model_name='address', 17 | name='address', 18 | field=models.EmailField(blank=True, max_length=254), 19 | ), 20 | ] 21 | -------------------------------------------------------------------------------- /email_extras/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stephenmcd/django-email-extras/8399792998cee84810be2b315dd9b51b200f9218/email_extras/migrations/__init__.py -------------------------------------------------------------------------------- /email_extras/models.py: -------------------------------------------------------------------------------- 1 | 2 | from __future__ import unicode_literals 3 | from django.db import models 4 | from django.utils.encoding import python_2_unicode_compatible 5 | from django.utils.translation import ugettext_lazy as _ 6 | 7 | from email_extras.settings import USE_GNUPG, GNUPG_HOME 8 | from email_extras.utils import addresses_for_key 9 | 10 | 11 | if USE_GNUPG: 12 | from gnupg import GPG 13 | 14 | @python_2_unicode_compatible 15 | class Key(models.Model): 16 | """ 17 | Accepts a key and imports it via admin's save_model which 18 | omits saving. 19 | """ 20 | 21 | class Meta: 22 | verbose_name = _("Key") 23 | verbose_name_plural = _("Keys") 24 | 25 | key = models.TextField() 26 | fingerprint = models.CharField(max_length=200, blank=True, editable=False) 27 | use_asc = models.BooleanField(default=False, help_text=_( 28 | "If True, an '.asc' extension will be added to email attachments " 29 | "sent to the address for this key.")) 30 | 31 | def __str__(self): 32 | return self.fingerprint 33 | 34 | @property 35 | def email_addresses(self): 36 | return ",".join(str(address) for address in self.address_set.all()) 37 | 38 | def save(self, *args, **kwargs): 39 | gpg = GPG(gnupghome=GNUPG_HOME) 40 | result = gpg.import_keys(self.key) 41 | 42 | addresses = [] 43 | for key in result.results: 44 | addresses.extend(addresses_for_key(gpg, key)) 45 | 46 | self.fingerprint = result.fingerprints[0] 47 | 48 | super(Key, self).save(*args, **kwargs) 49 | for address in addresses: 50 | address, _ = Address.objects.get_or_create(key=self, address=address) 51 | address.use_asc = self.use_asc 52 | address.save() 53 | 54 | @python_2_unicode_compatible 55 | class Address(models.Model): 56 | """ 57 | Stores the address for a successfully imported key and allows 58 | deletion. 59 | """ 60 | 61 | class Meta: 62 | verbose_name = _("Address") 63 | verbose_name_plural = _("Addresses") 64 | 65 | address = models.EmailField(blank=True) 66 | key = models.ForeignKey('email_extras.Key', null=True, editable=False) 67 | use_asc = models.BooleanField(default=False, editable=False) 68 | 69 | def __str__(self): 70 | return self.address 71 | 72 | def delete(self): 73 | """ 74 | Remove any keys for this address. 75 | """ 76 | gpg = GPG(gnupghome=GNUPG_HOME) 77 | for key in gpg.list_keys(): 78 | if self.address in addresses_for_key(gpg, key): 79 | gpg.delete_keys(key["fingerprint"], True) 80 | gpg.delete_keys(key["fingerprint"]) 81 | super(Address, self).delete() 82 | -------------------------------------------------------------------------------- /email_extras/settings.py: -------------------------------------------------------------------------------- 1 | 2 | from django.conf import settings 3 | from django.core.exceptions import ImproperlyConfigured 4 | 5 | 6 | GNUPG_HOME = getattr(settings, "EMAIL_EXTRAS_GNUPG_HOME", None) 7 | USE_GNUPG = getattr(settings, "EMAIL_EXTRAS_USE_GNUPG", GNUPG_HOME is not None) 8 | ALWAYS_TRUST = getattr(settings, "EMAIL_EXTRAS_ALWAYS_TRUST_KEYS", False) 9 | GNUPG_ENCODING = getattr(settings, "EMAIL_EXTRAS_GNUPG_ENCODING", None) 10 | 11 | if USE_GNUPG: 12 | try: 13 | import gnupg # noqa: F401 14 | except ImportError: 15 | raise ImproperlyConfigured("Could not import gnupg") 16 | -------------------------------------------------------------------------------- /email_extras/templates/admin/email_extras/address/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | 3 | {% load admin_urls %} 4 | {% load i18n %} 5 | 6 | {% block extrahead %}{{ block.super }} 7 | 11 | {% endblock %} 12 | 13 | {% block after_related_objects %}{{ block.super }} 14 | 17 | {% endblock %} 18 | -------------------------------------------------------------------------------- /email_extras/templates/admin/email_extras/key/change_form.html: -------------------------------------------------------------------------------- 1 | {% extends "admin/change_form.html" %} 2 | 3 | {% load i18n %} 4 | 5 | {% block extrahead %}{{ block.super }} 6 | 10 | {% endblock %} 11 | 12 | {% block after_related_objects %}{{ block.super }} 13 |
14 | 15 |
16 | {% endblock %} 17 | -------------------------------------------------------------------------------- /email_extras/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import with_statement 2 | from os.path import basename 3 | from warnings import warn 4 | 5 | from django.template import loader 6 | from django.core.mail import EmailMultiAlternatives, get_connection 7 | from django.utils import six 8 | from django.utils.encoding import smart_text 9 | 10 | from email_extras.settings import (USE_GNUPG, GNUPG_HOME, ALWAYS_TRUST, 11 | GNUPG_ENCODING) 12 | 13 | 14 | if USE_GNUPG: 15 | from gnupg import GPG 16 | 17 | 18 | class EncryptionFailedError(Exception): 19 | pass 20 | 21 | 22 | def addresses_for_key(gpg, key): 23 | """ 24 | Takes a key and extracts the email addresses for it. 25 | """ 26 | fingerprint = key["fingerprint"] 27 | addresses = [] 28 | for key in gpg.list_keys(): 29 | if key["fingerprint"] == fingerprint: 30 | addresses.extend([address.split("<")[-1].strip(">") 31 | for address in key["uids"] if address]) 32 | return addresses 33 | 34 | 35 | def send_mail(subject, body_text, addr_from, recipient_list, 36 | fail_silently=False, auth_user=None, auth_password=None, 37 | attachments=None, body_html=None, html_message=None, 38 | connection=None, headers=None): 39 | """ 40 | Sends a multipart email containing text and html versions which 41 | are encrypted for each recipient that has a valid gpg key 42 | installed. 43 | """ 44 | 45 | # Make sure only one HTML option is specified 46 | if body_html is not None and html_message is not None: # pragma: no cover 47 | raise ValueError("You cannot specify body_html and html_message at " 48 | "the same time. Please only use html_message.") 49 | 50 | # Push users to update their code 51 | if body_html is not None: # pragma: no cover 52 | warn("Using body_html is deprecated; use the html_message argument " 53 | "instead. Please update your code.", DeprecationWarning) 54 | html_message = body_html 55 | 56 | # Allow for a single address to be passed in. 57 | if isinstance(recipient_list, six.string_types): 58 | recipient_list = [recipient_list] 59 | 60 | connection = connection or get_connection( 61 | username=auth_user, password=auth_password, 62 | fail_silently=fail_silently) 63 | 64 | # Obtain a list of the recipients that have gpg keys installed. 65 | key_addresses = {} 66 | if USE_GNUPG: 67 | from email_extras.models import Address 68 | key_addresses = dict(Address.objects.filter(address__in=recipient_list) 69 | .values_list('address', 'use_asc')) 70 | # Create the gpg object. 71 | if key_addresses: 72 | gpg = GPG(gnupghome=GNUPG_HOME) 73 | if GNUPG_ENCODING is not None: 74 | gpg.encoding = GNUPG_ENCODING 75 | 76 | # Check if recipient has a gpg key installed 77 | def has_pgp_key(addr): 78 | return addr in key_addresses 79 | 80 | # Encrypts body if recipient has a gpg key installed. 81 | def encrypt_if_key(body, addr_list): 82 | if has_pgp_key(addr_list[0]): 83 | encrypted = gpg.encrypt(body, addr_list[0], 84 | always_trust=ALWAYS_TRUST) 85 | if encrypted == "" and body != "": # encryption failed 86 | raise EncryptionFailedError("Encrypting mail to %s failed.", 87 | addr_list[0]) 88 | return smart_text(encrypted) 89 | return body 90 | 91 | # Load attachments and create name/data tuples. 92 | attachments_parts = [] 93 | if attachments is not None: 94 | for attachment in attachments: 95 | # Attachments can be pairs of name/data, or filesystem paths. 96 | if not hasattr(attachment, "__iter__"): 97 | with open(attachment, "rb") as f: 98 | attachments_parts.append((basename(attachment), f.read())) 99 | else: 100 | attachments_parts.append(attachment) 101 | 102 | # Send emails - encrypted emails needs to be sent individually, while 103 | # non-encrypted emails can be sent in one send. So the final list of 104 | # lists of addresses to send to looks like: 105 | # [[unencrypted1, unencrypted2, unencrypted3], [encrypted1], [encrypted2]] 106 | unencrypted = [addr for addr in recipient_list 107 | if addr not in key_addresses] 108 | unencrypted = [unencrypted] if unencrypted else unencrypted 109 | encrypted = [[addr] for addr in key_addresses] 110 | for addr_list in unencrypted + encrypted: 111 | msg = EmailMultiAlternatives(subject, 112 | encrypt_if_key(body_text, addr_list), 113 | addr_from, addr_list, 114 | connection=connection, headers=headers) 115 | if html_message is not None: 116 | if has_pgp_key(addr_list[0]): 117 | mimetype = "application/gpg-encrypted" 118 | else: 119 | mimetype = "text/html" 120 | msg.attach_alternative(encrypt_if_key(html_message, addr_list), 121 | mimetype) 122 | for parts in attachments_parts: 123 | name = parts[0] 124 | if key_addresses.get(addr_list[0]): 125 | name += ".asc" 126 | msg.attach(name, encrypt_if_key(parts[1], addr_list)) 127 | msg.send(fail_silently=fail_silently) 128 | 129 | 130 | def send_mail_template(subject, template, addr_from, recipient_list, 131 | fail_silently=False, attachments=None, context=None, 132 | connection=None, headers=None): 133 | """ 134 | Send email rendering text and html versions for the specified 135 | template name using the context dictionary passed in. 136 | """ 137 | 138 | if context is None: 139 | context = {} 140 | 141 | # Loads a template passing in vars as context. 142 | def render(ext): 143 | name = "email_extras/%s.%s" % (template, ext) 144 | return loader.get_template(name).render(context) 145 | 146 | send_mail(subject, render("txt"), addr_from, recipient_list, 147 | fail_silently=fail_silently, attachments=attachments, 148 | html_message=render("html"), connection=connection, 149 | headers=headers) 150 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | 2 | import sys 3 | from shutil import rmtree 4 | from setuptools import setup, find_packages 5 | 6 | 7 | if sys.argv[:2] == ["setup.py", "bdist_wheel"]: 8 | # Remove previous build dir when creating a wheel build, 9 | # since if files have been removed from the project, 10 | # they'll still be cached in the build dir and end up 11 | # as part of the build, which is unexpected. 12 | try: 13 | rmtree("build") 14 | except: 15 | pass 16 | 17 | 18 | setup( 19 | name="django-email-extras", 20 | version=__import__("email_extras").__version__, 21 | author="Stephen McDonald", 22 | author_email="steve@jupo.org", 23 | description="A Django reusable app providing the ability to send PGP " 24 | "encrypted and multipart emails using the Django templating " 25 | "system.", 26 | long_description=open("README.rst").read(), 27 | url="https://github.com/stephenmcd/django-email-extras", 28 | packages=find_packages(), 29 | zip_safe=False, 30 | include_package_data=True, 31 | install_requires=[ 32 | "python-gnupg" 33 | ], 34 | extras_require={ 35 | 'dev': [ 36 | "sphinx-me", 37 | ] 38 | }, 39 | classifiers=[ 40 | "Development Status :: 5 - Production/Stable", 41 | "Environment :: Web Environment", 42 | "Intended Audience :: Developers", 43 | "Operating System :: OS Independent", 44 | "Programming Language :: Python", 45 | "Programming Language :: Python :: 2.6", 46 | "Programming Language :: Python :: 2.7", 47 | "Programming Language :: Python :: 3", 48 | "Programming Language :: Python :: 3.3", 49 | "Framework :: Django", 50 | "Topic :: Communications :: Email", 51 | "Topic :: Security :: Cryptography", 52 | ] 53 | ) 54 | --------------------------------------------------------------------------------