1
0
Fork 1
mirror of https://github.com/coddingtonbear/django-mailbox.git synced 2026-07-09 22:38:19 +02:00

Major refactor of message attachment handling; no longer stores verbatim

message copy in database.

Bumping version number to 2.1

* Walks through incoming message, write attachments to disk as they are found,
  and alters message body removing actual attachment body, and adding
  header 'X-Django-Mailbox-Interpolate-Attachment' referencing the ID of the
  stored attachment.
* When calling ``get_email_object``, will walk through stored message, and
  're-hydrate' the message by finding said headers, searching for the
  appropriate record in the MessageAttachments table, and rebuild the message
  object as closely as possible.

Minor fixes:

* Properly collect text/plain content from any part of the message; previously
  would only check the message's first level of payloads, now walks through
  all payloads, building a string of all text/plain content.
* Remove use of deprecated `assertEquals` message.
This commit is contained in:
Adam Coddington 2013-06-23 20:27:50 -07:00
parent 548cc66c52
commit 5f289289ce
10 changed files with 525 additions and 126 deletions

View file

@ -70,9 +70,6 @@ class MessageAdmin(admin.ModelAdmin):
raw_id_fields = ( raw_id_fields = (
'in_reply_to', 'in_reply_to',
) )
exclude = (
'body',
)
readonly_fields = ( readonly_fields = (
'text', 'text',
) )

View file

@ -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']

View file

@ -1,9 +1,13 @@
import email import email
from email.message import Message as EmailMessage from email.message import Message as EmailMessage
from email.utils import formatdate, parseaddr from email.utils import formatdate, parseaddr
from email.encoders import encode_base64
import urllib import urllib
import os import mimetypes
import os.path
from quopri import encode as encode_quopri
import sys import sys
import uuid
try: try:
import urllib.parse as urlparse import urllib.parse as urlparse
@ -13,12 +17,12 @@ except ImportError:
from django.conf import settings from django.conf import settings
from django.core.mail.message import make_msgid from django.core.mail.message import make_msgid
from django.core.files import File from django.core.files import File
from django.core.files.temp import NamedTemporaryFile
from django.db import models from django.db import models
from django_mailbox.transports import Pop3Transport, ImapTransport,\ from django_mailbox.transports import Pop3Transport, ImapTransport,\
MaildirTransport, MboxTransport, BabylTransport, MHTransport, \ MaildirTransport, MboxTransport, BabylTransport, MHTransport, \
MMDFTransport MMDFTransport
from django_mailbox.signals import message_received from django_mailbox.signals import message_received
import six
SKIPPED_EXTENSIONS = getattr( SKIPPED_EXTENSIONS = getattr(
settings, settings,
@ -38,11 +42,24 @@ ALLOWED_MIMETYPES = getattr(
'text/html' 'text/html'
] ]
) )
TEXT_STORED_MIMETYPES = getattr(
settings,
'DJANGO_MAILBOX_TEXT_STORED_MIMETYPES',
[
'text/plain',
'text/html'
]
)
ALTERED_MESSAGE_HEADER = getattr( ALTERED_MESSAGE_HEADER = getattr(
settings, settings,
'DJANGO_MAILBOX_ALTERED_MESSAGE_HEADER', 'DJANGO_MAILBOX_ALTERED_MESSAGE_HEADER',
'X-Django-Mailbox-Altered-Message' 'X-Django-Mailbox-Altered-Message'
) )
ATTACHMENT_INTERPOLATION_HEADER = getattr(
settings,
'DJANGO_MAILBOX_ATTACHMENT_INTERPOLATION_HEADER',
'X-Django-Mailbox-Interpolate-Attachment'
)
class ActiveMailboxManager(models.Manager): class ActiveMailboxManager(models.Manager):
@ -177,26 +194,57 @@ class Mailbox(models.Model):
msg.save() msg.save()
return msg return msg
def _filter_message_body(self, message): def _get_dehydrated_message(self, msg, record):
if not message.is_multipart() or not STRIP_UNALLOWED_MIMETYPES:
return message
stripped_content = {}
new = EmailMessage() new = EmailMessage()
for header, value in message.items(): if msg.is_multipart():
new[header] = value for header, value in msg.items():
for part in message.walk(): new[header] = value
content_type = part.get_content_type() for part in msg.get_payload():
if not content_type in ALLOWED_MIMETYPES: new.attach(
if content_type not in stripped_content: self._get_dehydrated_message(part, record)
stripped_content[content_type] = 0
stripped_content[content_type] = (
stripped_content[content_type] + 1
) )
continue elif (
new.attach(part) STRIP_UNALLOWED_MIMETYPES
new[ALTERED_MESSAGE_HEADER] = 'Stripped ' + ', '.join( and not msg.get_content_type() in ALLOWED_MIMETYPES
['%s*%s' % (key, value) for key, value in stripped_content.items()] ):
) 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 return new
def _process_message(self, message): def _process_message(self, message):
@ -206,7 +254,8 @@ class Mailbox(models.Model):
msg.message_id = message['message-id'][0:255] msg.message_id = message['message-id'][0:255]
msg.from_header = message['from'] msg.from_header = message['from']
msg.to_header = message['to'] 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() msg.body = message.as_string()
if message['in-reply-to']: if message['in-reply-to']:
try: try:
@ -216,39 +265,6 @@ class Mailbox(models.Model):
except IndexError: except IndexError:
pass pass
msg.save() 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 return msg
def get_new_mail(self): def get_new_mail(self):
@ -385,18 +401,72 @@ class Message(models.Model):
def get_text_body(self): def get_text_body(self):
def get_body_from_message(message): def get_body_from_message(message):
body = '' body = ''
if message.is_multipart(): for part in message.walk():
for part in message.get_payload(): if (
mime_type = part.get('content-type', '').split(';')[0] part.get_content_maintype() == 'text'
if mime_type == 'text/plain': and part.get_content_subtype() == 'plain'
body = body + get_body_from_message(part) ):
else: body = body + part.get_payload()
body = body + message.get_payload()
return body return body
return get_body_from_message( return get_body_from_message(
self.get_email_object() 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): def get_email_object(self):
""" Returns an `email.message.Message` instance for this message. """ Returns an `email.message.Message` instance for this message.
@ -413,9 +483,12 @@ class Message(models.Model):
encoded bytes is probably the safest answer. encoded bytes is probably the safest answer.
""" """
body = self.body
if sys.version_info < (2, 7): if sys.version_info < (2, 7):
return email.message_from_string(self.body.encode('utf-8')) body = body.encode('utf-8')
return email.message_from_string(self.body) return self._rehydrate(
email.message_from_string(body)
)
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
for attachment in self.attachments.all(): for attachment in self.attachments.all():
@ -434,11 +507,42 @@ class MessageAttachment(models.Model):
null=True, null=True,
blank=True, blank=True,
) )
headers = models.TextField(null=True, blank=True)
document = models.FileField(upload_to='mailbox_attachments/%Y/%m/%d/') document = models.FileField(upload_to='mailbox_attachments/%Y/%m/%d/')
def delete(self, *args, **kwargs): def delete(self, *args, **kwargs):
self.document.delete() self.document.delete()
return super(MessageAttachment, self).delete(*args, **kwargs) 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): def __unicode__(self):
return self.document.url return self.document.url

