diff --git a/.gitignore b/.gitignore index d3e19c6..db89701 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,7 @@ bin/* lib/* src/* +dist/* include/* .Python +*egg* diff --git a/django_mailbox/tests/__init__.py b/django_mailbox/tests/__init__.py index 42d5abe..b83434c 100644 --- a/django_mailbox/tests/__init__.py +++ b/django_mailbox/tests/__init__.py @@ -1,3 +1,4 @@ from .test_mailbox import * from .test_message_flattening import * from .test_process_email import * +from .test_transports import * diff --git a/django_mailbox/tests/base.py b/django_mailbox/tests/base.py index b062797..a559b9e 100644 --- a/django_mailbox/tests/base.py +++ b/django_mailbox/tests/base.py @@ -1,8 +1,9 @@ import email import os.path +import six + from django.test import TestCase -import sys from django_mailbox import models from django_mailbox.models import Mailbox, Message @@ -22,7 +23,7 @@ class EmailMessageTestCase(TestCase): self.mailbox = Mailbox.objects.create() super(EmailMessageTestCase, self).setUp() - def _get_email_object(self, name): + def _get_email_as_text(self, name): with open( os.path.join( os.path.dirname(__file__), @@ -31,10 +32,14 @@ class EmailMessageTestCase(TestCase): ), 'rb' ) as f: - if sys.version_info < (3, 0): - return email.message_from_string(f.read()) - else: - return email.message_from_bytes(f.read()) + return f.read() + + def _get_email_object(self, name): + copy = self._get_email_as_text(name) + if six.PY3: + return email.message_from_bytes(copy) + else: + return email.message_from_string(copy) def _headers_identical(self, left, right, header=None): """ Check if headers are (close enough to) identical. diff --git a/django_mailbox/tests/test_transports.py b/django_mailbox/tests/test_transports.py new file mode 100644 index 0000000..b731748 --- /dev/null +++ b/django_mailbox/tests/test_transports.py @@ -0,0 +1,87 @@ +import mock +import six + +from django_mailbox.tests.base import EmailMessageTestCase +from django_mailbox.transports import ImapTransport, Pop3Transport + + +class TestImapTransport(EmailMessageTestCase): + def setUp(self): + self.arbitrary_hostname = 'one.two.three' + self.arbitrary_port = 100 + self.ssl = False + self.transport = ImapTransport( + self.arbitrary_hostname, + self.arbitrary_port, + self.ssl + ) + self.transport.server = None + super(TestImapTransport, self).setUp() + + def test_get_email_message(self): + with mock.patch.object(self.transport, 'server') as server: + server.search.return_value = ( + 'OK', + [ + 'One', # This is totally arbitrary + ] + ) + server.fetch.return_value = ( + 'OK', + [ + [ + '1 (RFC822 {8190}', # Wat? + self._get_email_as_text('generic_message.eml') + ], + ')', + ] + ) + + actual_messages = list(self.transport.get_message()) + + self.assertEqual(len(actual_messages), 1) + + actual_message = actual_messages[0] + expected_message = self._get_email_object('generic_message.eml') + + self.assertEqual(expected_message, actual_message) + + + +class TestPop3Transport(EmailMessageTestCase): + def setUp(self): + self.arbitrary_hostname = 'one.two.three' + self.arbitrary_port = 100 + self.ssl = False + self.transport = Pop3Transport( + self.arbitrary_hostname, + self.arbitrary_port, + self.ssl + ) + self.transport.server = None + super(TestPop3Transport, self).setUp() + + def test_get_email_message(self): + with mock.patch.object(self.transport, 'server') as server: + # Consider this value arbitrary, the second parameter + # should have one entry per message in the inbox + server.list.return_value = [None, ['some_msg']] + server.retr.return_value = [ + '+OK message follows', + [ + line.encode('ascii') + for line in self._get_email_as_text( + 'generic_message.eml' + ).decode('ascii').split('\n') + ], + 10018, # Some arbitrary size, ideally matching the above + ] + + actual_messages = list(self.transport.get_message()) + + self.assertEqual(len(actual_messages), 1) + + actual_message = actual_messages[0] + expected_message = self._get_email_object('generic_message.eml') + + self.assertEqual(expected_message, actual_message) diff --git a/django_mailbox/transports/base.py b/django_mailbox/transports/base.py new file mode 100644 index 0000000..753df52 --- /dev/null +++ b/django_mailbox/transports/base.py @@ -0,0 +1,19 @@ +import email + +import six + +# Do *not* remove this, we need to use this in subclasses of EmailTransport +if six.PY3: + from email.errors import MessageParseError +else: + from email.Errors import MessageParseError + + +class EmailTransport(object): + def get_email_from_bytes(self, contents): + if six.PY3: + message = email.message_from_bytes(contents) + else: + message = email.message_from_string(contents) + + return message diff --git a/django_mailbox/transports/generic.py b/django_mailbox/transports/generic.py index 059d078..5a9562d 100644 --- a/django_mailbox/transports/generic.py +++ b/django_mailbox/transports/generic.py @@ -1,4 +1,7 @@ -class GenericFileMailbox(object): +from .base import EmailTransport + + +class GenericFileMailbox(EmailTransport): _variant = None _path = None diff --git a/django_mailbox/transports/imap.py b/django_mailbox/transports/imap.py index 2ba3db2..ff3074c 100644 --- a/django_mailbox/transports/imap.py +++ b/django_mailbox/transports/imap.py @@ -1,8 +1,9 @@ -import email from imaplib import IMAP4, IMAP4_SSL +from .base import EmailTransport, MessageParseError -class ImapTransport(object): + +class ImapTransport(EmailTransport): def __init__(self, hostname, port=None, ssl=False): self.hostname = hostname self.port = port @@ -29,9 +30,9 @@ class ImapTransport(object): for key in inbox[0].split(): try: typ, msg_contents = self.server.fetch(key, '(RFC822)') - message = email.message_from_string(msg_contents[0][1]) + message = self.get_email_from_bytes(msg_contents[0][1]) yield message - except email.Errors.MessageParseError: + except MessageParseError: continue self.server.store(key, "+FLAGS", "\\Deleted") self.server.expunge() diff --git a/django_mailbox/transports/pop3.py b/django_mailbox/transports/pop3.py index 652fbe7..8520557 100644 --- a/django_mailbox/transports/pop3.py +++ b/django_mailbox/transports/pop3.py @@ -1,8 +1,11 @@ -import email +import six + from poplib import POP3, POP3_SSL +from .base import EmailTransport, MessageParseError -class Pop3Transport(object): + +class Pop3Transport(EmailTransport): def __init__(self, hostname, port=None, ssl=False): self.hostname = hostname self.port = port @@ -20,14 +23,21 @@ class Pop3Transport(object): self.server.user(username) self.server.pass_(password) + def get_message_body(self, message_lines): + if six.PY3: + return six.binary_type('\r\n', 'ascii').join(message_lines) + return '\r\n'.join(message_lines) + def get_message(self): message_count = len(self.server.list()[1]) for i in range(message_count): try: - msg_contents = "\r\n".join(self.server.retr(i + 1)[1]) - message = email.message_from_string(msg_contents) + msg_contents = self.get_message_body( + self.server.retr(i + 1)[1] + ) + message = self.get_email_from_bytes(msg_contents) yield message - except email.Errors.MessageParseError: + except MessageParseError: continue self.server.dele(i + 1) self.server.quit() diff --git a/setup.py b/setup.py index 12aa67f..836014e 100755 --- a/setup.py +++ b/setup.py @@ -2,6 +2,7 @@ from setuptools import setup tests_require = [ 'django', + 'mock', ] setup(