diff --git a/.travis.yml b/.travis.yml index a7c2eec..6e26bdb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,6 +38,7 @@ matrix: python: '3.4' install: - pip install -q Django$DJANGO +- pip install -r test_requirements.txt - pip install -q -e . script: - python setup.py test diff --git a/django_mailbox/models.py b/django_mailbox/models.py index ce218de..3c7e785 100644 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -20,13 +20,13 @@ import uuid import six from six.moves.urllib.parse import parse_qs, unquote, urlparse -from django.conf import settings +from django.conf import settings as django_settings from django.core.files.base import ContentFile from django.core.mail.message import make_msgid from django.db import models from django.utils.translation import ugettext_lazy as _ -from .utils import convert_header_to_unicode, get_body_from_message +from django_mailbox import utils from django_mailbox.signals import message_received from django_mailbox.transports import Pop3Transport, ImapTransport, \ MaildirTransport, MboxTransport, BabylTransport, MHTransport, \ @@ -35,55 +35,6 @@ from django_mailbox.transports import Pop3Transport, ImapTransport, \ logger = logging.getLogger(__name__) -STRIP_UNALLOWED_MIMETYPES = getattr( - settings, - 'DJANGO_MAILBOX_STRIP_UNALLOWED_MIMETYPES', - False -) - -ALLOWED_MIMETYPES = getattr( - settings, - 'DJANGO_MAILBOX_ALLOWED_MIMETYPES', - [ - 'text/plain', - 'text/html' - ] -) - -TEXT_STORED_MIMETYPES = getattr( - settings, - 'DJANGO_MAILBOX_TEXT_STORED_MIMETYPES', - [ - 'text/plain', - 'text/html' - ] -) - -ALTERED_MESSAGE_HEADER = getattr( - settings, - 'DJANGO_MAILBOX_ALTERED_MESSAGE_HEADER', - 'X-Django-Mailbox-Altered-Message' -) - -ATTACHMENT_INTERPOLATION_HEADER = getattr( - settings, - 'DJANGO_MAILBOX_ATTACHMENT_INTERPOLATION_HEADER', - 'X-Django-Mailbox-Interpolate-Attachment' -) - -ATTACHMENT_UPLOAD_TO = getattr( - settings, - 'DJANGO_MAILBOX_ATTACHMENT_UPLOAD_TO', - 'mailbox_attachments/%Y/%m/%d/' -) - -STORE_ORIGINAL_MESSAGE = getattr( - settings, - 'DJANGO_MAILBOX_STORE_ORIGINAL_MESSAGE', - False -) - - class ActiveMailboxManager(models.Manager): def get_queryset(self): return super(ActiveMailboxManager, self).get_queryset().filter( @@ -272,6 +223,8 @@ class Mailbox(models.Model): return msg def _get_dehydrated_message(self, msg, record): + settings = utils.get_settings() + new = EmailMessage() if msg.is_multipart(): for header, value in msg.items(): @@ -281,25 +234,27 @@ class Mailbox(models.Model): self._get_dehydrated_message(part, record) ) elif ( - STRIP_UNALLOWED_MIMETYPES - and not msg.get_content_type() in ALLOWED_MIMETYPES + settings['strip_unallowed_mimetypes'] + and not msg.get_content_type() in settings['allowed_mimetypes'] ): for header, value in msg.items(): new[header] = value # Delete header, otherwise when attempting to deserialize the # payload, it will be expecting a body for this. del new['Content-Transfer-Encoding'] - new[ALTERED_MESSAGE_HEADER] = ( + new[settings['altered_message_header']] = ( 'Stripped; Content type %s not allowed' % ( msg.get_content_type() ) ) new.set_payload('') elif ( - (msg.get_content_type() not in TEXT_STORED_MIMETYPES) or + ( + msg.get_content_type() not in settings['text_stored_mimetypes'] + ) or ('attachment' in msg.get('Content-Disposition', '')) ): - filename = msg.get_filename() + filename = utils.convert_header_to_unicode(msg.get_filename()) if not filename: extension = mimetypes.guess_extension(msg.get_content_type()) else: @@ -323,7 +278,9 @@ class Mailbox(models.Model): attachment.save() placeholder = EmailMessage() - placeholder[ATTACHMENT_INTERPOLATION_HEADER] = str(attachment.pk) + placeholder[ + settings['attachment_interpolation_header'] + ] = str(attachment.pk) new = placeholder else: content_charset = msg.get_content_charset() @@ -361,19 +318,25 @@ class Mailbox(models.Model): def _process_message(self, message): msg = Message() - if STORE_ORIGINAL_MESSAGE: - msg.eml.save('%s.eml' % uuid.uuid4(), ContentFile(message), save=False) + settings = utils.get_settings() + + if settings['store_original_message']: + msg.eml.save( + '%s.eml' % uuid.uuid4(), ContentFile(message), save=False + ) msg.mailbox = self if 'subject' in message: - msg.subject = convert_header_to_unicode(message['subject'])[0:255] + msg.subject = ( + utils.convert_header_to_unicode(message['subject'])[0:255] + ) if 'message-id' in message: msg.message_id = message['message-id'][0:255].strip() if 'from' in message: - msg.from_header = convert_header_to_unicode(message['from']) + msg.from_header = utils.convert_header_to_unicode(message['from']) if 'to' in message: - msg.to_header = convert_header_to_unicode(message['to']) + msg.to_header = utils.convert_header_to_unicode(message['to']) elif 'Delivered-To' in message: - msg.to_header = convert_header_to_unicode(message['Delivered-To']) + msg.to_header = utils.convert_header_to_unicode(message['Delivered-To']) msg.save() message = self._get_dehydrated_message(message, msg) msg.set_body(message.as_string()) @@ -557,7 +520,7 @@ class Message(models.Model): if self.mailbox.from_email: message.from_email = self.mailbox.from_email else: - message.from_email = settings.DEFAULT_FROM_EMAIL + message.from_email = django_settings.DEFAULT_FROM_EMAIL message.extra_headers['Message-ID'] = make_msgid() message.extra_headers['Date'] = formatdate() message.extra_headers['In-Reply-To'] = self.message_id.strip() @@ -573,7 +536,7 @@ class Message(models.Model): """ Returns the message body matching content type 'text/plain'. """ - return get_body_from_message( + return utils.get_body_from_message( self.get_email_object(), 'text', 'plain' ).replace('=\n', '').strip() @@ -582,12 +545,14 @@ class Message(models.Model): """ Returns the message body matching content type 'text/html'. """ - return get_body_from_message( + return utils.get_body_from_message( self.get_email_object(), 'text', 'html' ).replace('\n', '').strip() def _rehydrate(self, msg): new = EmailMessage() + settings = utils.get_settings() + if msg.is_multipart(): for header, value in msg.items(): new[header] = value @@ -595,10 +560,10 @@ class Message(models.Model): new.attach( self._rehydrate(part) ) - elif ATTACHMENT_INTERPOLATION_HEADER in msg.keys(): + elif settings['attachment_interpolation_header'] in msg.keys(): try: attachment = MessageAttachment.objects.get( - pk=msg[ATTACHMENT_INTERPOLATION_HEADER] + pk=msg[settings['attachment_interpolation_header']] ) for header, value in attachment.items(): new[header] = value @@ -627,9 +592,9 @@ class Message(models.Model): del new['Content-Transfer-Encoding'] encode_base64(new) except MessageAttachment.DoesNotExist: - new[ALTERED_MESSAGE_HEADER] = ( + new[settings['altered_message_header']] = ( 'Missing; Attachment %s not found' % ( - msg[ATTACHMENT_INTERPOLATION_HEADER] + msg[settings['attachment_interpolation_header']] ) ) new.set_payload('') @@ -723,7 +688,7 @@ class MessageAttachment(models.Model): document = models.FileField( _(u'Document'), - upload_to=ATTACHMENT_UPLOAD_TO, + upload_to=utils.get_attachment_save_path, ) def delete(self, *args, **kwargs): diff --git a/django_mailbox/runtests.py b/django_mailbox/runtests.py deleted file mode 100755 index b96be50..0000000 --- a/django_mailbox/runtests.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python -import sys - -from os.path import dirname, abspath - -try: - from django import setup -except ImportError: - pass -from django.conf import settings - -if not settings.configured: - settings.configure( - DATABASES={ - 'default': { - 'ENGINE': 'django.db.backends.sqlite3', - }, - }, - INSTALLED_APPS=[ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django_mailbox', - ] - ) - -try: - from django.test.runner import DiscoverRunner as TestRunner -except ImportError: - from django.test.simple import DjangoTestSuiteRunner as TestRunner - - -def runtests(*test_args): - if not test_args: - test_args = ['django_mailbox'] - parent = dirname(abspath(__file__)) - sys.path.insert(0, parent) - try: - # ensure that AppRegistry has loaded - setup() - except NameError: - # This version of Django is too old for an app registry. - pass - runner = TestRunner( - verbosity=1, - interactive=False, - failfast=False - ) - failures = runner.run_tests(test_args) - sys.exit(failures) - - -if __name__ == '__main__': - runtests(*sys.argv[1:]) diff --git a/django_mailbox/tests/__init__.py b/django_mailbox/tests/__init__.py index e30b827..e69de29 100644 --- a/django_mailbox/tests/__init__.py +++ b/django_mailbox/tests/__init__.py @@ -1,6 +0,0 @@ -from .test_mailbox import * -from .test_message_flattening import * -from .test_process_email import * -from .test_transports import * -from .test_integration_imap import * -from .test_settings import * diff --git a/django_mailbox/tests/base.py b/django_mailbox/tests/base.py index 5e50fa4..0f5516c 100644 --- a/django_mailbox/tests/base.py +++ b/django_mailbox/tests/base.py @@ -7,7 +7,7 @@ import six from django.conf import settings from django.test import TestCase -from django_mailbox import models +from django_mailbox import models, utils from django_mailbox.models import Mailbox, Message @@ -34,9 +34,13 @@ class EmailMessageTestCase(TestCase): ] def setUp(self): - self._ALLOWED_MIMETYPES = models.ALLOWED_MIMETYPES - self._STRIP_UNALLOWED_MIMETYPES = models.STRIP_UNALLOWED_MIMETYPES - self._TEXT_STORED_MIMETYPES = models.TEXT_STORED_MIMETYPES + dm_settings = utils.get_settings() + + self._ALLOWED_MIMETYPES = dm_settings['allowed_mimetypes'] + self._STRIP_UNALLOWED_MIMETYPES = ( + dm_settings['strip_unallowed_mimetypes'] + ) + self._TEXT_STORED_MIMETYPES = dm_settings['text_stored_mimetypes'] self.mailbox = Mailbox.objects.create(from_email='from@example.com') diff --git a/django_mailbox/tests/settings.py b/django_mailbox/tests/settings.py new file mode 100644 index 0000000..0dcd799 --- /dev/null +++ b/django_mailbox/tests/settings.py @@ -0,0 +1,11 @@ +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + }, +} +INSTALLED_APPS = [ + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django_mailbox', +] +SECRET_KEY = 'beepboop' diff --git a/django_mailbox/tests/test_message_flattening.py b/django_mailbox/tests/test_message_flattening.py index 1399a34..fe97b5c 100644 --- a/django_mailbox/tests/test_message_flattening.py +++ b/django_mailbox/tests/test_message_flattening.py @@ -1,4 +1,8 @@ -from django_mailbox import models +import copy + +import mock + +from django_mailbox import models, utils from django_mailbox.models import Message from django_mailbox.tests.base import EmailMessageTestCase @@ -74,10 +78,16 @@ class TestMessageFlattening(EmailMessageTestCase): expected_email_object = self._get_email_object( 'message_with_many_multiparts_stripped_html.eml', ) - models.STRIP_UNALLOWED_MIMETYPES = True - models.ALLOWED_MIMETYPES = ['text/plain'] + default_settings = utils.get_settings() - msg = self.mailbox.process_incoming_message(incoming_email_object) + with mock.patch('django_mailbox.utils.get_settings') as get_settings: + altered = copy.deepcopy(default_settings) + altered['strip_unallowed_mimetypes'] = True + altered['allowed_mimetypes'] = ['text/plain'] + + get_settings.return_value = altered + + msg = self.mailbox.process_incoming_message(incoming_email_object) actual_email_object = msg.get_email_object() diff --git a/django_mailbox/tests/test_settings.py b/django_mailbox/tests/test_settings.py deleted file mode 100644 index c183cf2..0000000 --- a/django_mailbox/tests/test_settings.py +++ /dev/null @@ -1,10 +0,0 @@ -from django.test import TestCase -from django.conf import settings -from django_mailbox.models import ATTACHMENT_UPLOAD_TO - - -class TestSettings(TestCase): - def test_default_attachment_upload_to(self): - user_setting = getattr(settings, 'DJANGO_MAILBOX_ATTACHMENT_UPLOAD_TO', False) - self.assertFalse(user_setting) - self.assertEqual(ATTACHMENT_UPLOAD_TO, 'mailbox_attachments/%Y/%m/%d/') diff --git a/django_mailbox/utils.py b/django_mailbox/utils.py index 75b1ee2..bd47e4d 100644 --- a/django_mailbox/utils.py +++ b/django_mailbox/utils.py @@ -1,5 +1,6 @@ import email.header import logging +import os import six @@ -9,19 +10,65 @@ from django.conf import settings logger = logging.getLogger(__name__) -DEFAULT_CHARSET = getattr( - settings, - 'DJANGO_MAILBOX_DEFAULT_CHARSET', - 'iso8859-1', -) +def get_settings(): + return { + 'strip_unallowed_mimetypes': getattr( + settings, + 'DJANGO_MAILBOX_STRIP_UNALLOWED_MIMETYPES', + False + ), + 'allowed_mimetypes': getattr( + settings, + 'DJANGO_MAILBOX_ALLOWED_MIMETYPES', + [ + 'text/plain', + 'text/html' + ] + ), + 'text_stored_mimetypes': getattr( + settings, + 'DJANGO_MAILBOX_TEXT_STORED_MIMETYPES', + [ + 'text/plain', + 'text/html' + ] + ), + 'altered_message_header': getattr( + settings, + 'DJANGO_MAILBOX_ALTERED_MESSAGE_HEADER', + 'X-Django-Mailbox-Altered-Message' + ), + 'attachment_interpolation_header': getattr( + settings, + 'DJANGO_MAILBOX_ATTACHMENT_INTERPOLATION_HEADER', + 'X-Django-Mailbox-Interpolate-Attachment' + ), + 'attachment_upload_to': getattr( + settings, + 'DJANGO_MAILBOX_ATTACHMENT_UPLOAD_TO', + 'mailbox_attachments/%Y/%m/%d/' + ), + 'store_original_message': getattr( + settings, + 'DJANGO_MAILBOX_STORE_ORIGINAL_MESSAGE', + False + ), + 'default_charset': getattr( + settings, + 'DJANGO_MAILBOX_default_charset', + 'iso8859-1', + ) + } def convert_header_to_unicode(header): + default_charset = get_settings()['default_charset'] + def _decode(value, encoding): if isinstance(value, six.text_type): return value if not encoding or encoding == 'unknown-8bit': - encoding = DEFAULT_CHARSET + encoding = default_charset return value.decode(encoding, 'replace') try: @@ -36,9 +83,9 @@ def convert_header_to_unicode(header): logger.exception( 'Errors encountered decoding header %s into encoding %s.', header, - DEFAULT_CHARSET, + default_charset, ) - return unicode(header, DEFAULT_CHARSET, 'replace') + return unicode(header, default_charset, 'replace') def get_body_from_message(message, maintype, subtype): @@ -78,3 +125,12 @@ def get_body_from_message(message, maintype, subtype): body += this_part return body + + +def get_attachment_save_path(instance, filename): + settings = get_settings() + + return os.path.join( + settings['attachment_upload_to'], + filename, + ) diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..68ebb70 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,4 @@ +[pytest] +norecursedirs=env docs lib .eggs +DJANGO_SETTINGS_MODULE=django_mailbox.tests.settings +addopts = --tb=short -rxs diff --git a/test_requirements.txt b/test_requirements.txt new file mode 100644 index 0000000..dfb4f23 --- /dev/null +++ b/test_requirements.txt @@ -0,0 +1,2 @@ +pytest==2.9.1 +pytest-django==2.9.1