diff --git a/README.rst b/README.rst index 8b70af8..ac59547 100644 --- a/README.rst +++ b/README.rst @@ -17,8 +17,8 @@ local web browser during development is also provided. Dependencies ============ - * `python-gnupg `_ is - required for sending PGP encrypted email. +* `python-gnupg `_ is + required for sending PGP encrypted email. Installation @@ -26,12 +26,16 @@ Installation The easiest way to install django-email-extras is directly from PyPi using `pip `_ by running the command -below:: +below: + +.. code-block:: bash $ pip install -U django-email-extras Otherwise you can download django-email-extras and install it directly -from source:: +from source: + +.. code-block:: bash $ python setup.py install @@ -43,8 +47,8 @@ Once installed, first add ``email_extras`` to your ``INSTALLED_APPS`` setting and run the migrations. Then there are two functions for sending email in the ``email_extras.utils`` module: - * ``send_mail`` - * ``send_mail_template`` +* ``send_mail`` +* ``send_mail_template`` The former mimics the signature of ``django.core.mail.send_mail`` while the latter provides the ability to send multipart emails @@ -75,15 +79,83 @@ When an ``Address`` is deleted via the Django Admin, the key is removed from the key ring on the server. +Sending PGP Signed Email +======================== + +Adding a private/public signing keypair is different than importing a +public encryption key, since the private key will be stored on the +server. + +This project ships with a Django management command to generate and +export private signing keys: ``email_signing_key`` +management command. + +You first need to set the ``EMAIL_EXTRAS_SIGNING_KEY_DATA`` option in your project's +``settings.py``. This is a dictionary that is passed as keyword arguments +directly to ``GPG.gen_key()``, so please read and understand all of the +available `options in their documentation `_. The default settings are: + +.. code-block:: python + + EMAIL_EXTRAS_SIGNING_KEY_DATA = { + 'key_type': "RSA", + 'key_length': 4096, + 'name_real': settings.SITE_NAME, + 'name_comment': "Outgoing email server", + 'name_email': settings.DEFAULT_FROM_EMAIL, + 'expire_date': '2y', + } + +You may wish to change the ``key_type`` to a signing-only type of key, +such as DSA, or the expire date. + +Once you are content with the signing key settings, generate a new +signing key with the ``--generate`` option: + +.. code-block:: bash + + python manage.py email_signing_key --generate + +To work with specific keys, identify them by their fingerprint + +.. code-block:: bash + + python manage.py email_signing_key 7AB59FE794A7AC12EBA87507EF33F601153CFE28 + +You can print the private key to your terminal/console with: + +.. code-block:: bash + + python manage.py email_signing_key 7AB59FE794A7AC12EBA87507EF33F601153CFE28 --print-private-key + +And you can upload the public signing key to one or more specified +keyservers by passing the key server hostnames with the ``-k`` or +``--keyserver`` options: + +.. code-block:: bash + + python manage.py email_signing_key 7AB59FE794A7AC12EBA87507EF33F601153CFE28 -k keys.ubuntu.com keys.redhat.com -k pgp.mit.edu + +You can also perform all tasks with one command: + +.. code-block:: bash + + python manage.py email_signing_key --generate --keyserver pgp.mit.edu --print-private-key + +Use the ``--help`` option to see the complete help text for the command. + + Sending Multipart Email with Django Templates ============================================= As mentioned above, the following function is provided in -the ``email_extras.utils`` module:: +the ``email_extras.utils`` module: + +.. code-block:: python - send_mail_template(subject, template, addr_from, addr_to, - fail_silently=False, attachments=None, context=None, - headers=None) + send_mail_template(subject, template, addr_from, addr_to, + fail_silently=False, attachments=None, context=None, + headers=None) The arguments that differ from ``django.core.mail.send_mail`` are ``template`` and ``context``. The ``template`` argument is simply @@ -95,8 +167,8 @@ the ``email_extras`` directory where your templates are stored, therefore if the name ``contact_form`` was given for the ``template`` argument, the two template files for the email would be: - * ``templates/email_extras/contact_form.html`` - * ``templates/email_extras/contact_form.txt`` +* ``templates/email_extras/contact_form.html`` +* ``templates/email_extras/contact_form.txt`` The ``attachments`` argument is a list of files to attach to the email. Each attachment can be the full filesystem path to the file, or a @@ -117,18 +189,18 @@ Configuration There are two settings you can configure in your project's ``settings.py`` module: - * ``EMAIL_EXTRAS_USE_GNUPG`` - Boolean that controls whether the PGP - encryption features are used. Defaults to ``True`` if - ``EMAIL_EXTRAS_GNUPG_HOME`` is specified, otherwise ``False``. - * ``EMAIL_EXTRAS_GNUPG_HOME`` - String representing a custom location - for the GNUPG keyring. - * ``EMAIL_EXTRAS_GNUPG_ENCODING`` - String representing a gnupg encoding. - Defaults to GNUPG ``latin-1`` and could be changed to e.g. ``utf-8`` - if needed. Check out - `python-gnupg docs `_ - for more info. - * ``EMAIL_EXTRAS_ALWAYS_TRUST_KEYS`` - Skip key validation and assume - that used keys are always fully trusted. +* ``EMAIL_EXTRAS_USE_GNUPG`` - Boolean that controls whether the PGP + encryption features are used. Defaults to ``True`` if + ``EMAIL_EXTRAS_GNUPG_HOME`` is specified, otherwise ``False``. +* ``EMAIL_EXTRAS_GNUPG_HOME`` - String representing a custom location + for the GNUPG keyring. +* ``EMAIL_EXTRAS_GNUPG_ENCODING`` - String representing a gnupg encoding. + Defaults to GNUPG ``latin-1`` and could be changed to e.g. ``utf-8`` + if needed. Check out + `python-gnupg docs `_ + for more info. +* ``EMAIL_EXTRAS_ALWAYS_TRUST_KEYS`` - Skip key validation and assume + that used keys are always fully trusted. Local Browser Testing @@ -138,9 +210,11 @@ When sending multipart emails during development, it can be useful to view the HTML part of the email in a web browser, without having to actually send emails and open them in a mail client. To use this feature during development, simply set your email backend as follows -in your development ``settings.py`` module:: +in your development ``settings.py`` module: + +.. code-block:: python - EMAIL_BACKEND = 'email_extras.backends.BrowsableEmailBackend' + EMAIL_BACKEND = 'email_extras.backends.BrowsableEmailBackend' With this configured, each time a multipart email is sent, it will be written to a temporary file, which is then automatically opened diff --git a/email_extras/admin.py b/email_extras/admin.py index 82bea64..0cb1030 100644 --- a/email_extras/admin.py +++ b/email_extras/admin.py @@ -1,4 +1,3 @@ - from email_extras.settings import USE_GNUPG diff --git a/email_extras/apps.py b/email_extras/apps.py index 4a810d5..d75963d 100644 --- a/email_extras/apps.py +++ b/email_extras/apps.py @@ -1,6 +1,13 @@ from django.apps import AppConfig +from email_extras.utils import check_signing_key + class EmailExtrasConfig(AppConfig): name = 'email_extras' verbose_name = 'Email Extras' + + # AFAICT, this is impossible to test + def ready(self): # pragma: noqa + # Fail early and loudly if the signing key fingerprint is misconfigured + check_signing_key() diff --git a/email_extras/backends.py b/email_extras/backends.py index c5f618b..9980c35 100644 --- a/email_extras/backends.py +++ b/email_extras/backends.py @@ -1,9 +1,23 @@ +from __future__ import with_statement +from os.path import basename from tempfile import NamedTemporaryFile import webbrowser from django.conf import settings from django.core.mail.backends.base import BaseEmailBackend +from django.core.mail.backends.console import EmailBackend as ConsoleBackend +from django.core.mail.backends.locmem import EmailBackend as LocmemBackend +from django.core.mail.backends.filebased import EmailBackend as FileBackend +from django.core.mail.backends.smtp import EmailBackend as SmtpBackend +from django.core.mail.message import EmailMultiAlternatives +from django.utils.encoding import smart_text + +from .handlers import (handle_failed_message_encryption, + handle_failed_alternative_encryption, + handle_failed_attachment_encryption) +from .settings import USE_GNUPG +from .utils import (EncryptionFailedError, encrypt_kwargs, get_gpg) class BrowsableEmailBackend(BaseEmailBackend): @@ -26,3 +40,166 @@ def open(self, body): temp.write(body.encode('utf-8')) webbrowser.open("file://" + temp.name) + + +if USE_GNUPG: + from .models import Address + + # Create the GPG object + gpg = get_gpg() + + def copy_message(msg): + return EmailMultiAlternatives( + to=msg.to, + cc=msg.cc, + bcc=msg.bcc, + reply_to=msg.reply_to, + from_email=msg.from_email, + subject=msg.subject, + body=msg.body, + attachments=msg.attachments, + headers=msg.extra_headers, + connection=msg.connection) + + def encrypt(text, addr): + encryption_result = gpg.encrypt(text, addr, **encrypt_kwargs) + if not encryption_result.ok: + raise EncryptionFailedError("Encrypting mail to %s failed: '%s'", + addr, encryption_result.status) + if smart_text(encryption_result) == "" and text != "": + raise EncryptionFailedError("Encrypting mail to %s failed.", + addr) + return smart_text(encryption_result) + + def encrypt_attachment(address, attachment, use_asc): + # Attachments can either just be filenames or a + # (filename, content, mimetype) triple + if not hasattr(attachment, "__iter__"): + filename = basename(attachment) + mimetype = None + + # If the attachment is just a filename, open the file, + # encrypt it, and attach it + with open(attachment, "rb") as f: + content = f.read() + else: + # Unpack attachment tuple + filename, content, mimetype = attachment + + # Ignore attachments if they're already encrypted + if mimetype == "application/gpg-encrypted": + return attachment + + try: + encrypted_content = encrypt(content, address) + except EncryptionFailedError as e: + # This function will need to decide what to do. Possibilities include + # one or more of: + # + # * Mail admins (possibly without encrypting the message to them) + # * Remove the offending key automatically + # * Set the body to a blank string + # * Set the body to the cleartext + # * Set the body to the cleartext, with a warning message prepended + # * Set the body to a custom error string + # * Reraise the exception + # + # However, the behavior will be very site-specific, because each site + # will have different attackers, different threat profiles, different + # compliance requirements, and different policies. + # + handle_failed_attachment_encryption(e) + else: + if use_asc and filename is not None: + filename += ".asc" + + return (filename, encrypted_content, "application/gpg-encrypted") + + def encrypt_messages(email_messages): + unencrypted_messages = [] + encrypted_messages = [] + for msg in email_messages: + # Copied out of utils.py + # Obtain a list of the recipients that have GPG keys installed + key_addrs = dict(Address.objects.filter(address__in=msg.to) + .values_list('address', 'use_asc')) + + # Encrypt emails - encrypted emails need to be sent individually, + # while non-encrypted emails can be sent in one send. So we split + # up each message into 1 or more parts: the unencrypted message + # that is addressed to everybody who doesn't have a key, and a + # separate message for people who do have keys. + unencrypted_msg = copy_message(msg) + unencrypted_msg.to = [addr for addr in msg.to + if addr not in key_addrs] + if unencrypted_msg.to: + unencrypted_messages.append(unencrypted_msg) + + # Make a new message object for each recipient with a key + new_msg = copy_message(msg) + + # Encrypt the message body and all attachments for all addresses + # we have keys for + for address, use_asc in key_addrs.items(): + if getattr(msg, 'do_not_encrypt_this_message', False): + unencrypted_messages.append(new_msg) + continue + + # Replace the message body with the encrypted message body + try: + new_msg.body = encrypt(new_msg.body, address) + except EncryptionFailedError as e: + handle_failed_message_encryption(e) + + # If the message has alternatives, encrypt them all + alternatives = [] + for alt, mimetype in getattr(new_msg, 'alternatives', []): + # Ignore alternatives if they're already encrypted + if mimetype == "application/gpg-encrypted": + alternatives.append((alt, mimetype)) + continue + + try: + encrypted_alternative = encrypt(alt, address) + except EncryptionFailedError as e: + handle_failed_alternative_encryption(e) + else: + alternatives.append((encrypted_alternative, + "application/gpg-encrypted")) + # Replace all of the alternatives + new_msg.alternatives = alternatives + + # Replace all unencrypted attachments with their encrypted + # versions + attachments = [] + for attachment in new_msg.attachments: + attachments.append( + encrypt_attachment(address, attachment, use_asc)) + new_msg.attachments = attachments + + encrypted_messages.append(new_msg) + + return unencrypted_messages + encrypted_messages + + class EncryptingEmailBackendMixin(object): + def send_messages(self, email_messages): + if USE_GNUPG: + email_messages = encrypt_messages(email_messages) + super(EncryptingEmailBackendMixin, self)\ + .send_messages(email_messages) + + class EncryptingConsoleEmailBackend(EncryptingEmailBackendMixin, + ConsoleBackend): + pass + + class EncryptingLocmemEmailBackend(EncryptingEmailBackendMixin, + LocmemBackend): + pass + + class EncryptingFilebasedEmailBackend(EncryptingEmailBackendMixin, + FileBackend): + pass + + class EncryptingSmtpEmailBackend(EncryptingEmailBackendMixin, + SmtpBackend): + pass diff --git a/email_extras/forms.py b/email_extras/forms.py index 3be4453..ad7c46c 100644 --- a/email_extras/forms.py +++ b/email_extras/forms.py @@ -2,10 +2,7 @@ from django import forms from django.utils.translation import ugettext_lazy as _ -from email_extras.settings import USE_GNUPG, GNUPG_HOME - -if USE_GNUPG: - from gnupg import GPG +from email_extras.utils import get_gpg class KeyForm(forms.ModelForm): @@ -15,7 +12,7 @@ def clean_key(self): Validate the key contains an email address. """ key = self.cleaned_data["key"] - gpg = GPG(gnupghome=GNUPG_HOME) + gpg = get_gpg() result = gpg.import_keys(key) if result.count == 0: raise forms.ValidationError(_("Invalid Key")) diff --git a/email_extras/handlers.py b/email_extras/handlers.py new file mode 100644 index 0000000..eefd06e --- /dev/null +++ b/email_extras/handlers.py @@ -0,0 +1,109 @@ +import inspect + +from django.conf import settings +from django.core.mail import mail_admins + +from .models import Address +from .settings import FAILURE_HANDLERS + + +ADMIN_ADDRESSES = [admin[1] for admin in settings.ADMINS] + + +def get_variable_from_exception(exception, variable_name): + """ + Grab the variable from closest frame in the stack + """ + for frame in reversed(inspect.trace()): + try: + # From http://stackoverflow.com/a/9059407/6461688 + frame_variable = frame[0].f_locals[variable_name] + except KeyError: + pass + else: + return frame_variable + else: + raise KeyError("Variable '%s' not in any stack frames", variable_name) + + +def default_handle_failed_encryption(exception): + """ + Handle failures when trying to encrypt alternative content for messages + """ + raise exception + + +def default_handle_failed_alternative_encryption(exception): + """ + Handle failures when trying to encrypt alternative content for messages + """ + raise exception + + +def default_handle_failed_attachment_encryption(exception): + """ + Handle failures when trying to encrypt alternative content for messages + """ + raise exception + + +def force_mail_admins(unencrypted_message, address): + """ + Mail admins when encryption fails, and send the message unencrypted if + the recipient is an admin + """ + + if address in ADMIN_ADDRESSES: + # We assume that it is more important to mail the admin *without* + # encrypting the message + force_send_message(unencrypted_message) + else: + mail_admins( + "Failed encryption attempt", + """ + There was a problem encrypting an email message. + + Subject: "{subject}" + Address: "{address}" + """) + + +def force_delete_key(address): + """ + Delete the key from the keyring and the Key and Address objects from the + database + """ + address_object = Address.objects.get(address=address) + address_object.key.delete() + address_object.delete() + + +def force_send_message(unencrypted_message): + """ + Send the message unencrypted + """ + unencrypted_message.do_not_encrypt_this_message = True + unencrypted_message.send() + + +def import_function(key): + mod, _, function = FAILURE_HANDLERS[key].rpartition('.') + try: + # Python 3.4+ + from importlib import import_module + except ImportError: + # Python < 3.4 + # From http://stackoverflow.com/a/8255024/6461688 + mod = __import__(mod, globals(), locals(), [function]) + else: + mod = import_module(mod) + return getattr(mod, function) + +exception_handlers = { + 'message': 'handle_failed_message_encryption', + 'alternative': 'handle_failed_alternative_encryption', + 'attachment': 'handle_failed_attachment_encryption', +} + +for key, value in exception_handlers.items(): + locals()[value] = import_function(key) diff --git a/email_extras/management/__init__.py b/email_extras/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/email_extras/management/commands/__init__.py b/email_extras/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/email_extras/management/commands/email_signing_key.py b/email_extras/management/commands/email_signing_key.py new file mode 100644 index 0000000..b457bf6 --- /dev/null +++ b/email_extras/management/commands/email_signing_key.py @@ -0,0 +1,103 @@ +""" +Script to generate and upload a signing key to keyservers +""" +from __future__ import print_function + +import argparse +import sys + +from django.core.management.base import LabelCommand +from django.utils.translation import ugettext as _ + +from email_extras.models import Key +from email_extras.settings import SIGNING_KEY_DATA +from email_extras.utils import get_gpg + + +gpg = get_gpg() + + +# Create an action that *extends* a list, instead of *appending* to it +class ExtendAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + items = getattr(namespace, self.dest) or [] + items.extend(values) + setattr(namespace, self.dest, items) + + +class Command(LabelCommand): + label = "FINGERPRINT" + missing_args_message = ("Enter at least one fingerprint or use the " + "--generate option.") + + def add_arguments(self, parser): + # Register our extending action + parser.register('action', 'extend', ExtendAction) + + parser.add_argument('args', metavar=self.label, nargs='*') + parser.add_argument( + '--generate', + action='store_true', + default=False, + help=_("Generate a new signing key")) + parser.add_argument( + '--print-private-key', + action='store_true', + default=False, + dest='print_private_key', + help=_("Print the private signing key")) + parser.add_argument( + '-k', '--keyserver', + # We want multiple uses of -k server1 server 2 -k server3 server4 + # to be interpreted as [server1, server2, server3, server4], so we + # need to use the custom ExtendAction we defiend before + action='extend', + nargs='+', + dest='keyservers', + help=_("Upload (the most recently generated) public signing key " + "to the specified keyservers")) + + def handle(self, *labels, **options): + # EITHER specify the key fingerprints OR generate a key + if options.get('generate') and labels: + print("You cannot specify fingerprints and --generate when " + "running this command") + sys.exit(-1) + + if options.get('generate'): + signing_key_cmd = gpg.gen_key_input(**SIGNING_KEY_DATA) + new_signing_key = gpg.gen_key(signing_key_cmd) + + exported_signing_key = gpg.export_keys( + new_signing_key.fingerprint) + + self.key = Key.objects.create(key=exported_signing_key, + use_asc=False) + labels = [self.key.fingerprint] + + return super(Command, self).handle(*labels, **options) + + def handle_label(self, label, **options): + try: + self.key = Key.objects.get(fingerprint=label) + except Key.DoesNotExist: + print("Key matching fingerprint '%(fp)s' not found." % { + 'fp': label, + }) + sys.exit(-1) + + for ks in set(options.get('keyservers')): + gpg.send_keys(ks, self.key.fingerprint) + + output = '' + + if options.get('print_private_key'): + output += gpg.export_keys([self.key.fingerprint], True) + + # If we havne't been told to do anything else, print out the public + # signing key + if not options.get('keyservers') and \ + not options.get('print_private_key'): + output += gpg.export_keys([self.key.fingerprint]) + + return output diff --git a/email_extras/migrations/0003_auto_20161103_0315.py b/email_extras/migrations/0003_auto_20161103_0315.py index a8085dc..a6dbc12 100644 --- a/email_extras/migrations/0003_auto_20161103_0315.py +++ b/email_extras/migrations/0003_auto_20161103_0315.py @@ -3,9 +3,8 @@ from django.db import migrations, models import django.db.models.deletion -from gnupg import GPG -from email_extras.settings import GNUPG_HOME +from email_extras.utils import get_gpg def forward_change(apps, schema_editor): @@ -16,7 +15,7 @@ def forward_change(apps, schema_editor): addresses = Address.objects.filter(address__in=key.addresses.split(',')) addresses.update(key=key) - gpg = GPG(gnupghome=GNUPG_HOME) + gpg = get_gpg() result = gpg.import_keys(key.key) key.fingerprint = result.fingerprints[0] key.save() diff --git a/email_extras/models.py b/email_extras/models.py index c2ef146..913776a 100644 --- a/email_extras/models.py +++ b/email_extras/models.py @@ -4,13 +4,11 @@ from django.utils.encoding import python_2_unicode_compatible from django.utils.translation import ugettext_lazy as _ -from email_extras.settings import USE_GNUPG, GNUPG_HOME -from email_extras.utils import addresses_for_key +from email_extras.settings import USE_GNUPG +from email_extras.utils import addresses_for_key, get_gpg if USE_GNUPG: - from gnupg import GPG - @python_2_unicode_compatible class Key(models.Model): """ @@ -36,7 +34,7 @@ def email_addresses(self): return ",".join(str(address) for address in self.address_set.all()) def save(self, *args, **kwargs): - gpg = GPG(gnupghome=GNUPG_HOME) + gpg = get_gpg() result = gpg.import_keys(self.key) addresses = [] @@ -73,7 +71,7 @@ def delete(self): """ Remove any keys for this address. """ - gpg = GPG(gnupghome=GNUPG_HOME) + gpg = get_gpg() for key in gpg.list_keys(): if self.address in addresses_for_key(gpg, key): gpg.delete_keys(key["fingerprint"], True) diff --git a/email_extras/settings.py b/email_extras/settings.py index fe17e4d..ffc0f05 100644 --- a/email_extras/settings.py +++ b/email_extras/settings.py @@ -1,12 +1,29 @@ - from django.conf import settings from django.core.exceptions import ImproperlyConfigured GNUPG_HOME = getattr(settings, "EMAIL_EXTRAS_GNUPG_HOME", None) USE_GNUPG = getattr(settings, "EMAIL_EXTRAS_USE_GNUPG", GNUPG_HOME is not None) + ALWAYS_TRUST = getattr(settings, "EMAIL_EXTRAS_ALWAYS_TRUST_KEYS", False) +FAILURE_HANDLERS = { + 'message': 'email_extras.handlers.default_handle_failed_encryption', + 'alternative': 'email_extras.handlers.default_handle_failed_alternative_encryption', + 'attachment': 'email_extras.handlers.default_handle_failed_attachment_encryption', +} +FAILURE_HANDLERS.update(getattr(settings, "EMAIL_EXTRAS_FAILURE_HANDLERS", {})) GNUPG_ENCODING = getattr(settings, "EMAIL_EXTRAS_GNUPG_ENCODING", None) +SIGNING_KEY_DATA = { + 'key_type': "RSA", + 'key_length': 4096, + 'name_real': settings.SITE_NAME, + 'name_comment': "Outgoing email server", + 'name_email': settings.DEFAULT_FROM_EMAIL, + 'expire_date': '2y', +} +SIGNING_KEY_DATA.update(getattr(settings, "EMAIL_EXTRAS_SIGNING_KEY_DATA", {})) +SIGNING_KEY_FINGERPRINT = getattr( + settings, "EMAIL_EXTRAS_SIGNING_KEY_FINGERPRINT", None) if USE_GNUPG: try: diff --git a/email_extras/utils.py b/email_extras/utils.py index ebb2352..5f1d0eb 100644 --- a/email_extras/utils.py +++ b/email_extras/utils.py @@ -1,4 +1,5 @@ from __future__ import with_statement + from os.path import basename from warnings import warn @@ -7,18 +8,54 @@ from django.utils import six from django.utils.encoding import smart_text -from email_extras.settings import (USE_GNUPG, GNUPG_HOME, ALWAYS_TRUST, - GNUPG_ENCODING) +from gnupg import GPG + +from email_extras.settings import (ALWAYS_TRUST, GNUPG_ENCODING, GNUPG_HOME, + USE_GNUPG, SIGNING_KEY_FINGERPRINT) if USE_GNUPG: from gnupg import GPG + def get_gpg(): + gpg = GPG(gnupghome=GNUPG_HOME) + if GNUPG_ENCODING is not None: + gpg.encoding = GNUPG_ENCODING + return gpg + +# Used internally +encrypt_kwargs = { + 'always_trust': ALWAYS_TRUST, + 'sign': SIGNING_KEY_FINGERPRINT, +} + class EncryptionFailedError(Exception): pass +class BadSigningKeyError(KeyError): + pass + + +def check_signing_key(): + if USE_GNUPG and SIGNING_KEY_FINGERPRINT is not None: + gpg = get_gpg() + try: + gpg.list_keys(True).key_map[SIGNING_KEY_FINGERPRINT] + except KeyError as e: + raise BadSigningKeyError( + "The key specified by the " + "EMAIL_EXTRAS_SIGNING_KEY_FINGERPRINT setting " + "({fp}) does not exist in the GPG keyring. Adjust the " + "EMAIL_EXTRAS_GNUPG_HOME setting (currently set to " + "{gnupg_home}, correct the key fingerprint, or generate a new " + "key by running python manage.py email_signing_key --generate " + "to fix.".format( + fp=SIGNING_KEY_FINGERPRINT, + gnupg_home=GNUPG_HOME)) + + def addresses_for_key(gpg, key): """ Takes a key and extracts the email addresses for it. @@ -69,9 +106,7 @@ def send_mail(subject, body_text, addr_from, recipient_list, .values_list('address', 'use_asc')) # Create the gpg object. if key_addresses: - gpg = GPG(gnupghome=GNUPG_HOME) - if GNUPG_ENCODING is not None: - gpg.encoding = GNUPG_ENCODING + gpg = get_gpg() # Check if recipient has a gpg key installed def has_pgp_key(addr): @@ -80,8 +115,7 @@ def has_pgp_key(addr): # Encrypts body if recipient has a gpg key installed. def encrypt_if_key(body, addr_list): if has_pgp_key(addr_list[0]): - encrypted = gpg.encrypt(body, addr_list[0], - always_trust=ALWAYS_TRUST) + encrypted = gpg.encrypt(body, addr_list[0], **encrypt_kwargs) if encrypted == "" and body != "": # encryption failed raise EncryptionFailedError("Encrypting mail to %s failed.", addr_list[0])