View file

@ -4,7 +4,7 @@ import os.path
from django.test import TestCase from django.test import TestCase
import six import six
import django_mailbox from django_mailbox import models
from django_mailbox.models import Mailbox, Message from django_mailbox.models import Mailbox, Message
@ -16,22 +16,96 @@ class TestMailbox(TestCase):
expected_protocol = 'alpha' expected_protocol = 'alpha'
actual_protocol = mailbox._protocol_info.scheme actual_protocol = mailbox._protocol_info.scheme
self.assertEquals( self.assertEqual(
expected_protocol, expected_protocol,
actual_protocol, actual_protocol,
) )
class EmailMessageTestCase(TestCase): 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): def _get_email_object(self, name):
with open(os.path.join(os.path.dirname(__file__), name), 'r') as f: with open(os.path.join(os.path.dirname(__file__), name), 'r') as f:
return email.message_from_string( return email.message_from_string(
f.read() 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): def tearDown(self):
for message in Message.objects.all(): for message in Message.objects.all():
message.delete() 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): class TestProcessEmail(EmailMessageTestCase):
@ -48,7 +122,10 @@ class TestProcessEmail(EmailMessageTestCase):
self.assertEqual(msg.subject, 'Message Without Attachment') self.assertEqual(msg.subject, 'Message Without Attachment')
self.assertEqual( self.assertEqual(
msg.message_id, msg.message_id,
'<CAMdmm+hGH8Dgn-_0xnXJCd=PhyNAiouOYm5zFP0z-foqTO60zA@mail.gmail.com>' (
'<CAMdmm+hGH8Dgn-_0xnXJCd=PhyNAiouOYm5zFP0z'
'-foqTO60zA@mail.gmail.com>'
)
) )
self.assertEqual( self.assertEqual(
msg.from_header, msg.from_header,
@ -68,14 +145,14 @@ class TestProcessEmail(EmailMessageTestCase):
expected_count = 1 expected_count = 1
actual_count = msg.attachments.count() actual_count = msg.attachments.count()
self.assertEquals( self.assertEqual(
expected_count, expected_count,
actual_count, actual_count,
) )
attachment = msg.attachments.all()[0] attachment = msg.attachments.all()[0]
self.assertEquals( self.assertEqual(
os.path.basename(attachment.document.name), attachment.get_filename(),
'heart.png', 'heart.png',
) )
@ -88,59 +165,12 @@ class TestProcessEmail(EmailMessageTestCase):
expected_results = 'Hello there!' expected_results = 'Hello there!'
actual_results = msg.get_text_body().strip() actual_results = msg.get_text_body().strip()
self.assertEquals( self.assertEqual(
expected_results, expected_results,
actual_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: <CAMdmm+jYCgrxrekAxszmDnBjAytcBym-Ec+uM-+HEtzuKy=M_g@mail.gmail.com>\n"
"Subject: Message With Attachment\n"
"From: Adam Coddington <test@adamcoddington.net>\n"
"To: Adam Coddington <test@adamcoddington.net>\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): class TestGetMessage(EmailMessageTestCase):
def test_get_text_body_properly_recomposes_line_continuations(self): def test_get_text_body_properly_recomposes_line_continuations(self):
message = Message() message = Message()
@ -156,12 +186,93 @@ class TestGetMessage(EmailMessageTestCase):
'but a man stopped to help us and gave us his pump.' 'but a man stopped to help us and gave us his pump.'
) )
self.assertEquals( self.assertEqual(
actual_text, actual_text,
expected_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): class TestMessageGetEmailObject(TestCase):
def test_get_body_properly_handles_unicode_body(self): def test_get_body_properly_handles_unicode_body(self):
with open( with open(
@ -178,7 +289,7 @@ class TestMessageGetEmailObject(TestCase):
expected_body = unicode_body expected_body = unicode_body
actual_body = message.get_email_object().as_string() actual_body = message.get_email_object().as_string()
self.assertEquals( self.assertEqual(
expected_body, expected_body,
actual_body actual_body
) )

View file

@ -25,4 +25,5 @@ ICESkFAJkRAJSIgEpEQCEqYfu6QUkn7sCcyDGQiSACKSKCAkGwBJwhDwZQNMEiYAIBdQvk7rfaHf
AO8NBJwCxTGhtFgTHVNaNaJeWFu44AXEHzKCktc7zZ0vss+bMoHSiM2b9mQoX1eZCgGqnWskY3gi AO8NBJwCxTGhtFgTHVNaNaJeWFu44AXEHzKCktc7zZ0vss+bMoHSiM2b9mQoX1eZCgGqnWskY3gi
XXAAxb8BqFiUgBNY7k49Tu/kV7UKPsefrjEOT9GmghYzrk9V03pjDGYKj3d0c06dKZkpTboRaD9o XXAAxb8BqFiUgBNY7k49Tu/kV7UKPsefrjEOT9GmghYzrk9V03pjDGYKj3d0c06dKZkpTboRaD9o
B+1m2m81d2Az948xzgdjLaFe95e83AAAAABJRU5ErkJggg== B+1m2m81d2Az948xzgdjLaFe95e83AAAAABJRU5ErkJggg==
--047d7b33dd729737fe04d3bde348-- --047d7b33dd729737fe04d3bde348--

View file

@ -0,0 +1,28 @@
X-sender: <sender@sendersdomain.com>
X-receiver: <somerecipient@recipientdomain.com>
From: "Senders Name" <sender@sendersdomain.com>
To: "Recipient Name" <somerecipient@recipientdomain.com>
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--

View file

@ -0,0 +1,28 @@
X-sender: <sender@sendersdomain.com>
X-receiver: <somerecipient@recipientdomain.com>
From: "Senders Name" <sender@sendersdomain.com>
To: "Recipient Name" <somerecipient@recipientdomain.com>
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--

View file

@ -0,0 +1,33 @@
X-sender: <sender@sendersdomain.com>
X-receiver: <somerecipient@recipientdomain.com>
From: "Senders Name" <sender@sendersdomain.com>
To: "Recipient Name" <somerecipient@recipientdomain.com>
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
<html>
<h1>Hello!</h1>
</html>
------=_OtherPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB011C--
------=_NextPart_DC7E1BB5_1105_4DB3_BAE3_2A6208EB099D--

View file

@ -0,0 +1,29 @@
X-sender: <sender@sendersdomain.com>
X-receiver: <somerecipient@recipientdomain.com>
From: "Senders Name" <sender@sendersdomain.com>
To: "Recipient Name" <somerecipient@recipientdomain.com>
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--

View file

@ -1,14 +1,17 @@
from setuptools import setup from setuptools import setup
tests_require=[ tests_require = [
'django', 'django',
] ]
setup( setup(
name='django-mailbox', name='django-mailbox',
version='2.0', version='2.1',
url='http://bitbucket.org/latestrevision/django-mailbox/', 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='Adam Coddington',
author_email='me@adamcoddington.net', author_email='me@adamcoddington.net',
tests_require=tests_require, tests_require=tests_require,