diff --git a/django_mailbox/admin.py b/django_mailbox/admin.py index db39c26..896bf6b 100644 --- a/django_mailbox/admin.py +++ b/django_mailbox/admin.py @@ -70,9 +70,6 @@ class MessageAdmin(admin.ModelAdmin): raw_id_fields = ( 'in_reply_to', ) - exclude = ( - 'body', - ) readonly_fields = ( 'text', ) diff --git a/django_mailbox/migrations/0015_auto__add_field_messageattachment_headers.py b/django_mailbox/migrations/0015_auto__add_field_messageattachment_headers.py new file mode 100644 index 0000000..7531244 --- /dev/null +++ b/django_mailbox/migrations/0015_auto__add_field_messageattachment_headers.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + + +class Migration(SchemaMigration): + + def forwards(self, orm): + # Removing M2M table for field attachments on 'Message' + db.delete_table('django_mailbox_message_attachments') + + # Adding field 'MessageAttachment.headers' + db.add_column(u'django_mailbox_messageattachment', 'headers', + self.gf('django.db.models.fields.TextField')(null=True, blank=True), + keep_default=False) + + + def backwards(self, orm): + # Adding M2M table for field attachments on 'Message' + db.create_table(u'django_mailbox_message_attachments', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('message', models.ForeignKey(orm[u'django_mailbox.message'], null=False)), + ('messageattachment', models.ForeignKey(orm[u'django_mailbox.messageattachment'], null=False)) + )) + db.create_unique(u'django_mailbox_message_attachments', ['message_id', 'messageattachment_id']) + + # Deleting field 'MessageAttachment.headers' + db.delete_column(u'django_mailbox_messageattachment', 'headers') + + + models = { + u'django_mailbox.mailbox': { + 'Meta': {'object_name': 'Mailbox'}, + 'active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'from_email': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'uri': ('django.db.models.fields.CharField', [], {'default': 'None', 'max_length': '255', 'null': 'True', 'blank': 'True'}) + }, + u'django_mailbox.message': { + 'Meta': {'object_name': 'Message'}, + 'body': ('django.db.models.fields.TextField', [], {}), + 'from_header': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'in_reply_to': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'replies'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}), + 'mailbox': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'messages'", 'to': u"orm['django_mailbox.Mailbox']"}), + 'message_id': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'outgoing': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'processed': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'read': ('django.db.models.fields.DateTimeField', [], {'default': 'None', 'null': 'True', 'blank': 'True'}), + 'subject': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'to_header': ('django.db.models.fields.TextField', [], {}) + }, + u'django_mailbox.messageattachment': { + 'Meta': {'object_name': 'MessageAttachment'}, + 'document': ('django.db.models.fields.files.FileField', [], {'max_length': '100'}), + 'headers': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'message': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'attachments'", 'null': 'True', 'to': u"orm['django_mailbox.Message']"}) + } + } + + complete_apps = ['django_mailbox'] \ No newline at end of file diff --git a/django_mailbox/models.py b/django_mailbox/models.py index 94dddd9..315048b 100755 --- a/django_mailbox/models.py +++ b/django_mailbox/models.py @@ -1,9 +1,13 @@ import email from email.message import Message as EmailMessage from email.utils import formatdate, parseaddr +from email.encoders import encode_base64 import urllib -import os +import mimetypes +import os.path +from quopri import encode as encode_quopri import sys +import uuid try: import urllib.parse as urlparse @@ -13,12 +17,12 @@ except ImportError: from django.conf import settings from django.core.mail.message import make_msgid from django.core.files import File -from django.core.files.temp import NamedTemporaryFile from django.db import models from django_mailbox.transports import Pop3Transport, ImapTransport,\ MaildirTransport, MboxTransport, BabylTransport, MHTransport, \ MMDFTransport from django_mailbox.signals import message_received +import six SKIPPED_EXTENSIONS = getattr( settings, @@ -38,11 +42,24 @@ ALLOWED_MIMETYPES = getattr( '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' +) class ActiveMailboxManager(models.Manager): @@ -177,26 +194,57 @@ class Mailbox(models.Model): msg.save() return msg - def _filter_message_body(self, message): - if not message.is_multipart() or not STRIP_UNALLOWED_MIMETYPES: - return message - stripped_content = {} + def _get_dehydrated_message(self, msg, record): new = EmailMessage() - for header, value in message.items(): - new[header] = value - for part in message.walk(): - content_type = part.get_content_type() - if not content_type in ALLOWED_MIMETYPES: - if content_type not in stripped_content: - stripped_content[content_type] = 0 - stripped_content[content_type] = ( - stripped_content[content_type] + 1 + if msg.is_multipart(): + for header, value in msg.items(): + new[header] = value + for part in msg.get_payload(): + new.attach( + self._get_dehydrated_message(part, record) ) - continue - new.attach(part) - new[ALTERED_MESSAGE_HEADER] = 'Stripped ' + ', '.join( - ['%s*%s' % (key, value) for key, value in stripped_content.items()] - ) + elif ( + STRIP_UNALLOWED_MIMETYPES + and not msg.get_content_type() in 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] = ( + 'Stripped; Content type %s not allowed' % ( + msg.get_content_type() + ) + ) + new.set_payload('') + elif msg.get_content_type() not in TEXT_STORED_MIMETYPES: + filename = msg.get_filename() + extension = '.bin' + if not filename: + extension = mimetypes.guess_extension(msg.get_content_type()) + else: + _, extension = os.path.splitext(filename) + + attachment = MessageAttachment() + attachment.document.save( + uuid.uuid4().hex + extension, + File(six.BytesIO(msg.get_payload(decode=True))) + ) + attachment.message = record + for key, value in msg.items(): + attachment[key] = value + attachment.save() + + placeholder = EmailMessage() + placeholder[ATTACHMENT_INTERPOLATION_HEADER] = str(attachment.pk) + new = placeholder + else: + for header, value in msg.items(): + new[header] = value + new.set_payload( + msg.get_payload() + ) return new def _process_message(self, message): @@ -206,7 +254,8 @@ class Mailbox(models.Model): msg.message_id = message['message-id'][0:255] msg.from_header = message['from'] msg.to_header = message['to'] - message = self._filter_message_body(message) + msg.save() + message = self._get_dehydrated_message(message, msg) msg.body = message.as_string() if message['in-reply-to']: try: @@ -216,39 +265,6 @@ class Mailbox(models.Model): except IndexError: pass msg.save() - if message.is_multipart(): - for part in message.walk(): - if part.get_content_maintype() == 'multipart': - continue - if part.get('Content-Disposition') is None: - continue - filename = part.get_filename() - if not filename: - continue - filename_basename, filename_extension = os.path.splitext( - filename - ) - buffer_space = 40 - if len(filename) > 100 - buffer_space: - # Ensure that there're at least a few chars available - # afterward for duplication things like _1, _2 ... _99 and - # the FileField's upload_to path. - filename_basename = filename_basename[ - 0:100-len(filename_extension)-buffer_space - ] - filename = filename_basename + filename_extension - if filename_extension in SKIPPED_EXTENSIONS: - continue - data = part.get_payload(decode=True) - if not data: - continue - temp_file = NamedTemporaryFile(delete=True) - temp_file.write(data) - temp_file.flush() - attachment = MessageAttachment() - attachment.document.save(filename, File(temp_file)) - attachment.message = msg - attachment.save() return msg def get_new_mail(self): @@ -385,18 +401,72 @@ class Message(models.Model): def get_text_body(self): def get_body_from_message(message): body = '' - if message.is_multipart(): - for part in message.get_payload(): - mime_type = part.get('content-type', '').split(';')[0] - if mime_type == 'text/plain': - body = body + get_body_from_message(part) - else: - body = body + message.get_payload() + for part in message.walk(): + if ( + part.get_content_maintype() == 'text' + and part.get_content_subtype() == 'plain' + ): + body = body + part.get_payload() return body return get_body_from_message( self.get_email_object() - ).replace('=\n', '').rstrip('\n') + ).replace('=\n', '').strip() + + def _rehydrate(self, msg): + new = EmailMessage() + if msg.is_multipart(): + for header, value in msg.items(): + new[header] = value + for part in msg.get_payload(): + new.attach( + self._rehydrate(part) + ) + elif ATTACHMENT_INTERPOLATION_HEADER in msg.keys(): + try: + attachment = MessageAttachment.objects.get( + pk=msg[ATTACHMENT_INTERPOLATION_HEADER] + ) + for header, value in attachment.items(): + new[header] = value + encoding = new['Content-Transfer-Encoding'] + if encoding and encoding.lower() == 'quoted-printable': + # Cannot use `email.encoders.encode_quopri due to + # bug 14360: http://bugs.python.org/issue14360 + output = six.BytesIO() + encode_quopri( + six.BytesIO( + attachment.document.read() + ), + output, + quotetabs=True, + header=False, + ) + new.set_payload( + output.getvalue().decode().replace(' ', '=20') + ) + del new['Content-Transfer-Encoding'] + new['Content-Transfer-Encoding'] = 'quoted-printable' + else: + new.set_payload( + attachment.document.read() + ) + del new['Content-Transfer-Encoding'] + encode_base64(new) + except MessageAttachment.DoesNotExist: + new[ALTERED_MESSAGE_HEADER] = ( + 'Missing; Attachment %s not found' % ( + msg[ATTACHMENT_INTERPOLATION_HEADER] + ) + ) + new.set_payload('') + else: + for header, value in msg.items(): + new[header] = value + new.set_payload( + msg.get_payload() + ) + return new def get_email_object(self): """ Returns an `email.message.Message` instance for this message. @@ -413,9 +483,12 @@ class Message(models.Model): encoded bytes is probably the safest answer. """ + body = self.body if sys.version_info < (2, 7): - return email.message_from_string(self.body.encode('utf-8')) - return email.message_from_string(self.body) + body = body.encode('utf-8') + return self._rehydrate( + email.message_from_string(body) + ) def delete(self, *args, **kwargs): for attachment in self.attachments.all(): @@ -434,11 +507,42 @@ class MessageAttachment(models.Model): null=True, blank=True, ) + headers = models.TextField(null=True, blank=True) document = models.FileField(upload_to='mailbox_attachments/%Y/%m/%d/') def delete(self, *args, **kwargs): self.document.delete() return super(MessageAttachment, self).delete(*args, **kwargs) + def _get_rehydrated_headers(self): + headers = self.headers + if headers is None: + return EmailMessage() + if sys.version_info < (2, 7): + headers = headers.encode('utf-8') + return email.message_from_string(headers) + + def _set_dehydrated_headers(self, email_object): + self.headers = email_object.as_string() + + def __delitem__(self, name): + rehydrated = self._get_rehydrated_headers() + del rehydrated[name] + self._set_dehydrated_headers(rehydrated) + + def __setitem__(self, name, value): + rehydrated = self._get_rehydrated_headers() + rehydrated[name] = value + self._set_dehydrated_headers(rehydrated) + + def get_filename(self): + return self._get_rehydrated_headers().get_filename() + + def items(self): + return self._get_rehydrated_headers().items() + + def __getitem__(self, name): + return self._get_rehydrated_headers()[name] + def __unicode__(self): return self.document.url diff --git a/django_mailbox/tests/__init__.py b/django_mailbox/tests/__init__.py index abf35d2..42a69b5 100644 --- a/django_mailbox/tests/__init__.py +++ b/django_mailbox/tests/__init__.py @@ -4,7 +4,7 @@ import os.path from django.test import TestCase import six -import django_mailbox +from django_mailbox import models from django_mailbox.models import Mailbox, Message @@ -16,22 +16,96 @@ class TestMailbox(TestCase): expected_protocol = 'alpha' actual_protocol = mailbox._protocol_info.scheme - self.assertEquals( + self.assertEqual( expected_protocol, actual_protocol, ) 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 + + self.mailbox = Mailbox.objects.create() + super(EmailMessageTestCase, self).setUp() + def _get_email_object(self, name): with open(os.path.join(os.path.dirname(__file__), name), 'r') as f: return email.message_from_string( f.read() ) + def compare_email_objects(self, left, right): + # Compare headers + for key, value in left.items(): + if not right[key]: + raise AssertionError("Extra header '%s'" % key) + if right[key].replace('\n\t', ' ') != value.replace('\n\t', ' '): + raise AssertionError( + "Header '%s' unequal:\n%s\n%s" % ( + key, + repr(value), + repr(right[key]), + ) + ) + for key, value in right.items(): + if not left[key]: + raise AssertionError("Extra header '%s'" % key) + if left[key].replace('\n\t', ' ') != value.replace('\n\t', ' '): + raise AssertionError( + "Header '%s' unequal:\n%s\n%s" % ( + key, + repr(value), + repr(right[key]), + ) + ) + if left.is_multipart() != right.is_multipart(): + self._raise_mismatched(left, right) + if left.is_multipart(): + left_payloads = left.get_payload() + right_payloads = right.get_payload() + if len(left_payloads) != len(right_payloads): + self._raise_mismatched(left, right) + for n in range(len(left_payloads)): + self.compare_email_objects( + left_payloads[n], + right_payloads[n] + ) + else: + if left.get_payload() is None or right.get_payload() is None: + if left.get_payload() is None: + if right.get_payload is not None: + self._raise_mismatched(left, right) + if right.get_payload() is None: + if left.get_payload is not None: + self._raise_mismatched(left, right) + elif left.get_payload().strip() != right.get_payload().strip(): + self._raise_mismatched(left, right) + + def _raise_mismatched(self, left, right): + raise AssertionError( + "Message payloads do not match:\n%s\n%s" % ( + left.as_string(), + right.as_string() + ) + ) + + def assertEqual(self, left, right): + if not isinstance(left, email.message.Message): + return super(EmailMessageTestCase, self).assertEqual(left, right) + return self.compare_email_objects(left, right) + def tearDown(self): for message in Message.objects.all(): message.delete() + models.ALLOWED_MIMETYPES = self._ALLOWED_MIMETYPES + models.STRIP_UNALLOWED_MIMETYPES = self._STRIP_UNALLOWED_MIMETYPES + models.TEXT_STORED_MIMETYPES = self._TEXT_STORED_MIMETYPES + + self.mailbox.delete() + super(EmailMessageTestCase, self).tearDown() class TestProcessEmail(EmailMessageTestCase): @@ -48,7 +122,10 @@ class TestProcessEmail(EmailMessageTestCase): self.assertEqual(msg.subject, 'Message Without Attachment') self.assertEqual( msg.message_id, - '' + ( + '' + ) ) self.assertEqual( msg.from_header, @@ -68,14 +145,14 @@ class TestProcessEmail(EmailMessageTestCase): expected_count = 1 actual_count = msg.attachments.count() - self.assertEquals( + self.assertEqual( expected_count, actual_count, ) attachment = msg.attachments.all()[0] - self.assertEquals( - os.path.basename(attachment.document.name), + self.assertEqual( + attachment.get_filename(), 'heart.png', ) @@ -88,59 +165,12 @@ class TestProcessEmail(EmailMessageTestCase): expected_results = 'Hello there!' actual_results = msg.get_text_body().strip() - self.assertEquals( + self.assertEqual( expected_results, actual_results, ) -class TestFilterMessageBody(EmailMessageTestCase): - def setUp(self): - django_mailbox.models.STRIP_UNALLOWED_MIMETYPES = True - super(TestFilterMessageBody, self).setUp() - - def tearDown(self): - django_mailbox.models.STRIP_UNALLOWED_MIMETYPES = False - super(TestFilterMessageBody, self).tearDown() - - def test_filter_message_does_not_filter_message_if_disabled(self): - django_mailbox.models.STRIP_UNALLOWED_MIMETYPES = False - message = self._get_email_object('message_with_attachment.eml') - mailbox = Mailbox.objects.create() - - self.assertEquals( - message.as_string(), - mailbox._filter_message_body(message).as_string() - ) - - def test_filter_message_removes_unknown_content_if_disabled(self): - # The below is the _same_ as message_with_attachment.eml, but missing - # its attached png image, and adding the expected message altered header. - message_without_non_plaintext = ( - "MIME-Version: 1.0\n" - "Received: by 10.221.0.211 with HTTP; Sun, 20 Jan 2013 12:07:07 -0800 (PST)\n" - "X-Originating-IP: [24.22.122.177]\n" - "Date: Sun, 20 Jan 2013 12:07:07 -0800\n" - "Delivered-To: test@adamcoddington.net\n" - "Message-ID: \n" - "Subject: Message With Attachment\n" - "From: Adam Coddington \n" - "To: Adam Coddington \n" - "Content-Type: multipart/mixed; boundary=047d7b33dd729737fe04d3bde348\n" - "X-Django-Mailbox-Altered-Message: Stripped image/png*1, multipart/mixed*1\n" - "\n--047d7b33dd729737fe04d3bde348\n" - "Content-Type: text/plain; charset=UTF-8\n\n" - "This message has an attachment.\n\n--047d7b33dd729737fe04d3bde348--" - ) - message = self._get_email_object('message_with_attachment.eml') - mailbox = Mailbox.objects.create() - - self.assertEquals( - message_without_non_plaintext, - mailbox._filter_message_body(message).as_string() - ) - - class TestGetMessage(EmailMessageTestCase): def test_get_text_body_properly_recomposes_line_continuations(self): message = Message() @@ -156,12 +186,93 @@ class TestGetMessage(EmailMessageTestCase): 'but a man stopped to help us and gave us his pump.' ) - self.assertEquals( + self.assertEqual( actual_text, expected_text ) +class TestMessageFlattening(EmailMessageTestCase): + def test_quopri_message_is_properly_rehydrated(self): + incoming_email_object = self._get_email_object( + 'message_with_many_multiparts.eml', + ) + # Note: this is identical to the above, but it appears that + # while reading-in an e-mail message, we do alter it slightly + expected_email_object = self._get_email_object( + 'message_with_many_multiparts.eml', + ) + models.TEXT_STORED_MIMETYPES = ['text/plain'] + + msg = self.mailbox.process_incoming_message(incoming_email_object) + + actual_email_object = msg.get_email_object() + + self.assertEqual( + actual_email_object, + expected_email_object, + ) + + def test_base64_message_is_properly_rehydrated(self): + incoming_email_object = self._get_email_object( + 'message_with_attachment.eml', + ) + # Note: this is identical to the above, but it appears that + # while reading-in an e-mail message, we do alter it slightly + expected_email_object = self._get_email_object( + 'message_with_attachment.eml', + ) + + msg = self.mailbox.process_incoming_message(incoming_email_object) + + actual_email_object = msg.get_email_object() + + self.assertEqual( + actual_email_object, + expected_email_object, + ) + + def test_message_handles_rehydration_problems(self): + incoming_email_object = self._get_email_object( + 'message_with_defective_attachment_association.eml', + ) + expected_email_object = self._get_email_object( + 'message_with_defective_attachment_association_result.eml', + ) + # Note: this is identical to the above, but it appears that + # while reading-in an e-mail message, we do alter it slightly + message = Message() + message.body = incoming_email_object.as_string() + + msg = self.mailbox.process_incoming_message(incoming_email_object) + + actual_email_object = msg.get_email_object() + + self.assertEqual( + actual_email_object, + expected_email_object, + ) + + def test_message_content_type_stripping(self): + incoming_email_object = self._get_email_object( + 'message_with_many_multiparts.eml', + ) + expected_email_object = self._get_email_object( + 'message_with_many_multiparts_stripped_html.eml', + ) + models.STRIP_UNALLOWED_MIMETYPES = True + models.ALLOWED_MIMETYPES = ['text/plain'] + + msg = self.mailbox.process_incoming_message(incoming_email_object) + + actual_email_object = msg.get_email_object() + + self.assertEqual( + actual_email_object, + expected_email_object, + ) + + class TestMessageGetEmailObject(TestCase): def test_get_body_properly_handles_unicode_body(self): with open( @@ -178,7 +289,7 @@ class TestMessageGetEmailObject(TestCase): expected_body = unicode_body actual_body = message.get_email_object().as_string() - self.assertEquals( + self.assertEqual( expected_body, actual_body ) diff --git a/django_mailbox/tests/message_with_attachment.eml b/django_mailbox/tests/message_with_attachment.eml index d678018..34e7093 100644 --- a/django_mailbox/tests/message_with_attachment.eml +++ b/django_mailbox/tests/message_with_attachment.eml @@ -25,4 +25,5 @@ ICESkFAJkRAJSIgEpEQCEqYfu6QUkn7sCcyDGQiSACKSKCAkGwBJwhDwZQNMEiYAIBdQvk7rfaHf AO8NBJwCxTGhtFgTHVNaNaJeWFu44AXEHzKCktc7zZ0vss+bMoHSiM2b9mQoX1eZCgGqnWskY3gi XXAAxb8BqFiUgBNY7k49Tu/kV7UKPsefrjEOT9GmghYzrk9V03pjDGYKj3d0c06dKZkpTboRaD9o B+1m2m81d2Az948xzgdjLaFe95e83AAAAABJRU5ErkJggg== + --047d7b33dd729737fe04d3bde348-- diff --git a/django_mailbox/tests/message_with_defective_attachment_association.eml b/django_mailbox/tests/message_with_defective_attachment_association.eml new file mode 100644 index 0000000..4e1d9d4 --- /dev/null +++ b/django_mailbox/tests/message_with_defective_attachment_association.eml @@ -0,0 +1,28 @@ +X-sender: +X-receiver: +From: "Senders Name" +To: "Recipient Name" +Message-ID: <5bec11c119194c14999e592feb46e3cf@sendersdomain.com> +Date: Sat, 24 Sep 2005 15:06:49 -0400 +Subject: Sample Multi-Part +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="----=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D" + +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D +Content-type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Sample Text Content +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D +Content-Type: multipart/alternative; boundary="----=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C" + +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Multipart inside of multipart! +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C +X-Django-Mailbox-Interpolate-Attachment: 9013 + +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C-- +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D-- diff --git a/django_mailbox/tests/message_with_defective_attachment_association_result.eml b/django_mailbox/tests/message_with_defective_attachment_association_result.eml new file mode 100644 index 0000000..b63b217 --- /dev/null +++ b/django_mailbox/tests/message_with_defective_attachment_association_result.eml @@ -0,0 +1,28 @@ +X-sender: +X-receiver: +From: "Senders Name" +To: "Recipient Name" +Message-ID: <5bec11c119194c14999e592feb46e3cf@sendersdomain.com> +Date: Sat, 24 Sep 2005 15:06:49 -0400 +Subject: Sample Multi-Part +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="----=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D" + +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D +Content-type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Sample Text Content +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D +Content-Type: multipart/alternative; boundary="----=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C" + +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Multipart inside of multipart! +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C +X-Django-Mailbox-Altered-Message: Missing; Attachment 9013 not found + +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C-- +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D-- diff --git a/django_mailbox/tests/message_with_many_multiparts.eml b/django_mailbox/tests/message_with_many_multiparts.eml new file mode 100644 index 0000000..14bae19 --- /dev/null +++ b/django_mailbox/tests/message_with_many_multiparts.eml @@ -0,0 +1,33 @@ +X-sender: +X-receiver: +From: "Senders Name" +To: "Recipient Name" +Message-ID: <5bec11c119194c14999e592feb46e3cf@sendersdomain.com> +Date: Sat, 24 Sep 2005 15:06:49 -0400 +Subject: Sample Multi-Part +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="----=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D" + +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D +Content-type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Sample Text Content +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D +Content-Type: multipart/alternative; boundary="----=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C" + +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Multipart inside of multipart! +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C +Content-type: text/html; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + + +

