Skip to content

Commit

Permalink
Add encrypting backend mixin and mix it in with Django's built-in bac…
Browse files Browse the repository at this point in the history
…kends
  • Loading branch information
blag committed Mar 30, 2017
1 parent 3b37325 commit 61e049d
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 4 deletions.
176 changes: 176 additions & 0 deletions email_extras/backends.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
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 .settings import (GNUPG_HOME, GNUPG_ENCODING, USE_GNUPG, encrypt_kwargs)
from .utils import EncryptionFailedError


class BrowsableEmailBackend(BaseEmailBackend):
Expand All @@ -26,3 +37,168 @@ def open(self, body):
temp.write(body.encode('utf-8'))

webbrowser.open("file://" + temp.name)


class AttachmentEncryptionFailedError(EncryptionFailedError):
pass


class AlternativeEncryptionFailedError(EncryptionFailedError):
pass


if USE_GNUPG:
from gnupg import GPG

from .models import Address

# Create the GPG object
gpg = GPG(gnupghome=GNUPG_HOME)
if GNUPG_ENCODING is not None:
gpg.encoding = GNUPG_ENCODING

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 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:
# SECURITY: We could include a piece of the content here, but that
# would leak information in logs and to the admins. So instead, we
# only try to include the filename.
raise AttachmentEncryptionFailedError(
"Encrypting attachment to %s failed: %s (%s)", address,
filename, e.msg)
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
new_msg.body = encrypt(new_msg.body, address)

# 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,
**encrypt_kwargs)
except EncryptionFailedError as e:
raise AlternativeEncryptionFailedError(
"Encrypting alternative to %s failed: %s (%s)",
address, alt, e.msg)
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
6 changes: 6 additions & 0 deletions email_extras/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
ALWAYS_TRUST = getattr(settings, "EMAIL_EXTRAS_ALWAYS_TRUST_KEYS", False)
GNUPG_ENCODING = getattr(settings, "EMAIL_EXTRAS_GNUPG_ENCODING", None)

# Used internally
encrypt_kwargs = {
'always_trust': ALWAYS_TRUST,
}


if USE_GNUPG:
try:
import gnupg # noqa: F401
Expand Down
7 changes: 3 additions & 4 deletions email_extras/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
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 email_extras.settings import (USE_GNUPG, GNUPG_HOME, GNUPG_ENCODING,
encrypt_kwargs)


if USE_GNUPG:
Expand Down Expand Up @@ -80,8 +80,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])
Expand Down

0 comments on commit 61e049d

Please sign in to comment.