Hello!

+ + +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C-- +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D-- diff --git a/django_mailbox/tests/message_with_many_multiparts_stripped_html.eml b/django_mailbox/tests/message_with_many_multiparts_stripped_html.eml new file mode 100644 index 0000000..5344dfc --- /dev/null +++ b/django_mailbox/tests/message_with_many_multiparts_stripped_html.eml @@ -0,0 +1,29 @@ +X-sender: +X-receiver: +From: "Senders Name" +To: "Recipient Name" +Message-ID: <5bec11c119194c14999e592feb46e3cf@sendersdomain.com> +Date: Sat, 24 Sep 2005 15:06:49 -0400 +Subject: Sample Multi-Part +MIME-Version: 1.0 +Content-Type: multipart/alternative; boundary="----=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D" + +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D +Content-type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Sample Text Content +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D +Content-Type: multipart/alternative; boundary="----=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C" + +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C +Content-Type: text/plain; charset=iso-8859-1 +Content-Transfer-Encoding: quoted-printable + +Multipart inside of multipart! +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C +Content-type: text/html; charset=iso-8859-1 +X-Django-Mailbox-Altered-Message: Stripped; Content type text/html not allowed + +------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C-- +------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D-- diff --git a/setup.py b/setup.py index de91ded..83026e5 100755 --- a/setup.py +++ b/setup.py @@ -1,14 +1,17 @@ from setuptools import setup -tests_require=[ +tests_require = [ 'django', ] setup( name='django-mailbox', - version='2.0', + version='2.1', url='http://bitbucket.org/latestrevision/django-mailbox/', - description='Import mail from POP3, IMAP, local mailboxes or directly from Postfix or Exim4 into your Django application automatically.', + description=( + 'Import mail from POP3, IMAP, local mailboxes or directly from ' + 'Postfix or Exim4 into your Django application automatically.' + ), author='Adam Coddington', author_email='me@adamcoddington.net', tests_require=tests_require,